User:Capezotte/s6 on Gentoo

'''Incomplete. Even if it was complete to my standards, remember s6 is not supported by Gentoo, and that I don't guarantee it will work.'''

This aims to be a guide on how to set up a Gentoo system with a full s6 stack (s6-linux-init, s6-svscan+s6-supervise, s6-rc), ultimately allowing you to replace sysvinit and OpenRC.

Included in the LEGO set

 * s6-svscan and s6-supervise (and associated tools) are the workhorse of system management. They start services, ensure they are reachable and there's only one instance of them, and restarts them automatically if they crash.
 * s6-linux-init performs the bare minimum of system initialization needed for s6-svscan, executes it as PID 1, and handles shutdown.
 * s6-rc enhances s6-svscan with dependency management and the ability to run oneshots (scripts do one thing on system startup/shutdown).

Getting started
You'll want to emerge (s6-svscan+s6-supervise),  and. Avoid emerging the latter with so you can fall back to sysvinit+OpenRC if things go awry.

Introduction to service definitions
Creating services for  will feel familiar if you've already used , but it also has support for one-shot scripts, which allow us to perform the core system initialization and starting actual long-running services (what runit calls, respectively, stage 1 and 2) with the same tool.

With, services are folders with at least one file called  , whose contents are one of these three strings:
 * : what we usually think of being services: programs that run for the lifetime of the machine, providing some sort of functionality (e.g. device management, usually udev). If you choose this type, there must also be an executable file called . It's usually a script that performs necessary setup and then execs into the program, which must be in foreground mode (otherwise, s6 will lose track of it). Again, runit users will be familiar with this requirement.
 * : a small script performing setup before or after a service is started. For instance, mounting filesystems (i. e. calling mount -a), or calling udev to recognize currently plugged devices (i. e. udevadm trigger). If you choose this type, you must provide an  file, which is an execline script. For very simple scripts, it will be exactly like shell, but you can't use single quotes. If you aren't willing to fully learn it, no worries -- just write the script in whichever language you like the most, mark it as executable and write its filename to.
 * : a set of longruns, oneshots or even other bundles. You write the name of the each item, one per line, to a file named, which is mandatory.

Both  and   can have an additional   file, which lists, one per line, which other longruns, oneshots or bundles must be working before this service is started.

This is just a very simplistic introduction. For the full description, read the docs.

Core system initialization is basically mounting the virtual filesystems ( /dev, /proc, /sys ), reading settings from the file system ( sysctl, hostname ) and creating the utmp files. For reference, you can see how Artix implements core s6 services. I'll admit I copy pasted a lot of them for this setup:


 * (pay special attention to the {u,b,w}tmp related portions if you use elogind).
 * services
 * (pay special attention to the {u,b,w}tmp related portions if you use elogind).
 * services
 * (pay special attention to the {u,b,w}tmp related portions if you use elogind).
 * services
 * (pay special attention to the {u,b,w}tmp related portions if you use elogind).
 * services
 * services
 * services

You should probably prepare some services now. You can pick any folder for this, I personally prefer, but you can put or. Create subfolders and start working on the service definitions inside of them.

When you're done, you need to compile the database. s6-rc will build a dependency list and make your s6-rc services ready to be plugged into s6-svscan, and complain if the dependencies/types are wrong. For convenience later (we'll explain in the s6-linux-init-part), we'll make it compile to a folder under with a unique name, then symlink to :

However, it doesn't make any sort of verification in the run/up files themselves, so double check them - you can even test them by running:

- for oneshots, or

- for longruns, where  can be ,  ,  ... depending on the file's shebang.

Setting up s6-linux-init
will be the first process of the system and a piece bundled with it will remain operational for the lifetime of the machine, waiting for the fateful shutdown command. It's configured through scripts in.

First, let's use included  program to create the  directory (though it's still non-functional):


 * With , we can specify an emergency service that will always be started. In this example, it's a getty on tty12 (Ctrl+Alt+F12) which you can use to login to your system even if your s6-rc config is borked.
 * sends system initialization logs to the tty as well as to the file. If this is not specied, only  will have the logs.
 * More details here.

