commit f0df6e3a39a3b9386b57b8338802f337aa3e38a0
parent 28c44b6b94a870f2942c37f9cfbae8b770595712
Author: cowmonk <cowmonk@based.pt>
Date: Tue, 23 Jun 2026 19:06:41 +0000
First commit
Diffstat:
| A | .gitignore | | | 9 | +++++++++ |
| M | LICENSE | | | 54 | +++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | Makefile | | | 62 | +++++++++++++++++++++++++++++++++++--------------------------- |
| M | README | | | 272 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
| A | arg.h | | | 65 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | config.def.h | | | 5 | ----- |
| M | config.mk | | | 18 | +++++++----------- |
| A | files/bin/poweroff | | | 3 | +++ |
| A | files/bin/reboot | | | 3 | +++ |
| A | files/rc.boot | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| A | files/rc.local | | | 4 | ++++ |
| A | files/rc.shutdown | | | 23 | +++++++++++++++++++++++ |
| A | files/rc.single | | | 6 | ++++++ |
| A | origo.c | | | 193 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | serva.c | | | 878 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | sinit.8 | | | 60 | ------------------------------------------------------------ |
| D | sinit.c | | | 91 | ------------------------------------------------------------------------------- |
| A | slog.c | | | 80 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | svc.c | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | util.c | | | 324 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | util.h | | | 30 | ++++++++++++++++++++++++++++++ |
21 files changed, 2082 insertions(+), 211 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,9 @@
+origo
+origo.o
+serva
+serva.o
+svc
+svc.o
+slog
+slog.o
+util.o
diff --git a/LICENSE b/LICENSE
@@ -1,6 +1,58 @@
MIT/X Consortium License
-© 2014-2015 Dimitris Papastamos <sin@2f30.org>
+Copyright (C) 2006, 2019 Rob Landley [rob@landley.net](mailto:rob@landley.net)
+Copyright (c) 2011 Connor Lane Smith [cls@lubutu.com](mailto:cls@lubutu.com)
+Copyright (c) 2011-2016 Dimitris Papastamos [sin@2f30.org](mailto:sin@2f30.org)
+Copyright (c) 2011 Hiltjo Posthuma [hiltjo@codemadness.org](mailto:hiltjo@codemadness.org)
+Copyright (c) 2011 Kamil Cholewiński [harry666t@gmail.com](mailto:harry666t@gmail.com)
+Copyright (c) 2011 pancake [pancake@youterm.com](mailto:pancake@youterm.com)
+Copyright (c) 2011 Random832 [random832@fastmail.us](mailto:random832@fastmail.us)
+Copyright (c) 2011 Rob Pilling [robpilling@gmail.com](mailto:robpilling@gmail.com)
+Copyright (c) 2012 Christoph Lohmann [20h@r-36.net](mailto:20h@r-36.net)
+Copyright (c) 2012 David Galos [galosd83@students.rowan.edu](mailto:galosd83@students.rowan.edu)
+Copyright (c) 2012 Kurt H. Maier [khm@sciops.net](mailto:khm@sciops.net)
+Copyright (c) 2012 Robert Ransom [rransom.8774@gmail.com](mailto:rransom.8774@gmail.com)
+Copyright (c) 2012 William Haddon [william@haddonthethird.net](mailto:william@haddonthethird.net)
+Copyright (c) 2013 Anselm R Garbe [anselm@garbe.us](mailto:anselm@garbe.us)
+Copyright (c) 2013 Christian Hesse [mail@eworm.de](mailto:mail@eworm.de)
+Copyright (c) 2013 David Galos [galosd83@students.rowan.edu](mailto:galosd83@students.rowan.edu)
+Copyright (c) 2013-2016 Dimitris Papapastamos [sin@2f30.org](mailto:sin@2f30.org)
+Copyright (c) 2013 dsp [dsp@2f30.org](mailto:dsp@2f30.org)
+Copyright (c) 2013 Federico G. Benavento [benavento@gmail.com](mailto:benavento@gmail.com)
+Copyright (c) 2013 Jakob Kramer [jakob.kramer@gmx.de](mailto:jakob.kramer@gmx.de)
+Copyright (c) 2013 Jesse Ogle [jesse.p.ogle@gmail.com](mailto:jesse.p.ogle@gmail.com)
+Copyright (c) 2013 Lorenzo Cogotti [miciamail@hotmail.it](mailto:miciamail@hotmail.it)
+Copyright (c) 2013 Markus Teich [markus.teich@stusta.mhn.de](mailto:markus.teich@stusta.mhn.de)
+Copyright (c) 2013 Markus Wichmann [nullplan@gmx.net](mailto:nullplan@gmx.net)
+Copyright (c) 2013 oblique [psyberbits@gmail.com](mailto:psyberbits@gmail.com)
+Copyright (c) 2013 Roberto E. Vargas Caballero [k0ga@shike2.com](mailto:k0ga@shike2.com)
+Copyright (c) 2013 s-p-k [mr.dwts@gmail.com](mailto:mr.dwts@gmail.com)
+Copyright (c) 2013 Truls Becken [truls.becken@gmail.com](mailto:truls.becken@gmail.com)
+Copyright (c) 2014 Adria Garriga [rhaps0dy@installgentoo.com](mailto:rhaps0dy@installgentoo.com)
+Copyright (c) 2014 Ari Malinen [ari.malinen@gmail.com](mailto:ari.malinen@gmail.com)
+Copyright (c) 2014 Brandon Mulcahy [brandon@jangler.info](mailto:brandon@jangler.info)
+Copyright (c) 2014 Carlos J. Torres [vlaadbrain@gmail.com](mailto:vlaadbrain@gmail.com)
+Copyright (c) 2014 Daniel Bainton [dpb@driftaway.org](mailto:dpb@driftaway.org)
+Copyright (c) 2014 Evan Gates [evan.gates@gmail.com](mailto:evan.gates@gmail.com)
+Copyright (c) 2014-2015 Greg Reagle [greg.reagle@umbc.edu](mailto:greg.reagle@umbc.edu)
+Copyright (c) 2014-2016 Hiltjo Posthuma [hiltjo@codemadness.org](mailto:hiltjo@codemadness.org)
+Copyright (c) 2014 Jan Tatje [jan@jnt.io](mailto:jan@jnt.io)
+Copyright (c) 2014 Jeffrey Picard [jeff@jeffreypicard.com](mailto:jeff@jeffreypicard.com)
+Copyright (c) 2014-2016 Laslo Hunhold [dev@frign.de](mailto:dev@frign.de)
+Copyright (c) 2014 Michael Forney [mforney@mforney.org](mailto:mforney@mforney.org)
+Copyright (c) 2014 Roberto E. Vargas Caballero [k0ga@shike2.com](mailto:k0ga@shike2.com)
+Copyright (c) 2014 Silvan Jegen [s.jegen@gmail.com](mailto:s.jegen@gmail.com)
+Copyright (c) 2014 Tuukka Kataja [stuge@xor.fi](mailto:stuge@xor.fi)
+Copyright (c) 2015 Dionysis Grigoropoulos [info@erethon.com](mailto:info@erethon.com)
+Copyright (c) 2015 Quentin Rameau [quinq@fifth.space](mailto:quinq@fifth.space)
+Copyright (c) 2015 Risto Salminen [ripejcp@gmail.com](mailto:ripejcp@gmail.com)
+Copyright (c) 2015 Tai Chi Minh Ralph Eastwood [tcmreastwood@gmail.com](mailto:tcmreastwood@gmail.com)
+Copyright (c) 2015 Wolfgang Corcoran-Mathe [wcm@sigwinch.xyz](mailto:wcm@sigwinch.xyz)
+Copyright (c) 2016 Eivind Uggedal [eivind@uggedal.com](mailto:eivind@uggedal.com)
+Copyright (c) 2016 Mattias Andrée [m@maandree.se](mailto:m@maandree.se)
+Copyright (c) 2026 xplshn [anto@xplshn.com.ar](mailto:anto@xplshn.com.ar) (https://github.com/xplshn)
+
+Copyright (c) 2026 cowmonk [cowmonk@based.pt](mailto:cowmonk@based.pt)
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
diff --git a/Makefile b/Makefile
@@ -1,39 +1,47 @@
+# Makefile: origo - minimal supervised init
+.POSIX:
+
include config.mk
-OBJ = sinit.o
-BIN = sinit
+SRC = origo.c serva.c svc.c slog.c util.c
+OBJ = $(SRC:.c=.o)
+BIN = origo serva svc slog
all: $(BIN)
-$(BIN): $(OBJ)
- $(CC) $(LDFLAGS) -o $@ $(OBJ) $(LDLIBS)
+origo: origo.o util.o
+ $(CC) $(LDFLAGS) -o $@ origo.o util.o
+
+serva: serva.o util.o
+ $(CC) $(LDFLAGS) -o $@ serva.o util.o
+
+svc: svc.o util.o
+ $(CC) $(LDFLAGS) -o $@ svc.o util.o
-$(OBJ): config.h
+slog: slog.o util.o
+ $(CC) $(LDFLAGS) -o $@ slog.o util.o
+
+%.o: %.c util.h arg.h
+ $(CC) $(CFLAGS) -c -o $@ $<
install: all
- mkdir -p $(DESTDIR)$(PREFIX)/bin
- cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
- mkdir -p $(DESTDIR)$(MANPREFIX)/man8
- sed "s/VERSION/$(VERSION)/g" < $(BIN).8 > $(DESTDIR)$(MANPREFIX)/man8/$(BIN).8
-
-uninstall:
- rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN)
- rm -f $(DESTDIR)$(MANPREFIX)/man8/$(BIN).8
-
-dist: clean
- mkdir -p sinit-$(VERSION)
- cp LICENSE Makefile README config.def.h config.mk sinit.8 sinit.c sinit-$(VERSION)
- tar -cf sinit-$(VERSION).tar sinit-$(VERSION)
- gzip sinit-$(VERSION).tar
- rm -rf sinit-$(VERSION)
+ install -Dm755 origo $(DESTDIR)$(PREFIX)$(SBIN_DIR)/origo
+ install -Dm755 serva $(DESTDIR)$(PREFIX)$(SBIN_DIR)/serva
+ install -Dm755 svc $(DESTDIR)$(PREFIX)$(BIN_DIR)/svc
+ install -Dm755 slog $(DESTDIR)$(PREFIX)$(BIN_DIR)/slog
+ install -d $(DESTDIR)/etc/ssv/boot
+ install -d $(DESTDIR)/etc/ssv/default
+
+install-files:
+ install -Dm755 files/rc.boot $(DESTDIR)/etc/rc.boot
+ install -Dm755 files/rc.local $(DESTDIR)/etc/rc.local
+ install -Dm755 files/rc.shutdown $(DESTDIR)/etc/rc.shutdown
+ install -Dm755 files/rc.single $(DESTDIR)/etc/rc.single
+ install -Dm755 files/bin/poweroff $(DESTDIR)$(PREFIX)/bin/poweroff
+ install -Dm755 files/bin/reboot $(DESTDIR)$(PREFIX)/bin/reboot
clean:
- rm -f $(BIN) $(OBJ) sinit-$(VERSION).tar.gz
-
-.SUFFIXES: .def.h
+ rm -f $(BIN) $(OBJ)
-.def.h.h:
- cp $< $@
+.PHONY: all install install-files clean
-.PHONY:
- all install uninstall dist clean
diff --git a/README b/README
@@ -1,26 +1,266 @@
-sinit - suckless init
-=====================
+RAdix iniT Suite
+================
-sinit is a simple init. It was initially based on
-Rich Felker's minimal init[1].
+RATS is an init suite forked from sinit to extend the features
+so it could be used standalone.
Why?
----
+Even though sinit is pretty good, unfortunately, I want service
+supervision, dependency management, logging, etc all in one instead
+of using separate utilities bootstrapped together. My old setup was
+using sinit + nitro with a bunch of shell scripting to handle things
+I felt that the init is okay to do.
-I wanted to get rid of Busybox init on my toy distro[2].
+General boot process
+--------------------
+kernel -> init=/sbin/origo
+ |- origo mounts /proc /sys /dev /dev/pts
+ |- origo runs /etc/rc.boot (which sources /etc/rc.local)
+ |- origo spawns serva
+ |- serva scans /etc/ssv/{boot,default,...}
+ |- serva starts services in (stage, name) alphabetical order
+ |- serva listens on /run/serva.sock
+ |- origo enters signal loop (sigwait)
+ * SIGCHLD - reap zombies, respawn serva if dead
+ * SIGUSR1 - poweroff
+ * SIGUSR2 - reboot
+ * SIGINT - reboot (CtrlAltDel)
+ * SIGTERM - single-user (rc.single or /bin/sh)
+
+
+Project Overview
+================
+
+origo: PID 1 - minimal init.
+----------------------------
+Mounts /proc /sys /dev /dev/pts, runs /etc/rc.boot,
+spawns+respawns serva, handles poweroff/reboot/single-user.
+Signal mapping (same as sinit):
+ - SIGINT -> reboot (CtrlAltDel)
+ - SIGUSR1 -> poweroff
+ - SIGUSR2 -> reboot
+ - SIGTERM -> single-user
+
+origo will do the following on shutdown/reboot:
+ 1. stopping serva (which stops all supervised services)
+ 2. kills all remaining processes
+ 3. remount the root read-only
+ 4. syncs the fs
+
+serva: a simple service supervisor.
+-----------------------------------
+Scans /etc/ssv/ for stage directories, forks run per service in
+(stage, name) alphabetical order, restarts with backoff.
+
+It is entirely possible to use either numerical or words for stages
+if desired.
+
+Pipes stdout/stderr to log (logger) if present (has to be executable).
+Listens on /run/serva.sock for svc control commands.
+
+Layout:
+ /etc/ssv/
+ <stage>/ stage directory (e.g. boot, default)
+ <service>/
+ run run executable (required; $1 = @-arg)
+ log logger executable (optional)
+ once touch-file: run once, don't supervise
+ noreset touch-file: run but don't restart on exit
+ down touch-file: don't auto-start; svc -u works
+ need/<name> hard dep: wait for <name> to be running
+ after/<name> soft hint: start after <name> if possible
+
+Symlink templates (names ending with @ are skipped):
+ /etc/ssv/boot/tty@/ template directory
+ /etc/ssv/boot/tty@tty1 symlink -> tty@; "tty1" passed as $1
+
+Service identifiers for svc:
+ sshd first match across all stages
+ boot/sshd specific stage + service
+
+
+NOTE: to the logger, serva sets the following environment variables:
+ - SVC_NAME : Service name (e.g. sshd)
+ - SVC_STAGE : Stage name (e.g. default)
+ - SVC_ARG : @ -arg if applicable (e.g. tty1 if getty@tty1)
+
+you can use these in the log script:
+
+ #!/bin/sh
+ exec slog -d "/var/log/$SVC_NAME-$SVC_ARG"
+
+More information on slog later.
+
+This design makes serva logger-agnostic, you are free to use whatever you like
+as long as you can pipe the output into your logger of choice.
+
+svc: control client for serva.
+------------------------------
+Talks to /run/serva.sock over a Unix socket.
+
+usage: svc [-udrkts] [-a] [name...]
+ -u bring up -d bring down -r restart
+ -k kill (SIGKILL) -t terminate -s status
+ -a all services (with -u or -d)
+
+slog: simple logging daemon.
+----------------------------
+Reads stdin, writes to <dir>/current, rotates on size.
+Rotated files: <dir>/0, <dir>/1, ..., <dir>/<n-1>
+
+example:
+ /var/log/sshd/current - active log
+ /var/log/sshd/0 - most recent rotation
+ /var/log/sshd/1
+ ...
+ /var/log/sshd/9 - oldest, deleted on next rotation
+
+usage: slog [-d dir] [-s bytes] [-n count]
+ -d log directory (default: .)
+ -s max size per file in bytes (default: 1048576 = 1MB)
+ -n max number of rotated files (default: 10)
+
+slog works perfectly fine outside of serva:
+
+ some-daemon 2>&1 | slog -d /var/log/some-daemon -s 524288 -n 5
+
+What does slog NOT do:
+ - No syslog protocol. It reads stdin, writes files. If you need syslog
+ (/dev/log socket), run a real syslogd as a service.
+ - No timestamps. The service's run script should add them if needed
+ (e.g. pipe throughts or use a logger that adds timestamps). Or modify
+ slog to prepend timestamps, it's a 5-line change. Most daemons already
+ provide timestamps anyways.
+ - No compression. Rotated files are plain text. Add a cron job to gzip
+ them if needed.
+
+
+Installed Files
+===============
+
+# bin/sbin can be set separately
+$(PREFIX)/sbin/origo # PID 1
+$(PREFIX)/sbin/serva # supervisor
+$(PREFIX)/bin/svc # control client
+$(PREFIX)/bin/slog # logging daemon
+
+Optional Installed Files
+------------------------
+All files below are example files, it's entirely possible to run a system with
+these example files with little to no changes.
+
+ /etc/rc.boot # (REQUIRED) first run before serva
+ /etc/rc.shutdown # (REQUIRED) run before full shutdown
+
+ /etc/rc.local # (optional) rc.boot should source this
+ /etc/rc.single # (optional) single-user script, else origo will run /bin/sh
+
+NOTE: Below are shell scripts.
+
+ files/bin/poweroff # sends SIGUSR1 to PID1
+ files/bin/reboot # sends SIGINT to PID1
+
+
+Requirements
+============
+
+ cc POSIX.1-2008-compliant C compiler (pcc, etc)
+ make POSIX make (aruu make, pdpmake, gmake, bmake)
+
+
+Building
+========
+
+Building this project is trivial, to build the suite:
+
+ make
+
+Configuration is in config.mk, you can override compile flags via make.
+
+NOTE: by default RATS will try to build statically, you can override this by passing `LDFLAGS=`.
+Basically, just make LDFLAGS empty.
+
+
+Installation
+============
+
+Follows regular make install conventions (DESTDIR and PREFIX).
+Additionally, you are able to set SBIN_DIR and BIN_DIR (/sbin, /bin).
+
+ make install # will install to /usr/local by default
+
+Install example rc.* files along with poweroff/reboot commands:
+
+ make install-files
+
+
+Source Layout
+=============
+
+ config.mk build configuration
+ Makefile posix (should work with any *competent* make impl)
+ files/
+ rc.* optional example configurations
+ bin/* optional scripts to use along with RATS
+ *.c c files
+ *.h headers
+
+
+TODO
+====
+
+manpages - someone else needs to do this or I'm going to die learning troff
+serva -- primarily with handling errrors coming from the services itself,
+ | or at least returning ERR when that's the case.
+ |
+ |- additionally, would be nice to return log/better status from
+ using svc when requesting the status of per service (e.g.
+ svc -s <svc_name>)
+user - still thinking on this one, I think it would be interesting to
+ have something like dinit. It might be an overreach for this project
+ but it can be used for "bloated" setups where users need dbus-session,
+ pipewire, etc. to run when booted.
+Makefile - it looks garbage, improving it would be nice
+
+
+Contribution
+============
+
+Sending patches is trivial, the main site of development is on the
+[stagit repo](https://git.daat.foo/rats/log.html).
+This means that all patches aren't sent via PRs on any of the mirrors.
+
+Run a quick `git format-patch HEAD~1` and send to *cowmonk@based.pt* with the
+subject head of `[PATCH] RATS`. Consider reading the manpages of `git-send-email`
+if you are interested in moving towards email for development.
+
+I *strongly* suggest pgp encrypting your email, in which case my public keys can be
+found on `keys.openpgp.org` or here:
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: 52B0 852C 5A4C 823C FAB7 420A 2F73 9BCA BDD5 B880
+Comment: cowmonk@based.pt
+
+xjMEaSJi8xYJKwYBBAHaRw8BAQdAEVP64bLVXCxZYH5rItmtvThKyTrRZ4HFCZ42
+IqFx+crNEGNvd21vbmtAYmFzZWQucHTCiQQTFggAGgQLCQgHAhUIAhYBAhkABYJp
+ImLzAp4BApsDACEJEC9zm8q91biAFiEEUrCFLFpMgjz6t0IKL3Obyr3VuIBOEQEA
++gIa9t3yKL3FAHg5VT2xV8M2VdLf1ZqaFlCPyGhXBZsA+QGGstK6+YsUbmP+H6I5
+r4AgLawGeB9IH7WC0w4dhTsKzjgEaSJi8xIKKwYBBAGXVQEFAQEHQMsmen1ddl9a
+ddXu4ktm8BATTlEy2k4zMzo6RnyRB+d4AwEIB8J4BBgWCAAJBYJpImLzApsMACEJ
+EC9zm8q91biAFiEEUrCFLFpMgjz6t0IKL3Obyr3VuIA9AQD/S/EL51hMSBI35Lr3
+RjuNFtqnlgYpfkMxgPvp2bWRY60A/2NhDRNUjY2wA4+mESlCIf94FVvqcfclnSZU
+A7wM/YAB
+=nZZp
+-----END PGP PUBLIC KEY BLOCK-----
-How?
-----
-There are 3 signals that sinit will act on.
+LICENSE
+=======
- SIGUSR1: powers off the machine.
- SIGINT: reboots the machine (or alternatively via ctrl-alt-del).
- SIGCHLD: reap children
+See ./LICENSE for details.
-To see how sinit integrates with the init scripts, then have
-a look at [3].
+Portions of this software are partially based on code written by Aruu authors and
+contributors, with portions within itself derived from various BSD/MIT-licensed
+open source projects.
-[1] https://gist.github.com/rofl0r/6168719
-[2] http://git.2f30.org/morpheus/
-[3] http://git.2f30.org/ports/tree/fs/
diff --git a/arg.h b/arg.h
@@ -0,0 +1,65 @@
+/*
+ * Copy me if you can.
+ * by 20h
+ */
+
+#ifndef ARG_H__
+#define ARG_H__
+
+extern char *argv0;
+
+/* use main(int argc, char *argv[]) */
+#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\
+ argv[0] && argv[0][0] == '-'\
+ && argv[0][1];\
+ argc--, argv++) {\
+ char argc_;\
+ char **argv_;\
+ int brk_;\
+ if (argv[0][1] == '-' && argv[0][2] == '\0') {\
+ argv++;\
+ argc--;\
+ break;\
+ }\
+ for (brk_ = 0, argv[0]++, argv_ = argv;\
+ argv[0][0] && !brk_;\
+ argv[0]++) {\
+ if (argv_ != argv)\
+ break;\
+ argc_ = argv[0][0];\
+ switch (argc_)
+
+/* Handles obsolete -NUM syntax */
+#define ARGNUM case '0':\
+ case '1':\
+ case '2':\
+ case '3':\
+ case '4':\
+ case '5':\
+ case '6':\
+ case '7':\
+ case '8':\
+ case '9'
+
+#define ARGEND }\
+ }
+
+#define ARGC() argc_
+
+#define ARGNUMF() (brk_ = 1, estrtonum(argv[0], 0, INT_MAX))
+
+#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\
+ ((x), abort(), (char *)0) :\
+ (brk_ = 1, (argv[0][1] != '\0')?\
+ (&argv[0][1]) :\
+ (argc--, argv++, argv[0])))
+
+#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\
+ (char *)0 :\
+ (brk_ = 1, (argv[0][1] != '\0')?\
+ (&argv[0][1]) :\
+ (argc--, argv++, argv[0])))
+
+#define LNGARG() &argv[0][0]
+
+#endif
diff --git a/config.def.h b/config.def.h
@@ -1,5 +0,0 @@
-/* See LICENSE file for copyright and license details. */
-
-static char *const rcinitcmd[] = { "/bin/rc.init", NULL };
-static char *const rcrebootcmd[] = { "/bin/rc.shutdown", "reboot", NULL };
-static char *const rcpoweroffcmd[] = { "/bin/rc.shutdown", "poweroff", NULL };
diff --git a/config.mk b/config.mk
@@ -1,12 +1,8 @@
-# sinit version
-VERSION = 1.1
+# config.mk: build configuration for RITS
-# paths
-PREFIX = /usr/local
-MANPREFIX = $(PREFIX)/share/man
-
-CC = cc
-LD = $(CC)
-CPPFLAGS =
-CFLAGS = -Wextra -Wall -Os
-LDFLAGS = -s -static
+CC = cc
+CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -O2
+LDFLAGS = -static
+PREFIX = /usr/local
+SBIN_DIR = /sbin
+BIN_DIR = /bin
diff --git a/files/bin/poweroff b/files/bin/poweroff
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+kill -s USR1 1
diff --git a/files/bin/reboot b/files/bin/reboot
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+kill -s INT 1
diff --git a/files/rc.boot b/files/rc.boot
@@ -0,0 +1,36 @@
+#!/bin/sh
+# EXAMPLE CONFIGURATION
+# rc.boot: early system bring-up, run by origo before serva starts.
+# origo already mounts: /proc /sys /dev /dev/pts
+
+# hostname
+hostname mybox
+
+# optional filesystems — uncomment as needed
+#mount -t tmpfs tmpfs /run
+#mount -t tmpfs tmpfs /tmp
+
+# /etc/fstab
+mount -a
+
+# some distros might need this for applications like steam
+#ln -sf /proc/self/fd /dev/fd
+
+# ensure /run exists for serva.sock
+mkdir -p /run
+
+# load keymap, set console font, etc.
+#loadkeys us
+#setfont Lat2-Terminus16
+
+# network — adjust to taste
+#ip link set lo up
+#ip link set eth0 up
+#sdhcp -i eth0
+
+# smdev/mdev
+#smdev -s
+#echo /bin/smdev > /proc/sys/kernel/hotplug
+
+# source local customisation
+[ -x /etc/rc.local ] && . /etc/rc.local
diff --git a/files/rc.local b/files/rc.local
@@ -0,0 +1,4 @@
+#!/bin/sh
+# rc.local: site-local boot customisation, sourced by rc.boot.
+
+# add your stuff here
diff --git a/files/rc.shutdown b/files/rc.shutdown
@@ -0,0 +1,23 @@
+#!/bin/sh
+# EXAMPLE CONFIGURATION
+# rc.shutdown: run by origo before sync+reboot/poweroff.
+# origo already handles:
+# 1. stopping serva (which stops all supervised services)
+# 2. killing all remaining processes
+# 3. remounting root read-only
+# 4. syncing filesystems
+#
+# Put things here that should clean up BEFORE the kill, e.g.:
+# - unmount network filesystems
+# - bring down network interfaces
+#
+
+# deactivate network
+#ip link set eth0 down
+#ip link set lo down
+
+# unmount network fs
+#umount /mnt/nfs
+
+# unmount everything (except root)
+#umount -a -r
diff --git a/files/rc.single b/files/rc.single
@@ -0,0 +1,6 @@
+#!/bin/sh
+# rc.single: single-user mode, run by origo on SIGTERM.
+# serva is already stopped. A shell is provided on /dev/console.
+
+echo "Entering single-user mode"
+exec /bin/sh
diff --git a/origo.c b/origo.c
@@ -0,0 +1,193 @@
+/* origo: PID 1 - minimal init */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* needed for usleep, kill, and sig*() */
+#endif
+
+#include "util.h"
+
+#include <sys/mount.h>
+#include <sys/reboot.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static sigset_t set;
+static pid_t serva_pid;
+static char *const serva_argv[] = { "serva", NULL };
+
+static pid_t
+spawn(const char *path, char *const argv[])
+{
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ eprintf("fork:");
+ if (pid == 0) {
+ sigprocmask(SIG_UNBLOCK, &set, NULL);
+ setsid();
+ setpgid(0, 0);
+ execvp(path, argv);
+ eprintf("execvp %s:", path);
+ }
+ return pid;
+}
+
+/* run_script: execute a shell script and wait for it */
+static void
+run_script(const char *path)
+{
+ char *const argv[] = { "sh", (char *)path, NULL };
+ pid_t pid;
+
+ if (access(path, F_OK) < 0)
+ return;
+ pid = spawn("/bin/sh", argv);
+ for (;;) {
+ if (waitpid(pid, NULL, 0) >= 0)
+ break;
+ if (errno != EINTR)
+ break;
+ }
+}
+
+/* mount_fs: bare-minimum kernel vfs */
+static void
+mount_fs(void)
+{
+ if (mount("proc", "/proc", "proc", 0, NULL) < 0)
+ weprintf("mount /proc:");
+ if (mount("sysfs", "/sys", "sysfs", 0, NULL) < 0)
+ weprintf("mount /sys:");
+ if (mount("devtmpfs", "/dev", "devtmpfs", 0, NULL) < 0)
+ weprintf("mount /dev:");
+ mkdir("/dev/pts", 0755);
+ if (mount("devpts", "/dev/pts", "devpts", 0, NULL) < 0)
+ weprintf("mount /dev/pts:");
+}
+
+/* reap: handle SIGCHLD, respawn serva if it died */
+static void
+reap(void)
+{
+ pid_t pid;
+
+ while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
+ if (pid == serva_pid)
+ serva_pid = spawn("serva", serva_argv);
+ }
+}
+
+/* do_shutdown: stop serva, run rc.shutdown, sync, reboot */
+static void
+do_shutdown(int how)
+{
+ int i;
+
+ if (serva_pid > 0) {
+ kill(serva_pid, SIGTERM);
+ for (i = 0; i < 50; i++) {
+ if (waitpid(serva_pid, NULL, WNOHANG) > 0)
+ break;
+ if (i == 40)
+ kill(serva_pid, SIGKILL);
+ usleep(100000);
+ }
+ serva_pid = 0;
+ }
+ run_script("/etc/rc.shutdown");
+
+ /* kill any remaining processes - exclude PID 1 */
+ kill(-1, SIGTERM);
+ usleep(500000);
+ kill(-1, SIGKILL);
+
+ /* remount / ro */
+ if (mount(NULL, "/", NULL, MS_REMOUNT | MS_RDONLY, NULL) < 0)
+ weprintf("remount / read-only:");
+
+ sync();
+ reboot(how);
+ eprintf("reboot failed:");
+}
+
+/* single_user: stop serva, run rc.single or /bin/sh, respawn serva */
+static void
+single_user(void)
+{
+ if (serva_pid > 0) {
+ kill(serva_pid, SIGTERM);
+ waitpid(serva_pid, NULL, 0);
+ serva_pid = 0;
+ }
+ kill(-1, SIGTERM);
+ usleep(500000);
+ kill(-1, SIGKILL);
+ reap();
+ if (access("/etc/rc.single", F_OK) == 0)
+ run_script("/etc/rc.single");
+ else
+ run_script("/bin/sh");
+ serva_pid = spawn("serva", serva_argv);
+}
+
+int
+main(void)
+{
+ int sig;
+ int fd;
+
+ if (getpid() != 1)
+ eprintf("must be run as PID 1");
+
+ /* reopen console */
+ fd = open("/dev/console", O_RDWR);
+ if (fd >= 0) {
+ dup2(fd, STDIN_FILENO);
+ dup2(fd, STDOUT_FILENO);
+ dup2(fd, STDERR_FILENO);
+ if (fd > STDERR_FILENO)
+ close(fd);
+ }
+
+ /* block all signals — we use sigwait */
+ sigfillset(&set);
+ sigprocmask(SIG_BLOCK, &set, NULL);
+
+ setenv("PATH", "/sbin:/bin:/usr/sbin:/usr/bin", 1);
+
+ mount_fs();
+
+ run_script("/etc/rc.boot");
+
+ /* start supervisor */
+ serva_pid = spawn("serva", serva_argv);
+
+ /* signal loop */
+ for (;;) {
+ if (sigwait(&set, &sig) < 0)
+ continue;
+ switch (sig) {
+ case SIGCHLD:
+ reap();
+ break;
+ case SIGINT:
+ case SIGUSR2:
+ do_shutdown(RB_AUTOBOOT);
+ break;
+ case SIGUSR1:
+ do_shutdown(RB_POWER_OFF);
+ break;
+ case SIGTERM:
+ single_user();
+ break;
+ }
+ }
+ return 0;
+}
diff --git a/serva.c b/serva.c
@@ -0,0 +1,878 @@
+/* serva: a simple service supervisor */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* needed for PATH_MAX */
+#endif
+
+#include "util.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#define SVC_DIR "/etc/ssv"
+#define SOCK_PATH "/run/serva.sock"
+#define MAX_SVCS 256
+#define MAX_BACKOFF 60
+#define MAX_DEPS 16
+#define MAX_DEPNAME 64
+
+struct Service {
+ char stage[NAME_MAX];
+ char name[NAME_MAX];
+ char dir[PATH_MAX];
+ char arg[NAME_MAX];
+ pid_t pid;
+ pid_t logpid;
+ int pipefd[2];
+ time_t last_start;
+ time_t next_restart;
+ int backoff;
+ int want_up;
+ int noreset;
+ int once;
+ int down;
+ int has_logger;
+ char need[MAX_DEPS][MAX_DEPNAME];
+ int nneed;
+ char after[MAX_DEPS][MAX_DEPNAME];
+ int nafter;
+};
+
+static struct Service svcs[MAX_SVCS];
+static size_t nsvcs;
+static int sigpipe[2];
+static volatile sig_atomic_t got_term;
+
+/* signal handler: wake self-pipe on SIGCHLD, flag SIGTERM */
+static void
+signal_handler(int sig)
+{
+ char c;
+
+ if (sig == SIGTERM)
+ got_term = 1;
+ c = 0;
+ write(sigpipe[1], &c, 1);
+}
+
+/* parse @-templates: "tty@tty1" -> arg "tty1" */
+static void
+parse_arg(const char *name, char *arg)
+{
+ char *at;
+
+ at = strchr(name, '@');
+ if (at)
+ strlcpy(arg, at + 1, NAME_MAX);
+ else
+ arg[0] = '\0';
+}
+
+/* has_file: check if <dir>/<name> exists */
+static int
+has_file(const char *dir, const char *name)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "%s/%s", dir, name);
+ return access(path, F_OK) == 0;
+}
+
+/* svc_cmp: sort by (stage, name) */
+static int
+svc_cmp(const void *a, const void *b)
+{
+ const struct Service *sa = a, *sb = b;
+ int c;
+
+ c = strcmp(sa->stage, sb->stage);
+ if (c != 0)
+ return c;
+ return strcmp(sa->name, sb->name);
+}
+
+/* endswith_at: true if name ends with '@' (template dir) */
+static int
+endswith_at(const char *name)
+{
+ size_t len;
+
+ len = strlen(name);
+ return len > 0 && name[len - 1] == '@';
+}
+
+/* find_service: lookup by "name" or "stage/name" */
+static struct Service *
+find_service(const char *id)
+{
+ size_t i;
+ const char *slash;
+ char stage[NAME_MAX];
+
+ slash = strchr(id, '/');
+ if (slash) {
+ size_t len;
+
+ len = slash - id;
+ if (len >= NAME_MAX)
+ return NULL;
+ memcpy(stage, id, len);
+ stage[len] = '\0';
+ for (i = 0; i < nsvcs; i++)
+ if (strcmp(svcs[i].stage, stage) == 0
+ && strcmp(svcs[i].name, slash + 1) == 0)
+ return &svcs[i];
+ } else {
+ for (i = 0; i < nsvcs; i++)
+ if (strcmp(svcs[i].name, id) == 0)
+ return &svcs[i];
+ }
+ return NULL;
+}
+
+/* deps_met: true if all need-deps are running */
+static int
+deps_met(struct Service *s)
+{
+ int i;
+ struct Service *dep;
+
+ for (i = 0; i < s->nneed; i++) {
+ dep = find_service(s->need[i]);
+ if (!dep || dep->pid <= 0)
+ return 0;
+ }
+ return 1;
+}
+
+/* has_after: true if s has after-hint for name */
+static int
+has_after(struct Service *s, const char *name)
+{
+ int i;
+
+ for (i = 0; i < s->nafter; i++)
+ if (strcmp(s->after[i], name) == 0)
+ return 1;
+ return 0;
+}
+
+/* adjust_after_order: swap services so after-hints are respected
+ within each stage. */
+static void
+adjust_after_order(void)
+{
+ int swapped;
+ size_t i, j;
+ struct Service tmp;
+
+ do {
+ swapped = 0;
+ for (i = 0; i < nsvcs; i++) {
+ for (j = i + 1; j < nsvcs; j++) {
+ if (strcmp(svcs[i].stage, svcs[j].stage) != 0)
+ continue;
+ if (has_after(&svcs[i], svcs[j].name)) {
+ tmp = svcs[i];
+ svcs[i] = svcs[j];
+ svcs[j] = tmp;
+ swapped = 1;
+ }
+ }
+ }
+ } while (swapped);
+}
+
+/* scan_deps: read need/ and after/ directories into service struct */
+static void
+scan_deps(struct Service *s)
+{
+ DIR *d;
+ struct dirent *e;
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "%s/need", s->dir);
+ d = opendir(path);
+ if (d) {
+ while ((e = readdir(d)) != NULL && s->nneed < MAX_DEPS) {
+ if (e->d_name[0] == '.')
+ continue;
+ strlcpy(s->need[s->nneed], e->d_name, MAX_DEPNAME);
+ s->nneed++;
+ }
+ closedir(d);
+ }
+
+ snprintf(path, sizeof(path), "%s/after", s->dir);
+ d = opendir(path);
+ if (d) {
+ while ((e = readdir(d)) != NULL && s->nafter < MAX_DEPS) {
+ if (e->d_name[0] == '.')
+ continue;
+ strlcpy(s->after[s->nafter], e->d_name, MAX_DEPNAME);
+ s->nafter++;
+ }
+ closedir(d);
+ }
+}
+
+static void
+scan_services(void)
+{
+ DIR *d, *sd;
+ struct dirent *e, *se;
+ char stagepath[PATH_MAX], svcpath[PATH_MAX], tmp[PATH_MAX];
+ struct stat st, sst;
+
+ d = opendir(SVC_DIR);
+ if (!d)
+ eprintf("opendir %s:", SVC_DIR);
+
+ nsvcs = 0;
+ while ((e = readdir(d)) != NULL && nsvcs < MAX_SVCS) {
+ if (e->d_name[0] == '.')
+ continue;
+
+ snprintf(stagepath, sizeof(stagepath), "%s/%s",
+ SVC_DIR, e->d_name);
+ if (stat(stagepath, &st) < 0)
+ continue;
+ if (!S_ISDIR(st.st_mode))
+ continue;
+
+ /* scan services within this stage */
+ sd = opendir(stagepath);
+ if (!sd)
+ continue;
+ while ((se = readdir(sd)) != NULL && nsvcs < MAX_SVCS) {
+ struct Service *s;
+
+ if (se->d_name[0] == '.')
+ continue;
+ if (endswith_at(se->d_name))
+ continue;
+
+ snprintf(svcpath, sizeof(svcpath), "%s/%s",
+ stagepath, se->d_name);
+ if (stat(svcpath, &sst) < 0)
+ continue;
+ if (!S_ISDIR(sst.st_mode))
+ continue;
+
+ /* run is required */
+ snprintf(tmp, sizeof(tmp), "%s/run", svcpath);
+ if (access(tmp, X_OK) < 0)
+ continue;
+
+ s = &svcs[nsvcs];
+ memset(s, 0, sizeof(*s));
+ strlcpy(s->stage, e->d_name, NAME_MAX);
+ strlcpy(s->name, se->d_name, NAME_MAX);
+
+ if (!realpath(svcpath, s->dir))
+ strlcpy(s->dir, svcpath, PATH_MAX);
+
+ parse_arg(s->name, s->arg);
+
+ snprintf(tmp, sizeof(tmp), "%s/log", s->dir);
+ s->has_logger = (access(tmp, X_OK) == 0);
+
+ s->noreset = has_file(s->dir, "noreset");
+ s->once = has_file(s->dir, "once");
+ s->down = has_file(s->dir, "down");
+
+ scan_deps(s);
+
+ s->pipefd[0] = -1;
+ s->pipefd[1] = -1;
+ nsvcs++;
+ }
+ closedir(sd);
+ }
+ closedir(d);
+
+ qsort(svcs, nsvcs, sizeof(*svcs), svc_cmp);
+ adjust_after_order();
+}
+
+static void
+start_service(struct Service *s)
+{
+ char runpath[PATH_MAX], logpath[PATH_MAX];
+ char *argv[3];
+ pid_t pid;
+
+ if (s->pid > 0)
+ return;
+
+ snprintf(runpath, sizeof(runpath), "%s/run", s->dir);
+ snprintf(logpath, sizeof(logpath), "%s/log", s->dir);
+
+ /* create logging pipe if needed */
+ if (s->has_logger && s->pipefd[0] < 0) {
+ if (pipe(s->pipefd) < 0)
+ weprintf("pipe:");
+ }
+
+ s->last_start = time(NULL);
+
+ /* fork service process */
+ pid = fork();
+ if (pid < 0) {
+ weprintf("fork:");
+ return;
+ }
+ if (pid == 0) {
+ sigset_t empty;
+
+ sigemptyset(&empty);
+ sigprocmask(SIG_SETMASK, &empty, NULL);
+ setsid();
+
+ if (chdir(s->dir) < 0)
+ eprintf("chdir %s:", s->dir);
+
+ if (s->pipefd[1] >= 0) {
+ dup2(s->pipefd[1], STDOUT_FILENO);
+ dup2(s->pipefd[1], STDERR_FILENO);
+ if (s->pipefd[1] > STDERR_FILENO)
+ close(s->pipefd[1]);
+ close(s->pipefd[0]);
+ }
+
+ setenv("SVC_NAME", s->name, 1);
+ setenv("SVC_STAGE", s->stage, 1);
+ if (s->arg[0])
+ setenv("SVC_ARG", s->arg, 1);
+
+ argv[0] = "run";
+ argv[1] = s->arg[0] ? s->arg : NULL;
+ argv[2] = NULL;
+
+ execv(runpath, argv);
+ eprintf("exec %s:", runpath);
+ }
+ s->pid = pid;
+
+ /* fork logger process if pipe was created */
+ if (s->pipefd[0] >= 0) {
+ pid = fork();
+ if (pid < 0) {
+ weprintf("fork logger:");
+ } else if (pid == 0) {
+ sigset_t empty;
+
+ sigemptyset(&empty);
+ sigprocmask(SIG_SETMASK, &empty, NULL);
+
+ if (chdir(s->dir) < 0)
+ eprintf("chdir %s:", s->dir);
+
+ dup2(s->pipefd[0], STDIN_FILENO);
+ if (s->pipefd[0] > STDIN_FILENO)
+ close(s->pipefd[0]);
+ close(s->pipefd[1]);
+
+ setenv("SVC_NAME", s->name, 1);
+ setenv("SVC_STAGE", s->stage, 1);
+ if (s->arg[0])
+ setenv("SVC_ARG", s->arg, 1);
+
+ execl(logpath, "log", NULL);
+ eprintf("exec %s:", logpath);
+ }
+ s->logpid = pid;
+
+ /* serva closes both pipe ends */
+ close(s->pipefd[0]);
+ close(s->pipefd[1]);
+ s->pipefd[0] = -1;
+ s->pipefd[1] = -1;
+ }
+}
+
+static void
+stop_service(struct Service *s)
+{
+ int i;
+
+ if (s->pid > 0) {
+ kill(s->pid, SIGTERM);
+ for (i = 0; i < 50; i++) {
+ if (waitpid(s->pid, NULL, WNOHANG) > 0)
+ break;
+ if (i == 40)
+ kill(s->pid, SIGKILL);
+ usleep(100000);
+ }
+ s->pid = 0;
+ }
+ if (s->logpid > 0) {
+ kill(s->logpid, SIGTERM);
+ waitpid(s->logpid, NULL, 0);
+ s->logpid = 0;
+ }
+ s->want_up = 0;
+ s->next_restart = 0;
+ s->backoff = 0;
+}
+
+/* stop_for_dep: stop but keep want_up - waiting for dependency */
+static void
+stop_for_dep(struct Service *s)
+{
+ int i;
+
+ if (s->pid > 0) {
+ kill(s->pid, SIGTERM);
+ for (i = 0; i < 50; i++) {
+ if (waitpid(s->pid, NULL, WNOHANG) > 0)
+ break;
+ if (i == 40)
+ kill(s->pid, SIGKILL);
+ usleep(100000);
+ }
+ s->pid = 0;
+ }
+ if (s->logpid > 0) {
+ kill(s->logpid, SIGTERM);
+ waitpid(s->logpid, NULL, 0);
+ s->logpid = 0;
+ }
+ s->next_restart = 0;
+ s->backoff = 0;
+ /* want_up stays 1 - will restart when deps are met */
+}
+
+static void
+restart_service(struct Service *s)
+{
+ stop_service(s);
+ s->want_up = 1;
+ start_service(s);
+}
+
+/* stop_cascade: stop all services whose need-deps are not running. */
+static void
+stop_cascade(void)
+{
+ size_t i, j;
+ int found;
+
+ do {
+ found = 0;
+ for (i = 0; i < nsvcs; i++) {
+ struct Service *s = &svcs[i];
+
+ if (s->pid <= 0 || !s->want_up)
+ continue;
+ for (j = 0; j < s->nneed; j++) {
+ struct Service *dep;
+
+ dep = find_service(s->need[j]);
+ if (!dep || dep->pid <= 0) {
+ stop_for_dep(s);
+ found = 1;
+ break;
+ }
+ }
+ }
+ } while (found);
+}
+
+static void
+handle_child(pid_t pid)
+{
+ size_t i;
+ time_t now;
+
+ now = time(NULL);
+ for (i = 0; i < nsvcs; i++) {
+ struct Service *s = &svcs[i];
+
+ if (s->pid == pid) {
+ s->pid = 0;
+ /* kill logger too - pipe is broken */
+ if (s->logpid > 0) {
+ kill(s->logpid, SIGTERM);
+ waitpid(s->logpid, NULL, 0);
+ s->logpid = 0;
+ }
+
+ /* cascade-stop services that need this one */
+ stop_cascade();
+
+ if (!s->want_up)
+ return;
+ if (s->once || s->noreset) {
+ /* don't restart - clear want_up so check_restart skips */
+ s->want_up = 0;
+ return;
+ }
+ if (now - s->last_start < 60)
+ s->backoff = MIN(s->backoff + 1, MAX_BACKOFF);
+ else
+ s->backoff = 1;
+ s->next_restart = now + s->backoff;
+ return;
+ }
+ if (s->logpid == pid) {
+ s->logpid = 0;
+ /* logger died: restart whole service for clean pipe */
+ if (s->want_up && s->pid > 0)
+ restart_service(s);
+ return;
+ }
+ }
+}
+
+static void
+reap_all(void)
+{
+ pid_t pid;
+
+ while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
+ handle_child(pid);
+}
+
+/* check_restarts: start services that are ready (deps met, backoff
+ expired). */
+static void
+check_restarts(void)
+{
+ size_t i;
+ time_t now;
+ int progress;
+
+ now = time(NULL);
+ do {
+ progress = 0;
+ for (i = 0; i < nsvcs; i++) {
+ struct Service *s = &svcs[i];
+
+ if (!s->want_up || s->pid > 0 || s->down)
+ continue;
+ if (!deps_met(s))
+ continue;
+ if (s->next_restart > 0 && now < s->next_restart)
+ continue;
+ s->next_restart = 0;
+ s->backoff = 0;
+ start_service(s);
+ if (s->pid > 0)
+ progress = 1;
+ }
+ } while (progress);
+}
+
+static int
+compute_timeout(void)
+{
+ size_t i;
+ time_t now, min, wait;
+
+ now = time(NULL);
+ min = 0;
+ for (i = 0; i < nsvcs; i++) {
+ struct Service *s = &svcs[i];
+
+ if (!s->want_up || s->pid > 0 || s->next_restart == 0)
+ continue;
+ if (!deps_met(s))
+ continue;
+ wait = s->next_restart - now;
+ if (wait <= 0)
+ return 0;
+ if (min == 0 || wait < min)
+ min = wait;
+ }
+ if (min == 0)
+ return -1;
+ return (int)(min * 1000);
+}
+
+/* status_str: human-readable service state */
+static const char *
+status_str(struct Service *s)
+{
+ if (s->down)
+ return "DOWN*";
+ if (s->pid > 0)
+ return "RUN";
+ if (s->want_up && !deps_met(s))
+ return "WAIT*";
+ if (s->want_up)
+ return "WAIT";
+ if (s->once)
+ return "DONE";
+ return "DOWN";
+}
+
+/* handle_conn: process one control connection */
+static void
+handle_conn(int conn)
+{
+ char buf[4096], resp[8192], cmd, *name, *p;
+ char full[2 * NAME_MAX + 2];
+ ssize_t n;
+ size_t i, j, off;
+ struct Service *s;
+
+ n = read(conn, buf, sizeof(buf) - 1);
+ if (n <= 0) {
+ close(conn);
+ return;
+ }
+ buf[n] = '\0';
+
+ p = strchr(buf, '\n');
+ if (p)
+ *p = '\0';
+
+ cmd = buf[0];
+ name = (buf[1] == ' ') ? buf + 2 : buf + 1;
+
+ switch (cmd) {
+ case 'u':
+ if (strcmp(name, "a") == 0) {
+ for (i = 0; i < nsvcs; i++) {
+ if (svcs[i].down)
+ continue;
+ svcs[i].want_up = 1;
+ }
+ check_restarts();
+ off = snprintf(resp, sizeof(resp),
+ "OK: started all\n");
+ } else {
+ s = find_service(name);
+ if (!s) {
+ off = snprintf(resp, sizeof(resp),
+ "ERR: no such service: %s\n", name);
+ break;
+ }
+ /* explicit svc -u overrides down and deps */
+ s->down = 0;
+ s->want_up = 1;
+ s->next_restart = 0;
+ s->backoff = 0;
+ start_service(s);
+ off = snprintf(resp, sizeof(resp),
+ "OK: %s up\n", name);
+ }
+ break;
+ case 'd':
+ if (strcmp(name, "a") == 0) {
+ for (i = 0; i < nsvcs; i++)
+ stop_service(&svcs[i]);
+ off = snprintf(resp, sizeof(resp),
+ "OK: stopped all\n");
+ } else {
+ s = find_service(name);
+ if (!s) {
+ off = snprintf(resp, sizeof(resp),
+ "ERR: no such service: %s\n", name);
+ break;
+ }
+ stop_service(s);
+ off = snprintf(resp, sizeof(resp),
+ "OK: %s down\n", name);
+ }
+ break;
+ case 'r':
+ s = find_service(name);
+ if (!s) {
+ off = snprintf(resp, sizeof(resp),
+ "ERR: no such service: %s\n", name);
+ break;
+ }
+ restart_service(s);
+ off = snprintf(resp, sizeof(resp),
+ "OK: %s restarted\n", name);
+ break;
+ case 'k':
+ s = find_service(name);
+ if (!s) {
+ off = snprintf(resp, sizeof(resp),
+ "ERR: no such service: %s\n", name);
+ break;
+ }
+ if (s->pid > 0)
+ kill(s->pid, SIGKILL);
+ off = snprintf(resp, sizeof(resp),
+ "OK: %s killed\n", name);
+ break;
+ case 't':
+ s = find_service(name);
+ if (!s) {
+ off = snprintf(resp, sizeof(resp),
+ "ERR: no such service: %s\n", name);
+ break;
+ }
+ if (s->pid > 0)
+ kill(s->pid, SIGTERM);
+ off = snprintf(resp, sizeof(resp),
+ "OK: %s terminated\n", name);
+ break;
+ case 's':
+ off = 0;
+ for (i = 0; i < nsvcs; i++) {
+ s = &svcs[i];
+ if (off + 128 >= sizeof(resp))
+ break;
+ snprintf(full, sizeof(full), "%s/%s",
+ s->stage, s->name);
+ off += snprintf(resp + off, sizeof(resp) - off,
+ "%-32s %s%s pid=%d",
+ full,
+ status_str(s),
+ s->noreset ? " (noreset)" : "",
+ s->pid);
+ if (s->nneed > 0) {
+ off += snprintf(resp + off,
+ sizeof(resp) - off, " need=");
+ for (j = 0; j < s->nneed; j++)
+ off += snprintf(resp + off,
+ sizeof(resp) - off, "%s%s",
+ j ? "," : "", s->need[j]);
+ }
+ if (s->nafter > 0) {
+ off += snprintf(resp + off,
+ sizeof(resp) - off, " after=");
+ for (j = 0; j < s->nafter; j++)
+ off += snprintf(resp + off,
+ sizeof(resp) - off, "%s%s",
+ j ? "," : "", s->after[j]);
+ }
+ off += snprintf(resp + off, sizeof(resp) - off,
+ "\n");
+ }
+ break;
+ default:
+ off = snprintf(resp, sizeof(resp),
+ "ERR: unknown command\n");
+ break;
+ }
+
+ writeall(conn, resp, off);
+ close(conn);
+}
+
+static void
+shutdown_serva(void)
+{
+ size_t i;
+
+ for (i = 0; i < nsvcs; i++)
+ stop_service(&svcs[i]);
+ unlink(SOCK_PATH);
+}
+
+int
+main(void)
+{
+ int sock, nfds, timeout, conn;
+ struct sockaddr_un addr;
+ struct pollfd pfds[2];
+ struct sigaction sa;
+ char c;
+ size_t i;
+
+ /* self-pipe for signal integration with poll */
+ if (pipe(sigpipe) < 0)
+ eprintf("pipe:");
+ fcntl(sigpipe[0], F_SETFL, O_NONBLOCK);
+ fcntl(sigpipe[1], F_SETFL, O_NONBLOCK);
+
+ /* install signal handlers */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = signal_handler;
+ sigemptyset(&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sigaction(SIGCHLD, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ scan_services();
+
+ /* create control socket */
+ sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sock < 0)
+ eprintf("socket:");
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strlcpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path));
+ unlink(SOCK_PATH);
+ if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
+ eprintf("bind %s:", SOCK_PATH);
+ if (listen(sock, 16) < 0)
+ eprintf("listen:");
+ chmod(SOCK_PATH, 0600);
+
+ /* mark all non-down services as wanting to be up */
+ for (i = 0; i < nsvcs; i++) {
+ if (svcs[i].down)
+ continue;
+ svcs[i].want_up = 1;
+ }
+
+ /* start services whose deps are met */
+ check_restarts();
+
+ /* warn about services stuck on unmet deps */
+ for (i = 0; i < nsvcs; i++) {
+ if (svcs[i].want_up && svcs[i].pid == 0
+ && !svcs[i].down && !deps_met(&svcs[i]))
+ weprintf("unmet dependency for %s/%s",
+ svcs[i].stage, svcs[i].name);
+ }
+
+ /* main loop */
+ for (;;) {
+ timeout = compute_timeout();
+
+ pfds[0].fd = sock;
+ pfds[0].events = POLLIN;
+ pfds[1].fd = sigpipe[0];
+ pfds[1].events = POLLIN;
+
+ nfds = poll(pfds, 2, timeout);
+ if (nfds < 0) {
+ if (errno == EINTR)
+ continue;
+ eprintf("poll:");
+ }
+
+ if (pfds[0].revents & POLLIN) {
+ conn = accept(sock, NULL, NULL);
+ if (conn >= 0)
+ handle_conn(conn);
+ }
+
+ if (pfds[1].revents & POLLIN) {
+ while (read(sigpipe[0], &c, 1) > 0)
+ ;
+ reap_all();
+ }
+
+ if (got_term)
+ break;
+
+ check_restarts();
+ }
+
+ shutdown_serva();
+ return 0;
+}
diff --git a/sinit.8 b/sinit.8
@@ -1,60 +0,0 @@
-.Dd December 4, 2014
-.Dt SINIT 8 sinit\-VERSION
-.Os
-.Sh NAME
-.Nm sinit
-.Nd simple init
-.Sh DESCRIPTION
-.Nm
-is a simple init. It is configured by modifying
-.Dq config.h
-and recompiling the code.
-.Ss Init sequence
-By default
-.Nm
-will execute
-.Dq /bin/rc.init .
-You can chain your init scripts together at that point to allow for
-single-user and multi-user operation.
-.Ss Signal handling
-.Nm
-will respond to the following signals:
-.Bl -tag -width xxxxxxxx
-.It USR1
-Default action is to initiate the shutdown sequence by
-executing
-.Dq /bin/rc.shutdown poweroff .
-.It INT
-Default action is to initiate the reboot sequence by
-executing
-.Dq /bin/rc.shutdown reboot .
-.It CHLD
-Reap children.
-.El
-.Ss General considerations
-Running the
-.Dq rc.shutdown
-script directly is not recommended. If any
-process in your session has stale filesystem references then it is
-likely your init scripts will fail to unmount the filesystem cleanly.
-It is recommended to signal
-.Nm
-via a wrapper script.
-.Pp
-.Nm
-does not clear utmp records. The version of
-.Xr getty 8
-in ubase clears the utmp entries on the specified tty before it
-spawns the given program, usually
-.Xr login 1 .
-.Pp
-.Nm
-does not restart
-.Xr getty 8
-or interact with it in any way. You will need an external
-respawning mechanism to restart it.
-.Sh SEE ALSO
-.Xr getty 8 ,
-.Xr killall5 8
-.Sh AUTHORS
-.An Dimitris Papastamos Aq Mt sin@2f30.org .
diff --git a/sinit.c b/sinit.c
@@ -1,91 +0,0 @@
-/* See LICENSE file for copyright and license details. */
-#include <sys/types.h>
-#include <sys/wait.h>
-
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-
-#define LEN(x) (sizeof (x) / sizeof *(x))
-#define TIMEO 30
-
-static void sigpoweroff(void);
-static void sigreap(void);
-static void sigreboot(void);
-static void spawn(char *const []);
-
-static struct {
- int sig;
- void (*handler)(void);
-} sigmap[] = {
- { SIGUSR1, sigpoweroff },
- { SIGCHLD, sigreap },
- { SIGALRM, sigreap },
- { SIGINT, sigreboot },
-};
-
-#include "config.h"
-
-static sigset_t set;
-
-int
-main(void)
-{
- int sig;
- size_t i;
-
- if (getpid() != 1)
- return 1;
- chdir("/");
- sigfillset(&set);
- sigprocmask(SIG_BLOCK, &set, NULL);
- spawn(rcinitcmd);
- while (1) {
- alarm(TIMEO);
- sigwait(&set, &sig);
- for (i = 0; i < LEN(sigmap); i++) {
- if (sigmap[i].sig == sig) {
- sigmap[i].handler();
- break;
- }
- }
- }
- /* not reachable */
- return 0;
-}
-
-static void
-sigpoweroff(void)
-{
- spawn(rcpoweroffcmd);
-}
-
-static void
-sigreap(void)
-{
- while (waitpid(-1, NULL, WNOHANG) > 0)
- ;
- alarm(TIMEO);
-}
-
-static void
-sigreboot(void)
-{
- spawn(rcrebootcmd);
-}
-
-static void
-spawn(char *const argv[])
-{
- switch (fork()) {
- case 0:
- sigprocmask(SIG_UNBLOCK, &set, NULL);
- setsid();
- execvp(argv[0], argv);
- perror("execvp");
- _exit(1);
- case -1:
- perror("fork");
- }
-}
diff --git a/slog.c b/slog.c
@@ -0,0 +1,80 @@
+/* slog: simple logging daemon */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* needed for PATH_MAX, to, and from */
+#endif
+
+#include "arg.h"
+#include "util.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-d dir] [-s bytes] [-n count]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *dir;
+ char cur[PATH_MAX], from[PATH_MAX], to[PATH_MAX];
+ long maxsize, maxfiles;
+ int fd, i, n;
+ size_t total;
+ char buf[4096];
+
+ dir = ".";
+ maxsize = 1048576;
+ maxfiles = 10;
+
+ ARGBEGIN {
+ case 'd': dir = EARGF(usage()); break;
+ case 's': maxsize = estrtonum(EARGF(usage()), 1, LONG_MAX); break;
+ case 'n': maxfiles = estrtonum(EARGF(usage()), 1, 1000); break;
+ default: usage();
+ } ARGEND
+
+ snprintf(cur, sizeof(cur), "%s/current", dir);
+ fd = open(cur, O_WRONLY | O_CREAT | O_APPEND, 0644);
+ if (fd < 0)
+ eprintf("open %s:", cur);
+
+ total = 0;
+ while ((n = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
+ if (writeall(fd, buf, n) < 0)
+ eprintf("write %s:", cur);
+ total += n;
+ if (total < (size_t)maxsize)
+ continue;
+
+ /* rotate: delete oldest, shift others, current -> 0 */
+ close(fd);
+ snprintf(to, sizeof(to), "%s/%ld", dir, maxfiles - 1);
+ unlink(to);
+ for (i = maxfiles - 2; i >= 0; i--) {
+ snprintf(from, sizeof(from), "%s/%d", dir, i);
+ snprintf(to, sizeof(to), "%s/%d", dir, i + 1);
+ if (rename(from, to) < 0 && errno != ENOENT)
+ weprintf("rename %s -> %s:", from, to);
+ }
+ snprintf(to, sizeof(to), "%s/0", dir);
+ if (rename(cur, to) < 0)
+ weprintf("rename %s -> %s:", cur, to);
+
+ fd = open(cur, O_WRONLY | O_CREAT | O_APPEND, 0644);
+ if (fd < 0)
+ eprintf("open %s:", cur);
+ total = 0;
+ }
+
+ close(fd);
+ return 0;
+}
diff --git a/svc.c b/svc.c
@@ -0,0 +1,77 @@
+/* svc: control client for ssv */
+
+#include "arg.h"
+#include "util.h"
+
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+static void
+usage(void)
+{
+ eprintf("usage: %s [-udrkts] [-a] [service...]\n", argv0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ int sock, aflag, action, i, n;
+ struct sockaddr_un addr;
+ char buf[4096], resp[4096];
+ size_t off;
+
+ aflag = 0;
+ action = 0;
+
+ ARGBEGIN {
+ case 'u': action = 'u'; break;
+ case 'd': action = 'd'; break;
+ case 'r': action = 'r'; break;
+ case 'k': action = 'k'; break;
+ case 't': action = 't'; break;
+ case 's': action = 's'; break;
+ case 'a': aflag = 1; break;
+ default: usage();
+ } ARGEND
+
+ if (!action)
+ usage();
+ if (action != 's' && !aflag && argc == 0)
+ usage();
+
+ /* connect to ssv control socket */
+ sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sock < 0)
+ eprintf("socket:");
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strlcpy(addr.sun_path, "/run/serva.sock", sizeof(addr.sun_path));
+ if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
+ eprintf("connect /run/serva.sock:");
+
+ /* build command line */
+ if (action == 's') {
+ strlcpy(buf, "s\n", sizeof(buf));
+ } else if (aflag) {
+ snprintf(buf, sizeof(buf), "%c a\n", action);
+ } else {
+ off = snprintf(buf, sizeof(buf), "%c", action);
+ for (i = 0; i < argc; i++)
+ off += snprintf(buf + off, sizeof(buf) - off,
+ " %s", argv[i]);
+ off += snprintf(buf + off, sizeof(buf) - off, "\n");
+ }
+
+ /* send command, print response */
+ writeall(sock, buf, strlen(buf));
+ while ((n = read(sock, resp, sizeof(resp))) > 0)
+ writeall(STDOUT_FILENO, resp, n);
+ close(sock);
+
+ if (fshut(stdout, "<stdout>"))
+ return 2;
+ return 0;
+}
diff --git a/util.c b/util.c
@@ -0,0 +1,324 @@
+/* Repackaged small subset of libutil/%.c to one util.c */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* needed for strdup */
+#endif
+
+#include "util.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+char *argv0 = "origo";
+
+void
+xvprintf(const char *fmt, va_list ap)
+{
+ if (argv0 && strncmp(fmt, "usage", strlen("usage")))
+ fprintf(stderr, "%s: ", argv0);
+
+ vfprintf(stderr, fmt, ap);
+
+ if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
+ fputc(' ', stderr);
+ perror(NULL);
+ }
+}
+
+void
+eprintf(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+
+ exit(1);
+}
+
+void
+weprintf(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+}
+
+void
+enprintf(int status, const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ xvprintf(fmt, ap);
+ va_end(ap);
+
+ exit(status);
+}
+
+
+void *
+enmalloc(int status, size_t size)
+{
+ void *p;
+
+ p = malloc(size);
+ if (!p)
+ enprintf(status, "malloc: out of memory\n");
+ return p;
+}
+
+void *
+emalloc(size_t size)
+{
+ return enmalloc(1, size);
+}
+
+void *
+encalloc(int status, size_t nmemb, size_t size)
+{
+ void *p;
+
+ p = calloc(nmemb, size);
+ if (!p)
+ enprintf(status, "calloc: out of memory\n");
+ return p;
+}
+
+void *
+ecalloc(size_t nmemb, size_t size)
+{
+ return encalloc(1, nmemb, size);
+}
+
+void *
+enrealloc(int status, void *p, size_t size)
+{
+ p = realloc(p, size);
+ if (!p)
+ enprintf(status, "realloc: out of memory\n");
+ return p;
+}
+
+void *
+reallocarray(void *optr, size_t nmemb, size_t size)
+{
+ if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
+ nmemb > 0 && SIZE_MAX / nmemb < size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ return realloc(optr, size * nmemb);
+}
+
+void *
+erealloc(void *p, size_t size)
+{
+ return enrealloc(1, p, size);
+}
+
+void *
+enreallocarray(int status, void *optr, size_t nmemb, size_t size)
+{
+ void *p;
+
+ if (!(p = reallocarray(optr, nmemb, size)))
+ enprintf(status, "reallocarray: out of memory\n");
+
+ return p;
+}
+
+void *
+ereallocarray(void *optr, size_t nmemb, size_t size)
+{
+ return enreallocarray(1, optr, nmemb, size);
+}
+
+char *
+enstrdup(int status, const char *s)
+{
+ char *p;
+
+ p = strdup(s);
+ if (!p)
+ enprintf(status, "strdup: out of memory\n");
+ return p;
+}
+
+char *
+estrdup(const char *s)
+{
+ return enstrdup(1, s);
+}
+
+char *
+enstrndup(int status, const char *s, size_t n)
+{
+ char *p;
+
+ p = strndup(s, n);
+ if (!p)
+ enprintf(status, "strndup: out of memory\n");
+ return p;
+}
+
+size_t
+strlcpy(char *dst, const char *src, size_t siz)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+ /* Copy as many bytes as will fit */
+ if (n != 0) {
+ while (--n != 0) {
+ if ((*d++ = *s++) == '\0')
+ break;
+ }
+ }
+ if (n == 0) {
+ if (siz != 0)
+ *d = '\0'; /* NUL-terminate dst */
+ while (*s++)
+ ;
+ }
+ return(s - src - 1); /* count does not include NUL */
+}
+
+
+size_t
+strlcat(char *dst, const char *src, size_t siz)
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+ size_t dlen;
+ /* Find the end of dst and adjust bytes left but don't go past end */
+ while (n-- != 0 && *d != '\0')
+ d++;
+ dlen = d - dst;
+ n = siz - dlen;
+ if (n == 0)
+ return(dlen + strlen(s));
+ while (*s != '\0') {
+ if (n != 1) {
+ *d++ = *s;
+ n--;
+ }
+ s++;
+ }
+ *d = '\0';
+ return(dlen + (s - src)); /* count does not include NUL */
+}
+
+#define INVALID 1
+#define TOOSMALL 2
+#define TOOLARGE 3
+
+long long
+strtonum(const char *numstr, long long minval, long long maxval,
+ const char **errstrp)
+{
+ long long ll = 0;
+ int error = 0;
+ char *ep;
+ struct errval {
+ const char *errstr;
+ int err;
+ } ev[4] = {
+ { NULL, 0 },
+ { "invalid", EINVAL },
+ { "too small", ERANGE },
+ { "too large", ERANGE },
+ };
+
+ ev[0].err = errno;
+ errno = 0;
+ if (minval > maxval) {
+ error = INVALID;
+ } else {
+ ll = strtoll(numstr, &ep, 10);
+ if (numstr == ep || *ep != '\0')
+ error = INVALID;
+ else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
+ error = TOOSMALL;
+ else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
+ error = TOOLARGE;
+ }
+ if (errstrp != NULL)
+ *errstrp = ev[error].errstr;
+ errno = ev[error].err;
+ if (error)
+ ll = 0;
+
+ return (ll);
+}
+
+long long
+enstrtonum(int status, const char *numstr, long long minval, long long maxval)
+{
+ const char *errstr;
+ long long ll;
+
+ ll = strtonum(numstr, minval, maxval, &errstr);
+ if (errstr)
+ enprintf(status, "strtonum %s: %s\n", numstr, errstr);
+ return ll;
+}
+
+long long
+estrtonum(const char *numstr, long long minval, long long maxval)
+{
+ return enstrtonum(1, numstr, minval, maxval);
+}
+
+ssize_t
+writeall(int fd, const void *buf, size_t len)
+{
+ const char *p = buf;
+ ssize_t n;
+
+ while (len) {
+ n = write(fd, p, len);
+ if (n <= 0)
+ return n;
+ p += n;
+ len -= n;
+ }
+
+ return p - (const char *)buf;
+}
+
+int
+fshut(FILE *fp, const char *fname)
+{
+ int ret = 0;
+
+ /* fflush() is undefined for input streams by ISO C,
+ * but not POSIX 2008 if you ignore ISO C overrides.
+ * Leave it unchecked and rely on the following
+ * functions to detect errors.
+ */
+ fflush(fp);
+
+ if (ferror(fp) && !ret) {
+ weprintf("ferror %s:", fname);
+ ret = 1;
+ }
+
+ if (fclose(fp) && !ret) {
+ weprintf("fclose %s:", fname);
+ ret = 1;
+ }
+
+ return ret;
+}
+
diff --git a/util.h b/util.h
@@ -0,0 +1,30 @@
+/* util.h: minimal utility wrappers for origo. */
+
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/types.h>
+
+size_t strlcpy(char *, const char *, size_t);
+size_t strlcat(char *, const char *, size_t);
+
+void eprintf(const char *, ...);
+void weprintf(const char *, ...);
+
+void *emalloc(size_t);
+void *ecalloc(size_t, size_t);
+void *erealloc(void *, size_t);
+void *ereallocarray(void *, size_t, size_t);
+char *estrdup(const char *);
+char *estrndup(const char *, size_t);
+
+long long estrtonum(const char *, long long, long long);
+
+ssize_t writeall(int, const void *, size_t);
+int fshut(FILE *, const char *);
+
+#define LEN(a) (sizeof(a) / sizeof((a)[0]))
+#define MIN(x, y) ((x) < (y) ? (x) : (y))
+#define MAX(x, y) ((x) > (y) ? (x) : (y))
+#define MUL_NO_OVERFLOW (1UL << (sizeof(size_t) * 4))
+
+extern char *argv0;