15
15
import dataclasses
16
16
import datetime
17
17
import io
18
+ import logging
18
19
import math
19
20
import os
20
21
import pathlib
21
22
import re
23
+ import signal
22
24
import subprocess
25
+ import sys
23
26
import threading
27
+ import time
24
28
25
29
import lib .litani
26
30
@@ -129,16 +133,62 @@ def start(self):
129
133
130
134
131
135
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
+ p_group : int = None
156
+
157
+
158
+ def __post_init__ (self ):
159
+ try :
160
+ self .p_group = 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 .p_group , 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
+
132
180
@dataclasses .dataclass
133
181
class Runner :
134
182
ninja_file : pathlib .Path
135
183
dry_run : bool
136
184
parallelism : int
137
185
pipelines : list
138
186
ci_stage : str
187
+ render_killer : threading .Event
139
188
proc : subprocess .CompletedProcess = None
140
189
status_parser : _StatusParser = _StatusParser ()
141
190
out_acc : _OutputAccumulator = None
191
+ signal_handler : _SignalHandler = None
142
192
143
193
144
194
def _get_cmd (self ):
@@ -162,19 +212,35 @@ def _get_cmd(self):
162
212
return [str (c ) for c in cmd ]
163
213
164
214
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
+
165
229
def run (self ):
166
230
env = {
167
231
** os .environ ,
168
232
** {"NINJA_STATUS" : self .status_parser .status_format },
169
233
}
170
234
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 ()
178
244
179
245
180
246
def was_successful (self ):
0 commit comments