Skip to content

Commit 97f06a0

Browse files
committed
Correctly handle signals TERM, INT, and HUP
`litani run-build` now makes ninja the leader of its own process group. This allows run-build to forward signals to that entire process group, so that ninja and all its descendants receive the signal. This ensures that when users press Ctrl-C on Litani's controlling terminal, the INT is propagated to the commands in each Litani job. This fixes awslabs#162.
1 parent f667458 commit 97f06a0

File tree

2 files changed

+75
-9
lines changed

2 files changed

+75
-9
lines changed

lib/ninja.py

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@
1515
import dataclasses
1616
import datetime
1717
import io
18+
import logging
1819
import math
1920
import os
2021
import pathlib
2122
import re
23+
import signal
2224
import subprocess
25+
import sys
2326
import threading
27+
import time
2428

2529
import lib.litani
2630

@@ -129,16 +133,62 @@ def start(self):
129133

130134

131135

136+
def _make_pgroup_leader():
137+
try:
138+
os.setpgid(0, 0)
139+
except OSError as err:
140+
logging.error(
141+
"Failed to create new process group for ninja (errno: %s)",
142+
err.errno)
143+
sys.exit(1)
144+
145+
146+
147+
@dataclasses.dataclass
148+
class _SignalHandler:
149+
"""
150+
Objects of this class are callable. They implement the API of a signal
151+
handler, and can thus be passed to Python's signal.signal call.
152+
"""
153+
proc: subprocess.Popen
154+
render_killer: threading.Event
155+
pgroup: int = None
156+
157+
158+
def __post_init__(self):
159+
try:
160+
self.pgroup = os.getpgid(self.proc.pid)
161+
except ProcessLookupError:
162+
logging.error(
163+
"Failed to find ninja process group %d", self.proc.pid)
164+
sys.exit(1)
165+
166+
167+
def __call__(self, signum, _frame):
168+
self.render_killer.set()
169+
try:
170+
os.killpg(self.pgroup, signum)
171+
except OSError as err:
172+
logging.error(
173+
"Failed to send signal '%s' to ninja process group (errno: %s)",
174+
signum, err.errno)
175+
sys.exit(1)
176+
sys.exit(0)
177+
178+
179+
132180
@dataclasses.dataclass
133181
class Runner:
134182
ninja_file: pathlib.Path
135183
dry_run: bool
136184
parallelism: int
137185
pipelines: list
138186
ci_stage: str
187+
render_killer: threading.Event
139188
proc: subprocess.CompletedProcess = None
140189
status_parser: _StatusParser = _StatusParser()
141190
out_acc: _OutputAccumulator = None
191+
signal_handler: _SignalHandler = None
142192

143193

144194
def _get_cmd(self):
@@ -162,19 +212,35 @@ def _get_cmd(self):
162212
return [str(c) for c in cmd]
163213

164214

215+
def _register_signal_handler(self):
216+
sigs = (signal.SIGTERM, signal.SIGINT, signal.SIGHUP)
217+
fails = []
218+
for sig in sigs:
219+
try:
220+
signal.signal(sig, self.signal_handler)
221+
except ValueError:
222+
fails.append(str(sig))
223+
if fails:
224+
logging.error(
225+
"Failed to set signal handler for %s", ", ".join(fails))
226+
sys.exit(1)
227+
228+
165229
def run(self):
166230
env = {
167231
**os.environ,
168232
**{"NINJA_STATUS": self.status_parser.status_format},
169233
}
170234

171-
with subprocess.Popen(
172-
self._get_cmd(), env=env, stdout=subprocess.PIPE, text=True,
173-
) as proc:
174-
self.proc = proc
175-
self.out_acc = _OutputAccumulator(proc.stdout, self.status_parser)
176-
self.out_acc.start()
177-
self.out_acc.join()
235+
self.proc = subprocess.Popen(
236+
self._get_cmd(), env=env, stdout=subprocess.PIPE, text=True,
237+
preexec_fn=_make_pgroup_leader)
238+
self.signal_handler = _SignalHandler(self.proc, self.render_killer)
239+
self._register_signal_handler()
240+
241+
self.out_acc = _OutputAccumulator(self.proc.stdout, self.status_parser)
242+
self.out_acc.start()
243+
self.out_acc.join()
178244

179245

180246
def was_successful(self):

lib/run_build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,12 @@ async def run_build(args):
166166
killer = threading.Event()
167167
render_thread = threading.Thread(
168168
group=None, target=lib.render.continuous_render_report,
169-
args=(cache_dir, killer, args.out_file, render))
169+
args=(cache_dir, killer, args.out_file, render), daemon=True)
170170
render_thread.start()
171171

172172
runner = lib.ninja.Runner(
173173
ninja_file, args.dry_run, args.parallel, args.pipelines,
174-
args.ci_stage)
174+
args.ci_stage, killer)
175175

176176
lib.pid_file.write()
177177
sig_handler = lib.run_printer.DumpRunSignalHandler(cache_dir)

0 commit comments

Comments
 (0)