157 lines
6.2 KiB
Markdown
157 lines
6.2 KiB
Markdown
# 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
|
||
|
||
```go
|
||
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:
|
||
|
||
```go
|
||
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:
|
||
|
||
```go
|
||
virtfs.Each("{sname}", listSessions)
|
||
```
|
||
|
||
### `FileNode(name, mode, options...)`
|
||
|
||
Declares a file node. Options provide handlers, metadata, ownership, aliases, and help text:
|
||
|
||
```go
|
||
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.
|
||
|
||
```go
|
||
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:
|
||
|
||
```sh
|
||
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
|
||
|
||
```go
|
||
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.
|
||
|
||
```go
|
||
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. |