Skip to content

Commit 314cc49

Browse files
authored
Detect parent change in more cases on unix (#1271)
1 parent 3089438 commit 314cc49

File tree

3 files changed

+43
-5
lines changed

3 files changed

+43
-5
lines changed

ipykernel/kernelapp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def init_poller(self):
220220
# PID 1 (init) is special and will never go away,
221221
# only be reassigned.
222222
# Parent polling doesn't work if ppid == 1 to start with.
223-
self.poller = ParentPollerUnix()
223+
self.poller = ParentPollerUnix(parent_pid=self.parent_handle)
224224

225225
def _try_bind_socket(self, s, port):
226226
iface = f"{self.transport}://{self.ip}"

ipykernel/parentpoller.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,41 @@ class ParentPollerUnix(Thread):
2222
when the parent process no longer exists.
2323
"""
2424

25-
def __init__(self):
26-
"""Initialize the poller."""
25+
def __init__(self, parent_pid=0):
26+
"""Initialize the poller.
27+
28+
Parameters
29+
----------
30+
parent_handle : int, optional
31+
If provided, the program will terminate immediately when
32+
process parent is no longer this original parent.
33+
"""
2734
super().__init__()
35+
self.parent_pid = parent_pid
2836
self.daemon = True
2937

3038
def run(self):
3139
"""Run the poller."""
3240
# We cannot use os.waitpid because it works only for child processes.
3341
from errno import EINTR
3442

43+
# before start, check if the passed-in parent pid is valid
44+
original_ppid = os.getppid()
45+
if original_ppid != self.parent_pid:
46+
self.parent_pid = 0
47+
48+
get_logger().debug(
49+
"%s: poll for parent change with original parent pid=%d",
50+
type(self).__name__,
51+
self.parent_pid,
52+
)
53+
3554
while True:
3655
try:
37-
if os.getppid() == 1:
56+
ppid = os.getppid()
57+
parent_is_init = not self.parent_pid and ppid == 1
58+
parent_has_changed = self.parent_pid and ppid != self.parent_pid
59+
if parent_is_init or parent_has_changed:
3860
get_logger().warning("Parent appears to have exited, shutting down.")
3961
os._exit(1)
4062
time.sleep(1.0)

tests/test_parentpoller.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
@pytest.mark.skipif(os.name == "nt", reason="only works on posix")
12-
def test_parent_poller_unix():
12+
def test_parent_poller_unix_to_pid1():
1313
poller = ParentPollerUnix()
1414
with mock.patch("os.getppid", lambda: 1): # noqa: PT008
1515

@@ -27,6 +27,22 @@ def mock_getppid():
2727
poller.run()
2828

2929

30+
@pytest.mark.skipif(os.name == "nt", reason="only works on posix")
31+
def test_parent_poller_unix_reparent_not_pid1():
32+
parent_pid = 221
33+
parent_pids = iter([parent_pid, parent_pid - 1])
34+
35+
poller = ParentPollerUnix(parent_pid=parent_pid)
36+
37+
with mock.patch("os.getppid", lambda: next(parent_pids)): # noqa: PT008
38+
39+
def exit_mock(*args):
40+
sys.exit(1)
41+
42+
with mock.patch("os._exit", exit_mock), pytest.raises(SystemExit):
43+
poller.run()
44+
45+
3046
@pytest.mark.skipif(os.name != "nt", reason="only works on windows")
3147
def test_parent_poller_windows():
3248
poller = ParentPollerWindows(interrupt_handle=1)

0 commit comments

Comments
 (0)