One of systemd's (admittedly) good features is the ability for non-privileged users to manager their own services without needing root privileges at all. You can actually accomplish the same thing (for much much less) by using the power of s6 and s6-rc. It just takes a little bit of setup.
Note: You can actually set this up on any Artix installation regardless of what init system you personally use. s6 and s6-rc can be installed on any machine without interfering with any boot-related things. What differs across the init system is the part that starts the local user's s6-rc services on boot (which is optional of course). If you want to get your feet wet trying s6, this wouldn't be a bad start.
Setup the local source directories:
The source directory for your services can be anywhere your user has read access. I am following the XDG_DATA_DIR spec, so my source directory is in ~/.local/share. We will keep things simple by copying the structure of the /etc/s6 folder. My directory structure looks like this:
~/.local/share
└── s6
├── rc
└── sv
Or as commands that would simply be:
$ mkdir ~/.local/share/s6
$ mkdir ~/.local/share/s6/rc
$ mkdir ~/.local/share/s6/sv
Now we need to actually put some services in ~/.local/share/s6/sv. Any userspace daemons and/or oneshot scripts you want to run are good candidates here. For this example, I will use udiskie which is a userspace daemon I use for automounting usb devices.
$ mkdir ~/.local/share/s6/sv/udiskie
Then we just need to make the run and type files here. They look like this.
~/.local/share/s6/sv/udiskie/run
--------------------
#!/bin/execlineb -P
exec udiskie
~/.local/share/s6/sv/udiskie/type
-------------------
longrun
I used execline for my run script because is a lightweight, non-interactive shell language designed to work well with s6 and s6-rc. However, you can use any scripting language you want with s6-rc*. It does not care as long as the shebang is valid.
*Technically a oneshot type (up script) must be execlineb because of how s6-rc internally works. This script can simply be one single call to another script though (i.e. exec sh /path/to/shell/script) so there is no limitation in practice. Some of the oneshots in s6-scripts work this way.
For convenience, let's also make a bundle called default that contains udiskie for this user so all services/scripts can be brought up with a single command.
$ mkdir -p ~/.local/share/s6/sv/default/contents.d
$ touch ~/.local/share/s6/sv/default/contents.d/udiskie
~/.locah/share/s6/sv/default/type
-------------------
bundle
Of course, there's a ton of things you can put in these directories. For the full details, see the upstream documentation for the source directories and service directories.
Setup and compile the s6-rc database:
Note: The s6-base package in Artix comes with scripts that do the below process for you. You can simply run "s6-db-reload -u" to update your local user database. It assumes you store them in "~/.local/share/s6/sv". The below section is left for historical/informational reasons.
Now that we have our service, we need to create and setup the s6-rc database. First, let's compile it.
$ s6-rc-compile ~/.local/share/s6/rc/compiled-$(date +%s) ~/.local/share/s6/sv
This command takes two arguments. The first is simply the path to the database you are creating and the second is the path of your source directories. If you notice, the databases name is in the format of compiled-$(date +%s). That command merely gives a unix timestamp of the current time to ensure that the database name is unique. You can call your databases whatever you like, but I would highly recommend using something that generates unique names everytime you run it.
Now that the database exists, we need to make the symlink. Remember that symlinks must be absolute paths. Replace timestamp below with whatever the database actually is.
$ ln -sf /home/${USER}/.local/share/s6/rc/compiled-timestamp /home/${USER}/.local/share/s6/rc/compiled
Important note: If you are updating/changing to a new database (first by executing s6-rc-update) and the compiled symlink already exists, you must make the new symlink atomically. The default ln does not do this. This will be omitted in this guide but consult the upstream documentation on database management for more details.
At this point, you're probably thinking, "this sucks can't I make life easier?" Don't worry, the answer is yes. Artix has used a script for automatically updating and changing databases ever since s6 was first implemented. With a little bit of tweaking, it can be easily used for local services. Here it is below.
#!/bin/sh
DATAPATH="/home/${USER}/.local/share/s6"
RCPATH="${DATAPATH}/rc"
DBPATH="${RCPATH}/compiled"
SVPATH="${DATAPATH}/sv"
SVDIRS="/run/${USER}/s6-rc/servicedirs"
TIMESTAMP=$(date +%s)
if ! s6-rc-compile "${DBPATH}"-"${TIMESTAMP}" "${SVPATH}"; then
echo "Error compiling database. Please double check the ${SVPATH} directories."
exit 1
fi
if [ -e "/runp/${USER}/s6-rc" ]; then
for dir in "${SVDIRS}"/*; do
if [ -e "${dir}/down" ]; then
s6-svc -x "${dir}"
fi
done
s6-rc-update -l "/run/${USER}/s6-rc" "${DBPATH}"-"${TIMESTAMP}"
fi
if [ -d "${DBPATH}" ]; then
ln -sf "${DBPATH}"-"${TIMESTAMP}" "${DBPATH}"/compiled && mv -f "${DBPATH}"/compiled "${RCPATH}"
else
ln -sf "${DBPATH}"-"${TIMESTAMP}" "${DBPATH}"
fi
echo "==> Switched to a new database for ${USER}."
echo " Remove any old unwanted/unneeded database directories in ${RCPATH}."
Just run that as your local user and as long as you follow the paths in there it should just work™. Feel free to modify to your liking.
Now for this next part, I am going to assume you are an Artix s6 user and you want to hook up this new database to your overall supervision tree (that runs as root). If this is not you, you can skip down to the bonus section.
Plugging the local user database to the root supervision tree:
Now it's time to use administrator privileges to finish the job. I implemented this by creating a user-services bundle, a local-s6-user longrun, and a local-s6-rc-user oneshot. However let's first create a simple conf file (/etc/s6/config/user-services.conf) for ease of use.
/etc/s6/config/user-services.conf
---------------------------------
# username for the user-services bundle
USER=username
If you want to do multiple users, you could easily put more variables in there as needed.
Now let's setup that user-services bundle.
$ mkdir -p /etc/s6/adminsv/user-services/contents.d
$ touch /etc/s6/adminsv/user-services/local-s6-user
$ touch /etc/s6/adminsv/user-services/local-s6-rc-user
/etc/s6/adminsv/user-services/type
------------
bundle
For s6-rc to work, we first need an s6-svscan process running. Since this is for the local user, we will make sure all of the commands in this script are run as the local user. We also need to pick a scan directory for s6-svscan to use. It needs to be something the local user has full read/write access to. In this example, the /run/${USER}/service directory will be used. Upstream recommends having this be a RAM filesystem (such as tmpfs) and it works the best with s6-rc. Here are the details.
$ mkdir -p /etc/s6/adminsv/local-s6-user/dependencies.d
$ touch /etc/s6/adminsv/local-s6-user/dependencies.d/mount-filesystems
/etc/s6/adminsv/local-s6-user/notification-fd
----------------
3
/etc/s6/adminsv/local-s6-user/run
---------------------------
#!/bin/execlineb -P
envfile /etc/s6/config/user-services.conf
importas -uD "username" USER USER
foreground { install -d -o ${USER} -g ${USER} /run/${USER} }
foreground { install -d -o ${USER} -g ${USER} /run/${USER}/service }
s6-setuidgid ${USER} exec s6-svscan -d 3 /run/${USER}/service
/etc/s6/adminsv/local-s6-user/type
---------------
longrun
While this script does parse the conf file for the USER variable, note that the "username" part allows for a fallback USER in case the envfile fails somehow. Take advantage of it to put your user in there.
Now finally, it is time for the local-s6-rc-user piece. This is merely a oneshot that runs after we have local-s6-user running.
$ mkdir -p /etc/s6/adminsv/local-s6-rc-user/dependencies.d
$ touch /etc/s6/adminsv/local-s6-rc-user/mount-filesystems
$ touch /etc/s6/adminsv/local-s6-rc-user/local-s6-user
/etc/s6/adminsv/local-s6-rc-user/down
-----------------
#!/bin/execlineb -P
envfile /etc/s6/config/user-services.conf
importas -uD "username" USER USER
foreground { s6-setuidgid ${USER} s6-rc -l /run/${USER}/s6-rc -bDa change }
foreground { s6-setuidgid ${USER} rm -r /run/${USER}/service }
s6-setuidgid ${USER}
elglob -0 dirs /run/${USER}/s6-rc*
forx -E dir { ${dirs} }
rm -r ${dir}
/etc/s6/adminsv/local-s6-rc-user/type
----------------
oneshot
/etc/s6/adminsv/local-s6-rc-user/up
-----------------
#!/bin/execlineb -P
envfile /etc/s6/config/user-services.conf
importas -uD "username" USER USER
foreground { s6-setuidgid ${USER}
s6-rc-init -c /home/${USER}/.local/share/s6/rc/compiled -l /run/${USER}/s6-rc /run/${USER}/service }
s6-setuidgid ${USER}
exec s6-rc -l /run/${USER}/s6-rc -up change default
The same note about the "username" applies here.
Now we can finally update our root/administrator database.
$ sudo s6-db-reload
Let's start up that local s6-rc database session.
$ sudo s6-rc -u change user-services
That's it! Now you have a fully local process supervisor and fully local service manager for the user to use. You just have to supply the s6-rc command the -l argument to point to the correct live database. In the case of our udiskie example.
$ s6-rc -l /run/${USER}/s6-rc -u change udiskie
Or if you want to bring the user's default bundle down, that would be:
$ s6-rc -l /run/${USER}/s6-rc -d change default
The really nice thing about this setup is that the local s6-svscan process is completely supervised. It will never die during the lifetime of the machine unless you purposely tell it to die. If you blindly give the PID a kill command, the s6-supervise process for it will simply respawn it. Your user can continue to use s6-rc and s6 commands on their local services as normal. These scripts are fairly generic you so if you want to add more users, you can basically just copy and paste and just change a few paths/variable names and add them to the user-services bundle. To get these services to always start on boot, just add user-services to your default bundle in the root database and you are good to go.
Bonus: I don't want to plug it into an existing supervision tree/I'm not using s6 as init
In the previous section, all the commands that are run to start up s6-rc are completely done as a local user. You don't have to plug it into any existing frame work if you don't want to. Here's a quick and dirty way to get it working on any system.
First, we need to get s6-svscan working. Let's create those folders in /tmp.
$ mkdir /tmp/${USER}
$ mkdir /tmp/${USER}/service
$ s6-svscan /tmp/${USER}/service
The s6-svscan will run in the foreground. This is good and what you want. Keep that running. In a new terminal, let's do this.
$ s6-rc-init -c /home/${USER}/.local/share/s6/rc/compiled -l /tmp/${USER}/s6-rc /tmp/${USER}/service
$ s6-rc -l /tmp/${USER}/s6-rc -up change default
That's it! You have a fully local s6-rc ready for use. Just be sure to pass "-l /tmp/${USER}/s6-rc" to all of your s6-rc commands like so:
$ s6-rc -l /tmp/${USER}/s6-rc -u change udiskie
If you want to quit, just send the s6-svscan process the kill signal. The various directories in /tmp will need to be cleaned up/removed if you wish to start up s6-rc again. Note that it is 100% possible to run these commands in another init system's startup process if you want to (openrc, runit, even systemd would work). I'll leave that as an exercise to the reader if he is very curious.
Final Thoughts:
This ended up being a quite a bit wordier and longer than I expected, but I hope it is interesting. To my knowledge, nobody has really detailed how to get something like this setup. All the information you need is in the skarnet documentation, but it is scattered across quite a few different pages and you need to conceptually understand the system to piece it all together. I hope this was useful to people. I know I want to start migrating more things to be supervised/handled by s6-rc at least.