diff --git a/README.md b/README.md index 0ea9215..da73a59 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -

up

+# up +
Logo
@@ -39,7 +40,7 @@ Check [the examples](examples) to see how to use this project in your own code. ## License -This project is under the MIT License. See the [LICENSE](LICENSE) file for the full license text. +This project is under the MIT License. See the [LICENSE](LICENSE) file for the full text. [doc-img]: https://pkg.go.dev/badge/github.com/jesusprubio/up [doc]: https://pkg.go.dev/github.com/jesusprubio/up diff --git a/Taskfile.yml b/Taskfile.yml index 1071833..6bde099 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -10,7 +10,7 @@ tasks: build: summary: "Build application" - cmd: go build -o dist/up cmd/main.go + cmd: go build -o dist/up . clean: summary: "Clean the project" diff --git a/examples/doc.go b/examples/doc.go deleted file mode 100644 index a12e7bd..0000000 --- a/examples/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package examples provides examples of how to use this project as a library. -package examples diff --git a/examples/probe.go b/examples/probe.go deleted file mode 100644 index a4e427f..0000000 --- a/examples/probe.go +++ /dev/null @@ -1,3 +0,0 @@ -package examples - -// TODO(#28) diff --git a/main.go b/main.go index 84a39b1..6264254 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// Package main implements a simple CLI to use the library. package main import ( @@ -15,6 +16,8 @@ import ( "github.com/jesusprubio/up/pkg" ) +// TODO(#39): STDIN piped input. + const ( appName = "up" appDesc = ` @@ -29,72 +32,41 @@ const ( This utility exits with one of the following values: 0 At least one response was heard. 2 The transmission was successful but no responses were received. - 1 Any other value An error occurred. + 1 Any other error occurred. ` ) -// CLI initialization. func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // Input options. - flagProto := flag.String( - "p", - "", - fmt.Sprintf("Use only one protocol: %v", pkg.Protocols), - ) - flagCount := flag.Uint("c", 0, "Number of iterations. (0 = infinite)") - flagTimeout := flag.Duration("t", - 5*time.Second, - "Time to wait for a response", - ) - flagDelay := flag.Duration( - "d", - 500*time.Millisecond, - "Delay between requests", - ) - flagStop := flag.Bool("s", false, "Stop after the first successful request") - // Output options. - flagJSONOutput := flag.Bool("j", false, "Output in JSON format") - flagNoColor := flag.Bool("nc", false, "Disable color output") - flagVerbose := flag.Bool("v", false, "Verbose output") - flagHelp := flag.Bool("h", false, "Show app documentation") - flag.Parse() + // Only used for debugging. lvl := new(slog.LevelVar) lvl.Set(slog.LevelError) - // Only used for debugging. logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: lvl, })) - if *flagVerbose { + var opts options + opts.parse() + if opts.verbose { + // TODO(#37): Debug and verbose should not be the same thing. lvl.Set(slog.LevelDebug) } - logger.Debug("Running", - "protocol", *flagProto, - "count", *flagCount, - "timeout", *flagTimeout, - "delay", *flagDelay, - "stop", *flagStop, - "json", *flagJSONOutput, - "no-color", *flagNoColor, - "verbose", *flagVerbose, - "help", *flagHelp, - ) + logger.Debug("Starting", "options", opts) protocols := pkg.Protocols - if *flagProto != "" { - protocol := pkg.ProtocolByID(*flagProto) + if opts.protocol != "" { + protocol := pkg.ProtocolByID(opts.protocol) if protocol == nil { - fatal(fmt.Errorf("unknown protocol: %s", *flagProto)) + fatal(fmt.Errorf("unknown protocol: %s", opts.protocol)) } protocols = []*pkg.Protocol{protocol} } - logger.Info("Running", "protocols", protocols, "count", *flagCount) - if *flagHelp { + logger.Info("Running", "protocols", protocols, "count", opts.count) + if opts.help { fmt.Fprintf(os.Stderr, "%s\n", appDesc) flag.Usage() os.Exit(1) } - if *flagNoColor { + if opts.noColor { color.NoColor = true } // To wait for termination signals. @@ -111,9 +83,9 @@ func main() { }() probe := pkg.Probe{ Protocols: protocols, - Count: *flagCount, - Delay: *flagDelay, - Timeout: *flagTimeout, + Count: opts.count, + Timeout: opts.timeout, + Delay: opts.delay, Logger: logger, ReportCh: make(chan *pkg.Report), } @@ -122,19 +94,19 @@ func main() { for report := range probe.ReportCh { logger.Debug("New report", "report", *report) var line string - if *flagJSONOutput { + if opts.jsonOutput { reportJSON, err := json.Marshal(report) if err != nil { fatal(fmt.Errorf("marshaling report: %w", err)) } line = string(reportJSON) } else { - line = report.String() + line = reportToLine(report) } fmt.Println(line) if report.Error == nil { - if *flagStop { - logger.Debug("Stop requested") + if opts.stop { + logger.Debug("Stopping after first successful request") cancel() } } @@ -143,12 +115,87 @@ func main() { logger.Debug("Running", "setup", probe) err := probe.Run(ctx) if err != nil { - fatal(err) + fatal(fmt.Errorf("running probe: %w", err)) } logger.Debug("Bye!") } +// Flags passed by the user. +type options struct { + // Input flags. + // Protocol to use. + protocol string + // Number of iterations. Zero means infinite. + count uint + // Time to wait for a response. + timeout time.Duration + // Delay between requests. + delay time.Duration + // Stop after the first successful request. + stop bool + // Output flags. + // Output in JSON format. + jsonOutput bool + // Disable color output. + noColor bool + // Verbose output. + verbose bool + // Show app documentation. + help bool +} + +// Parses the command line flags provided by the user. +func (opts *options) parse() { + flag.StringVar(&opts.protocol, "p", "", "Test only one protocol") + flag.UintVar(&opts.count, "c", 0, "Number of iterations") + flag.DurationVar( + &opts.timeout, + "t", + 5*time.Second, + "Time to wait for a response", + ) + flag.DurationVar( + &opts.delay, + "d", + 500*time.Millisecond, + "Delay between requests", + ) + flag.BoolVar( + &opts.stop, + "s", + false, + "Stop after the first successful request", + ) + flag.BoolVar(&opts.jsonOutput, "j", false, "Output in JSON format") + flag.BoolVar(&opts.noColor, "nc", false, "Disable color output") + flag.BoolVar(&opts.verbose, "v", false, "Verbose output") + flag.BoolVar(&opts.help, "h", false, "Show app documentation") + flag.Parse() +} + +// Logs the error to the standard output and exits with status 1. func fatal(err error) { fmt.Fprintf(os.Stderr, "%s: %s\n", appName, err) os.Exit(1) } + +// String returns a human-readable representation of the report. +func reportToLine(r *pkg.Report) string { + // TODO(#40): Use Go string padding. + line := fmt.Sprintf("%s\t%s\t%s", bold(r.ProtocolID), r.Time, r.RHost) + suffix := r.Extra + prefix := green("✔") + if r.Error != nil { + prefix = red("✘") + suffix = r.Error.Error() + } + suffix = fmt.Sprintf("(%s)", suffix) + return fmt.Sprintf("%s %s %s", prefix, line, faint(suffix)) +} + +var ( + green = color.New(color.FgGreen).SprintFunc() + red = color.New(color.FgRed).SprintFunc() + bold = color.New(color.Bold).SprintFunc() + faint = color.New(color.Faint).SprintFunc() +) diff --git a/pkg/probe.go b/pkg/probe.go index 3b0eac2..1867fb0 100644 --- a/pkg/probe.go +++ b/pkg/probe.go @@ -16,13 +16,13 @@ type Probe struct { Protocols []*Protocol // Number of iterations. Zero means infinite. Count uint - // Delay between requests. - Delay time.Duration // Time to wait for a response. Timeout time.Duration + // Delay between requests. + Delay time.Duration // For debugging purposes. Logger *slog.Logger - // Optional channel to send back partial results. + // Channel to send back partial results. ReportCh chan *Report } @@ -34,6 +34,7 @@ func (p Probe) validate() error { if p.Timeout == 0 { return fmt.Errorf(tmplRequiredProp, "Timeout") } + // 'Delay' could be zero. if p.Logger == nil { return fmt.Errorf(tmplRequiredProp, "Logger") } @@ -43,17 +44,17 @@ func (p Probe) validate() error { return nil } -// Run the connection attempts to the public servers. +// Run the connection requests against the public servers. // // The context can be cancelled between different protocol attempts or count // iterations. // Returns an error if the setup is invalid. func (p Probe) Run(ctx context.Context) error { - p.Logger.Debug("Starting", "setup", p) err := p.validate() if err != nil { return fmt.Errorf("invalid setup: %w", err) } + p.Logger.Debug("Starting", "setup", p) count := uint(0) for { select { @@ -99,11 +100,11 @@ func (p Probe) Run(ctx context.Context) error { time.Sleep(p.Delay) } } - count++ p.Logger.Debug( "End of iteration", "count", count, "p.Count", p.Count, ) + count++ if count == p.Count { p.Logger.Debug("Count limit reached", "count", count) return nil diff --git a/pkg/protocol.go b/pkg/protocols.go similarity index 100% rename from pkg/protocol.go rename to pkg/protocols.go index a194d40..94951b7 100644 --- a/pkg/protocol.go +++ b/pkg/protocols.go @@ -9,9 +9,9 @@ import ( // Protocols included in the library. var Protocols = []*Protocol{ - {ID: "http", Request: requestHTTP, RHost: RandomCaptivePortal}, {ID: "tcp", Request: requestTCP, RHost: RandomTCPServer}, {ID: "dns", Request: requestDNS, RHost: RandomDomain}, + {ID: "http", Request: requestHTTP, RHost: RandomCaptivePortal}, } // ProtocolByID returns a protocol from the list. diff --git a/pkg/report.go b/pkg/report.go index ba45f51..44747e6 100644 --- a/pkg/report.go +++ b/pkg/report.go @@ -1,23 +1,12 @@ package pkg import ( - "fmt" "time" - - "github.com/fatih/color" -) - -var ( - green = color.New(color.FgGreen).SprintFunc() - red = color.New(color.FgRed).SprintFunc() - bold = color.New(color.Bold).SprintFunc() - faint = color.New(color.Faint).SprintFunc() ) // Report is the result of a connection attempt. // -// Depending on the result, only one of the properties 'Response' or 'Error' -// is set. +// Only one of the properties 'Response' or 'Error' is set. type Report struct { // Protocol used to connect to. ProtocolID string `json:"protocol"` @@ -33,16 +22,3 @@ type Report struct { // Network error. Error error `json:"error,omitempty"` } - -// String returns a human-readable representation of the report. -func (r *Report) String() string { - line := fmt.Sprintf("%s\t%s\t%s", bold(r.ProtocolID), r.Time, r.RHost) - suffix := r.Extra - prefix := green("✔") - if r.Error != nil { - prefix = red("✘") - suffix = r.Error.Error() - } - suffix = fmt.Sprintf("(%s)", suffix) - return fmt.Sprintf("%s %s %s", prefix, line, faint(suffix)) -}