Skip to content

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:

  1. Start a server process with custom configuration
  2. Mount it at a path of their choosing
  3. Unmount and remount it elsewhere
  4. 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: SrvCapable follows the ExecCapable pattern. 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 guard isSrvCapable(fs) provides runtime detection.

File Protocol

srvFS implements the standard Fileserver protocol for shell discoverability:

OperationBehavior
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)
writeEPERM — posting is programmatic, not byte-oriented
mkdir/remove/rename/wstatEPERM

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, /srv entries are file descriptors — integers that serialize trivially as bytes. In fishbowl, Fileservers are JavaScript objects. You can’t serialize them through write(Uint8Array). Rather than inventing a serialization scheme, we use ProcContext callbacks — the same mechanism used for registerExec and catalogInstall.

Posting Mechanism

The posting mechanism follows the registerExec pattern exactly:

  1. image.ts creates srvFS at boot and mounts it at /srv
  2. Creates postServer/getServer/removeServer closures closing over the srvFS instance
  3. Kernel threads them onto ProcContext as optional methods
  4. The kernel has zero awareness of srvFS semantics — it just passes callbacks through
// In types.ts — ProcContext additions
postServer?: (name: string, server: Fileserver) => void
getServer?: (name: string) => Fileserver | undefined
removeServer?: (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 ProcContext

Decision: kernel stays ignorant. The kernel receives postServer/getServer/removeServer as opaque callbacks in KernelOpts, just like registerExec and catalogInstall. 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.mountssrvFS
WhenBuild/install timeRuntime
ConfigFixed at package definitionParameterized at process start
LifecycleLives for entire sessionControlled by posting process
Use caseStateless 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.