From f06176db3c9cbc2977eea9a63564bfe474cc1830 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 2 Feb 2026 21:38:34 +0100 Subject: [PATCH] Fix: Report worker process exit codes in error messages (#1278) When a worker process terminates with a non-zero exit code (e.g., via native code calling exit() directly), the exit code is now included in the error message. This helps diagnose crashes that bypass Python's exception handling mechanism. Previously, such cases would only report "Not properly terminated", making it difficult to determine the actual cause of the crash. Co-Authored-By: Claude Opus 4.5 --- changelog/1278.bugfix.rst | 1 + src/xdist/workermanage.py | 28 +++++++++++++++++++++++++++- testing/acceptance_test.py | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 changelog/1278.bugfix.rst diff --git a/changelog/1278.bugfix.rst b/changelog/1278.bugfix.rst new file mode 100644 index 00000000..58e3a39e --- /dev/null +++ b/changelog/1278.bugfix.rst @@ -0,0 +1 @@ +Worker process exit codes are now reported when the process terminates abnormally (e.g., via native code calling ``exit()`` directly). The exit code is included in the error message, helping diagnose crashes that bypass Python's exception handling. diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index 201c8e71..56450260 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -389,6 +389,25 @@ def notify_inproc(self, eventname: str, **kwargs: object) -> None: self.log(f"queuing {eventname}(**{kwargs})") self.putevent((eventname, kwargs)) + def _get_worker_exit_code(self) -> int | None: + """Get the exit code of the worker process, if available. + + Returns the exit code if the worker process has terminated, + or None if the exit code cannot be determined (e.g., for + non-popen gateways or if the process hasn't terminated yet). + """ + try: + io = getattr(self.gateway, "_io", None) + if io is None: + return None + popen = getattr(io, "popen", None) + if popen is None: + return None + # Poll to check if process has terminated and get return code + return popen.poll() + except Exception: + return None + def process_from_remote( self, eventcall: tuple[str, dict[str, Any]] | Literal[Marker.END] ) -> None: @@ -404,7 +423,14 @@ def process_from_remote( err: object | None = self.channel._getremoteerror() # type: ignore[no-untyped-call] if not self._down: if not err or isinstance(err, EOFError): - err = "Not properly terminated" # lost connection? + # Check if the worker process exited with non-zero status + # This handles cases like direct exit(1) calls from native code + # where Python exception handling is bypassed (#1278) + exit_code = self._get_worker_exit_code() + if exit_code is not None and exit_code != 0: + err = f"Worker exited with non-zero exit code: {exit_code}" + else: + err = "Not properly terminated" # lost connection? self.notify_inproc("errordown", node=self, error=err) self._down = True return diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 1b44985d..105e16dc 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -666,6 +666,24 @@ def test_ok(): ) +def test_exit_code_reported_in_error(pytester: pytest.Pytester) -> None: + """Ensure worker exit code is reported when process terminates (#1278). + + This specifically tests for cases where native code terminates the process + directly (e.g., via C's exit() function), bypassing Python exception handling. + """ + pytester.makepyfile( + """ + import os + def test_exit_nonzero(): + os._exit(42) + """ + ) + result = pytester.runpytest("-n1", "-v") + # The error message should include the exit code + result.stdout.fnmatch_lines(["*exit code*42*"]) + + def test_multiple_log_reports(pytester: pytest.Pytester) -> None: """ Ensure that pytest-xdist supports plugins that emit multiple logreports