peterhinch / micropython-samples

Assorted code ideas, unofficial MP FAQ, plus index to my other repositories.
MIT License
459 stars 92 forks source link

add global exception handling #13

Closed kevinkk525 closed 4 years ago

kevinkk525 commented 4 years ago

Support global exception handling to uasyncio according to CPython error handling: https://docs.python.org/3/library/asyncio-eventloop.html#error-handling-api

Adds the Loop methods (also changing all loop methods to be staticmethos or classmethods since there is only one loop) and a default exception handler.

This is especially interesting since this new version of uasyncio doesn't throw exceptions to the caller of "loop.run_forever()" and therefore exceptions in other tasks are only printed to repl, where the user might never see it since the device will be deployed without logging the repl output. With a global exception handling a user can catch those exceptions and send them by mqtt or log them to a file on the device.

The implementation preallocates a context dictionary so in case of an exception there shouldn't be any RAM allocations.

The used approach is compatible to CPython except for 2 problems: 1) There is no way to log the Exception traceback because "sys.print_exception" only prints the traceback to the repl. So there is no way to actually log the traceback, which would be very helpful. Hopefully this can be implemented. I understand that this might cause RAM allocation but a user might decide to use it anyway in a custom exception handler because it makes debugging a lot easier if you know in what file and line the error occured. 2) In CPython the exception handler is called once the task is finished which created the task that threw an uncaught exception, whereas in UPy the exception handler is called immediately when the exception is thrown. This makes a difference in the following testcase but is generally just a minor difference that shouldn't cause any abnormal behaviour.

async def test_catch_uncaught_exception():
    # can't work with a local return value because the exception handler runs after
    # the current coroutine is finished in CPython. Works in UPy though.
    res = False

    async def fail():
        raise TypeError("uncaught exception")

    def handle_exception(loop, context):
        # context["message"] will always be there; but context["exception"] may not
        print(context)
        print(context["message"])
        print(context["exception"])
        msg = context.get("exception", context["message"])
        if mp:
            print("Caught: {}{}".format(type(context["exception"]), msg))
        else:
            print("Caught: {}".format({msg}))
        nonlocal res
        print("res is", res)
        res = True
        print("done")

    t = asyncio.create_task(fail())
    loop = asyncio.get_event_loop()
    loop.set_exception_handler(handle_exception)
    await asyncio.sleep(0.1)
    await asyncio.sleep(0.1)
    print("coro done")
    return res

I'd be glad to discuss this PR.