From ad97a60002e93f3b260f4f69df5e20b2652019f8 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Wed, 19 Jul 2023 14:05:24 +0200
Subject: [PATCH] add KernelManager.exit_status
---
jupyter_client/manager.py | 11 ++++++++++
tests/test_kernelmanager.py | 40 +++++++++++++++++++++++++++++++++++++
2 files changed, 51 insertions(+)
diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py
index f04bd987..8626407d 100644
--- a/jupyter_client/manager.py
+++ b/jupyter_client/manager.py
@@ -655,6 +655,17 @@ async def _async_is_alive(self) -> bool:
is_alive = run_sync(_async_is_alive)
+ async def _async_exit_status(self) -> int | None:
+ """Returns 0 if there's no kernel or it exited gracefully,
+ None if the kernel is running, or a negative value `-N` if the
+ kernel was killed by signal `N` (posix only)."""
+ if not self.has_kernel:
+ return 0
+ assert self.provisioner is not None
+ return await self.provisioner.poll()
+
+ exit_status = run_sync(_async_exit_status)
+
async def _async_wait(self, pollinterval: float = 0.1) -> None:
# Use busy loop at 100ms intervals, polling until the process is
# not alive. If we find the process is no longer alive, complete
diff --git a/tests/test_kernelmanager.py b/tests/test_kernelmanager.py
index f2d749eb..989d5198 100644
--- a/tests/test_kernelmanager.py
+++ b/tests/test_kernelmanager.py
@@ -160,6 +160,46 @@ async def test_async_signal_kernel_subprocesses(self, name, install, expected):
assert km._shutdown_status in expected
+class TestKernelManagerExitStatus:
+ @pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals")
+ @pytest.mark.parametrize('_signal', [signal.SIGHUP, signal.SIGTERM, signal.SIGKILL])
+ async def test_exit_status(self, _signal):
+ # install kernel
+ _install_kernel(name="test_exit_status")
+
+ # start kernel
+ km, kc = start_new_kernel(kernel_name="test_exit_status")
+
+ # stop restarter - not needed?
+ # km.stop_restarter()
+
+ # check that process is running
+ assert km.exit_status() is None
+
+ # get the provisioner
+ # send signal
+ provisioner = km.provisioner
+ assert provisioner is not None
+ assert provisioner.has_process
+ await provisioner.send_signal(_signal)
+
+ # wait for the process to exit
+ try:
+ await asyncio.wait_for(km._async_wait(), timeout=3.0)
+ except TimeoutError:
+ assert False, f'process never stopped for signal {signal}'
+
+ # check that the signal is correct
+ assert km.exit_status() == -_signal
+
+ # doing a proper shutdown now wipes the status, might be bad?
+ km.shutdown_kernel(now=True)
+ assert km.exit_status() == 0
+
+ # stop channels so cleanup doesn't complain
+ kc.stop_channels()
+
+
class TestKernelManager:
def test_lifecycle(self, km):
km.start_kernel(stdout=PIPE, stderr=PIPE)