Skip to main content
Topic: dinit for user services (Read 5834 times) previous topic - next topic
0 Members and 2 Guests are viewing this topic.

dinit for user services

A lot of ideas will come from Dudemanguy's post on the same concept.



In addition to system management, Dinit is explicitly intended for user services (even changing its default paths if you invoke it as a normal user). I believe it's a great fit for this use case due to its ease-of-use (including declarative configs) and more versatile dependency/restart handling. All of this in a modular and well-scoped package, in the way users of this distribution certainly appreciate: by installing just dinit-base, you can spawn dinit from whichever init you choose (for instance, I'm leaving system management to s6 while dinit takes care of user services).

Setting up the service directory

Dinit-as-user looks for services on ~/dinit.d. To make our lives easier, we'll use it.

Service definitions for Dinit consist of a file in a KEY=VALUE format, i.e. are declarative (like systemd or suite66). To have a clearly defined service, you need at least two keys:

  • type: the kind of service. It can be internal (do nothing but depend on other services), process (a supervised service, running on the foreground, runit/s6 style) and script (simply run a script - pretty much a oneshot). There's additionally bgprocess, but unless you're trying to supervise a closed-source service that forks, it's not relevant here.
  • command: the command to be run and its arguments. Remember that it must execute the service in the foreground. Internal services do not need command.

There are three types of dependencies.

  • depends-on: This service must succeed before our service starts; if it fails or is stopped, our service will stop too.
  • depends-ms: This service must succeed before our service starts; but if it's stopped, our service will stay.
  • waits-for: This service will be brought up when ours starts, if it's not already present, with no regard for if it succeeds or not.

There must be one dependency per line. This means writing, for instance, depends-on=service1 service2, will count as a dependency on a single service named literally "service1 service2" - write two lines with depends-on if you want to specify two dependencies).

Talking about dependencies upfront is necessary because the official way to start services on system boot (or on our case, when our user service tree starts) is by creating an type=internal service called boot. Dinit will look for a service with this name and automatically start it (and, here's the deal, its dependencies).

Let's reproduce the example on DMG's tutorial: starting udiskie with your user tree:

Code: [Select]
~/dinit.d/udiskie
===========
type=process
command=udiskie
Code: [Select]
~/dinit.d/boot
===========
type=internal
waits-for=udiskie

We defined a service that runs udiskie in the foreground, and specified it as a waits-for dependency of boot, so it's called when dinit is started. You can test it by running dinit from your terminal (While I'm a self-admitted a s6 fanboy, I'll admit it can't compete with this on the "quick and easy" department).

Other than hand-writing boot and rewriting+reloading it every time you want to enable a service (what we did here), instead of writing multiple lines of waits-for, you can replace it with a single waits-for.d directive. It tells dinit to read a subfolder of dinit.d and use the services linked there as waits-for-type dependencies. dinitctl enable/disable can then manage this folder for you, which makes it consistent with how dinit works system-wide on Artix.

Code: [Select]
# Create boot.d folder with links to dependencies
mkdir ~/dinit.d/boot.d

Code: [Select]
~/dinit.d/boot
=========
type=internal
waits-for.d=boot.d

After creating the folder and rewriting boot, what you need to do is:

Code: [Select]
# load new definition for boot - only needed if dinit is already running
$ dinitctl reload boot
Service 'boot' reloaded.
# make dinit add a link to boot.d for us.
$ dinitctl enable udiskie
Service 'udiskie' has been enabled.

On using dinitctl enable/disable for simulating s6-rc's bundles
Spoiler (click to show/hide)

On starting user Dinit

Option 1 - From a complete graphical session

The easiest way to use dinit is to put
Code: [Select]
dinit &
on your xinitrc, sxrc or Wayland compositor config to start user services, inheriting "for free" the environment variables needed for GUI programs to work. Remember to call dinitctl shutdown when your session exits to bring services (and dinit itself) down, otherwise you won't be able to restart it.

Option 2 - from PID 1

The other, slightly harder option is plugging it into your system's initialization, which requires some setup, but allows for supervised services regardless if the GUI is up. Dinit has an option -e which makes it source a file containing key-value pairs for environment variables, making the variables available to the running user services (on DMG's tutorial, it'd be equivalent to the user-services.conf file). For instance, mine is:

