Files
Maven/internal/worker/server.go
claude c0de473382 ipc, worker: dial and bind through netaddr (V-484)
Five hardcoded transports, three in internal/ipc and two in
internal/worker, all now go through the seam address. The unix perms
logic moved into netaddr, so the two copies of parentDir and the umask
dance are gone.

peerCaller already returned ok=false for a non-unix conn, so the
SO_PEERCRED path degrades correctly on tcp with no change.
2026-08-02 15:08:07 +02:00

217 lines
6.1 KiB
Go

// worker/server.go — the worker-side listener + accept loop.
//
// Each call is dispatched to a single handler (Transcriber OR Synthesizer,
// depending on what the module process);
// the other verb returns ErrUnknownMethod — a stt process won't serve
// synthesize. robust to a misconfigured client (the daemon wiring chooses
// which module to dial; mixing the two is a config error caught cleanly by
// the wire, not a runtime goroutine panic). One Server per module process.
//
// The seam address decides the transport. On the default unix socket the
// perms mirror ipc.Server — dir 0700, socket 0600 ⇒ same unix user — and that
// is the whole auth floor, because the seam never leaves the box. A tcp
// address moves the module to another host and takes that floor away, so
// netaddr checks a shared token before the first frame. See internal/netaddr.
package worker
import (
"context"
"encoding/json"
"fmt"
"net"
"sync"
"sync/atomic"
"github.com/kami/maven/internal/netaddr"
)
// Server — a worker module process's listener. Wires either a Transcriber,
// a Synthesizer, or both (the both case is unusual; the daemon typically
// runs two separate module processes). The unset verb returns
// ErrUnknownMethod.
type Server struct {
t Transcriber
s Synthesizer
path string
addr netaddr.Addr
ln net.Listener
wg sync.WaitGroup
done chan struct{}
// connCount — assigned per accepted conn, used in logs to distinguish
// concurrent connections. Monotonic; not load-bearing for correctness.
connCount atomic.Uint64
}
// NewServer builds a Server with a Transcriber. The caller wires a
// Synthesizer via SetSynthesizer if this process serves tts. Use
// NewSynthesizerServer for the tts-only case (mirrors this constructor).
func NewServer(path string, t Transcriber) *Server {
return &Server{t: t, path: path, done: make(chan struct{})}
}
// NewSynthesizerServer builds a Server with a Synthesizer (the tts module).
func NewSynthesizerServer(path string, s Synthesizer) *Server {
return &Server{s: s, path: path, done: make(chan struct{})}
}
// SetSynthesizer wires the synthesize verb on a Transcriber-built Server.
// Used only when one process serves both (non-default; the daemon prefers
// two separate processes per the restart-free / fail-independent invariant).
func (srv *Server) SetSynthesizer(s Synthesizer) { srv.s = s }
// Listen binds the seam address the Server was built with.
//
// A bare path is a unix socket with 0700 dir + 0600 socket perms, the same
// floor as internal/ipc, and a stale socket is removed first so the worker
// process restarts cleanly after a crash. A "tcp://host:port?token=..."
// address binds a network listener instead, so this module can run on the
// workstation while core stays on homesrv; the token is mandatory there,
// because there is no filesystem to be the auth floor. See internal/netaddr.
func (srv *Server) Listen() error {
addr, err := netaddr.Parse(srv.path)
if err != nil {
return err
}
ln, err := netaddr.Listen(addr)
if err != nil {
return err
}
srv.addr = addr
srv.ln = ln
return nil
}
// Path returns the bound socket path (after Listen; "" before).
func (srv *Server) Path() string { return srv.path }
// Serve accepts connections until the listener closes. Each connection is
// served in its own goroutine; a panicking handler tears down only that conn
// (the rest of the module keeps serving, restart-free per spec).
func (srv *Server) Serve() error {
if srv.ln == nil {
return fmt.Errorf("worker: serve before listen")
}
for {
c, err := srv.ln.Accept()
if err != nil {
select {
case <-srv.done:
return nil
default:
return fmt.Errorf("worker: accept: %w", err)
}
}
srv.wg.Add(1)
go func(c net.Conn) {
defer srv.wg.Done()
defer c.Close()
srv.serveConn(c)
}(c)
}
}
func (srv *Server) serveConn(c net.Conn) {
id := srv.connCount.Add(1)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for {
var req Request
if err := readFrame(c, &req); err != nil {
return // EOF / malformed ⇒ end this conn
}
result, err := srv.safeDispatch(ctx, c.RemoteAddr(), id, req)
resp := Response{}
if err != nil {
resp.Error = rpcErr(err)
} else {
resp.Result = result
}
if err := writeFrame(c, resp); err != nil {
return
}
}
}
func (srv *Server) safeDispatch(ctx context.Context, addr net.Addr, id uint64, req Request) (result json.RawMessage, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("worker: panic dispatching %s (conn %d): %v", req.Method, id, r)
}
}()
return srv.dispatch(ctx, req)
}
func (srv *Server) dispatch(ctx context.Context, req Request) (json.RawMessage, error) {
switch req.Method {
case MethodTranscribe:
if srv.t == nil {
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
}
var p TranscribeReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
resp, err := srv.t.Transcribe(ctx, p)
if err != nil {
return nil, err
}
return marshalResult(resp), nil
case MethodSynthesize:
if srv.s == nil {
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
}
var p SynthesizeReq
if err := unmarshalParams(req.Params, &p); err != nil {
return nil, err
}
resp, err := srv.s.Synthesize(ctx, p)
if err != nil {
return nil, err
}
return marshalResult(resp), nil
default:
return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, req.Method)
}
}
// Close stops accepting and waits for in-flight connections to drain. The
// socket file is removed so a restart can rebind cleanly. Idempotent.
func (srv *Server) Close() error {
select {
case <-srv.done:
return nil
default:
close(srv.done)
}
if srv.ln == nil {
return nil
}
err := srv.ln.Close()
srv.wg.Wait()
netaddr.Cleanup(srv.addr)
return err
}
func unmarshalParams(raw json.RawMessage, v any) error {
if len(raw) == 0 {
raw = []byte("null")
}
if err := json.Unmarshal(raw, v); err != nil {
return fmt.Errorf("%w: %v", ErrBadParams, err)
}
return nil
}
func marshalResult(v any) json.RawMessage {
if v == nil {
return json.RawMessage("null")
}
b, _ := json.Marshal(v)
return b
}