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>
This commit is contained in:
1 parent
b86632006e
commit
bc26a73aeb
29 files changed
+1145
-584
No files matched your search
+16
-23
@@ -29,10 +29,7 @@ import (
|
||||
|
||||
// Die Binaries werden einmal pro Testlauf gebaut (Build-Cache macht es
|
||||
// billig) und von allen Tests geteilt.
|
||||
var (
|
||||
talkBin string
|
||||
talkdBin string
|
||||
)
|
||||
var talkBin string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dir, err := os.MkdirTemp("", "talk-bins-*")
|
||||
@@ -45,19 +42,14 @@ func TestMain(m *testing.M) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, b := range []struct{ out, pkg string }{
|
||||
{"talk", "./cmd/talk"},
|
||||
{"talkd", "./cmd/talkd"},
|
||||
} {
|
||||
cmd := exec.Command("go", "build", "-o", filepath.Join(dir, b.out), b.pkg)
|
||||
cmd.Dir = repo
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "go build %s: %v\n%s", b.pkg, err, out)
|
||||
os.RemoveAll(dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
build := exec.Command("go", "build", "-o", filepath.Join(dir, "talk"), "./cmd/talk")
|
||||
build.Dir = repo
|
||||
if out, err := build.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "go build ./cmd/talk: %v\n%s", err, out)
|
||||
os.RemoveAll(dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
talkBin, talkdBin = filepath.Join(dir, "talk"), filepath.Join(dir, "talkd")
|
||||
talkBin = filepath.Join(dir, "talk")
|
||||
|
||||
code := m.Run()
|
||||
os.RemoveAll(dir)
|
||||
@@ -254,19 +246,19 @@ type daemonProc struct {
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// startDaemon startet talkd als echten Prozess (freier Port per TALK_PORT,
|
||||
// nie der Default-Port) und wartet, bis GET /status antwortet. Stop per
|
||||
// startDaemon startet `talk daemon run` als echten Prozess (freier Port per
|
||||
// TALK_PORT, nie der Default-Port) und wartet, bis GET /status antwortet. Stop per
|
||||
// t.Cleanup: SIGINT, nach Frist Kill.
|
||||
func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *daemonProc {
|
||||
t.Helper()
|
||||
port := freePort(t)
|
||||
env := daemonEnv(root, port, fakeURL, extra)
|
||||
stderr := &syncBuffer{}
|
||||
cmd := exec.Command(talkdBin)
|
||||
cmd := exec.Command(talkBin, "daemon", "run")
|
||||
cmd.Env = env
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("talkd starten: %v", err)
|
||||
t.Fatalf("talk daemon run starten: %v", err)
|
||||
}
|
||||
d := &daemonProc{
|
||||
cmd: cmd,
|
||||
@@ -288,7 +280,7 @@ func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *d
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("talkd antwortet nicht auf %s — stderr:\n%s", d.baseURL+"/status", stderr.String())
|
||||
t.Fatalf("talk daemon antwortet nicht auf %s — stderr:\n%s", d.baseURL+"/status", stderr.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -310,7 +302,7 @@ func (d *daemonProc) stop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// daemonEnv baut die Prozess-Umgebung für talk- und talkd-Prozesse:
|
||||
// daemonEnv baut die Prozess-Umgebung für talk-Prozesse (CLI und Daemon):
|
||||
// geerbtes Environment ohne fremde TALK_*-Variablen (Isolation von Keys/
|
||||
// Ports der Außenwelt), isoliertes HOME/XDG_CONFIG_HOME (Config-Dir liegt
|
||||
// damit unter <root>/talk), audio.sink=null und der Fake-Provider als
|
||||
@@ -338,7 +330,8 @@ func daemonEnv(root string, port int, fakeURL string, extra map[string]string) [
|
||||
"TALK_ELEVENLABS_ENDPOINT": fakeURL,
|
||||
"TALK_EDGE_ENDPOINT": "ws://127.0.0.1:1/", // Kette soll nie auf echte Fremd-Endpunkte fallen
|
||||
"TALK_PORT": strconv.Itoa(port),
|
||||
"TALK_RATE_LIMIT": "0s", // Gesetz aus, bis ein Test es anstellt (siehe oben)
|
||||
"TALK_RATE_LIMIT": "0s", // Gesetz aus, bis ein Test es anstellt (siehe oben)
|
||||
"TALK_AUTOSTART": "false", // kein Test darf unbemerkt einen Daemon hinterlassen; autostart_test.go schaltet es bewusst an
|
||||
}
|
||||
for k, v := range extra {
|
||||
set[k] = v
|
||||
|
||||
Reference in new issue
Block a user