diff --git a/osbuild/inputs.py b/osbuild/inputs.py index 229c12641..24c580e62 100644 --- a/osbuild/inputs.py +++ b/osbuild/inputs.py @@ -88,10 +88,12 @@ def map(self, ip: Input, store: ObjectStore) -> Tuple[str, Dict]: } } - with make_args_file(store.tmp, args) as fd: - fds = [fd] + with make_args_and_reply_files(store.tmp, args) as (fd_args, fd_reply): + fds = [fd_args, fd_reply] client = self.service_manager.start(f"input/{ip.name}", ip.info.path) - reply, _ = client.call_with_fds("map", {}, fds) + _, _ = client.call_with_fds("map", {}, fds) + with os.fdopen(os.dup(fd_reply)) as f: + reply = json.loads(f.read()) path = reply["path"] @@ -106,11 +108,12 @@ def map(self, ip: Input, store: ObjectStore) -> Tuple[str, Dict]: @contextlib.contextmanager -def make_args_file(tmp, args): - with tempfile.TemporaryFile("w+", dir=tmp, encoding="utf-8") as f: - json.dump(args, f) - f.seek(0) - yield f.fileno() +def make_args_and_reply_files(tmp, args): + with tempfile.TemporaryFile("w+", dir=tmp, encoding="utf-8") as f_args, \ + tempfile.TemporaryFile("w+", dir=tmp, encoding="utf-8") as f_reply: + json.dump(args, f_args) + f_args.seek(0) + yield f_args.fileno(), f_reply.fileno() class InputService(host.Service): @@ -126,9 +129,11 @@ def unmap(self): def stop(self): self.unmap() - def dispatch(self, method: str, _, _fds): + def dispatch(self, method: str, _, fds): if method == "map": - with os.fdopen(_fds.steal(0)) as f: + # map() sends fd[0] to read the arguments from and fd[1] to + # write the reply back. This avoids running into EMSGSIZE + with os.fdopen(fds.steal(0)) as f: args = json.load(f) store = StoreClient(connect_to=args["api"]["store"]) r = self.map(store, @@ -136,6 +141,9 @@ def dispatch(self, method: str, _, _fds): args["refs"], args["target"], args["options"]) - return r, None + with os.fdopen(fds.steal(1), "w") as f: + f.write(json.dumps(r)) + f.seek(0) + return "{}", None raise host.ProtocolError("Unknown method") diff --git a/osbuild/util/jsoncomm.py b/osbuild/util/jsoncomm.py index 048212570..8a0cbb7c2 100644 --- a/osbuild/util/jsoncomm.py +++ b/osbuild/util/jsoncomm.py @@ -35,7 +35,7 @@ class FdSet: def __init__(self, *, rawfds): for i in rawfds: if not isinstance(i, int) or i < 0: - raise ValueError() + raise ValueError(f"unexpected fd {i}") self._fds = rawfds diff --git a/test/mod/test_inputs.py b/test/mod/test_inputs.py new file mode 100644 index 000000000..915d13138 --- /dev/null +++ b/test/mod/test_inputs.py @@ -0,0 +1,47 @@ +import json +import os +from unittest.mock import patch + +from osbuild import inputs +from osbuild.util.jsoncomm import FdSet + + +class FakeInputService(inputs.InputService): + def __init__(self, args): # pylint: disable=super-init-not-called + # do not call "super().__init__()" here to make it testable + self.map_calls = [] + + def map(self, _store, origin, refs, target, options): + self.map_calls.append([origin, refs, target, options]) + return "complex", 2, "reply" + + +def test_inputs_dispatches_map(tmp_path): + store_api_path = tmp_path / "api-store" + store_api_path.write_text("") + + args_path = tmp_path / "args" + reply_path = tmp_path / "reply" + args = { + "api": { + "store": os.fspath(store_api_path), + }, + "origin": "some-origin", + "refs": "some-refs", + "target": "some-target", + "options": "some-options", + } + args_path.write_text(json.dumps(args)) + reply_path.write_text("") + + with args_path.open() as f_args, reply_path.open("w") as f_reply: + fd_args, fd_reply = os.dup(f_args.fileno()), os.dup(f_reply.fileno()) + fds = FdSet.from_list([fd_args, fd_reply]) + fake_service = FakeInputService(args="some") + with patch.object(inputs, "StoreClient"): + r = fake_service.dispatch("map", None, fds) + assert r == ('{}', None) + assert fake_service.map_calls == [ + ["some-origin", "some-refs", "some-target", "some-options"], + ] + assert reply_path.read_text() == '["complex", 2, "reply"]'