Skip to content

Commit 170e553

Browse files
authored
fix: Convey wrapped process's exit code in metrics wrapper (openedx-unsupported#768)
1 parent 971f52f commit 170e553

2 files changed

Lines changed: 61 additions & 7 deletions

File tree

scripts/send-metrics.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def read_git_state():
199199
def run_wrapped(make_target, config):
200200
"""
201201
Runs specified make shell target and collects performance data.
202+
203+
Return exit code of process (negative for signals).
202204
"""
203205
# Do as much as possible inside try blocks
204206
do_collect = True
@@ -219,16 +221,16 @@ def run_wrapped(make_target, config):
219221
file=sys.stderr)
220222

221223
completed_process = run_target(make_target)
224+
subprocess_exit_code = completed_process.returncode
222225

223226
# Do as much as possible inside try blocks
224227
try:
225228
# Skip metrics reporting if setup failed
226229
if not do_collect:
227-
return
230+
return subprocess_exit_code
228231

229232
signal(SIGINT, SIG_DFL) # stop trapping SIGINT (if haven't already)
230233
end_time = datetime.now(timezone.utc)
231-
exit_code = completed_process.returncode
232234
time_diff_millis = (end_time - start_time).microseconds // 1000
233235
# Must be compatible with our Segment schema, and must not be
234236
# expanded to include additional attributes without an
@@ -244,7 +246,7 @@ def run_wrapped(make_target, config):
244246
# https://docs.python.org/3.8/library/subprocess.html#subprocess.CompletedProcess
245247
#
246248
# If a make subprocess exits non-zero, make exits with code 2.
247-
'exit_status': exit_code,
249+
'exit_status': subprocess_exit_code,
248250
**read_git_state()
249251
}
250252
send_metrics_to_segment('devstack.command.run', event_properties, config)
@@ -258,16 +260,24 @@ def run_wrapped(make_target, config):
258260
"(This should not have affected the outcome of your make command.)",
259261
file=sys.stderr)
260262

263+
return subprocess_exit_code
264+
261265

262266
def run_target(make_target):
263-
"""Just run make on the given target."""
267+
"""
268+
Just run make on the given target.
269+
270+
Return exit code of process (negative for signals).
271+
"""
264272
return subprocess.run(["make", "impl-%s" % make_target], check=False)
265273

266274

267275
def do_wrap(make_target):
268276
"""
269277
Run the given make target, and collect and report data if and only if
270278
the user has consented to data collection.
279+
280+
Return exit code to exit with (signals become 128 + signal value).
271281
"""
272282
try:
273283
consent_state = check_consent()
@@ -277,9 +287,9 @@ def do_wrap(make_target):
277287
consent_state = {}
278288

279289
if consent_state.get('ok_to_collect'):
280-
run_wrapped(make_target, consent_state.get('config'))
290+
subprocess_exit_code = run_wrapped(make_target, consent_state.get('config'))
281291
else:
282-
run_target(make_target)
292+
subprocess_exit_code = run_target(make_target).returncode
283293
if consent_state.get('print_invitation'):
284294
print(
285295
"Would you like to assist devstack development by sending "
@@ -288,6 +298,12 @@ def do_wrap(make_target):
288298
file=sys.stderr
289299
)
290300

301+
if subprocess_exit_code < 0:
302+
# Translate to shell convention.
303+
return 128 + -subprocess_exit_code
304+
else:
305+
return subprocess_exit_code
306+
291307

292308
def do_opt_in():
293309
"""
@@ -435,7 +451,8 @@ def main(args):
435451
if len(action_args) != 1:
436452
print("send-metrics wrap takes one argument", file=sys.stderr)
437453
sys.exit(1)
438-
do_wrap(action_args[0])
454+
conveyed_exit_code = do_wrap(action_args[0])
455+
sys.exit(conveyed_exit_code)
439456
elif action == 'opt-in':
440457
if len(action_args) != 0:
441458
print("send-metrics opt-in takes zero arguments", file=sys.stderr)

tests/metrics.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ def test_no_arbitrary_target_instrumented():
233233
assert 'Send metrics info:' not in p.before.decode()
234234
assert invitation not in p.before.decode()
235235

236+
# Also confirm that the exit code is conveyed properly
237+
p.close()
238+
assert p.exitstatus == 2 # make's generic error code
239+
assert p.signalstatus is None
240+
236241

237242
def test_metrics():
238243
"""
@@ -294,3 +299,35 @@ def test_handle_ctrl_c():
294299

295300
# Exit status is negative of signal's value (SIGINT = 2)
296301
assert data['properties']['exit_status'] == -2
302+
303+
# Exit signal here actually comes from make, so this doesn't
304+
# really test the wrapper's own exit code. This assertion
305+
# really just serves as documentation of behavior.
306+
p.close()
307+
assert p.exitstatus == None
308+
assert p.signalstatus is 2
309+
310+
311+
def test_signal_conversion():
312+
"""
313+
This is like test_handle_ctrl_c, except calling the wrapper
314+
directly to avoid Make's conversion of all exit codes to 2.
315+
"""
316+
with environment_as({'collection_enabled': True}):
317+
do_opt_in()
318+
p = pexpect.spawn('scripts/send-metrics.py wrap dev.pull', timeout=60)
319+
# Make sure wrapped command has started before we interrupt,
320+
# otherwise signal handler won't even have been registered
321+
# yet.
322+
p.expect(r'Are you sure you want to run this command')
323+
p.send(b'\x03') # send Ctrl-C to process group
324+
# Confirm that the process is actually catching the signal, as
325+
# proven by it printing some things before ending.
326+
p.expect(r'Send metrics info:.*"exit_status": ?-2')
327+
p.expect(EOF)
328+
329+
# This time we're calling the script directly, so we see the
330+
# script exiting with code 130 (128 + SIGINT).
331+
p.close()
332+
assert p.exitstatus == 130
333+
assert p.signalstatus is None

0 commit comments

Comments
 (0)