Speech-dispatcher handling improved.
This commit is contained in:
1 parent
60abcc37d9
commit
ec32c6e4c4
11 files changed
+638
-222
No files matched your search
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package speech
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"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 connect_with_autospawn(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
|
||||
}
|
||||
// Only an absent or stale endpoint warrants starting a daemon.
|
||||
if !errors.Is(err, syscall.ENOENT) && !errors.Is(err, syscall.ECONNREFUSED) {
|
||||
return nil, err
|
||||
}
|
||||
if spawnErr := spawn(ctx, endpoint); spawnErr != nil {
|
||||
return nil, fmt.Errorf("%w; autostart Speech Dispatcher: %v", err, spawnErr)
|
||||
}
|
||||
retryContext, cancel := context.WithTimeout(ctx, commandTimeout)
|
||||
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 spawn_arguments(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 spawn_server(ctx context.Context, endpoint serverAddress) error {
|
||||
// Never create a root-owned server in the invoking user's runtime directory.
|
||||
if uid := os.Getenv("SUDO_UID"); uid != "" && uid != strconv.Itoa(os.Geteuid()) {
|
||||
return fmt.Errorf("start Speech Dispatcher as the invoking user before running Fenrir with sudo")
|
||||
}
|
||||
spawnContext, cancel := context.WithTimeout(ctx, commandTimeout)
|
||||
defer cancel()
|
||||
args, err := spawn_arguments(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
|
||||
}
|
||||
Reference in new issue
Block a user