Contacting the UNIX Daemons of Old¶
Daemons is a type of program on UNIX-based systems that run unobtrusively in the background unlike most applications you may be used to where you are in direct control. On Windows these types of programs are often called services however the concept is relatively the same, however it’s important to note that while services can be daemons, not all services are daemons - a user application with a graphical user interface could have a service built into it (something often seen is file sharing applications).
Important
Mac OS X is a UNIC based system and uses daemons, while the term service is used for software that performs a function selected from the services menu.
In UNIX daemons are usually started as a process. A process is a running instance of a program. Processes are run by the kernel (the core of the operating system) which assigned a unique PID.
So a daemon is created when it’s parent process is terminated and the daemon is assigned a PID of one as it has no parent process and no controlling terminal – however, when speaking more generally a daemons can be any background process whether it is a child of another process or not.
There are generally a number of steps that need to be taken by a computer to turn a process into a daemon which we will be covering here, but don’t worry this isn’t daemon making one-o-one, I’ll leave teaching that to the more advanced witches and wizards.
(Optional) Remove unnecessary variables from the environment - variables not passed when the daemon is started are often lost to the ether as the child process has no context of what the parent was doing
Execute as a background task by forking (breaking off) and exiting in the parent half of the fork. This allows the daemons parent to receive exit notifications and run like normal
Detach from the session that invoked the daemon
Dissociating from the controlling tty
Create a new session and become the leader of that session
Become a process group leader
If the daemon wants to ensure that it won’t acquire a new controlling tty even by accident the daemon may fork and exit again.
Set the root directory
/
as the current working directory so the process does not keep any directory in use that may be on a mounted file system – this allows the computer to continue normal operation and allow unmounting of the file systemChange the
umask
to 0 - this allowsopen()
,creat()
and other operating system calls to specify their own permissions and not depend on those provided by the parentClose all files opened by the parent, this may include file descriptors, standard streams (
stdin
,stdout
andstderr
). Any files required by the daemon can be opened later, or passed inUsing a logfile, the console, or
/dev/null
asstdin
,stdout
, andstderr
.
Further to this, you will often hear people talk about what makes a “well-behaved” daemon, and let me tell you it generally means it stays in the circle you summoned it in without needing to bring out the table salt.
Using system.d
¶
systemd
is a UNIX based software suite that provides the fundamental building blocks for Linux. It includes a System and Service Manager as well as an init
system that can be used to bootstrap user space, and manage user processes. systemd
aims to unify service configuration and behavior across Linux distributions.
Important
systemd
often makes daemonising applications a thing of the past, however it’s important to consider who is using your application and whether they are using systemd
.
Once you have your Python application ready to go you will want to create a service file for systemd
. This will need the .service
extension and it should be saved in /lib/systemd/system/
(this will require sudo
).
vim /lib/systemd/system/myapplication.service
You will want to add the following content to the file. Ensure you change the script filename and location as well as the description.
[Unit]
Description=Dummy Service
After=multi-user.target
Conflicts=getty@tty1.service
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/bin/dummy_service.py
StandardInput=tty-force
[Install]
WantedBy=multi-user.target
To escape and save the file using sudo
you can use the following :w !sudo tee %
Once you have created the service you will want to reload the systemctl
daemon to read the new file.
Important
You will need to make sure you reload the daemon each time you make changes to the .service
file.
$ sudo systemctl damemon-reload
Next, we will want to enable the servie to start on system-boot as well as start the service in general.
$ sudo systemctl enable myapplication.service
$ sudo systemctl start myapplication.service
If you want to check the status of the service you can run the status
command which will show you important information about wether the status is running, the pid
it is using, memory usage and CPU usage.
$ sudo systemctl status myapplication.service
Finally, if for any reason you need to stop the service you can simply run the stop
command.
$ sudo systemctl stop myapplication.service
Using python-daemon
¶
python-daemon
is based on PEP 3143 which defines a standard daemon process library. It is not in Python 3.7 by default so should be installed using pipenv install python-daemon
.
Important
Many examples on Google and Stack Overflow will suggest you use the DaemonRunner
object to handle your daemon, however this is now considered depricated and DameonContext
should be used instead.
To get started with DaemonContext
all you need to do is create a with
control flow using the DaemonContext
.
with daemon.DaemonContext():
main()
This implementation will give you a working albeit simple implementation of a daemon. You’re probably wondering about all the other things like setting the working directory, preserving important files and handling operating system signals. So let’s get into it!
Handling the File System¶
As we mentioned earlier daemons are funny little things, because they are unbound from our command (i.e. we have no control over them) will have its own keys to be identified user-wise. This means that, irrespective of the user that started a daemon, it will have its own UID, GID, its own root and working directories, and its own umask. While the default configuration will handle all of this automatically, sometimes we need to customise to make sure our little miscreants work the way we expect.
To change the root directory, useful for confining your daemon to it’s directory, you can set the chroot_directory
variable to a valid directory on your file system. The working_directory
can be set in a similar way, and it the more common way of confining you daemon. By default, DaemonContext will set your working directory to root “/”.
with daemon.DaemonContext(
chroot_directory=None,
working_directory='/var/lib/ose'):
print(os.getcwd())
Tip
Ose is a Great President of Hell and part of the Goetia. Ose can also conveniently turn people into loafs of bread.
However you might notice that you don’t get any output from print(os.getcwd())
this is because when we daemonise a process step seven says that we “Close all files opened by the parent, this may include file descriptors, standard streams (stdin
, stdout
and stderr
). Any files required by the daemon can be opened later, or passed in”. But never fear! Preserving files is straight-forward enough and is covered in the next section.
Configuring the UID and GID may also be critical to preserve any privilege elevation the user who started the daemon may of had. This can easily be done by setting the uid
and/or the gid
. Keep in mind your user running the daemon will need to have these permissions, in the case they don’t DaemonContext
will raise a DaemonOSEnvironmentError
exception.
with daemon.DaemonContext(
uid=1001,
gid=777):
print(os.getuid())
print(os.getgid())
Additionally, you might want to set the daemon umask, which will set the mode the daemon will create files with.
with daemon.DaemonContext(
umask=0o002):
your_mask = os.umask(0)
print(your_mask)
os.umask(your_mask)
How to Calculate umask
¶
To calculate the final permission for directories you can simply subtract the umask from the base permissions to determine the final permission for a directory. Keeping in mind the left most bit is the permission for the owner, the second left-most bit is for the group and the final bit if for others.
Octal Value |
Permission |
---|---|
|
read, write and execute |
|
read and write |
|
read and execute |
|
read only |
|
write and execute |
|
write only |
|
execute only |
|
no permissions |
Preserving Files¶
So while we know closing any files is what the DaemonContext
is supposed to do, this can sometimes be undesirable as we need particular files open. We can ensure when the daemon starts it still has access to these files by specifying which files should remain open using the files_preserve
variable.
some_important_file = open('camio.db', 'r')
with daemon.DaemonContext(
files_preserve=['camio.db']):
print(some_important_file.readlines())
Tip
Camio will answer questions, tell you about the past and teach you a thing or two about “Liberal Sciences” as well as grant you “the Understanding of all Birds, Lowing of Bullocks, Barking of Dogs and other Creatures; and also of the Voice of the Waters.”.
So along with keeping files open, we can also redirect stdin
, stdout
and stderr
from os.devnull
and keep them open.
with daemon.DaemonContext(
stdout=sys.stdout,
stderr=sys.stderr):
print("Hello Forneus!")
Tip
The demon Marquis Forneus is all about chit-chat – which is why we conveniently say hello! Forneus can also make you totally rad at rhetoric, which is the art of talking to people and getting them to think and do what you want. Like thinking they are a loaf of bread. But not really that’s Ose’s job.
Handing Operating System Signals¶
Signals coming from the operating system are important irrespective of the way the process is used. Because of this it makes it even more important to ensure we preserve these signals as they may be one of the few ways a user can interact with the daemon. DaemonContext
will allow you to define a dictionary using the signal_map
argument that allows you to map to common signals used.
Signal |
Portable Number |
Default action |
Description |
---|---|---|---|
|
6 |
Terminate (core dump) |
Process abort signal |
|
14 |
Terminate |
Alarm clock |
|
N/A |
Terminate (core dump) |
Access to an undefined portion of a memory object |
|
N/A |
Ignore |
Child process terminated, stopped, or continued |
|
N/A |
Continue |
Continue executing, if stopped |
|
N/A |
Terminate (core dump) |
Erroneous arithmetic operation |
|
1 |
Terminate |
Hangup |
|
N/A |
Terminate (core dump) |
Illegal instruction |
|
2 |
Terminate |
Terminal interrupt signal |
|
9 |
Terminate |
Kill (cannot be caught or ignored) |
|
N/A |
Terminate |
Write on a pipe with no one to read it |
|
N/A |
Terminate |
Pollable event |
|
N/A |
Terminate |
Profiling timer expired |
|
3 |
Terminate (core dump) |
Terminal quit signal |
|
N/A |
Terminate (core dump) |
Invalid memory reference |
|
N/A |
Stop |
Stop executing (cannot be caught or ignored) |
|
N/A |
Terminate (core dump) |
Bad system call |
|
15 |
Terminate |
Termination signal |
|
5 |
Terminate (core dump) |
Trace/breakpoint trap |
|
N/A |
Stop |
Terminal stop signal |
|
N/A |
Stop |
Background process attempting read |
|
N/A |
Stop |
Background process attempting write |
|
N/A |
Ignore |
High bandwidth data is available at a socket |
|
N/A |
Terminate |
User-defined signal 1 |
|
N/A |
Terminate |
User-defined signal 2 |
|
N/A |
Terminate |
Virtual timer expired |
|
N/A |
Ignore |
Terminal window size changed |
|
N/A |
Terminate (core dump) |
CPU time limit exceeded |
|
N/A |
Terminate (core dump) |
File size limit exceeded |
import signal
def shutdown(signum, frame): # signum and frame are mandatory
sys.exit(0)
with daemon.DaemonContext(
signal_map={
signal.SIGTERM: shutdown,
signal.SIGTSTP: shutdown
}):
main()
There Can Be Only ONE!¶
Daemons often use resources, the problem with some resources is that only one thing can access them at a time. This is often the case for TCP ports or some files on disk. Therefore, you want to make sure that multiple daemons aren’t competing for these resources as it can often lead to exceptions or race-conditions.
To ensure only one daemon is running at a time we can create a PID lock file. This is a file that contains the PID of a process that prevents the same program from running on more than one instance.
Note
Part of the spawning a new process is ensuring there is no lock file.
import lockfile
with daemon.DaemonContext(
pidfile=lockfile.FileLock('/var/run/spam.pid')):
main()
Starting/Stopping/Restarting¶
This is unfortunately where the benefits of python-daemon
run out. DaemonContext
doesn’t take care of this functionality for you and while DaemonRunner
does have code in regard to this behaviour it is not advisable for you to use it as it is deprecated. For those feeling adventurous you can always use it as a reference for building out these functions.
One extension to this (although not the most eloquent) can be useful in getting around this problem.
Given a Python application that does something (that’s the technical term for it anyway) we can create an initialisation shell script that runs the application for you and manages termination and restarting.
#!/usr/bin/env python3.5
import sys
import os
import time
import argparse
import logging
import daemon
from daemon import pidfile
debug_p = False
def do_something(logf):
### This does the "work" of the daemon
logger = logging.getLogger('eg_daemon')
logger.setLevel(logging.INFO)
fh = logging.FileHandler(logf)
fh.setLevel(logging.INFO)
formatstr = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
formatter = logging.Formatter(formatstr)
fh.setFormatter(formatter)
logger.addHandler(fh)
while True:
logger.debug("this is a DEBUG message")
logger.info("this is an INFO message")
logger.error("this is an ERROR message")
time.sleep(5)
def start_daemon(pidf, logf):
### This launches the daemon in its context
with daemon.DaemonContext(
working_directory='/var/lib/eg_daemon',
umask=0o002,
pidfile=pidfile.TimeoutPIDLockFile(pidf),
) as context:
do_something(logf)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Example daemon in Python")
parser.add_argument('-p', '--pid-file', default='/var/run/eg_daemon.pid')
parser.add_argument('-l', '--log-file', default='/var/log/eg_daemon.log')
args = parser.parse_args()
start_daemon(pidf=args.pid_file, logf=args.log_file)
You can then use the following .sh
script to start, stop and restart the application. One of the features I prefer to add when creating an application this way is also a run()
command that allows me to not daemonise the application. This can be especially helpful for debugging or if I don’t want it running in the background.
#!/bin/bash
# ------------------------------------------------------------------
# A bash script for better management of python-daemon
# ------------------------------------------------------------------
VERSION=0.1.0
SUBJECT=some-unique-id
USAGE="Usage: command -ihv args"
startscript(){
# get the current PID of the script you are trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# if the PID does not exist start the application
if [ -z "$PID" ]; then
./scriptname.py start
else
# else print an error and do not start the process
echo -n "ERROR: The process is already running."
echo
fi
}
stopscriptname(){
# get the current PID of the script you are trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# if the PID does not exist there is nothing to stop
if [ -z "$PID" ]; then
echo -n "ERROR: scriptname is not running"
else
# else kill the script using the UNIX in built kill
kill $PID
fi
}
statusscriptname(){
# get the current PID of the script you are trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# return if the application is running or not and return the PID
if [ -z "$PID" ]; then
echo "scriptname is not running"
else
echo "scriptname is running with PID $PID"
fi
}
runscriptname(){
# get the current PID of the script you are trying to run
PID=$(ps aux | grep '[s]criptname.py' | awk '{print $2}')
# if the PID does not exist run the application like normal - do not daemonise
if [ -z "$PID" ]; then
./scriptname.py run
else
# else print an error and do not start the process
echo -n "ERROR: The process is already running."
echo
fi
}
restartscriptname(){
# run the stop script and then the start script to restart the process (turn it off and on again)
stopscriptname
startscriptname
}
case "$1" in
# sort of like a case statement, depending on the first argument determines which function to run
start) startscriptname ;;
stop) stopscriptname ;;
run) runscriptname ;;
restart) restartscriptname ;;
status) statusscriptname ;;
*) echo "usage: $0 start | stop | run | restart | status" >$2
exit 1
;;
esac