Skip to content

run (Linux): daemon spawn failure surfaces as bare EOF, real cause only in daemon.log #801

Description

@magurotuna

clawpatrol run on a host whose daemon can't boot prints:

clawpatrol: daemon connect: spawn daemon: daemon ready: EOF (read "")
  (if this machine was joined with --whole-machine, run the command directly — `clawpatrol run` isn't needed; traffic already routes through the gateway)

Neither line is actionable, and the second one is wrong. The real cause was sitting in ~/.local/state/clawpatrol/run/daemon.log the whole time:

daemon pid=3862242 starting
daemon: transport: no mode file at ~/.clawpatrol/mode and no wg.conf — re-run `clawpatrol join`

That message is exactly what the operator needs, and it never reaches them. Diagnosing this took reading daemon_linux.go to learn the log existed.

Two separate defects:

1. The ready-pipe EOF is content-free. daemonSpawn (cmd/clawpatrol/daemon_linux.go:290) waits for ready\n on the inherited fd 3. The daemon boots its transport before binding the control socket — deliberate, per runDaemon's comment, so a parent that reads ready\n can go straight to a session START — and log.Fatalfs when daemonStartTransport fails. So every transport-boot failure reaches the client as an unadorned EOF, regardless of cause.

daemonSpawn already computes daemonLogPath() a few lines earlier. Sketch: stat the log before cmd.Start() to record its size, and on a ready-read failure read from that offset and fold the last non-empty line into the returned error. Reading from the recorded offset rather than tailing matters because the log is opened O_APPEND and shared across respawns — the last line may belong to an older daemon.

2. The --whole-machine hint is unconditional. run_linux.go:147 appends it to every daemon connect failure. On the host above it is actively misleading: there is no whole-machine marker, and isWholeMachineJoin() — already checked earlier in the same function at run_linux.go:119 — had returned false. Either gate the hint on that same predicate, or replace it with a pointer to the daemon log, which covers more failure modes than the one it guesses at.

Worth noting what triggered it, since it may deserve its own fix: the host was joined by a build predating the mode marker. daemonStartTransport's empty-mode fallback (daemon_linux.go:429) rescues legacy joins by stat'ing wg.conf and assuming WireGuard, but a legacy tailscale join has neither file, so it falls through to the fatal error. The remedy is a re-join either way; the point of this issue is that the operator should be able to learn that from the command's own output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions