summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--svc/doc/design.md437
-rw-r--r--svc/doc/inventory.md113
-rw-r--r--svc/doc/todo.md101
-rw-r--r--svc/lib/bootmark6
-rw-r--r--svc/lib/cs5
-rw-r--r--svc/lib/dns8
-rw-r--r--svc/lib/ipconfig7
-rw-r--r--svc/lib/ramdisk6
-rw-r--r--svc/lib/timesync8
-rw-r--r--svc/man/init655
-rw-r--r--svc/src/dat.h85
-rw-r--r--svc/src/fs.c241
-rw-r--r--svc/src/init.c851
-rw-r--r--svc/src/mkfile9
-rwxr-xr-xsvc/termrc.work80
-rw-r--r--svc/test/svc/after5
-rw-r--r--svc/test/svc/afterboth6
-rw-r--r--svc/test/svc/badoneshot6
-rw-r--r--svc/test/svc/cyclea5
-rw-r--r--svc/test/svc/cycleb5
-rw-r--r--svc/test/svc/missingdep5
-rw-r--r--svc/test/svc/ramdisk6
-rw-r--r--svc/test/svc/setup6
24 files changed, 2663 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..50bae94
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+# 9front build artifacts
+*.6
+*.5
+*.8
+6.out
+5.out
+8.out
diff --git a/svc/doc/design.md b/svc/doc/design.md
new file mode 100644
index 0000000..343d23a
--- /dev/null
+++ b/svc/doc/design.md
@@ -0,0 +1,437 @@
+# init
+
+**init is the service manager. It provides svcfs if you want to interact with services.**
+
+Status: design. Nothing implemented yet.
+
+## Why
+
+A machine is a set of running services. That's all it is. 9front instead has
+three half-mechanisms that each know a piece of that and never talk to each
+other.
+
+`service=` in `plan9.ini` picks between `/rc/bin/termrc` and `/rc/bin/cpurc`.
+One word decides "what kind of machine is this."
+
+`/rc/bin/service/` is a directory of scripts named by port, where the enable bit
+is a `!` prefix on the filename:
+
+ /rc/bin/service/!tcp23 disabled
+ /rc/bin/service/tcp17019 enabled
+
+Everything else is a bare line in a shell script — `plumber`, `webfs`, `ndb/cs`,
+`ndb/dns`, `aux/timesync` — started once, owned by nobody, restarted never.
+
+So: nothing is supervised, nothing has state, nothing has logs, nothing has
+dependencies, and nothing can be managed remotely without a shell and an editor.
+
+A real example from this project. The serial control shell was one line in
+`termrc`, non-interactive:
+
+ rc </dev/eia0 >/dev/eia0 >[2=1] &
+
+One malformed command produced a syntax error, rc exited, and the machine was
+unreachable over serial for the rest of the session. The fix was to make it
+interactive and wrap it in a restart loop:
+
+ while(){ rc -i </dev/eia0 >/dev/eia0 >[2=1]; sleep 1 }&
+
+That line is a service manager for exactly one service. Every daemon on the box
+wants the same thing and none of them get it.
+
+## Roles are vocabulary
+
+"Terminal", "cpu server", "auth server", "file server" are not kernel concepts.
+The kernel does not know which one it is running. Each is a bundle of userspace
+programs:
+
+| Role | What it actually is |
+|---|---|
+| auth server | `auth/keyfs` + `auth/authsrv` on 567 |
+| cpu server | a listener on 17010, plus `exportfs` |
+| file server | `hjfs`/`cwfs`/`gefs` + a 9P listener on 564 |
+| terminal | a window system, if you want one |
+
+Nothing ever stopped one machine being all four. The words are the obstacle:
+they imply a machine *is* one thing, and that exactly one machine holds each
+role in a single administrative domain.
+
+We delete the words. Role is emergent and plural.
+
+## How it works
+
+`init` runs services. It:
+
+1. reads service definitions from `/lib/svc`
+2. starts the enabled ones in dependency order
+3. supervises them, restarting per policy
+4. serves `svcfs` — posted at `/srv/svc`, mounted at `/mnt/svcs`
+5. handles `reset` and `reexec`
+
+None of that depends on how it was started, and an instance can be run by
+anyone over any directory of services in its own namespace. Run as the program
+that brings a machine up, it does four more things: builds the base namespace,
+offers a rescue console, halts the root file server last, and carries out
+`halt` and `reboot`.
+
+**init is not pid 1 on 9front today.** Pid 1 is `rc` running `/bin/bootrc`,
+sitting in `Await`; init is an ordinary child — pid 143 on the machine this was
+checked against. Since the boot chain is being replaced anyway, whether init
+runs as pid 1 is a decision rather than an inherited fact, and nothing in this
+design requires it.
+
+`svcfs` is a namespace, not a program. There is no second daemon. It's what init
+looks like from outside, the same way `/proc` is what the kernel's process table
+looks like from outside.
+
+**The control plane is optional.** Services start at boot whether or not anything
+ever mounts `/mnt/svcs`. If posting the filesystem fails, init logs it and keeps
+supervising. A bug in the 9P layer must never be a boot failure.
+
+## A service
+
+`/lib/svc/dns`, in ndb format — the parser already exists and it's the same
+shape as factotum keys and `/lib/ndb/local`:
+
+ svc=dns
+ exec=/bin/ndb/dns
+ args=-r
+ needs=cs
+ ready=srv:dns
+ restart=always
+ enable=yes
+
+Ten attributes exist in total: `svc exec args needs ready restart ns user
+enable stop`. There are deliberately no tuning knobs — restart delays and
+readiness timeouts are constants in the code until something proves it needs to
+be configurable.
+
+An unknown attribute is an **error**, not a warning. ndb has no schema, so
+`exce=/bin/ndb/dns` parses fine as ndb and means nothing. Ignoring unknowns is
+how a typo silently does nothing forever.
+
+## Three shapes of service
+
+Not everything is a daemon that stays in the foreground, and the difference is
+declared rather than guessed:
+
+| Shape | Liveness is | On exit |
+|---|---|---|
+| foreground | the pid | restart per policy |
+| detaching | the `/srv` file | not a failure — don't restart |
+| oneshot | nothing, it's done | never restart |
+
+Detaching matters because the `postmountsrv` idiom forks a server proc and lets
+the original exit *on purpose*. A supervisor that assumes exit means death will
+restart-loop something that is running fine.
+
+Which is why **readiness and liveness are separate questions**. `ready=srv:dns`
+answers "is it up yet"; the same file answers "is it still up" for a service
+whose pid is already gone.
+
+## Namespaces and identity
+
+Each service gets `rfork(RFNAMEG)` and a namespace built from its `ns=` file via
+`newns` — the same machinery `cpu` already uses.
+
+This is the part that isn't a systemd transliteration. Two services on one
+machine can hold completely different views of the filesystem: different `/net`,
+different `/srv`, different roots. That is what would let one box participate in
+two grids at once, each with its own `/mnt/factotum` and its own identity —
+impossible today, because a machine has one hostowner, one `authdom` and one
+namespace.
+
+Identity comes from the capability device (`/dev/caphash`, `/dev/capuse`), the
+path `cpu` and `rx` already use, since Plan 9 has no setuid. That means init runs
+as hostowner and is security-critical. Not a detail to discover later.
+
+**Services consume namespaces; they never construct the shared one.** A service
+that runs `bind` inside its own namespace group affects nobody, and would appear
+to work while doing nothing. Base namespace construction belongs to init, before
+any service starts.
+
+## Talking to it
+
+The filesystem is the mechanism, commands are what people type. Both.
+
+ svc list services, their state, and why
+ svc start dns
+ svc restart dns
+ svc log dns
+ halt stop services, halt the fs, power off
+ shutdown alias for halt
+ reboot ... and restart the kernel
+ reset stop services, then start them again
+
+Commands are pure translation — a few lines that open a file and write a word.
+If `svc restart dns` can ever do something a write to `/mnt/svcs/dns/ctl`
+cannot, we've built two interfaces and they will drift.
+
+`reset` is worth naming because it's the cheap one: it's the stopping half
+followed by the starting half, both of which already exist. No new machinery.
+
+## Ctl files say what they accept
+
+The worst thing about Plan 9 ctl files is that they're write-only channels for
+magic strings. `/net/tcp/0/ctl`, `#S/sdctl`, `/dev/mousectl` — the only way to
+learn the verbs is to read source.
+
+So every ctl file here reads back its own command list:
+
+ % cat /mnt/svcs/dns/ctl
+ start
+ stop
+ restart
+ note <string> post a note to the process
+
+and every rejected write names the alternatives:
+
+ % echo frobnicate >/mnt/svcs/ctl
+ echo: write error: unknown command "frobnicate"; try
+ halt reboot reset start stop restart enable disable reexec
+
+This deviates from convention — most Plan 9 ctl files return nothing or return
+state on read. We can afford to spend the read on documentation because `status`
+carries the state. Given that the convention is the complaint, it's the right
+one to break.
+
+## State is ndb too
+
+`/proc/mdstat` on Linux is the canonical disease: positional, bracket-encoded,
+with an ASCII progress bar inside the data. Plan 9 does it too —
+`/net/ipifc/0/status` opens with genuine attribute/value pairs (`maxtu 1514
+sendra 0`) and then collapses into unlabelled positional columns the moment
+there's a list.
+
+So status output is ndb, not columns:
+
+ svc=dns state=running pid=231
+ svc=authsrv state=failed restarts=3 exit='cannot open /adm/keys'
+ svc=listen state=waiting needs=cs
+
+Self-describing, quoting handles the exit string, and fields can be added
+without breaking readers. `svc` with no arguments prints aligned columns for
+humans; the file stays ndb for programs.
+
+Nothing in the tree currently serves ndb — all 15 libndb consumers are readers
+of static config. This would be the first, which also means it shouldn't be
+called a convention until a second, genuinely different server carries it.
+
+## Configuration goes one way
+
+`/lib/svc` is the only source of truth. The filesystem accepts *verbs*, not
+configuration. No writing `exec=` into `/mnt/svcs/dns/args` and having init
+persist it back — that's two paths to one state, and they will disagree. It's
+also how the system already behaves: you configure an interface by writing
+`/net/ipifc/0/ctl`, not by editing `status`.
+
+`reload` re-reads `/lib/svc`, applies what parses, and reports what doesn't:
+
+ % echo reload >/mnt/svcs/ctl
+ echo: write error: /lib/svc/dns:4: unknown attribute "exce"
+
+One bad file never costs you the whole machine — same behaviour at boot.
+
+## Permissions
+
+init is the file server, so it checks the 9P attach identity itself. One
+mechanism covers local writes and remote mounts; there's no separate "is this
+allowed remotely" path to get wrong later.
+
+ /mnt/svcs/ctl 0644 hostowner halt, reboot, reset
+ /mnt/svcs/<n>/ctl 0664 hostowner:user start, stop, restart
+ /mnt/svcs/<n>/status 0444 anyone
+ /mnt/svcs/<n>/log 0640
+
+Note the ctl files are world *readable*. Restricting reads was the first
+thing tried and it immediately broke the rule above — reading a ctl file is
+how you learn what it accepts, which is no use if only one user may do it.
+Writing is the thing that wants restricting.
+
+This isn't cosmetic: if `/srv/svc` is exported, anyone who can mount it can halt
+the machine.
+
+## Shutdown
+
+`fshalt` is the proof that this is needed. It spends ninety lines rediscovering
+the system at shutdown — a hardcoded glob for every file server 9front has
+shipped (`cwfs*cmd`, `hjfs*cmd`, `ext4*cmd`, `gefs*cmd`, `fscons*`), plus
+hardcoded knowledge of each one's stop protocol (`echo halt` for most,
+`echo fsys all halt` and a `sleep 2` for fscons).
+
+Then the strangest part:
+
+ ramfs
+ cp /bin/echo /tmp
+ cp /bin/rc /tmp
+ # put this in a shell function so this rc script doesn't get read
+ # when it's no longer accessible
+ fn x { ... }
+
+It copies its own tools into a ramfs and hides its body in a function so rc
+won't re-read the script — because the shutdown logic lives *on the filesystem
+it is shutting down*. init has none of that problem: it's resident, it's process
+1, and it re-reads nothing.
+
+So shutdown is:
+
+1. stop services in reverse dependency order — `hangup` note, wait, then `kill`
+2. halt the adopted file server
+3. power off or reboot
+
+A wedged service must never stall shutdown, because the thing being protected is
+a clean filesystem. Timeout, hard kill, proceed.
+
+**Adopted services.** The root file server is started by `bootrc` before init
+exists. init isn't its parent, can't `await` it, can't `kill` it — it can only
+stop it the way fshalt does, by writing its `/srv` ctl file. So it's a second
+category: known but not owned, stop-only, always last. Each file server declares
+its own `stop=` rather than init carrying a table of how to kill everyone else.
+
+`fshalt` goes away.
+
+## Logging
+
+Service output is appended to `/log/<name>`. `/mnt/svcs/<name>/log` is a view of
+that file, and a blocking read at EOF gives live tailing.
+
+**Rotation is a non-goal.** Appending is twenty lines; rotation is a subsystem,
+and it isn't going in init. Files grow until something else deals with them.
+
+Two places this doesn't work, both the same problem: there's no filesystem yet,
+or there won't be one shortly. Early boot messages go to console. Once teardown
+starts, logging to disk stops and the rest goes to console.
+
+## Testing without booting
+
+Nothing about supervising services requires being process 1:
+
+ init -d /tmp/svc.test -s svc.test -m /mnt/svcs.test
+
+runs the real init as an ordinary user, supervising toy services, serving a test
+filesystem. No reboot, no risk.
+
+That only works if it's designed in, so it's a rule: **no hardcoded paths, no
+assumption of being pid 1, every path a flag.** The only parts needing a real
+boot are base namespace construction, the rescue console, and adopted-fs
+shutdown — which is the argument for keeping those three thin and separable.
+
+This is where recoverability actually comes from, not from splitting init into
+two programs.
+
+## Implementation
+
+C, and it isn't close: the program that brings a machine up should need nothing but libc at runtime.
+
+- **lib9p** — the file server. `threadpostmountsrv`, fill in a `Srv`.
+- **libndb** — service definitions and status output.
+- **libauth** — `newns`.
+- **libthread** — `threadwaitchan()` hands you a channel of `Waitmsg*`, so the
+ main loop is literally a select over "a child exited" and "a 9P request
+ arrived." The concurrency model matches the problem.
+
+Roughly 1500–2500 lines. Small enough to hold in your head, which for
+something this hard to restart is the point.
+
+Services written in rc are just `exec=/bin/rc args=/bin/foo`. **An rc service
+script must `exec` its final program, not background it** — otherwise init
+supervises rc, rc exits immediately, and the real work is orphaned.
+
+## Getting there
+
+Staged because you need a working machine tomorrow, not because the end state
+keeps any of it. First: init starts `svcfs` and supervises a handful of services
+alongside the existing `termrc`, fully reversible. Then service definitions
+replace `/rc/bin/service/` and the loose daemon lines. Then `termrc`, `cpurc`
+and `service=` are deleted.
+
+## Open questions
+
+- **`reexec` state handover.** Init replacing itself without dropping services
+ means supervision state survives the exec — handed over in memory, or
+ re-derived from `/proc` and `/srv`. The genuinely hard part.
+- **Detaching liveness.** Plan 9 has no file-change notification, so noticing
+ that a detached service's `/srv` file vanished means polling, or only
+ noticing on a `status` read.
+- **Who restarts init?** If init dies, nothing catches it. Confirm `bootrc`
+ honours `init=` in `plan9.ini` — that's the escape hatch for a bad init.
+- **A machine-readable interface description.** An ndb file next to `ctl`
+ describing fields, actions, and which files block, so a generic client can
+ render a UI over any 9P server. Not part of init, but init is a reasonable
+ first carrier — and it shouldn't be frozen until a second server carries it
+ too.
+
+## Namespaces are the capability set
+
+This started as a federation feature and turned out to be the privilege
+mechanism, which is a better justification and changes the defaults.
+
+In Plan 9 there is no access control for devices beyond what is in your
+namespace. If `#S` is not bound, you cannot touch storage — not "you get
+permission denied", but there is no name to open. That is stronger than
+Unix, where `/dev/sda` exists for everyone and mode bits are the only guard.
+
+But termrc binds all nine kernel devices into `/dev` for everything:
+
+ for(i in P S f æ t L A J '$')
+ bind -qa '#'^$i /dev
+
+So today `timesync` — which talks to an NTP server — can reach raw disk and
+PCI config space. `#p` is worse: every process can walk `/proc`, read other
+processes' memory, and post notes to them, so any service can kill init.
+
+### Consequences for the design
+
+**No default namespace.** A service with no namespace declaration is a load
+error, like an unknown attribute. Inheriting by default means granting
+everything by default to services that asked for nothing.
+
+**Ship profiles so explicit is not verbose.** The base is ~40 lines and most
+services want most of it; hand-written namespaces per service would be
+copy-pasted and would drift.
+
+ /lib/ns/net #I #c #e #d — dials out, no disk, no /proc
+ /lib/ns/disk net plus #S
+ /lib/ns/draw net plus #i #m
+ /lib/ns/full everything termrc currently binds
+
+A service says `nsfile=/lib/ns/net` plus optional inline `ns=bind …` lines.
+
+**`full` will be abused, and that is survivable, because it is greppable.**
+`grep -l ns/full /lib/services/*` is a list of over-privileged services, which
+is a work queue. Inheritance produces no such list.
+
+### /srv is a hole
+
+Binding `#s` undoes most of the above. A service that can see `/srv` can
+`mount /srv/boot` and get the whole root filesystem back, write
+`/srv/hjfs.cmd` to halt the machine, or mount factotum and reach keys.
+
+Three layers, in order of strength:
+
+1. **Do not bind `#s`.** The only airtight one — the channels are not nameable.
+2. **`rfork(RFNOMNT)` before exec.** Plan 9's pledge: a one-way drop after
+ which the process cannot mount, bind or unmount. Default on; `nsmount=yes`
+ is the visible opt-out for services that genuinely mount things.
+3. **`user=`.** File permissions on whatever remains.
+
+RFNOMNT is not a wall. It blocks the `mount` call, not a process that opens
+`/srv/foo` and speaks 9P down the fd by hand. It stops accidents and casual
+escalation; only omitting `#s` stops intent.
+
+### Unresolved: how this interacts with everything else
+
+- **Providers of namespaces need mount privilege.** A service that imports a
+ remote `/net` for others must mount, and must post to `/srv` so dependents
+ can reach it — so the exception to rule 1 and the exception to rule 2 are
+ the same services. Circular and not thought through.
+- **Namespace changes do not propagate.** A provider's mount dies with its
+ process group, so `/srv` is the only rendezvous, which is the thing we are
+ trying to restrict.
+- **init's own bootstrap** predates all of this: it needs `/lib/services` and
+ `#s` before it can read a profile.
+- **`user=` ordering.** The capability device must still be in the namespace
+ at the moment we drop privilege, so profiles cannot omit `#¤` blindly.
+- **Adopted services** were started by bootrc in a namespace we never chose.
+- **Migration.** `cs`, `dns` and `timesync` work today with no namespace
+ declaration. Making it mandatory breaks them until profiles exist.
diff --git a/svc/doc/inventory.md b/svc/doc/inventory.md
new file mode 100644
index 0000000..a63907d
--- /dev/null
+++ b/svc/doc/inventory.md
@@ -0,0 +1,113 @@
+# What termrc and cpurc actually contain
+
+Taken from reading both scripts and from `ps` on a booted machine, which is
+the more reliable of the two — the scripts are full of conditionals that never
+fire.
+
+Kernel processes (0K, `Wakeme`/`Idle`: `pager`, `alarm`, `etherread4`,
+`iasata`, …) are not in scope. They are not started by anything in userspace.
+
+## Adopted — init can stop these but never starts them
+
+Started by `bootrc`, before init exists. No `exec=`, `adopt=yes`, stop only.
+
+| what | evidence | how to stop it |
+|---|---|---|
+| `hjfs` | pids 320–328, `/srv/hjfs.cmd` | `stop=write:/srv/hjfs.cmd:halt` |
+| `factotum` | pid 260, `/srv/factotum` | note |
+| `paqfs` | pid 8, serves the boot archive | note |
+
+`hjfs` is the one that must be stopped last, after everything using it.
+
+## Base namespace — init does these directly, they are not services
+
+None of this survives as a service, because a service runs in its own namespace
+group and so cannot change anyone else's.
+
+- `for(i in P S f æ t L A J '$') bind -qa '#'^$i /dev`
+- `mount -qb /srv/cons /dev`
+- binding the `mntgen` channels onto `/n`, `/mnt`, `/mnt/exportfs`
+- `mount /srv/factotum /mnt/factotum` and the `bind -q` after it
+
+## Services
+
+| service | evidence | ready | needs | notes |
+|---|---|---|---|---|
+| `slashn` | pid 11 | `srv:slashn` | | `mntgen` for `/n` |
+| `slashmnt` | pid 14 | `srv:slashmnt` | | `mntgen` for `/mnt` |
+| `mntexport` | pid 17 | `srv:mntexport` | | `mntgen` for `/mnt/exportfs` |
+| `kbdfs` | pids 70–74 | `exec` | | posts nothing in `/srv` |
+| `usbd` | pids 82–83 | `exec` | | from `nusbrc` |
+| `kb` | pids 91–92 | `exec` | `usbd` | from `nusbrc` |
+| `cs` | pid 372 | `srv:cs` | | |
+| `dns` | pid 446 | `srv:dns` | `cs` | |
+| `timesync` | pid 450 | `exec` | `dns` | needs a name to resolve |
+| `realemu` | pids 456–457 | `exec` | | vga real-mode calls |
+| `webfs` | pid 500 | `exec` | `dns` | |
+| `webcookies` | pid 497 | `exec` | | |
+| `plumber` | pids 503–504 | see below | | |
+| `ipconfig-ra6` | pid 439/440 | `exec` | | the lingering RA listener |
+
+Only on a machine that is meant to accept logins:
+
+| service | from | ready | needs |
+|---|---|---|---|
+| `listen` | `cpurc` | `exec` | `cs` |
+| `keyfs` | `cpurc`, auth branch | `srv:keyfs` | |
+| `authsrv` | `cpurc`, auth branch | `dial:tcp!*!567` | `keyfs` |
+
+## Oneshots — `ready=exit`
+
+- `diskparts`
+- `swap` (only when `/dev/sd*/swap` exists)
+- `ip/ipconfig -h $sysname ether $ether` — the DHCP one, as opposed to the RA
+ listener above, which stays
+- setting `/dev/sysname`
+- `screenrc`
+
+## What simply disappears
+
+- **The `service=` branch itself.** `termrc` and `cpurc` are 80% identical: the
+ same device binds, the same `mntgen` lines, the same `factotum` mount, the
+ same `cs`/`dns`/`diskparts`/`swap`. The difference is which of two nearly
+ identical scripts runs.
+- **The role detection block.** `cpurc` decides whether this machine is an
+ authentication server by comparing `$sysname` against the `auth` attribute in
+ ndb, and starts `keyfs` plus a different `listen` if it matches. That is the
+ archetype logic, in one `if`. It becomes: is `keyfs` enabled or not.
+- **`serviced=` selection** — `/cfg/$sysname/service`, `/cfg/default/service`,
+ `/rc/bin/service`, in that order. Replaced by `/lib/svc`.
+- **The `.local` and `/cfg/$sysname` hooks** — `cpurc.local`, `termrc.local`,
+ `/cfg/$sysname/cpurc`, `/cfg/$sysname/cpustart`. Four hook points that exist
+ because a shell script has no other way to be extended. A directory of
+ service files does not need them.
+- **`rm -f /env/i`, `rm -f /env/disk`, `rm -f /env/ether /env/addrs /env/addr`**
+ — these clean up after `for` loops and backquote assignments. They exist
+ only because this is a shell script, and vanish with it.
+- **`NPROC`, `prompt`, `fn term%`** — shell configuration that has no business
+ in system startup. Belongs in `profile`.
+- **`dontkill`** — a list of process names protected from `kill`. Worth
+ revisiting rather than porting: with a supervisor that knows what it started,
+ protecting things by name is the wrong shape.
+
+## Two problems this turned up
+
+**`plumber`'s `/srv` name is dynamic.** It posts `/srv/plumb.glenda.502` —
+user and pid baked into the name. `ready=srv:name` cannot express that, and
+neither can `stop=write:...`. Options: allow a glob in `ready=srv:`, accept
+`ready=exec` and lose the liveness check, or treat it as a general escape and
+add `ready=file:pattern`. Unresolved, and it will not be the only such server.
+
+**`ipconfig` appears twice with different lifetimes.** One does DHCP and exits
+(a oneshot); the other listens for router advertisements and stays (a service).
+They are the same binary with different arguments, so they must be two service
+files with different names. Fine, but it means service name and program name
+cannot be assumed to match — which the current implementation already allows,
+since `svc=` and `exec=` are separate.
+
+## Also worth noting
+
+There is no `aux/listen` running on this machine, because it booted as
+`service=terminal`. Every service above marked "only on a machine meant to
+accept logins" is absent purely because of one word in `plan9.ini` — which is
+the whole argument, visible in `ps`.
diff --git a/svc/doc/todo.md b/svc/doc/todo.md
new file mode 100644
index 0000000..5598ae3
--- /dev/null
+++ b/svc/doc/todo.md
@@ -0,0 +1,101 @@
+# TODO
+
+State as of the end of the second session. Ordered by what unblocks what,
+not by size.
+
+## Do first: kill the serial dependency
+
+Everything else is cheaper once this is done. The serial shell wedged three
+times in one session and is lossy, non-interactive, and hostile to quoting.
+
+- [ ] `/adm/keys` and a user: `auth/keyfs`, then `auth/changeuser glenda`
+- [ ] service files for `keyfs`, `authsrv` (listener on 567), `listen` (rcpu
+ on 17019). This is literally `svc enable keyfs authsrv` from the man page
+- [ ] add a 567 hostfwd to `run.sh` next to the existing 17019 one
+- [ ] client: build conterm (github.com/0intro/conterm, text-only, drivable
+ over a pipe) or use the already-installed drawterm interactively
+
+`tcp17019` runs `tlssrv -a /bin/rc -c server`, so it authenticates through
+factotum and needs the auth server to validate. `rc/bin/service.auth/` already
+ships `tcp567`. `run.sh` already forwards 17019.
+
+## Unblocked, mechanical
+
+- [ ] drop `svc=`; the filename is the service name
+- [ ] `.ndb` extension on service files, stripped to get the name
+- [ ] migrate more services per `inventory.md` — `plumber`, `webfs`,
+ `webcookies`, `kbdfs`, `usbd`, `realemu`
+- [ ] doc drift: `/log` → `/sys/log` in `design.md` and `man/init`, with the
+ reason (root is mounted without create permission)
+- [ ] doc drift: ctl permissions are `0644`, not `0600` — reads must be open
+ or ctl cannot document itself
+- [ ] record the session's gotchas in `CLAUDE.md`: non-interactive rc dies on
+ a syntax error and takes the serial with it; `pkill -f` kills the calling
+ shell even with the bracket trick; rc treats double quotes as literal
+ characters, so `|` inside them becomes a pipe
+
+## Needs a decision before code
+
+- [ ] **`stop` does not work for detaching services.** `Ksrv` services have no
+ pid init can signal, so `stopsvc` is a no-op and a later `restart` fails
+ with "another instance is running". Proposed: for `Ksrv` with no `stop=`,
+ remove the `/srv` entry, which hangs up the channel and a well-behaved
+ server exits. Untested.
+- [ ] **Dynamic `/srv` names.** `plumber` posts `plumb.glenda.502`, `rio` posts
+ `rio.glenda.1483` — user and pid in the name. `ready=srv:` cannot express
+ it and it is a pattern, not an exception. Needs a glob, a new `ready=`
+ form, or accepting `ready=exec` and losing liveness.
+- [ ] **Namespace profiles.** Ship `/lib/ns/{net,disk,draw,full}`, require
+ every service to name one, no default. Blocked on the item below.
+- [ ] **The provider circularity.** A service that provides a namespace to
+ others (an `import` of a remote `/net`, say) must be able to mount *and*
+ must post to `/srv` so dependents can reach it — so it is simultaneously
+ the exception to "do not bind `#s`" and to freezing the namespace. Not
+ resolved. Profiles built before this is settled get built twice.
+
+## Unimplemented attributes and features
+
+Parsed and ignored today: `ns`, `user`, `stop`, `adopt`.
+
+- [ ] `ns=` inline lines and `nsfile=`, via `newns`
+- [ ] `user=` via the capability device — note `#¤` must still be in the
+ namespace at the moment privilege is dropped
+- [ ] `stop=` — `note:`, `write:file:word`, `exec:` forms
+- [ ] `adopt=yes` for services init can stop but never started (the root file
+ server, `factotum`, `paqfs`)
+- [ ] `halt`, `reboot`, `reset` ctl verbs and commands
+- [ ] `reexec`
+- [ ] rescue console when services will not come up
+- [ ] base namespace construction by init, replacing the device-bind loop in
+ termrc
+
+## Environment
+
+- [ ] **The bootargs prompt needs a manual Enter every boot.** Listed as
+ unresolved in `CLAUDE.md`; it now blocks unattended testing, which makes
+ it worth actually fixing.
+- [ ] sshfs has to be remounted by hand after every reboot — a candidate for
+ being a service itself, gated on a `sshkey` oneshot
+- [ ] `9front-base.qcow2` is stale relative to `9front.qcow2`; overlays made by
+ `newvm.sh` lack the supervised serial shell and the rio change
+
+## Verification debts
+
+Claims the design rests on that have not been tested:
+
+- [ ] does `RFNOMNT` survive `exec`? That is the last step before a service
+ runs, and the plausible place for a flag to be cleared
+- [ ] does binding a single `/srv` entry (`bind #s/cs /srv/cs`) work as cleanly
+ as binding the directory? The fine-grained capability story depends on it
+- [ ] does `import` have a flag to post to `/srv`, or is `srvfs` needed?
+- [ ] does `bootrc` honour `init=` in `plan9.ini`? That is the escape hatch if
+ we ever make init the boot program and get it wrong
+
+## Deferred deliberately
+
+- The filesystem hierarchy rework. Until then, do not churn `/lib/svc` →
+ `/lib/services`; every path in init is a flag, so it is a one-line change
+ whenever the hierarchy lands.
+- `timesync` reaching an external NTP server. It fails on DNS resolution in a
+ NAT'd VM and is not worth chasing; the dependency machinery around it is
+ already proven.
diff --git a/svc/lib/bootmark b/svc/lib/bootmark
new file mode 100644
index 0000000..94a4fa8
--- /dev/null
+++ b/svc/lib/bootmark
@@ -0,0 +1,6 @@
+svc=bootmark
+ exec=/bin/echo
+ args=svcinit-ran-at-boot
+ ready=exit
+ restart=never
+ enable=yes
diff --git a/svc/lib/cs b/svc/lib/cs
new file mode 100644
index 0000000..519d5c0
--- /dev/null
+++ b/svc/lib/cs
@@ -0,0 +1,5 @@
+svc=cs
+ exec=/bin/ndb/cs
+ ready=srv:cs
+ restart=always
+ enable=yes
diff --git a/svc/lib/dns b/svc/lib/dns
new file mode 100644
index 0000000..82e7a73
--- /dev/null
+++ b/svc/lib/dns
@@ -0,0 +1,8 @@
+svc=dns
+ exec=/bin/ndb/dns
+ args=-r
+ ready=srv:dns
+ needs=cs
+ needs=ipconfig
+ restart=always
+ enable=yes
diff --git a/svc/lib/ipconfig b/svc/lib/ipconfig
new file mode 100644
index 0000000..7cb6e97
--- /dev/null
+++ b/svc/lib/ipconfig
@@ -0,0 +1,7 @@
+svc=ipconfig
+ exec=/bin/ip/ipconfig
+ args=ether
+ args=/net/ether0
+ ready=exit
+ restart=never
+ enable=yes
diff --git a/svc/lib/ramdisk b/svc/lib/ramdisk
new file mode 100644
index 0000000..49d177c
--- /dev/null
+++ b/svc/lib/ramdisk
@@ -0,0 +1,6 @@
+svc=ramdisk
+ exec=/bin/ramfs
+ args=-s
+ ready=srv:ramfs
+ restart=always
+ enable=yes
diff --git a/svc/lib/timesync b/svc/lib/timesync
new file mode 100644
index 0000000..eb1b3ca
--- /dev/null
+++ b/svc/lib/timesync
@@ -0,0 +1,8 @@
+svc=timesync
+ exec=/bin/aux/timesync
+ args=-n
+ args=pool.ntp.org
+ needs=dns
+ needs=ipconfig
+ restart=always
+ enable=yes
diff --git a/svc/man/init b/svc/man/init
new file mode 100644
index 0000000..1646649
--- /dev/null
+++ b/svc/man/init
@@ -0,0 +1,655 @@
+.TH INIT
+.SH NAME
+init, svc, halt, shutdown, reboot, reset \- run and control services
+.SH SYNOPSIS
+.B init
+[
+.B -d
+.I svcdir
+] [
+.B -l
+.I logdir
+] [
+.B -m
+.I mtpt
+] [
+.B -s
+.I srvname
+]
+.PP
+.B svc
+[
+.I cmd
+[
+.I name ...
+]]
+.PP
+.B halt
+.br
+.B shutdown
+.br
+.B reboot
+.br
+.B reset
+.SH DESCRIPTION
+.I Init
+runs the services described in
+.BR /lib/svc .
+It is not tied to booting; see
+.B AS SYSINIT
+below.
+.PP
+To interact with services, mount the filesystem
+.I init
+serves:
+.PP
+.EX
+mount /srv/svc /mnt/svcs
+.EE
+.PP
+This is optional. Services run whether or not anyone ever mounts it, and if
+.I init
+cannot post the filesystem it says so and carries on supervising. The control
+plane is for people, not for the machine.
+.PP
+Options are:
+.TP
+.BI -d " svcdir"
+Read service definitions from
+.I svcdir
+instead of
+.BR /lib/svc .
+.TP
+.BI -l " logdir"
+Write service output to
+.I logdir
+instead of
+.BR /log .
+.TP
+.BI -m " mtpt"
+Mount at
+.I mtpt
+instead of
+.BR /mnt/svcs .
+.TP
+.BI -s " srvname"
+Post at
+.BI /srv/ srvname
+instead of
+.BR /srv/svc .
+.PP
+Every path is an option because
+.I init
+must be runnable as an ordinary process for testing. See
+.B TESTING
+below.
+.SH CONFIGURATION IS FILES
+.I Init
+never writes
+.BR /lib/svc .
+Everything about how a service is configured, including whether it starts at
+boot, is in its file and is changed by editing that file. The filesystem
+described below accepts commands about what is running now; it does not accept
+configuration, and there is no exception to this.
+.PP
+The consequence worth knowing before it surprises you:
+.B stop
+does not survive a reboot. To stop a service from starting at boot, set
+.B enable=no
+in its file. To act on an edit without rebooting, use
+.BR reload .
+.SH COMMANDS
+These are small programs that open a file and write a word to it. Anything they
+do can be done by hand; see
+.B THE SERVICE FILESYSTEM
+below.
+.TP
+.B svc
+With no arguments, print every service, its state, and why it is in that state,
+in aligned columns.
+.TP
+.BI svc " cmd name ..."
+.I Cmd
+is
+.BR start ,
+.BR stop ,
+.BR restart ,
+.BR status ,
+or
+.BR log .
+.B Svc log
+prints what the service has written and then keeps printing as it writes more,
+until interrupted.
+.TP
+.B halt
+Stop all services, halt the file server, and power off.
+.B Shutdown
+is another name for it.
+.TP
+.B reboot
+As
+.BR halt ,
+but restart the kernel.
+.TP
+.B reset
+Stop all services and start them again. The kernel keeps running and the file
+server is not halted.
+.PP
+To make this machine an authentication server, put
+.B enable=yes
+in the
+.B keyfs
+and
+.B authsrv
+files and then:
+.PP
+.EX
+svc reload
+svc start keyfs authsrv
+.EE
+.SH THE SERVICE FILESYSTEM
+.EX
+/mnt/svcs/
+ ctl control everything
+ status state of every service
+ <name>/
+ ctl control one service
+ status state of one service
+ args command line
+ env environment
+ ns namespace in effect
+ user identity it runs as
+ log output
+ pid
+.EE
+.PP
+Every file except the two
+.B ctl
+files is read-only. Configuration is changed by editing
+.BR /lib/svc ,
+never here.
+.PP
+Which file you write to says how far a command reaches. The two
+.B ctl
+files accept:
+.PP
+.EX
+ /mnt/svcs/ctl /mnt/svcs/<name>/ctl
+start start <name> start
+stop stop <name> stop
+restart restart <name> restart
+note - note <string>
+reload reload -
+reexec reexec -
+halt reboot reset yes -
+.EE
+.PP
+.B Start
+and
+.B stop
+affect only what is running now.
+.B Note
+posts a note to the service's process.
+.B Reload
+re-reads
+.BR /lib/svc .
+.B Reexec
+replaces
+.I init
+with a new
+.I init
+binary without stopping services or rebooting.
+.PP
+Reading a
+.B ctl
+file lists the commands it accepts, so you never have to guess or read source:
+.PP
+.EX
+% cat /mnt/svcs/dns/ctl
+start
+stop
+restart
+note <string> post a note to the process
+.EE
+.PP
+A write that is not understood fails and names the alternatives:
+.PP
+.EX
+% echo frobnicate >/mnt/svcs/ctl
+echo: write error: unknown command "frobnicate"; try
+ halt reboot reset start stop restart reload reexec
+.EE
+.PP
+.B Status
+is in
+.I ndb
+format rather than columns, so that fields can be added without breaking
+anything that reads it, and so that an exit message containing blanks needs no
+special case:
+.PP
+.EX
+% cat /mnt/svcs/status
+svc=dns state=running pid=231 enable=yes
+svc=authsrv state=failed restarts=3 exit='cannot open /adm/keys'
+svc=listen state=waiting needs=cs
+svc=ipconf state=done
+.EE
+.PP
+The states are:
+.TF starting
+.TP
+.B stopped
+Not running, and not trying to be.
+.TP
+.B waiting
+Wants to run, but something in its
+.B needs
+has not come up yet. The
+.B needs
+attribute in
+.B status
+says which.
+.TP
+.B starting
+Started, but not yet up according to its
+.B ready
+attribute.
+.TP
+.B running
+Up.
+.TP
+.B done
+A
+.B ready=exit
+service that finished successfully. It is not running and will not be started
+again.
+.TP
+.B failed
+Gave up. The
+.B exit
+attribute says why.
+.PP
+.B Pid
+is empty for a service that has no process of its own: a
+.B ready=srv:
+service whose process has exited by design, or one that has finished or not
+started.
+.PP
+Permissions are ordinary file permissions, checked by
+.I init
+against the identity of whoever attached, so the same rules apply to a local
+write and to a remote mount. By default the global
+.B ctl
+is writable only by the host owner, and a service's own
+.B ctl
+is writable by the host owner and by the user the service runs as. Note that if
+.B /srv/svc
+is exported, anyone who can mount it can halt the machine.
+.SH SERVICE FILES
+Each file in
+.B /lib/svc
+describes one service, in
+.I ndb
+format.
+.PP
+.EX
+svc=dns
+ exec=/bin/ndb/dns
+ args=-r
+ needs=cs
+ ready=srv:dns
+ restart=always
+ enable=yes
+.EE
+.PP
+The attributes are:
+.TF restart
+.TP
+.B svc
+The name. Must match the file name.
+.TP
+.B exec
+Program to run. Required unless
+.B adopt=yes
+is set.
+.TP
+.B args
+An argument. May be repeated, and repeated attributes are passed in the order
+they appear in the file.
+.TP
+.B env
+A
+.IB name = value
+pair for the environment. May be repeated.
+.TP
+.B needs
+Another service that must come up first. May be repeated. A dependency is
+satisfied when it reaches
+.B running
+or
+.BR done .
+This is ordering only: if a dependency later fails or is restarted, nothing
+happens to the services that named it. Cycles are reported by
+.BR reload .
+.TP
+.B ready
+How to tell the service has come up, and for one form, how to tell it is still
+up. See
+.B KINDS OF SERVICE
+below.
+.TP
+.B restart
+.BR never ,
+.B onfail
+(the default: restart only on a non-empty exit status), or
+.BR always .
+What is watched depends on
+.BR ready ,
+so see
+.B KINDS OF SERVICE
+before setting this.
+.TP
+.B stop
+How to stop it, when a
+.B hangup
+note will not do. One of:
+.RS
+.TP
+.BI note: string
+Post
+.I string
+as a note. The default is
+.BR note:hangup .
+.TP
+.IB write: file : word
+Write
+.I word
+to
+.IR file .
+This is how a file server that shuts down on a command to its
+.B /srv
+file says so, for example
+.BR write:/srv/hjfs.cmd:halt .
+.TP
+.BI exec: cmd
+Run
+.IR cmd .
+.RE
+.TP
+.B adopt
+.B yes
+for a service
+.I init
+did not start and cannot start: it has no
+.BR exec ,
+.I init
+is not its parent, and it can only be stopped. The root file server is the
+case this exists for. An adopted service needs a
+.B ready
+attribute so that
+.I init
+can tell whether it is there, and a
+.B stop
+attribute so that
+.I init
+can shut it down.
+.TP
+.B ns
+A namespace file, in the format of
+.IR namespace .
+.TP
+.B user
+The identity to run as.
+.TP
+.B enable
+.B yes
+to start at boot. Only a person writes this;
+.I init
+never does.
+.PP
+An attribute that is not in this list is an error. Nothing here is a hint:
+.B exce=/bin/ndb/dns
+is valid ndb and means nothing, so ignoring unknown attributes would let a typo
+do nothing quietly forever.
+.PP
+There is nothing else to set. The times involved are fixed: a service gets 30
+seconds to become ready, 5 seconds to stop before it is killed, and 1 second
+between restarts; a service that restarts more than 5 times in 60 seconds is
+marked
+.BR failed .
+These are constants until something demonstrates they need to be otherwise.
+.PP
+.B Reload
+applies every file that parses and leaves the rest alone, so one bad file never
+costs you the machine. The write fails with a summary, and each service that
+could not be loaded says why in its own
+.BR status :
+.PP
+.EX
+% echo reload >/mnt/svcs/ctl
+echo: write error: 2 of 14 service files rejected; see status
+% grep 'state=failed' /mnt/svcs/status
+svc=dns state=failed exit='/lib/svc/dns:4: unknown attribute "exce"'
+.EE
+.SH KINDS OF SERVICE
+Not everything stays in the foreground, and
+.I init
+does not guess. The
+.B ready
+attribute says how to tell a service came up, and in one case what to watch
+afterwards.
+.TF srv:name
+.TP
+.B exec
+It stays in the foreground and is up as soon as it is started. Its process is
+watched, and
+.B restart
+applies to that process exiting. This is the default.
+.TP
+.BI srv: name
+It is up once
+.BI /srv/ name
+exists. Use this for a service that posts to
+.B /srv
+and lets the process you started exit, which many Plan 9 file servers do on
+purpose. For these the
+.B /srv
+file is watched and the process is not, so the process exiting is normal and
+never causes a restart;
+.B restart
+applies to the
+.B /srv
+file going away.
+.TP
+.BI dial: addr
+It is up once a dial of
+.I addr
+succeeds. This is a readiness test only: the process is still what is watched
+afterwards, exactly as for
+.BR exec ,
+and
+.I init
+does not keep dialling.
+.TP
+.B exit
+It runs once and finishes; it is not a daemon. Use this for setup that has to
+happen before something else starts. It reaches
+.B done
+rather than
+.BR running ,
+it satisfies
+.B needs
+by doing so, and it is never restarted \- a
+.B restart
+attribute on it is an error rather than something quietly ignored.
+.SH NAMESPACE AND USER
+Each service runs in its own namespace group. If it has an
+.B ns
+attribute that namespace is built from the named file; otherwise it inherits the
+namespace
+.I init
+is using. This is what lets two services on one machine hold entirely different
+views of the filesystem \- a different
+.BR /net ,
+a different
+.BR /srv ,
+a different
+.B /mnt/factotum
+and so different keys and a different identity.
+.PP
+Because a service has its own namespace, a service that runs
+.I bind
+changes nothing for anyone else. Services use namespaces; they do not build the
+shared one. The namespace everything starts from is built by
+.I init
+before any service runs.
+.PP
+A service with a
+.B user
+attribute is run under that identity using the capability device, since Plan 9
+has no setuid. This requires
+.I init
+to be running as the host owner; an
+.I init
+that is not cannot honour
+.B user
+and fails any service that asks for one, rather than silently running it as the
+wrong identity.
+.SH STARTING AND STOPPING
+Services marked
+.B enable=yes
+are started in dependency order, each waiting for the ones it
+.B needs
+to reach
+.B running
+or
+.BR done .
+.PP
+They are stopped in the reverse of that order. Each is stopped the way its
+.B stop
+attribute says, or by a
+.B hangup
+note; if it has not gone after 5 seconds it is killed. A stuck service never
+holds up the rest.
+.SH AS SYSINIT
+Nothing above depends on how
+.I init
+was started. An instance can be run by anyone, over any directory of services,
+in its own namespace \- to hold up the parts of an application, say, and
+redeploy them by discarding the namespace and starting again.
+.PP
+When it is the program that brings a machine up, it does four more things.
+.PP
+It builds the base namespace that everything else inherits. Services run in
+their own namespace groups and so cannot do this for one another.
+.PP
+It offers a shell on the console when the machine cannot be reached any other
+way.
+.PP
+It stops the adopted root file server last, after everything that might be using
+it.
+.PP
+It powers the machine off, or reboots it, which is what
+.B halt
+and
+.B reboot
+finally do.
+.SH LOGGING
+Everything a service writes to standard output or standard error is appended to
+.BI /log/ name\fR.
+.B /mnt/svcs/\fIname\fB/log
+is a view of that file; reading it at the end blocks until there is more, so it
+can be followed.
+.PP
+Nothing rotates these files. That is deliberate: appending is a few lines and
+rotating is a program, and it is not going in init.
+.PP
+Before there is a writable filesystem, and again once shutdown has started,
+there is nowhere to append to and messages go to the console instead.
+.SH TESTING
+Supervising services does not require being the program that boots the machine,
+so
+.I init
+can be run as an ordinary program against a directory of test services:
+.PP
+.EX
+init -d /tmp/svc.test -l /tmp/log.test -s svc.test -m /mnt/svcs.test
+.EE
+.PP
+Give
+.B -l
+as well as the rest, or the test instance appends to the real
+.BR /log .
+.PP
+What such an instance cannot exercise is the base namespace, the rescue console,
+halting the adopted root file server, and any service with a
+.B user
+attribute, since changing identity needs the host owner.
+.SH FILES
+.TF /mnt/svcs
+.TP
+.B /lib/svc
+service definitions
+.TP
+.B /mnt/svcs
+the service filesystem
+.TP
+.B /srv/svc
+where it is posted
+.TP
+.B /log
+service output
+.SH SOURCE
+.B /sys/src/cmd/init.c
+.br
+.B /sys/src/cmd/svc
+.SH SEE ALSO
+.IR ndb ,
+.IR namespace ,
+.IR cap ,
+.IR srv ,
+.IR rc
+.SH BUGS
+A service written in
+.I rc
+must
+.I exec
+its final program rather than starting it with
+.BR & .
+Otherwise
+.I init
+supervises the shell, the shell exits immediately, and the program it started is
+left running with nothing watching it.
+.PP
+There is no notification when a file changes, so a service watched by its
+.B /srv
+file is not noticed to have died until something looks. The entry itself is
+removed when the server dies, with or without an active mount held on it, so
+looking is enough; only the delay is at issue.
+.PP
+Nothing detects a service that is running but not answering. A wedged server
+keeps both its process and its
+.B /srv
+entry and so reads as
+.B running
+under every
+.B ready
+form. Only a real 9P transaction would show otherwise, and
+.I init
+does not make one.
+.PP
+.B Reexec
+keeps services running but not mounts:
+anyone who already has
+.B /srv/svc
+mounted holds fids that cannot survive the exec, and has to mount it again.
+.PP
+On a machine that cannot be powered off,
+.B halt
+stops everything, says so, and leaves the machine running.
+.PP
+Nothing restarts
+.I init
+if it dies.
+.PP
+There is no resource control of any kind.
diff --git a/svc/src/dat.h b/svc/src/dat.h
new file mode 100644
index 0000000..f2cdec2
--- /dev/null
+++ b/svc/src/dat.h
@@ -0,0 +1,85 @@
+/*
+ * Shared between the supervisor (init.c) and the control
+ * filesystem (fs.c).
+ */
+
+enum
+{
+ /* states */
+ Sstopped,
+ Swaiting, /* wants to run; a needs= is not up yet */
+ Sstarting, /* started; not yet ready */
+ Srunning,
+ Sfailed,
+ Sdone, /* ready=exit service that finished cleanly */
+
+ /* kinds, from ready= */
+ Kexec, /* up as soon as started; watch the process */
+ Ksrv, /* up when /srv/x appears; watch that file */
+ Kdial, /* up when a dial succeeds; watch the process */
+ Kexit, /* oneshot; done when it exits cleanly */
+
+ Maxargs = 64,
+ Maxneeds = 32,
+ Maxrestarts = 5, /* within Restartwindow seconds */
+ Restartwindow = 60,
+ Restartwait = 1, /* seconds between restarts */
+ Readywait = 30, /* seconds to become ready */
+ Stopwait = 5, /* seconds before killing */
+ Tickms = 250, /* poll interval */
+};
+
+typedef struct Svc Svc;
+struct Svc
+{
+ char *name;
+ char *exec;
+ char *argv[Maxargs];
+ int argc;
+ char *env[Maxargs];
+ int envc;
+ char *restart;
+ int enable;
+
+ int kind;
+ char *readyarg;
+ char *needs[Maxneeds];
+ int nneeds;
+
+ int wanted; /* should it be running? */
+ int stopping; /* we asked it to go; not a crash */
+ int pid;
+ int state;
+ long tstart;
+ long tstop; /* when we asked it to stop */
+ long tretry; /* earliest time to start again */
+ int restarts;
+ long rtimes[Maxrestarts];
+ int rnext;
+ char *exits;
+
+ int mark; /* cycle detection */
+ Svc *next;
+};
+
+extern Svc *svcs;
+extern char *statename[];
+extern char *svcdir;
+extern char *logdir;
+extern int verbose;
+extern QLock statelock; /* covers svcs and the command queue */
+
+/* init.c */
+Svc* findname(char*);
+void startsvc(Svc*);
+void stopsvc(Svc*);
+char* statusline(Svc*); /* malloc'd ndb line, with newline */
+char* allstatus(void); /* malloc'd, every service */
+
+/* command queue: the filesystem asks, the supervisor acts */
+char* queuecmd(char *verb, char *name); /* nil ok, else error */
+void draincmds(void);
+int cmdspending(void);
+
+/* fs.c */
+void startfs(char *srvname, char *mtpt);
diff --git a/svc/src/fs.c b/svc/src/fs.c
new file mode 100644
index 0000000..63150fb
--- /dev/null
+++ b/svc/src/fs.c
@@ -0,0 +1,241 @@
+/*
+ * svcfs - the control filesystem init serves.
+ *
+ * This runs in a proc of its own, sharing memory with the supervisor,
+ * so everything it touches is under statelock. It never starts or
+ * stops anything itself: a service has to be a child of the supervisor
+ * for the supervisor to wait for it, so a write here is validated now
+ * and queued for the supervisor to carry out on its next tick.
+ */
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include "dat.h"
+
+enum
+{
+ Qctl,
+ Qstatus,
+ Qargs,
+ Qpid,
+ Qlog,
+};
+
+typedef struct Fref Fref;
+struct Fref
+{
+ int type;
+ Svc *svc; /* nil for the root ctl and status */
+};
+
+static char *rootctltext =
+ "start <name> start a service\n"
+ "stop <name> stop a service\n"
+ "restart <name> stop it, then start it\n"
+ "reset stop every service, then start them again\n";
+
+static char *svcctltext =
+ "start\n"
+ "stop\n"
+ "restart\n";
+
+static Fref*
+fref(int type, Svc *s)
+{
+ Fref *f;
+
+ f = mallocz(sizeof *f, 1);
+ if(f == nil)
+ sysfatal("out of memory");
+ f->type = type;
+ f->svc = s;
+ return f;
+}
+
+static void
+fsread(Req *r)
+{
+ Fref *f;
+ char *s;
+ char buf[64];
+ int fd, n;
+ char path[512];
+
+ f = r->fid->file->aux;
+ if(f == nil){
+ respond(r, "not a readable file");
+ return;
+ }
+
+ switch(f->type){
+ case Qctl:
+ readstr(r, f->svc == nil ? rootctltext : svcctltext);
+ break;
+
+ case Qstatus:
+ qlock(&statelock);
+ s = f->svc == nil ? allstatus() : statusline(f->svc);
+ qunlock(&statelock);
+ if(s == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ readstr(r, s);
+ free(s);
+ break;
+
+ case Qargs:
+ qlock(&statelock);
+ s = smprint("%s", f->svc->exec);
+ qunlock(&statelock);
+ if(s == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ readstr(r, s);
+ free(s);
+ break;
+
+ case Qpid:
+ qlock(&statelock);
+ if(f->svc->pid == 0)
+ buf[0] = '\0';
+ else
+ snprint(buf, sizeof buf, "%d\n", f->svc->pid);
+ qunlock(&statelock);
+ readstr(r, buf);
+ break;
+
+ case Qlog:
+ snprint(path, sizeof path, "%s/%s", logdir, f->svc->name);
+ fd = open(path, OREAD);
+ if(fd < 0){
+ /* no output yet is not an error */
+ r->ofcall.count = 0;
+ break;
+ }
+ seek(fd, r->ifcall.offset, 0);
+ n = read(fd, r->ofcall.data, r->ifcall.count);
+ close(fd);
+ if(n < 0){
+ responderror(r);
+ return;
+ }
+ r->ofcall.count = n;
+ break;
+
+ default:
+ respond(r, "not a readable file");
+ return;
+ }
+ respond(r, nil);
+}
+
+static void
+fswrite(Req *r)
+{
+ Fref *f;
+ char *buf, *err;
+ char *fld[3];
+ int nf;
+
+ f = r->fid->file->aux;
+ if(f == nil || f->type != Qctl){
+ respond(r, "not a writable file");
+ return;
+ }
+
+ buf = mallocz(r->ifcall.count+1, 1);
+ if(buf == nil){
+ respond(r, "out of memory");
+ return;
+ }
+ memmove(buf, r->ifcall.data, r->ifcall.count);
+ buf[r->ifcall.count] = '\0';
+
+ nf = tokenize(buf, fld, nelem(fld));
+ if(nf == 0){
+ free(buf);
+ respond(r, "no command");
+ return;
+ }
+
+ if(f->svc != nil){
+ /*
+ * A per-service ctl takes no name: which file you wrote to
+ * already said which service you meant.
+ */
+ if(nf != 1){
+ free(buf);
+ respond(r, "this ctl takes no arguments; "
+ "try start, stop or restart");
+ return;
+ }
+ err = queuecmd(fld[0], f->svc->name);
+ }else if(strcmp(fld[0], "reset") == 0)
+ err = queuecmd("reset", nil);
+ else if(nf != 2){
+ free(buf);
+ respond(r, "usage: start, stop or restart <service>, or reset");
+ return;
+ }else
+ err = queuecmd(fld[0], fld[1]);
+
+ free(buf);
+ if(err != nil){
+ respond(r, err);
+ return;
+ }
+ r->ofcall.count = r->ifcall.count;
+ respond(r, nil);
+}
+
+static Srv fs =
+{
+.read= fsread,
+.write= fswrite,
+};
+
+void
+startfs(char *srvname, char *mtpt)
+{
+ Svc *s;
+ File *d;
+ Tree *t;
+ char *me;
+
+ /*
+ * Files belong to whoever is running us, so that the host owner
+ * can write the ctl files. They are world readable: reading a
+ * ctl file is how you find out what it accepts, which is no use
+ * if only one user may do it. Writing is what wants restricting.
+ */
+ me = getuser();
+ if(me == nil)
+ me = "none";
+
+ t = alloctree(me, me, DMDIR|0555, nil);
+ if(t == nil)
+ sysfatal("alloctree: %r");
+ fs.tree = t;
+
+ createfile(t->root, "ctl", me, 0644, fref(Qctl, nil));
+ createfile(t->root, "status", me, 0444, fref(Qstatus, nil));
+
+ for(s = svcs; s != nil; s = s->next){
+ d = createfile(t->root, s->name, me, DMDIR|0555, nil);
+ if(d == nil){
+ fprint(2, "init: svcfs: create %s: %r\n", s->name);
+ continue;
+ }
+ createfile(d, "ctl", me, 0644, fref(Qctl, s));
+ createfile(d, "status", me, 0444, fref(Qstatus, s));
+ createfile(d, "args", me, 0444, fref(Qargs, s));
+ createfile(d, "pid", me, 0444, fref(Qpid, s));
+ createfile(d, "log", me, 0444, fref(Qlog, s));
+ }
+
+ postmountsrv(&fs, srvname, mtpt, MREPL);
+}
diff --git a/svc/src/init.c b/svc/src/init.c
new file mode 100644
index 0000000..7babe28
--- /dev/null
+++ b/svc/src/init.c
@@ -0,0 +1,851 @@
+/*
+ * init - run the services described in a directory of ndb files.
+ *
+ * Stage 3: load, start in dependency order, wait for readiness,
+ * supervise, log, and serve svcfs. No namespaces, no identity
+ * switching, no halt/reboot. Runs as an ordinary process; being
+ * pid 1 is not assumed anywhere.
+ */
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <ndb.h>
+#include "dat.h"
+
+char *statename[] =
+{
+[Sstopped] "stopped",
+[Swaiting] "waiting",
+[Sstarting] "starting",
+[Srunning] "running",
+[Sfailed] "failed",
+[Sdone] "done",
+};
+
+Svc *svcs;
+char *svcdir = "/lib/svc";
+/*
+ * Not /log: the root is mounted without create permission, so a new
+ * top level directory cannot be made from a running system. /sys/log
+ * already exists and is where Plan 9 keeps this.
+ */
+char *logdir = "/sys/log";
+int verbose;
+QLock statelock;
+
+static int tickpid;
+static int fsrunning;
+
+typedef struct Cmd Cmd;
+struct Cmd
+{
+ char verb[16];
+ char name[64];
+ Cmd *next;
+};
+static Cmd *cmdq;
+
+char *known[] =
+{
+ "svc", "exec", "args", "env", "restart", "enable", "ready", "needs",
+ "ns", "user", "stop", "adopt",
+ nil
+};
+
+char *unimpl[] = { "ns", "user", "stop", "adopt", nil };
+
+void*
+emalloc(ulong n)
+{
+ void *p;
+
+ p = mallocz(n, 1);
+ if(p == nil)
+ sysfatal("out of memory");
+ setmalloctag(p, getcallerpc(&n));
+ return p;
+}
+
+char*
+estrdup(char *s)
+{
+ char *p;
+
+ p = strdup(s);
+ if(p == nil)
+ sysfatal("out of memory");
+ return p;
+}
+
+int
+inlist(char **list, char *s)
+{
+ int i;
+
+ for(i = 0; list[i] != nil; i++)
+ if(strcmp(list[i], s) == 0)
+ return 1;
+ return 0;
+}
+
+Svc*
+findname(char *name)
+{
+ Svc *s;
+
+ for(s = svcs; s != nil; s = s->next)
+ if(strcmp(s->name, name) == 0)
+ return s;
+ return nil;
+}
+
+static Svc*
+findpid(int pid)
+{
+ Svc *s;
+
+ for(s = svcs; s != nil; s = s->next)
+ if(s->pid == pid)
+ return s;
+ return nil;
+}
+
+static void
+failsvc(Svc *s, char *why)
+{
+ s->state = Sfailed;
+ s->wanted = 0;
+ free(s->exits);
+ s->exits = estrdup(why);
+ fprint(2, "init: %s: %s\n", s->name, why);
+}
+
+static int
+parseready(Svc *s, char *v)
+{
+ if(strcmp(v, "exec") == 0)
+ s->kind = Kexec;
+ else if(strcmp(v, "exit") == 0)
+ s->kind = Kexit;
+ else if(strncmp(v, "srv:", 4) == 0){
+ s->kind = Ksrv;
+ s->readyarg = estrdup(v+4);
+ }else if(strncmp(v, "dial:", 5) == 0){
+ s->kind = Kdial;
+ s->readyarg = estrdup(v+5);
+ }else
+ return -1;
+ return 0;
+}
+
+static Svc*
+loadone(char *path, char *name)
+{
+ Ndb *db;
+ Ndbtuple *tu, *t;
+ Svc *s;
+
+ db = ndbopen(path);
+ if(db == nil){
+ fprint(2, "init: %s: %r\n", path);
+ return nil;
+ }
+ tu = ndbparse(db);
+ if(tu == nil){
+ fprint(2, "init: %s: no service defined\n", path);
+ ndbclose(db);
+ return nil;
+ }
+
+ s = emalloc(sizeof *s);
+ s->restart = "onfail";
+ s->state = Sstopped;
+ s->kind = Kexec;
+
+ for(t = tu; t != nil; t = t->entry){
+ if(!inlist(known, t->attr)){
+ fprint(2, "init: %s: unknown attribute %q\n", path, t->attr);
+ goto Bad;
+ }
+ if(inlist(unimpl, t->attr)){
+ fprint(2, "init: %s: %s not implemented yet, ignored\n",
+ path, t->attr);
+ continue;
+ }
+ if(strcmp(t->attr, "svc") == 0)
+ s->name = estrdup(t->val);
+ else if(strcmp(t->attr, "exec") == 0)
+ s->exec = estrdup(t->val);
+ else if(strcmp(t->attr, "args") == 0){
+ if(s->argc < Maxargs-2)
+ s->argv[++s->argc] = estrdup(t->val);
+ }else if(strcmp(t->attr, "env") == 0){
+ if(s->envc < Maxargs-1)
+ s->env[s->envc++] = estrdup(t->val);
+ }else if(strcmp(t->attr, "needs") == 0){
+ if(s->nneeds < Maxneeds)
+ s->needs[s->nneeds++] = estrdup(t->val);
+ }else if(strcmp(t->attr, "ready") == 0){
+ if(parseready(s, t->val) < 0){
+ fprint(2, "init: %s: ready=%q is not exec, exit, "
+ "srv:name or dial:addr\n", path, t->val);
+ goto Bad;
+ }
+ }else if(strcmp(t->attr, "restart") == 0)
+ s->restart = estrdup(t->val);
+ else if(strcmp(t->attr, "enable") == 0)
+ s->enable = strcmp(t->val, "yes") == 0;
+ }
+ ndbfree(tu);
+ ndbclose(db);
+
+ if(s->name == nil){
+ fprint(2, "init: %s: no svc= attribute\n", path);
+ goto Bad2;
+ }
+ if(strcmp(s->name, name) != 0){
+ fprint(2, "init: %s: svc=%s does not match file name\n", path, s->name);
+ goto Bad2;
+ }
+ if(s->exec == nil){
+ fprint(2, "init: %s: no exec= attribute\n", path);
+ goto Bad2;
+ }
+ if(strcmp(s->restart, "never") != 0 && strcmp(s->restart, "onfail") != 0
+ && strcmp(s->restart, "always") != 0){
+ fprint(2, "init: %s: restart=%q is not never, onfail or always\n",
+ path, s->restart);
+ goto Bad2;
+ }
+ if(s->kind == Kexit && strcmp(s->restart, "never") != 0){
+ fprint(2, "init: %s: restart=%s with ready=exit; a oneshot is "
+ "never restarted\n", path, s->restart);
+ goto Bad2;
+ }
+
+ s->argv[0] = strrchr(s->exec, '/');
+ if(s->argv[0] == nil)
+ s->argv[0] = s->exec;
+ else
+ s->argv[0]++;
+ s->argv[s->argc+1] = nil;
+
+ return s;
+
+Bad:
+ ndbfree(tu);
+ ndbclose(db);
+Bad2:
+ free(s);
+ return nil;
+}
+
+static int
+cyclic(Svc *s)
+{
+ Svc *d;
+ int i;
+
+ if(s->mark == 1)
+ return 1;
+ if(s->mark == 2)
+ return 0;
+ s->mark = 1;
+ for(i = 0; i < s->nneeds; i++){
+ d = findname(s->needs[i]);
+ if(d != nil && cyclic(d)){
+ fprint(2, "init: %s: needs= cycle through %s\n",
+ s->name, d->name);
+ return 1;
+ }
+ }
+ s->mark = 2;
+ return 0;
+}
+
+static void
+dropsvc(Svc *bad)
+{
+ Svc **p;
+
+ for(p = &svcs; *p != nil; p = &(*p)->next)
+ if(*p == bad){
+ *p = bad->next;
+ free(bad);
+ return;
+ }
+}
+
+static void
+checkdeps(void)
+{
+ Svc *s, *next;
+ int i;
+
+ for(s = svcs; s != nil; s = next){
+ next = s->next;
+ for(i = 0; i < s->nneeds; i++)
+ if(findname(s->needs[i]) == nil){
+ fprint(2, "init: %s: needs=%s, which is not a service\n",
+ s->name, s->needs[i]);
+ dropsvc(s);
+ break;
+ }
+ }
+ for(s = svcs; s != nil; s = next){
+ next = s->next;
+ if(cyclic(s))
+ dropsvc(s);
+ }
+}
+
+static void
+loadsvcs(void)
+{
+ Dir *d;
+ int fd, i, n;
+ char path[512];
+ Svc *s, **tail;
+
+ fd = open(svcdir, OREAD);
+ if(fd < 0)
+ sysfatal("open %s: %r", svcdir);
+ n = dirreadall(fd, &d);
+ close(fd);
+ if(n < 0)
+ sysfatal("read %s: %r", svcdir);
+
+ tail = &svcs;
+ for(i = 0; i < n; i++){
+ if(d[i].qid.type & QTDIR)
+ continue;
+ snprint(path, sizeof path, "%s/%s", svcdir, d[i].name);
+ s = loadone(path, d[i].name);
+ if(s == nil)
+ continue;
+ if(findname(s->name) != nil){
+ fprint(2, "init: %s: duplicate service name\n", path);
+ free(s);
+ continue;
+ }
+ *tail = s;
+ tail = &s->next;
+ }
+ free(d);
+ checkdeps();
+}
+
+static int
+toofast(Svc *s)
+{
+ long now, old;
+
+ now = time(nil);
+ old = s->rtimes[s->rnext];
+ s->rtimes[s->rnext] = now;
+ s->rnext = (s->rnext + 1) % Maxrestarts;
+ return old != 0 && now - old < Restartwindow;
+}
+
+static int
+srvexists(char *name)
+{
+ char path[256];
+
+ snprint(path, sizeof path, "/srv/%s", name);
+ return access(path, AEXIST) == 0;
+}
+
+static int
+dialok(char *addr)
+{
+ int fd;
+
+ fd = dial(addr, nil, nil, nil);
+ if(fd < 0)
+ return 0;
+ close(fd);
+ return 1;
+}
+
+static int
+isready(Svc *s)
+{
+ switch(s->kind){
+ case Ksrv:
+ return srvexists(s->readyarg);
+ case Kdial:
+ return dialok(s->readyarg);
+ }
+ return 1;
+}
+
+static int
+depsok(Svc *s)
+{
+ Svc *d;
+ int i;
+
+ for(i = 0; i < s->nneeds; i++){
+ d = findname(s->needs[i]);
+ if(d == nil)
+ return 0;
+ if(d->state != Srunning && d->state != Sdone)
+ return 0;
+ }
+ return 1;
+}
+
+void
+startsvc(Svc *s)
+{
+ int pid, fd, i;
+ char path[512];
+
+ if(s->pid != 0)
+ return;
+
+ switch(pid = rfork(RFPROC|RFFDG|RFNOTEG|RFENVG)){
+ case -1:
+ failsvc(s, "rfork failed");
+ return;
+ case 0:
+ snprint(path, sizeof path, "%s/%s", logdir, s->name);
+ fd = open(path, OWRITE);
+ if(fd < 0)
+ fd = create(path, OWRITE, 0644);
+ if(fd >= 0){
+ seek(fd, 0, 2);
+ dup(fd, 1);
+ dup(fd, 2);
+ if(fd > 2)
+ close(fd);
+ }
+ for(i = 0; i < s->envc; i++){
+ char *p;
+
+ p = strchr(s->env[i], '=');
+ if(p == nil)
+ continue;
+ *p = '\0';
+ putenv(s->env[i], p+1);
+ }
+ exec(s->exec, s->argv);
+ fprint(2, "init: exec %s: %r\n", s->exec);
+ exits("exec");
+ }
+ s->pid = pid;
+ s->wanted = 1;
+ s->tstart = time(nil);
+ free(s->exits);
+ s->exits = nil;
+ s->state = s->kind == Kexec ? Srunning : Sstarting;
+ if(verbose)
+ fprint(2, "init: started %s pid %d (%s)\n", s->name, pid,
+ statename[s->state]);
+}
+
+void
+stopsvc(Svc *s)
+{
+ s->wanted = 0;
+ s->tretry = 0;
+ if(s->pid == 0){
+ if(s->state != Sdone && s->state != Sfailed)
+ s->state = Sstopped;
+ return;
+ }
+ s->tstop = time(nil);
+ s->stopping = 1;
+ if(postnote(PNGROUP, s->pid, "hangup") < 0)
+ postnote(PNPROC, s->pid, "hangup");
+ if(verbose)
+ fprint(2, "init: stopping %s pid %d\n", s->name, s->pid);
+}
+
+/*
+ * The filesystem runs in another proc and cannot fork services itself,
+ * so it leaves them here. Errors are constant strings: several procs
+ * may be in here at once.
+ */
+char*
+queuecmd(char *verb, char *name)
+{
+ Cmd *c;
+ Svc *s;
+
+ if(strcmp(verb, "start") != 0 && strcmp(verb, "stop") != 0
+ && strcmp(verb, "restart") != 0 && strcmp(verb, "reset") != 0)
+ return "unknown command; try start, stop, restart or reset";
+
+ qlock(&statelock);
+ if(name != nil){
+ s = findname(name);
+ if(s == nil){
+ qunlock(&statelock);
+ return "no such service";
+ }
+ }
+ c = mallocz(sizeof *c, 1);
+ if(c == nil){
+ qunlock(&statelock);
+ return "out of memory";
+ }
+ strecpy(c->verb, c->verb+sizeof c->verb, verb);
+ if(name != nil)
+ strecpy(c->name, c->name+sizeof c->name, name);
+ c->next = cmdq;
+ cmdq = c;
+ qunlock(&statelock);
+ return nil;
+}
+
+int
+cmdspending(void)
+{
+ return cmdq != nil;
+}
+
+/* called with statelock held */
+void
+draincmds(void)
+{
+ Cmd *c, *next;
+ Svc *s;
+
+ c = cmdq;
+ cmdq = nil;
+ for(; c != nil; c = next){
+ next = c->next;
+ if(strcmp(c->verb, "reset") == 0){
+ for(s = svcs; s != nil; s = s->next)
+ if(s->pid != 0 || s->state == Srunning)
+ stopsvc(s);
+ for(s = svcs; s != nil; s = s->next)
+ if(s->enable){
+ s->wanted = 1;
+ s->state = Swaiting;
+ }
+ free(c);
+ continue;
+ }
+ s = findname(c->name);
+ if(s == nil){
+ free(c);
+ continue;
+ }
+ if(strcmp(c->verb, "start") == 0){
+ if(s->pid == 0 && s->state != Srunning){
+ s->wanted = 1;
+ s->tretry = 0;
+ s->state = Swaiting;
+ }
+ }else if(strcmp(c->verb, "stop") == 0)
+ stopsvc(s);
+ else if(strcmp(c->verb, "restart") == 0){
+ stopsvc(s);
+ s->wanted = 1;
+ s->state = Swaiting;
+ }
+ free(c);
+ }
+}
+
+static void
+startall(void)
+{
+ Svc *s;
+
+ for(s = svcs; s != nil; s = s->next)
+ if(s->enable){
+ s->wanted = 1;
+ s->state = Swaiting;
+ }
+}
+
+static int
+needtick(void)
+{
+ Svc *s;
+
+ if(fsrunning) /* commands may arrive at any time */
+ return 1;
+ for(s = svcs; s != nil; s = s->next){
+ if(s->state == Swaiting || s->state == Sstarting)
+ return 1;
+ if(s->state == Srunning && s->kind == Ksrv)
+ return 1;
+ if(s->pid != 0 && s->wanted == 0)
+ return 1; /* waiting for it to go */
+ }
+ return 0;
+}
+
+static void
+armtick(void)
+{
+ int pid;
+
+ if(tickpid != 0 || !needtick())
+ return;
+ switch(pid = rfork(RFPROC|RFNOTEG)){
+ case -1:
+ return;
+ case 0:
+ sleep(Tickms);
+ _exits(nil);
+ }
+ tickpid = pid;
+}
+
+/* called with statelock held */
+static void
+ontick(void)
+{
+ Svc *s;
+ long now;
+
+ now = time(nil);
+ for(s = svcs; s != nil; s = s->next){
+ /* a service we asked to stop that will not go */
+ if(s->pid != 0 && s->wanted == 0 && now - s->tstop >= Stopwait){
+ if(verbose)
+ fprint(2, "init: %s did not stop, killing\n", s->name);
+ postnote(PNGROUP, s->pid, "kill");
+ postnote(PNPROC, s->pid, "kill");
+ s->tstop = now;
+ }
+ switch(s->state){
+ case Swaiting:
+ if(s->wanted && now >= s->tretry && depsok(s) && s->pid == 0)
+ startsvc(s);
+ break;
+ case Sstarting:
+ if(isready(s)){
+ s->state = Srunning;
+ if(verbose)
+ fprint(2, "init: %s ready\n", s->name);
+ }else if(now - s->tstart >= Readywait)
+ failsvc(s, "not ready in time");
+ break;
+ case Srunning:
+ if(s->kind == Ksrv && s->wanted && !srvexists(s->readyarg)){
+ if(verbose)
+ fprint(2, "init: %s: /srv/%s went away\n",
+ s->name, s->readyarg);
+ s->pid = 0;
+ if(strcmp(s->restart, "never") == 0){
+ s->state = Sstopped;
+ s->wanted = 0;
+ break;
+ }
+ if(toofast(s)){
+ failsvc(s, "restarting too fast");
+ break;
+ }
+ s->restarts++;
+ s->state = Swaiting;
+ s->tretry = now + Restartwait;
+ }
+ break;
+ }
+ }
+}
+
+/* called with statelock held */
+static void
+reap(Waitmsg *w)
+{
+ Svc *s;
+ int failed;
+
+ s = findpid(w->pid);
+ if(s == nil)
+ return;
+ s->pid = 0;
+ failed = w->msg[0] != '\0';
+ free(s->exits);
+ s->exits = failed ? estrdup(w->msg) : nil;
+
+ if(verbose || failed)
+ fprint(2, "init: %s exited%s%s\n", s->name,
+ failed ? ": " : " cleanly", failed ? w->msg : "");
+
+ if(s->kind == Kexit){
+ s->wanted = 0;
+ s->state = failed ? Sfailed : Sdone;
+ return;
+ }
+ if(s->kind == Ksrv){
+ /*
+ * Its process exiting is normal: the service is whatever is
+ * behind /srv, and the tick watches that.
+ */
+ if(failed && s->state == Sstarting)
+ failsvc(s, w->msg);
+ return;
+ }
+ /*
+ * If we asked it to go, this is not a crash: do not count it
+ * against the restart rate, or restarting a service by hand a
+ * few times would mark it failed.
+ */
+ if(s->stopping){
+ s->stopping = 0;
+ s->state = s->wanted ? Swaiting : Sstopped;
+ return;
+ }
+ if(!s->wanted){
+ s->state = Sstopped;
+ return;
+ }
+ if(strcmp(s->restart, "always") == 0
+ || (strcmp(s->restart, "onfail") == 0 && failed)){
+ if(toofast(s)){
+ failsvc(s, "restarting too fast");
+ return;
+ }
+ s->restarts++;
+ s->state = Swaiting;
+ s->tretry = time(nil) + Restartwait;
+ return;
+ }
+ s->wanted = 0;
+ s->state = failed ? Sfailed : Sstopped;
+}
+
+/* called with statelock held */
+char*
+statusline(Svc *s)
+{
+ char buf[512], *p, *e;
+ Svc *d;
+ int i;
+
+ p = buf;
+ e = buf + sizeof buf;
+ p = seprint(p, e, "svc=%s state=%s", s->name, statename[s->state]);
+ if(s->pid != 0)
+ p = seprint(p, e, " pid=%d", s->pid);
+ if(s->restarts != 0)
+ p = seprint(p, e, " restarts=%d", s->restarts);
+ if(s->state == Swaiting)
+ for(i = 0; i < s->nneeds; i++){
+ d = findname(s->needs[i]);
+ if(d != nil && d->state != Srunning && d->state != Sdone)
+ p = seprint(p, e, " needs=%s", s->needs[i]);
+ }
+ if(s->enable)
+ p = seprint(p, e, " enable=yes");
+ if(s->exits != nil)
+ p = seprint(p, e, " exit=%q", s->exits);
+ seprint(p, e, "\n");
+ return strdup(buf);
+}
+
+/* called with statelock held */
+char*
+allstatus(void)
+{
+ Svc *s;
+ char *all, *one, *t;
+
+ all = strdup("");
+ if(all == nil)
+ return nil;
+ for(s = svcs; s != nil; s = s->next){
+ one = statusline(s);
+ if(one == nil)
+ break;
+ t = smprint("%s%s", all, one);
+ free(one);
+ if(t == nil)
+ break;
+ free(all);
+ all = t;
+ }
+ return all;
+}
+
+static void
+usage(void)
+{
+ fprint(2, "usage: init [-v] [-d svcdir] [-l logdir] "
+ "[-s srvname] [-m mtpt]\n");
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ Waitmsg *w;
+ char *srvname, *mtpt;
+
+ quotefmtinstall();
+ srvname = nil;
+ mtpt = nil;
+
+ ARGBEGIN{
+ case 'd':
+ svcdir = EARGF(usage());
+ break;
+ case 'l':
+ logdir = EARGF(usage());
+ break;
+ case 's':
+ srvname = EARGF(usage());
+ break;
+ case 'm':
+ mtpt = EARGF(usage());
+ break;
+ case 'v':
+ verbose++;
+ break;
+ default:
+ usage();
+ }ARGEND;
+
+ if(argc != 0)
+ usage();
+
+ loadsvcs();
+ if(svcs == nil)
+ sysfatal("no services loaded from %s", svcdir);
+
+ qlock(&statelock);
+ startall();
+ ontick();
+ qunlock(&statelock);
+
+ /*
+ * The control plane is optional: if it will not come up we say so
+ * and carry on supervising. A machine must not fail to boot over
+ * a filesystem nobody may ever mount.
+ */
+ if(srvname != nil || mtpt != nil){
+ startfs(srvname != nil ? srvname : "svc", mtpt);
+ fsrunning = 1;
+ }
+
+ for(;;){
+ armtick();
+ w = wait();
+ if(w == nil){
+ fprint(2, "init: wait: %r\n");
+ sleep(1000);
+ continue;
+ }
+ if(w->pid == tickpid){
+ tickpid = 0;
+ qlock(&statelock);
+ draincmds();
+ ontick();
+ qunlock(&statelock);
+ }else{
+ qlock(&statelock);
+ reap(w);
+ qunlock(&statelock);
+ }
+ free(w);
+ }
+}
diff --git a/svc/src/mkfile b/svc/src/mkfile
new file mode 100644
index 0000000..5e1326e
--- /dev/null
+++ b/svc/src/mkfile
@@ -0,0 +1,9 @@
+</$objtype/mkfile
+
+TARG=init
+OFILES=init.$O fs.$O
+HFILES=dat.h
+LIB=/$objtype/lib/lib9p.a /$objtype/lib/libthread.a /$objtype/lib/libndb.a /$objtype/lib/libbio.a
+BIN=/$objtype/bin
+
+</sys/src/cmd/mkone
diff --git a/svc/termrc.work b/svc/termrc.work
new file mode 100755
index 0000000..9c12bf0
--- /dev/null
+++ b/svc/termrc.work
@@ -0,0 +1,80 @@
+#!/bin/rc
+# this file is run if service=terminal
+TIMESYNCARGS=(-rLa1000000)
+
+# parallelism for mk
+NPROC=`{wc -l </dev/sysstat}
+
+# bind all likely devices
+for(i in P S f æ t L A J '$')
+ bind -qa '#'^$i /dev
+rm -f /env/i
+
+mount -qb /srv/cons /dev
+
+# serial shell for host-side control
+while(){ rc -i </dev/eia0 >/dev/eia0 >[2=1]; sleep 1 }&
+
+# mount points (if not done by bootrc already)
+>[2]/dev/null {
+mntgen -s slashn /n && chmod 666 /srv/slashn
+mntgen -s slashmnt /mnt && chmod 666 /srv/slashmnt
+mntgen -s mntexport /mnt/exportfs && chmod 666 /srv/mntexport}
+
+# now that /mnt exists, mount factotum
+mount /srv/factotum /mnt/factotum factotum
+bind -q /mnt/factotum/factotum /mnt/factotum
+
+# usb listener
+nusbrc
+
+# supervised services. cs, dns and timesync are defined in /lib/svc and
+# are started from here on; if svcinit will not run the machine still boots,
+# it just has none of them.
+if(test -e /amd64/bin/svcinit && test -d /lib/svc)
+ svcinit -s svc -m /mnt/svcs >>/sys/log/svcinit >[2=1] &
+
+# we do this before we have a name. we may need to do network
+# setup so that we can get a name.
+if(test -e /rc/bin/termrc.local)
+ . /rc/bin/termrc.local
+
+# cs is a service; see /lib/svc/cs
+sysname=`{cat /dev/sysname}
+if(~ $#sysname 0 || ~ $sysname ''){
+ sysname=cirno # default
+ echo -n $sysname >/dev/sysname
+}
+
+# set up any partitions
+diskparts
+
+# start up local swapping
+disk=`{ls /dev/sd*/swap >[2]/dev/null}
+if (! ~ $#disk 0)
+ swap $disk(1) >/dev/null >[2=1]
+rm -f /env/disk
+
+# machine specific startup (e.g., for devices not probed)
+if(test -e /cfg/$sysname/termrc)
+ . /cfg/$sysname/termrc
+
+# ip configuration is a service; see /lib/svc/ipconfig
+
+# dns is a service; see /lib/svc/dns
+
+# timesync is a service; see /lib/svc/timesync
+
+# setup mouse and graphics
+screenrc
+
+if(test -f /dev/apm)
+ aux/apm
+
+if(~ $terminal *reform*){
+ reform/pm
+ reform/audio
+}
+
+dontkill '^(9660srv|cfs|cs|cwfs.*|disk|dns|dossrv|ether|factotum|gefs|hjfs|ipconfig|kb|kfs|mntgen|paqfs|reboot|usbd|venti|wpa)$'
+
diff --git a/svc/test/svc/after b/svc/test/svc/after
new file mode 100644
index 0000000..bd0ef9d
--- /dev/null
+++ b/svc/test/svc/after
@@ -0,0 +1,5 @@
+svc=after
+ exec=/bin/sleep
+ args=3600
+ needs=ramdisk
+ enable=yes
diff --git a/svc/test/svc/afterboth b/svc/test/svc/afterboth
new file mode 100644
index 0000000..d154f6d
--- /dev/null
+++ b/svc/test/svc/afterboth
@@ -0,0 +1,6 @@
+svc=afterboth
+ exec=/bin/sleep
+ args=3600
+ needs=ramdisk
+ needs=setup
+ enable=yes
diff --git a/svc/test/svc/badoneshot b/svc/test/svc/badoneshot
new file mode 100644
index 0000000..d362ba3
--- /dev/null
+++ b/svc/test/svc/badoneshot
@@ -0,0 +1,6 @@
+svc=badoneshot
+ exec=/bin/echo
+ args=x
+ ready=exit
+ restart=always
+ enable=yes
diff --git a/svc/test/svc/cyclea b/svc/test/svc/cyclea
new file mode 100644
index 0000000..40b3bcb
--- /dev/null
+++ b/svc/test/svc/cyclea
@@ -0,0 +1,5 @@
+svc=cyclea
+ exec=/bin/sleep
+ args=1
+ needs=cycleb
+ enable=yes
diff --git a/svc/test/svc/cycleb b/svc/test/svc/cycleb
new file mode 100644
index 0000000..157a1dc
--- /dev/null
+++ b/svc/test/svc/cycleb
@@ -0,0 +1,5 @@
+svc=cycleb
+ exec=/bin/sleep
+ args=1
+ needs=cyclea
+ enable=yes
diff --git a/svc/test/svc/missingdep b/svc/test/svc/missingdep
new file mode 100644
index 0000000..d75e292
--- /dev/null
+++ b/svc/test/svc/missingdep
@@ -0,0 +1,5 @@
+svc=missingdep
+ exec=/bin/sleep
+ args=1
+ needs=nosuchthing
+ enable=yes
diff --git a/svc/test/svc/ramdisk b/svc/test/svc/ramdisk
new file mode 100644
index 0000000..49d177c
--- /dev/null
+++ b/svc/test/svc/ramdisk
@@ -0,0 +1,6 @@
+svc=ramdisk
+ exec=/bin/ramfs
+ args=-s
+ ready=srv:ramfs
+ restart=always
+ enable=yes
diff --git a/svc/test/svc/setup b/svc/test/svc/setup
new file mode 100644
index 0000000..f9202fb
--- /dev/null
+++ b/svc/test/svc/setup
@@ -0,0 +1,6 @@
+svc=setup
+ exec=/bin/echo
+ args=setup-done
+ ready=exit
+ restart=never
+ enable=yes