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:
~/dinit.d/udiskie
===========
type=process
command=udiskie
~/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.
# Create boot.d folder with links to dependencies
mkdir ~/dinit.d/boot.d
~/dinit.d/boot
=========
type=internal
waits-for.d=boot.d
After creating the folder and rewriting boot, what you need to do is:
# 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
dinitctl enable/disable also has a
--from option, which makes it manage
waits-for.d of services other than boot.
Example:
~/dinit.d/graphical
==================
type=internal
waits-for.d=graphical.d
$ mkdir -p ~/dinit.d/graphical.d
$ dinitctl enable --from graphical foot-server
Service 'foot-server' has been enabled.
$ dinitctl enable --from graphical pipewire
Service 'pipewire' has been enabled.
Now,
dinitctl start graphical will start both foot-server and pipewire.
On starting user Dinit
Option 1 - From a complete graphical session
The easiest way to use dinit is to put 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:
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.
~/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}"
~/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:
~/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:
~/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].
~/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:
~/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 ) and mentioning waits-for.d on a comment.