[M120 Migration][VD] Remove accessing oom_score_adj in zygote process
[platform/framework/web/chromium-efl.git] / testing / xvfb.py
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 and Openbox or Weston on Linux and normally on other
7    platforms."""
8
9 from __future__ import print_function
10
11 import copy
12 import os
13 import os.path
14 import random
15 import re
16 import signal
17 import socket
18 import subprocess
19 import sys
20 import tempfile
21 import threading
22 import time
23
24 import psutil
25
26 import test_env
27
28 DEFAULT_XVFB_WHD = '1280x800x24'
29
30 # pylint: disable=useless-object-inheritance
31
32
33 class _XvfbProcessError(Exception):
34   """Exception raised when Xvfb cannot start."""
35
36
37 class _WestonProcessError(Exception):
38   """Exception raised when Weston cannot start."""
39
40
41 def kill(proc, name, timeout_in_seconds=10):
42   """Tries to kill |proc| gracefully with a timeout for each signal."""
43   if not proc:
44     return
45
46   thread = threading.Thread(target=proc.wait)
47   try:
48     proc.terminate()
49     thread.start()
50
51     thread.join(timeout_in_seconds)
52     if thread.is_alive():
53       print('%s running after SIGTERM, trying SIGKILL.\n' % name,
54         file=sys.stderr)
55       proc.kill()
56   except OSError as e:
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)
60
61   thread.join(timeout_in_seconds)
62   if thread.is_alive():
63     print('%s running after SIGTERM and SIGKILL; good luck!\n' % name,
64           file=sys.stderr)
65
66
67 def launch_dbus(env): # pylint: disable=inconsistent-return-statements
68   """Starts a DBus session.
69
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.
79
80   [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12
81
82   Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and
83   DBUS_SESSION_BUS_PID set.
84
85   Returns the pid of the dbus-daemon if started, or None otherwise.
86   """
87   if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
88     return
89   try:
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)
94       if m:
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)
99
100
101 # TODO(crbug.com/949194): Encourage setting flags to False.
102 def run_executable(
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
106      platforms.
107
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.
111
112   Args:
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.
124
125   Returns:
126     the exit code of the specified commandline, or 1 on failure.
127   """
128
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.
134   use_xvfb = True
135   if '--no-xvfb' in cmd:
136     use_xvfb = False
137     cmd.remove('--no-xvfb')
138
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.
142   use_weston = False
143   if '--use-weston' in cmd:
144     if use_xvfb:
145       print('Unable to use Weston with xvfb.\n', file=sys.stderr)
146       return 1
147     use_weston = True
148     cmd.remove('--use-weston')
149
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)
153   if use_weston:
154     return _run_with_weston(cmd, env, stdoutfile, cwd)
155   return test_env.run_executable(cmd, env, stdoutfile, cwd)
156
157
158 def _run_with_xvfb(cmd, env, stdoutfile, use_openbox,
159                    use_xcompmgr, xvfb_whd, cwd):
160   openbox_proc = None
161   openbox_ready = MutableBoolean()
162   def set_openbox_ready(*_):
163     openbox_ready.setvalue(True)
164
165   xcompmgr_proc = None
166   xvfb_proc = None
167   xvfb_ready = MutableBoolean()
168   def set_xvfb_ready(*_):
169     xvfb_ready.setvalue(True)
170
171   dbus_pid = None
172   try:
173     signal.signal(signal.SIGTERM, raise_xvfb_error)
174     signal.signal(signal.SIGINT, raise_xvfb_error)
175
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
182     # required.
183     #
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')
189
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.
192     for _ in range(10):
193       xvfb_ready.setvalue(False)
194       display = find_display()
195
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']
200
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)
208       for _ in range(30):
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.
212
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
217
218     if xvfb_proc.poll() is not None:
219       raise _XvfbProcessError('Failed to start after 10 tries')
220
221     env['DISPLAY'] = display
222     # Set dummy variable for scripts.
223     env['XVFB_DISPLAY'] = display
224
225     dbus_pid = launch_dbus(env)
226
227     if use_openbox:
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()
231
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)
240
241       for _ in range(30):
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.
245
246       if openbox_proc.poll() is not None or not openbox_ready.getvalue():
247         raise _XvfbProcessError('Failed to start OpenBox.')
248
249     if use_xcompmgr:
250       xcompmgr_proc = subprocess.Popen(
251           'xcompmgr', stderr=subprocess.STDOUT, env=env)
252
253     return test_env.run_executable(cmd, env, stdoutfile, cwd)
254   except OSError as e:
255     print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr)
256     return 1
257   except _XvfbProcessError as e:
258     print('Xvfb fail: %s\n' % str(e), file=sys.stderr)
259     return 1
260   finally:
261     kill(openbox_proc, 'openbox')
262     kill(xcompmgr_proc, 'xcompmgr')
263     kill(xvfb_proc, 'Xvfb')
264
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.
268     if dbus_pid:
269       os.kill(dbus_pid, signal.SIGKILL)
270
271
272 # TODO(https://crbug.com/1060466): Write tests.
273 def _run_with_weston(cmd, env, stdoutfile, cwd):
274   weston_proc = None
275
276   try:
277     signal.signal(signal.SIGTERM, raise_weston_error)
278     signal.signal(signal.SIGINT, raise_weston_error)
279
280     dbus_pid = launch_dbus(env)
281
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)
296
297     # Set $XDG_RUNTIME_DIR if it is not set.
298     _set_xdg_runtime_dir(env)
299
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')
305
306     # Weston is compiled along with the Ozone/Wayland platform, and is
307     # fetched as data deps. Thus, run it from the current directory.
308     #
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
320     # of windows.
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()]
325
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')
330
331     if '--weston-debug-logging' in cmd:
332       cmd.remove('--weston-debug-logging')
333       env = copy.deepcopy(env)
334       env['WAYLAND_DEBUG'] = '1'
335
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
342     # more details.
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()
347
348       weston_proc_display = None
349       for _ in range(10):
350         weston_proc = subprocess.Popen(
351           weston_cmd,
352           stderr=subprocess.STDOUT, env=env)
353
354         for _ in range(25):
355           time.sleep(0.1)  # Gives weston some time to start.
356           try:
357             if notify_socket.recv(512) == b'READY=1':
358               break
359           except BlockingIOError:
360             continue
361
362         for _ in range(25):
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
366           # have been spawned.
367           time.sleep(0.1)
368
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
372           # separately.
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.
376
377         # Also break from the outer loop.
378         if weston_proc_display is not None:
379           break
380
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.')
384
385     env.pop('NOTIFY_SOCKET')
386
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'
391     else:
392       env['WAYLAND_DEBUG'] = '0'
393
394     return test_env.run_executable(cmd, env, stdoutfile, cwd)
395   except OSError as e:
396     print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
397     return 1
398   except _WestonProcessError as e:
399     print('Weston fail: %s\n' % str(e), file=sys.stderr)
400     return 1
401   finally:
402     kill(weston_proc, 'weston')
403
404     if os.path.exists(_weston_notify_socket_address()):
405       os.remove(_weston_notify_socket_address())
406
407     if os.path.exists(_weston_config_file_path()):
408       os.remove(_weston_config_file_path())
409
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.
413     if dbus_pid:
414       os.kill(dbus_pid, signal.SIGKILL)
415
416 def _weston_notify_socket_address():
417   return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston-notify.sock')
418
419 def _weston_config_file_path():
420   return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston.ini')
421
422 def _get_display_from_weston(weston_proc_pid):
423   """Retrieves $WAYLAND_DISPLAY set by Weston.
424
425   Returns the $WAYLAND_DISPLAY variable from one of weston's subprocesses.
426
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.
432
433   Args:
434     weston_proc_pid: The process of id of the main Weston process.
435
436   Returns:
437     the display set by Wayland, which clients can use to connect to.
438   """
439
440   # Take the parent process.
441   parent = psutil.Process(weston_proc_pid)
442   if parent is None:
443     return None # The process is not found. Give up.
444
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
454   return None
455
456
457 class MutableBoolean(object):
458   """Simple mutable boolean class. Used to be mutated inside an handler."""
459
460   def __init__(self):
461     self._val = False
462
463   def setvalue(self, val):
464     assert isinstance(val, bool)
465     self._val = val
466
467   def getvalue(self):
468     return self._val
469
470
471 def raise_xvfb_error(*_):
472   raise _XvfbProcessError('Terminated')
473
474
475 def raise_weston_error(*_):
476   raise _WestonProcessError('Terminated')
477
478
479 def find_display():
480   """Iterates through X-lock files to find an available display number.
481
482   The lower bound follows xvfb-run standard at 99, and the upper bound
483   is set to 119.
484
485   Returns:
486     A string of a random available display number for Xvfb ':{99-119}'.
487
488   Raises:
489     _XvfbProcessError: Raised when displays 99 through 119 are unavailable.
490   """
491
492   available_displays = [
493       d for d in range(99, 120)
494       if not os.path.isfile('/tmp/.X{}-lock'.format(d))
495   ]
496   if available_displays:
497     return ':{}'.format(random.choice(available_displays))
498   raise _XvfbProcessError('Failed to find display number')
499
500
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')
504   if not 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
509
510
511 def main():
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)
515     return 2
516
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],
521           file=sys.stderr)
522     print(usage + '\n', file=sys.stderr)
523     return 3
524
525   return run_executable(sys.argv[1:], os.environ.copy())
526
527
528 if __name__ == '__main__':
529   sys.exit(main())