1#!/usr/bin/env vpython3 2# Copyright 2012 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Runs tests with Xvfb and Openbox or Weston on Linux and normally on other 7 platforms.""" 8 9from __future__ import print_function 10 11import copy 12import os 13import os.path 14import random 15import re 16import signal 17import socket 18import subprocess 19import sys 20import tempfile 21import threading 22import time 23 24import psutil 25 26import test_env 27 28DEFAULT_XVFB_WHD = '1280x800x24' 29 30# pylint: disable=useless-object-inheritance 31 32 33class _XvfbProcessError(Exception): 34 """Exception raised when Xvfb cannot start.""" 35 36 37class _WestonProcessError(Exception): 38 """Exception raised when Weston cannot start.""" 39 40 41def kill(proc, name, timeout_in_seconds=10): 42 """Tries to kill |proc| gracefully with a timeout for each signal.""" 43 if not proc: 44 return 45 46 thread = threading.Thread(target=proc.wait) 47 try: 48 proc.terminate() 49 thread.start() 50 51 thread.join(timeout_in_seconds) 52 if thread.is_alive(): 53 print('%s running after SIGTERM, trying SIGKILL.\n' % name, 54 file=sys.stderr) 55 proc.kill() 56 except OSError as e: 57 # proc.terminate()/kill() can raise, not sure if only ProcessLookupError 58 # which is explained in https://bugs.python.org/issue40550#msg382427 59 print('Exception while killing process %s: %s' % (name, e), file=sys.stderr) 60 61 thread.join(timeout_in_seconds) 62 if thread.is_alive(): 63 print('%s running after SIGTERM and SIGKILL; good luck!\n' % name, 64 file=sys.stderr) 65 66 67def launch_dbus(env): # pylint: disable=inconsistent-return-statements 68 """Starts a DBus session. 69 70 Works around a bug in GLib where it performs operations which aren't 71 async-signal-safe (in particular, memory allocations) between fork and exec 72 when it spawns subprocesses. This causes threads inside Chrome's browser and 73 utility processes to get stuck, and this harness to hang waiting for those 74 processes, which will never terminate. This doesn't happen on users' 75 machines, because they have an active desktop session and the 76 DBUS_SESSION_BUS_ADDRESS environment variable set, but it can happen on 77 headless environments. This is fixed by glib commit [1], but this workaround 78 will be necessary until the fix rolls into Chromium's CI. 79 80 [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12 81 82 Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and 83 DBUS_SESSION_BUS_PID set. 84 85 Returns the pid of the dbus-daemon if started, or None otherwise. 86 """ 87 if 'DBUS_SESSION_BUS_ADDRESS' in os.environ: 88 return 89 try: 90 dbus_output = subprocess.check_output( 91 ['dbus-launch'], env=env).decode('utf-8').split('\n') 92 for line in dbus_output: 93 m = re.match(r'([^=]+)\=(.+)', line) 94 if m: 95 env[m.group(1)] = m.group(2) 96 return int(env['DBUS_SESSION_BUS_PID']) 97 except (subprocess.CalledProcessError, OSError, KeyError, ValueError) as e: 98 print('Exception while running dbus_launch: %s' % e) 99 100 101# TODO(crbug.com/949194): Encourage setting flags to False. 102def run_executable( 103 cmd, env, stdoutfile=None, use_openbox=True, use_xcompmgr=True, 104 xvfb_whd=None, cwd=None): 105 """Runs an executable within Weston or Xvfb on Linux or normally on other 106 platforms. 107 108 The method sets SIGUSR1 handler for Xvfb to return SIGUSR1 109 when it is ready for connections. 110 https://www.x.org/archive/X11R7.5/doc/man/man1/Xserver.1.html under Signals. 111 112 Args: 113 cmd: Command to be executed. 114 env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is 115 used. "WAYLAND_DISPLAY" will be set if Weston is used. 116 stdoutfile: If provided, symbolization via script is disabled and stdout 117 is written to this file as well as to stdout. 118 use_openbox: A flag to use openbox process. 119 Some ChromeOS tests need a window manager. 120 use_xcompmgr: A flag to use xcompmgr process. 121 Some tests need a compositing wm to make use of transparent visuals. 122 xvfb_whd: WxHxD to pass to xvfb or DEFAULT_XVFB_WHD if None 123 cwd: Current working directory. 124 125 Returns: 126 the exit code of the specified commandline, or 1 on failure. 127 """ 128 129 # It might seem counterintuitive to support a --no-xvfb flag in a script 130 # whose only job is to start xvfb, but doing so allows us to consolidate 131 # the logic in the layers of buildbot scripts so that we *always* use 132 # xvfb by default and don't have to worry about the distinction, it 133 # can remain solely under the control of the test invocation itself. 134 use_xvfb = True 135 if '--no-xvfb' in cmd: 136 use_xvfb = False 137 cmd.remove('--no-xvfb') 138 139 # Tests that run on Linux platforms with Ozone/Wayland backend require 140 # a Weston instance. However, it is also required to disable xvfb so 141 # that Weston can run in a pure headless environment. 142 use_weston = False 143 if '--use-weston' in cmd: 144 if use_xvfb: 145 print('Unable to use Weston with xvfb.\n', file=sys.stderr) 146 return 1 147 use_weston = True 148 cmd.remove('--use-weston') 149 150 if sys.platform.startswith('linux') and use_xvfb: 151 return _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr, 152 xvfb_whd or DEFAULT_XVFB_WHD, cwd) 153 if use_weston: 154 return _run_with_weston(cmd, env, stdoutfile, cwd) 155 return test_env.run_executable(cmd, env, stdoutfile, cwd) 156 157 158def _run_with_xvfb(cmd, env, stdoutfile, use_openbox, 159 use_xcompmgr, xvfb_whd, cwd): 160 openbox_proc = None 161 openbox_ready = MutableBoolean() 162 def set_openbox_ready(*_): 163 openbox_ready.setvalue(True) 164 165 xcompmgr_proc = None 166 xvfb_proc = None 167 xvfb_ready = MutableBoolean() 168 def set_xvfb_ready(*_): 169 xvfb_ready.setvalue(True) 170 171 dbus_pid = None 172 try: 173 signal.signal(signal.SIGTERM, raise_xvfb_error) 174 signal.signal(signal.SIGINT, raise_xvfb_error) 175 176 # Before [1], the maximum number of X11 clients was 256. After, the default 177 # limit is 256 with a configurable maximum of 512. On systems with a large 178 # number of CPUs, the old limit of 256 may be hit for certain test suites 179 # [2] [3], so we set the limit to 512 when possible. This flag is not 180 # available on Ubuntu 16.04 or 18.04, so a feature check is required. Xvfb 181 # does not have a '-version' option, so checking the '-help' output is 182 # required. 183 # 184 # [1] d206c240c0b85c4da44f073d6e9a692afb6b96d2 185 # [2] https://crbug.com/1187948 186 # [3] https://crbug.com/1120107 187 xvfb_help = subprocess.check_output( 188 ['Xvfb', '-help'], stderr=subprocess.STDOUT).decode('utf8') 189 190 # Due to race condition for display number, Xvfb might fail to run. 191 # If it does fail, try again up to 10 times, similarly to xvfb-run. 192 for _ in range(10): 193 xvfb_ready.setvalue(False) 194 display = find_display() 195 196 xvfb_cmd = ['Xvfb', display, '-screen', '0', xvfb_whd, '-ac', 197 '-nolisten', 'tcp', '-dpi', '96', '+extension', 'RANDR'] 198 if '-maxclients' in xvfb_help: 199 xvfb_cmd += ['-maxclients', '512'] 200 201 # Sets SIGUSR1 to ignore for Xvfb to signal current process 202 # when it is ready. Due to race condition, USR1 signal could be sent 203 # before the process resets the signal handler, we cannot rely on 204 # signal handler to change on time. 205 signal.signal(signal.SIGUSR1, signal.SIG_IGN) 206 xvfb_proc = subprocess.Popen(xvfb_cmd, stderr=subprocess.STDOUT, env=env) 207 signal.signal(signal.SIGUSR1, set_xvfb_ready) 208 for _ in range(30): 209 time.sleep(.1) # gives Xvfb time to start or fail. 210 if xvfb_ready.getvalue() or xvfb_proc.poll() is not None: 211 break # xvfb sent ready signal, or already failed and stopped. 212 213 if xvfb_proc.poll() is None: 214 if xvfb_ready.getvalue(): 215 break # xvfb is ready 216 kill(xvfb_proc, 'Xvfb') # still not ready, give up and retry 217 218 if xvfb_proc.poll() is not None: 219 raise _XvfbProcessError('Failed to start after 10 tries') 220 221 env['DISPLAY'] = display 222 # Set dummy variable for scripts. 223 env['XVFB_DISPLAY'] = display 224 225 dbus_pid = launch_dbus(env) 226 227 if use_openbox: 228 # Openbox will send a SIGUSR1 signal to the current process notifying the 229 # script it has started up. 230 current_proc_id = os.getpid() 231 232 # The CMD that is passed via the --startup flag. 233 openbox_startup_cmd = 'kill --signal SIGUSR1 %s' % str(current_proc_id) 234 # Setup the signal handlers before starting the openbox instance. 235 signal.signal(signal.SIGUSR1, signal.SIG_IGN) 236 signal.signal(signal.SIGUSR1, set_openbox_ready) 237 openbox_proc = subprocess.Popen( 238 ['openbox', '--sm-disable', '--startup', 239 openbox_startup_cmd], stderr=subprocess.STDOUT, env=env) 240 241 for _ in range(30): 242 time.sleep(.1) # gives Openbox time to start or fail. 243 if openbox_ready.getvalue() or openbox_proc.poll() is not None: 244 break # openbox sent ready signal, or failed and stopped. 245 246 if openbox_proc.poll() is not None or not openbox_ready.getvalue(): 247 raise _XvfbProcessError('Failed to start OpenBox.') 248 249 if use_xcompmgr: 250 xcompmgr_proc = subprocess.Popen( 251 'xcompmgr', stderr=subprocess.STDOUT, env=env) 252 253 return test_env.run_executable(cmd, env, stdoutfile, cwd) 254 except OSError as e: 255 print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr) 256 return 1 257 except _XvfbProcessError as e: 258 print('Xvfb fail: %s\n' % str(e), file=sys.stderr) 259 return 1 260 finally: 261 kill(openbox_proc, 'openbox') 262 kill(xcompmgr_proc, 'xcompmgr') 263 kill(xvfb_proc, 'Xvfb') 264 265 # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it. 266 # To ensure it exits, use SIGKILL which should be safe since all other 267 # processes that it would have been servicing have exited. 268 if dbus_pid: 269 os.kill(dbus_pid, signal.SIGKILL) 270 271 272# TODO(https://crbug.com/1060466): Write tests. 273def _run_with_weston(cmd, env, stdoutfile, cwd): 274 weston_proc = None 275 276 try: 277 signal.signal(signal.SIGTERM, raise_weston_error) 278 signal.signal(signal.SIGINT, raise_weston_error) 279 280 dbus_pid = launch_dbus(env) 281 282 # The bundled weston (//third_party/weston) is used by Linux Ozone Wayland 283 # CI and CQ testers and compiled by //ui/ozone/platform/wayland whenever 284 # there is a dependency on the Ozone/Wayland and use_bundled_weston is set 285 # in gn args. However, some tests do not require Wayland or do not use 286 # //ui/ozone at all, but still have --use-weston flag set by the 287 # OZONE_WAYLAND variant (see //testing/buildbot/variants.pyl). This results 288 # in failures and those tests cannot be run because of the exception that 289 # informs about missing weston binary. Thus, to overcome the issue before 290 # a better solution is found, add a check for the "weston" binary here and 291 # run tests without Wayland compositor if the weston binary is not found. 292 # TODO(https://1178788): find a better solution. 293 if not os.path.isfile("./weston"): 294 print('Weston is not available. Starting without Wayland compositor') 295 return test_env.run_executable(cmd, env, stdoutfile, cwd) 296 297 # Set $XDG_RUNTIME_DIR if it is not set. 298 _set_xdg_runtime_dir(env) 299 300 # Write options that can't be passed via CLI flags to the config file. 301 # 1) panel-position=none - disables the panel, which might interfere with 302 # the tests by blocking mouse input. 303 with open(_weston_config_file_path(), 'w') as weston_config_file: 304 weston_config_file.write('[shell]\npanel-position=none') 305 306 # Weston is compiled along with the Ozone/Wayland platform, and is 307 # fetched as data deps. Thus, run it from the current directory. 308 # 309 # Weston is used with the following flags: 310 # 1) --backend=headless-backend.so - runs Weston in a headless mode 311 # that does not require a real GPU card. 312 # 2) --idle-time=0 - disables idle timeout, which prevents Weston 313 # to enter idle state. Otherwise, Weston stops to send frame callbacks, 314 # and tests start to time out (this typically happens after 300 seconds - 315 # the default time after which Weston enters the idle state). 316 # 3) --modules=ui-controls.so,systemd-notify.so - enables support for the 317 # ui-controls Wayland protocol extension and the systemd-notify protocol. 318 # 4) --width && --height set size of a virtual display: we need to set 319 # an adequate size so that tests can have more room for managing size 320 # of windows. 321 # 5) --config=... - tells Weston to use our custom config. 322 weston_cmd = ['./weston', '--backend=headless-backend.so', '--idle-time=0', 323 '--modules=ui-controls.so,systemd-notify.so', '--width=1280', 324 '--height=800', '--config=' + _weston_config_file_path()] 325 326 if '--weston-use-gl' in cmd: 327 # Runs Weston using hardware acceleration instead of SwiftShader. 328 weston_cmd.append('--use-gl') 329 cmd.remove('--weston-use-gl') 330 331 if '--weston-debug-logging' in cmd: 332 cmd.remove('--weston-debug-logging') 333 env = copy.deepcopy(env) 334 env['WAYLAND_DEBUG'] = '1' 335 336 # We use the systemd-notify protocol to detect whether weston has launched 337 # successfully. We listen on a unix socket and set the NOTIFY_SOCKET 338 # environment variable to the socket's path. If we tell it to load its 339 # systemd-notify module, weston will send a 'READY=1' message to the socket 340 # once it has loaded that module. 341 # See the sd_notify(3) man page and weston's compositor/systemd-notify.c for 342 # more details. 343 with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM 344 | socket.SOCK_NONBLOCK) as notify_socket: 345 notify_socket.bind(_weston_notify_socket_address()) 346 env['NOTIFY_SOCKET'] = _weston_notify_socket_address() 347 348 weston_proc_display = None 349 for _ in range(10): 350 weston_proc = subprocess.Popen( 351 weston_cmd, 352 stderr=subprocess.STDOUT, env=env) 353 354 for _ in range(25): 355 time.sleep(0.1) # Gives weston some time to start. 356 try: 357 if notify_socket.recv(512) == b'READY=1': 358 break 359 except BlockingIOError: 360 continue 361 362 for _ in range(25): 363 # The 'READY=1' message is sent as soon as weston loads the 364 # systemd-notify module. This happens shortly before spawning its 365 # subprocesses (e.g. desktop-shell). Wait some more to ensure they 366 # have been spawned. 367 time.sleep(0.1) 368 369 # Get the $WAYLAND_DISPLAY set by Weston and pass it to the test 370 # launcher. Please note that this env variable is local for the 371 # process. That's the reason we have to read it from Weston 372 # separately. 373 weston_proc_display = _get_display_from_weston(weston_proc.pid) 374 if weston_proc_display is not None: 375 break # Weston could launch and we found the display. 376 377 # Also break from the outer loop. 378 if weston_proc_display is not None: 379 break 380 381 # If we couldn't find the display after 10 tries, raise an exception. 382 if weston_proc_display is None: 383 raise _WestonProcessError('Failed to start Weston.') 384 385 env.pop('NOTIFY_SOCKET') 386 387 env['WAYLAND_DISPLAY'] = weston_proc_display 388 if '--chrome-wayland-debugging' in cmd: 389 cmd.remove('--chrome-wayland-debugging') 390 env['WAYLAND_DEBUG'] = '1' 391 else: 392 env['WAYLAND_DEBUG'] = '0' 393 394 return test_env.run_executable(cmd, env, stdoutfile, cwd) 395 except OSError as e: 396 print('Failed to start Weston: %s\n' % str(e), file=sys.stderr) 397 return 1 398 except _WestonProcessError as e: 399 print('Weston fail: %s\n' % str(e), file=sys.stderr) 400 return 1 401 finally: 402 kill(weston_proc, 'weston') 403 404 if os.path.exists(_weston_notify_socket_address()): 405 os.remove(_weston_notify_socket_address()) 406 407 if os.path.exists(_weston_config_file_path()): 408 os.remove(_weston_config_file_path()) 409 410 # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it. 411 # To ensure it exits, use SIGKILL which should be safe since all other 412 # processes that it would have been servicing have exited. 413 if dbus_pid: 414 os.kill(dbus_pid, signal.SIGKILL) 415 416def _weston_notify_socket_address(): 417 return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston-notify.sock') 418 419def _weston_config_file_path(): 420 return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston.ini') 421 422def _get_display_from_weston(weston_proc_pid): 423 """Retrieves $WAYLAND_DISPLAY set by Weston. 424 425 Returns the $WAYLAND_DISPLAY variable from one of weston's subprocesses. 426 427 Weston updates this variable early in its startup in the main process, but we 428 can only read the environment variables as they were when the process was 429 created. Therefore we must use one of weston's subprocesses, which are all 430 spawned with the new value for $WAYLAND_DISPLAY. Any of them will do, as they 431 all have the same value set. 432 433 Args: 434 weston_proc_pid: The process of id of the main Weston process. 435 436 Returns: 437 the display set by Wayland, which clients can use to connect to. 438 """ 439 440 # Take the parent process. 441 parent = psutil.Process(weston_proc_pid) 442 if parent is None: 443 return None # The process is not found. Give up. 444 445 # Traverse through all the children processes and find one that has 446 # $WAYLAND_DISPLAY set. 447 children = parent.children(recursive=True) 448 for process in children: 449 weston_proc_display = process.environ().get('WAYLAND_DISPLAY') 450 # If display is set, Weston could start successfully and we can use 451 # that display for Wayland connection in Chromium. 452 if weston_proc_display is not None: 453 return weston_proc_display 454 return None 455 456 457class MutableBoolean(object): 458 """Simple mutable boolean class. Used to be mutated inside an handler.""" 459 460 def __init__(self): 461 self._val = False 462 463 def setvalue(self, val): 464 assert isinstance(val, bool) 465 self._val = val 466 467 def getvalue(self): 468 return self._val 469 470 471def raise_xvfb_error(*_): 472 raise _XvfbProcessError('Terminated') 473 474 475def raise_weston_error(*_): 476 raise _WestonProcessError('Terminated') 477 478 479def find_display(): 480 """Iterates through X-lock files to find an available display number. 481 482 The lower bound follows xvfb-run standard at 99, and the upper bound 483 is set to 119. 484 485 Returns: 486 A string of a random available display number for Xvfb ':{99-119}'. 487 488 Raises: 489 _XvfbProcessError: Raised when displays 99 through 119 are unavailable. 490 """ 491 492 available_displays = [ 493 d for d in range(99, 120) 494 if not os.path.isfile('/tmp/.X{}-lock'.format(d)) 495 ] 496 if available_displays: 497 return ':{}'.format(random.choice(available_displays)) 498 raise _XvfbProcessError('Failed to find display number') 499 500 501def _set_xdg_runtime_dir(env): 502 """Sets the $XDG_RUNTIME_DIR variable if it hasn't been set before.""" 503 runtime_dir = env.get('XDG_RUNTIME_DIR') 504 if not runtime_dir: 505 runtime_dir = '/tmp/xdg-tmp-dir/' 506 if not os.path.exists(runtime_dir): 507 os.makedirs(runtime_dir, 0o700) 508 env['XDG_RUNTIME_DIR'] = runtime_dir 509 510 511def main(): 512 usage = 'Usage: xvfb.py [command [--no-xvfb or --use-weston] args...]' 513 if len(sys.argv) < 2: 514 print(usage + '\n', file=sys.stderr) 515 return 2 516 517 # If the user still thinks the first argument is the execution directory then 518 # print a friendly error message and quit. 519 if os.path.isdir(sys.argv[1]): 520 print('Invalid command: \"%s\" is a directory\n' % sys.argv[1], 521 file=sys.stderr) 522 print(usage + '\n', file=sys.stderr) 523 return 3 524 525 return run_executable(sys.argv[1:], os.environ.copy()) 526 527 528if __name__ == '__main__': 529 sys.exit(main()) 530