summaryrefslogtreecommitdiff
path: root/svc/src/init.c
diff options
context:
space:
mode:
authorCalvin <calvinm@kissingerassoc.com>2026-08-16 20:52:12 -0400
committerCalvin <calvinm@kissingerassoc.com>2026-08-16 20:52:12 -0400
commit147b0ca1e4e00f66abb47fff3c543b853033f609 (patch)
treea388172fc16723c139deff1e45a74c3d1c567cc0 /svc/src/init.c
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.
Diffstat (limited to 'svc/src/init.c')
-rw-r--r--svc/src/init.c851
1 files changed, 851 insertions, 0 deletions
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);
+ }
+}