User:Flexibeast/guides/OpenRC user services via s6 and s6-rc
User services are not yet implemented in OpenRC (though such functionality is being worked on). However, s6 and s6-rc can be used to provide such services.
This guide demonstrates the basics of creating s6 and s6-rc services, building up from core concepts through to running and managing a collection of services.
Note that this guide only intends to provide 'Minimal Working Examples', and is not intended as a general introduction to s6, s6-rc, or their inner workings - for that, refer to the s6 and s6-rc pages.
Service supervision with s6
Install sys-apps/s6 (and optionally, app-doc/s6-man-pages):
root #
emerge --ask sys-apps/s6
root #
emerge --ask app-doc/s6-man-pages
For this guide, we'll make use of two different directories: a services directory, which will contain service directories ('servicedirs'), and a scan directory ('scandir'), both building on the XDG/Base Directories specification.
Firstly, create a 'shortcut' variable containing the path to the services directory:
user $
S6_SERVICES=~/$XDG_STATE_HOME/s6/services
Now create that directory, using the -p
option to ensure all intermediate directories exist:
user $
mkdir -p $S6_SERVICES
This guide assumes that there's a directory called ~/.logs/s6, which services can refer to; create that directory if necessary:
user $
mkdir -p ~/.logs/s6
An s6 Emacs Service
As a first example, we'll set up an Emacs service.
Change to the servicedir described in the previous section, S6_SERVICES
user $
cd $S6_SERVICES
Create an emacs servicedir in that directory, and change to that directory:
user $
mkdir emacs
user $
cd emacs
Now use your editor to create a file called run. This file will be run by s6-supervise, and thus needs to be something executable. The lightweight execline scripting language can be used, but it's not compulsory; we'll just use POSIX sh:
#!/bin/sh
# Create log file if necessary.
if [ ! -f "${HOME}/.logs/s6/emacs.log" ]
then
touch "${HOME}/.logs/s6/emacs.log"
fi
exec /usr/bin/emacs --fg-daemon 1>>"${HOME}/.logs/s6/emacs.log" 2>&1
Note the use of exec on the last line of the script; this is crucial. s6 (and other daemontools-style supervisors) start the supervised program as a direct child of the supervising process, and are therefore able to directly determine the status of the service process. This means that the program must not be backgrounded, and that exec must be used to replace the run script with the program as the direct child of the s6-supervise process.
The script also sets up basic logging via redirection to a (presumed preexisting) ${HOME}/.logs/emacs.log file. A more sophisticated approach to logging is provided by s6-rc.
Set the permissions of run to 700:
user $
chmod 700 run
And that's it! We now have an Emacs service.
s6-supervise is used to supervise a service. Normally each instance of s6-supervise shouldn't be started manually, but instead started by another program, such as s6-svscan - more on that below. However, for educational purposes, for now, we'll demonstrate starting a supervisor process manually.
To start a service, run s6-supervise with the path to the servicedir:
user $
s6-supervise $S6_SERVICES/emacs
With this basic setup, s6-supervise will try to ensure the service is always up: manually terminating the process, e.g. via:
user $
pkill emacs
will subsequently result in a new Emacs server process being created:
user $
pgrep -a emacs
When supervising a program, s6-supervise creates two directories in the servicedir: events/, a fifodir for service-related events, and supervise/, for storing internal state.
The s6-svc program can be used to manage the service. For example, to force a restart:
user $
s6-svc -r $S6_SERVICES/emacs
or to stop the service:
user $
s6-svc -dx $S6_SERVICES/emacs
where the -d
option brings the service down, and the -x
option tells the s6-supervise process to exit once the service is down.
Assuming a console-based login, the emacs
service could be started at login, once the appropriate environment variables have been set up, via one of the shell's configuration files, e.g. ~/.bash_login:
s6-supervise "${XDG_STATE_HOME}/s6/services/emacs"
Optional: Modify script to add D-Bus support
Some Emacs functionality requires the presence of a D-Bus session bus (which is distinct from the D-Bus system bus provided by the dbus
service). Assuming that a D-Bus session bus has been created and its value saved somewhere, e.g. via something like:
dbus-daemon --session --fork --print-address 4 4>|"${XDG_STATE_HOME}/session-bus-address"
The run script can be modified to set the value of DBUS_SESSION_BUS_ADDRESS for the Emacs service:
#!/bin/sh
# Create log file if necessary.
if [ ! -f "${HOME}/.logs/s6/emacs.log" ]
then
touch "${HOME}/.logs/s6/emacs.log"
fi
export DBUS_SESSION_BUS_ADDRESS=$(cat "${XDG_STATE_HOME}/session-bus-address" | tr -d '\n')
exec /usr/bin/emacs --fg-daemon 1>>"${HOME}/.logs/s6/emacs.log" 2>&1
Optional: Set up readiness notification
Thanks to forums user GDH-Gentoo for help with using s6-notifyoncheck!
Readiness notification allows a service to send a notification when it's ready for use, rather than other programs having to continually poll the service to check whether it's ready or not. Emacs doesn't currently (2024-11-24) have builtin support for s6 readiness notification. However, we can nevertheless use s6-notifyoncheck to poll the Emacs server about whether it's ready to accept connections.
First, create a file in the service directory called notification-fd, containing only a number indicating the file descriptor that should be used for readiness notification. 3
is a reasonable value; it cannot be 0
, 1
, or 2
, as those are the file descriptors for STDIN, STDOUT, and STDERR, respectively.
3
Add a check file in the data directory of the service directory, creating the data directory if necessary. This is the program that s6-notifyoncheck will call to check if the service is ready or not; the service is considered ready when the check script returns 0.
#!/bin/sh
exec emacsclient -e '(if server-mode t)'
emacsclient(1) will return a non-zero value if the server is not yet accepting connections. Passing emacsclient the option -e '(if server-mode t)'
indicates that it shouldn't open a new frame, but instead try to run some ELisp. The actual ELisp is irrelevant, so (if server-mode t)
is used to indicate to the reader that we're checking whether an Emacs server is running.
Ensure the check file has execute permissions:
user $
chmod 700 $S6_SERVICES/emacs/data/check
Finally, modify the run script to get s6-notifycheck to start Emacs:
#!/bin/sh
# Create log file if necessary.
if [ ! -f "${HOME}/.logs/s6/emacs.log" ]
then
touch "${HOME}/.logs/s6/emacs.log"
fi
exec s6-notifyoncheck -n 20 /usr/bin/emacs --fg-daemon 1>>"${HOME}/.logs/s6/emacs.log" 2>&1
Note the -n 20
option. This sets the number of checks that will be made before giving up; the default is 7. The default time between checks (specified by the -w
option) is 1000 milliseconds, so setting -n 20
means that Emacs will have 20 x 1000 = 20000 milliseconds = 20 seconds to reach a state of readiness. Depending on your Emacs configuration, a lower value might be sufficient.
Now, s6-svstat can be used to check whether the readiness status of the service:
user $
s6-svstat $S6_SERVICES/emacs
and s6-svwait can be used to block until the service is ready. Additionally, the -wU
option can be passed to s6-svc:
-wU : s6-svc will not exit until the service is up and ready as notified by the daemon itself. If the service directory does not contain a notification-fd file to tell s6-supervise to accept readiness notification, s6-svc will print a warning and act as if the -wu option had been given instead.
An s6 D-Bus session bus service
The dbus
service provided by systemd and OpenRC only provides a system bus, used for communications that are relevant system-wide (e.g. hardware being connected or disconnected). A session bus, used for communications relevant to a specific user session (e.g. that an email for a specific user has been received), needs to be created separately.
We can create a session bus service that can notify other services when is sufficiently ready, and in particular, only start the above Emacs service once that occurs.
The following again assumes the existence of the S6_SERVICES and XDG_STATE_HOME directories, as described above.
cd to S6_SERVICES and create a dbus-session-bus directory:
user $
cd $S6_SERVICES
user $
mkdir dbus-session-bus
Change to the dbus-session-bus directory, and create a file in it called run:
user $
cd dbus-session-bus
#!/bin/sh
umask 077 # Ensure files created are only readable by this user.
# Create log file if necessary.
if [ ! -f "${HOME}/.logs/s6/dbus-session-bus.log" ]
then
touch "${HOME}/.logs/s6/dbus-session-bus.log"
fi
rm -f "${XDG_STATE_HOME}/session-bus-address"
exec s6-notifyoncheck /usr/bin/dbus-daemon --session --print-address 4 --print-pid 5 4>|"${XDG_STATE_HOME}/session-bus-address" 5>|"${XDG_STATE_HOME}/session-bus-pid" 1>>"${HOME}/.logs/s6/dbus-session-bus.log" 2>&1
This will save the session bus address to $XDG_STATE_HOME/session-bus-address (and the session bus PID to $XDG_STATE_HOME/session-bus-pid) for later reference.
Make the run file executable:
user $
chmod 700 run
Create a file called notification-fd:
3
Create a directory called data, and create a file in that called check
#!/bin/sh
if [ -s "${XDG_STATE_HOME}/session-bus-address" ]
then
exit 0
else
exit 1
fi
This check script will return 0 once the $XDG_STATE_HOME/session-bus-address file, containing the value for DBUS_SESSION_BUS_ADDRESS, has been created; the file will have been deleted by the run script before dbus-run-session is started. Once the script returns 0, s6-notifyoncheck will consider the dbus-session-bus
service ready.
Make the data/check file executable:
user $
chmod 700 data/check
The Emacs service can now be modified to wait until the dbus-session-bus
service is ready, e.g.
#!/bin/sh
# Create log file if necessary.
if [ ! -f "${HOME}/.logs/s6/emacs.log" ]
then
touch "${HOME}/.logs/s6/emacs.log"
fi
s6-svwait -U "${XDG_STATE_HOME}/s6/services/dbus-session-bus"
export DBUS_SESSION_BUS_ADDRESS=$(cat "${XDG_STATE_HOME}/session-bus-address" | tr -d '\n')
exec s6-notifyoncheck -n 20 /usr/bin/emacs --fg-daemon 1>>"${HOME}/.logs/s6/emacs.log" 2>&1
Tying it together: supervising the supervisors
As noted at earlier, individual s6-supervise processes shouldn't be started manually. Instead they should be started via a scan directory, or scandir. A scandir is a collection of services intended to be supervised by s6-svscan.
The scandir should be separate from the servicedir. Create another 'shortcut' variable to store the path to the scandir, and then create that directory:
user $
S6_SCANDIR=$XDG_STATE_HOME/s6/scandir
user $
mkdir -p $S6_SCANDIR
The services to be supervised are then simply symlinks to the services in the S6_SERVICES directory:
user $
cd $S6_SCANDIR
user $
ln -s $S6_SERVICES/dbus-session-bus .
user $
ln -s $S6_SERVICES/emacs .
Then, rather than having to start s6-supervise manually for each service, we just start s6-svscan on the scandir, e.g.
user $
s6-svscan -- "${S6_SCANDIR}" 1>>"${HOME}/.logs/s6-svscan.log" 2>&1 &
Refer to the s6-svscan documentation for options, such as whether to rescan the scandir on a regular basis (the -t
option) and whether to notify of readiness to accept commands from s6-svscanctl (the -d
option).
s6-svscanctl can be used to collectively manage the services being supervised, such as by sending them all a SIGTERM
or SIGHUP
(the -t
option), or reaping all zombies (the -z
option). Refer to the linked documentation for further information.
Service management with s6-rc
s6-rc builds on s6. It not only provides service supervision and management, but also additional functionality, such as dependency management and service bundles.
Initial setup
Install sys-apps/s6-rc (and, optionally, app-doc/s6-rc-man-pages):
root #
emerge --ask sys-apps/s6-rc
root #
emerge --ask sys-apps/s6-rc-man-pages
If sys-apps/s6 is not already installed, it will be installed as a dependency.
Specify the location of your s6-rc service definitions. For this example, we'll store the services in ${XDG_DATA_HOME}/s6-rc/services:
user $
S6RC_SERVICES="${XDG_DATA_HOME}/s6-rc/services"
Create the user scan directory, e.g.:
user $
S6RC_SCANDIR="${XDG_STATE_HOME}/s6-rc/scandir"
user $
mkdir -p "${S6RC_SCANDIR}"
This directory doesn't get populated manually - s6-rc populates it based on a compiled database, more on which below.
Finally, this section assumes that there's a directory called ~/.logs/s6-rc, which services can refer to; create that directory if necessary:
user $
mkdir -p ~/.logs/s6-rc
s6-rc services and logging services
The s6-rc service directory format expands that of s6. In addition to the run script and the notification-fd file containing the file descriptor for notifications, an s6-rc service directory must also contain at least:
- A file named type, containing the type of service; and
- A file named producer-for, containing the name of the logging service for the service.
An s6-rc logging service for a service is just another s6-rc service. In addition to the run, notification-fd, and type files, the logging service directory must also contain at least:
- A file named consumer-for, containing the name of the service being logged; and
- A file named pipeline-name, containing the name of the logging pipeline.
However, the format supports functionality well beyond these minimums; refer to the documentation for s6-rc-compile for further information.
As with s6 services, s6-rc services generally do not need to be written in the execline scripting language; service scripts can be written in any shell or scripting language, using the appropriate shebang line (e.g.
#!/bin/sh
) instead of #!/usr/bin/execlineb -P
. The only exception to this is that up and down scripts for oneshot services must be written in execline, but even there, those scripts can simply chain load a script written in any language.An s6-rc D-Bus session bus service
Create the service directory and change to it:
user $
mkdir $S6RC_SERVICES/dbus-session-bus
user $
cd $S6RC_SERVICES/dbus-session-bus
Create the run file, which is the same file used in the s6 service above, without logging-related redirections:
"${XDG_STATE_HOME}/session-bus-address" 5>
Make the run file executable:
user $
chmod 700 run
Add the same data/check and notification-fd files as for the s6 service:
#!//bin/sh
if [ -s "${XDG_STATE_HOME}/session-bus-address" ]
then
exit 0
else
exit 1
fi
3
Make the data/check file executable:
user $
chmod 700 data/check
Note that s6-rc doesn't know that a service is up until notified, so if the software doesn't itself provide s6-style readiness notification, s6-notifyoncheck will need to be used in the run file and a data/check file will need to be written.
Now create a file called type. This file will contain only the type of the service; in this case, it's a longrun
:
longrun
Finally, create a file called producer-for. This file will contain only the name of the logging service for this service:
dbus-session-bus-log
The s6-rc dbus-session-bus-log service
The dbus-session-bus-log service will use s6-log to perform logging services.
Create the logging service directory and change to it:
user $
mkdir $S6RC_SERVICES/dbus-session-bus-log
user $
cd $S6RC_SERVICES/dbus-session-bus-log
Create the run file:
#!/bin/sh
exec /usr/bin/s6-log -d3 -- t "${HOME}/.logs/s6-rc/dbus-session-bus"
The -d3
option specifies the notification descriptor to use, and the t
argument is an s6-log control directive, as described in the s6-log documentation:
The logged line will be prepended with a TAI64N[2] timestamp (and a space) before being processed by the next action directive.
Make the run file executable:
user $
chmod 700 run
Create the notification-fd file:
3
Create the type file:
longrun
Create a file, to indicate which service's output it's logging:
dbus-session-bus
Finally, create a pipeline-name file, to specify a name for the pipeline between the logged service and the logging service:
dbus-session-bus-pipeline
The service database
With the dbus-session-bus and dbus-session-bus-log services now defined, we need to specify that we want to use them. We'll do so by creating a wanted directory that links to all the services we want to use
user $
S6RC_WANTED="${XDG_STATE_HOME}/s6-rc/wanted"
user $
mkdir -p $S6RC_WANTED
user $
cd $S6RC_WANTED
Within the S6RC_WANTED directory, create symlinks to the wanted services:
user $
ln -s $S6RC_SERVICES/dbus-session-bus .
user $
ln -s $S6RC_SERVICES/dbus-session-bus-log .
We can now use s6-rc-compile to compile a service database in $XDG_STATE_HOME/s6-rc/compiled:
user $
S6RC_COMPILED="${XDG_STATE_HOME}/s6-rc/compiled"
user $
s6-rc-compile "${S6RC_COMPILED}" "${S6RC_WANTED}"
Starting s6-rc
We're now in a position to start s6-rc.
First, start s6-svscan on the scan directory we specified above. For this example, we'll assume there's a pre-existing file $HOME/.logs/s6-svscan.log for logging the output of the s6-svscan process:
user $
s6-svscan -- "${S6RC_SCANDIR}" 2>>"${HOME}/.logs/s6-svscan.log"
Next, for the purposes of this guide, create a 'shortcut variable' that will contain the name of a directory that s6-rc can create to store the live state of the s6-rc system.
user $
S6RC_LIVE="${XDG_RUNTIME_DIR}/s6-rc"
Now start s6-rc via s6-rc-init:
user $
S6RCs6-rc-init -c $S6RC_COMPILED -l $S6RC_LIVE $S6RC_SCANDIR
The s6-rc command can now be used to control services. For example, to start the dbus-session-bus service:
user $
s6-rc -l $S6RC_LIVE start dbus-session-bus
Note that the live directory needs to be specified.
Starting the dbus-session-bus service should also result in the dbus-session-bus-log service being started. We can check this via s6-svstat:
user $
s6-svstat $S6RC_SCANDIR/dbus-session-bus-log
up (pid 15767 pgid 15767) 276 seconds, ready 276 seconds
We can also stop the dbus-session-bus service, and confirm that it's down:
user $
s6-rc -l $S6RC_LIVE stop dbus-session-bus
user $
s6-svstat $S6RC_SCANDIR/dbus-session-bus
down (exitcode 0) 3 seconds, ready 3 seconds
However, the logging service will need to be stopped manually:
user $
s6-rc -l $S6RC_LIVE stop dbus-session-bus-log
If service definitions are changed, the live database can be updated via s6-rc-update:
user $
s6-rc-update -l $S6RC_LIVE $S6RC_WANTED
Specifying dependencies
Service dependencies - that is, services which a given service needs to be running before it can start - can be specified by creating a directory named dependencies.d containing files whose names are the required services, with the files' contents being irrelevant.
For example, if an emacs service needs the dbus-session-bus service running before it starts, then the emacs servicedir should contain a dependencies.d directory containing a file named dbus-session-bus.
Refer to the documentation for s6-rc-compile for further information.
Restarting services
Services can be restarted using s6-svc. Assuming the above configuration, the following POSIX sh wrapper function can be defined:
s6rc-svc() { cmd='s6-svc' while [ ${#} -gt 0 ] do if [ ${#} -gt 1 ] then cmd="${cmd} ${1}" else cmd="${cmd} ${XDG_RUNTIME_DIR}/s6-rc/${1}" fi shift done $cmd }
to enable a service to be restarted like so:
user $
s6rc-svc -r emacs
Alternatively, in Zsh, and assuming the Zsh KSH_ZERO_SUBSCRIPT and KSH_ARRAYS options are unset, the wrapper function can be defined as:
function s6rc-svc { service=${argv[-1]} opts=${argv[1,-2]} s6-svc ${opts} "${XDG_RUNTIME_DIR}/s6-rc/servicedirs/${service}" }