Files

96 lines
2.7 KiB
Go

// 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
}