From a083e57e2e27811c7168bf1f5dc6502d03b7393c Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 19 Oct 2026 05:59:51 +0000 Subject: [PATCH] netwatch-server refuses arguments it does not understand (closes #179) cli.Main starts the server only when given no arguments. check-cidr CIDR and prepare-data-dir DATA_DIR run as before. Any other command line, such as a subcommand with no value or two values, or a misspelt subcommand, gets a one-line usage message naming both forms on stderr and exit code 2. It used to start the server, which then held the port. Model: opus-5-5 --- README.md | 3 ++- TODO.md | 9 +++++++ internal/cli/cli.go | 36 ++++++++++++++++++-------- internal/cli/cli_test.go | 55 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f422108..ec857e8 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,8 @@ packages are in `internal/`: - **`cli`**: Runs `netwatch-server` from its command line: the `check-cidr` and `prepare-data-dir` subcommands, which `bin/entrypoint.sh` runs before it - starts the server, and otherwise the server, with the `fx` wiring. + starts the server, and, given no arguments, the server, with the `fx` wiring. + It refuses any other arguments with a usage message and exit code 2. - **`config`**: Loads configuration from environment variables and config files via Viper. - **`handlers`**: HTTP request handlers for the API (health check, report diff --git a/TODO.md b/TODO.md index 1431c2c..27b791b 100644 --- a/TODO.md +++ b/TODO.md @@ -22,6 +22,15 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of # Completed Steps +- 2026-10-19: `netwatch-server` starts the server only when given no arguments + ([#179](https://git.eeqj.de/sneak/netwatch/issues/179)). `check-cidr CIDR` and + `prepare-data-dir DATA_DIR` behave as before; any other arguments, such as a + subcommand with no value or two, or a misspelt one, print a one-line usage + message naming both forms to stderr and exit 2. They used to start the server. + Unit tests call `cli.Main` with no arguments and check that it starts the + server, and with each subcommand given no value and two values and with a + misspelt subcommand, and check the exit code, the message and that the server + does not start - 2026-10-19: `main()` in `cmd/netwatch-server/main.go` is one call into a new package, `internal/cli`, as the Go style guide asks ([#160](https://git.eeqj.de/sneak/netwatch/issues/160)). `internal/cli` runs diff --git a/internal/cli/cli.go b/internal/cli/cli.go index c53793d..0b618c4 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,7 +1,7 @@ // Package cli runs netwatch-server from its command line: the // check-cidr and prepare-data-dir subcommands, which bin/entrypoint.sh -// runs before it starts the server, and otherwise the server, with the -// fx wiring. +// runs before it starts the server, and, given no arguments, the +// server, with the fx wiring. package cli import ( @@ -22,16 +22,21 @@ import ( "go.uber.org/fx/fxevent" ) +// usageExitCode is what Main returns for a command line it does not +// understand, the usual exit code for a usage error. +const usageExitCode = 2 + // Main runs netwatch-server with args, its command line after the -// program's name, and returns the exit code; the subcommands write -// their errors to stderr. Unless args are "check-cidr CIDR" or -// "prepare-data-dir DATA_DIR", Main runs the server by calling serve -// with appname and version, the name and version the server logs and -// reports. main passes os.Stderr and RunServer; the tests pass a -// buffer and a function that fails them, so that no test starts the -// server. When serve is RunServer and the server fails to start, to -// listen or to stop, fx's Run exits the process with status 1 and Main -// never returns. +// program's name, and returns the exit code. With no args, Main runs +// the server by calling serve with appname and version, the name and +// version the server logs and reports. With "check-cidr CIDR" or +// "prepare-data-dir DATA_DIR" it runs that subcommand, which writes its +// errors to stderr. Any other args get a one-line usage message on +// stderr and exit code 2. main passes os.Stderr and RunServer; the +// tests pass a buffer and a function of their own, so that no test +// starts the server. When serve is RunServer and the server fails to +// start, to listen or to stop, fx's Run exits the process with status 1 +// and Main never returns. func Main( stderr io.Writer, appname, version string, @@ -53,6 +58,15 @@ func Main( return prepareDataDir(stderr, "/data", args[1], "netwatch") } + // Any other command line is refused, so that a mistyped one does + // not start the server. + if len(args) > 0 { + _, _ = fmt.Fprintln(stderr, + "usage: netwatch-server [check-cidr CIDR | prepare-data-dir DATA_DIR]") + + return usageExitCode + } + serve(appname, version) return 0 diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index de6c46a..b76428e 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -61,6 +61,61 @@ func TestMainRunsPrepareDataDir(t *testing.T) { } } +// TestMainRunsServerWithNoArguments: Main given no arguments starts the +// server with the name and version it was given, writes nothing and +// returns 0. +func TestMainRunsServerWithNoArguments(t *testing.T) { + t.Parallel() + + var ( + stderr bytes.Buffer + gotAppname, gotVersion string + ) + + code := cli.Main(&stderr, "netwatch", "test", []string{}, + func(appname, version string) { + gotAppname, gotVersion = appname, version + }) + if code != 0 || stderr.Len() != 0 { + t.Errorf("exit %d, stderr %q; want exit 0 and no output", + code, stderr.String()) + } + + if gotAppname != "netwatch" || gotVersion != "test" { + t.Errorf("server started with %q %q; want %q %q", + gotAppname, gotVersion, "netwatch", "test") + } +} + +// TestMainRefusesOtherCommandLines: Main given a subcommand with too +// few or too many values, or a misspelt subcommand, writes the usage +// message to stderr, returns 2 and does not start the server. +func TestMainRefusesOtherCommandLines(t *testing.T) { + t.Parallel() + + want := "usage: netwatch-server " + + "[check-cidr CIDR | prepare-data-dir DATA_DIR]\n" + + for _, commandLine := range []string{ + "netwatch-server check-cidr", + "netwatch-server check-cidr 192.0.2.0/24 198.51.100.0/24", + "netwatch-server prepare-data-dir", + "netwatch-server prepare-data-dir /data/reports /data/other", + "netwatch-server check-cdir 192.0.2.0/24", + } { + var stderr bytes.Buffer + + // Main takes the command line after the program's name. + args := strings.Fields(commandLine)[1:] + + code := cli.Main(&stderr, "netwatch", "test", args, failIfServerStarts(t)) + if code != 2 || stderr.String() != want { + t.Errorf("%q: exit %d, stderr %q; want exit 2 and %q", + commandLine, code, stderr.String(), want) + } + } +} + // TestCheckCIDRTakesCIDR: check-cidr exits 0, writing nothing, for a // CIDR the server takes in its TRUSTED_PROXIES. func TestCheckCIDRTakesCIDR(t *testing.T) {