Skip to content

Commit

Permalink
cmd, lib, util: Add a new ansi logf library
Browse files Browse the repository at this point in the history
This does some ansi magic that enables us to remove previous log
messages that we didn't want to see. The magic is that it works when
multiple writers are logging all in parallel.
  • Loading branch information
amznpurple committed Oct 21, 2022
1 parent 0545ac8 commit a81407e
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 21 deletions.
63 changes: 43 additions & 20 deletions cmd/yesiscan/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/awslabs/yesiscan/interfaces"
"github.com/awslabs/yesiscan/lib"
"github.com/awslabs/yesiscan/s3"
"github.com/awslabs/yesiscan/util/ansi"
"github.com/awslabs/yesiscan/util/errwrap"
"github.com/awslabs/yesiscan/util/safepath"
"github.com/awslabs/yesiscan/web"
Expand Down Expand Up @@ -86,7 +87,7 @@ const (
)

// CLI is the entry point for the CLI frontend.
func CLI(program, version string, debug bool, logf func(format string, v ...interface{})) error {
func CLI(program, version string, debug bool) error {

flags := []cli.Flag{
&cli.StringFlag{
Expand All @@ -101,6 +102,10 @@ func CLI(program, version string, debug bool, logf func(format string, v ...inte
Name: "quiet",
Usage: "remove most log messages",
},
&cli.BoolFlag{
Name: "ansi-magic",
Usage: "do some ansi terminal escape sequence magic",
},
&cli.StringFlag{
Name: "regexp-path",
Usage: "path to regexp rules file",
Expand Down Expand Up @@ -153,10 +158,7 @@ func CLI(program, version string, debug bool, logf func(format string, v ...inte
Name: program,
Usage: "scan code for legal things",
Action: func(c *cli.Context) error {
logf("Hello from purpleidea! This is %s, version: %s", program, version)
defer logf("Done!")

return App(c, program, version, debug, logf)
return App(c, program, version, debug)
},
Flags: flags,
EnableBashCompletion: true,
Expand All @@ -167,10 +169,7 @@ func CLI(program, version string, debug bool, logf func(format string, v ...inte
Aliases: []string{"web"},
Usage: "launch a web server mode",
Action: func(c *cli.Context) error {
logf("Hello from purpleidea! This is %s, version: %s", program, version)
defer logf("Done!")

return Web(c, program, version, debug, logf)
return Web(c, program, version, debug)
},
Flags: []cli.Flag{
&cli.StringSliceFlag{
Expand All @@ -190,12 +189,13 @@ func CLI(program, version string, debug bool, logf func(format string, v ...inte
}

// App is the main entry point action for the regular yesiscan cli application.
func App(c *cli.Context, program, version string, debug bool, logf func(format string, v ...interface{})) error {
func App(c *cli.Context, program, version string, debug bool) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

bigIntStr := "" // for our int
var quiet bool
var ansiMagic bool
var regexpPath string
// config-path makes no sense here
var outputType string
Expand Down Expand Up @@ -223,6 +223,9 @@ func App(c *cli.Context, program, version string, debug bool, logf func(format s
if config.Quiet != nil {
quiet = *config.Quiet
}
if config.AnsiMagic != nil {
ansiMagic = *config.AnsiMagic
}
if config.RegexpPath != nil {
regexpPath = *config.RegexpPath
}
Expand Down Expand Up @@ -268,6 +271,9 @@ func App(c *cli.Context, program, version string, debug bool, logf func(format s
if c.IsSet("quiet") {
quiet = c.Bool("quiet")
}
if c.IsSet("ansi-magic") {
ansiMagic = c.Bool("ansi-magic")
}
if c.IsSet("regexp-path") {
regexpPath = c.String("regexp-path")
}
Expand Down Expand Up @@ -297,6 +303,21 @@ func App(c *cli.Context, program, version string, debug bool, logf func(format s
// }
//}

logf := (&ansi.Logf{
Prefix: "main: ",
Ellipsis: "...",
Enable: ansiMagic,
Prefixes: []string{
//"core: ",
"backend: installed: ",
"backend: running: ",
"iterator: ",
"core: scanner: scanning: ",
},
}).Init()
logf("Hello from purpleidea! This is %s, version: %s", program, version)
defer logf("Done!")

// auto config URI magic...
if autoConfigURI != "" { // we must try to auto config
logf("getting config from: %s", autoConfigURI)
Expand Down Expand Up @@ -341,7 +362,7 @@ func App(c *cli.Context, program, version string, debug bool, logf func(format s

// recurse!
logf("recursing on new config...")
return App(c, program, version, debug, logf)
return App(c, program, version, debug)

} else if err != nil {
// provide logs so users know something is wrong...
Expand Down Expand Up @@ -445,7 +466,7 @@ func App(c *cli.Context, program, version string, debug bool, logf func(format s
}
if recurse {
logf("recursing on new additional config...")
return App(c, program, version, debug, logf)
return App(c, program, version, debug)
}

if outputPath == "-" || quiet { // if output is stdout, noop logs
Expand Down Expand Up @@ -664,6 +685,10 @@ type Config struct {
// This is implied if you use the stdout option of --output-path.
Quiet *bool `json:"quiet"`

// AnsiMagic will do some ansi terminal escape sequence magic to keep
// the console output cleaner if this is set.
AnsiMagic *bool `json:"ansi-magic"`

// RegexpPath specifies a path the regular expressions to use.
RegexpPath *string `json:"regexp-path"`
// config-path makes no sense here
Expand Down Expand Up @@ -809,27 +834,25 @@ func DownloadConfig(uri string) ([]byte, error) {

func main() {
debug := false // TODO: hardcoded for now
logf := func(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, "main: "+format+"\n", v...)
}

program = strings.TrimSpace(program)
version = strings.TrimSpace(version)
if program == "" || version == "" {
// run `go generate` before you build it.
logf("program was not compiled correctly")
fmt.Printf("program was not compiled correctly\n")
os.Exit(1)
return
}

// FIXME: We discard output from lib's that use `log` package directly.
log.SetOutput(io.Discard)

err := CLI(program, version, debug, logf) // TODO: put these args in an input struct
if err != nil {
// TODO: put these args in an input struct
if err := CLI(program, version, debug); err != nil {
if debug {
logf("failed: %+v", err)
fmt.Printf("failed: %+v\n", err)
} else {
logf("failed: %+v", errwrap.Cause(err))
fmt.Printf("failed: %+v\n", errwrap.Cause(err))
}
os.Exit(1)
return
Expand Down
11 changes: 10 additions & 1 deletion cmd/yesiscan/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"os/signal"
"strings"

"github.com/awslabs/yesiscan/util/ansi"
"github.com/awslabs/yesiscan/web"

cli "github.com/urfave/cli/v2" // imports as package "cli"
Expand All @@ -39,7 +40,15 @@ import (
// server.
// TODO: replace the *cli.Context with a more general context that can be used
// by all the different frontends.
func Web(c *cli.Context, program, version string, debug bool, logf func(format string, v ...interface{})) error {
func Web(c *cli.Context, program, version string, debug bool) error {
logf := (&ansi.Logf{
Prefix: "main: ",
Ellipsis: "...",
Enable: false,
Prefixes: []string{},
}).Init()
logf("Hello from purpleidea! This is %s, version: %s", program, version)
defer logf("Done!")

server := &web.Server{
Program: program,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ require (
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/ssgelm/cookiejarparser v1.0.1 // indirect
github.com/urfave/cli/v2 v2.14.1 // indirect
golang.org/x/term v0.1.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
2 changes: 2 additions & 0 deletions lib/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ func (obj *Core) Run(ctx context.Context) (interfaces.ResultSet, error) {
return nil, errwrap.Wrapf(ea, "core run errored")
}

obj.Logf("scanning complete!") // clears the last "scanning: ..." message

return allResultSets, nil
}

Expand Down
103 changes: 103 additions & 0 deletions util/ansi/ansi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright Amazon.com Inc or its affiliates and the project contributors
// Written by James Shubin <[email protected]> and the project contributors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//
// We will never require a CLA to submit a patch. All contributions follow the
// `inbound == outbound` rule.
//
// This is not an official Amazon product. Amazon does not offer support for
// this project.
//
// SPDX-License-Identifier: Apache-2.0

package ansi

import (
"fmt"
"os"
"strings"
"sync"

"golang.org/x/term"
)

// Logf is a complex printing thing to do some ansi terminal escape sequence
// magic.
// FIXME: there might be bugs if Ellipsis is very big and Width is very small.
type Logf struct {
// Prefix is a prefix to append to each message. You can leave this
// empty.
Prefix string

// Ellipsis is what is appended to the end of each message when
// truncating. You can leave this empty.
Ellipsis string

// Enable specifies whether you want to turn this on or not.
Enable bool

// Prefixes are a list of string prefixes to match when deciding to
// delete a previous entry.
Prefixes []string

mutex *sync.Mutex
previous string
isTerminal bool
width int
}

// Init must be called once before Logf is used. As a convenience, this returns
// the Logf function that you should use!
func (obj *Logf) Init() func(format string, v ...interface{}) {
obj.mutex = &sync.Mutex{}
//obj.previous = ""
obj.isTerminal = term.IsTerminal(0)
var err error
obj.width, _, err = term.GetSize(0)
if err != nil {
obj.isTerminal = false // keep it simple, who cares
}

return obj.Logf
}

// Logf is the actual Logf function you should use. You must run Init before
// you use this.
func (obj *Logf) Logf(format string, v ...interface{}) {
s := fmt.Sprintf(format, v...)

if obj.isTerminal {
// TODO: what about multi-char width UTF-8 stuff?
if len(s) > obj.width-len(obj.Prefix) { // truncate/ellipsize
s = s[0:obj.width-len(obj.Prefix)-len(obj.Ellipsis)] + obj.Ellipsis
}
}
s = s + "\n" // add the newline in

obj.mutex.Lock() // for safety
validPrefix := false
for _, p := range obj.Prefixes {
b := strings.HasPrefix(obj.previous, p)
validPrefix = validPrefix || b
}

if obj.Enable && obj.previous != "" && validPrefix {
// move up 1 line, clear to left
fmt.Fprint(os.Stderr, "\033[1A\033[K") // not 1K as you'd think
}
fmt.Fprint(os.Stderr, obj.Prefix+s) // actually print

obj.previous = s // save for later
obj.mutex.Unlock()
}

0 comments on commit a81407e

Please sign in to comment.