Files
zero/internal/daemon/client_test.go
KRATOS fbbfdbf3f2 Harden background daemon: stdout integrity, lifecycle, and single-instance lock (#274)
* harden background daemon: stdout integrity, lifecycle, single-instance lock

Deep review of internal/daemon (worker pool, launcher, control server,
session fan-out, single-instance lock) found and fixes a stdout-integrity
bug plus several lifecycle/locking hazards. Each fix has a regression test;
the daemon suite passes under -race.

Worker stdout integrity
- Replace the capped bufio.Scanner (64 KiB) on a worker's stdout with an
  uncapped bufio.Reader: a legitimately large stream-json line from our own
  trusted worker is no longer silently dropped (the 1 MiB cap belongs to
  untrusted network frames, not this local pipe).
- Surface a stdout read error as a run failure instead of swallowing it as a
  clean success, and kill the worker first so it can't block writing to the
  now-unread pipe and hang Wait.

Control-server lifecycle
- Track open connections and close them on Shutdown so a single idle/hostile
  client can no longer wedge drain (and SIGTERM cleanup) forever.
- Guard the listener with a mutex and re-check the shutdown signal after bind
  so a shutdown requested in the bind window is not lost.
- Honor shutdown during buffered-history replay too.

Session fan-out
- A slow subscriber that drops live lines now receives a namespaced gap
  notice when its channel drains, instead of silently losing output.
- Hold the session lock across the (non-blocking) broadcast sends to close a
  latent send-on-closed-channel panic when a subscriber cancels mid-send.

Single-instance lock
- Reclaim a stale lock with an atomic rename-with-verify instead of a blind
  remove, so two instances starting at once cannot both reclaim and end up
  both holding the lock; a holder that reacquires in the gap is restored.

Client + pool
- Bound the control handshake with an I/O deadline so a peer that never
  replies can't wedge Dial/NewClientConn.
- Key the active-worker map by a monotonic id rather than PID (the OS reuses
  PIDs the instant a worker exits).
- Group-terminate on context cancel so a stuck worker's children are not
  orphaned.

* daemon: add -race regression test for the Session.Line lock-hold invariant

Session.Line holds s.mu across its non-blocking subscriber sends so cancel()/
finish() cannot close a subscriber channel mid-send (send-on-closed-channel
panic). That invariant had no concurrency test. Add one that drives Line
concurrently with Subscribe/cancel and finish; it panics with a data race if
the lock-hold is reverted and passes with it (verified both ways under -race).
2026-06-20 22:21:28 +05:30

37 lines
992 B
Go

package daemon
import (
"net"
"testing"
"time"
)
// TestClientHandshakeTimesOut verifies the handshake is bounded: a peer that
// accepts the connection but never completes the version exchange makes
// NewClientConn fail promptly instead of blocking forever (D9).
func TestClientHandshakeTimesOut(t *testing.T) {
orig := handshakeTimeout
handshakeTimeout = 100 * time.Millisecond
defer func() { handshakeTimeout = orig }()
// net.Pipe is synchronous: with no reader on the server end the client's hello
// write blocks, so only the handshake deadline can unblock it.
client, server := net.Pipe()
defer server.Close()
done := make(chan error, 1)
go func() {
_, err := NewClientConn(client)
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Fatal("NewClientConn must fail when the peer never completes the handshake")
}
case <-time.After(2 * time.Second):
t.Fatal("NewClientConn blocked past the handshake timeout — deadline not applied")
}
}