SrvFS — The Server Registry
A shared fileserver mounted at /srv at boot. Processes post Fileserver objects here for other processes to discover and mount. The fishbowl analog of Plan 9’s /srv.
Why /srv Exists
In Plan 9, /srv is a bulletin board where server processes post file descriptors. Other processes open those descriptors and mount them into their namespace. The server is already running and configured — mounting is separate from instantiation.
fishbowl needs the same separation. Without /srv, a package that provides a fileserver can only mount it at a fixed path via Extension.mounts at install time. There’s no way for a user to:
- Start a server process with custom configuration
- Mount it at a path of their choosing
- Unmount and remount it elsewhere
- Share a server across processes that weren’t spawned from the same parent
The fundamental problem: per-process namespaces mean a child process that creates a Fileserver can’t “give” it to its parent or to unrelated processes. /srv solves this — it’s a shared registry visible to all processes because it was mounted before any of them spawned.
Interface
srvFS implements both the standard Fileserver protocol and a SrvCapable side-channel for object references:
interface SrvCapable { getServer(name: string): Fileserver | undefined postServer(name: string, server: Fileserver): void removeServer(name: string): void}Decision:
SrvCapablefollows theExecCapablepattern. ExecCapable solved the same problem — storing JavaScript object references (BinFunction) that can’t be serialized as bytes — on memoryFS. SrvCapable does the same for Fileserver references. A type guardisSrvCapable(fs)provides runtime detection.
File Protocol
srvFS implements the standard Fileserver protocol for shell discoverability:
| Operation | Behavior |
|---|---|
readdir('/') | Lists posted server names as directory entries |
stat('/<name>') | { type: 'file', name } for posted entries |
open('/<name>') + read() | Returns UTF-8 string: "<name>\n" (the posted server’s name) |
write | EPERM — posting is programmatic, not byte-oriented |
mkdir/remove/rename/wstat | EPERM |
This means ls /srv shows what’s available. cat /srv/webcam could show status info. But the actual Fileserver object exchange goes through the programmatic API, not bytes.
Decision: posting is programmatic, not byte-oriented. In Plan 9,
/srventries are file descriptors — integers that serialize trivially as bytes. In fishbowl, Fileservers are JavaScript objects. You can’t serialize them throughwrite(Uint8Array). Rather than inventing a serialization scheme, we use ProcContext callbacks — the same mechanism used forregisterExecandcatalogInstall.
Posting Mechanism
The posting mechanism follows the registerExec pattern exactly:
image.tscreates srvFS at boot and mounts it at/srv- Creates
postServer/getServer/removeServerclosures closing over the srvFS instance - Kernel threads them onto ProcContext as optional methods
- The kernel has zero awareness of srvFS semantics — it just passes callbacks through
// In types.ts — ProcContext additionspostServer?: (name: string, server: Fileserver) => voidgetServer?: (name: string) => Fileserver | undefinedremoveServer?: (name: string) => void// In image.ts — boot-time wiring (pseudocode)const srv = srvFS()mounts.set('/srv', srv)
const postServer = (name: string, server: Fileserver): void => { srv.postServer(name, server)}const getServer = (name: string): Fileserver | undefined => { return srv.getServer(name)}const removeServer = (name: string): void => { srv.removeServer(name)}// Pass to KernelOpts, threaded onto every ProcContextDecision: kernel stays ignorant. The kernel receives
postServer/getServer/removeServeras opaque callbacks in KernelOpts, just likeregisterExecandcatalogInstall. It threads them onto ProcContext. It has no idea what they do. This preserves the principle that the kernel routes I/O and manages processes — nothing more.
Lifecycle
- Posted servers persist until explicitly removed or the system shuts down
- Server processes clean up via
proc.removeServer?.('name')before exit or in a SIGTERM handler - For service-supervised servers, the supervisor handles cleanup on stop and re-posting on restart
- If a posted server’s backing process dies without cleanup, the entry becomes stale — reads will fail with whatever error the orphaned Fileserver produces (this matches Plan 9 semantics where a dead server’s fd returns errors)
Mount Flow
webcam-srv bin mount bin user process | | | | 1. createWebcamFS() | | | 2. proc.postServer('webcam', fs) | | | → srvFS stores reference | | | | | | 3. proc.getServer('webcam') | | → returns Fileserver ref | | 4. proc.mount(server, '~/cam') | | → namespace entry created | | | | | | 5. cat ~/cam/frame.jpg | | | → namespace resolves | | | to webcamFS directly |After step 4, the data path goes directly from the user process to the webcam Fileserver. srvFS is not on the I/O path — it was only used for discovery. This matches Plan 9, where /srv is a rendezvous point, not a proxy.
Relationship to Extension.mounts
Extension.mounts and srvFS serve different use cases:
Extension.mounts | srvFS | |
|---|---|---|
| When | Build/install time | Runtime |
| Config | Fixed at package definition | Parameterized at process start |
| Lifecycle | Lives for entire session | Controlled by posting process |
| Use case | Stateless devices (/dev/clipboard) | Dynamic servers (9p, S3, webcam) |
Packages that provide simple, stateless fileservers should use Extension.mounts. Packages that provide configurable, lifecycle-managed servers should install a daemon bin that posts to /srv.