From 50d122e5f00689bccd46994b38bcc0dd3d5c130b Mon Sep 17 00:00:00 2001 From: Suriyakumarvijayanayagam Date: Wed, 30 Sep 2026 15:14:09 +0530 Subject: [PATCH] An unused constant was a Stop() that could hang forever Ran staticcheck across all three Go modules for the first time. server (23k lines) and desktop came back clean. agent had seven findings, and one of them was not tidiness. `stopGrace = 10 * time.Second` was declared and wired to nothing. Stop() cancels the context, cmd.Cancel kills the process tree, and then Stop() blocks on cmd.Wait() - which, with no WaitDelay set, waits not just for the process but for every writer of its stdout pipe to close. One grandchild still holding that pipe hangs Wait, hangs Stop, and on the desktop app that is the tray's Quit never returning. The constant named the intent and nothing read it. cmd.WaitDelay = stopGrace is the line that was missing. The rest were real but small: an unused field in the live relay, an unused sleep helper in the pump, and "net/url" imported twice under two names - both genuinely used, in two functions doing the same job for the same reason, so they are unified rather than one deleted. My first pass deleted the wrong one on a bad grep and the build caught it immediately. Three findings are suppressed rather than fixed, with the reason stated: - Two "error strings should not end with punctuation". Both are multi-line messages a shop operator reads at a counter, not errors anything wraps. ST1005 exists because wrapped errors concatenate mid-sentence; stripping the full stops would run three sentences together to satisfy a rule that does not apply. - A deliberately nil context in a pump test - the point of the test is that an unconnected client does not panic. It already carried //nolint:staticcheck, which is golangci-lint's directive and staticcheck ignores, which is why it kept being reported. Also tidied agent/go.mod, which had paho and x/sys marked indirect while being imported directly. All three modules clean, all suites pass: 21 Go packages, 226 engine tests. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj --- agent/cmd/behavision-setup/main.go | 6 ++++++ agent/go.mod | 7 +++++-- agent/main.go | 1 + agent/pkg/cameras/live.go | 1 - agent/pkg/engine/supervisor.go | 7 +++++++ agent/pkg/mqtt/client.go | 3 +-- agent/pkg/mqtt/client_test.go | 6 +++++- agent/pkg/mqtt/pump.go | 11 ----------- 8 files changed, 25 insertions(+), 17 deletions(-) diff --git a/agent/cmd/behavision-setup/main.go b/agent/cmd/behavision-setup/main.go index 5cd4996..50fd96b 100644 --- a/agent/cmd/behavision-setup/main.go +++ b/agent/cmd/behavision-setup/main.go @@ -93,6 +93,12 @@ func run() error { } if running := behavisionRunning(); running != "" { + // lint:ignore ST1005 — this is not a wrapped error, it is the whole + // message an operator reads at a shop counter. ST1005 forbids + // trailing punctuation because errors get concatenated mid-sentence; + // nothing wraps this one, and stripping the full stops would make + // three sentences run together. + //lint:ignore ST1005 operator-facing prose, never wrapped return fmt.Errorf("%s is running. Quit Behavision from the tray icon first, then run setup again.\n\n"+ "Setting up underneath a running copy starts a second engine on the same port and, in a demo,\n"+ "re-claims the shop while the open app still holds the old credentials.", running) diff --git a/agent/go.mod b/agent/go.mod index 2fa92c6..3795dc2 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -3,9 +3,12 @@ module github.com/loyaly/behavision-agent go 1.22 require ( - github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect + github.com/eclipse/paho.mqtt.golang v1.4.3 + golang.org/x/sys v0.20.0 +) + +require ( github.com/gorilla/websocket v1.5.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.20.0 // indirect ) diff --git a/agent/main.go b/agent/main.go index 57e5af7..62c5898 100644 --- a/agent/main.go +++ b/agent/main.go @@ -81,6 +81,7 @@ usage: %s // agent.json - which is the state that screen was built to end. func cmdClaim(args []string) error { if len(args) == 0 { + //lint:ignore ST1005 usage text read by a person, never wrapped return fmt.Errorf("usage: behavision-agent claim \n" + "Ask whoever manages your shops for one - they can create it from\n" + "the Behavision platform, under the shop.") diff --git a/agent/pkg/cameras/live.go b/agent/pkg/cameras/live.go index 64082ff..dec4e59 100644 --- a/agent/pkg/cameras/live.go +++ b/agent/pkg/cameras/live.go @@ -36,7 +36,6 @@ type Live struct { FPS float64 Width int Quality int - pollDelay time.Duration } // Defaults, measured against the office camera rather than guessed. diff --git a/agent/pkg/engine/supervisor.go b/agent/pkg/engine/supervisor.go index d25d63b..aa49d90 100644 --- a/agent/pkg/engine/supervisor.go +++ b/agent/pkg/engine/supervisor.go @@ -244,6 +244,13 @@ func (s *Supervisor) runOnce(ctx context.Context) error { } return kill() } + // Cancel sends the kill; WaitDelay bounds how long Wait() then waits for + // the output pipes to close. Without it Wait() blocks until every writer + // is gone - and Stop() blocks on Wait() - so one grandchild still holding + // the engine's stdout hangs Stop FOREVER, which on the desktop app means + // the tray's Quit never returns. stopGrace was declared for exactly this + // and never wired to anything; staticcheck found it as an unused const. + cmd.WaitDelay = stopGrace prepare(cmd) if err := cmd.Start(); err != nil { return fmt.Errorf("engine failed to start: %w", err) diff --git a/agent/pkg/mqtt/client.go b/agent/pkg/mqtt/client.go index 45cf7be..0866e05 100644 --- a/agent/pkg/mqtt/client.go +++ b/agent/pkg/mqtt/client.go @@ -18,7 +18,6 @@ import ( "log" "net" "net/url" - neturl "net/url" "os" "strings" "time" @@ -225,7 +224,7 @@ func checkTransport(raw string) error { // url.Parse, not hand-rolled splitting: an IPv6 literal is bracketed and // full of colons, so scanning for the first ":" turns "[::1]:1883" into // "[" and refuses a perfectly good loopback address. - u, err := neturl.Parse(raw) + u, err := url.Parse(raw) if err != nil { return fmt.Errorf("mqtt: cannot parse broker url %q: %w", raw, err) } diff --git a/agent/pkg/mqtt/client_test.go b/agent/pkg/mqtt/client_test.go index fc1f616..b8b01b2 100644 --- a/agent/pkg/mqtt/client_test.go +++ b/agent/pkg/mqtt/client_test.go @@ -92,7 +92,11 @@ func TestPublishOnADeadClientErrorsRatherThanPanics(t *testing.T) { // The pump calls this on every tick; a nil-client panic would take the // whole agent down instead of backing off. c := &Client{} - if err := c.Publish(nil, "t", []byte("{}")); err == nil { //nolint:staticcheck + // The nil context is the POINT: the pump must not panic on a client that + // never connected. //nolint is golangci-lint's directive and staticcheck + // ignores it, which is why this kept being reported. + //lint:ignore SA1012 passing nil is what is under test + if err := c.Publish(nil, "t", []byte("{}")); err == nil { t.Fatal("publish on an unconnected client reported success") } if c.Connected() { diff --git a/agent/pkg/mqtt/pump.go b/agent/pkg/mqtt/pump.go index 75884c2..940995a 100644 --- a/agent/pkg/mqtt/pump.go +++ b/agent/pkg/mqtt/pump.go @@ -205,14 +205,3 @@ func (p *Pump) logf(format string, args ...any) { p.Log.Printf(format, args...) } } - -func sleep(ctx context.Context, d time.Duration) bool { - t := time.NewTimer(d) - defer t.Stop() - select { - case <-ctx.Done(): - return false - case <-t.C: - return true - } -}