6.2 KiB
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
- Directories use
ChildrenorBindings; file handlers belong on leaf declarations. BlockOnce,Stream, andRdwrare mutually exclusive.- A template-style name such as
{sname}must have aBindingscallback. - Ownership inherits through empty
UIDandGIDvalues. - A zero mode inherits from the parent. Root directories default to
0755; root files default to0444.
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.