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."""
47 thread = threading.Thread(target=proc.wait)
50 thread.join(timeout_in_seconds)
52 print('%s running after SIGTERM, trying SIGKILL.\n' % name, file=sys.stderr)
55 thread.join(timeout_in_seconds)
57 print('%s running after SIGTERM and SIGKILL; good luck!\n' % name,
61 def launch_dbus(env): # pylint: disable=inconsistent-return-statements
62 """Starts a DBus session.
64 Works around a bug in GLib where it performs operations which aren't
65 async-signal-safe (in particular, memory allocations) between fork and exec
66 when it spawns subprocesses. This causes threads inside Chrome's browser and
67 utility processes to get stuck, and this harness to hang waiting for those
68 processes, which will never terminate. This doesn't happen on users'
69 machines, because they have an active desktop session and the
70 DBUS_SESSION_BUS_ADDRESS environment variable set, but it can happen on
71 headless environments. This is fixed by glib commit [1], but this workaround
72 will be necessary until the fix rolls into Chromium's CI.
74 [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12
76 Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and
77 DBUS_SESSION_BUS_PID set.
79 Returns the pid of the dbus-daemon if started, or None otherwise.
81 if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
84 dbus_output = subprocess.check_output(
85 ['dbus-launch'], env=env).decode('utf-8').split('\n')
86 for line in dbus_output:
87 m = re.match(r'([^=]+)\=(.+)', line)
89 env[m.group(1)] = m.group(2)
90 return int(env['DBUS_SESSION_BUS_PID'])
91 except (subprocess.CalledProcessError, OSError, KeyError, ValueError) as e:
92 print('Exception while running dbus_launch: %s' % e)
95 # TODO(crbug.com/949194): Encourage setting flags to False.
97 cmd, env, stdoutfile=None, use_openbox=True, use_xcompmgr=True,
98 xvfb_whd=None, cwd=None):
99 """Runs an executable within Weston or Xvfb on Linux or normally on other
102 The method sets SIGUSR1 handler for Xvfb to return SIGUSR1
103 when it is ready for connections.
104 https://www.x.org/archive/X11R7.5/doc/man/man1/Xserver.1.html under Signals.
107 cmd: Command to be executed.
108 env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is
109 used. "WAYLAND_DISPLAY" will be set if Weston is used.
110 stdoutfile: If provided, symbolization via script is disabled and stdout
111 is written to this file as well as to stdout.
112 use_openbox: A flag to use openbox process.
113 Some ChromeOS tests need a window manager.
114 use_xcompmgr: A flag to use xcompmgr process.
115 Some tests need a compositing wm to make use of transparent visuals.
116 xvfb_whd: WxHxD to pass to xvfb or DEFAULT_XVFB_WHD if None
117 cwd: Current working directory.
120 the exit code of the specified commandline, or 1 on failure.
123 # It might seem counterintuitive to support a --no-xvfb flag in a script
124 # whose only job is to start xvfb, but doing so allows us to consolidate
125 # the logic in the layers of buildbot scripts so that we *always* use
126 # xvfb by default and don't have to worry about the distinction, it
127 # can remain solely under the control of the test invocation itself.
129 if '--no-xvfb' in cmd:
131 cmd.remove('--no-xvfb')
133 # Tests that run on Linux platforms with Ozone/Wayland backend require
134 # a Weston instance. However, it is also required to disable xvfb so
135 # that Weston can run in a pure headless environment.
137 if '--use-weston' in cmd:
139 print('Unable to use Weston with xvfb.\n', file=sys.stderr)
142 cmd.remove('--use-weston')
144 if sys.platform.startswith('linux') and use_xvfb:
145 return _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr,
146 xvfb_whd or DEFAULT_XVFB_WHD, cwd)
148 return _run_with_weston(cmd, env, stdoutfile, cwd)
149 return test_env.run_executable(cmd, env, stdoutfile, cwd)
152 def _run_with_xvfb(cmd, env, stdoutfile, use_openbox,
153 use_xcompmgr, xvfb_whd, cwd):
155 openbox_ready = MutableBoolean()
156 def set_openbox_ready(*_):
157 openbox_ready.setvalue(True)
161 xvfb_ready = MutableBoolean()
162 def set_xvfb_ready(*_):
163 xvfb_ready.setvalue(True)
167 signal.signal(signal.SIGTERM, raise_xvfb_error)
168 signal.signal(signal.SIGINT, raise_xvfb_error)
170 # Before [1], the maximum number of X11 clients was 256. After, the default
171 # limit is 256 with a configurable maximum of 512. On systems with a large
172 # number of CPUs, the old limit of 256 may be hit for certain test suites
173 # [2] [3], so we set the limit to 512 when possible. This flag is not
174 # available on Ubuntu 16.04 or 18.04, so a feature check is required. Xvfb
175 # does not have a '-version' option, so checking the '-help' output is
178 # [1] d206c240c0b85c4da44f073d6e9a692afb6b96d2
179 # [2] https://crbug.com/1187948
180 # [3] https://crbug.com/1120107
181 xvfb_help = subprocess.check_output(
182 ['Xvfb', '-help'], stderr=subprocess.STDOUT).decode('utf8')
184 # Due to race condition for display number, Xvfb might fail to run.
185 # If it does fail, try again up to 10 times, similarly to xvfb-run.
187 xvfb_ready.setvalue(False)
188 display = find_display()
190 xvfb_cmd = ['Xvfb', display, '-screen', '0', xvfb_whd, '-ac',
191 '-nolisten', 'tcp', '-dpi', '96', '+extension', 'RANDR']
192 if '-maxclients' in xvfb_help:
193 xvfb_cmd += ['-maxclients', '512']
195 # Sets SIGUSR1 to ignore for Xvfb to signal current process
196 # when it is ready. Due to race condition, USR1 signal could be sent
197 # before the process resets the signal handler, we cannot rely on
198 # signal handler to change on time.
199 signal.signal(signal.SIGUSR1, signal.SIG_IGN)
200 xvfb_proc = subprocess.Popen(xvfb_cmd, stderr=subprocess.STDOUT, env=env)
201 signal.signal(signal.SIGUSR1, set_xvfb_ready)
203 time.sleep(.1) # gives Xvfb time to start or fail.
204 if xvfb_ready.getvalue() or xvfb_proc.poll() is not None:
205 break # xvfb sent ready signal, or already failed and stopped.
207 if xvfb_proc.poll() is None:
208 break # xvfb is running, can proceed.
209 if xvfb_proc.poll() is not None:
210 raise _XvfbProcessError('Failed to start after 10 tries')
212 env['DISPLAY'] = display
213 # Set dummy variable for scripts.
214 env['XVFB_DISPLAY'] = display
216 dbus_pid = launch_dbus(env)
219 # Openbox will send a SIGUSR1 signal to the current process notifying the
220 # script it has started up.
221 current_proc_id = os.getpid()
223 # The CMD that is passed via the --startup flag.
224 openbox_startup_cmd = 'kill --signal SIGUSR1 %s' % str(current_proc_id)
225 # Setup the signal handlers before starting the openbox instance.
226 signal.signal(signal.SIGUSR1, signal.SIG_IGN)
227 signal.signal(signal.SIGUSR1, set_openbox_ready)
228 openbox_proc = subprocess.Popen(
229 ['openbox', '--sm-disable', '--startup',
230 openbox_startup_cmd], stderr=subprocess.STDOUT, env=env)
233 time.sleep(.1) # gives Openbox time to start or fail.
234 if openbox_ready.getvalue() or openbox_proc.poll() is not None:
235 break # openbox sent ready signal, or failed and stopped.
237 if openbox_proc.poll() is not None:
238 raise _XvfbProcessError('Failed to start OpenBox.')
241 xcompmgr_proc = subprocess.Popen(
242 'xcompmgr', stderr=subprocess.STDOUT, env=env)
244 return test_env.run_executable(cmd, env, stdoutfile, cwd)
246 print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr)
248 except _XvfbProcessError as e:
249 print('Xvfb fail: %s\n' % str(e), file=sys.stderr)
252 kill(openbox_proc, 'openbox')
253 kill(xcompmgr_proc, 'xcompmgr')
254 kill(xvfb_proc, 'Xvfb')
256 # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
257 # To ensure it exits, use SIGKILL which should be safe since all other
258 # processes that it would have been servicing have exited.
260 os.kill(dbus_pid, signal.SIGKILL)
263 # TODO(https://crbug.com/1060466): Write tests.
264 def _run_with_weston(cmd, env, stdoutfile, cwd):
268 signal.signal(signal.SIGTERM, raise_weston_error)
269 signal.signal(signal.SIGINT, raise_weston_error)
271 dbus_pid = launch_dbus(env)
273 # The bundled weston (//third_party/weston) is used by Linux Ozone Wayland
274 # CI and CQ testers and compiled by //ui/ozone/platform/wayland whenever
275 # there is a dependency on the Ozone/Wayland and use_bundled_weston is set
276 # in gn args. However, some tests do not require Wayland or do not use
277 # //ui/ozone at all, but still have --use-weston flag set by the
278 # OZONE_WAYLAND variant (see //testing/buildbot/variants.pyl). This results
279 # in failures and those tests cannot be run because of the exception that
280 # informs about missing weston binary. Thus, to overcome the issue before
281 # a better solution is found, add a check for the "weston" binary here and
282 # run tests without Wayland compositor if the weston binary is not found.
283 # TODO(https://1178788): find a better solution.
284 if not os.path.isfile("./weston"):
285 print('Weston is not available. Starting without Wayland compositor')
286 return test_env.run_executable(cmd, env, stdoutfile, cwd)
288 # Set $XDG_RUNTIME_DIR if it is not set.
289 _set_xdg_runtime_dir(env)
291 # Weston is compiled along with the Ozone/Wayland platform, and is
292 # fetched as data deps. Thus, run it from the current directory.
294 # Weston is used with the following flags:
295 # 1) --backend=headless-backend.so - runs Weston in a headless mode
296 # that does not require a real GPU card.
297 # 2) --idle-time=0 - disables idle timeout, which prevents Weston
298 # to enter idle state. Otherwise, Weston stops to send frame callbacks,
299 # and tests start to time out (this typically happens after 300 seconds -
300 # the default time after which Weston enters the idle state).
301 # 3) --modules=test-plugin.so,systemd-notify.so - enables support for the
302 # weston-test Wayland protocol extension and the systemd-notify protocol.
303 # 4) --width && --height set size of a virtual display: we need to set
304 # an adequate size so that tests can have more room for managing size
306 weston_cmd = ['./weston', '--backend=headless-backend.so', '--idle-time=0',
307 '--modules=test-plugin.so,systemd-notify.so', '--width=1024',
310 if '--weston-use-gl' in cmd:
311 # Runs Weston using hardware acceleration instead of SwiftShader.
312 weston_cmd.append('--use-gl')
313 cmd.remove('--weston-use-gl')
315 if '--weston-debug-logging' in cmd:
316 cmd.remove('--weston-debug-logging')
317 env = copy.deepcopy(env)
318 env['WAYLAND_DEBUG'] = '1'
320 # We use the systemd-notify protocol to detect whether weston has launched
321 # successfully. We listen on a unix socket and set the NOTIFY_SOCKET
322 # environment variable to the socket's path. If we tell it to load its
323 # systemd-notify module, weston will send a 'READY=1' message to the socket
324 # once it has loaded that module.
325 # See the sd_notify(3) man page and weston's compositor/systemd-notify.c for
327 with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM
328 | socket.SOCK_NONBLOCK) as notify_socket:
329 notify_socket.bind(_weston_notify_socket_address())
330 env['NOTIFY_SOCKET'] = _weston_notify_socket_address()
332 weston_proc_display = None
334 weston_proc = subprocess.Popen(
336 stderr=subprocess.STDOUT, env=env)
339 time.sleep(0.1) # Gives weston some time to start.
341 if notify_socket.recv(512) == b'READY=1':
343 except BlockingIOError:
347 # The 'READY=1' message is sent as soon as weston loads the
348 # systemd-notify module. This happens shortly before spawning its
349 # subprocesses (e.g. desktop-shell). Wait some more to ensure they
353 # Get the $WAYLAND_DISPLAY set by Weston and pass it to the test
354 # launcher. Please note that this env variable is local for the
355 # process. That's the reason we have to read it from Weston
357 weston_proc_display = _get_display_from_weston(weston_proc.pid)
358 if weston_proc_display is not None:
359 break # Weston could launch and we found the display.
361 # Also break from the outer loop.
362 if weston_proc_display is not None:
365 # If we couldn't find the display after 10 tries, raise an exception.
366 if weston_proc_display is None:
367 raise _WestonProcessError('Failed to start Weston.')
369 env.pop('NOTIFY_SOCKET')
371 env['WAYLAND_DISPLAY'] = weston_proc_display
372 if '--chrome-wayland-debugging' in cmd:
373 cmd.remove('--chrome-wayland-debugging')
374 env['WAYLAND_DEBUG'] = '1'
376 env['WAYLAND_DEBUG'] = '0'
378 return test_env.run_executable(cmd, env, stdoutfile, cwd)
380 print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
382 except _WestonProcessError as e:
383 print('Weston fail: %s\n' % str(e), file=sys.stderr)
386 kill(weston_proc, 'weston')
388 if os.path.exists(_weston_notify_socket_address()):
389 os.remove(_weston_notify_socket_address())
391 # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
392 # To ensure it exits, use SIGKILL which should be safe since all other
393 # processes that it would have been servicing have exited.
395 os.kill(dbus_pid, signal.SIGKILL)
397 def _weston_notify_socket_address():
398 return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston-notify.sock')
400 def _get_display_from_weston(weston_proc_pid):
401 """Retrieves $WAYLAND_DISPLAY set by Weston.
403 Returns the $WAYLAND_DISPLAY variable from one of weston's subprocesses.
405 Weston updates this variable early in its startup in the main process, but we
406 can only read the environment variables as they were when the process was
407 created. Therefore we must use one of weston's subprocesses, which are all
408 spawned with the new value for $WAYLAND_DISPLAY. Any of them will do, as they
409 all have the same value set.
412 weston_proc_pid: The process of id of the main Weston process.
415 the display set by Wayland, which clients can use to connect to.
418 # Take the parent process.
419 parent = psutil.Process(weston_proc_pid)
421 return None # The process is not found. Give up.
423 # Traverse through all the children processes and find one that has
424 # $WAYLAND_DISPLAY set.
425 children = parent.children(recursive=True)
426 for process in children:
427 weston_proc_display = process.environ().get('WAYLAND_DISPLAY')
428 # If display is set, Weston could start successfully and we can use
429 # that display for Wayland connection in Chromium.
430 if weston_proc_display is not None:
431 return weston_proc_display
435 class MutableBoolean(object):
436 """Simple mutable boolean class. Used to be mutated inside an handler."""
441 def setvalue(self, val):
442 assert isinstance(val, bool)
449 def raise_xvfb_error(*_):
450 raise _XvfbProcessError('Terminated')
453 def raise_weston_error(*_):
454 raise _WestonProcessError('Terminated')
458 """Iterates through X-lock files to find an available display number.
460 The lower bound follows xvfb-run standard at 99, and the upper bound
464 A string of a random available display number for Xvfb ':{99-119}'.
467 _XvfbProcessError: Raised when displays 99 through 119 are unavailable.
470 available_displays = [
471 d for d in range(99, 120)
472 if not os.path.isfile('/tmp/.X{}-lock'.format(d))
474 if available_displays:
475 return ':{}'.format(random.choice(available_displays))
476 raise _XvfbProcessError('Failed to find display number')
479 def _set_xdg_runtime_dir(env):
480 """Sets the $XDG_RUNTIME_DIR variable if it hasn't been set before."""
481 runtime_dir = env.get('XDG_RUNTIME_DIR')
483 runtime_dir = '/tmp/xdg-tmp-dir/'
484 if not os.path.exists(runtime_dir):
485 os.makedirs(runtime_dir, 0o700)
486 env['XDG_RUNTIME_DIR'] = runtime_dir
490 usage = 'Usage: xvfb.py [command [--no-xvfb or --use-weston] args...]'
491 if len(sys.argv) < 2:
492 print(usage + '\n', file=sys.stderr)
495 # If the user still thinks the first argument is the execution directory then
496 # print a friendly error message and quit.
497 if os.path.isdir(sys.argv[1]):
498 print('Invalid command: \"%s\" is a directory\n' % sys.argv[1],
500 print(usage + '\n', file=sys.stderr)
503 return run_executable(sys.argv[1:], os.environ.copy())
506 if __name__ == '__main__':