ollie/cmd/toolsrv/internal/fs/spec.go

176 lines
4.2 KiB
Go

// spec.go - fsedsl-based namespace specification for toolsrv.
package fs
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"ollie/fsedsl"
)
// Ctx is the context passed through the fsedsl tree.
type Ctx struct {
Server *Server
Proc *Proc // set when traversing into /proc/{pid}
}
// Type aliases from fsedsl, specialized to Ctx.
type (
FsNodeDecl = fsedsl.FsNodeDecl[Ctx]
Binding = fsedsl.Binding[Ctx]
)
// Specialized fsedsl constructors for Ctx.
var (
DirNode = fsedsl.DirNode[Ctx]
FileNode = fsedsl.FileNode[Ctx]
Each = fsedsl.Each[Ctx]
Doc = fsedsl.Doc[Ctx]
Read = fsedsl.Read[Ctx]
Write = fsedsl.Write[Ctx]
BlockOnce = fsedsl.BlockOnce[Ctx]
Request = fsedsl.Request[Ctx]
)
// Spec returns the fsedsl specification for the toolsrv namespace.
func Spec() FsNodeDecl {
return DirNode("/",
FileNode("ctl", 0222,
Doc("Control: write 'load <tool>' or 'unload <tool>'"),
Write(handleCtlWrite),
),
FileNode("tools", 0666,
Doc("Tools: write agent ID, read tool list (rdwr)"),
Request(handleToolsRequest),
),
FileNode("info", 0444,
Doc("Host info: platform, arch"),
Read(handleInfoRead),
),
DirNode("proc",
FileNode("new", 0666,
Doc("Execute tool: write token + tool + args, read result (blocking)"),
Request(handleProcNew),
),
FileNode("new.bg", 0666,
Doc("Execute tool in background: write token + tool + args, read pid"),
Request(handleProcNewBg),
),
Each("{pid}", procBindings,
FileNode("out", 0444,
Doc("Process output"),
Read(handleProcOutRead),
),
FileNode("wait", 0444,
Doc("Block until process exits, returns exit code"),
BlockOnce(handleProcWait),
),
FileNode("stat", 0444,
Doc("Process status: running/exited, runtime, tool"),
Read(handleProcStatRead),
),
FileNode("ctl", 0222,
Doc("Process control: write 'signal <N>' or 'dismiss'"),
Write(handleProcCtlWrite),
),
),
),
)
}
// --- Handlers ---
func handleCtlWrite(ctx Ctx, data []byte) error {
return ctx.Server.Fs.HandleCtl(strings.TrimSpace(string(data)))
}
func handleToolsRequest(ctx Ctx, data []byte) ([]byte, error) {
agentID := strings.TrimSpace(string(data))
if agentID == "" {
return nil, fmt.Errorf("agent ID required")
}
return []byte(ctx.Server.Fs.HandleToolsRequest(agentID)), nil
}
func handleInfoRead(ctx Ctx) ([]byte, error) {
return []byte(ctx.Server.HostInfo()), nil
}
func handleProcNew(ctx Ctx, data []byte) ([]byte, error) {
result, err := ctx.Server.Fs.HandleProcNew(context.Background(), string(data))
if err != nil {
return nil, err
}
return []byte(result), nil
}
func handleProcNewBg(ctx Ctx, data []byte) ([]byte, error) {
result, err := ctx.Server.Fs.HandleProcNewBg(context.Background(), string(data))
if err != nil {
return nil, err
}
return []byte(result), nil
}
// procBindings returns bindings for each running process.
func procBindings(ctx Ctx) ([]Binding, error) {
pids := ctx.Server.Fs.ListProcs()
var bindings []Binding
for _, pid := range pids {
proc := ctx.Server.Fs.GetProc(pid)
if proc == nil {
continue
}
p := proc // capture for closure
bindings = append(bindings, Binding{
Name: strconv.Itoa(pid),
Applier: func(c Ctx) Ctx {
c.Proc = p
return c
},
})
}
return bindings, nil
}
func handleProcOutRead(ctx Ctx) ([]byte, error) {
if ctx.Proc == nil {
return nil, fmt.Errorf("no process context")
}
return []byte(ctx.Proc.Output()), nil
}
func handleProcWait(ctx Ctx, cctx context.Context, base string) ([]byte, string, error) {
if ctx.Proc == nil {
return nil, "", fmt.Errorf("no process context")
}
exitCode := ctx.Proc.Wait()
return []byte(fmt.Sprintf("%d\n", exitCode)), "", nil
}
func handleProcStatRead(ctx Ctx) ([]byte, error) {
if ctx.Proc == nil {
return nil, fmt.Errorf("no process context")
}
return []byte(ctx.Proc.Stat()), nil
}
func handleProcCtlWrite(ctx Ctx, data []byte) error {
if ctx.Proc == nil {
return fmt.Errorf("no process context")
}
return ctx.Server.Fs.HandleProcCtl(ctx.Proc.ID, strings.TrimSpace(string(data)))
}
// Mode constants for convenience.
const (
ModeReadOnly os.FileMode = 0444
ModeWriteOnly os.FileMode = 0222
ModeReadWrite os.FileMode = 0666
)