Now onto editing, and why specifically in the previous steps.


 * : uncomment  line. This will make, after   is up, copy the s6-rc → s6-svscan translated service definitions from  (remember him?) to  (where   expects services to be, by default, when spawned from  ). However, it won't start any of them.
 * : uncomment the exec . This will bring all of our services down before the system is powered off.
 * : uncomment . When we call a runlevel change through   (or on system initialization, as we'll see later), it will be translated into a call to , i.e. start the service/bundle $RUNLEVEL, and stop everything else.

Earlier I told you  won't start any services. So how are we supposed to bring up the services on system boot? If you read down, you'll find:

exec /etc/s6-linux-init/current/scripts/runlevel "$rl"

This is what actually starts our services on boot. If you didn't specify any runlevels on the kernel command line,  will be. Usually,  will be a bundle with the services you want most of the time. If you didn't create a bundle called, take the opportunity to remove the   symlink and perform that set of commands again (the fact that there's this whole compiling dance on s6-rc is one of its drawbacks, unfortunately).

I recommend not making a single giant  bundle, but rather work with layers. For instance,  with just the filesystems, utmp, ttys and the device manager, and then include this boot bundle inside of default, alongside services like CUPS, bluetoothd, elogind, etc.. Adding  to the command line could then act as a sort of "safe mode", with no potentially misbehaving services, in addition to making   useful.

Trying it out
To make this setup bootable, you need to symlink the contents of to. You can use the following script:

Now, you can put  on your kernel command line, and use  /  to perform power management on your s6 session. If anything goes wrong, go to the emergency tty we set up, or reboot with OpenRC.

On managing s6 within s6
Boot into s6. So far, we've learned how what the database is and how to compile it, and hopefully we have a working one.

We're just getting started.

Bringing services up or down
Just in case you have made this far and haven't read the documentation of, here's a cheatsheet:


 * to bring a service or a bundle of services up.
 * to bring a service or a bundle of services down.

can also take a  option, which either means "Stop everything else and bring these up" (  +  ) or "Stop these and services that depend on it, and bring up everything else" (  +  ).

Changing /etc/s6-rc/compiled in place
Simply re-linking  when we want to add services, as we did before booting into s6, will bring   to an inconsistent state where you can't bring services up or down without errors - if you think "waiting for session C2 of user X" on systemd was bad, you haven't accidentally overwritten  on an s6 install.

This doesn't mean we can't change service definitions without rebooting - there's a tool to change, in place,  from  to somewhere else, dynamically -.

We first need to compile a database with a unique name (as we've been doing):

Now, tell s6-rc to use a new database:

Now that s6-rc is looking away, we can safely overwrite.

You'll be doing this a lot, so it's recommended to make it a script. Let's say,  with the following contents:

NAME=$(date +%s) && s6-rc-compile -- "/etc/s6-rc/$NAME" /etc/s6-rc/src && s6-rc-update -- "/etc/s6-rc/$NAME" && ln -sfT -- "$NAME" /etc/s6-rc/compiled

The script above is slightly wrong
According to the POSIX standard,  must perform two system calls when overwriting an existing symlink: one to remove the existing link, and other to create the new link. So there's the possibility, however slim, that the first system call can succeed, while the second doesn't, or that you press Ctrl+C right between the two, etc., and you're left without, setting you up for a nasty surprise on the next reboot. You'd have to be very unlucky, but just in case, here's a rewritten version with a workaround ( is a single system call).

NAME=$(date +%s) && s6-rc-compile -- "/etc/s6-rc/$NAME" /etc/s6-rc/src && s6-rc-update -- "/etc/s6-rc/$NAME" && ln -sf -- "$NAME" /etc/s6-rc/compiled/compiled && mv -f /etc/s6-rc/compiled/compiled /etc/s6-rc

Readiness notification
Most non-trivial services take a certain amount of time before they're actually ready to perform their duty. This means that if the service manager is fast enough (and s6-rc is fast), dependant services might start before the "dependee" is actually ready to perform its duty. To account for this case,  has implemented a simple readiness notification mechanism: daemons write a newline to a pipe (in a location specified in a file called  ), and   understands it's ready to communicate with other processes and broadcasts this information to anyone who asks. asks and takes it into account when ordering services.

Most programs included in s6 have an option for this, and many other programs have options that, although not even intended for systems using s6, can work just as well for this purpose (such as DBus'  and Xorg's  ). The latter case -- fitting in places where it wasn't even expected -- is a testament to its outright genius, in my opinion.

For example, a definition of an  instance with readiness notification, which will be relevant for the next section, might look like.

Note that the argument for option  must the same as the content of the file , as it should for non-s6 programs either intentionally or accidentally compatible with s6's notifications.

Logging chain
Syslog under  is not natively supported. Instead, the preferred mechanism is sending daemon's standard output and error to a second logger daemon,. Again,  users will be familiar with this, as it requires a similar design with   in place of. However, unlike runit's service-dir + service-dir/log scheme,  has a different scheme.

Let's say you want to log a daemon called. Create two folders.

The first one - named, if you're following Artix's conventions, though this is not a requirement - you populate it as normal with run, type, notification-fd, etc, for the daemon itself. You should write  (shell) or   (execline) in   so error messages go to standard output.

A second folder - conventionally,  - is then populated with another service, preferably   writing to a unique location (conventionally, ), and with readiness notification.

Now, you can use s6-rc's pipeline mechanism. It can supervise entire equivalents of shell script pipelines, but the one pipeline in particular we want to supervise is.

The steps we take are:


 * Write  to   - so  's standard output gets connect to  's standard input.
 * Write  to   - this confirms the above in  's side.
 * Write  to   - the file that contains the name of the bundle with.

After recompiling, your database will contain a  bundle, that will start the service and its logger. If this sounds like the kind of boilerplate you'd want to automate with a script, that's because it is.

SERVICE=${1?:Need service.}
 * 1) !/bin/sh

mkdir -- "$SERVICE-srv" "$SERVICE-log" || exit

( cd -- "$SERVICE-srv" touch run # write it echo longrun > type echo "$SERVICE-log" > producer-for ) ( cd -- "$SERVICE-log" printf '%s\n' '#!/bin/execlineb -P' "s6-log -d 3 -- /var/log/$SERVICE" > run echo 3 > notification-fd echo longrun > type echo "$SERVICE-srv" > consumer-for echo "$SERVICE" > pipeline-name )

Services without a dedicated logger will have their logs sent to, and, if  was given in the   step, to the console.

Replacing OpenRC
Preferably, do these outside of OpenRC + Sysvinit, so you can shutdown your system without the good (?) ol' Alt+PrintScreen+REISUB.

Allowing s6-rc as a virtual/service-manager
is unarguably a legitimate service manager, but the doesn't account for it. It's outside of the scope of this tutorial, but you should create a custom ebuild repository, with a custom  ebuild that allows  as a dependency.

For instance, mine, which is a slightly changed copy of Gentoo's official ebuild, is
 * 1) Copyright 1999-2021 Gentoo Authors
 * 2) Distributed under the terms of the GNU General Public License v2

EAPI=7

DESCRIPTION="Virtual for various service managers"

SLOT="0" KEYWORDS="~alpha amd64 arm arm64 hppa ~ia64 ~m68k ~mips ppc ppc64 ~riscv ~s390 sparc x86 ~x64-cygwin ~amd64-linux ~x86-linux ~ppc-macos ~x64-macos ~sparc-solaris ~sparc64-solaris ~x64-solaris ~x86-solaris ~x86-winnt" IUSE="kernel_linux"

RDEPEND="	prefix-guest? ( >=sys-apps/baselayout-prefix-2.2 )	!prefix-guest? (		|| (		sys-apps/openrc		kernel_linux? ( || (			sys-apps/systemd			sys-process/runit			virtual/daemontools			sys-apps/s6-rc	) ) ) )"

Removing OpenRC and Sysvinit
Emerge the new virtual/service-manager, deselect and. Gentoo packages usually won't have an explicit OpenRC dependency just because of the init script. They will be kept around after we switch, which will come quite handy when you want to rewrite services for your new init. Eventually,  should agree to remove OpenRC and sysvinit. If you're in a hurry, just unmerge both immediately and deal with the fallout (including updates trying to reinstall them) later.

Reemerging sys-apps/s6-linux-init
Re-emerge with. Rename your symlink to, or create it now if you haven't.

A note on rewriting init scripts
OpenRC, in most cases, is an absent parent which would rather not deal with children nagging them, so it usually avoids passing arguments that would make daemons be in the foreground, or even actively passes arguments that make programs go to the background.

Like runit (and to be fair, supervise-daemon), however, s6 considers processes to have failed when they exit, and tries to restart then in this case.

This means that, if you just blindly copy-paste the command line used by OpenRC, a conflict might happen: first, s6 spawns the program with OpenRC flags. Then program spawned by s6 will spawn the actual service, and leave s6 in the dust. After that, s6 keeps trying to restart the first program, which will fail due to there already being a PID file, there already being something waiting for commands on the same location, etc.

For instance, if you blindly copy-paste, you'll end up with a file that calls , which will go to the background and cause s6 to repeatedly start elogind. Just omitting the  option makes it work properly.

On the other hand, programs like, say,, by default have the behavior OpenRC expects, so you'll have to read the manual and look for a "do not fork", "run in foreground", "no detaching/backgrouding", "supervised", "debug" etc. option and apply it to your service definition. For reference, you can look at Artix Linux's s6 implementation or at runit init scripts for the service you're trying to port, which have the same restriction.

As a last resort, if the service is really obnoxious about not being watched by anything, you can write  to. is, well, a hackish program that will try to stay alive for as long as there are processes spawned by, giving s6-svscan and s6-supervise "a bone to chew", so to speak. However, stopping s6-fghack (which we've tricked s6 into thinking is the service) won't stop the actual service, which was spawned by  and left without a trace. Therefore, you'll then have to write, whatever code OpenRC uses to stop the service, or  , to a file called   (which is run after the service is stopped).

WIP
more precise service examples.

explain even more s6 concepts (notifywhenup with s6-notifyoncheck, etc.).