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
@@ -81,6 +81,11 @@ func (c *Client) SetMute(ctx context.Context, muted bool) error {
|
||||
return c.do(ctx, http.MethodPost, path, nil, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// Shutdown ruft POST /shutdown (`talk daemon stop`).
|
||||
func (c *Client) Shutdown(ctx context.Context) error {
|
||||
return c.do(ctx, http.MethodPost, "/shutdown", nil, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
// do ist der gemeinsame Kern: Request bauen, senden, Status prüfen,
|
||||
// dekodieren. Netz-Fehler kommen ungefiltert zurück (Aufrufer unterscheidet
|
||||
// über errors.As(&HTTPError)).
|
||||
|
||||
@@ -38,6 +38,11 @@ type HandlerConfig struct {
|
||||
// AllowedOrigins ist die CORS-Erlaubnisliste für POST /speak (leer =
|
||||
// keine CORS-Header, der Browser blockt; siehe cors.go).
|
||||
AllowedOrigins []string
|
||||
|
||||
// Shutdown fährt den Daemon herunter (POST /shutdown, `talk daemon
|
||||
// stop`). Muss nicht blockieren und mehrfach aufrufbar sein. Optional:
|
||||
// nil → der Endpunkt antwortet 501.
|
||||
Shutdown func()
|
||||
}
|
||||
|
||||
// NewHandler liefert die HTTP-Routen des Daemons. Methoden-Bindung per
|
||||
@@ -52,6 +57,7 @@ func NewHandler(cfg HandlerConfig) http.Handler {
|
||||
known: make(map[string]bool, len(cfg.KnownProviders)),
|
||||
provider: cfg.Provider,
|
||||
allowedOrigins: cfg.AllowedOrigins,
|
||||
shutdown: cfg.Shutdown,
|
||||
}
|
||||
for _, name := range cfg.KnownProviders {
|
||||
h.known[name] = true
|
||||
@@ -63,6 +69,7 @@ func NewHandler(cfg HandlerConfig) http.Handler {
|
||||
mux.HandleFunc("POST /cancel", h.cancel)
|
||||
mux.HandleFunc("POST /mute", h.muteEndpoint(true))
|
||||
mux.HandleFunc("POST /unmute", h.muteEndpoint(false))
|
||||
mux.HandleFunc("POST /shutdown", h.shutdownEndpoint)
|
||||
mux.HandleFunc("POST /mcp", h.serveMCP) // MCP Streamable HTTP (Task 06)
|
||||
mux.HandleFunc("GET /mcp", h.mcpGet) // 405: keine server-initiierten Streams
|
||||
return h.originGuard(mux)
|
||||
@@ -76,6 +83,7 @@ type handler struct {
|
||||
known map[string]bool
|
||||
provider func() string
|
||||
allowedOrigins []string
|
||||
shutdown func()
|
||||
}
|
||||
|
||||
// speak behandelt POST /speak: validieren, bei Mute verwerfen (202,
|
||||
@@ -241,6 +249,18 @@ func (h *handler) muteEndpoint(muted bool) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// shutdownEndpoint behandelt POST /shutdown (`talk daemon stop`): Antwort
|
||||
// zuerst, dann der Shutdown-Trigger — der Aufrufer bekommt sein 200, bevor
|
||||
// der Server die Verbindung schließt.
|
||||
func (h *handler) shutdownEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if h.shutdown == nil {
|
||||
writeError(w, http.StatusNotImplemented, "dieser Daemon kennt keinen Shutdown-Trigger")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, ShutdownResponse{OK: true})
|
||||
h.shutdown()
|
||||
}
|
||||
|
||||
// writeJSON schreibt eine JSON-Antwort mit Status-Code.
|
||||
func writeJSON(w http.ResponseWriter, code int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -131,3 +131,8 @@ type CancelResponse struct {
|
||||
type MuteResponse struct {
|
||||
Muted bool `json:"muted"`
|
||||
}
|
||||
|
||||
// ShutdownResponse ist die Antwort von POST /shutdown.
|
||||
type ShutdownResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
Reference in new issue
Block a user