// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package speech import ( "context" "fmt" "net" "os" "os/exec" "strings" "time" ) type serverDialer func(context.Context, string, string) (net.Conn, error) // Like Python speechd, try the existing server first, then ask the daemon to // spawn according to its own configuration. Never start a local daemon for a // remote TCP endpoint, and never stop a daemon owned by the session. func connectWithAutospawn(ctx context.Context, endpoint serverAddress, dial serverDialer, spawn func(context.Context, serverAddress) error) (net.Conn, error) { conn, err := dial(ctx, endpoint.network, endpoint.address) if ctx.Err() != nil { if conn != nil { _ = conn.Close() } return nil, ctx.Err() } if err == nil { return conn, err } if spawnErr := spawn(ctx, endpoint); spawnErr != nil { return nil, fmt.Errorf("%w; autostart Speech Dispatcher: %v", err, spawnErr) } retryContext, cancel := context.WithTimeout(ctx, connectTimeout) defer cancel() for { conn, err = dial(retryContext, endpoint.network, endpoint.address) if err == nil { return conn, nil } timer := time.NewTimer(50 * time.Millisecond) select { case <-retryContext.Done(): timer.Stop() return nil, fmt.Errorf("after Speech Dispatcher autostart: %w", err) case <-timer.C: } } } func spawnArguments(ctx context.Context, endpoint serverAddress) ([]string, error) { args := []string{"--spawn"} if endpoint.network == "unix" { return append(args, "--communication-method", "unix_socket", "--socket-path", endpoint.address), nil } host, port, err := net.SplitHostPort(endpoint.address) if err != nil { return nil, err } addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host) if err != nil { return nil, err } if len(addresses) == 0 { return nil, fmt.Errorf("no addresses for %s", host) } for _, address := range addresses { if !address.IP.IsLoopback() { return nil, fmt.Errorf("cannot autostart a remote Speech Dispatcher at %s", host) } } return append(args, "--communication-method", "inet_socket", "--port", port), nil } func spawnServer(ctx context.Context, endpoint serverAddress) error { spawnContext, cancel := context.WithTimeout(ctx, ioTimeout) defer cancel() args, err := spawnArguments(spawnContext, endpoint) if err != nil { return err } command := os.Getenv("SPEECHD_CMD") if command == "" { command = "speech-dispatcher" } process := exec.CommandContext(spawnContext, command, args...) process.WaitDelay = 100 * time.Millisecond output, err := process.CombinedOutput() if err != nil { return fmt.Errorf("%s --spawn: %w: %s", command, err, strings.TrimSpace(string(output))) } return nil }