rats

RAdix iniT Suite
git clone https://git.daat.foo/repos/rats.git
Log | Files | Refs | README | LICENSE

commit f0df6e3a39a3b9386b57b8338802f337aa3e38a0
parent 28c44b6b94a870f2942c37f9cfbae8b770595712
Author: cowmonk <cowmonk@based.pt>
Date:   Tue, 23 Jun 2026 19:06:41 +0000

First commit

Diffstat:
A.gitignore | 9+++++++++
MLICENSE | 54+++++++++++++++++++++++++++++++++++++++++++++++++++++-
MMakefile | 62+++++++++++++++++++++++++++++++++++---------------------------
MREADME | 272++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Aarg.h | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dconfig.def.h | 5-----
Mconfig.mk | 18+++++++-----------
Afiles/bin/poweroff | 3+++
Afiles/bin/reboot | 3+++
Afiles/rc.boot | 36++++++++++++++++++++++++++++++++++++
Afiles/rc.local | 4++++
Afiles/rc.shutdown | 23+++++++++++++++++++++++
Afiles/rc.single | 6++++++
Aorigo.c | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserva.c | 878+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsinit.8 | 60------------------------------------------------------------
Dsinit.c | 91-------------------------------------------------------------------------------
Aslog.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asvc.c | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.c | 324+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.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;