i screwed upMerge branch 'testing' of https://git.stormux.org/storm/fenrir into testing

This commit is contained in:
deedra committed 2026-10-28 08:58:20 -07:00
commit 55d550583c
11 files changed
+638 -222

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package speech
import (
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"strings"
)
type serverAddress struct {
network string
address string
}
// default_socket_path follows Python speechd, retaining Fenrir's sudo-user
// runtime directory selection for console sessions.
func default_socket_path() string {
if uid := os.Getenv("SUDO_UID"); uid != "" {
if _, err := strconv.ParseUint(uid, 10, 32); err == nil {
return filepath.Join("/run/user", uid, "speech-dispatcher/speechd.sock")
}
}
runtimeDirectory := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDirectory == "" {
runtimeDirectory = os.Getenv("XDG_CACHE_HOME")
}
if runtimeDirectory == "" {
cacheDirectory, err := os.UserCacheDir()
if err == nil {
runtimeDirectory = cacheDirectory
}
}
return filepath.Join(runtimeDirectory, "speech-dispatcher", "speechd.sock")
}
// resolve_server_address gives an internal socket override priority over speechd's
// standard address variables. SPEECHD_SOCKET is the older Unix-only setting.
func resolve_server_address(socket string) (serverAddress, error) {
if socket != "" {
return serverAddress{network: "unix", address: socket}, nil
}
if address := os.Getenv("SPEECHD_ADDRESS"); address != "" {
return parse_server_address(address)
}
if socket := os.Getenv("SPEECHD_SOCKET"); socket != "" {
return serverAddress{network: "unix", address: socket}, nil
}
return serverAddress{network: "unix", address: default_socket_path()}, nil
}
func parse_server_address(address string) (serverAddress, error) {
method, value, _ := strings.Cut(address, ":")
switch method {
case "unix_socket":
if value == "" {
value = default_socket_path()
}
if !filepath.IsAbs(value) {
return serverAddress{}, fmt.Errorf("Speech Dispatcher Unix socket must be an absolute path: %q", value)
}
return serverAddress{network: "unix", address: value}, nil
case "inet_socket":
host, port := "127.0.0.1", 6560
if value != "" {
parts := strings.Split(value, ":")
if len(parts) > 2 {
return serverAddress{}, fmt.Errorf("invalid Speech Dispatcher address %q", address)
}
if parts[0] != "" {
host = parts[0]
}
if len(parts) == 2 {
parsed, err := strconv.Atoi(parts[1])
if err != nil || parsed < 1 || parsed > 65535 {
return serverAddress{}, fmt.Errorf("invalid Speech Dispatcher port in %q", address)
}
port = parsed
}
}
return serverAddress{network: "tcp4", address: net.JoinHostPort(host, strconv.Itoa(port))}, nil
default:
return serverAddress{}, fmt.Errorf("unsupported Speech Dispatcher address %q", address)
}
}
+124
View File
@@ -0,0 +1,124 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package speech
import (
"net"
"os"
"path/filepath"
"strings"
"testing"
)
func TestResolveServerAddressPrecedence(t *testing.T) {
t.Setenv("SUDO_UID", "")
runtimeDirectory := t.TempDir()
cacheDirectory := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", runtimeDirectory)
t.Setenv("XDG_CACHE_HOME", cacheDirectory)
t.Setenv("SPEECHD_SOCKET", "/tmp/legacy-speech.sock")
t.Setenv("SPEECHD_ADDRESS", "unix_socket:/tmp/address-speech.sock")
for _, test := range []struct {
name, override, speechAddress, legacySocket, runtimeDir, want string
}{
{"explicit", "/tmp/explicit-speech.sock", "unix_socket:/tmp/address-speech.sock", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/explicit-speech.sock"},
{"address", "", "unix_socket:/tmp/address-speech.sock", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/address-speech.sock"},
{"legacy", "", "", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/legacy-speech.sock"},
{"runtime", "", "", "", runtimeDirectory, filepath.Join(runtimeDirectory, "speech-dispatcher", "speechd.sock")},
{"cache", "", "", "", "", filepath.Join(cacheDirectory, "speech-dispatcher", "speechd.sock")},
} {
t.Run(test.name, func(t *testing.T) {
t.Setenv("SPEECHD_ADDRESS", test.speechAddress)
t.Setenv("SPEECHD_SOCKET", test.legacySocket)
t.Setenv("XDG_RUNTIME_DIR", test.runtimeDir)
got, err := resolve_server_address(test.override)
if err != nil || got.network != "unix" || got.address != test.want {
t.Fatalf("address = %+v, error = %v; want %q", got, err, test.want)
}
})
}
}
func TestParseServerAddress(t *testing.T) {
for _, test := range []struct {
input, network, address string
}{
{"unix_socket:/tmp/speech.sock", "unix", "/tmp/speech.sock"},
{"inet_socket", "tcp4", "127.0.0.1:6560"},
{"inet_socket:localhost:6561", "tcp4", "localhost:6561"},
{"inet_socket::6561", "tcp4", "127.0.0.1:6561"},
} {
got, err := parse_server_address(test.input)
if err != nil || got.network != test.network || got.address != test.address {
t.Fatalf("parse %q = %+v, %v", test.input, got, err)
}
}
for _, address := range []string{"invalid:/tmp/speech.sock", "unix_socket:relative.sock", "inet_socket:localhost:0", "inet_socket:localhost:bad"} {
if _, err := parse_server_address(address); err == nil {
t.Fatalf("accepted invalid address %q", address)
}
}
}
func TestSpeechAddressUsedByClientAndDiscovery(t *testing.T) {
for _, network := range []string{"unix", "tcp4"} {
t.Run(network, func(t *testing.T) {
var server *fakeServer
if network == "unix" {
server = new_fake_server(t, false)
} else {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
server = new_fake_server_listener(t, false, listener)
}
address := "unix_socket:" + server.path
if network == "tcp4" {
address = "inet_socket:" + server.path
}
t.Setenv("SPEECHD_ADDRESS", address)
t.Setenv("SPEECHD_SOCKET", "/absent.sock")
t.Setenv("SPEECHD_CMD", "/nonexistent/speech-dispatcher")
client, err := Prepare("speech-dispatcher", Options{Module: "fixture", Voice: "Base Voice"})
if err != nil {
t.Fatal(err)
}
defer client.Close()
if _, err := Discover(Options{}); err != nil {
t.Fatal(err)
}
if err := client.Say("connection fixture"); err != nil {
t.Fatal(err)
}
wait_event(t, server.events, "say:connection fixture")
if err := client.Cancel(); err != nil {
t.Fatal(err)
}
wait_event(t, server.events, "CANCEL 41")
})
}
}
func TestInvalidAddressDoesNotFallBack(t *testing.T) {
server := new_fake_server(t, false)
t.Setenv("SPEECHD_ADDRESS", "unsupported:address")
t.Setenv("SPEECHD_SOCKET", server.path)
if _, err := Open(Options{}); err == nil || !strings.Contains(err.Error(), "unsupported") {
t.Fatalf("error = %v", err)
}
}
func TestDefaultSocketWithoutRuntimeOrCacheEnvironment(t *testing.T) {
t.Setenv("SUDO_UID", "")
t.Setenv("XDG_RUNTIME_DIR", "")
t.Setenv("XDG_CACHE_HOME", "")
t.Setenv("HOME", t.TempDir())
want := filepath.Join(os.Getenv("HOME"), ".cache", "speech-dispatcher", "speechd.sock")
if got := default_socket_path(); got != want {
t.Fatalf("socket %q, want %q", got, want)
}
}
+106
View File
@@ -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
}
+235
View File
@@ -0,0 +1,235 @@
// 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"
"path/filepath"
"reflect"
"strconv"
"strings"
"syscall"
"testing"
)
func TestColdSpeechConnectionAutostartsAndRetriesSelectedEndpoint(t *testing.T) {
for _, connectError := range []error{syscall.ENOENT, syscall.ECONNREFUSED} {
t.Run(connectError.Error(), func(t *testing.T) {
endpoint := serverAddress{network: "unix", address: "/tmp/selected-speech.sock"}
client, server := net.Pipe()
defer client.Close()
defer server.Close()
attempts, starts := 0, 0
got, err := connect_with_autospawn(context.Background(), endpoint, func(_ context.Context, network, address string) (net.Conn, error) {
if network != endpoint.network || address != endpoint.address {
t.Fatal("connection address changed during autostart")
}
attempts++
if attempts == 1 {
return nil, connectError
}
return client, nil
}, func(_ context.Context, selected serverAddress) error {
if selected != endpoint {
t.Fatal("spawn address differs from selected server")
}
starts++
return nil
})
if err != nil || got != client || attempts != 2 || starts != 1 {
t.Fatalf("connection = %v, %v; attempts %d, starts %d", got, err, attempts, starts)
}
})
}
}
func TestAutospawnKeepsRefusalAndCancellationErrors(t *testing.T) {
dial := func(context.Context, string, string) (net.Conn, error) { return nil, syscall.ECONNREFUSED }
starts := 0
spawn := func(context.Context, serverAddress) error { starts++; return errors.New("autospawn disabled") }
_, err := connect_with_autospawn(context.Background(), serverAddress{}, dial, spawn)
if !errors.Is(err, syscall.ECONNREFUSED) || !strings.Contains(err.Error(), "autospawn disabled") || starts != 1 {
t.Fatalf("error = %v, starts = %d", err, starts)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _ = connect_with_autospawn(ctx, serverAddress{}, dial, spawn)
if starts != 1 {
t.Fatal("canceled connection started a daemon")
}
ctx, cancel = context.WithCancel(context.Background())
_, err = connect_with_autospawn(ctx, serverAddress{}, func(ctx context.Context, _, _ string) (net.Conn, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
return nil, syscall.ECONNREFUSED
}, func(context.Context, serverAddress) error { cancel(); return nil })
if !errors.Is(err, context.Canceled) {
t.Fatalf("retry error = %v", err)
}
}
func TestSpeechSpawnArgumentsPreserveEndpointAndRejectRemote(t *testing.T) {
for _, test := range []struct {
endpoint serverAddress
want []string
}{
{serverAddress{"unix", "/tmp/custom.sock"}, []string{"--spawn", "--communication-method", "unix_socket", "--socket-path", "/tmp/custom.sock"}},
{serverAddress{"tcp4", "127.0.0.1:6561"}, []string{"--spawn", "--communication-method", "inet_socket", "--port", "6561"}},
} {
args, err := spawn_arguments(context.Background(), test.endpoint)
if err != nil || !reflect.DeepEqual(args, test.want) {
t.Fatalf("arguments = %v, %v", args, err)
}
}
if _, err := spawn_arguments(context.Background(), serverAddress{"tcp4", "192.0.2.1:6560"}); err == nil {
t.Fatal("remote server allowed local autospawn")
}
}
func TestSpeechSpawnUsesConfiguredExecutableAndReportsRefusal(t *testing.T) {
t.Setenv("SUDO_UID", "")
path := filepath.Join(t.TempDir(), "fake-speech-dispatcher")
if err := os.WriteFile(path, []byte("#!/bin/sh\necho 'autospawn disabled by daemon' >&2\nexit 1\n"), 0700); err != nil {
t.Fatal(err)
}
t.Setenv("SPEECHD_CMD", path)
err := spawn_server(context.Background(), serverAddress{"unix", "/tmp/custom.sock"})
if err == nil || !strings.Contains(err.Error(), "autospawn disabled by daemon") {
t.Fatalf("error = %v", err)
}
}
func TestExistingSpeechServerDoesNotInvokeSpawn(t *testing.T) {
t.Setenv("SPEECHD_CMD", "/nonexistent/speech-dispatcher")
server := new_fake_server(t, false)
client, err := Open(Options{Socket: server.path})
if err != nil {
t.Fatal(err)
}
defer client.Close()
}
func TestPermissionErrorDoesNotSpawn(t *testing.T) {
starts := 0
_, err := connect_with_autospawn(context.Background(), serverAddress{},
func(context.Context, string, string) (net.Conn, error) { return nil, syscall.EACCES },
func(context.Context, serverAddress) error { starts++; return nil })
if !errors.Is(err, syscall.EACCES) || starts != 0 {
t.Fatalf("error %v, starts %d", err, starts)
}
}
func TestDiscoveryDoesNotSpawn(t *testing.T) {
marker := filepath.Join(t.TempDir(), "started")
command := filepath.Join(t.TempDir(), "fake-speech-dispatcher")
if err := os.WriteFile(command, []byte("#!/bin/sh\ntouch "+marker+"\n"), 0700); err != nil {
t.Fatal(err)
}
t.Setenv("SPEECHD_CMD", command)
options := Options{Socket: filepath.Join(t.TempDir(), "absent.sock")}
if _, err := Discover(options); err == nil {
t.Fatal("expected connection failure")
}
if _, err := discover_modules(options); err == nil {
t.Fatal("expected connection failure")
}
if _, err := discover_voices(options); err == nil {
t.Fatal("expected connection failure")
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("discovery started a daemon: %v", err)
}
}
func TestSudoDoesNotSpawnAsAnotherUser(t *testing.T) {
t.Setenv("SUDO_UID", strconv.Itoa(os.Geteuid()+1))
err := spawn_server(context.Background(), serverAddress{"unix", "/tmp/custom.sock"})
if err == nil || !strings.Contains(err.Error(), "invoking user") {
t.Fatalf("error = %v", err)
}
}
// Run against a separate dummy-output daemon, never the user's speech server.
func TestNativeSpeechDispatcherColdStart(t *testing.T) {
if os.Getenv("FENRIR_NATIVE_SPEECH_STARTUP") != "1" {
t.Skip("requires isolated Speech Dispatcher startup")
}
if _, err := exec.LookPath("speech-dispatcher"); err != nil {
t.Fatal(err)
}
for _, staleSocket := range []bool{false, true} {
t.Run(fmt.Sprintf("stale-socket-%v", staleSocket), func(t *testing.T) {
directory, err := os.MkdirTemp("", "speech-start-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(directory)
configPath := filepath.Join(directory, "speechd.conf")
config := "LogLevel 0\nLogDir \"" + directory + "\"\nAddModule \"dummy\" \"sd_dummy\" \"\"\nDefaultModule dummy\nTimeout 1\n"
if err := os.WriteFile(configPath, []byte(config), 0600); err != nil {
t.Fatal(err)
}
pidPath := filepath.Join(directory, "speechd.pid")
wrapper := filepath.Join(directory, "spawn")
script := "#!/bin/sh\nexec speech-dispatcher --config-dir \"$FENRIR_TEST_SPEECH_CONFIG\" --pid-file \"$FENRIR_TEST_SPEECH_PID\" \"$@\"\n"
if err := os.WriteFile(wrapper, []byte(script), 0700); err != nil {
t.Fatal(err)
}
t.Setenv("FENRIR_TEST_SPEECH_CONFIG", directory)
t.Setenv("FENRIR_TEST_SPEECH_PID", pidPath)
t.Setenv("SPEECHD_CMD", wrapper)
t.Setenv("XDG_RUNTIME_DIR", directory)
t.Setenv("SPEECHD_ADDRESS", "")
t.Setenv("SPEECHD_SOCKET", "")
t.Setenv("SUDO_UID", "")
defer func() {
if data, err := os.ReadFile(pidPath); err == nil {
if pid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil {
_ = syscall.Kill(pid, syscall.SIGTERM)
}
}
}()
socket := default_socket_path()
if staleSocket {
if err := os.MkdirAll(filepath.Dir(socket), 0700); err != nil {
t.Fatal(err)
}
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
if err != nil {
t.Fatal(err)
}
listener.SetUnlinkOnClose(false)
listener.Close()
}
// Selection preparation must also start a cold daemon before querying
// membership, even when the saved module is no longer installed.
if staleSocket {
candidate, prepareErr := Prepare("speech-dispatcher", Options{Module: "unavailable"})
if candidate != nil {
candidate.Close()
t.Fatal("unexpected candidate")
}
if prepareErr == nil || !strings.Contains(prepareErr.Error(), `speech module "unavailable" is unavailable`) {
t.Fatalf("selection error = %v", prepareErr)
}
}
client, err := Open(Options{})
if err != nil {
t.Fatal(err)
}
defer client.Close()
if client.id == "" || client.control == nil {
t.Fatal("cold-start client did not initialize both channels")
}
})
}
}
+3 -8
View File
@@ -40,11 +40,8 @@ func Validate_selection(module, language, voice string) error {
// deadline and the entire catalog has a four-second budget. Partial failures
// retain successful modules and mark unavailable voice lists explicitly.
func Discover(options Options) (Catalog, error) {
if options.Socket == "" {
options.Socket = Default_socket()
}
result := Catalog{Voices: map[string][]Voice{}, Errors: map[string]error{}}
w, err := connect(options.Socket)
w, err := connect_options(options)
if err != nil {
return result, err
}
@@ -195,6 +192,7 @@ func Prepare(driver string, options Options) (Transport, error) {
if err = d.Availability(); err != nil {
return nil, err
}
options.autospawn = driver == "speech-dispatcher"
if options.Module != "" {
modules, err := d.Modules(options)
if err != nil {
@@ -230,10 +228,7 @@ func discover_voices(options Options) ([]Voice, error) {
if err := Validate_selection(options.Module, options.Language, options.Voice); err != nil {
return nil, err
}
if options.Socket == "" {
options.Socket = Default_socket()
}
w, err := connect(options.Socket)
w, err := connect_options(options)
if err != nil {
return nil, err
}
+39 -31
View File
@@ -20,10 +20,9 @@ package speech
import (
"bufio"
"context"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -96,8 +95,27 @@ func (w *wire) command(command string) ([]string, error) {
return w.reply()
}
func connect(path string) (*wire, error) {
conn, err := net.DialTimeout("unix", path, commandTimeout)
// Queries share the address resolver. Only Prepare opts into daemon startup;
// ordinary settings discovery never starts a service.
func connect_options(options Options) (*wire, error) {
endpoint, err := resolve_server_address(options.Socket)
if err != nil {
return nil, err
}
return connect_endpoint(endpoint, options.autospawn)
}
func connect_endpoint(endpoint serverAddress, autospawn bool) (*wire, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*commandTimeout)
defer cancel()
dialer := &net.Dialer{Timeout: commandTimeout}
var conn net.Conn
var err error
if autospawn {
conn, err = connect_with_autospawn(ctx, endpoint, dialer.DialContext, spawn_server)
} else {
conn, err = dialer.DialContext(ctx, endpoint.network, endpoint.address)
}
if err != nil {
return nil, err
}
@@ -113,14 +131,16 @@ type Client struct {
}
type Options struct {
Socket string
Rate int
Pitch *int
Volume *int
Module string
Language string
Voice string
Hardware HardwareOptions
// Only Prepare enables startup during its prerequisite selection queries.
autospawn bool
Socket string
Rate int
Pitch *int
Volume *int
Module string
Language string
Voice string
Hardware HardwareOptions
}
type Parameters struct {
@@ -134,22 +154,6 @@ func (p Parameters) Validate() error {
return nil
}
func Default_socket() string {
if value := os.Getenv("SPEECHD_ADDRESS"); strings.HasPrefix(value, "unix_socket:") {
return strings.TrimPrefix(value, "unix_socket:")
}
if uid := os.Getenv("SUDO_UID"); uid != "" {
if _, err := strconv.ParseUint(uid, 10, 32); err == nil {
return filepath.Join("/run/user", uid, "speech-dispatcher/speechd.sock")
}
}
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
runtimeDir = filepath.Join("/run/user", strconv.Itoa(os.Getuid()))
}
return filepath.Join(runtimeDir, "speech-dispatcher/speechd.sock")
}
func Open(options Options) (*Client, error) {
if options.Rate < -100 || options.Rate > 100 {
return nil, fmt.Errorf("speech rate must be between -100 and 100")
@@ -162,9 +166,13 @@ func Open(options Options) (*Client, error) {
if err := Validate_selection(options.Module, options.Language, options.Voice); err != nil {
return nil, err
}
data, err := connect(options.Socket)
endpoint, err := resolve_server_address(options.Socket)
if err != nil {
return nil, fmt.Errorf("connect Speech Dispatcher at %s: %w", options.Socket, err)
return nil, err
}
data, err := connect_endpoint(endpoint, true)
if err != nil {
return nil, fmt.Errorf("connect Speech Dispatcher at %s: %w", endpoint.address, err)
}
client := &Client{data: data}
fail := func(err error) (*Client, error) {
@@ -213,7 +221,7 @@ func Open(options Options) (*Client, error) {
return fail(fmt.Errorf("invalid Speech Dispatcher client ID"))
}
client.id = strconv.FormatUint(id, 10)
client.control, err = connect(options.Socket)
client.control, err = connect_endpoint(endpoint, false)
if err != nil {
return fail(err)
}
+15 -9
View File
@@ -62,14 +62,19 @@ type fakeServer struct {
func new_fake_server(t *testing.T, block bool) *fakeServer {
t.Helper()
server := &fakeServer{path: filepath.Join(t.TempDir(), "s"), events: make(chan string, 64), ack: make(chan struct{}), stop: make(chan struct{})}
if !block {
close(server.ack)
}
listener, err := net.Listen("unix", server.path)
listener, err := net.Listen("unix", filepath.Join(t.TempDir(), "s"))
if err != nil {
t.Fatal(err)
}
return new_fake_server_listener(t, block, listener)
}
func new_fake_server_listener(t *testing.T, block bool, listener net.Listener) *fakeServer {
t.Helper()
server := &fakeServer{path: listener.Addr().String(), events: make(chan string, 64), ack: make(chan struct{}), stop: make(chan struct{})}
if !block {
close(server.ack)
}
server.wg.Add(1)
acceptedDone := make(chan struct{})
go func() {
@@ -293,13 +298,14 @@ func TestLiveParametersUseSpeakingClientAndRejectInvalidValues(t *testing.T) {
func TestDefaultSocketForSudo(t *testing.T) {
t.Setenv("SPEECHD_ADDRESS", "")
t.Setenv("SPEECHD_SOCKET", "")
t.Setenv("SUDO_UID", "1000")
t.Setenv("XDG_RUNTIME_DIR", "/run/user/0")
if got := Default_socket(); got != "/run/user/1000/speech-dispatcher/speechd.sock" {
t.Fatal(got)
if got, err := resolve_server_address(""); err != nil || got.address != "/run/user/1000/speech-dispatcher/speechd.sock" {
t.Fatal(got, err)
}
t.Setenv("SPEECHD_ADDRESS", "unix_socket:/tmp/example-speechd.sock")
if got := Default_socket(); got != "/tmp/example-speechd.sock" {
t.Fatal(got)
if got, err := resolve_server_address(""); err != nil || got.address != "/tmp/example-speechd.sock" {
t.Fatal(got, err)
}
}
+2 -8
View File
@@ -43,9 +43,6 @@ func (d Driver) Availability() error {
var drivers = append([]Driver{{
ID: "speech-dispatcher", Label: "Speech Dispatcher",
Open: func(options Options) (Transport, error) {
if options.Socket == "" {
options.Socket = Default_socket()
}
return Open(options)
},
Modules: discover_modules,
@@ -100,12 +97,9 @@ func Open_driver(id string, options Options) (Transport, error) {
}
// discover_modules uses a separate, bounded connection. It never changes the
// active speaking client, cancels speech or starts a speech service.
// active speaking client or cancels speech. Only Prepare permits daemon startup.
func discover_modules(options Options) ([]string, error) {
if options.Socket == "" {
options.Socket = Default_socket()
}
wire, err := connect(options.Socket)
wire, err := connect_options(options)
if err != nil {
return nil, err
}