Systemd timer for user script

Hello everyone. I’m trying to write a timer for systemd that runs a script I wrote. The script checks whether the battery is charged over 80% or under 40% and notifies the user using notify-send.

According to journalctl --user -u bat_max the problem seems to be that the service unit doesn’t find the executable script in my ~/.local/bin/ folder. I followed the advice in ArchWiki’s systemd/User article to make the PATH environment variable available to systemd so that it can find the executable. Only difference is that instead of editing the ~/.bash_profile file, I edited the ~/.bashrc file which is where I set the PATH env variable. When I run

systemctl --user show-environment

I obtain

...
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/<user>/.local/bin
...

So I’m not sure why it isn’t finding the executable. When I run the executable from the command line, it does so without any issues.

Any ideas about what could be going wrong are much appreciated.

Here’s the relevant output of journalctl -xb --user -u bat_max.

Dec 26 02:03:46 systemd[991]: Starting Check if battery is over 80% charge and notify...
...
Dec 26 02:03:46 systemd[14286]: bat_max.service: Failed to locate executable check-battery-charge.sh: No such file or directory
░░ Subject: Process check-battery-charge.sh could not be executed
░░ Defined-By: systemd
░░ Support: https://lists.freedesktop.org/mailman/listinfo/systemd-devel
░░ 
░░ The process check-battery-charge.sh could not be executed and failed.
░░ 
░░ The error number returned by this process is ERRNO.
Dec 26 02:03:46 systemd[14286]: bat_max.service: Failed at step EXEC spawning check-battery-charge.sh: No such file or directory
....
Dec 26 02:03:46 systemd[991]: bat_max.service: Main process exited, code=exited, status=203/EXEC
░░ Subject: Unit process exited
░░ Defined-By: systemd
░░ Support: https://lists.freedesktop.org/mailman/listinfo/systemd-devel
░░ 
░░ An ExecStart= process belonging to unit UNIT has exited.
░░ 
░░ The process' exit code is 'exited' and its exit status is 203.
Dec 26 02:03:46 systemd[991]: bat_max.service: Failed with result 'exit-code'.
░░ Subject: Unit failed
░░ Defined-By: systemd
░░ Support: https://lists.freedesktop.org/mailman/listinfo/systemd-devel
░░ 
░░ The unit UNIT has entered the 'failed' state with result 'exit-code'.
Dec 26 02:03:46 systemd[991]: Failed to start Check if battery is over 80% charge and notify.
░░ Subject: A start job for unit UNIT has failed
░░ Defined-By: systemd
░░ Support: https://lists.freedesktop.org/mailman/listinfo/systemd-devel
░░ 
░░ A start job for unit UNIT has finished with a failure.
░░ 
░░ The job identifier is 7508 and the job result is failed.

Here are the timer, the service unit and the script in case it’s useful.

bat_max.timer:

[Unit]
Description=Check if battery is over 80% charge and notify

[Timer]
AccuracySec=1sec
OnStartupSec=30sec
OnUnitActiveSec=1min

[Install]
WantedBy=timers.target

bat_max.service:

[Unit]
Description=Check if battery is over 80% charge and notify

[Service]
RuntimeDirectory=battery-notification
RuntimeDirectoryPreserve=yes
Type=oneshot
ExecStart=check-battery-charge.sh

[Install]
WantedBy=bat_max.timer

check-battery-charge.sh:

#!/bin/bash

currentCharge=`cat /sys/class/power_supply/BAT0/charge_now`
fullCharge=`cat /sys/class/power_supply/BAT0/charge_full`
tmpFile="battery-notification-sent"
if (( $(echo "$currentCharge/$fullCharge >= .8" | bc -l) )); then
    if [ ! -e "$tmpFile" ]; then
        notify-send --urgency=critical --icon=battery "Battery" \
                    "Hey, this is your battery talking: I'm charged above 80%"
        paplay /usr/share/sounds/Oxygen-Sys-App-Error-Serious-Very.ogg
        touch "$tmpFile"
    fi
elif (( $(echo "$currentCharge/$fullCharge <= .4" | bc -l) )); then
    if [ ! -e "$tmpFile" ]; then
        notify-send --urgency=normal --icon=battery "Battery" \
                    "Hey, I'm charged below 40%"
        paplay /usr/share/sounds/Oxygen-Sys-App-Error-Serious-Very.ogg
        touch "$tmpFile"
    fi
elif [ -e "$tmpFile" ]; then
    rm "$tmpFile"
fi

Thanks for reading and having the intention to help me.

Why dont you use the full path in “Exec” then?

EDIT:
You might want to add “User=” and “Group=” to your service file.

2 Likes

I think your need to retrieve the path from your path variable. Or you need the user= to use that. Try something like,

[Unit]
Description=vmmount
After=sysinit.target
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=no
RemainAfterExit=yes
User=root
WorkingDirectory=/etc/systemd/system
ExecStart=bash -c "mount -t 9p -o trans=virtio,version=9p2000.L /VMShare /home/s4ndm4n/VMShare"

[Install]
WantedBy=multi-user.target

This is a script I use. Change it to match yours.

:point_up:

I would always use fully qualified paths in a service file unless you can’t for some reason.

I thought you had to use the full paths anyway in systemd script Exec field, with some optional %... wildcards for things like the user’s runtime directory. However, you can use the systemd script to call a shell script that doesn’t.

I’m not using the full path because that wouldn’t be portable from my system to yours. I’d like to upload the code and share it with other people that would like to get notifications when the battery crosses a threshold.

For the same reason, I’d prefer not to add “User=” and “Group=” to the service.

It may be though that the only way to make the installation of this timer easy is to write an installation script.

Just to make sure, is this file marked as executable?

Yes. In fact, I can run it from the command line.

$ ls -l ~/.local/bin/
total 0
lrwxrwxrwx 1 user group 54 Dec 13 18:13 check-battery-charge.sh -> /home/user/code/systemd/bat-max/check-battery-charge.sh
$ ls -l ~/code/systemd/bat-max/
total 12
-rw-r--r-- 1 user group  250 Dec 26 01:55 bat_max.service
-rw-r--r-- 1 user group  166 Dec 26 01:59 bat_max.timer
-rwxr-xr-x 1 user group 1289 Dec 20 03:33 check-battery-charge.sh

What if you add the script symlink to /usr/local/bin?

It would be if you created a PKGBUILD for it which would install the script to the proper location.

That being said, if you want it to use your path just use bash -c "script" in your exec.

It works. Thanks :slight_smile:

Yet, that requires the user has root superpowers to create the symlink.

Thank you for the suggestions. Using bash -c "script" did the trick.

If I understand you correctly, your first suggestion is to make this a global systemd timer instead of a per-user timer. It seems to me that this particular script would be more suited to a per-user timer. Am I understanding you correctly? Why do you think it would be better to make it a global timer?

No, not at all.

You were saying that you didn’t want to put the full path again because it wouldn’t be portable.

The script that a user timer calls can be installed to the system. It doesn’t need to be in the home directory.

1 Like

I see. So you’re proposing that the script check-battery-charge.sh to be installed system-wide using a PKGBUILD but bat_max.timer and bat_max.service to be installed in ~/.config/systemd/user/ by the user manually?

The unit files also don’t have to be in the home folder. They can be installed to system locations as well. They just need to be run with --user

Yeah I see. A PKGBUILD could be created for this in case the user has root superpowers. I’ll start by uploading the code somewhere first. Then I’ll create the PKGBUILD but I haven’t uploaded one since the times you could just upload a tarball directly on the AUR website. I’d have to learn how to upload to the AUR now that they use git.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.