From 5a09e603cd78de1d2179cfc1c1ebcc09678c4b2c Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 11 Aug 2026 20:24:29 +0200 Subject: [PATCH] virtfs: eliminate Binding type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each() now returns []FsNodeDecl directly. The Binding type was a redundant subset of FsNodeDecl with a slightly different Remove signature. Dynamic entries are now expressed uniformly — Remove, Rename, Children, Aliases all live on FsNodeDecl like everything else. Also added: Alias() and RenameNode() options, DirNode accepts []FsNodeDecl for passing pre-built child slices. --- cmd/olliesrv/internal/fs/handlers_session.go | 8 ++-- cmd/olliesrv/internal/fs/spec.go | 40 ++++++++++---------- cmd/toolsrv/internal/fs/spec.go | 8 ++-- virtfs/builder.go | 36 +++++------------- virtfs/decl.go | 38 +++++++++---------- virtfs/decl_test.go | 28 +++++++------- 6 files changed, 71 insertions(+), 87 deletions(-) diff --git a/cmd/olliesrv/internal/fs/handlers_session.go b/cmd/olliesrv/internal/fs/handlers_session.go index dcdd229..3ee199c 100644 --- a/cmd/olliesrv/internal/fs/handlers_session.go +++ b/cmd/olliesrv/internal/fs/handlers_session.go @@ -64,15 +64,13 @@ func readSessionConnected(s *SessionNode) ([]byte, error) { return []byte("false\n"), nil } -func sessionCtlHandler(s *SessionNode, removeFn func(), renameFn func(string) error) func([]byte) ([]byte, error) { +func sessionCtlHandler(s *SessionNode, removeFn func() error, renameFn func(string) error) func([]byte) ([]byte, error) { handlers := map[string]func([]string) ([]byte, error){ "kill": func(_ []string) ([]byte, error) { - removeFn() - return []byte("ok\n"), nil + return []byte("ok\n"), removeFn() }, ".": func(_ []string) ([]byte, error) { - removeFn() - return []byte("ok\n"), nil + return []byte("ok\n"), removeFn() }, "save": func(_ []string) ([]byte, error) { s.Save() diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index 81b1474..6c4610d 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -94,7 +94,7 @@ func buildTreeSpec(rs *RootState) virtfs.FsNodeDecl { virtfs.Read(func() ([]byte, error) { return readBypassPolicy(broker) }), virtfs.Write(func(data []byte) error { return writeBypassPolicy(broker, data) }), ), - virtfs.Each("{reqid}", func() ([]virtfs.Binding, error) { + virtfs.Each("{reqid}", func() ([]virtfs.FsNodeDecl, error) { return adaptBypassBindings(broker) }), ), @@ -109,7 +109,7 @@ func buildTreeSpec(rs *RootState) virtfs.FsNodeDecl { virtfs.Doc("Session index"), virtfs.Read(readSessionIdx), ), - virtfs.Each("{sname}", func() ([]virtfs.Binding, error) { + virtfs.Each("{sname}", func() ([]virtfs.FsNodeDecl, error) { return buildSessionBindings(rs, broker) }), ), @@ -117,18 +117,19 @@ func buildTreeSpec(rs *RootState) virtfs.FsNodeDecl { } // buildSessionBindings produces bindings for each session. -func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]virtfs.Binding, error) { +func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]virtfs.FsNodeDecl, error) { sessions := session.Sessions() - var out []virtfs.Binding + var out []virtfs.FsNodeDecl for name, sess := range sessions { node := rs.GetOrCreateNode(sess) s := node // capture n := name // capture - removeFn := func() { + removeFn := func() error { session.Kill(n) rs.RemoveNode(n) + return nil } renameFn := func(newName string) error { if err := session.Rename(n, newName); err != nil { @@ -138,7 +139,7 @@ func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]virtfs.Bindin return nil } - out = append(out, virtfs.Binding{ + out = append(out, virtfs.FsNodeDecl{ Name: sess.Name(), Aliases: []string{sess.ID}, Remove: removeFn, @@ -154,7 +155,7 @@ func buildSessionChildren( s *SessionNode, rs *RootState, broker *bypass.Broker, - removeFn func(), + removeFn func() error, renameFn func(string) error, ) []virtfs.FsNodeDecl { return []virtfs.FsNodeDecl{ @@ -188,7 +189,7 @@ func buildSessionChildren( virtfs.FileNode("idx", 0444, virtfs.Read(func() ([]byte, error) { return readAgentIdx(s.Session) }), ), - virtfs.Each("{aname}", func() ([]virtfs.Binding, error) { + virtfs.Each("{aname}", func() ([]virtfs.FsNodeDecl, error) { return buildAgentBindings(s, rs) }), ), @@ -196,9 +197,9 @@ func buildSessionChildren( } // buildAgentBindings produces bindings for each agent in a session. -func buildAgentBindings(s *SessionNode, rs *RootState) ([]virtfs.Binding, error) { +func buildAgentBindings(s *SessionNode, rs *RootState) ([]virtfs.FsNodeDecl, error) { agents := s.Agents() - var out []virtfs.Binding + var out []virtfs.FsNodeDecl for _, ag := range agents { a := ag @@ -209,12 +210,13 @@ func buildAgentBindings(s *SessionNode, rs *RootState) ([]virtfs.Binding, error) wireAgentEvents(s.ID, a, al) } - out = append(out, virtfs.Binding{ + out = append(out, virtfs.FsNodeDecl{ Name: a.Name(), Aliases: []string{a.ID()}, - Remove: func() { + Remove: func() error { s.RemoveAgent(a.ID()) session.PublishEvent("session."+s.ID+".agent."+a.ID()+".kill", "") + return nil }, Children: buildAgentChildren(a, al, s), }) @@ -283,7 +285,7 @@ func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []virtfs.F virtfs.Read(func() ([]byte, error) { return readAgentID(a) }), ), virtfs.DirNode("proc", - virtfs.Each("{pid}", func() ([]virtfs.Binding, error) { + virtfs.Each("{pid}", func() ([]virtfs.FsNodeDecl, error) { return adaptProcessBindings(s) }), ), @@ -291,15 +293,15 @@ func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []virtfs.F } // adaptBypassBindings returns bindings for pending bypass requests. -func adaptBypassBindings(broker *bypass.Broker) ([]virtfs.Binding, error) { +func adaptBypassBindings(broker *bypass.Broker) ([]virtfs.FsNodeDecl, error) { if broker == nil { return nil, errNoBypass } pending := broker.Pending() - var out []virtfs.Binding + var out []virtfs.FsNodeDecl for _, r := range pending { req := r - out = append(out, virtfs.Binding{ + out = append(out, virtfs.FsNodeDecl{ Name: req.ID, Children: []virtfs.FsNodeDecl{ virtfs.FileNode("request", 0666, @@ -317,7 +319,7 @@ func adaptBypassBindings(broker *bypass.Broker) ([]virtfs.Binding, error) { } // adaptProcessBindings returns bindings for detached processes. -func adaptProcessBindings(s *SessionNode) ([]virtfs.Binding, error) { +func adaptProcessBindings(s *SessionNode) ([]virtfs.FsNodeDecl, error) { if s == nil { return nil, nil } @@ -327,12 +329,12 @@ func adaptProcessBindings(s *SessionNode) ([]virtfs.Binding, error) { } defer conn.Close() raw := conn.ListDetachedRaw() - var out []virtfs.Binding + var out []virtfs.FsNodeDecl for _, r := range raw { if m, ok := r.(map[string]any); ok { if id, ok := m["pid"].(int); ok { pid := id - out = append(out, virtfs.Binding{ + out = append(out, virtfs.FsNodeDecl{ Name: strconv.Itoa(pid), Children: []virtfs.FsNodeDecl{ virtfs.FileNode("out", 0444, diff --git a/cmd/toolsrv/internal/fs/spec.go b/cmd/toolsrv/internal/fs/spec.go index 42f0e36..d4faa2f 100644 --- a/cmd/toolsrv/internal/fs/spec.go +++ b/cmd/toolsrv/internal/fs/spec.go @@ -58,16 +58,16 @@ func Spec(srv *Server) virtfs.FsNodeDecl { return []byte(result), nil }), ), - virtfs.Each("{pid}", func() ([]virtfs.Binding, error) { + virtfs.Each("{pid}", func() ([]virtfs.FsNodeDecl, error) { pids := srv.Fs.ListProcs() - var bindings []virtfs.Binding + var out []virtfs.FsNodeDecl for _, pid := range pids { proc := srv.Fs.GetProc(pid) if proc == nil { continue } p := proc - bindings = append(bindings, virtfs.Binding{ + out = append(out, virtfs.FsNodeDecl{ Name: strconv.Itoa(pid), Children: []virtfs.FsNodeDecl{ virtfs.FileNode("out", 0444, @@ -98,7 +98,7 @@ func Spec(srv *Server) virtfs.FsNodeDecl { }, }) } - return bindings, nil + return out, nil }), ), ) diff --git a/virtfs/builder.go b/virtfs/builder.go index 640fe97..6ccd1d3 100644 --- a/virtfs/builder.go +++ b/virtfs/builder.go @@ -128,16 +128,15 @@ func listDir(d *FsNodeDecl, uid, gid string) ([]os.DirEntry, error) { for _, c := range d.Children { if isTemplateName(c.Name) { if c.Bindings != nil { - bindings, err := c.Bindings() + nodes, err := c.Bindings() if err != nil { continue } - for _, b := range bindings { - // If binding has children, it's a directory; otherwise check template for file handlers - if len(b.Children) > 0 { - entries = append(entries, dirEntry(b.Name, 0755)) + for _, n := range nodes { + if len(n.Children) > 0 || n.Bindings != nil { + entries = append(entries, dirEntry(n.Name, 0755)) } else { - entries = append(entries, fileEntry(b.Name, c.Mode)) + entries = append(entries, fileEntry(n.Name, c.Mode)) } } } @@ -321,31 +320,16 @@ func findChild(d *FsNodeDecl, name string) *FsNodeDecl { } } - // Dynamic children (Each): find matching binding by name/alias. + // Dynamic children (Each): find matching entry by name/alias. for i := range d.Children { if isTemplateName(d.Children[i].Name) && d.Children[i].Bindings != nil { - bindings, err := d.Children[i].Bindings() + entries, err := d.Children[i].Bindings() if err != nil { continue } - for _, b := range bindings { - if b.Name == name || matchAlias(b.Aliases, name) { - // Build a synthetic FsNodeDecl from the binding's children. - node := FsNodeDecl{ - Name: b.Name, - Aliases: b.Aliases, - UID: b.UID, - GID: b.GID, - Children: b.Children, - } - if b.Remove != nil { - rm := b.Remove - node.Remove = func() error { rm(); return nil } - } - if b.Rename != nil { - node.Rename = b.Rename - } - return &node + for j := range entries { + if entries[j].Name == name || matchAlias(entries[j].Aliases, name) { + return &entries[j] } } } diff --git a/virtfs/decl.go b/virtfs/decl.go index afaaaf1..d311de2 100644 --- a/virtfs/decl.go +++ b/virtfs/decl.go @@ -12,11 +12,11 @@ // FileNode("version", 0444, Read(func() ([]byte, error) { // return []byte("1.0\n"), nil // })), -// Each("{user}", func() ([]Binding, error) { -// var out []Binding +// Each("{user}", func() ([]FsNodeDecl, error) { +// var out []FsNodeDecl // for _, u := range users { // user := u -// out = append(out, Binding{ +// out = append(out, FsNodeDecl{ // Name: user.Name, // Children: []FsNodeDecl{ // FileNode("name", 0444, Read(func() ([]byte, error) { @@ -52,8 +52,8 @@ type FsNodeDecl struct { Stat func() os.FileInfo // Twalk — children (implies directory; at most one set). - Children []FsNodeDecl // static, known at compile time - Bindings func() ([]Binding, error) // Each: dynamic children + Children []FsNodeDecl // static, known at compile time + Bindings func() ([]FsNodeDecl, error) // Each: dynamic children // Tread (non-blocking) + Twrite. Read func() ([]byte, error) @@ -74,17 +74,6 @@ type FsNodeDecl struct { // NodeOption configures a FsNodeDecl at construction time. type NodeOption func(*FsNodeDecl) -// Binding provides identity and pre-bound children for one iteration of Each. -type Binding struct { - Name string - Aliases []string - UID string - GID string - Children []FsNodeDecl // fully-bound subtree (handlers are closures) - Remove func() - Rename func(string) error -} - // ── EDSL constructors ──────────────────────────────────────────── // DirNode declares a static directory node. @@ -94,6 +83,8 @@ func DirNode(name string, args ...any) FsNodeDecl { switch v := a.(type) { case FsNodeDecl: d.Children = append(d.Children, v) + case []FsNodeDecl: + d.Children = append(d.Children, v...) case NodeOption: v(&d) } @@ -102,9 +93,8 @@ func DirNode(name string, args ...any) FsNodeDecl { } // Each declares a directory whose children are produced dynamically. -// The bindings function returns pre-bound children for each instance. -func Each(name string, bindings func() ([]Binding, error), opts ...NodeOption) FsNodeDecl { - d := FsNodeDecl{Name: name, Bindings: bindings} +func Each(name string, list func() ([]FsNodeDecl, error), opts ...NodeOption) FsNodeDecl { + d := FsNodeDecl{Name: name, Bindings: list} for _, o := range opts { o(&d) } @@ -152,6 +142,16 @@ func RemoveNode(fn func() error) NodeOption { return func(d *FsNodeDecl) { d.Remove = fn } } +// RenameNode sets the rename handler for a node. +func RenameNode(fn func(string) error) NodeOption { + return func(d *FsNodeDecl) { d.Rename = fn } +} + +// Alias adds alternate lookup names for this node. +func Alias(names ...string) NodeOption { + return func(d *FsNodeDecl) { d.Aliases = append(d.Aliases, names...) } +} + // StatOverride sets a custom stat handler. func StatOverride(fn func() os.FileInfo) NodeOption { return func(d *FsNodeDecl) { d.Stat = fn } diff --git a/virtfs/decl_test.go b/virtfs/decl_test.go index ad9f233..ca82476 100644 --- a/virtfs/decl_test.go +++ b/virtfs/decl_test.go @@ -183,10 +183,10 @@ func TestFileConfigModes(t *testing.T) { func TestDeleteDir(t *testing.T) { var deleted bool spec := DirNode("/", - Each("{id}", func() ([]Binding, error) { - return []Binding{{ + Each("{id}", func() ([]FsNodeDecl, error) { + return []FsNodeDecl{{ Name: "item", - Remove: func() { deleted = true }, + Remove: func() error { deleted = true; return nil }, Children: []FsNodeDecl{ FileNode("data", 0444), }, @@ -215,8 +215,8 @@ func TestDeleteDirNotFound(t *testing.T) { func TestRenameDir(t *testing.T) { var newName string spec := DirNode("/", - Each("{id}", func() ([]Binding, error) { - return []Binding{{ + Each("{id}", func() ([]FsNodeDecl, error) { + return []FsNodeDecl{{ Name: "item", Rename: func(n string) error { newName = n; return nil }, Children: []FsNodeDecl{ @@ -462,11 +462,11 @@ func TestEachClosureCapture(t *testing.T) { items := []Item{{"foo", "hello"}, {"bar", "world"}} spec := DirNode("/", - Each("{item}", func() ([]Binding, error) { - var out []Binding + Each("{item}", func() ([]FsNodeDecl, error) { + var out []FsNodeDecl for _, item := range items { it := item - out = append(out, Binding{ + out = append(out, FsNodeDecl{ Name: it.Name, Children: []FsNodeDecl{ FileNode("value", 0444, Read(func() ([]byte, error) { @@ -510,8 +510,8 @@ func TestEachClosureCapture(t *testing.T) { func TestEachDirListing(t *testing.T) { spec := DirNode("/", - Each("{item}", func() ([]Binding, error) { - return []Binding{ + Each("{item}", func() ([]FsNodeDecl, error) { + return []FsNodeDecl{ {Name: "alpha", Children: []FsNodeDecl{FileNode("x", 0444)}}, {Name: "beta", Children: []FsNodeDecl{FileNode("x", 0444)}}, }, nil @@ -533,8 +533,8 @@ func TestEachDirListing(t *testing.T) { func TestEachWithAliases(t *testing.T) { spec := DirNode("/", - Each("{item}", func() ([]Binding, error) { - return []Binding{{ + Each("{item}", func() ([]FsNodeDecl, error) { + return []FsNodeDecl{{ Name: "friendly", Aliases: []string{"uuid-123"}, Children: []FsNodeDecl{ @@ -561,8 +561,8 @@ func TestEachWithAliases(t *testing.T) { func TestStatDir(t *testing.T) { spec := DirNode("/", - Each("{item}", func() ([]Binding, error) { - return []Binding{{ + Each("{item}", func() ([]FsNodeDecl, error) { + return []FsNodeDecl{{ Name: "sub", Children: []FsNodeDecl{FileNode("file", 0444)}, }}, nil