68 lines
1.7 KiB
Go
68 lines
1.7 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package speechdispatcher
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
type Rejection struct{ Code string }
|
|
|
|
func (r *Rejection) Error() string { return "Speech Dispatcher rejected a command: " + r.Code }
|
|
|
|
// Query owns a separate discovery connection. Its caller serializes commands.
|
|
// Discovery never starts a daemon unless preparation explicitly requests it.
|
|
type Query struct {
|
|
conn net.Conn
|
|
reader *bufio.Reader
|
|
}
|
|
|
|
func OpenQuery(ctx context.Context, socket string, autospawn, systemFallback bool) (*Query, error) {
|
|
endpoint, err := ResolveAddress(socket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dial := Dialer((&net.Dialer{Timeout: connectTimeout}).DialContext)
|
|
if systemFallback && AutomaticAddress(socket) {
|
|
dial = WithSystemFallback(dial)
|
|
}
|
|
var conn net.Conn
|
|
if autospawn {
|
|
conn, err = ConnectWithAutospawn(ctx, endpoint, dial, SpawnServer)
|
|
} else {
|
|
conn, err = dial(ctx, endpoint.Network, endpoint.Address)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Query{conn: conn, reader: bufio.NewReader(conn)}, nil
|
|
}
|
|
func (q *Query) Close() { q.conn.Close() }
|
|
func (q *Query) Command(ctx context.Context, command string) ([]string, error) {
|
|
if strings.ContainsAny(command, "\r\n\x00") {
|
|
return nil, fmt.Errorf("invalid SSIP command")
|
|
}
|
|
finish, err := prepareIO(ctx, q.conn, ioTimeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer finish()
|
|
if _, err = fmt.Fprint(q.conn, command+"\r\n"); err != nil {
|
|
return nil, err
|
|
}
|
|
response, err := readReply(q.reader)
|
|
if err != nil {
|
|
q.Close()
|
|
return nil, err
|
|
}
|
|
if response.code < 200 || response.code >= 300 {
|
|
return nil, &Rejection{Code: fmt.Sprint(response.code)}
|
|
}
|
|
return response.data, nil
|
|
}
|