fix: Windows daemon survives session jobs; quieter wrap note; correct mute error

Verified on a Windows 11 VM over WinRM: doctor, SAPI synthesis, Edge,
oto/WASAPI playback, autostart, mute file, wrap exit codes, doctor --probe
and toast mode all work. Three findings fixed:

- The autostarted daemon died with the WinRM shell: Windows session hosts
  (WinRM, SSH, CI runners) put children into a job object that is
  terminated with the session. Spawn now starts the daemon with
  CREATE_BREAKAWAY_FROM_JOB first and falls back to a start without it when
  the job forbids breakaway. Confirmed: the daemon now outlives the WinRM
  shell and `talk daemon stop` still ends it.
- `talk mute` without a daemon printed "daemon call failed (<nil>)": the
  outer err was reported instead of the call's. Now "no daemon (<reason>);
  writing the mute file directly".
- `talk wrap` printed "command did not exit cleanly" for every non-zero
  exit code. The note is now reserved for commands that produced no exit
  code of their own (not found, signal); a plain failure is carried by the
  announcement and the exit code alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 11:43:42 +02:00
1 parent 85fa64cfe6
commit 1b971bc3f6
7 files changed
+52 -22

No files matched your search

+18 -8
View File
@@ -17,7 +17,6 @@ import (
// port in use) without waiting for a timeout. If the caller exits first,
// the daemon keeps running (that is the point).
func Spawn(exe, logPath string, env []string) (<-chan error, error) {
var logFile *os.File
if logPath == "" {
logPath = os.DevNull
}
@@ -27,13 +26,24 @@ func Spawn(exe, logPath string, env []string) (<-chan error, error) {
}
defer logFile.Close() // the child holds its own handle
cmd := exec.Command(exe, "daemon", "run")
cmd.Env = env
cmd.Stdin = nil
cmd.Stdout = logFile
cmd.Stderr = logFile
detach(cmd)
if err := cmd.Start(); err != nil {
// First attempt: fully detached, including a breakaway from the parent's
// job object (Windows). Shells such as WinRM, SSH servers or CI runners
// put children into a job that is terminated with the session; the
// breakaway lets the daemon outlive it. If the job forbids breakaway,
// CreateProcess fails, and the second attempt starts without it.
var cmd *exec.Cmd
for _, breakaway := range []bool{true, false} {
cmd = exec.Command(exe, "daemon", "run")
cmd.Env = env
cmd.Stdin = nil
cmd.Stdout = logFile
cmd.Stderr = logFile
detach(cmd, breakaway)
if err = cmd.Start(); err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf("starting daemon (%s daemon run): %w", exe, err)
}
exited := make(chan error, 1)
+1 -1
View File
@@ -5,4 +5,4 @@ package daemon
import "os/exec"
// detach is a no-op on this platform (no Setsid, no console).
func detach(*exec.Cmd) {}
func detach(*exec.Cmd, bool) {}
+3 -2
View File
@@ -7,8 +7,9 @@ import (
"syscall"
)
// detach detaches the child from the terminal: own session (no SIGHUP when
// detach detaches the child from the terminal (the breakaway flag is a
// Windows concept and ignored here): own session (no SIGHUP when
// the terminal closes, no Ctrl-C from the shell).
func detach(cmd *exec.Cmd) {
func detach(cmd *exec.Cmd, _ bool) {
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
}
+11 -3
View File
@@ -11,10 +11,18 @@ import (
// binding to the caller's console.
const detachedProcess = 0x00000008
// createBreakawayFromJob is the Win32 flag CREATE_BREAKAWAY_FROM_JOB: the
// child leaves the parent's job object, so a session-bound job (WinRM, SSH,
// CI) does not take the daemon down with it. Only works when the job allows
// it; Spawn falls back to a start without the flag otherwise.
const createBreakawayFromJob = 0x01000000
// detach detaches the child from the console: own process group (no Ctrl-C
// from the shell) and no console (no flashing window).
func detach(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP | detachedProcess,
func detach(cmd *exec.Cmd, breakaway bool) {
flags := uint32(syscall.CREATE_NEW_PROCESS_GROUP | detachedProcess)
if breakaway {
flags |= createBreakawayFromJob
}
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: flags}
}