User:Flexibeast/guides/OpenRC user services via s6 and s6-rc

From Gentoo Wiki
Jump to:navigation Jump to:search

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:

FILE run
#!/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:

FILE ~/.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:

FILE run
#!/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

Note
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.

FILE $S6_SERVICES/emacs/notification-fd
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.

FILE $S6_SERVICES/emacs/data/check
#!/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:

FILE $S6_SERVICES/emacs/run
#!/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
up (pid 32684 pgid 32684) 282 seconds, ready 274 seconds

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
FILE run
#!/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:

FILE $S6_SERVICES/emacs/notification-fd
3

Create a directory called data, and create a file in that called check

FILE $S6_SERVICES/emacs/data/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.

FILE $S6_SERVICES/emacs/run
#!/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.

Important
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:

FILE $S6RC_SERVICES/dbus-session-bus/run
"${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:

FILE $S6RC_SERVICES/dbus-session-bus/data/check
#!//bin/sh

if [ -s "${XDG_STATE_HOME}/session-bus-address" ]
then
    exit 0
else
    exit 1
fi
FILE $S6RC_SERVICES/dbus-session-bus/notification-fd
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:

FILE $S6RC_SERVICES/dbus-session-bus/type
longrun

Finally, create a file called producer-for. This file will contain only the name of the logging service for this service:

FILE $S6RC_SERVICES/dbus-session-bus/producer-for
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:

FILE $S6RC_SERVICES/dbus-session-bus-log/run
#!/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:

FILE $S6RC_SERVICES/dbus-session-bus-log/notification-fd
3

Create the type file:

FILE $S6RC_SERVICES/dbus-session-bus-log/type
longrun

Create a file, to indicate which service's output it's logging:

FILE $S6RC_SERVICES/dbus-session-bus-log/consumer-for
dbus-session-bus

Finally, create a pipeline-name file, to specify a name for the pipeline between the logged service and the logging service:

FILE $S6RC_SERVICES/dbus-session-bus-log/pipeline-name
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}"

}