If you have a Bash script that needs to run periodically, you can run it using a crontab entry. But you can also have it invoked by Systemd using systemd.timer.
Furthermore, you can run Systemd services as user-level services instead of the typical system-level service for even further isolation.
Running via Systemd provides more powerful constructs for invocation, configuration, monitoring, and logging. In this article, I will show how to periodically run a simple Bash script using a user-level Systemd timer.
Prerequisites
Install the package prerequisites and download the github project code.
# packages sudo apt install curl git -y # pull project git clone https://github.com/fabianlee/systemd-periodic-bash.git cd systemd-periodic-bash
Running user-level Systemd service
Now use the supplied script to create the user-level Systemd service and timer, and run the Bash script every 60 seconds. We will go into the details of how this works in the upcoming sections.
cd systemd-userlevel # create user specific systemd files, start ./create-simple-userservice.sh # configure environment variable # set 'env_arg' to any value, I set mine to 'my foo user' vi ~/default/simple
If you tail the logs, you should see output similar to below every 60 seconds.
$ tail -f ~/log/simple/simple.log ======================== Mon 03 Jan 2022 08:10:38 PM EST ======================== env_arg environment variable: my foo fabian loop counter 1 loop counter 2 loop counter
Overview
Because we are running this Bash script via Systemd, we get a lot of production-level features over using a simple cron. Basically, we get all the power of Systemd.
- Invocation – run once at boot, periodically, or after the network is initialized
- Isolation – as a specific user or group
- Configuration – pull in properties from ‘/etc/default’ or other locations
- Monitoring – process monitored for success/error, watchdog process
- Logging – to syslog or file
Systemd service unit file
Our simple.service file is shown below. The “$userdir” and “$scriptdir” values are placeholders, replaced by create-simple-userservice.sh.
[Unit] Description=Simple service user level ConditionPathExists=$scriptdir/simple-bash.sh After=network.target [Service] Type=forking #User= #Group= # define 'env_arg' env var, secured by root ownership and mode 600 EnvironmentFile=-$userdir/default/simple WorkingDirectory=$scriptdir ExecStart=$scriptdir/simple-bash.sh 3 # make sure log directory exists and owned by syslog PermissionsStartOnly=true ExecStartPre=/bin/mkdir -p $userdir/log/simple ExecStartPre=/bin/chmod -R 755 $userdir/log StandardOutput=append:$userdir/log/simple/simple.log StandardError=append:$userdir/log/simple/simple.err
This has the effect of:
- Starting the service after the network is initialized (After=network.target)
- Running the service as the current user (#User commented out)
- Loading environment keys from an optional default file (EnvironmentFile)
- Invoking the Bash script with a command line argument (ExecStart)
- Hooks that run before the invocation (ExecStartPre)
- Appending output of script to file (StandardOutput)
service and timer files
The .service and .timer files used by Systemd are kept in “~/.config/systemd/user” for user-level services.
These are enabled and started like this.
service=simple # enable and start service and timer systemctl --user enable $service.service $service.timer systemctl --user start $service.service systemctl --user start $service.timer
REFERENCES
lostsaloon.com, running cron as specific user
silentlad.com, explanation of systemd timers OnCalendar
man7.org, man page for systemd.timer
NOTES
check that user level systemd process is running
# you should see 'lib/systemd/systemd --user' ps aux | grep systemd | grep "^$USER"
tried reinstalling dbus
sudo apt install --reinstall dbus sudo apt install --reinstall dbus-user-session
checked env vars
id -u id -un echo $XDG_RUNTIME_DIR echo $DBUS_SESSION_BUS_ADDRESS
tried enabling linger
loginctl enable-linger $USER
this showed errors, so restarted
# tried restart of login service sudo systemctl restart systemd-logind. service # then errors were shown in user system, so restarted sudo systemctl status user@1000.service --no-pager -l sudo systemctl restart user@1000.service # then this worked systemctl --user status