Up: GROVE
Last edit: <2021-01-04>

systemd user services


This is a text version of a lighting talk I gave at recurse center Feb 2020.

Back when I was first getting into Linux, I had arch installed on my laptop that I was using for schoolwork, and was trying all the latest mnml window managers, which meant there was no builtin display for how much battery I had left.

Some googling showed me that you could do the following to get the charge on Linux:

$ cat /sys/class/power_supply/BAT0/capacity
92

That’s a pain to type out, so I aliased it to something spam-able like a and called it a day. But then… surprise shutdowns still happened on occasion. So I just got into the habit of spamming checking all the time due to laptop FOMO. This is not ideal.

Next step was to wrap the above in a script and display it in my panel. Nice! now the battery status is always a glance away. BUT! pixel real estate is precious on my 1366 x 768 resolution, so I would often hide the panel or full-screen something like media, or my text editor. We are back where we started.

1 Alerts

With something like dunst, I can send notifications that appear only when I’m concerned with them. This birthed a script that would only bother me if my battery was below a certain level, while polling periodically. I decoupled that last part into a script named periodically, so ended up with a battery_alert daemon-like script that looked like periodically 60 check_battery.

Making this call in my .xinitrc worked out OK, but I’m sometimes at a desktop using the same set of dotfiles, and there’s no battery to check there. About this point I realized I had stuffed a lot in my startup, such as mpd, picom, dunst, you name it. I had heard of systemd user services and decided to look into them.

2 The big D

Systemd user services are regular systemd service files that live in $HOME/.config/systemd/user. A service file matching the above problem might look like this:

battery_alert.service:

[Unit]
Description=Notify when the battery is low

[Service]
ExecStart=/path/to/bin/periodically 60 check_battery
ExecStop=/path/to/bin/kill -int $MAINPID

Environment=PATH=/paths/that/contain/check_battery:...:....
Environment=DISPLAY=":0"
Environment=XAUTHORITY="/home/neeasade/.Xauthority"

[Install]
WantedBy=default.target

Some things to note:

  • The full path to the ExecStart and ExecStop executable
  • The inclusion of DISPLAY and XAUTHORITY – this is so the notify-send reaches dunst
  • The setting of PATH such that “the usual suspects” are available within my daemon script

With the above I get the systemd goodness but for managing my local stuff, just passing --user:

# start, stop
systemctl --user start battery_alert
systemctl --user stop battery_alert

# logs (can also tail theme)
systemctl --user status battery_alert

# automatic startup
systemctl --user enable battery_alert

So the next step in this systemd journey was to integrate service generation into my dotfiles setup, generating a number of service files that contain full paths to binaries with intended arguments for a range of daemons.

3 The big.. T?

Those of you in the know will see something very wrong with the above. Systemd has a “when to run” concept: timers! Using timers files you can do things like:

  • run on boot
  • run on login
  • run once per day/once per time interval
  • run periodically

Let’s take a look at what that last one looks like. You create a file in the same directory with the same name, but extension timer:

battery_alert.timer:

[Unit]
Description=Run check_battery every 60 seconds

[Timer]

# on boot
# OnBootSec=0min

# on service manager activation
OnStartupSec=0min

OnUnitActiveSec=60s

[Install]
WantedBy=timers.target

See the reference for time-span syntax here

And then we would change the ExecStart in our battery_alert.service file to:

ExecStart=/path/to/check_battery

And bam! we have done the thing.