Skip to content

Commit 02c9e71

Browse files
authored
Add a --force-browser-process-termination flag to the test runner (#25512)
A major headache when working with the browser test runs in both the single-threaded and parallel harness on the CI are stray browser processes that stay alive across browser test runs. This happens with Safari, and also with Firefox browser, on each of Windows, Linux and macOS systems. It is easy to get especially MacOS systems to hang so that one cannot even remote-desktop in to the CI hardware anymore, since all system resources are taken up by the runaway browser processes. One might argue that these are just "bugs", and after we fix those one by one, we would have a working test harness where browser processes are cleanly shut down so that stray browser processes would not remain. That might be the case, though we need a system that is robust to these types of bugs. To help this, add a failsafe mechanism in the form of a new `--force-browser-process-termination` flag to the test runner, to help tear down browser processes before and after a test suite run. This way even if the harness is misbehaving, there will be a backstop that aggressively attempts to clean up all browsers between test runs. This way I can have the CI tasks force-shutdown all runners between suite runs, to ensure that even if things are still not working 100%, I will not need to go and physically keep pressing the power button on my Mac Minis to boot them back up.
1 parent abe4564 commit 02c9e71

File tree

2 files changed

+41
-23
lines changed

2 files changed

+41
-23
lines changed

test/common.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ def is_firefox():
199199
return EMTEST_BROWSER and 'firefox' in EMTEST_BROWSER.lower()
200200

201201

202+
def get_browser_config():
203+
if is_chrome():
204+
return ChromeConfig()
205+
elif is_firefox():
206+
return FirefoxConfig()
207+
return None
208+
209+
202210
def compiler_for(filename, force_c=False):
203211
if shared.suffix(filename) in ('.cc', '.cxx', '.cpp') and not force_c:
204212
return EMXX
@@ -2410,11 +2418,7 @@ def configure_test_browser():
24102418
EMTEST_BROWSER = '"' + EMTEST_BROWSER.replace("\\", "\\\\") + '"'
24112419

24122420
if EMTEST_BROWSER_AUTO_CONFIG:
2413-
config = None
2414-
if is_chrome():
2415-
config = ChromeConfig()
2416-
elif is_firefox():
2417-
config = FirefoxConfig()
2421+
config = get_browser_config()
24182422
if config:
24192423
EMTEST_BROWSER += ' ' + ' '.join(config.default_flags)
24202424
if EMTEST_HEADLESS == 1:
@@ -2435,6 +2439,22 @@ def list_processes_by_name(exe_name):
24352439
return pids
24362440

24372441

2442+
def terminate_list_of_processes(proc_list):
2443+
for proc in proc_list:
2444+
try:
2445+
proc.terminate()
2446+
# If the browser doesn't shut down gracefully (in response to SIGTERM)
2447+
# after 2 seconds kill it with force (SIGKILL).
2448+
try:
2449+
proc.wait(2)
2450+
except (subprocess.TimeoutExpired, psutil.TimeoutExpired):
2451+
logger.info('Browser did not respond to `terminate`. Using `kill`')
2452+
proc.kill()
2453+
proc.wait()
2454+
except (psutil.NoSuchProcess, ProcessLookupError):
2455+
pass
2456+
2457+
24382458
class FileLock:
24392459
"""Implements a filesystem-based mutex, with an additional feature that it
24402460
returns an integer counter denoting how many times the lock has been locked
@@ -2519,19 +2539,7 @@ def __init__(self, *args, **kwargs):
25192539

25202540
@classmethod
25212541
def browser_terminate(cls):
2522-
for proc in cls.browser_procs:
2523-
try:
2524-
proc.terminate()
2525-
# If the browser doesn't shut down gracefully (in response to SIGTERM)
2526-
# after 2 seconds kill it with force (SIGKILL).
2527-
try:
2528-
proc.wait(2)
2529-
except (subprocess.TimeoutExpired, psutil.TimeoutExpired):
2530-
logger.info('Browser did not respond to `terminate`. Using `kill`')
2531-
proc.kill()
2532-
proc.wait()
2533-
except (psutil.NoSuchProcess, ProcessLookupError):
2534-
pass
2542+
terminate_list_of_processes(cls.browser_procs)
25352543

25362544
@classmethod
25372545
def browser_restart(cls):
@@ -2564,11 +2572,8 @@ def browser_open(cls, url):
25642572
# Recreate the new data directory.
25652573
os.mkdir(browser_data_dir)
25662574

2567-
if is_chrome():
2568-
config = ChromeConfig()
2569-
elif is_firefox():
2570-
config = FirefoxConfig()
2571-
else:
2575+
config = get_browser_config()
2576+
if not config:
25722577
exit_with_error(f'EMTEST_BROWSER_AUTO_CONFIG only currently works with firefox or chrome. EMTEST_BROWSER was "{EMTEST_BROWSER}"')
25732578
if WINDOWS:
25742579
# Escape directory delimiter backslashes for shlex.split.

test/runner.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def parse_args():
488488
'Useful when combined with --failfast')
489489
parser.add_argument('--force64', action='store_true')
490490
parser.add_argument('--crossplatform-only', action='store_true')
491+
parser.add_argument('--force-browser-process-termination', action='store_true', help='If true, a fail-safe method is used to ensure that all browser processes are terminated before and after the test suite run. Note that this option will terminate all browser processes, not just those launched by the harness, so will result in loss of all open browsing sessions.')
491492
parser.add_argument('--repeat', type=int, default=1,
492493
help='Repeat each test N times (default: 1).')
493494
parser.add_argument('--bell', action='store_true', help='Play a sound after the test suite finishes.')
@@ -566,6 +567,18 @@ def set_env(name, option_value):
566567
utils.delete_file(common.flaky_tests_log_filename)
567568
utils.delete_file(common.browser_spawn_lock_filename)
568569
utils.delete_file(f'{common.browser_spawn_lock_filename}_counter')
570+
if options.force_browser_process_termination or os.getenv('EMTEST_FORCE_BROWSER_PROCESS_TERMINATION'):
571+
config = common.get_browser_config()
572+
573+
def terminate_all_browser_processes():
574+
procs = common.list_processes_by_name(config.executable_name)
575+
if len(procs) > 0:
576+
print(f'Terminating {len(procs)} stray browser processes.')
577+
common.terminate_list_of_processes(procs)
578+
579+
if config and hasattr(config, 'executable_name'):
580+
atexit.register(terminate_all_browser_processes)
581+
terminate_all_browser_processes()
569582

570583
def prepend_default(arg):
571584
if arg.startswith('test_'):

0 commit comments

Comments
 (0)