Code: [Select]
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/capezotte/dbus
XDG_RUNTIME_DIR=/run/user/1000
DISPLAY=:0
SWAYSOCK=/run/user/1000/sway.sock
WAYLAND_DISPLAY=wayland-1
XDG_CURRENT_DESKTOP=sway

Let's explain each entry here:
  • DISPLAY, WAYLAND_DISPLAY and XDG_RUNTIME_DIR are variables necessary for user services to be graphical programs - these specify the locations where things like X11 or Wayland compositors are waiting for clients.
  • XDG_CURRENT_DESKTOP is needed for xdg-desktop-portal-wlr.
  • SWAYSOCK is where Sway awaits for commands coming from swaymsg and the like. Just exporting SWAYSOCK before starting Sway is enough for it to agree with our environment file, so you can now have services calling swaymsg (for instance, terminals in daemon mode). If your WM has fancy IPC like Awesome and i3, making equivalent "agreements" between this file and your window manager would be a good idea. Read relevant docs.
  • DBUS_SESSION_BUS_ADDRESS is a fixed location for D-Bus sending-and-receiving of messages. Next up I'll make a quick tutorial showcasing dinit's dependency management by starting D-Bus and a dependent service.
  • If you're using X11, remember add XAUTHORITY variable there - startx uses XAUTHORITY=/home/youruser/.Xauthority, while sx uses XAUTHORITY=/home/youruser/.local/share/sx/xauthority.

Dinit can't import variables after it's started like systemd can with systemctl --user import-environment, so, if you want to supervise graphical user services, setting these to predefined, known values (or launching dinit from the graphical environment itself, where these variables are already set) is necessary.

Advanced example #1 - D-Bus + Notification Daemon

On non-systemd distributions, user D-Bus is usually a unsupervised service that spawns more unsupervised services. We can do slightly better than that[1].

First, we can write a D-Bus service that listens on a fixed address, passed to it through either the file on the -e option, or through a call to export DBUS_SESSION_BUS_ADDRESS=... before dinit itself.

Code: [Select]
~/dinit.d/dbus.sh
===========
#!/bin/sh
exec dbus-daemon --nofork --nopidfile --print-address="${DINIT_FD?:No ready fd}" --address="${DBUS_SESSION_BUS_ADDRESS:?No address}"
Code: [Select]
~/dinit.d/dbus
==========
type=process
command=/home/youruser/dinit.d/dbus.sh
ready-notification=pipevar:DINIT_FD

Here we showcase interesting another feature of Dinit: it has readiness notification compatible with s6 (i.e. print a newline to the specified descriptor), and extends it by allowing it to be a file descriptor chosen by dinit itself. We've used pipevar (fd chosen by dinit and passed as a variable), but it could also be pipefd:# (pipe on a fixed number greater than 3; then we'd write whatever number # is after --print-address=).

Then we can write the notification daemon definition:
Code: [Select]
~/dinit.d/mako
==========
type=process
command=mako
depends-on=dbus

Now we have a dependency estabilished between them with readiness notification: mako will only start after dbus is listening, and it will be supervised. You can add more services following this model.

For this to work with a user Dinit plugged to the main system service set, your -e file needs to have XDG_RUNTIME_DIR and WAYLAND_DISPLAY set, so it knows what Wayland session to connect to; if this were dunst, it needs DISPLAY and XAUTHORITY set.

Advanced example #2 - GPG Agent

I think this is too interesting not to mention, and will also serve as an example of a service with a -pre counterpart.

Many people (including me) have tried to run gpg-agent --supervised on runit only to get cryptic error messages about LISTEN_FDS not being set. LISTEN_FDS being how systemd passes sockets to programs under its Socket Activation(tm) regimen. S6 can be made to work with this requirement, but it requires a somewhat deep understanding of execline to implement properly. With dinit, it's one line on the service definition.

First, GPG-using programs will try to connect to gpg-agent through a socket at $XDG_RUNTIME_DIR/gnupg/S.gpg-agent. We can write a service with the relevant socket-listen directive:

Code: [Select]
~/dinit.d/gpg-agent
==============
type=process
socket-listen=/run/user/1000/gnupg/S.gpg-agent
command=gpg-agent --supervised

