This is a rather long post going into the detail of making a Linux service with Python. It’s long, but hopefully because it contains enough detail to be helpful! Let me know on Twitter in the usual place if it’s helped…
This post is going to cover how to get a Linux computer to run your Python code when it starts up. Why might you want to do this? Well, perhaps you’re building a robot and want to start up a control menu when you turn it on, or a weather station which should start capturing data when it’s connected to power (including after a power cut!). I’m using the Raspberry Pi 3 running Raspbian but these instructions should apply to any similar system.
Specifically, we want to:
Our solution consists of three components:
When you’re creating a service, you really don’t want to accidentally change something elsewhere which prevents it from running or which changes its behaviour. Python provides a mechanism to handle this, and in ‘Isolating your Python with VirtualEnv’ I outline why and how you can do this (see that post for more detail). For now we’ll create a new virtual environment in the default user’s home directory. Open a shell, and, as the ‘pi’ user, run the following:
cd /home/pi virtualenv venv
You should see a new directory ‘venv’ created in your home directory, and if you look inside that directory you’ll see it contains a full Python installation. At this point you can activate the virtual environment and install any libraries you might need for your code. For example, I might want to install the picamera module because my code needs access to the camera module on the Pi – remember that this environment is completely separate from the system Python, so you’ll have to re-install any libraries you need even if they’re included in the default operating system installation of Python!
By way of example, here’s how I’d install picamera:
cd /home/pi source venv/bin/activate pip install picamera
We’ve now got our very own Python installation, which we’ll use exclusively for our new service, and we’ve installed any libraries we’re going to need. Next we need the actual Python code we want to run!
The following code is a skeleton – it doesn’t do anything interesting but it handles all the plumbing your service needs to respond to shutdown signals, change users etc. Create a new file ‘myservice.py’ in the ‘pi’ user’s home directory and copy this into it:
#!/home/pi/venv/bin/python import grp import os import pwd from signal import signal, SIGINT, SIGTERM from sys import exit def drop_privileges(uid_name='nobody', gid_name='nogroup'): if os.getuid() != 0: # We're not root so, like, whatever dude return # Get the uid/gid from the name running_uid = pwd.getpwnam(uid_name).pw_uid running_gid = grp.getgrnam(gid_name).gr_gid # Reset group access list os.initgroups(uid_name, running_gid) # Try setting the new uid/gid os.setgid(running_gid) os.setuid(running_uid) # Ensure a very conservative umask old_umask = os.umask(077) def get_shutdown_handler(message=None): """ Build a shutdown handler, called from the signal methods :param message: The message to show on the second line of the LCD, if any. Defaults to None """ def handler(signum, frame): # If we want to do anything on shutdown, such as stop motors on a robot, # you can add it here. print(message) exit(0) return handler signal(SIGINT, get_shutdown_handler('SIGINT received')) signal(SIGTERM, get_shutdown_handler('SIGTERM received')) # Do anything you need to do before changing to the 'pi' user (our service # script will run as root initially so we can do things like bind to low # number network ports or memory map GPIO pins) # Become 'pi' to avoid running as root drop_privileges(uid_name='pi', gid_name='pi') # Do the rest of your service after this point, you'll be running as 'pi' while True: # This is where the main work of your service should go pass
Let’s go through this from the top:
The very first line is a comment, but it’s a special kind of comment. Specifically, a comment line which starts with #!
will tell the operating system that the rest of the line is the command which should be run to run this script. So, putting this line there is saying that when Linux runs this Python script, it’s equivalent to saying /home/pi/venv/bin/python myservice.py
. You’ll notice that this is the absolute path to the Python interpreter in our virtual environment – by specifying that we use this we’ll get any libraries we installed into that environment in the previous step.
Next we import some basic bits and pieces from the standard Python language so we can use them later.
Lines 10 to 27 define a function, drop_privileges
which changes the current user. We’re going to configure the service to start running as root, but we don’t want to actually run the main meat of the service as the root user, that’s a terrible idea, so we create a function which will drop all our root privileges as soon as we don’t need them.
Lines 30 to 43 define a function, which itself returns a function (take a moment to look at the code if that sounds crazy!). The function it returns can be customised with a message, we’ll use this to create appropriate handler functions for different system events – the version here doesn’t really do anything, but if you had code you needed to run when your service shuts down this is the place to put it.
Lines 46 and 47 use our previously defined function and associates functions it produces with signals. Signals are a mechanism used by Linux to tell processes to terminate, stop, resume etc. The service infrastructure will send these signals to our script when we tell the service to stop, these lines will bind those signals to the functions we want to run when they’re received.
At this point you should put any code that needs root access. This might include opening a low number network port (for example, starting up a web server on port 80), or any other operation which requires a high level of access to the system. On my most recent robot I used a library which required root access to initialise but which then worked fine as a regular user, so I put the init functions for that library here.
Line 54 calls our drop_privileges
function to become the ‘pi’ user. From this point on we are running as that user rather than root. This matters, because it means that any potential security flaw in your service (such as failing to be careful enough when processing input from a web server) can only give the attacker access to a relatively unprivileged user. If they could gain root access they could do literally anything they wanted.
Line 57 is your ‘real’ service code. By this point you’ll be running as the appropriate user, you’ll be configured to respond to shutdown messages and can get on with whatever it is your service actually does! In my robot example, here’s where I look for a controller and start responding to button presses, show my control menus on the LCD display etc etc.
Before you go any further, you need to decide on a name for your service! This is more than just cosmetic, because you’ll need to make certain things match up; the names of these files does matter. In this example my service is called ‘viridia’, as that’s the name of my last PiWars robot. Obviously where you see that word in this script and the following explanation you’ll need to replace it with your service name.
Create a new file, named the same as your service, in /etc/init.d/
. You’ll need to run as root to do this, so e.g.
sudo nano /etc/init.d/viridia
Copy the following into that file:
#!/bin/sh ### BEGIN INIT INFO # Provides: viridia # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Viridia Robot Service # Description: Starts up the environment and top level menu for Viridia ### END INIT INFO # Change the next 2 lines to suit where you install your script # and what you want to call it DAEMON=/home/pi/myservice.py DAEMON_NAME=viridia # Add any command line options for your daemon here DAEMON_OPTS="" # This next line determines what user the script runs as. # Root generally not recommended but could be required for some libraries. DAEMON_USER=root # The process ID of the script when it runs is stored here: PIDFILE=/var/run/$DAEMON_NAME.pid . /lib/lsb/init-functions do_start () { log_daemon_msg "Starting system $DAEMON_NAME daemon" start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS log_end_msg $? } do_stop () { log_daemon_msg "Stopping system $DAEMON_NAME daemon" start-stop-daemon --stop --pidfile $PIDFILE --retry 10 log_end_msg $? } case "$1" in start|stop) do_${1} ;; restart|reload|force-reload) do_stop do_start ;; status) status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $? ;; *) echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}" exit 1 ;; esac exit 0
Again, line by line from the top:
Line 1 tells Linux that we should run this file using the shell.
Lines 3 to 11 are comments, but (again) a special kind of comment. These lines contain the information the service infrastructure needs to determine when your service should run, its name, description and suchlike. The Default-Start
and Default-Stop
sections contain ‘run levels’. When Linux start up and shuts down it moves through a set of states, and these lines are specifying that we should start up when entering some of those states and shut down on entering others. The effect of the lines as used here is to start on boot after the core of the system has been initialised, and to shutdown when we shutdown the operating system.
Lines 14 to 26 set up some variables we’ll use later. The comments should show what they’re for – DAEMON
is your Python script from the previous step, DAEMON_NAME
is the name of your service (see above!), DAEMON_OPTS
is any extra stuff you need passed on the command line to your script, and DAEMON_USER
is the user we’ll start as. In this case we start as root, but our Python code will quickly relinquish root access. If you never need root access in your Python code you can change the user here and not bother with any of the privilege management in the actual Python code.
Line 28 pulls in some premade functions we’ll then use.
The rest of the script isn’t something you have to edit – it uses the variables defined in lines 14 to 26 to run your code, setting up a file containing the process identifier so it can find it again later to stop it etc.
Once we’ve got our file in /etc/init.d/myservice
(assuming we’re calling it ‘myservice’) you need to make it executable, and then tell the service infrastructure about it. This will read those comments at the top of the script containing things like run-levels and use the information there to configure the service framework such that your service will be started and stopped at the appropriate times.
This also makes your Python script executable.
As root, do the following:
chmod a+x /etc/init.d/myservice chmod a+x /home/pi/myservice.py update-rc.d myservice defaults
Your service is now installed! Next we can look at how to manage it.
You can now use the service infrastructure provided by Linux to start, stop, restart, disable, enable and get the status of your service:
If you’ve followed the instructions so far your service will be installed and enabled to run on startup. If you ever want to disable the service (you’re not using it for now, or for whatever reason you don’t want it to run automatically) you can do:
sudo update-rc.d -f myservice remove
To re-enable the service:
sudo update-rc.d myservice defaults
You may, especially during development, want to manually control your service. For example, you might disable it from running automatically using the command above, but then once you’ve made some changes to the code you don’t really want to have to re-enable and reboot to have it run. For those cases you can use
sudo service myservice stop
… to stop a running service,
sudo service myservice start
… to start the service manually, and
service myservice status
… to see any messages about the service, check whether it’s running etc. (You can run this command as a regular user, you only need root access when you’re actually changing things).
You may find that some people recommend the cron
tool to accomplish some of the same functionality. This works because cron, the unix scheduler, has an ‘on startup’ trigger, but it won’t get you the monitoring or management you can get by using the service infrastructure. Conversely, cron is absolutely the right option if you need your code to run once every Tuesday or similar.
Viridia’s service script is the linux service script used by my PiWars robot, and viridia_service.py the Python code to run the robot service.
James Coyle’s Cheat Sheet is a quick reference to update-rc.d and service commands.
The Debian wiki page for LSB init scripts has more information about how to specify exactly how and when your script gets run by the service infrastructure.
What do you think?
You must be logged in to post a comment.