Flushed with success from yesterday’s post where I made my first systemd service, I got carried away and wanted to show you how to create a service that runs as a regular user.
A fairly common question on the Raspberry Pi Forums is “How do I run a script every time I reboot?”. The traditional answer (and one I’ve given more than once) is to add a @reboot clause to your crontab. This will indeed run a command when the computer reboots, but it will run pretty early on in the boot sequence when there’s no guarantee of network or time services. So the usual remedy is a bit of a kludge:
@reboot sleep 60 &&…
This waits a full minute after rebooting, then executes the command. Network and time services are really likely to be available, but it’s not very elegant. Cron also has some weird gotchas with PATH settings, so while it’s ubiquitous and has worked for decades, it’s not easy to get working. Systemd, however, has a much better way of doing it, and better yet, you can do it all without ever hitting sudo.
I’ll take as a basis for this post the forum query “python and crontab”. The asker wanted to log the time when their Raspberry Pi had rebooted, but they’ve hit the usual problem that the clock didn’t have the right time when their script was triggered, so the log was useless.
(I’m not going to do exactly what the forum poster did, but this is more a demo of a systemd user service than recreating their results.)
First off, here’s the script to log the time to a file (saved as
#!/usr/bin/python3 from time import strftime with open("/home/pi/logs/boot_time.txt", "a") as log: log.write(strftime("%d-%m-%Y,%H:%M:%S\n"))
I’d have done this as a shell script, but the OP used Python, so why fight it?
FUN FACT: Under most Linux flavours, if you create a
bin folder in your home directory, it’s automatically added to your path. So I could just type
boot_time.py and the shell would find it.
(You might have to log out and log back in again for the shell to review your path.)
In order to get that to run, I need to do a little housekeeping: make the script executable, and make sure the
logs folder exits:
chmod +x ~/bin/boot_time.py mkdir -p ~/logs
Now we need to do the bits that pertain to systemd. First off, you must make a folder for user services:
mkdir -p ~/.config/systemd/user
mkdir -p … is useful here as it makes the directory and any parent directories that don’t exist. It also doesn’t complain if any of them already exist. It’s kind of a “make sure this directory exists” command. Make friends with it.
And here’s the service file, which I saved as
[Unit] Description=boot time log DefaultDependencies=no After=local-fs.target time-sync.target [Service] Type=oneshot ExecStart=/home/pi/bin/boot_time.py [Install] WantedBy=default.target
The service file does the following (even if I’m slightly mystified by some of the headings …):
- Description — a plain text name for the service. This appears in logs when it starts, stops or fails.
- DefaultDependencies — as this service runs once at boot-up, it doesn’t need the normal systemd functions of restarting and shutting down on reboot. Most service files omit this line.
- After — here we tell systemd what service targets must be running before this service is started. As we need to write to a file and have the right time, the
- Type — this is run once, so it’s a
oneshotrather than the usual
- ExecStart — this is the command to run when the service is required.
- Type — this is run once, so it’s a
- WantedBy — tbh no idea what this does, but if you omit it the service won’t install. Found the answer in this SE, and it works. So I guess what it does is make the service not fail …
Finally, you enable the service with:
systemctl --user enable boot_time_log.service
Next time you reboot, the time will be appended to the log file
Unlike most (that is,
Type=simple) services, it’s perfectly fine if this one spends most of its time inactive:
$ systemctl status --user boot_time_log.service ● boot_time_log.service - boot time log Loaded: loaded (/home/pi/.config/systemd/user/boot_time_log.service; enabled; Active: inactive (dead) since Sun 2017-10-22 22:17:56 EDT; 1h 5min ago Process: 722 ExecStart=/home/pi/bin/boot_time.py (code=exited, status=0/SUCCES Main PID: 722 (code=exited, status=0/SUCCESS)
It has executed successfully, so the process doesn’t have to stick around.