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