Catch CTRL+C and exit gracefully in Python with a multithreaded controller on Windows

163 views Asked by At

I'm trying to create a Windows SMTP server in Python that uses the aiosmtpd library, which provides a multithreaded controller that listens on a TCP port for connections.

This code works perfectly:

from aiosmtpd.controller import Controller

class CustomSMTPServer:
    # Proof of concept
    async def handle_DATA(self, server, session, envelope):
        print(f'> Incoming mail from \'{envelope.mail_from}\'')
        return '250 OK'

if __name__ == '__main__':
    controller = Controller(CustomSMTPServer(), hostname='127.0.0.1', port=25)
    controller.start()

    # I would like to wait for CTRL+C instead of ENTER
    input(f'SMTP listening on port {controller.port}. Press ENTER to exit.\n')

    # ... and stopping the controller after the key has been pressed
    controller.stop()

I would like to modify it such as it doesn't wait for ENTER but for CTRL+C instead, without using a while True endless loop because this would eat CPU. It would be great to not use a lot of libraries imports, if possible.

Is there anything better than using time.sleep(1) inside the loop, as the aiosmtpd.controller is already running in its thread and the main thread is not doing anything useful, apart from waiting for CTRL+C?

2

There are 2 answers

1
PrefixEt On

You can use the signal module to catch the SIGINT (Ctrl+C) signal and stop the dispatcher. Here is an example:

import signal
from aiosmtpd.controller import Controller

def stop_controller(signal, frame):
    controller.stop()

if __name__ == '__main__':
    controller = Controller(CustomSMTPServer(), hostname='127.0.0.1', port=25)
    controller.start()

    print(f'SMTP listening on port {controller.port}. Press Ctrl+C to exit.\n')

    signal.signal(signal.SIGINT, stop_controller)
    signal.pause()

Now the program will wait for the SIGINT signal instead of ENTER and will stop when the user presses Ctrl+C. The stop_controller function will be called when a signal is received and will stop the controller. The signal.pause() function waits for any signal, but does not consume CPU resources until a signal is received.

2
zetmain On

To wait for a CTRL+C signal instead of waiting for the user to press ENTER, you can try using the signal module:

import signal
from aiosmtpd.controller import Controller

# Create a flag to indicate if CTRL+C signal has been received
ctrlc_received = False

# Define a signal handler for CTRL+C
def handle_ctrlc(signal, frame):
    global ctrlc_received
    ctrlc_received = True

if __name__ == '__main__':
    # Register the signal handler for CTRL+C
    signal.signal(signal.SIGINT, handle_ctrlc)

    # Start the SMTP controller
    controller = Controller(CustomSMTPServer(), hostname='127.0.0.1', port=25)
    controller.start()

    # Wait for CTRL+C
    print(f'SMTP listening on port {controller.port}. Press CTRL+C to exit.')
    while not ctrlc_received:
        pass

    # Stop the controller after receiving CTRL+C
    controller.stop()