• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""Runs tests with Xvfb or Xorg and Openbox or Weston on Linux and normally on
6other platforms."""
7
8from __future__ import print_function
9
10import copy
11import os
12import os.path
13import random
14import re
15import signal
16import socket
17import subprocess
18import sys
19import tempfile
20import threading
21import time
22import uuid
23
24import psutil
25
26import test_env
27
28DEFAULT_XVFB_WHD = '1280x800x24'
29
30# pylint: disable=useless-object-inheritance
31
32class _X11ProcessError(Exception):
33  """Exception raised when Xvfb or Xorg cannot start."""
34
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(['dbus-launch'],
90                                          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/40621504): Encourage setting flags to False.
101def run_executable(cmd,
102                   env,
103                   stdoutfile=None,
104                   use_openbox=True,
105                   use_xcompmgr=True,
106                   xvfb_whd=None,
107                   cwd=None):
108  """Runs an executable within Weston, Xvfb or Xorg on Linux or normally on
109     other platforms.
110
111  The method sets SIGUSR1 handler for Xvfb to return SIGUSR1
112  when it is ready for connections.
113  https://www.x.org/archive/X11R7.5/doc/man/man1/Xserver.1.html under Signals.
114
115  Args:
116    cmd: Command to be executed.
117    env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is
118      used. "WAYLAND_DISPLAY" will be set if Weston is used.
119    stdoutfile: If provided, symbolization via script is disabled and stdout
120      is written to this file as well as to stdout.
121    use_openbox: A flag to use openbox process.
122      Some ChromeOS tests need a window manager.
123    use_xcompmgr: A flag to use xcompmgr process.
124      Some tests need a compositing wm to make use of transparent visuals.
125    xvfb_whd: WxHxD to pass to xvfb or DEFAULT_XVFB_WHD if None
126    cwd: Current working directory.
127
128  Returns:
129    the exit code of the specified commandline, or 1 on failure.
130  """
131
132  # It might seem counterintuitive to support a --no-xvfb flag in a script
133  # whose only job is to start xvfb, but doing so allows us to consolidate
134  # the logic in the layers of buildbot scripts so that we *always* use
135  # this script by default and don't have to worry about the distinction, it
136  # can remain solely under the control of the test invocation itself.
137  # Historically, this flag turned off xvfb, but now turns off both X11 backings
138  # (xvfb/Xorg). As of crrev.com/c/5631242, Xorg became the default backing when
139  # no flags are supplied. Xorg is mostly a drop in replacement to Xvfb but has
140  # better support for dummy drivers and multi-screen testing (See:
141  # crbug.com/40257169 and http://tinyurl.com/4phsuupf). Requires Xorg binaries
142  # (package: xserver-xorg-core)
143  use_xvfb = False
144  use_xorg = True
145
146  if '--no-xvfb' in cmd:
147    use_xvfb = False
148    use_xorg = False  # Backwards compatibly turns off all X11 backings.
149    cmd.remove('--no-xvfb')
150
151  # Support forcing legacy xvfb backing.
152  if '--use-xvfb' in cmd:
153    if not use_xorg and not use_xvfb:
154      print('Conflicting flags --use-xvfb and --no-xvfb\n', file=sys.stderr)
155      return 1
156    use_xvfb = True
157    use_xorg = False
158    cmd.remove('--use-xvfb')
159
160  # Tests that run on Linux platforms with Ozone/Wayland backend require
161  # a Weston instance. However, it is also required to disable xvfb so
162  # that Weston can run in a pure headless environment.
163  use_weston = False
164  if '--use-weston' in cmd:
165    if use_xvfb or use_xorg:
166      print('Unable to use Weston with xvfb or Xorg.\n', file=sys.stderr)
167      return 1
168    use_weston = True
169    cmd.remove('--use-weston')
170
171  if sys.platform.startswith('linux') and (use_xvfb or use_xorg):
172    return _run_with_x11(cmd, env, stdoutfile, use_openbox, use_xcompmgr,
173                         use_xorg, xvfb_whd or DEFAULT_XVFB_WHD, cwd)
174  if use_weston:
175    return _run_with_weston(cmd, env, stdoutfile, cwd)
176  return test_env.run_executable(cmd, env, stdoutfile, cwd)
177
178
179def _re_search_command(regex, args, **kwargs):
180  """Runs a subprocess defined by `args` and returns a regex match for the
181  given expression on the output."""
182  return re.search(
183      regex,
184      subprocess.check_output(args,
185                              stderr=subprocess.STDOUT,
186                              text=True,
187                              **kwargs), re.IGNORECASE)
188
189
190def _make_xorg_modeline(width, height, refresh):
191  """Generates a tuple of a modeline (list of parameters) and label based off a
192  specified width, height and refresh rate.
193  See: https://www.x.org/archive/X11R7.0/doc/html/chips4.html"""
194  re_matches = _re_search_command(
195      r'Modeline "(.*)"\s+(.*)',
196      ['cvt', str(width), str(height),
197       str(refresh)],
198  )
199  modeline_label = re_matches.group(1)
200  modeline = re_matches.group(2)
201  # Split the modeline string on spaces, and filter out empty element (cvt adds
202  # double spaces between in some parts).
203  return (modeline_label, list(filter(lambda a: a != '', modeline.split(' '))))
204
205
206def _get_supported_virtual_sizes(default_whd):
207  """Returns a list of tuples (width, height) for supported monitor resolutions.
208  The list will always include the default size defined in `default_whd`"""
209  # Note: 4K resolution 3840x2160 doesn't seem to be supported and the mode
210  # silently gets dropped which makes subsequent calls to xrandr --addmode fail.
211  (default_width, default_height, _) = default_whd.split('x')
212  default_size = (int(default_width), int(default_height))
213  return sorted(
214      set([default_size, (800, 600), (1024, 768), (1920, 1080), (1600, 1200)]))
215
216
217def _make_xorg_config(default_whd):
218  """Generates an Xorg config file and returns the file path. See:
219  https://www.x.org/releases/current/doc/man/man5/xorg.conf.5.xhtml"""
220  (_, _, depth) = default_whd.split('x')
221  mode_sizes = _get_supported_virtual_sizes(default_whd)
222  modelines = []
223  mode_labels = []
224  for width, height in mode_sizes:
225    (modeline_label, modeline) = _make_xorg_modeline(width, height, 60)
226    modelines.append('Modeline "%s" %s' % (modeline_label, ' '.join(modeline)))
227    mode_labels.append('"%s"' % modeline_label)
228  config = """
229Section "Monitor"
230  Identifier "Monitor0"
231  HorizSync 5.0 - 1000.0
232  VertRefresh 5.0 - 200.0
233  %s
234EndSection
235Section "Device"
236  Identifier "Device0"
237  # Dummy driver requires package `xserver-xorg-video-dummy`.
238  Driver "dummy"
239  VideoRam 256000
240EndSection
241Section "Screen"
242  Identifier "Screen0"
243  Device "Device0"
244  Monitor "Monitor0"
245  SubSection "Display"
246    Depth %s
247    Modes %s
248  EndSubSection
249EndSection
250  """ % ('\n'.join(modelines), depth, ' '.join(mode_labels))
251  config_file = os.path.join(tempfile.gettempdir(),
252                             'xorg-%s.config' % uuid.uuid4().hex)
253  with open(config_file, 'w') as f:
254    f.write(config)
255  return config_file
256
257def _setup_xrandr(env, default_whd):
258  """Configures xrandr display(s)"""
259
260  # Calls xrandr with the provided argument array
261  def call_xrandr(args):
262    subprocess.check_call(['xrandr'] + args,
263                          env=env,
264                          stdout=subprocess.DEVNULL,
265                          stderr=subprocess.STDOUT)
266
267  (default_width, default_height, _) = default_whd.split('x')
268  default_size = (int(default_width), int(default_height))
269
270  # The minimum version of xserver-xorg-video-dummy is 0.4.0-1 which adds
271  # XRANDR support. Older versions will be missing the "DUMMY" outputs.
272  # Reliably checking the version is difficult, so check if the xrandr output
273  # includes the DUMMY displays before trying to configure them.
274  dummy_displays_available = _re_search_command('DUMMY[0-9]', ['xrandr', '-q'],
275                                                env=env)
276  if dummy_displays_available:
277    screen_sizes = _get_supported_virtual_sizes(default_whd)
278    output_names = ['DUMMY0', 'DUMMY1', 'DUMMY2', 'DUMMY3', 'DUMMY4']
279    refresh_rate = 60
280    for width, height in screen_sizes:
281      (modeline_label, _) = _make_xorg_modeline(width, height, 60)
282      for output_name in output_names:
283        call_xrandr(['--addmode', output_name, modeline_label])
284    (default_mode_label, _) = _make_xorg_modeline(*default_size, refresh_rate)
285    # Set the mode of all monitors to connect and activate them.
286    for i, name in enumerate(output_names):
287      args = ['--output', name, '--mode', default_mode_label]
288      if i > 0:
289        args += ['--right-of', output_names[i - 1]]
290      call_xrandr(args)
291
292  # Sets the primary monitor to the default size and marks the rest as disabled.
293  call_xrandr(['-s', '%dx%d' % default_size])
294  # Set the DPI to something realistic (as required by some desktops).
295  call_xrandr(['--dpi', '96'])
296
297
298def _run_with_x11(cmd, env, stdoutfile, use_openbox, use_xcompmgr, use_xorg,
299                  xvfb_whd, cwd):
300  """Runs with an X11 server. Uses Xvfb by default and Xorg when use_xorg is
301  True."""
302  openbox_proc = None
303  openbox_ready = MutableBoolean()
304
305  def set_openbox_ready(*_):
306    openbox_ready.setvalue(True)
307
308  xcompmgr_proc = None
309  x11_proc = None
310  x11_ready = MutableBoolean()
311
312  def set_x11_ready(*_):
313    x11_ready.setvalue(True)
314
315  dbus_pid = None
316  x11_binary = 'Xorg' if use_xorg else 'Xvfb'
317  xorg_config_file = _make_xorg_config(xvfb_whd) if use_xorg else None
318  try:
319    signal.signal(signal.SIGTERM, raise_x11_error)
320    signal.signal(signal.SIGINT, raise_x11_error)
321
322    # Due to race condition for display number, Xvfb/Xorg might fail to run.
323    # If it does fail, try again up to 10 times, similarly to xvfb-run.
324    for _ in range(10):
325      x11_ready.setvalue(False)
326      display = find_display()
327
328      x11_cmd = None
329      if use_xorg:
330        x11_cmd = ['Xorg', display, '-noreset', '-config', xorg_config_file]
331      else:
332        x11_cmd = [
333            'Xvfb', display, '-screen', '0', xvfb_whd, '-ac', '-nolisten',
334            'tcp', '-dpi', '96', '+extension', 'RANDR', '-maxclients', '512'
335        ]
336
337      # Sets SIGUSR1 to ignore for Xvfb/Xorg to signal current process
338      # when it is ready. Due to race condition, USR1 signal could be sent
339      # before the process resets the signal handler, we cannot rely on
340      # signal handler to change on time.
341      signal.signal(signal.SIGUSR1, signal.SIG_IGN)
342      x11_proc = subprocess.Popen(x11_cmd, stderr=subprocess.STDOUT, env=env)
343      signal.signal(signal.SIGUSR1, set_x11_ready)
344      for _ in range(30):
345        time.sleep(.1)  # gives Xvfb/Xorg time to start or fail.
346        if x11_ready.getvalue() or x11_proc.poll() is not None:
347          break  # xvfb/xorg sent ready signal, or already failed and stopped.
348
349      if x11_proc.poll() is None:
350        if x11_ready.getvalue():
351          break  # xvfb/xorg is ready
352        kill(x11_proc, x11_binary)  # still not ready, give up and retry
353
354    if x11_proc.poll() is not None:
355      raise _X11ProcessError('Failed to start after 10 tries')
356
357    env['DISPLAY'] = display
358    # Set dummy variable for scripts.
359    env['XVFB_DISPLAY'] = display
360
361    dbus_pid = launch_dbus(env)
362
363    if use_openbox:
364      # Openbox will send a SIGUSR1 signal to the current process notifying the
365      # script it has started up.
366      current_proc_id = os.getpid()
367
368      # The CMD that is passed via the --startup flag.
369      openbox_startup_cmd = 'kill --signal SIGUSR1 %s' % str(current_proc_id)
370      # Setup the signal handlers before starting the openbox instance.
371      signal.signal(signal.SIGUSR1, signal.SIG_IGN)
372      signal.signal(signal.SIGUSR1, set_openbox_ready)
373      # Retry up to 10 times due to flaky fails (crbug.com/349187865)
374      for _ in range(10):
375        openbox_ready.setvalue(False)
376        openbox_proc = subprocess.Popen(
377            ['openbox', '--sm-disable', '--startup', openbox_startup_cmd],
378            stderr=subprocess.STDOUT,
379            env=env)
380        for _ in range(30):
381          time.sleep(.1)  # gives Openbox time to start or fail.
382          if openbox_ready.getvalue() or openbox_proc.poll() is not None:
383            break  # openbox sent ready signal, or failed and stopped.
384
385        if openbox_proc.poll() is None:
386          if openbox_ready.getvalue():
387            break  # openbox is ready
388          kill(openbox_proc, 'openbox')  # still not ready, give up and retry
389          print('Openbox failed to start. Retrying.', file=sys.stderr)
390
391      if openbox_proc.poll() is not None:
392        raise _X11ProcessError('Failed to start openbox after 10 tries')
393
394    if use_xcompmgr:
395      xcompmgr_proc = subprocess.Popen('xcompmgr',
396                                       stderr=subprocess.STDOUT,
397                                       env=env)
398
399    if use_xorg:
400      _setup_xrandr(env, xvfb_whd)
401
402    return test_env.run_executable(cmd, env, stdoutfile, cwd)
403  except OSError as e:
404    print('Failed to start %s or Openbox: %s\n' % (x11_binary, str(e)),
405          file=sys.stderr)
406    return 1
407  except _X11ProcessError as e:
408    print('%s fail: %s\n' % (x11_binary, str(e)), file=sys.stderr)
409    return 1
410  finally:
411    kill(openbox_proc, 'openbox')
412    kill(xcompmgr_proc, 'xcompmgr')
413    kill(x11_proc, x11_binary)
414    if xorg_config_file is not None:
415      os.remove(xorg_config_file)
416
417    # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
418    # To ensure it exits, use SIGKILL which should be safe since all other
419    # processes that it would have been servicing have exited.
420    if dbus_pid:
421      os.kill(dbus_pid, signal.SIGKILL)
422
423
424# TODO(crbug.com/40122046): Write tests.
425def _run_with_weston(cmd, env, stdoutfile, cwd):
426  weston_proc = None
427
428  try:
429    signal.signal(signal.SIGTERM, raise_weston_error)
430    signal.signal(signal.SIGINT, raise_weston_error)
431
432    dbus_pid = launch_dbus(env)
433
434    # The bundled weston (//third_party/weston) is used by Linux Ozone Wayland
435    # CI and CQ testers and compiled by //ui/ozone/platform/wayland whenever
436    # there is a dependency on the Ozone/Wayland and use_bundled_weston is set
437    # in gn args. However, some tests do not require Wayland or do not use
438    # //ui/ozone at all, but still have --use-weston flag set by the
439    # OZONE_WAYLAND variant (see //testing/buildbot/variants.pyl). This results
440    # in failures and those tests cannot be run because of the exception that
441    # informs about missing weston binary. Thus, to overcome the issue before
442    # a better solution is found, add a check for the "weston" binary here and
443    # run tests without Wayland compositor if the weston binary is not found.
444    # TODO(https://1178788): find a better solution.
445    if not os.path.isfile('./weston'):
446      print('Weston is not available. Starting without Wayland compositor')
447      return test_env.run_executable(cmd, env, stdoutfile, cwd)
448
449    # Set $XDG_RUNTIME_DIR if it is not set.
450    _set_xdg_runtime_dir(env)
451
452    # Write options that can't be passed via CLI flags to the config file.
453    # 1) panel-position=none - disables the panel, which might interfere with
454    # the tests by blocking mouse input.
455    with open(_weston_config_file_path(), 'w') as weston_config_file:
456      weston_config_file.write('[shell]\npanel-position=none')
457
458    # Weston is compiled along with the Ozone/Wayland platform, and is
459    # fetched as data deps. Thus, run it from the current directory.
460    #
461    # Weston is used with the following flags:
462    # 1) --backend=headless-backend.so - runs Weston in a headless mode
463    # that does not require a real GPU card.
464    # 2) --idle-time=0 - disables idle timeout, which prevents Weston
465    # to enter idle state. Otherwise, Weston stops to send frame callbacks,
466    # and tests start to time out (this typically happens after 300 seconds -
467    # the default time after which Weston enters the idle state).
468    # 3) --modules=ui-controls.so,systemd-notify.so - enables support for the
469    # ui-controls Wayland protocol extension and the systemd-notify protocol.
470    # 4) --width && --height set size of a virtual display: we need to set
471    # an adequate size so that tests can have more room for managing size
472    # of windows.
473    # 5) --config=... - tells Weston to use our custom config.
474    weston_cmd = [
475        './weston', '--backend=headless-backend.so', '--idle-time=0',
476        '--modules=ui-controls.so,systemd-notify.so', '--width=1280',
477        '--height=800', '--config=' + _weston_config_file_path()
478    ]
479
480    if '--weston-use-gl' in cmd:
481      # Runs Weston using hardware acceleration instead of SwiftShader.
482      weston_cmd.append('--use-gl')
483      cmd.remove('--weston-use-gl')
484
485    if '--weston-debug-logging' in cmd:
486      cmd.remove('--weston-debug-logging')
487      env = copy.deepcopy(env)
488      env['WAYLAND_DEBUG'] = '1'
489
490    # We use the systemd-notify protocol to detect whether weston has launched
491    # successfully. We listen on a unix socket and set the NOTIFY_SOCKET
492    # environment variable to the socket's path. If we tell it to load its
493    # systemd-notify module, weston will send a 'READY=1' message to the socket
494    # once it has loaded that module.
495    # See the sd_notify(3) man page and weston's compositor/systemd-notify.c for
496    # more details.
497    with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM
498                       | socket.SOCK_NONBLOCK) as notify_socket:
499      notify_socket.bind(_weston_notify_socket_address())
500      env['NOTIFY_SOCKET'] = _weston_notify_socket_address()
501
502      weston_proc_display = None
503      for _ in range(10):
504        weston_proc = subprocess.Popen(weston_cmd,
505                                       stderr=subprocess.STDOUT,
506                                       env=env)
507
508        for _ in range(25):
509          time.sleep(0.1)  # Gives weston some time to start.
510          try:
511            if notify_socket.recv(512) == b'READY=1':
512              break
513          except BlockingIOError:
514            continue
515
516        for _ in range(25):
517          # The 'READY=1' message is sent as soon as weston loads the
518          # systemd-notify module. This happens shortly before spawning its
519          # subprocesses (e.g. desktop-shell). Wait some more to ensure they
520          # have been spawned.
521          time.sleep(0.1)
522
523          # Get the $WAYLAND_DISPLAY set by Weston and pass it to the test
524          # launcher. Please note that this env variable is local for the
525          # process. That's the reason we have to read it from Weston
526          # separately.
527          weston_proc_display = _get_display_from_weston(weston_proc.pid)
528          if weston_proc_display is not None:
529            break  # Weston could launch and we found the display.
530
531        # Also break from the outer loop.
532        if weston_proc_display is not None:
533          break
534
535    # If we couldn't find the display after 10 tries, raise an exception.
536    if weston_proc_display is None:
537      raise _WestonProcessError('Failed to start Weston.')
538
539    env.pop('NOTIFY_SOCKET')
540
541    env['WAYLAND_DISPLAY'] = weston_proc_display
542    if '--chrome-wayland-debugging' in cmd:
543      cmd.remove('--chrome-wayland-debugging')
544      env['WAYLAND_DEBUG'] = '1'
545    else:
546      env['WAYLAND_DEBUG'] = '0'
547
548    return test_env.run_executable(cmd, env, stdoutfile, cwd)
549  except OSError as e:
550    print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
551    return 1
552  except _WestonProcessError as e:
553    print('Weston fail: %s\n' % str(e), file=sys.stderr)
554    return 1
555  finally:
556    kill(weston_proc, 'weston')
557
558    if os.path.exists(_weston_notify_socket_address()):
559      os.remove(_weston_notify_socket_address())
560
561    if os.path.exists(_weston_config_file_path()):
562      os.remove(_weston_config_file_path())
563
564    # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
565    # To ensure it exits, use SIGKILL which should be safe since all other
566    # processes that it would have been servicing have exited.
567    if dbus_pid:
568      os.kill(dbus_pid, signal.SIGKILL)
569
570
571def _weston_notify_socket_address():
572  return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston-notify.sock')
573
574
575def _weston_config_file_path():
576  return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston.ini')
577
578
579def _get_display_from_weston(weston_proc_pid):
580  """Retrieves $WAYLAND_DISPLAY set by Weston.
581
582  Returns the $WAYLAND_DISPLAY variable from one of weston's subprocesses.
583
584  Weston updates this variable early in its startup in the main process, but we
585  can only read the environment variables as they were when the process was
586  created. Therefore we must use one of weston's subprocesses, which are all
587  spawned with the new value for $WAYLAND_DISPLAY. Any of them will do, as they
588  all have the same value set.
589
590  Args:
591    weston_proc_pid: The process of id of the main Weston process.
592
593  Returns:
594    the display set by Wayland, which clients can use to connect to.
595  """
596
597  # Take the parent process.
598  parent = psutil.Process(weston_proc_pid)
599  if parent is None:
600    return None  # The process is not found. Give up.
601
602  # Traverse through all the children processes and find one that has
603  # $WAYLAND_DISPLAY set.
604  children = parent.children(recursive=True)
605  for process in children:
606    weston_proc_display = process.environ().get('WAYLAND_DISPLAY')
607    # If display is set, Weston could start successfully and we can use
608    # that display for Wayland connection in Chromium.
609    if weston_proc_display is not None:
610      return weston_proc_display
611  return None
612
613
614class MutableBoolean(object):
615  """Simple mutable boolean class. Used to be mutated inside an handler."""
616
617  def __init__(self):
618    self._val = False
619
620  def setvalue(self, val):
621    assert isinstance(val, bool)
622    self._val = val
623
624  def getvalue(self):
625    return self._val
626
627
628def raise_x11_error(*_):
629  raise _X11ProcessError('Terminated')
630
631
632def raise_weston_error(*_):
633  raise _WestonProcessError('Terminated')
634
635
636def find_display():
637  """Iterates through X-lock files to find an available display number.
638
639  The lower bound follows xvfb-run standard at 99, and the upper bound
640  is set to 119.
641
642  Returns:
643    A string of a random available display number for Xvfb ':{99-119}'.
644
645  Raises:
646    _X11ProcessError: Raised when displays 99 through 119 are unavailable.
647  """
648
649  available_displays = [
650      d for d in range(99, 120)
651      if not os.path.isfile('/tmp/.X{}-lock'.format(d))
652  ]
653  if available_displays:
654    return ':{}'.format(random.choice(available_displays))
655  raise _X11ProcessError('Failed to find display number')
656
657
658def _set_xdg_runtime_dir(env):
659  """Sets the $XDG_RUNTIME_DIR variable if it hasn't been set before."""
660  runtime_dir = env.get('XDG_RUNTIME_DIR')
661  if not runtime_dir:
662    runtime_dir = '/tmp/xdg-tmp-dir/'
663    if not os.path.exists(runtime_dir):
664      os.makedirs(runtime_dir, 0o700)
665    env['XDG_RUNTIME_DIR'] = runtime_dir
666
667
668def main():
669  usage = ('[command [--no-xvfb or --use-xvfb or --use-weston] args...]\n'
670           '\t --no-xvfb\t\tTurns off all X11 backings (Xvfb and Xorg).\n'
671           '\t --use-xvfb\t\tForces legacy Xvfb backing instead of Xorg.\n'
672           '\t --use-weston\t\tEnable Wayland server.')
673  # TODO(crbug.com/326283384): Argparse-ify this.
674  if len(sys.argv) < 2:
675    print(usage + '\n', file=sys.stderr)
676    return 2
677
678  # If the user still thinks the first argument is the execution directory then
679  # print a friendly error message and quit.
680  if os.path.isdir(sys.argv[1]):
681    print('Invalid command: \"%s\" is a directory\n' % sys.argv[1],
682          file=sys.stderr)
683    print(usage + '\n', file=sys.stderr)
684    return 3
685
686  return run_executable(sys.argv[1:], os.environ.copy())
687
688
689if __name__ == '__main__':
690  sys.exit(main())
691