💾 Archived View for gemini.circumlunar.space › users › kraileth › neunix › eerie › 2019 › daemon_fre… captured on 2023-03-20 at 19:25:43. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
Here I'm republishing an old blog post of mine originally from November 2019. The article has been slightly improved.
Being a sysadmin by profession, I don't code. At least not often enough or with as high quality output that programmers would accept to call coding. I do write and maintain shell scripts. I also write new formulas for configuration management with SaltStack.
The latter is Python-based and after hearing mostly good things about that language, I've been trying to do some simple things with it for a while now. And guess what: It's just so much more convenient compared to using shell code! I'll definitely keep doing some simple tasks in Python, just to get some experience with it.
Not too long I thought about a little project that I'd try to do and decided to go with Python again. Thinking about what the program should do, I figured that a daemon would make a nice fit for it. But how do you write a daemon? Fortunately it's especially easy on FreeBSD. So let's go!
The first thing that I did, was to create a new file called _bdaemon.py_ (for "baby daemon") and use _chmod_ to make it executable. And here's what I put into it as a first test:
#!/usr/local/bin/python3.6 # Imports # import time # Globals # TTL_SECONDS = 30 TTL_CHECK_INTERVAL = 5 # Fuctions # # Main # print("Started!") for i in range(1, TTL_SECONDS + 1): time.sleep(1) if i % TTL_CHECK_INTERVAL == 0: print("Running for " + str(i) + " seconds...") print("TTL reached, terminating!") exit(0)
This very simple program has the shebang line that points the operating system to the right interpreter. Then I import Python's time module which gives me access to a lot of time-related functions. Next I define two global variables that control how long the program runs and in which interval it will give output.
The main part of the program first outputs a starting message on the terminal. It then enters a _for loop_, that counts from 1 to 30. In Python you do this by providing a list of values after the _in_ keyword. Counting to 5 could have been written as _for i in [1, 2, 3, 4, 5]:_ for example.
With _range_ we can have Python create a list of sequential numeric values on the fly - and since it's much less to type (and allows for dynamic list creation by setting the final number via a variable), I chose to go with that. Oh, BTW: In Python the last value of those ranges is _exclusive_, not inclusive. This means that _range(1, 5)_ leads to [1, 2, 3, 4] - if you want the 5 included in the list, you have to use _range(1, 6)_! That's why I add 1 to the TTL_SECONDS variable.
I use _time.sleep_ to create a delay in the loop block. Then I do a check if the remainder of the division of the current running time by the defined check interval is zero (% is the modulus operator which gives that remainder value of the division). If it is, the program creates more output.
Mind the indentation: In Python it is used to create code blocks. The _for_ statement is not indented, but it ends with a colon. That means that it's starting a code block. Everything up to (but not including) the second to last print statement is indented by four spaces and thus part of the code block. Said print statement is indented two levels (8 spaces) - that's because it's another block of its own started by the _if_ statement before it. We could create a third, forth and so on level deep indentation if we required other blocks beneath the if block.
Eventually the program will print that the TTL has been reached and exit the program with an error code of 0 (which means that there was no error).
Have you noticed the _str(i)_ part in one of the print statements? That is required because the counter variable "i" holds numeric values and we're printing data of a different type. So to be able to concatenate (that's what the plus sign is doing in this case!) the variable's contents to the rest of the data, it needs to match its type. We're achieving this by doing a conversion to a string (think converting the number 5 to the literal "5" that can be part of a line of text where it looks similar but is actually a different thing).
Oh, and the pound signs are used to start comments that are ignored by Python. And that's already it for some fundamental Python basics. Hopefully enough to understand this little example code (if not, tell me!).
The next thing to explore is signal handling. Since a daemon is essentially a program running in the background, we need a way to tell it to quit for example. This is usually done by using signals. You can send some of them to normal programs running in the terminal by hitting key combinations, while all of them can be sent by the _kill(1)_ command.
If you press CTRL-C for example, you're sending _SIGINT_ to the currently running application, telling it "abort operation". A somewhat similar one is _SIGTERM_, which kind of means "hey, please quit". It's a graceful shutdown signal, allowing the program to e.g. do some cleanup and then shut down properly.
If you use _kill -9_, however, you're sending _SIGKILL_, the ungraceful shutdown signal, that effectively means "die!" for the process targeted (if you've ever done that to a live database or another touchy application, you know that you really have to think before using it - or you might be in for all kinds of pain for the next few hours).
#!/usr/local/bin/python3.6 # Imports # import signal import time # Globals # TTL_SECONDS = 30 TTL_CHECK_INTERVAL = 5 # Fuctions # def signal_handler(signum, frame): print("Received signal" + str(signum) + "!") if signum == 2: exit(0) # Main # signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGQUIT, signal_handler) signal.signal(signal.SIGILL, signal_handler) signal.signal(signal.SIGTRAP, signal_handler) signal.signal(signal.SIGABRT, signal_handler) signal.signal(signal.SIGEMT, signal_handler) #signal.signal(signal.SIGKILL, signal_handler) signal.signal(signal.SIGSEGV, signal_handler) signal.signal(signal.SIGSYS, signal_handler) signal.signal(signal.SIGPIPE, signal_handler) signal.signal(signal.SIGALRM, signal_handler) signal.signal(signal.SIGTERM, signal_handler) #signal.signal(signal.SIGSTOP, signal_handler) signal.signal(signal.SIGTSTP, signal_handler) signal.signal(signal.SIGCONT, signal_handler) signal.signal(signal.SIGCHLD, signal_handler) signal.signal(signal.SIGTTIN, signal_handler) signal.signal(signal.SIGTTOU, signal_handler) signal.signal(signal.SIGIO, signal_handler) signal.signal(signal.SIGXCPU, signal_handler) signal.signal(signal.SIGXFSZ, signal_handler) signal.signal(signal.SIGVTALRM, signal_handler) signal.signal(signal.SIGPROF, signal_handler) signal.signal(signal.SIGWINCH, signal_handler) signal.signal(signal.SIGINFO, signal_handler) signal.signal(signal.SIGUSR1, signal_handler) signal.signal(signal.SIGUSR2, signal_handler) #signal.signal(signal.SIGTHR, signal_handler) print("Started!") for i in range(1, TTL_SECONDS + 1): time.sleep(1) if i % TTL_CHECK_INTERVAL == 0: print("Running for " + str(i) + " seconds...") print("TTL reached, terminating!") exit(0)
For this little example code I've added a function called "signal_handler" - because that's what it is for. And in the main program I installed that signal handler for quite a lot of signals. To be able to do that, I needed to import the signal module, of course.
If this program is run, it will handle every signal you can send on a FreeBSD system (run <em>kill -l</em> to list all available signals on a Unix-like operating system). Why are some of those commented out? Well, try commenting those lines in! Python will complain and stop your program. This is because not all signals are allowed to be handled.
_SIGKILL_ for example by its nature is something that you don't want to allow to be overridden with custom behavior after all! While your program can choose to handle e.g. _SIGINT_ and choose to ignore it, _SIGKILL_ means that the process totally needs to be shutdown immediately.
Try running the program and send some signals while it's running. On FreeBSD systems you can e.g. send CTRL-T for _SIGINFO_. The operating system prints some information about the current load. And then the program has the chance to output some additional information (some may tell you what file they currently process, how much percent they have finished copying, etc.). If you send _SIGINT_, this program terminates as it should.
There's another thing that we have to consider when dealing with processes running in the background: A daemon detaches from the TTY. That means it can no longer receive input the usual way from STDIN. But we investigated signals so that's fine. However it also means a daemon cannot use STDOUT or STDERR to print anything to the terminal.
Where does the data go that a daemon writes to e.g. STDOUT? It goes to the system log. If no special configuration for it exists, you will find it in /var/log/messages. Since we expect quite a bit of debug output during the development phase, we don't really want to clutter /var/log/messages with all of that. So to write a well-behaving little daemon, there's one more topic that we have to look into: Logging.
#!/usr/local/bin/python3.6 # Imports # import logging import signal import time # Globals # TTL_SECONDS = 30 TTL_CHECK_INTERVAL = 5 # Fuctions # def handler_sigterm(signum, frame): logging.debug("Exiting on SIGTERM") exit(0) def handler_sigint(signum, frame): logging.debug("Not going to quit, there you have it!") # Main # signal.signal(signal.SIGINT, handler_sigint) signal.signal(signal.SIGTERM, handler_sigterm) try: logging.basicConfig(filename='bdaemon.log', format='%(levelname)s:%(message)s', level=logging.DEBUG) except: print("Error: Could not create log file! Exiting...") exit(1) logging.info("Started!") for i in range(1, TTL_SECONDS + 1): time.sleep(1) if i % TTL_CHECK_INTERVAL == 0: logging.info("Running for " + str(i) + " seconds...") logging.info("TTL reached, terminating!") exit(0)
The code has been simplified a bit: Now it installs only handlers for two signals - and we're using two different handler functions. One overrides the default behavior of _SIGINT_ with a dummy function, effectively refusing the expected behavior for testing purposes. The other one handles _SIGTERM_ in the way it should. If you are fast enough on another terminal window, you can figure out the PID of the running program and then _kill -15_ it.
Logging with Python is extremely simple: You import the module for it, call a function like _logging.basicConfig_ - and start logging. This line sets the filename of the log to "bdaemon.log" (for "baby daemon") in the current directory. It changes the default format to displaying just the log level and the actual message. And then it defines the lowest level that should be logged.
There are various pre-defined levels like debug, info, warning, critical, etc. But what's that _try_ and _except_ thing? Well, the logging module will attempt to create a logfile (or append to it, if it already exists). This is an operation that could fail. Perhaps we're running the program in a directory where we don't have the permission to create the log file? Or maybe for whatever reason a directory of that name exists? In both cases Python cannot create the file an an error occurs.
If such a thing happens, Python doesn't know what to do. It knows what the programmer wanted to do, but has no clue on what to do if things fail. Does it make sense to keep the program running if something unexpected happened? Probably not. So it _throws an exception_. If an unhandled exception occurs, the program aborts. But we can _catch the exception_.
By putting the function that opens the file in a _try_ block, we're telling Python that we're expecting it could fail. And with _except_ we can catch an exception and handle expected problems. There are a lot of exception types; by not specifying any, we're catching all of them. That might not be the best idea, because maybe something else happened and we're just expecting that the logfile could not be created. But let's keep it simple for now.
The one remaining thing to do is to change any print statements so that we're using the logging instead. Depending on how important the log entry is, we can also use different levels from least important (DEBUG) to most important (CRITICAL).
You can either wait for the program to finish and then take a look at the log, or you open a second terminal and _tail -f bdaemon.log_ there to watch output as the program is running.
Alright! With this we have everything required to daemonize the program next. Let's write a little init script for it, shall we?
Init scripts are used to control daemons (start and stop them, telling them to reload the configuration, etc.). There are various different init systems in use across the Unix-like operating system. FreeBSD uses the standard BSD init system called _rc.d_. It works with little (or not so little if you need to manage very complex daemons) shell scripts.
Since a lot of the functionality of the init system is all the same across most of these scripts, rc.d handles all the common cases in shell files of it's own that are then used in each of the scripts. In Python this would be done by importing a module; the term with shell scripting is _to source_ another shell script (or fragment).
Create the file _/usr/local/etc/rc.d/bdaemon_ with the following contents:
#!/bin/sh . /etc/rc.subr name=bdaemon rcvar=bdaemon_enable command="/usr/sbin/daemon" command_args="-p /var/run/${name}.pid /path/to/script/bdaemon.py" load_rc_config $name run_rc_command "$1"[/code]
Yes, you need root privileges to do that. Daemons are system services and so we're messing with the system now (totally at beginner level, though). Save the file and you should be able to start the program as a daemon e.g. by running _service bdaemon onestart_!
How's that? What does that all mean and where does the daemonization happen? Well, the first line after the shebang sources the main rc fragment with all the required functions (read the dot as "source"). Then it defines a name for the daemon and an rcvar.
What is an rcvar? Well, by putting "bdaemon_enable=YES" into your _/etc/rc.conf_ you could enable this daemon for automatic startup when the system is coming up. If that line is not present there, the daemon will not start. That's why we need to use "onestart" to start it anyway (try it without the "one" if you've never done that and see what happens!).
Then the command to run as well as the arguments for that command are defined. And eventually two helper functions from rc.subr are called which do all the actual complex magic that they thankfully hide from us!
Ok, but what is _/usr/sbin/daemon_? Well, FreeBSD comes with an extremely useful little utility that handles the daemonization process for others! This means it can help you if you want to use something as a background service but you don't want to handle the actual daemonization yourself. Which is perfect in our case! With it you could even write a daemon in shell script for example.
The "-p" argument tells the daemon utility to handle the PID file for the process as well. This is required for the init system to control the daemon. While our little example program is short-lived, we can still do something while it runs. Try out _service onestatus_ and _service onestop_ on it for example. If there was no PID file present, the init system would claim that the daemon is not running, even if it is! And it would not be able to shut it down.
There we go. Our first FreeBSD daemon process written in Python! One last thing that you should do is change the filename for the logfile to use an absolute path like _/var/log/bdaemon.log_. If you want to read more about the daemon utility, read it's manpage, daemon(8). And should you be curious about what the init system can do, have a look here:
While using /usr/sbin/daemon is perfectly fine, you might feel that we kind of cheated. So next time we'll take a brief look at daemonizing with Python directly.
I also want to explore IPC ("inter-process communication) with named pipes. This will allow for a little bit more advanced daemon that can be interacted with using a separate program.