Starting this service, however, can fail if the /run/user/1000/gnupg folder doesn't exist yet. One easy way to fix that is running a oneshot that will create this folder (which must be only readable by the user due do gpg's security requirements -- hence the -m 700 option) if it doesn't exist [2].

Code: [Select]
~/dinit.d/gpg-agent-pre
==============
type=scripted
command=mkdir -m 700 -p /run/user/1000/gnupg

The we can make gpg-agent start our gpg-agent-pre oneshot:

Code: [Select]
~/dinit.d/gpg-agent
==============
type=process
socket-listen=/run/user/1000/gnupg/S.gpg-agent
command=gpg-agent --supervised
depends-ms=gpg-agent-pre



[1] I really wish it would be easy to supervise D-Bus user services outside of systemd, but D-Bus hardcodes systemd's "start service" APIs and, if systemd is not available, it starts the service unsupervised through itself. For system services, we can repurpose D-Bus "spawn unsupervised services as root" mini-executable as a "start services through a non-systemd supervisor" script - to extend this to user services, however, a two-line patch is needed. Therefore, you need to start your notification daemon ASAP or else D-Bus might spawn it unsupervised.

[2] You might be wondering: "just make the service mkdir ... && exec gpg-agent". Well, command is not interpreted by a shell - echo $VAR && will literally write '$VAR &&' to the logs.



Ended quite lengthy, but I'm just wanted to show examples of what's possible.

Thanks to Dudemanguy for inspiring the post and providing a point of reference for it, and for konimex for packaging Dinit (I wouldn't be aware of it otherwise :P) and mentioning waits-for.d on a comment.

Re: dinit for user services

Reply #1
The guide is solid already. But I just want to add one more possible tweaks to make boot even more modular.
Instead of adding several services manually to boot, you can edit your boot service to become like this.

type = internal
waits-for.d = boot.d

Of course, you have to enable those services. Manual link is possible by adding symlinks from the main dinit.d directory to boot.d (e.g. ln -s ../bspwm $HOME/dinit.d/boot.d/) before dinit is started. Of course, when dinit is already started, you can easily add the service by using plain old dinitctl enable and dinitctl disable.
now only the dinit guy in artix

Re: dinit for user services

Reply #2
Warning: due to Artix's packaging, this is currently only possible if you're using Dinit as PID1 (or change the PKGBUILD to split off the reboot, half and shutdown tools).
The basic dinit package is now dinit-base, it should not conflict with other init packages so it's now possible to make user services with dinit.
now only the dinit guy in artix

Re: dinit for user services

Reply #3
Corrected some typos and inconsistencies, and mentioned dinit-base on the post.

Thanks @konimex.

Re: dinit for user services

Reply #4
In section "Advanced example #1 - D-Bus + Notification Daemon" dbus-daemon can be run directly from its service description file, without dbus.sh:

Code: (ini) [Select]
type = process
command = /usr/bin/dbus-daemon --session --print-address 3 --address $DBUS_SESSION_BUS_ADDRESS
load-options = sub-vars
ready-notification = pipefd:3

Notice that arguments for dbus are set without '=' (otherwise it doesn't work) and load-options set to sub-vars

Re: dinit for user services

Reply #5
I would also like to share how to run sway with readiness notification (it's actually obvious)
Sway service file looks like:

Code: (ini) [Select]
type = process
command = /usr/bin/sway
ready-notification = pipefd:3

and then in sway config file add

Code: [Select]
exec echo >&3

Now services dependent on sway (like waybar, mako and gammastep) will wait and won't fail to start before sway is really ready.

Also env-file is undocumented setting may be convenient.

Re: dinit for user services

Reply #6
Just want to ask, how might the gpg-agent service be done in the case of a custom GNUPGHOME? I'm finding that when using the script as given, any GPG commands insist on throwing up an entirely separate gpg-agent daemon.

EDIT: I figured it out. The path for the socket files changes depending on any custom GNUPGHOME to be under a hashed subdirectory. Hence, it's possible to use something like this to define a custom socket path to listen on.

Code: [Select]
GNUPGSOCKET="$XDG_RUNTIME_DIR/gnupg/$(gpgconf --list-dirs | grep "socketdir" - | sed 's:.*/::')"
export GNUPGSOCKET