8

I want to implement a proper SIGINT handling in my script, which opens multiple files and a database connection. These should be closed if the script is CTRL+C'd or somehow else interrupted.

Previously I've used the KeyboardInterrupt exception to catch CTRL+C, there I checked if files/connections are defined, if so close them, then exit.

Is this really the pythonic way to do it, or is it better adviced to use signal handlers? e.g.

import signal, sys, time

def handler(signum, frame):
    print("..kthxbye")
    sys.exit(1)

def main():
    signal.signal(signal.SIGINT, handler)
    i = 0
    while True:
        print(i)
        i += 1
        time.sleep(1)

if __name__ == "__main__":
    main()

This seems cleaner to me, yet don't I know how I would pass filenames or database connections to the handler.

Daedalus Mythos
  • 565
  • 2
  • 8
  • 24

2 Answers2

7

I would rather catch the KeyboardInterrupt exception on the main thread. KeyboardInterrupt is the result of the default SIGINT handler of python. The exception handler of a KeyboardInterrupt exception is a much safer/friendly context than what you are in when catching SIGINT directly.

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        cleanup()

EDIT: Here is how to share variables (state) between the two methods:

Procedural:

import sys, time

class SharedState:
    def __init__(self):
        self.var0 = 42
        self.var1 = 'string'

# method 1
shared_variable = 'woof woof'

# method 2: avoiding global declarations in functions
shared_state = SharedState()

def main():
    # In order to write a global variable you need a global
    # declaration otherwise the assignment would create a
    # local variable

    global shared_variable
    shared_variable = 5

    shared_state.var0 = 10

    time.sleep(10)

def cleanup():
    print shared_variable
    print shared_state.var0
    sys.exit(1)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        cleanup()

Object oriented (my preference):

import sys, time

# method 3: object oriented programming
class Program:
    def __init__(self):
        self.var0 = 42
        self.var1 = 'string'

    def main(self):
        self.var0 = 5
        self.var1 = 'woof woof'
        time.sleep(10)

    def cleanup(self):
        # both main and cleanup can access the member
        # variables of this class
        print self.var0
        print self.var1
        sys.exit(1)

    def execute(self):
        try:
            self.main()
        except KeyboardInterrupt:
            self.cleanup()

if __name__ == '__main__':
    Program().execute()
pasztorpisti
  • 3,760
  • 1
  • 16
  • 26
  • thank you for the answer. so I should rather wrap my few hundred lines of main in a try/except like: `try: print("files and databases and stuff") except KeyboardInterrupt: print("clean-up")`? – Daedalus Mythos Oct 15 '14 at 10:20
  • @DaedalusMythos I never prefer long methods or long blocks (let it be a try-except block). Instead put the whole meat of your main function into another function (eg.: safe_main()) and in your main function then you have only a simple try-except structure that has only a function call in its try block. You can simplify the except block similarly. In C++ for example the standard library calls your main function similarly, wrapped in a try-catch block. – pasztorpisti Oct 15 '14 at 10:28
  • @DaedalusMythos Well, I've just noticed that you have basically 2 main blocks already... You could do the try-except easily in the `if __name__ == "__main__":` block. Then your try block contains only the `main()` call. See my updated answer. – pasztorpisti Oct 15 '14 at 10:30
  • thanks for the update! I've considered implementing the try/except just as you demonstrated, but I don't know how to access files/database connections from the `main()` in the `cleanup()`.. – Daedalus Mythos Oct 15 '14 at 11:12
  • @DaedalusMythos Of course you have to have a variable/state shared between the `main()` and the `cleanup()`. You have to be aware that the interrupt can come any time so `cleanup()` must prepare for handling any possible states of your process. However it may be as simple as closing a few files or flushing some queues. This depends on the design of your program. – pasztorpisti Oct 15 '14 at 11:55
  • I've never implemented anything like a variable share between functions. Would you give me a hint on how to find the information needed to get the job done? – Daedalus Mythos Oct 15 '14 at 11:57
1

My suggestion is to use the signal library to handle the signals. Signals are not exceptions and they are part of Inter Process Communication (IPC) infrastructure of the Operating System.

Signals can help you to communicate with your program, like reloading the configuration file, closing your log file handler during log rotation and so on. Most of the daemon process like apache dose it.

Shell scripts have trap command to process the signals and take appropriate actions based on the signals captured.

Generally python closes all file handlers and database connection automatically during the time of exit. But to be safe we can have a function to handle them implicitly.

Below code traps SIGINT and closes the files properly.

import signal
import sys

die = False

def handler(signum, frame):
    global die
    print('Got SIGINT.')
    die = True

def closeFile(fh):
    fh.flush()
    fh.close()

signal.signal(signal.SIGINT, handler)

fh = open('/tmp/a.txt', 'w')

while True:
    data = input('> ')

    if data == 'q':
        closeFile(fh)
        break
    else:
        fh.write(data + '\n')

    if die:
        closeFile(fh)
        print('Completed cleanup.. ')
        sys.exit()
Kannan Mohan
  • 1,810
  • 12
  • 15
  • thank you for the answer! How can I pass a database connection/opened file to a handler - in order to close it when it's called? – Daedalus Mythos Oct 15 '14 at 11:14
  • 1
    In python you can not handle signals like you do in a C program. When your signal handler gets called the C signal handler has already returned. More info: https://docs.python.org/2/library/signal.html By handling `SIGINT` directly you may risk that your python program has been interrupted in the middle of an operation that is otherwise treated as "atomic" when it comes to exception handling. You can safely call python methods from an exception handler but you can not do the same from a signal handler. Unless signals, exceptions are thrown between "atomic" operations. – pasztorpisti Oct 15 '14 at 12:01
  • 1
    @DaedalusMythos I have added a sample code that closes up file handlers while receiving SIGINT and before termination. – Kannan Mohan Oct 17 '14 at 04:10