Skip to content

Commit b257cc1

Browse files
committed
Refactor: Improve subprocess timeout handling
This refactors `run_command` to use `subprocess.Popen.communicate(timeout=...)` for a more robust timeout mechanism. The old method, relying on a custom `threading.Timer`, could fail to terminate stubborn processes like `fastboot oem dmesg` which ignore `SIGTERM`. The new approach uses `communicate()`, which sends a forceful `SIGKILL` on timeout, guaranteeing the process is terminated. This change also updates the code to use the modern `text` argument introduced in Python 3.7 to replace `universal_newlines`.
1 parent c6692ad commit b257cc1

File tree

2 files changed

+19
-36
lines changed

2 files changed

+19
-36
lines changed

mobly/utils.py

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -474,36 +474,23 @@ def run_command(
474474
shell=shell,
475475
cwd=cwd,
476476
env=env,
477-
universal_newlines=universal_newlines,
477+
text=universal_newlines, # "text" is introdcued in Python 3.7.
478478
)
479-
timer = None
480-
timer_triggered = threading.Event()
481-
if timeout and timeout > 0:
482-
# The wait method on process will hang when used with PIPEs with large
483-
# outputs, so use a timer thread instead.
484-
485-
def timeout_expired():
486-
timer_triggered.set()
487-
process.terminate()
488-
489-
timer = threading.Timer(timeout, timeout_expired)
490-
timer.start()
491-
# If the command takes longer than the timeout, then the timer thread
492-
# will kill the subprocess, which will make it terminate.
493-
out, err = process.communicate()
494-
if timer is not None:
495-
timer.cancel()
496-
if timer_triggered.is_set():
497-
raise subprocess.TimeoutExpired(
498-
cmd=cmd, timeout=timeout, output=out, stderr=err
479+
out, err = None, None
480+
try:
481+
out, err = process.communicate(timeout=timeout)
482+
except subprocess.TimeoutExpired:
483+
process.kill()
484+
out, err = process.communicate()
485+
raise
486+
finally:
487+
logging.debug(
488+
'cmd: %s, stdout: %s, stderr: %s, ret: %s',
489+
cli_cmd_to_string(cmd),
490+
out,
491+
err,
492+
process.returncode,
499493
)
500-
logging.debug(
501-
'cmd: %s, stdout: %s, stderr: %s, ret: %s',
502-
cli_cmd_to_string(cmd),
503-
out,
504-
err,
505-
process.returncode,
506-
)
507494
return process.returncode, out, err
508495

509496

tests/mobly/utils_test.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,8 @@ def test_run_command_with_timeout_expired(self):
298298
with self.assertRaisesRegex(subprocess.TimeoutExpired, 'sleep'):
299299
_ = utils.run_command(self.sleep_cmd(4), timeout=0.01)
300300

301-
@mock.patch('threading.Timer')
302301
@mock.patch('subprocess.Popen')
303-
def test_run_command_with_default_params(self, mock_popen, mock_timer):
302+
def test_run_command_with_default_params(self, mock_popen):
304303
mock_command = mock.MagicMock(spec=dict)
305304
mock_proc = mock_popen.return_value
306305
mock_proc.communicate.return_value = ('fake_out', 'fake_err')
@@ -316,13 +315,11 @@ def test_run_command_with_default_params(self, mock_popen, mock_timer):
316315
shell=False,
317316
cwd=None,
318317
env=None,
319-
universal_newlines=False,
318+
text=False,
320319
)
321-
mock_timer.assert_not_called()
322320

323-
@mock.patch('threading.Timer')
324321
@mock.patch('subprocess.Popen')
325-
def test_run_command_with_custom_params(self, mock_popen, mock_timer):
322+
def test_run_command_with_custom_params(self, mock_popen):
326323
mock_command = mock.MagicMock(spec=dict)
327324
mock_stdout = mock.MagicMock(spec=int)
328325
mock_stderr = mock.MagicMock(spec=int)
@@ -352,9 +349,8 @@ def test_run_command_with_custom_params(self, mock_popen, mock_timer):
352349
shell=mock_shell,
353350
cwd=None,
354351
env=mock_env,
355-
universal_newlines=mock_universal_newlines,
352+
text=mock_universal_newlines,
356353
)
357-
mock_timer.assert_called_with(1234, mock.ANY)
358354

359355
def test_run_command_with_universal_newlines_false(self):
360356
_, out, _ = utils.run_command(

0 commit comments

Comments
 (0)