parentpoller.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. """A parent poller for unix."""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. try:
  5. import ctypes
  6. except ImportError:
  7. ctypes = None # type:ignore[assignment]
  8. import os
  9. import platform
  10. import signal
  11. import time
  12. import warnings
  13. from _thread import interrupt_main # Py 3
  14. from threading import Thread
  15. from traitlets.log import get_logger
  16. class ParentPollerUnix(Thread):
  17. """A Unix-specific daemon thread that terminates the program immediately
  18. when the parent process no longer exists.
  19. """
  20. def __init__(self, parent_pid=0):
  21. """Initialize the poller.
  22. Parameters
  23. ----------
  24. parent_handle : int, optional
  25. If provided, the program will terminate immediately when
  26. process parent is no longer this original parent.
  27. """
  28. super().__init__()
  29. self.parent_pid = parent_pid
  30. self.daemon = True
  31. def run(self):
  32. """Run the poller."""
  33. # We cannot use os.waitpid because it works only for child processes.
  34. from errno import EINTR
  35. # before start, check if the passed-in parent pid is valid
  36. original_ppid = os.getppid()
  37. if original_ppid != self.parent_pid:
  38. self.parent_pid = 0
  39. get_logger().debug(
  40. "%s: poll for parent change with original parent pid=%d",
  41. type(self).__name__,
  42. self.parent_pid,
  43. )
  44. while True:
  45. try:
  46. ppid = os.getppid()
  47. parent_is_init = not self.parent_pid and ppid == 1
  48. parent_has_changed = self.parent_pid and ppid != self.parent_pid
  49. if parent_is_init or parent_has_changed:
  50. get_logger().warning("Parent appears to have exited, shutting down.")
  51. os._exit(1)
  52. time.sleep(1.0)
  53. except OSError as e:
  54. if e.errno == EINTR:
  55. continue
  56. raise
  57. class ParentPollerWindows(Thread):
  58. """A Windows-specific daemon thread that listens for a special event that
  59. signals an interrupt and, optionally, terminates the program immediately
  60. when the parent process no longer exists.
  61. """
  62. def __init__(self, interrupt_handle=None, parent_handle=None):
  63. """Create the poller. At least one of the optional parameters must be
  64. provided.
  65. Parameters
  66. ----------
  67. interrupt_handle : HANDLE (int), optional
  68. If provided, the program will generate a Ctrl+C event when this
  69. handle is signaled.
  70. parent_handle : HANDLE (int), optional
  71. If provided, the program will terminate immediately when this
  72. handle is signaled.
  73. """
  74. assert interrupt_handle or parent_handle
  75. super().__init__()
  76. if ctypes is None:
  77. msg = "ParentPollerWindows requires ctypes" # type:ignore[unreachable]
  78. raise ImportError(msg)
  79. self.daemon = True
  80. self.interrupt_handle = interrupt_handle
  81. self.parent_handle = parent_handle
  82. def run(self):
  83. """Run the poll loop. This method never returns."""
  84. try:
  85. from _winapi import INFINITE, WAIT_OBJECT_0 # type:ignore[attr-defined]
  86. except ImportError:
  87. from _subprocess import INFINITE, WAIT_OBJECT_0
  88. # Build the list of handle to listen on.
  89. handles = []
  90. if self.interrupt_handle:
  91. handles.append(self.interrupt_handle)
  92. if self.parent_handle:
  93. handles.append(self.parent_handle)
  94. arch = platform.architecture()[0]
  95. c_int = ctypes.c_int64 if arch.startswith("64") else ctypes.c_int
  96. # Listen forever.
  97. while True:
  98. result = ctypes.windll.kernel32.WaitForMultipleObjects( # type:ignore[attr-defined]
  99. len(handles), # nCount
  100. (c_int * len(handles))(*handles), # lpHandles
  101. False, # bWaitAll
  102. INFINITE,
  103. ) # dwMilliseconds
  104. if WAIT_OBJECT_0 <= result < len(handles):
  105. handle = handles[result - WAIT_OBJECT_0]
  106. ctypes.windll.kernel32.ResetEvent(handle) # type:ignore[attr-defined]
  107. if handle == self.interrupt_handle:
  108. # check if signal handler is callable
  109. # to avoid 'int not callable' error (Python issue #23395)
  110. if callable(signal.getsignal(signal.SIGINT)):
  111. interrupt_main()
  112. elif handle == self.parent_handle:
  113. get_logger().warning("Parent appears to have exited, shutting down.")
  114. os._exit(1)
  115. elif result < 0:
  116. # wait failed, just give up and stop polling.
  117. warnings.warn(
  118. """Parent poll failed. If the frontend dies,
  119. the kernel may be left running. Please let us know
  120. about your system (bitness, Python, etc.) at
  121. ipython-dev@scipy.org""",
  122. stacklevel=2,
  123. )
  124. return