| #!/usr/bin/env vpython |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Runs tests with Xvfb and Openbox or Weston on Linux and normally on other |
| platforms.""" |
| |
| from __future__ import print_function |
| |
| import copy |
| import os |
| import os.path |
| import psutil |
| import random |
| import re |
| import signal |
| import subprocess |
| import sys |
| import threading |
| import time |
| import test_env |
| |
| class _XvfbProcessError(Exception): |
| """Exception raised when Xvfb cannot start.""" |
| pass |
| |
| |
| class _WestonProcessError(Exception): |
| """Exception raised when Weston cannot start.""" |
| pass |
| |
| |
| def kill(proc, name, timeout_in_seconds=10): |
| """Tries to kill |proc| gracefully with a timeout for each signal.""" |
| if not proc: |
| return |
| |
| proc.terminate() |
| thread = threading.Thread(target=proc.wait) |
| thread.start() |
| |
| thread.join(timeout_in_seconds) |
| if thread.is_alive(): |
| print('%s running after SIGTERM, trying SIGKILL.\n' % name, file=sys.stderr) |
| proc.kill() |
| |
| thread.join(timeout_in_seconds) |
| if thread.is_alive(): |
| print('%s running after SIGTERM and SIGKILL; good luck!\n' % name, |
| file=sys.stderr) |
| |
| |
| def launch_dbus(env): |
| """Starts a DBus session. |
| |
| Works around a bug in GLib where it performs operations which aren't |
| async-signal-safe (in particular, memory allocations) between fork and exec |
| when it spawns subprocesses. This causes threads inside Chrome's browser and |
| utility processes to get stuck, and this harness to hang waiting for those |
| processes, which will never terminate. This doesn't happen on users' |
| machines, because they have an active desktop session and the |
| DBUS_SESSION_BUS_ADDRESS environment variable set, but it can happen on |
| headless environments. This is fixed by glib commit [1], but this workaround |
| will be necessary until the fix rolls into Chromium's CI. |
| |
| [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12 |
| |
| Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and |
| DBUS_SESSION_BUS_PID set. |
| |
| Returns the pid of the dbus-daemon if started, or None otherwise. |
| """ |
| if 'DBUS_SESSION_BUS_ADDRESS' in os.environ: |
| return |
| try: |
| dbus_output = subprocess.check_output( |
| ['dbus-launch'], env=env).decode('utf-8').split('\n') |
| for line in dbus_output: |
| m = re.match(r'([^=]+)\=(.+)', line) |
| if m: |
| env[m.group(1)] = m.group(2) |
| return int(env['DBUS_SESSION_BUS_PID']) |
| except (subprocess.CalledProcessError, OSError, KeyError, ValueError) as e: |
| print('Exception while running dbus_launch: %s' % e) |
| |
| |
| # TODO(crbug.com/949194): Encourage setting flags to False. |
| def run_executable( |
| cmd, env, stdoutfile=None, use_openbox=True, use_xcompmgr=True): |
| """Runs an executable within Weston or Xvfb on Linux or normally on other |
| platforms. |
| |
| The method sets SIGUSR1 handler for Xvfb to return SIGUSR1 |
| when it is ready for connections. |
| https://www.x.org/archive/X11R7.5/doc/man/man1/Xserver.1.html under Signals. |
| |
| Args: |
| cmd: Command to be executed. |
| env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is |
| used. "WAYLAND_DISPLAY" will be set if Weston is used. |
| stdoutfile: If provided, symbolization via script is disabled and stdout |
| is written to this file as well as to stdout. |
| use_openbox: A flag to use openbox process. |
| Some ChromeOS tests need a window manager. |
| use_xcompmgr: A flag to use xcompmgr process. |
| Some tests need a compositing wm to make use of transparent visuals. |
| |
| Returns: |
| the exit code of the specified commandline, or 1 on failure. |
| """ |
| |
| # It might seem counterintuitive to support a --no-xvfb flag in a script |
| # whose only job is to start xvfb, but doing so allows us to consolidate |
| # the logic in the layers of buildbot scripts so that we *always* use |
| # xvfb by default and don't have to worry about the distinction, it |
| # can remain solely under the control of the test invocation itself. |
| use_xvfb = True |
| if '--no-xvfb' in cmd: |
| use_xvfb = False |
| cmd.remove('--no-xvfb') |
| |
| # Tests that run on Linux platforms with Ozone/Wayland backend require |
| # a Weston instance. However, it is also required to disable xvfb so |
| # that Weston can run in a pure headless environment. |
| use_weston = False |
| if '--use-weston' in cmd: |
| if use_xvfb: |
| print('Unable to use Weston with xvfb.\n', file=sys.stderr) |
| return 1 |
| use_weston = True |
| cmd.remove('--use-weston') |
| |
| if sys.platform.startswith('linux') and use_xvfb: |
| return _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr) |
| elif use_weston: |
| return _run_with_weston(cmd, env, stdoutfile) |
| else: |
| return test_env.run_executable(cmd, env, stdoutfile) |
| |
| |
| def _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr): |
| openbox_proc = None |
| xcompmgr_proc = None |
| xvfb_proc = None |
| xwmstartupcheck_proc = None |
| xvfb_ready = MutableBoolean() |
| def set_xvfb_ready(*_): |
| xvfb_ready.setvalue(True) |
| |
| dbus_pid = None |
| try: |
| signal.signal(signal.SIGTERM, raise_xvfb_error) |
| signal.signal(signal.SIGINT, raise_xvfb_error) |
| |
| # Before [1], the maximum number of X11 clients was 256. After, the default |
| # limit is 256 with a configurable maximum of 512. On systems with a large |
| # number of CPUs, the old limit of 256 may be hit for certain test suites |
| # [2] [3], so we set the limit to 512 when possible. This flag is not |
| # available on Ubuntu 16.04 or 18.04, so a feature check is required. Xvfb |
| # does not have a '-version' option, so checking the '-help' output is |
| # required. |
| # |
| # [1] d206c240c0b85c4da44f073d6e9a692afb6b96d2 |
| # [2] https://crbug.com/1187948 |
| # [3] https://crbug.com/1120107 |
| xvfb_help = subprocess.check_output( |
| ['Xvfb', '-help'], stderr=subprocess.STDOUT).decode('utf8') |
| |
| # Due to race condition for display number, Xvfb might fail to run. |
| # If it does fail, try again up to 10 times, similarly to xvfb-run. |
| for _ in range(10): |
| xvfb_ready.setvalue(False) |
| display = find_display() |
| |
| xvfb_cmd = ['Xvfb', display, '-screen', '0', '1280x800x24', '-ac', |
| '-nolisten', 'tcp', '-dpi', '96', '+extension', 'RANDR'] |
| if '-maxclients' in xvfb_help: |
| xvfb_cmd += ['-maxclients', '512'] |
| |
| # Sets SIGUSR1 to ignore for Xvfb to signal current process |
| # when it is ready. Due to race condition, USR1 signal could be sent |
| # before the process resets the signal handler, we cannot rely on |
| # signal handler to change on time. |
| signal.signal(signal.SIGUSR1, signal.SIG_IGN) |
| xvfb_proc = subprocess.Popen(xvfb_cmd, stderr=subprocess.STDOUT, env=env) |
| signal.signal(signal.SIGUSR1, set_xvfb_ready) |
| for _ in range(10): |
| time.sleep(.1) # gives Xvfb time to start or fail. |
| if xvfb_ready.getvalue() or xvfb_proc.poll() is not None: |
| break # xvfb sent ready signal, or already failed and stopped. |
| |
| if xvfb_proc.poll() is None: |
| break # xvfb is running, can proceed. |
| if xvfb_proc.poll() is not None: |
| raise _XvfbProcessError('Failed to start after 10 tries') |
| |
| env['DISPLAY'] = display |
| # Set dummy variable for scripts. |
| env['XVFB_DISPLAY'] = display |
| |
| dbus_pid = launch_dbus(env) |
| |
| if use_openbox: |
| # This is not ideal, but x11_unittests require that (other X11 tests have |
| # a race with the openbox as well, but they take more time to initialize. |
| # And thus, they do no time out compate to the x11_unittests that are |
| # quick enough to start up before openbox is ready. |
| # TODO(dpranke): remove this nasty hack once the test() template is |
| # reworked. |
| wait_for_openbox = False |
| wait_openbox_program = './xwmstartupcheck' |
| if not os.path.isfile(wait_openbox_program): |
| wait_for_openbox = False |
| # Creates a dummy window that waits for a ReparentNotify event that is |
| # sent whenever Openbox WM starts. Must be started before the OpenBox WM |
| # so that it does not miss the event. This helper program is located in |
| # the current build directory. The program terminates automatically after |
| # 30 seconds of waiting for the event. |
| if wait_for_openbox: |
| xwmstartupcheck_proc = subprocess.Popen( |
| wait_openbox_program, stderr=subprocess.STDOUT, env=env) |
| |
| openbox_proc = subprocess.Popen( |
| ['openbox', '--sm-disable'], stderr=subprocess.STDOUT, env=env) |
| |
| # Wait until execution is done. Does not block if the process has already |
| # been terminated. In that case, it's safe to read the return value. |
| if wait_for_openbox: |
| xwmstartupcheck_proc.wait() |
| if xwmstartupcheck_proc.returncode != 0: |
| raise _XvfbProcessError('Failed to get OpenBox up.') |
| |
| if use_xcompmgr: |
| xcompmgr_proc = subprocess.Popen( |
| 'xcompmgr', stderr=subprocess.STDOUT, env=env) |
| |
| return test_env.run_executable(cmd, env, stdoutfile) |
| except OSError as e: |
| print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr) |
| return 1 |
| except _XvfbProcessError as e: |
| print('Xvfb fail: %s\n' % str(e), file=sys.stderr) |
| return 1 |
| finally: |
| kill(openbox_proc, 'openbox') |
| kill(xcompmgr_proc, 'xcompmgr') |
| kill(xvfb_proc, 'Xvfb') |
| |
| # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it. |
| # To ensure it exits, use SIGKILL which should be safe since all other |
| # processes that it would have been servicing have exited. |
| if dbus_pid: |
| os.kill(dbus_pid, signal.SIGKILL) |
| |
| |
| # TODO(https://crbug.com/1060466): Write tests. |
| def _run_with_weston(cmd, env, stdoutfile): |
| weston_proc = None |
| |
| try: |
| signal.signal(signal.SIGTERM, raise_weston_error) |
| signal.signal(signal.SIGINT, raise_weston_error) |
| |
| dbus_pid = launch_dbus(env) |
| |
| # The bundled weston (//third_party/weston) is used by Linux Ozone Wayland |
| # CI and CQ testers and compiled by //ui/ozone/platform/wayland whenever |
| # there is a dependency on the Ozone/Wayland and use_bundled_weston is set |
| # in gn args. However, some tests do not require Wayland or do not use |
| # //ui/ozone at all, but still have --use-weston flag set by the |
| # OZONE_WAYLAND variant (see //testing/buildbot/variants.pyl). This results |
| # in failures and those tests cannot be run because of the exception that |
| # informs about missing weston binary. Thus, to overcome the issue before |
| # a better solution is found, add a check for the "weston" binary here and |
| # run tests without Wayland compositor if the weston binary is not found. |
| # TODO(https://1178788): find a better solution. |
| if not os.path.isfile("./weston"): |
| print('Weston is not available. Starting without Wayland compositor') |
| return test_env.run_executable(cmd, env, stdoutfile) |
| |
| # Set $XDG_RUNTIME_DIR if it is not set. |
| _set_xdg_runtime_dir(env) |
| |
| # Weston is compiled along with the Ozone/Wayland platform, and is |
| # fetched as data deps. Thus, run it from the current directory. |
| # |
| # Weston is used with the following flags: |
| # 1) --backend=headless-backend.so - runs Weston in a headless mode |
| # that does not require a real GPU card. |
| # 2) --idle-time=0 - disables idle timeout, which prevents Weston |
| # to enter idle state. Otherwise, Weston stops to send frame callbacks, |
| # and tests start to time out (this typically happens after 300 seconds - |
| # the default time after which Weston enters the idle state). |
| # 3) --width && --height set size of a virtual display: we need to set |
| # an adequate size so that tests can have more room for managing size |
| # of windows. |
| # 4) --use-gl - Runs Weston using hardware acceleration instead of |
| # SwiftShader. |
| weston_cmd = ['./weston', '--backend=headless-backend.so', '--idle-time=0', |
| '--width=1024', '--height=768', '--modules=test-plugin.so'] |
| |
| if '--weston-use-gl' in cmd: |
| weston_cmd.append('--use-gl') |
| cmd.remove('--weston-use-gl') |
| |
| if '--weston-debug-logging' in cmd: |
| cmd.remove('--weston-debug-logging') |
| env = copy.deepcopy(env) |
| env['WAYLAND_DEBUG'] = '1' |
| |
| weston_proc_display = None |
| for _ in range(10): |
| weston_proc = subprocess.Popen( |
| weston_cmd, |
| stderr=subprocess.STDOUT, env=env) |
| |
| # Get the $WAYLAND_DISPLAY set by Weston and pass it to the test launcher. |
| # Please note that this env variable is local for the process. That's the |
| # reason we have to read it from Weston separately. |
| weston_proc_display = _get_display_from_weston(weston_proc.pid) |
| if weston_proc_display is not None: |
| break # Weston could launch and we found the display. |
| |
| # If we couldn't find the display after 10 tries, raise an exception. |
| if weston_proc_display is None: |
| raise _WestonProcessError('Failed to start Weston.') |
| env['WAYLAND_DISPLAY'] = weston_proc_display |
| return test_env.run_executable(cmd, env, stdoutfile) |
| except OSError as e: |
| print('Failed to start Weston: %s\n' % str(e), file=sys.stderr) |
| return 1 |
| except _WestonProcessError as e: |
| print('Weston fail: %s\n' % str(e), file=sys.stderr) |
| return 1 |
| finally: |
| kill(weston_proc, 'weston') |
| |
| # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it. |
| # To ensure it exits, use SIGKILL which should be safe since all other |
| # processes that it would have been servicing have exited. |
| if dbus_pid: |
| os.kill(dbus_pid, signal.SIGKILL) |
| |
| def _get_display_from_weston(weston_proc_pid): |
| """Retrieves $WAYLAND_DISPLAY set by Weston. |
| |
| Searches for the child "weston-desktop-shell" process, takes its |
| environmental variables, and returns $WAYLAND_DISPLAY variable set |
| by that process. If the variable is not set, tries up to 10 times |
| and then gives up. |
| |
| Args: |
| weston_proc_pid: The process of id of the main Weston process. |
| |
| Returns: |
| the display set by Wayland, which clients can use to connect to. |
| |
| TODO(https://crbug.com/1060469): This is potentially error prone |
| function. See the bug for further details. |
| """ |
| |
| # Try 100 times as it is not known when Weston spawn child desktop shell |
| # process. The most seen so far is ~50 checks/~2.5 seconds, but startup |
| # is usually almost instantaneous. |
| for _ in range(100): |
| # gives weston time to start or fail. |
| time.sleep(.05) |
| # Take the parent process. |
| parent = psutil.Process(weston_proc_pid) |
| if parent is None: |
| break # The process is not found. Give up. |
| |
| # Traverse through all the children processes and find the |
| # "weston-desktop-shell" process that sets local to process env variables |
| # including the $WAYLAND_DISPLAY. |
| children = parent.children(recursive=True) |
| for process in children: |
| if process.name() == "weston-desktop-shell": |
| weston_proc_display = process.environ().get('WAYLAND_DISPLAY') |
| # If display is set, Weston could start successfully and we can use |
| # that display for Wayland connection in Chromium. |
| if weston_proc_display is not None: |
| return weston_proc_display |
| return None |
| |
| |
| class MutableBoolean(object): |
| """Simple mutable boolean class. Used to be mutated inside an handler.""" |
| |
| def __init__(self): |
| self._val = False |
| |
| def setvalue(self, val): |
| assert isinstance(val, bool) |
| self._val = val |
| |
| def getvalue(self): |
| return self._val |
| |
| |
| def raise_xvfb_error(*_): |
| raise _XvfbProcessError('Terminated') |
| |
| |
| def raise_weston_error(*_): |
| raise _WestonProcessError('Terminated') |
| |
| |
| def find_display(): |
| """Iterates through X-lock files to find an available display number. |
| |
| The lower bound follows xvfb-run standard at 99, and the upper bound |
| is set to 119. |
| |
| Returns: |
| A string of a random available display number for Xvfb ':{99-119}'. |
| |
| Raises: |
| _XvfbProcessError: Raised when displays 99 through 119 are unavailable. |
| """ |
| |
| available_displays = [ |
| d for d in range(99, 120) |
| if not os.path.isfile('/tmp/.X{}-lock'.format(d)) |
| ] |
| if available_displays: |
| return ':{}'.format(random.choice(available_displays)) |
| raise _XvfbProcessError('Failed to find display number') |
| |
| |
| def _set_xdg_runtime_dir(env): |
| """Sets the $XDG_RUNTIME_DIR variable if it hasn't been set before.""" |
| runtime_dir = env.get('XDG_RUNTIME_DIR') |
| if not runtime_dir: |
| runtime_dir = '/tmp/xdg-tmp-dir/' |
| if not os.path.exists(runtime_dir): |
| os.makedirs(runtime_dir, 0o700) |
| env['XDG_RUNTIME_DIR'] = runtime_dir |
| |
| |
| def main(): |
| usage = 'Usage: xvfb.py [command [--no-xvfb or --use-weston] args...]' |
| if len(sys.argv) < 2: |
| print(usage + '\n', file=sys.stderr) |
| return 2 |
| |
| # If the user still thinks the first argument is the execution directory then |
| # print a friendly error message and quit. |
| if os.path.isdir(sys.argv[1]): |
| print('Invalid command: \"%s\" is a directory\n' % sys.argv[1], |
| file=sys.stderr) |
| print(usage + '\n', file=sys.stderr) |
| return 3 |
| |
| return run_executable(sys.argv[1:], os.environ.copy()) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |