James Kent
James Kent

Reputation: 5933

linux detect system shutdown early in python

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

Upvotes: 0

Views: 1651

Answers (1)

Marcus Müller
Marcus Müller

Reputation: 36346

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.

Upvotes: 1

Related Questions