Files
talk/internal/daemon/spawn.go
T
m3ta-chironandClaude Fable 5.1 bc26a73aeb refactor: ein Binary — talk startet den Daemon selbst, talkd und Degraded Mode entfallen
Zwei Binaries erzeugten den halben Komplexitätsballast: Degraded Mode mit
Lock-Datei und Stale-Lock-Race, „läuft talkd?", Autostart-Konfiguration je
OS, doppelte Chain-Konstruktion in cmd/talk und cmd/talkd — und der
Degraded Mode kannte weder Rate-Limit noch Ruhezeiten.

Jetzt:
- internal/speak: der eine Sprech-Kern (Config + Sink + Kette → Ansage);
  Daemon und Direkt-Pfad benutzen ihn, es gibt keine zweite
  Zusammensetzung der Kette mehr.
- internal/daemon: Run (der bisherige talkd-Kern), Spawn (detached per
  Setsid bzw. DETACHED_PROCESS, stdout/stderr → daemon.log), PID-Datei
  daemon.pid, Shutdown-Trigger.
- `talk daemon run|start|stop`: Vordergrund (systemd/launchd), expliziter
  Hintergrund-Start, Stop per POST /shutdown — stop wartet, bis HTTP weg
  und die PID-Datei gelöscht ist (Prozess wirklich beendet).
- Autostart: say/wrap starten den Daemon beim ersten Sprechbefehl selbst
  (daemon.autostart, Default an; Env TALK_AUTOSTART). Verliert ein
  paralleler Aufruf das Port-Rennen, erkennt er das am Kind-Ende und nutzt
  den Daemon des anderen — kein Timeout-Warten.
- Direkt-Pfad statt Degraded Mode: ohne erreichbaren oder startbaren
  Daemon spricht talk direkt (Mute respektiert, kein Lock, Hinweis +
  Grund + Log-Pfad auf stderr). Steuerbefehle starten den Daemon bewusst
  nicht; `talk status` zeigt „läuft nicht" (Exit 1).
- cmd/talkd und cmd/talk/degraded.go entfernt; README-Daemon-Abschnitt
  neu.

Tests: Harness baut nur noch talk und startet `talk daemon run`;
TALK_AUTOSTART=false als Grundausstattung aller Tests (kein Test
hinterlässt Daemons), autostart_test.go schaltet es bewusst an: Autostart
beim ersten say, Idempotenz, paralleler Start in EINE Queue, expliziter
start/stop, Port fremd belegt → Direkt-Pfad ohne Timeout. Cross-Compile
aller sechs Targets mit CGO_ENABLED=0 verifiziert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 09:49:16 +02:00

42 lines
1.3 KiB
Go

package daemon
import (
"fmt"
"os"
"os/exec"
)
// Spawn startet `exe daemon run` als eigenständigen Hintergrundprozess
// (eigene Session bzw. eigene Prozessgruppe, losgelöst vom Terminal), mit
// stdout/stderr in die Log-Datei logPath (pro Start neu angelegt; leer =
// verwerfen). env ist die Umgebung des Kindes (Keys, TALK_*-Overrides).
//
// Rückgabe ist ein Kanal, der genau einmal das Wait-Ergebnis liefert, wenn
// das Kind endet — der Aufrufer erkennt daran einen fehlgeschlagenen Start
// (z. B. Port belegt), ohne auf einen Timeout warten zu müssen. Endet der
// Aufrufer zuerst, läuft der Daemon weiter (das ist der Zweck).
func Spawn(exe, logPath string, env []string) (<-chan error, error) {
var logFile *os.File
if logPath == "" {
logPath = os.DevNull
}
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return nil, fmt.Errorf("Daemon-Log %s: %w", logPath, err)
}
defer logFile.Close() // das Kind hält sein eigenes 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 {
return nil, fmt.Errorf("Daemon starten (%s daemon run): %w", exe, err)
}
exited := make(chan error, 1)
go func() { exited <- cmd.Wait() }()
return exited, nil
}