[M108 Migration][Callback] Bring up ewk callbacks which are related with page loading
[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   proc.terminate()
47   thread = threading.Thread(target=proc.wait)
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, file=sys.stderr)
53     proc.kill()
54
55   thread.join(timeout_in_seconds)
56   if thread.is_alive():
57     print('%s running after SIGTERM and SIGKILL; good luck!\n' % name,
58           file=sys.stderr)
59
60
61 def launch_dbus(env): # pylint: disable=inconsistent-return-statements
62   """Starts a DBus session.
63
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.
73
74   [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12
75
76   Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and
77   DBUS_SESSION_BUS_PID set.
78
79   Returns the pid of the dbus-daemon if started, or None otherwise.
80   """
81   if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
82     return
83   try:
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)
88       if m:
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)
93
94
95 # TODO(crbug.com/949194): Encourage setting flags to False.
96 def run_executable(
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
100      platforms.
101
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.
105
106   Args:
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.
118
119   Returns:
120     the exit code of the specified commandline, or 1 on failure.
121   """
122
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.
128   use_xvfb = True
129   if '--no-xvfb' in cmd:
130     use_xvfb = False
131     cmd.remove('--no-xvfb')
132
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.
136   use_weston = False
137   if '--use-weston' in cmd:
138     if use_xvfb:
139       print('Unable to use Weston with xvfb.\n', file=sys.stderr)
140       return 1
141     use_weston = True
142     cmd.remove('--use-weston')
143
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)
147   if use_weston:
148     return _run_with_weston(cmd, env, stdoutfile, cwd)
149   return test_env.run_executable(cmd, env, stdoutfile, cwd)
150
151
152 def _run_with_xvfb(cmd, env, stdoutfile, use_openbox,
153                    use_xcompmgr, xvfb_whd, cwd):
154   openbox_proc = None
155   openbox_ready = MutableBoolean()
156   def set_openbox_ready(*_):
157     openbox_ready.setvalue(True)
158
159   xcompmgr_proc = None
160   xvfb_proc = None
161   xvfb_ready = MutableBoolean()
162   def set_xvfb_ready(*_):
163     xvfb_ready.setvalue(True)
164
165   dbus_pid = None
166   try:
167     signal.signal(signal.SIGTERM, raise_xvfb_error)
168     signal.signal(signal.SIGINT, raise_xvfb_error)
169
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
176     # required.
177     #
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')
183
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.
186     for _ in range(10):
187       xvfb_ready.setvalue(False)
188       display = find_display()
189
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']
194
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)
202       for _ in range(10):
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.
206
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')
211
212     env['DISPLAY'] = display
213     # Set dummy variable for scripts.
214     env['XVFB_DISPLAY'] = display
215
216     dbus_pid = launch_dbus(env)
217
218     if use_openbox:
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()
222
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)
231
232       for _ in range(10):
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.
236
237       if openbox_proc.poll() is not None:
238         raise _XvfbProcessError('Failed to start OpenBox.')
239
240     if use_xcompmgr:
241       xcompmgr_proc = subprocess.Popen(
242           'xcompmgr', stderr=subprocess.STDOUT, env=env)
243
244     return test_env.run_executable(cmd, env, stdoutfile, cwd)
245   except OSError as e:
246     print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr)
247     return 1
248   except _XvfbProcessError as e:
249     print('Xvfb fail: %s\n' % str(e), file=sys.stderr)
250     return 1
251   finally:
252     kill(openbox_proc, 'openbox')
253     kill(xcompmgr_proc, 'xcompmgr')
254     kill(xvfb_proc, 'Xvfb')
255
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.
259     if dbus_pid:
260       os.kill(dbus_pid, signal.SIGKILL)
261
262
263 # TODO(https://crbug.com/1060466): Write tests.
264 def _run_with_weston(cmd, env, stdoutfile, cwd):
265   weston_proc = None
266
267   try:
268     signal.signal(signal.SIGTERM, raise_weston_error)
269     signal.signal(signal.SIGINT, raise_weston_error)
270
271     dbus_pid = launch_dbus(env)
272
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)
287
288     # Set $XDG_RUNTIME_DIR if it is not set.
289     _set_xdg_runtime_dir(env)
290
291     # Weston is compiled along with the Ozone/Wayland platform, and is
292     # fetched as data deps. Thus, run it from the current directory.
293     #
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
305     # of windows.
306     weston_cmd = ['./weston', '--backend=headless-backend.so', '--idle-time=0',
307           '--modules=test-plugin.so,systemd-notify.so', '--width=1024',
308           '--height=768']
309
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')
314
315     if '--weston-debug-logging' in cmd:
316       cmd.remove('--weston-debug-logging')
317       env = copy.deepcopy(env)
318       env['WAYLAND_DEBUG'] = '1'
319
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
326     # more details.
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()
331
332       weston_proc_display = None
333       for _ in range(10):
334         weston_proc = subprocess.Popen(
335           weston_cmd,
336           stderr=subprocess.STDOUT, env=env)
337
338         for _ in range(25):
339           time.sleep(0.1)  # Gives weston some time to start.
340           try:
341             if notify_socket.recv(512) == b'READY=1':
342               break
343           except BlockingIOError:
344             continue
345
346         for _ in range(25):
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
350           # have been spawned.
351           time.sleep(0.1)
352
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
356           # separately.
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.
360
361         # Also break from the outer loop.
362         if weston_proc_display is not None:
363           break
364
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.')
368
369     env.pop('NOTIFY_SOCKET')
370
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'
375     else:
376       env['WAYLAND_DEBUG'] = '0'
377
378     return test_env.run_executable(cmd, env, stdoutfile, cwd)
379   except OSError as e:
380     print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
381     return 1
382   except _WestonProcessError as e:
383     print('Weston fail: %s\n' % str(e), file=sys.stderr)
384     return 1
385   finally:
386     kill(weston_proc, 'weston')
387
388     if os.path.exists(_weston_notify_socket_address()):
389       os.remove(_weston_notify_socket_address())
390
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.
394     if dbus_pid:
395       os.kill(dbus_pid, signal.SIGKILL)
396
397 def _weston_notify_socket_address():
398   return os.path.join(tempfile.gettempdir(), '.xvfb.py-weston-notify.sock')
399
400 def _get_display_from_weston(weston_proc_pid):
401   """Retrieves $WAYLAND_DISPLAY set by Weston.
402
403   Returns the $WAYLAND_DISPLAY variable from one of weston's subprocesses.
404
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.
410
411   Args:
412     weston_proc_pid: The process of id of the main Weston process.
413
414   Returns:
415     the display set by Wayland, which clients can use to connect to.
416   """
417
418   # Take the parent process.
419   parent = psutil.Process(weston_proc_pid)
420   if parent is None:
421     return None # The process is not found. Give up.
422
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
432   return None
433
434
435 class MutableBoolean(object):
436   """Simple mutable boolean class. Used to be mutated inside an handler."""
437
438   def __init__(self):
439     self._val = False
440
441   def setvalue(self, val):
442     assert isinstance(val, bool)
443     self._val = val
444
445   def getvalue(self):
446     return self._val
447
448
449 def raise_xvfb_error(*_):
450   raise _XvfbProcessError('Terminated')
451
452
453 def raise_weston_error(*_):
454   raise _WestonProcessError('Terminated')
455
456
457 def find_display():
458   """Iterates through X-lock files to find an available display number.
459
460   The lower bound follows xvfb-run standard at 99, and the upper bound
461   is set to 119.
462
463   Returns:
464     A string of a random available display number for Xvfb ':{99-119}'.
465
466   Raises:
467     _XvfbProcessError: Raised when displays 99 through 119 are unavailable.
468   """
469
470   available_displays = [
471       d for d in range(99, 120)
472       if not os.path.isfile('/tmp/.X{}-lock'.format(d))
473   ]
474   if available_displays:
475     return ':{}'.format(random.choice(available_displays))
476   raise _XvfbProcessError('Failed to find display number')
477
478
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')
482   if not 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
487
488
489 def main():
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)
493     return 2
494
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],
499           file=sys.stderr)
500     print(usage + '\n', file=sys.stderr)
501     return 3
502
503   return run_executable(sys.argv[1:], os.environ.copy())
504
505
506 if __name__ == '__main__':
507   sys.exit(main())