From 473811bb5f9393d7460e01641b74b4a68fc160e3 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Wed, 9 Nov 2022 07:22:54 +0400 Subject: [PATCH 1/3] Expose tracebacks from actor exceptions Previously when leapp would be running actors, any exceptions raised by those actors in child processes would only be output to stderr, while the main process would only have the exit code to examine. Consequently, there was no real way to log occuring exceptions - only providing the aforementioned exit code was possible. This patch adds a pipe between the main and the actor processes, through which a formatted exception + traceback string is provided to the main process. This string is then packaged into the LeappRuntimeError thrown from the process, to be utilized downstream in workflow/command code. --- leapp/exceptions.py | 4 +++- leapp/repository/actor_definition.py | 31 +++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/leapp/exceptions.py b/leapp/exceptions.py index 1bd4222a0..ef9d647ac 100644 --- a/leapp/exceptions.py +++ b/leapp/exceptions.py @@ -109,7 +109,9 @@ def __init__(self, command): class LeappRuntimeError(LeappError): - pass + def __init__(self, message, exception_info): + super().__init__(message) + self.exception_info = exception_info class StopActorExecution(Exception): diff --git a/leapp/repository/actor_definition.py b/leapp/repository/actor_definition.py index 7c0beb9c6..2b5df1ab9 100644 --- a/leapp/repository/actor_definition.py +++ b/leapp/repository/actor_definition.py @@ -4,9 +4,10 @@ import os import pkgutil import sys +import traceback import warnings from io import UnsupportedOperation -from multiprocessing import Process, Queue +from multiprocessing import Process, Queue, Pipe import leapp.libraries.actor # noqa # pylint: disable=unused-import from leapp.actors import get_actor_metadata, get_actors @@ -56,7 +57,7 @@ def __init__(self, definition, logger, messaging, config_model, skip_dialogs): self.skip_dialogs = skip_dialogs @staticmethod - def _do_run(stdin, logger, messaging, definition, config_model, skip_dialogs, args, kwargs): + def _do_run(stdin, logger, messaging, definition, config_model, skip_dialogs, error_pipe, args, kwargs): if stdin is not None: try: sys.stdin = os.fdopen(stdin) @@ -69,7 +70,13 @@ def _do_run(stdin, logger, messaging, definition, config_model, skip_dialogs, ar target_actor = [actor for actor in get_actors() if actor.name == definition.name][0] actor_instance = target_actor(logger=logger, messaging=messaging, config_model=config_model, skip_dialogs=skip_dialogs) - actor_instance.run(*args, **kwargs) + try: + actor_instance.run(*args, **kwargs) + except Exception: + # Send the exception data string to the parent process + # and reraise. + error_pipe.send(traceback.format_exc()) + raise try: # By this time this is no longer set, so we have to get it back os.environ['LEAPP_CURRENT_ACTOR'] = actor_instance.name @@ -96,15 +103,25 @@ def run(self, *args, **kwargs): stdin = sys.stdin.fileno() except UnsupportedOperation: stdin = None + + pipe_receiver, pipe_sender = Pipe() p = Process(target=self._do_run, args=(stdin, self.logger, self.messaging, self.definition, self.config_model, - self.skip_dialogs, args, kwargs)) + self.skip_dialogs, pipe_sender, args, kwargs)) p.start() p.join() if p.exitcode != 0: - raise LeappRuntimeError( - 'Actor {actorname} unexpectedly terminated with exit code: {exitcode}' - .format(actorname=self.definition.name, exitcode=p.exitcode)) + err_message = "Actor {actorname} unexpectedly terminated with exit code: {exitcode}".format( + actorname=self.definition.name, exitcode=p.exitcode) + + exception_info = None + # If there's data in the pipe, it's formatted exception info. + if pipe_receiver.poll(): + exception_info = pipe_receiver.recv() + + # This LeappRuntimeError will contain an exception traceback + # in addition to the above message. + raise LeappRuntimeError(err_message, exception_info) class ActorDefinition(object): From 90d2ebb54a3134df3bc648a138e8a7ab2f90de60 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Wed, 9 Nov 2022 07:24:38 +0400 Subject: [PATCH 2/3] Add a default None value to the LeappRuntimeError field --- leapp/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leapp/exceptions.py b/leapp/exceptions.py index ef9d647ac..d6bbf0040 100644 --- a/leapp/exceptions.py +++ b/leapp/exceptions.py @@ -109,7 +109,7 @@ def __init__(self, command): class LeappRuntimeError(LeappError): - def __init__(self, message, exception_info): + def __init__(self, message, exception_info=None): super().__init__(message) self.exception_info = exception_info From 1b14d62d291f32a7e2ba44b4222c927332d38573 Mon Sep 17 00:00:00 2001 From: Roman Prilipskii Date: Mon, 28 Nov 2022 07:12:43 +0400 Subject: [PATCH 3/3] Fix super() args for Leapp exception classes --- leapp/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/leapp/exceptions.py b/leapp/exceptions.py index d6bbf0040..43c59051c 100644 --- a/leapp/exceptions.py +++ b/leapp/exceptions.py @@ -104,13 +104,13 @@ class CommandDefinitionError(LeappError): class UnknownCommandError(LeappError): def __init__(self, command): - super().__init__('Unknown command: {}'.format(command)) + super(UnknownCommandError, self).__init__('Unknown command: {}'.format(command)) self.requested = command class LeappRuntimeError(LeappError): def __init__(self, message, exception_info=None): - super().__init__(message) + super(LeappRuntimeError, self).__init__(message) self.exception_info = exception_info