Call Async function in Signal Handler, Asyncio

Viewed 1347

I am making a Discord bot using discord.py and sometimes the script I use to run it needs to close out the bot. When I close it out without using signal handlers there is a lot of errors about a loop not closing, so I added a signal handler (using the code below) and inside I need to call client.close() and client.logout(), but the problem is those are async functions and thus require me to await them, but I can't await the functions since the signal handler can't be an async function.

Here is the code:

def handler():
    print("Logging out of Discord Bot")
    client.logout()
    client.close()
    sys.exit()

@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))

    for signame in ('SIGINT', 'SIGTERM'):
        client.loop.add_signal_handler(getattr(signal, signame),
                                lambda: asyncio.ensure_future(handler()))

Is there a way to either logout properly using the signal handler, or at least just silence the warnings and errors from inside the code so no errors are printed in the console.

1 Answers

Your approach is on the right track - since add_signal_handler expects an ordinary function and not an async function, you do need to call ensure_future (or its cousin create_task) to submit an async function to run in the event loop. The next step is to actually make handler async, and await the coroutines it invokes:

async def handler():
    print("Logging out of Discord Bot")
    await client.logout()
    await client.close()
    asyncio.get_event_loop().stop()

Note that I changed sys.exit() to explicit stopping of the event loop, because asyncio doesn't react well to sys.exit() being invoked from the middle of a callback (it catches the SystemExit exception and complains of an unretrieved exception).

Since I don't have discord to test, I tested it by changing the logout and close with a sleep:

import asyncio, signal

async def handler():
    print("sleeping a bit...")
    await asyncio.sleep(0.2)
    print('exiting')
    asyncio.get_event_loop().stop()

def setup():
    loop = asyncio.get_event_loop()
    for signame in ('SIGINT', 'SIGTERM'):
        loop.add_signal_handler(getattr(signal, signame),
                                lambda: asyncio.create_task(handler()))

setup()
asyncio.get_event_loop().run_forever()

If you are starting the event loop using something other than run_forever, such as asyncio.run(some_function()), then you will need to replace loop.stop() with the code that sets whatever event the main coroutine awaits. For example, if it awaits server.serve_forever() on a server, then you'd pass the server object to handler and call server.close(), and so on.

Related