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.
6 """Runs tests with Xvfb and Openbox or Weston on Linux and normally on other
9 from __future__ import print_function
28 DEFAULT_XVFB_WHD = '1280x800x24'
30 # pylint: disable=useless-object-inheritance
33 class _XvfbProcessError(Exception):
34 """Exception raised when Xvfb cannot start."""
37 class _WestonProcessError(Exception):
38 """Exception raised when Weston cannot start."""
41 def kill(proc, name, timeout_in_seconds=10):
42 """Tries to kill |proc| gracefully with a timeout for each signal."""
46 thread = threading.Thread(target=proc.wait)
51 thread.join(timeout_in_seconds)
53 print('%s running after SIGTERM, trying SIGKILL.\n' % name,
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)
61 thread.join(timeout_in_seconds)
63 print('%s running after SIGTERM and SIGKILL; good luck!\n' % name,
67 def launch_dbus(env): # pylint: disable=inconsistent-return-statements
68 """Starts a DBus session.
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.
80 [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12
82 Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and
83 DBUS_SESSION_BUS_PID set.
85 Returns the pid of the dbus-daemon if started, or None otherwise.
87 if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
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)
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)
101 # TODO(crbug.com/949194): Encourage setting flags to False.
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
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.
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.
126 the exit code of the specified commandline, or 1 on failure.
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.
135 if '--no-xvfb' in cmd:
137 cmd.remove('--no-xvfb')
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.
143 if '--use-weston' in cmd:
145 print('Unable to use Weston with xvfb.\n', file=sys.stderr)
148 cmd.remove('--use-weston')
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)
154 return _run_with_weston(cmd, env, stdoutfile, cwd)
155 return test_env.run_executable(cmd, env, stdoutfile, cwd)
158 def _run_with_xvfb(cmd, env, stdoutfile, use_openbox,
159 use_xcompmgr, xvfb_whd, cwd):
161 openbox_ready = MutableBoolean()
162 def set_openbox_ready(*_):
163 openbox_ready.setvalue(True)
167 xvfb_ready = MutableBoolean()
168 def set_xvfb_ready(*_):
169 xvfb_ready.setvalue(True)
173 signal.signal(signal.SIGTERM, raise_xvfb_error)
174 signal.signal(signal.SIGINT, raise_xvfb_error)
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
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')
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.
193 xvfb_ready.setvalue(False)
194 display = find_display()
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']
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)
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.
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
218 if xvfb_proc.poll() is not None:
219 raise _XvfbProcessError('Failed to start after 10 tries')
221 env['DISPLAY'] = display
222 # Set dummy variable for scripts.
223 env['XVFB_DISPLAY'] = display
225 dbus_pid = launch_dbus(env)
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()
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)
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.
246 if openbox_proc.poll() is not None or not openbox_ready.getvalue():
247 raise _XvfbProcessError('Failed to start OpenBox.')
250 xcompmgr_proc = subprocess.Popen(
251 'xcompmgr', stderr=subprocess.STDOUT, env=env)
253 return test_env.run_executable(cmd, env, stdoutfile, cwd)
255 print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr)
257 except _XvfbProcessError as e:
258 print('Xvfb fail: %s\n' % str(e), file=sys.stderr)
261 kill(openbox_proc, 'openbox')
262 kill(xcompmgr_proc, 'xcompmgr')
263 kill(xvfb_proc, 'Xvfb')
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.
269 os.kill(dbus_pid, signal.SIGKILL)
272 # TODO(https://crbug.com/1060466): Write tests.
273 def _run_with_weston(cmd, env, stdoutfile, cwd):
277 signal.signal(signal.SIGTERM, raise_weston_error)
278 signal.signal(signal.SIGINT, raise_weston_error)
280 dbus_pid = launch_dbus(env)
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)
297 # Set $XDG_RUNTIME_DIR if it is not set.
298 _set_xdg_runtime_dir(env)
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')
306 # Weston is compiled along with the Ozone/Wayland platform, and is
307 # fetched as data deps. Thus, run it from the current directory.
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
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()]
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')
331 if '--weston-debug-logging' in cmd:
332 cmd.remove('--weston-debug-logging')
333 env = copy.deepcopy(env)
334 env['WAYLAND_DEBUG'] = '1'
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
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()
348 weston_proc_display = None
350 weston_proc = subprocess.Popen(
352 stderr=subprocess.STDOUT, env=env)
355 time.sleep(0.1) # Gives weston some time to start.
357 if notify_socket.recv(512) == b'READY=1':
359 except BlockingIOError:
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
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
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.
377 # Also break from the outer loop.
378 if weston_proc_display is not None:
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.')
385 env.pop('NOTIFY_SOCKET')
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'
392 env['WAYLAND_DEBUG'] = '0'
394 return test_env.run_executable(cmd, env, stdoutfile, cwd)
396 print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
398 except _WestonProcessError as e:
399 print('Weston fail: %s\n' % str(e), file=sys.stderr)
402 kill(weston_proc, 'weston')
404 if os.path.exists(_weston_notify_socket_address()):
405 os.remove(_weston_notify_socket_address())
407 if os.path.exists(_weston_config_file_path()):
408 os.remove(_weston_config_file_path())
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.
414 os.kill(dbus_pid, signal.SIGKILL)
416 def _weston_notify_socket_address():
417 return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston-notify.sock')
419 def _weston_config_file_path():
420 return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston.ini')
422 def _get_display_from_weston(weston_proc_pid):
423 """Retrieves $WAYLAND_DISPLAY set by Weston.
425 Returns the $WAYLAND_DISPLAY variable from one of weston's subprocesses.
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.
434 weston_proc_pid: The process of id of the main Weston process.
437 the display set by Wayland, which clients can use to connect to.
440 # Take the parent process.
441 parent = psutil.Process(weston_proc_pid)
443 return None # The process is not found. Give up.
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
457 class MutableBoolean(object):
458 """Simple mutable boolean class. Used to be mutated inside an handler."""
463 def setvalue(self, val):
464 assert isinstance(val, bool)
471 def raise_xvfb_error(*_):
472 raise _XvfbProcessError('Terminated')
475 def raise_weston_error(*_):
476 raise _WestonProcessError('Terminated')
480 """Iterates through X-lock files to find an available display number.
482 The lower bound follows xvfb-run standard at 99, and the upper bound
486 A string of a random available display number for Xvfb ':{99-119}'.
489 _XvfbProcessError: Raised when displays 99 through 119 are unavailable.
492 available_displays = [
493 d for d in range(99, 120)
494 if not os.path.isfile('/tmp/.X{}-lock'.format(d))
496 if available_displays:
497 return ':{}'.format(random.choice(available_displays))
498 raise _XvfbProcessError('Failed to find display number')
501 def _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')
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
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)
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],
522 print(usage + '\n', file=sys.stderr)
525 return run_executable(sys.argv[1:], os.environ.copy())
528 if __name__ == '__main__':