ollie/doc/architecture-virtfs.md

6.2 KiB
Raw Blame History

virtfs Architecture

virtfs is the embedded Go library that defines Ollie’s synthetic filesystem. It separates namespace declaration from protocol transport: cmd/olliesrv/internal/fs/spec.go declares the Ollie namespace, virtfs.BuildTree materializes it as an in-memory tree, and the 9P server exposes that tree to clients.

The library is installed as part of the normal Go build. It is not a separately installed runtime service or configuration directory. The repository package at virtfs/ is compiled into olliesrv by the ninep and core build targets in Makefile.

Design

Declarations are built with constructors and options. Handlers are plain closures that capture their target state; there is no handler-context object or later wiring pass. Dynamic directories use Each, whose callback returns fully bound child declarations.

The resulting *virtfs.Tree supports the filesystem operations needed by the 9P adapter and can also be used directly in tests.

Core Type

type FsNodeDecl struct {
    Name    string
    Aliases []string      // alternate lookup names
    UID     string        // "" = inherit from parent
    GID     string        // "" = inherit from parent
    Mode    os.FileMode   // 0 = inherit parent default
    Desc    string        // help description

    Stat     func() os.FileInfo
    Children []FsNodeDecl
    Bindings func() ([]FsNodeDecl, error)

    Read      func() ([]byte, error)
    Write     func([]byte) error
    BlockOnce func(context.Context, string) ([]byte, string, error)
    Stream    func(context.Context, string) ([]byte, string, error)
    Rdwr      func(context.Context, []byte) ([]byte, error)
    Remove    func() error
    Rename    func(string) error
}

A declaration is a directory when it has Children or Bindings; otherwise it is a file. BlockOnce, Stream, and Rdwr are mutually exclusive. Handlers capture their target state in closures.

DirNode(name, children_or_options...)

Declares a static directory. Arguments may be child declarations, slices of declarations, or NodeOption functions:

virtfs.DirNode("session",
    virtfs.FileNode("new", 0666, virtfs.Rdwr(requestSessionNew)),
    virtfs.FileNode("idx", 0444, virtfs.Read(readSessionIdx)),
    virtfs.GID("agent"),
)

Each(name, bindingsFn, options...)

Declares a dynamic directory. The callback returns the currently available child declarations. Each returned child is already bound to its target through closures:

virtfs.Each("{sname}", listSessions)

FileNode(name, mode, options...)

Declares a file node. Options provide handlers, metadata, ownership, aliases, and help text:

virtfs.FileNode("chat", 0444,
    virtfs.Doc("Agent chat log"),
    virtfs.Stream(streamAgentChat, agentSignal),
)

Node Options

Option Signature Purpose
Read(fn) func() ([]byte, error) Non-blocking read
Write(fn) func([]byte) error Write handler
Stream(fn, signal) read callback plus signal channel Blocking streaming read
StreamRaw(fn) func(context.Context, string) ... Custom streaming read
BlockOnce(fn, signal) read callback plus signal channel Return one changed value per open
BlockOnceRaw(fn) func(context.Context, string) ... Custom blocking read
Rdwr(fn) func(context.Context, []byte) ([]byte, error) Atomic write-then-read
RemoveNode(fn) func() error Remove handler
RenameNode(fn) func(string) error Rename handler
StatOverride(fn) func() os.FileInfo Custom stat
UID(uid) / GID(gid) string Ownership
Alias(names...) ...string Alternate lookup names
Doc(desc) string Help description

Dynamic Declarations

The Ollie namespace uses Each for runtime collections such as sessions and agents. The callback builds declarations for the current objects and captures each object in its handlers. It does not receive a context or return a generic template that is wired later.

virtfs.Each("{aname}", func() ([]virtfs.FsNodeDecl, error) {
    var out []virtfs.FsNodeDecl
    for _, a := range agents {
        agent := a
        out = append(out, virtfs.DirNode(agent.Name(),
            virtfs.Alias(agent.ID()),
            virtfs.FileNode("id", 0444, virtfs.Read(func() ([]byte, error) {
                return []byte(agent.ID() + "\n"), nil
            })),
        ))
    }
    return out, nil
})

Building and Installation

cmd/olliesrv/internal/fs.NewRoot initializes session state, calls buildTreeSpec, and passes the resulting declaration to virtfs.BuildTree. The returned tree is mounted into the 9P server. virtfs is therefore a linked package, not a separately installed daemon.

The repository build uses:

make build   # includes core and the 9P server
make install # installs the resulting runtime and data files

The install phase copies runtime data under ~/.config/ollie and ~/.local/share/ollie; it does not copy the virtfs package or install a separate virtfs directory.

Tree API

tree := virtfs.BuildTree(spec)
tree.List()
tree.Stat(path)
tree.Open(path)
tree.Create(path)
tree.Delete(path)
tree.Rename(oldPath, newPath)
tree.Readdir(path)
tree.MkdirAll(path, mode)

virtfs.GenerateHelp(spec) renders the Doc annotations used by the server’s help file.

Rules

  1. Directories use Children or Bindings; file handlers belong on leaf declarations.
  2. BlockOnce, Stream, and Rdwr are mutually exclusive.
  3. A template-style name such as {sname} must have a Bindings callback.
  4. Ownership inherits through empty UID and GID values.
  5. A zero mode inherits from the parent. Root directories default to 0755; root files default to 0444.

Aliases

A node may declare alternate names with Alias(...). During path walks, if no child matches Name, the tree checks Aliases. This lets clients address a node by immutable ID while listings use a mutable display name.

virtfs.DirNode(session.Name(),
    virtfs.Alias(session.ID()),
    // ...
)

Aliases are invisible in directory listings. Session and agent declarations use IDs as stable aliases where needed. Clients that maintain long-lived paths should use the ID form.