96 lines
2.7 KiB
Go
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
|
|
}
|