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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-30 15:14:09 +05:30
parent dd3331ee9d
commit 50d122e5f0
8 changed files with 25 additions and 17 deletions

View File

@@ -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)

View File

@@ -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
)

View File

@@ -81,6 +81,7 @@ usage: %s <command>
// 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 <installation code>\n" +
"Ask whoever manages your shops for one - they can create it from\n" +
"the Behavision platform, under the shop.")

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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
}
}