0

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

8
  • boot and shutdown ordering on 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 fully systemd-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/system and modify it appropriately. Commented Aug 12, 2016 at 17:55
  • you've mentioned both /run/systemd/system which is empty on my pi and /etc/systemd/system which doesn't contain any file related to my service any other places i should look? Commented Aug 12, 2016 at 19:05
  • I didn't catch that you're not running full Debian jessie - I'm not familiar with the "lite" version... But in the full version, there are three locations where systemd looks for unit files - in order, these are /etc/systemd/system, /run/systemd/system and /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... Commented Aug 12, 2016 at 19:20
  • my understanding is the "lite" version strips out the GUI/desktop stuff to save space, i can't see why the lite version would change the init system paths? but i shall look into it, it may be a rasbpian specific thing Commented Aug 12, 2016 at 19:26
  • just to be clear, i did find files in /etc/systemd/system but 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 Commented Aug 12, 2016 at 19:33

1 Answer 1

1

Don't react. Schedule.

Unixoid systems have well-established mechanisms for starting and stopping services when starting up and shutting down. Just add one of these to be stopped when your system shuts down; you can typically even define an order at which these shutdown scripts can be called.

Now, which of these systems your Linux uses is not known to me. Chances are that you're using either

  • SysV-style init scripts (classic)
  • systemd (relatively new)
  • upstart (if you're running one of Canonical's misguided experiments)

In either way, there's a lot of examples of such service files on your system; if you're running systemd, a systemctl will show which services are loaded currently, and shows which files you should look into copying and adding as your own service. If you're running a SysV-Style init, look into /etc/init.d for a lot of scripts.

You'll find a lot of information how to add and enable init scripts or systemd service files for specific runlevels/system targets.

Sign up to request clarification or add additional context in comments.

1 Comment

it is using systemd, it is scheduled but i cant work out how to schedule it to happen sooner, the script lives in /etc/init.d. i've added it to startup by running update-rc.d monitor defaults followed by update-rc.d monitor enable i did find one reference saying i could specify a sequence number after enable like this: update-rc.d monitor enable 5 and that the default is 20, however when i check the help text that update-rc.d prints seems to suggest that number is runlevel rather than sequency number

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.