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>
42 lines
1.3 KiB
Go
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
|
|
}
|