diff --git a/osbuild/api.py b/osbuild/api.py index 212e4531..1935001a 100644 --- a/osbuild/api.py +++ b/osbuild/api.py @@ -1,9 +1,12 @@ import abc import asyncio +import contextlib import io import json import os +import sys import tempfile +import traceback import threading from typing import Dict, Optional from .util.types import PathLike @@ -141,6 +144,7 @@ class API(BaseAPI): self._output_pipe = None self.monitor = monitor self.metadata = {} + self.exception = None @property def output(self): @@ -182,9 +186,14 @@ class API(BaseAPI): fds.append(data.fileno()) sock.send({"type": "fd", "fd": 0}, fds=fds) + def _get_exception(self, message): + self.exception = message["exception"] + def _message(self, msg, fds, sock): if msg["method"] == 'add-metadata': self._set_metadata(msg) + elif msg["method"] == 'exception': + self._get_exception(msg) elif msg["method"] == 'get-arguments': self._get_arguments(sock) @@ -193,6 +202,29 @@ class API(BaseAPI): os.close(self._output_pipe) self._output_pipe = None +def exception(e, path="/run/osbuild/api/osbuild"): + """Send exception to osbuild""" + traceback.print_exception(type(e), e, e.__traceback__, file=sys.stderr) + with jsoncomm.Socket.new_client(path) as client: + msg = { + "method": "exception", + "exception": { + "type": str(type(e)), + "value": str(e), + "traceback": str(e.__traceback__) + } + } + client.send(msg) + + sys.exit(2) + +# pylint: disable=broad-except +@contextlib.contextmanager +def exception_handler(path="/run/osbuild/api/osbuild"): + try: + yield + except Exception as e: + exception(e, path) def arguments(path="/run/osbuild/api/osbuild"): """Retrieve the input arguments that were supplied to API""" diff --git a/osbuild/pipeline.py b/osbuild/pipeline.py index 750eff88..e535a936 100644 --- a/osbuild/pipeline.py +++ b/osbuild/pipeline.py @@ -18,13 +18,14 @@ def cleanup(*objs): class BuildResult: - def __init__(self, origin, returncode, output, metadata): + def __init__(self, origin, returncode, output, metadata, error): self.name = origin.name self.id = origin.id self.options = origin.options self.success = returncode == 0 self.output = output self.metadata = metadata + self.error = error def as_dict(self): return vars(self) @@ -92,7 +93,7 @@ class Stage: binds=[os.fspath(tree) + ":/run/osbuild/tree"], readonly_binds=ro_binds) - return BuildResult(self, r.returncode, r.output, api.metadata) + return BuildResult(self, r.returncode, r.output, api.metadata, api.exception) class Assembler: @@ -151,7 +152,7 @@ class Assembler: binds=binds, readonly_binds=ro_binds) - return BuildResult(self, r.returncode, r.output, api.metadata) + return BuildResult(self, r.returncode, r.output, api.metadata, api.exception) class Pipeline: diff --git a/test/mod/test_api.py b/test/mod/test_api.py index 7f224af4..decb464f 100644 --- a/test/mod/test_api.py +++ b/test/mod/test_api.py @@ -79,6 +79,25 @@ class TestAPI(unittest.TestCase): self.assertEqual(data, args) + def test_exception(self): + # Check that 'api.exception' correctly sets 'API.exception' + tmpdir = self.tmp.name + path = os.path.join(tmpdir, "osbuild-api") + args = {} + monitor = osbuild.monitor.BaseMonitor(sys.stderr.fileno()) + + def exception(path): + with osbuild.api.exception_handler(path): + raise ValueError("osbuild test exception") + + api = osbuild.api.API(args, monitor, socket_address=path) + with api: + p = mp.Process(target=exception, args=(path, )) + p.start() + p.join() + self.assertIsNotNone(api.exception, "Exception not set") + self.assertEqual(api.exception["value"], "osbuild test exception") + def test_metadata(self): # Check that `api.metadata` leads to `API.metadata` being # set correctly