From 147b0ca1e4e00f66abb47fff3c543b853033f609 Mon Sep 17 00:00:00 2001 From: Calvin Date: Sun, 16 Aug 2026 20:52:12 -0400 Subject: Initial commit: svc supervisor, design docs, session transcript Existing work moved from /storage/vms/9front/svc, previously unversioned. Object files and linked binaries excluded via .gitignore. --- svc/doc/design.md | 437 +++++++++++++++++++++++++ svc/doc/inventory.md | 113 +++++++ svc/doc/todo.md | 101 ++++++ svc/lib/bootmark | 6 + svc/lib/cs | 5 + svc/lib/dns | 8 + svc/lib/ipconfig | 7 + svc/lib/ramdisk | 6 + svc/lib/timesync | 8 + svc/man/init | 655 +++++++++++++++++++++++++++++++++++++ svc/src/dat.h | 85 +++++ svc/src/fs.c | 241 ++++++++++++++ svc/src/init.c | 851 ++++++++++++++++++++++++++++++++++++++++++++++++ svc/src/mkfile | 9 + svc/termrc.work | 80 +++++ svc/test/svc/after | 5 + svc/test/svc/afterboth | 6 + svc/test/svc/badoneshot | 6 + svc/test/svc/cyclea | 5 + svc/test/svc/cycleb | 5 + svc/test/svc/missingdep | 5 + svc/test/svc/ramdisk | 6 + svc/test/svc/setup | 6 + 23 files changed, 2656 insertions(+) create mode 100644 svc/doc/design.md create mode 100644 svc/doc/inventory.md create mode 100644 svc/doc/todo.md create mode 100644 svc/lib/bootmark create mode 100644 svc/lib/cs create mode 100644 svc/lib/dns create mode 100644 svc/lib/ipconfig create mode 100644 svc/lib/ramdisk create mode 100644 svc/lib/timesync create mode 100644 svc/man/init create mode 100644 svc/src/dat.h create mode 100644 svc/src/fs.c create mode 100644 svc/src/init.c create mode 100644 svc/src/mkfile create mode 100755 svc/termrc.work create mode 100644 svc/test/svc/after create mode 100644 svc/test/svc/afterboth create mode 100644 svc/test/svc/badoneshot create mode 100644 svc/test/svc/cyclea create mode 100644 svc/test/svc/cycleb create mode 100644 svc/test/svc/missingdep create mode 100644 svc/test/svc/ramdisk create mode 100644 svc/test/svc/setup (limited to 'svc') 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 >[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 >[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 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//ctl 0664 hostowner:user start, stop, restart + /mnt/svcs//status 0444 anyone + /mnt/svcs//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/`. `/mnt/svcs//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 + / + 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//ctl +start start start +stop stop stop +restart restart restart +note - note +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 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 +#include +#include +#include +#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 start a service\n" + "stop stop a service\n" + "restart 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 , 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 +#include +#include +#include +#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 @@ +/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 -- cgit v1.2.3