Skip to content

Commit a96bd2a

Browse files
committed
Fix plain dev Ctrl+C shutdown reliability
- Second Ctrl+C now force-kills (SIGKILL) instead of being silently ignored - KILL_WAIT timer starts when terminate() is called, not when the first child exits — prevents hanging indefinitely if no child responds to SIGTERM - Make kill() idempotent to avoid repeated SIGKILL spam in the event loop
1 parent b3f50da commit a96bd2a

1 file changed

Lines changed: 14 additions & 8 deletions

File tree

plain-dev/plain/dev/poncho/manager.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def __init__(self, printer: Printer | None = None) -> None:
6161
self._processes = {}
6262

6363
self._terminating = False
64+
self._terminate_start = datetime.datetime.min
65+
self._killed = False
6466

6567
def add_process(
6668
self,
@@ -111,7 +113,11 @@ def _terminate(signum: int, frame: FrameType | None) -> None:
111113
sig = signal.Signals(signum)
112114
self._system_print("{} received\n".format(SIGNALS[sig]["name"]))
113115
self.returncode = SIGNALS[sig]["rc"]
114-
self.terminate()
116+
if self._terminating:
117+
self._system_print("forcing immediate shutdown\n")
118+
self.kill()
119+
else:
120+
self.terminate()
115121

116122
signal.signal(signal.SIGTERM, _terminate)
117123
signal.signal(signal.SIGINT, _terminate)
@@ -120,7 +126,6 @@ def _terminate(signum: int, frame: FrameType | None) -> None:
120126
self._start_process(name)
121127

122128
exit = False
123-
exit_start = None
124129

125130
while 1:
126131
try:
@@ -147,14 +152,11 @@ def _terminate(signum: int, frame: FrameType | None) -> None:
147152
if self._all_started() and self._all_stopped():
148153
exit = True
149154

150-
if exit_start is None and self._all_started() and self._any_stopped():
151-
exit_start = self._clock.now()
155+
if not self._terminating and self._all_started() and self._any_stopped():
152156
self.terminate()
153157

154-
if exit_start is not None:
155-
# If we've been in this loop for more than KILL_WAIT seconds,
156-
# it's time to kill all remaining children.
157-
waiting = self._clock.now() - exit_start
158+
if self._terminating and not self._all_stopped():
159+
waiting = self._clock.now() - self._terminate_start
158160
if waiting > datetime.timedelta(seconds=KILL_WAIT):
159161
self.kill()
160162

@@ -165,12 +167,16 @@ def terminate(self) -> None:
165167
if self._terminating:
166168
return
167169
self._terminating = True
170+
self._terminate_start = self._clock.now()
168171
self._killall()
169172

170173
def kill(self) -> None:
171174
"""
172175
Kill all processes managed by this ProcessManager.
173176
"""
177+
if self._killed:
178+
return
179+
self._killed = True
174180
self._killall(force=True)
175181

176182
def _killall(self, force: bool = False) -> None:

0 commit comments

Comments
 (0)