I've been working on a monitoring script for a raspberry pi that i'm running as a headless server. As part of that I want it to react to a shutdown event.
I tried using the signal module, and it does react and call my shutdown routine, however it happens very late into the shutdown routine, I'd like to try and find a way to make it react very quickly after the shutdown request is issued, rather than waiting for the operating system to ask python to exit.
this is running on a raspberry pi 1 B, using the latest jessie lite image I'm using python 3 and my python script itself is the init script:
monitor:
#!/usr/bin/python3
### BEGIN INIT INFO
# Provides: monitor
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start the monitor daemon
# Description: Start the monitor daemon during system boot
### END INIT INFO
import os, psutil, socket, sys, time
from daemon import Daemon
from RPLCD import CharLCD
from subprocess import Popen, PIPE
import RPi.GPIO as GPIO
GPIO.setwarnings(False)
def get_cpu_temperature():
process = Popen(['vcgencmd', 'measure_temp'], stdout=PIPE)
output, _error = process.communicate()
output = output.decode('utf8')
return float(output[output.index('=') + 1:output.rindex("'")])
class MyDaemon(Daemon):
def run(self):
lcd = CharLCD(pin_rs=7, pin_rw=4, pin_e=8, pins_data=[25, 24, 23, 18], numbering_mode=GPIO.BCM, cols=40, rows=2, dotsize=8)
while not self.exitflag:
gw = os.popen("ip -4 route show default").read().split()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect((gw[2], 0))
ipaddr = s.getsockname()[0]
lcd.cursor_pos = (0, 0)
lcd.write_string("IP:" + ipaddr)
gateway = gw[2]
lcd.cursor_pos = (1, 0)
lcd.write_string("GW:" + gateway)
except IndexError:
lcd.cursor_pos = (0, 0)
lcd.write_string("IP:No Network")
lcd.cursor_pos = (1, 0)
lcd.write_string("GW:No Network")
host = socket.gethostname()
lcd.cursor_pos = (0, 20)
lcd.write_string("Host:" + host)
for num in range(10):
temp = get_cpu_temperature()
perc = psutil.cpu_percent()
lcd.cursor_pos = (1, 20)
lcd.write_string("CPU :{:5.1f}% {:4.1f}\u00DFC".format(perc, temp))
if (self.exitflag):
break
time.sleep(2)
lcd.clear()
## lcd.cursor_pos = (13, 0)
lcd.write_string("Shutting Down")
if __name__ == "__main__":
daemon = MyDaemon('/var/run/monitor.pid')
if len(sys.argv) == 2:
if 'start' == sys.argv[1]:
daemon.start()
elif 'stop' == sys.argv[1]:
daemon.stop()
elif 'restart' == sys.argv[1]:
daemon.restart()
elif 'run' == sys.argv[1]:
daemon.run()
else:
print("Unknown command")
sys.exit(2)
sys.exit(0)
else:
print("usage: %s start|stop|restart" % sys.argv[0])
sys.exit(2)
daemon.py:
"""Generic linux daemon base class for python 3.x."""
import sys, os, time, signal
class Daemon:
"""A generic daemon class.
Usage: subclass the daemon class and override the run() method."""
def __init__(self, pidfile):
self.pidfile = pidfile
self.exitflag = False
signal.signal(signal.SIGINT, self.exit_signal)
signal.signal(signal.SIGTERM, self.exit_signal)
def daemonize(self):
"""Deamonize class. UNIX double fork mechanism."""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #1 failed: {0}\n'.format(err))
sys.exit(1)
# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError as err:
sys.stderr.write('fork #2 failed: {0}\n'.format(err))
sys.exit(1)
# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = open(os.devnull, 'r')
so = open(os.devnull, 'a+')
se = open(os.devnull, 'a+')
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
pid = str(os.getpid())
with open(self.pidfile,'w+') as f:
f.write(pid + '\n')
def start(self):
"""Start the daemon."""
# Check for a pidfile to see if the daemon already runs
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if pid:
message = "pidfile {0} already exist. Daemon already running?\n"
sys.stderr.write(message.format(self.pidfile))
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
def stop(self):
"""Stop the daemon."""
# Get the pid from the pidfile
try:
with open(self.pidfile,'r') as pf:
pid = int(pf.read().strip())
except IOError:
pid = None
if not pid:
message = "pidfile {0} does not exist. Daemon not running?\n"
sys.stderr.write(message.format(self.pidfile))
return # not an error in a restart
# Try killing the daemon process
try:
while 1:
os.kill(pid, signal.SIGTERM)
time.sleep(0.1)
except OSError as err:
e = str(err.args)
if e.find("No such process") > 0:
if os.path.exists(self.pidfile):
os.remove(self.pidfile)
else:
print (str(err.args))
sys.exit(1)
def restart(self):
"""Restart the daemon."""
self.stop()
self.start()
def exit_signal(self, sig, stack):
self.exitflag = True
try:
os.remove(self.pidfile)
except FileNotFoundError:
pass
def run(self):
"""You should override this method when you subclass Daemon.
It will be called after the process has been daemonized by
start() or restart()."""
so in short is there any way i can detect a shutdown even as early as possible in the shutdown no matter how its called, and preferably able to detect a reboot aswell from within python
systemd-based systems is handled by analyzing dependencies in the unit files (After,Before,Wants,Requires,Conflicts, etc.). You may need to copy the unit file automatically generated by jessie (in/run/systemd/system- unlike RedHat/CentOS, Debian jessie still isn't fullysystemd-converted in the sense that it still just has everything in/etc/init.d, but auto-generates LSB unit files for them) into/etc/systemd/systemand modify it appropriately./run/systemd/systemwhich is empty on my pi and/etc/systemd/systemwhich doesn't contain any file related to my service any other places i should look?systemdlooks for unit files - in order, these are/etc/systemd/system,/run/systemd/systemand/lib/systemd/system, with the idea being that/etc/...has locally configured and modified stuff,/run/...is all the auto-generated bits, and/lib/...is what comes out of the install packages. The lite version may have tweaked paths and such - check the documentation, I guess.../etc/systemd/systembut none related to my service, are you sure they are automatically created? i can see that when a package is installed one might be created for it, but as this is something i've written i haven't created a unit file for it