fix(garmin): wait for stderr-copy goroutine before cmd.Wait()

ensureStarted's stderr-copy goroutine could still be mid-Read when close()
called cmd.Wait(), which os/exec's StderrPipe docs call out as incorrect
and can truncate/garble trailing stderr diagnostics or surface a spurious
"file already closed" error. close() now waits on a stderrDone channel,
closed by the copy goroutine once it hits EOF (unblocked by killing the
process), before calling Wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 20:50:39 +02:00
parent 6c161f2206
commit df197f45fd
2 changed files with 61 additions and 2 deletions

View File

@@ -116,6 +116,7 @@ type subprocessClient struct {
scriptPath string // temp file holding the embedded wrapper.py, written once
started bool
nextID int
stderrDone chan struct{} // closed once the stderr-copy goroutine has finished reading
}
// ensureStarted spawns the wrapper subprocess if it isn't already running.
@@ -172,7 +173,15 @@ func (c *subprocessClient) ensureStarted() error {
if err := cmd.Start(); err != nil {
return fmt.Errorf("spawn garmin wrapper subprocess: %w", err)
}
go io.Copy(os.Stderr, stderr) // wrapper.py logs auth/rate-limit diagnostics to stderr
// wrapper.py logs auth/rate-limit diagnostics to stderr. Per os/exec's
// StderrPipe docs, it's incorrect to call Wait before all reads from the
// pipe have completed, so close() waits on stderrDone before Wait-ing.
c.stderrDone = make(chan struct{})
stderrDone := c.stderrDone
go func() {
io.Copy(os.Stderr, stderr)
close(stderrDone)
}()
c.cmd = cmd
c.stdin = stdin
@@ -246,11 +255,17 @@ func (c *subprocessClient) close() error {
if c.cmd != nil && c.cmd.Process != nil {
c.cmd.Process.Kill()
}
if c.stderrDone != nil {
// Killing the process closes its end of the stderr pipe, which
// unblocks the copy goroutine's Read with EOF; wait for it to finish
// before Wait, per os/exec's StderrPipe doc.
<-c.stderrDone
}
var err error
if c.cmd != nil {
err = c.cmd.Wait()
}
c.cmd, c.stdin, c.enc, c.scanner = nil, nil, nil, nil
c.cmd, c.stdin, c.enc, c.scanner, c.stderrDone = nil, nil, nil, nil, nil
return err
}