RDK
RDK

Reputation: 385

Trapping a shutdown event in Python

I posted a question about how to catch a "sudo shutdown -r 2" event in Python. I was sent to this thread: Run code in python script on shutdown signal .

I'm running a Raspberry Pi v2 with Jessy.

I have read about

signal

and have tried to follow the ideas in the above thread, but so far I have not been successful. Here is my code:

import time
import signal
import sys
def CloseAll(Code, Frame):
    f = open('/mnt/usbdrive/output/TestSignal.txt','a')
    f.write('Signal Code:' + Code)
    f.write('Signal Frame:' + Frame)
    f.write('\r\n')
    f.close()
    sys.exit(0)

signal.signal(signal.SIGTERM,CloseAll)
print('Program is running')
try:
  while True:
#get readings from sensors every 15 seconds 
    time.sleep(15)

    f = open('/mnt/usbdrive/output/TestSignal.txt','a')
    f.write('Hello ')
    f.write('\r\n')
    f.close()

except KeyboardInterrupt:
     f = open('/mnt/usbdrive/output/TestSignal.txt','a')
     f.write('Done')
     f.write('\r\n')
     f.close()

The program runs in a "screen" session/window and reacts as expected to a CNTL-C. However, when I exit the screen session, leaving the program running, and enter "sudo shutdown -r 2", the Pi reboots as expected after 2 minutes, but the TestSignal.txt file does not show that the signal.SIGTERM event was processed.

What am I doing wrong? Or better yet, how can I trap the shutdown event, usually initiated by a cron job, and close my Python program running in a screen session gracefully?

Upvotes: 0

Views: 3815

Answers (1)

Dilettant
Dilettant

Reputation: 3335

When you do not try to await such an event, but in a parallel session send SIGTERMto that process (e.g. by calling kill -15 $PID on the process id $PID of the python script running) , you should see an instructive error message ;-)

Also the comment about the mount point should be of interest after you repaired the python errors (TypeError: cannot concatenate 'str' and 'int' objects).

Try something like:

import time
import signal
import sys

LOG_PATH = '/mnt/usbdrive/output/TestSignal.txt'


def CloseAll(Code, Frame):
    f = open(LOG_PATH, 'a')
    f.write('Signal Code:' + str(Code) + ' ')
    f.write('Signal Frame:' + str(Frame))
    f.write('\r\n')
    f.close()
    sys.exit(0)

signal.signal(signal.SIGTERM, CloseAll)
print('Program is running')
try:
    while True:
        # get readings from sensors every 15 seconds
        time.sleep(15)

        f = open(LOG_PATH, 'a')
        f.write('Hello ')
        f.write('\r\n')
        f.close()

except KeyboardInterrupt:
    f = open(LOG_PATH, 'a')
    f.write('Done')
    f.write('\r\n')
    f.close()

as a starting point. If this works somehow on your system why not rewrite some portions like:

# ... 8< - - -
def close_all(signum, frame):
    with open(LOG_PATH, 'a') as f:
        f.write('Signal Code:%d Signal Frame:%s\r\n' % (signum, frame))
    sys.exit(0)

signal.signal(signal.SIGTERM, close_all)
# 8< - - - ...

Edit: To further isolate the error and adapt more to production like mode, one might rewrite the code like this (given that syslog is running on the machine, which it should, but I never worked on devices of that kind):

#! /usr/bin/env python
import datetime as dt
import time
import signal
import sys
import syslog

LOG_PATH = 'foobarbaz.log'  # '/mnt/usbdrive/output/TestSignal.txt'


def close_all(signum, frame):
    """Log to system log. Do not spend too much time after receipt of TERM."""
    syslog.syslog(syslog.LOG_CRIT, 'Signal Number:%d {%s}' % (signum, frame))
    sys.exit(0)

# register handler for SIGTERM(15) signal
signal.signal(signal.SIGTERM, close_all)


def get_sensor_readings_every(seconds):
    """Mock for sensor readings every seconds seconds."""
    time.sleep(seconds)
    return dt.datetime.now()


def main():
    """Main loop - maybe check usage patterns for file resources."""
    syslog.syslog(syslog.LOG_USER, 'Program %s is running' % (__file__,))
    try:
        with open(LOG_PATH, 'a') as f:
            while True:
                f.write('Hello at %s\r\n' % (
                    get_sensor_readings_every(15),))
    except KeyboardInterrupt:
        with open(LOG_PATH, 'a') as f:
            f.write('Done at %s\r\n' % (dt.datetime.now(),))

if __name__ == '__main__':
    sys.exit(main())

Points to note:

  1. the log file for the actual measurements is separate from the logging channel for operational alerts
  2. the log file handle is safeguarded in context managing blocks and in usual operation is just kept open
  3. for alerting the syslog channel is used.
  4. as a sample for the message routing the syslog.LOG_USER on my system (OS X) gives me in all terminals a message, whilst the syslog.LOG_ERR priority message in signal handler only targets the system log.
  5. should be more to the point during shutdown hassle (not opening a file, etc.)

The last point (5.) is important in case all processes receive a SIGTERM during shutdown, i.e. all want to do something (slowing things down), maybe screenalso does not accept any buffered input anymore (or does not flush), note stdout is block buffered not line buffered.

The decoupling of the output channels, should also ease the eventual disappearance of the mount point of the measurement log file.

Upvotes: 1

Related Questions