xvfb.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. #!/usr/bin/env vpython3
  2. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
  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 and Openbox or Weston on Linux and normally on other
  6. platforms."""
  7. from __future__ import print_function
  8. import copy
  9. import os
  10. import os.path
  11. import random
  12. import re
  13. import signal
  14. import subprocess
  15. import sys
  16. import threading
  17. import time
  18. import psutil
  19. import test_env
  20. # pylint: disable=useless-object-inheritance
  21. class _XvfbProcessError(Exception):
  22. """Exception raised when Xvfb cannot start."""
  23. class _WestonProcessError(Exception):
  24. """Exception raised when Weston cannot start."""
  25. def kill(proc, name, timeout_in_seconds=10):
  26. """Tries to kill |proc| gracefully with a timeout for each signal."""
  27. if not proc:
  28. return
  29. proc.terminate()
  30. thread = threading.Thread(target=proc.wait)
  31. thread.start()
  32. thread.join(timeout_in_seconds)
  33. if thread.is_alive():
  34. print('%s running after SIGTERM, trying SIGKILL.\n' % name, file=sys.stderr)
  35. proc.kill()
  36. thread.join(timeout_in_seconds)
  37. if thread.is_alive():
  38. print('%s running after SIGTERM and SIGKILL; good luck!\n' % name,
  39. file=sys.stderr)
  40. def launch_dbus(env): # pylint: disable=inconsistent-return-statements
  41. """Starts a DBus session.
  42. Works around a bug in GLib where it performs operations which aren't
  43. async-signal-safe (in particular, memory allocations) between fork and exec
  44. when it spawns subprocesses. This causes threads inside Chrome's browser and
  45. utility processes to get stuck, and this harness to hang waiting for those
  46. processes, which will never terminate. This doesn't happen on users'
  47. machines, because they have an active desktop session and the
  48. DBUS_SESSION_BUS_ADDRESS environment variable set, but it can happen on
  49. headless environments. This is fixed by glib commit [1], but this workaround
  50. will be necessary until the fix rolls into Chromium's CI.
  51. [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12
  52. Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and
  53. DBUS_SESSION_BUS_PID set.
  54. Returns the pid of the dbus-daemon if started, or None otherwise.
  55. """
  56. if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
  57. return
  58. try:
  59. dbus_output = subprocess.check_output(
  60. ['dbus-launch'], env=env).decode('utf-8').split('\n')
  61. for line in dbus_output:
  62. m = re.match(r'([^=]+)\=(.+)', line)
  63. if m:
  64. env[m.group(1)] = m.group(2)
  65. return int(env['DBUS_SESSION_BUS_PID'])
  66. except (subprocess.CalledProcessError, OSError, KeyError, ValueError) as e:
  67. print('Exception while running dbus_launch: %s' % e)
  68. # TODO(crbug.com/949194): Encourage setting flags to False.
  69. def run_executable(
  70. cmd, env, stdoutfile=None, use_openbox=True, use_xcompmgr=True):
  71. """Runs an executable within Weston or Xvfb on Linux or normally on other
  72. platforms.
  73. The method sets SIGUSR1 handler for Xvfb to return SIGUSR1
  74. when it is ready for connections.
  75. https://www.x.org/archive/X11R7.5/doc/man/man1/Xserver.1.html under Signals.
  76. Args:
  77. cmd: Command to be executed.
  78. env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is
  79. used. "WAYLAND_DISPLAY" will be set if Weston is used.
  80. stdoutfile: If provided, symbolization via script is disabled and stdout
  81. is written to this file as well as to stdout.
  82. use_openbox: A flag to use openbox process.
  83. Some ChromeOS tests need a window manager.
  84. use_xcompmgr: A flag to use xcompmgr process.
  85. Some tests need a compositing wm to make use of transparent visuals.
  86. Returns:
  87. the exit code of the specified commandline, or 1 on failure.
  88. """
  89. # It might seem counterintuitive to support a --no-xvfb flag in a script
  90. # whose only job is to start xvfb, but doing so allows us to consolidate
  91. # the logic in the layers of buildbot scripts so that we *always* use
  92. # xvfb by default and don't have to worry about the distinction, it
  93. # can remain solely under the control of the test invocation itself.
  94. use_xvfb = True
  95. if '--no-xvfb' in cmd:
  96. use_xvfb = False
  97. cmd.remove('--no-xvfb')
  98. # Tests that run on Linux platforms with Ozone/Wayland backend require
  99. # a Weston instance. However, it is also required to disable xvfb so
  100. # that Weston can run in a pure headless environment.
  101. use_weston = False
  102. if '--use-weston' in cmd:
  103. if use_xvfb:
  104. print('Unable to use Weston with xvfb.\n', file=sys.stderr)
  105. return 1
  106. use_weston = True
  107. cmd.remove('--use-weston')
  108. if sys.platform.startswith('linux') and use_xvfb:
  109. return _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr)
  110. if use_weston:
  111. return _run_with_weston(cmd, env, stdoutfile)
  112. return test_env.run_executable(cmd, env, stdoutfile)
  113. def _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr):
  114. openbox_proc = None
  115. openbox_ready = MutableBoolean()
  116. def set_openbox_ready(*_):
  117. openbox_ready.setvalue(True)
  118. xcompmgr_proc = None
  119. xvfb_proc = None
  120. xvfb_ready = MutableBoolean()
  121. def set_xvfb_ready(*_):
  122. xvfb_ready.setvalue(True)
  123. dbus_pid = None
  124. try:
  125. signal.signal(signal.SIGTERM, raise_xvfb_error)
  126. signal.signal(signal.SIGINT, raise_xvfb_error)
  127. # Before [1], the maximum number of X11 clients was 256. After, the default
  128. # limit is 256 with a configurable maximum of 512. On systems with a large
  129. # number of CPUs, the old limit of 256 may be hit for certain test suites
  130. # [2] [3], so we set the limit to 512 when possible. This flag is not
  131. # available on Ubuntu 16.04 or 18.04, so a feature check is required. Xvfb
  132. # does not have a '-version' option, so checking the '-help' output is
  133. # required.
  134. #
  135. # [1] d206c240c0b85c4da44f073d6e9a692afb6b96d2
  136. # [2] https://crbug.com/1187948
  137. # [3] https://crbug.com/1120107
  138. xvfb_help = subprocess.check_output(
  139. ['Xvfb', '-help'], stderr=subprocess.STDOUT).decode('utf8')
  140. # Due to race condition for display number, Xvfb might fail to run.
  141. # If it does fail, try again up to 10 times, similarly to xvfb-run.
  142. for _ in range(10):
  143. xvfb_ready.setvalue(False)
  144. display = find_display()
  145. xvfb_cmd = ['Xvfb', display, '-screen', '0', '1280x800x24', '-ac',
  146. '-nolisten', 'tcp', '-dpi', '96', '+extension', 'RANDR']
  147. if '-maxclients' in xvfb_help:
  148. xvfb_cmd += ['-maxclients', '512']
  149. # Sets SIGUSR1 to ignore for Xvfb to signal current process
  150. # when it is ready. Due to race condition, USR1 signal could be sent
  151. # before the process resets the signal handler, we cannot rely on
  152. # signal handler to change on time.
  153. signal.signal(signal.SIGUSR1, signal.SIG_IGN)
  154. xvfb_proc = subprocess.Popen(xvfb_cmd, stderr=subprocess.STDOUT, env=env)
  155. signal.signal(signal.SIGUSR1, set_xvfb_ready)
  156. for _ in range(10):
  157. time.sleep(.1) # gives Xvfb time to start or fail.
  158. if xvfb_ready.getvalue() or xvfb_proc.poll() is not None:
  159. break # xvfb sent ready signal, or already failed and stopped.
  160. if xvfb_proc.poll() is None:
  161. break # xvfb is running, can proceed.
  162. if xvfb_proc.poll() is not None:
  163. raise _XvfbProcessError('Failed to start after 10 tries')
  164. env['DISPLAY'] = display
  165. # Set dummy variable for scripts.
  166. env['XVFB_DISPLAY'] = display
  167. dbus_pid = launch_dbus(env)
  168. if use_openbox:
  169. # Openbox will send a SIGUSR1 signal to the current process notifying the
  170. # script it has started up.
  171. current_proc_id = os.getpid()
  172. # The CMD that is passed via the --startup flag.
  173. openbox_startup_cmd = 'kill --signal SIGUSR1 %s' % str(current_proc_id)
  174. # Setup the signal handlers before starting the openbox instance.
  175. signal.signal(signal.SIGUSR1, signal.SIG_IGN)
  176. signal.signal(signal.SIGUSR1, set_openbox_ready)
  177. openbox_proc = subprocess.Popen(
  178. ['openbox', '--sm-disable', '--startup',
  179. openbox_startup_cmd], stderr=subprocess.STDOUT, env=env)
  180. for _ in range(10):
  181. time.sleep(.1) # gives Openbox time to start or fail.
  182. if openbox_ready.getvalue() or openbox_proc.poll() is not None:
  183. break # openbox sent ready signal, or failed and stopped.
  184. if openbox_proc.poll() is not None:
  185. raise _XvfbProcessError('Failed to start OpenBox.')
  186. if use_xcompmgr:
  187. xcompmgr_proc = subprocess.Popen(
  188. 'xcompmgr', stderr=subprocess.STDOUT, env=env)
  189. return test_env.run_executable(cmd, env, stdoutfile)
  190. except OSError as e:
  191. print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr)
  192. return 1
  193. except _XvfbProcessError as e:
  194. print('Xvfb fail: %s\n' % str(e), file=sys.stderr)
  195. return 1
  196. finally:
  197. kill(openbox_proc, 'openbox')
  198. kill(xcompmgr_proc, 'xcompmgr')
  199. kill(xvfb_proc, 'Xvfb')
  200. # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
  201. # To ensure it exits, use SIGKILL which should be safe since all other
  202. # processes that it would have been servicing have exited.
  203. if dbus_pid:
  204. os.kill(dbus_pid, signal.SIGKILL)
  205. # TODO(https://crbug.com/1060466): Write tests.
  206. def _run_with_weston(cmd, env, stdoutfile):
  207. weston_proc = None
  208. try:
  209. signal.signal(signal.SIGTERM, raise_weston_error)
  210. signal.signal(signal.SIGINT, raise_weston_error)
  211. dbus_pid = launch_dbus(env)
  212. # The bundled weston (//third_party/weston) is used by Linux Ozone Wayland
  213. # CI and CQ testers and compiled by //ui/ozone/platform/wayland whenever
  214. # there is a dependency on the Ozone/Wayland and use_bundled_weston is set
  215. # in gn args. However, some tests do not require Wayland or do not use
  216. # //ui/ozone at all, but still have --use-weston flag set by the
  217. # OZONE_WAYLAND variant (see //testing/buildbot/variants.pyl). This results
  218. # in failures and those tests cannot be run because of the exception that
  219. # informs about missing weston binary. Thus, to overcome the issue before
  220. # a better solution is found, add a check for the "weston" binary here and
  221. # run tests without Wayland compositor if the weston binary is not found.
  222. # TODO(https://1178788): find a better solution.
  223. if not os.path.isfile("./weston"):
  224. print('Weston is not available. Starting without Wayland compositor')
  225. return test_env.run_executable(cmd, env, stdoutfile)
  226. # Set $XDG_RUNTIME_DIR if it is not set.
  227. _set_xdg_runtime_dir(env)
  228. # Weston is compiled along with the Ozone/Wayland platform, and is
  229. # fetched as data deps. Thus, run it from the current directory.
  230. #
  231. # Weston is used with the following flags:
  232. # 1) --backend=headless-backend.so - runs Weston in a headless mode
  233. # that does not require a real GPU card.
  234. # 2) --idle-time=0 - disables idle timeout, which prevents Weston
  235. # to enter idle state. Otherwise, Weston stops to send frame callbacks,
  236. # and tests start to time out (this typically happens after 300 seconds -
  237. # the default time after which Weston enters the idle state).
  238. # 3) --width && --height set size of a virtual display: we need to set
  239. # an adequate size so that tests can have more room for managing size
  240. # of windows.
  241. # 4) --use-gl - Runs Weston using hardware acceleration instead of
  242. # SwiftShader.
  243. weston_cmd = ['./weston', '--backend=headless-backend.so', '--idle-time=0',
  244. '--width=1024', '--height=768', '--modules=test-plugin.so']
  245. if '--weston-use-gl' in cmd:
  246. weston_cmd.append('--use-gl')
  247. cmd.remove('--weston-use-gl')
  248. if '--weston-debug-logging' in cmd:
  249. cmd.remove('--weston-debug-logging')
  250. env = copy.deepcopy(env)
  251. env['WAYLAND_DEBUG'] = '1'
  252. weston_proc_display = None
  253. for _ in range(10):
  254. weston_proc = subprocess.Popen(
  255. weston_cmd,
  256. stderr=subprocess.STDOUT, env=env)
  257. # Get the $WAYLAND_DISPLAY set by Weston and pass it to the test launcher.
  258. # Please note that this env variable is local for the process. That's the
  259. # reason we have to read it from Weston separately.
  260. weston_proc_display = _get_display_from_weston(weston_proc.pid)
  261. if weston_proc_display is not None:
  262. break # Weston could launch and we found the display.
  263. # If we couldn't find the display after 10 tries, raise an exception.
  264. if weston_proc_display is None:
  265. raise _WestonProcessError('Failed to start Weston.')
  266. env['WAYLAND_DISPLAY'] = weston_proc_display
  267. return test_env.run_executable(cmd, env, stdoutfile)
  268. except OSError as e:
  269. print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
  270. return 1
  271. except _WestonProcessError as e:
  272. print('Weston fail: %s\n' % str(e), file=sys.stderr)
  273. return 1
  274. finally:
  275. kill(weston_proc, 'weston')
  276. # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
  277. # To ensure it exits, use SIGKILL which should be safe since all other
  278. # processes that it would have been servicing have exited.
  279. if dbus_pid:
  280. os.kill(dbus_pid, signal.SIGKILL)
  281. def _get_display_from_weston(weston_proc_pid):
  282. """Retrieves $WAYLAND_DISPLAY set by Weston.
  283. Searches for the child "weston-desktop-shell" process, takes its
  284. environmental variables, and returns $WAYLAND_DISPLAY variable set
  285. by that process. If the variable is not set, tries up to 10 times
  286. and then gives up.
  287. Args:
  288. weston_proc_pid: The process of id of the main Weston process.
  289. Returns:
  290. the display set by Wayland, which clients can use to connect to.
  291. TODO(https://crbug.com/1060469): This is potentially error prone
  292. function. See the bug for further details.
  293. """
  294. # Try 100 times as it is not known when Weston spawn child desktop shell
  295. # process. The most seen so far is ~50 checks/~2.5 seconds, but startup
  296. # is usually almost instantaneous.
  297. for _ in range(100):
  298. # gives weston time to start or fail.
  299. time.sleep(.05)
  300. # Take the parent process.
  301. parent = psutil.Process(weston_proc_pid)
  302. if parent is None:
  303. break # The process is not found. Give up.
  304. # Traverse through all the children processes and find the
  305. # "weston-desktop-shell" process that sets local to process env variables
  306. # including the $WAYLAND_DISPLAY.
  307. children = parent.children(recursive=True)
  308. for process in children:
  309. if process.name() == "weston-desktop-shell":
  310. weston_proc_display = process.environ().get('WAYLAND_DISPLAY')
  311. # If display is set, Weston could start successfully and we can use
  312. # that display for Wayland connection in Chromium.
  313. if weston_proc_display is not None:
  314. return weston_proc_display
  315. return None
  316. class MutableBoolean(object):
  317. """Simple mutable boolean class. Used to be mutated inside an handler."""
  318. def __init__(self):
  319. self._val = False
  320. def setvalue(self, val):
  321. assert isinstance(val, bool)
  322. self._val = val
  323. def getvalue(self):
  324. return self._val
  325. def raise_xvfb_error(*_):
  326. raise _XvfbProcessError('Terminated')
  327. def raise_weston_error(*_):
  328. raise _WestonProcessError('Terminated')
  329. def find_display():
  330. """Iterates through X-lock files to find an available display number.
  331. The lower bound follows xvfb-run standard at 99, and the upper bound
  332. is set to 119.
  333. Returns:
  334. A string of a random available display number for Xvfb ':{99-119}'.
  335. Raises:
  336. _XvfbProcessError: Raised when displays 99 through 119 are unavailable.
  337. """
  338. available_displays = [
  339. d for d in range(99, 120)
  340. if not os.path.isfile('/tmp/.X{}-lock'.format(d))
  341. ]
  342. if available_displays:
  343. return ':{}'.format(random.choice(available_displays))
  344. raise _XvfbProcessError('Failed to find display number')
  345. def _set_xdg_runtime_dir(env):
  346. """Sets the $XDG_RUNTIME_DIR variable if it hasn't been set before."""
  347. runtime_dir = env.get('XDG_RUNTIME_DIR')
  348. if not runtime_dir:
  349. runtime_dir = '/tmp/xdg-tmp-dir/'
  350. if not os.path.exists(runtime_dir):
  351. os.makedirs(runtime_dir, 0o700)
  352. env['XDG_RUNTIME_DIR'] = runtime_dir
  353. def main():
  354. usage = 'Usage: xvfb.py [command [--no-xvfb or --use-weston] args...]'
  355. if len(sys.argv) < 2:
  356. print(usage + '\n', file=sys.stderr)
  357. return 2
  358. # If the user still thinks the first argument is the execution directory then
  359. # print a friendly error message and quit.
  360. if os.path.isdir(sys.argv[1]):
  361. print('Invalid command: \"%s\" is a directory\n' % sys.argv[1],
  362. file=sys.stderr)
  363. print(usage + '\n', file=sys.stderr)
  364. return 3
  365. return run_executable(sys.argv[1:], os.environ.copy())
  366. if __name__ == '__main__':
  367. sys.exit(main())