Fix for Geolocation webTCT failures
[platform/framework/web/chromium-efl.git] / build / lacros / mojo_connection_lacros_launcher.py
1 #!/usr/bin/env vpython3
2 #
3 # Copyright 2020 The Chromium Authors
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
6 """Helps launch lacros-chrome with mojo connection established on Linux
7   or Chrome OS. Use on Chrome OS is for dev purposes.
8
9   The main use case is to be able to launch lacros-chrome in a debugger.
10
11   Please first launch an ash-chrome in the background as usual except without
12   the '--lacros-chrome-path' argument and with an additional
13   '--lacros-mojo-socket-for-testing' argument pointing to a socket path:
14
15   XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime ./out/ash/chrome \\
16       --user-data-dir=/tmp/ash-chrome --enable-wayland-server \\
17       --no-startup-window --enable-features=LacrosOnly \\
18       --lacros-mojo-socket-for-testing=/tmp/lacros.sock
19
20   Then, run this script with '-s' pointing to the same socket path used to
21   launch ash-chrome, followed by a command one would use to launch lacros-chrome
22   inside a debugger:
23
24   EGL_PLATFORM=surfaceless XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime \\
25   ./build/lacros/mojo_connection_lacros_launcher.py -s /tmp/lacros.sock
26   gdb --args ./out/lacros-release/chrome --user-data-dir=/tmp/lacros-chrome
27 """
28
29 import argparse
30 import array
31 import contextlib
32 import getpass
33 import grp
34 import os
35 import pathlib
36 import pwd
37 import resource
38 import socket
39 import sys
40 import subprocess
41
42
43 _NUM_FDS_MAX = 3
44
45
46 # contextlib.nullcontext is introduced in 3.7, while Python version on
47 # CrOS is still 3.6. This is for backward compatibility.
48 class NullContext:
49   def __init__(self, enter_ret=None):
50     self.enter_ret = enter_ret
51
52   def __enter__(self):
53     return self.enter_ret
54
55   def __exit__(self, exc_type, exc_value, trace):
56     pass
57
58
59 def _ReceiveFDs(sock):
60   """Receives FDs from ash-chrome that will be used to launch lacros-chrome.
61
62     Args:
63       sock: A connected unix domain socket.
64
65     Returns:
66       File objects for the mojo connection and maybe startup data file.
67     """
68   # This function is borrowed from with modifications:
69   # https://docs.python.org/3/library/socket.html#socket.socket.recvmsg
70   fds = array.array("i")  # Array of ints
71   # Along with the file descriptor, ash-chrome also sends the version in the
72   # regular data.
73   version, ancdata, _, _ = sock.recvmsg(
74       1, socket.CMSG_LEN(fds.itemsize * _NUM_FDS_MAX))
75   for cmsg_level, cmsg_type, cmsg_data in ancdata:
76     if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
77       # There are three versions currently this script supports.
78       # The oldest one: ash-chrome returns one FD, the mojo connection of
79       # old bootstrap procedure (i.e., it will be BrowserService).
80       # The middle one: ash-chrome returns two FDs, the mojo connection of
81       # old bootstrap procedure, and the second for the start up data FD.
82       # The newest one: ash-chrome returns three FDs, the mojo connection of
83       # old bootstrap procedure, the second for the start up data FD, and
84       # the third for another mojo connection of new bootstrap procedure.
85       # TODO(crbug.com/1156033): Clean up the code to drop the support of
86       # oldest one after M91.
87       # TODO(crbug.com/1180712): Clean up the mojo procedure support of the
88       # the middle one after M92.
89       cmsg_len_candidates = [(i + 1) * fds.itemsize
90                              for i in range(_NUM_FDS_MAX)]
91       assert len(cmsg_data) in cmsg_len_candidates, (
92           'CMSG_LEN is unexpected: %d' % (len(cmsg_data), ))
93       fds.frombytes(cmsg_data[:])
94
95   if version == b'\x01':
96     assert len(fds) == 2, 'Expecting exactly 2 FDs'
97     startup_fd = os.fdopen(fds[0])
98     mojo_fd = os.fdopen(fds[1])
99   elif version:
100     raise AssertionError('Unknown version: \\x%s' % version.hex())
101   else:
102     raise AssertionError('Failed to receive startup message from ash-chrome. '
103                          'Make sure you\'re logged in to Chrome OS.')
104   return startup_fd, mojo_fd
105
106
107 def _MaybeClosing(fileobj):
108   """Returns closing context manager, if given fileobj is not None.
109
110     If the given fileobj is none, return nullcontext.
111     """
112   return (contextlib.closing if fileobj else NullContext)(fileobj)
113
114
115 def _ApplyCgroups():
116   """Applies cgroups used in ChromeOS to lacros chrome as well."""
117   # Cgroup directories taken from ChromeOS session_manager job configuration.
118   UI_FREEZER_CGROUP_DIR = '/sys/fs/cgroup/freezer/ui'
119   UI_CPU_CGROUP_DIR = '/sys/fs/cgroup/cpu/ui'
120   pid = os.getpid()
121   with open(os.path.join(UI_CPU_CGROUP_DIR, 'tasks'), 'a') as f:
122     f.write(str(pid) + '\n')
123   with open(os.path.join(UI_FREEZER_CGROUP_DIR, 'cgroup.procs'), 'a') as f:
124     f.write(str(pid) + '\n')
125
126
127 def _PreExec(uid, gid, groups):
128   """Set environment up for running the chrome binary."""
129   # Nice and realtime priority values taken ChromeOSs session_manager job
130   # configuration.
131   resource.setrlimit(resource.RLIMIT_NICE, (40, 40))
132   resource.setrlimit(resource.RLIMIT_RTPRIO, (10, 10))
133   os.setgroups(groups)
134   os.setgid(gid)
135   os.setuid(uid)
136
137
138 def Main():
139   arg_parser = argparse.ArgumentParser()
140   arg_parser.usage = __doc__
141   arg_parser.add_argument(
142       '-r',
143       '--root-env-setup',
144       action='store_true',
145       help='Set typical cgroups and environment for chrome. '
146       'If this is set, this script must be run as root.')
147   arg_parser.add_argument(
148       '-s',
149       '--socket-path',
150       type=pathlib.Path,
151       required=True,
152       help='Absolute path to the socket that were used to start ash-chrome, '
153       'for example: "/tmp/lacros.socket"')
154   flags, args = arg_parser.parse_known_args()
155
156   assert 'XDG_RUNTIME_DIR' in os.environ
157   assert os.environ.get('EGL_PLATFORM') == 'surfaceless'
158
159   if flags.root_env_setup:
160     # Check if we are actually root and error otherwise.
161     assert getpass.getuser() == 'root', \
162         'Root required environment flag specified, but user is not root.'
163     # Apply necessary cgroups to our own process, so they will be inherited by
164     # lacros chrome.
165     _ApplyCgroups()
166   else:
167     print('WARNING: Running chrome without appropriate environment. '
168           'This may affect performance test results. '
169           'Set -r and run as root to avoid this.')
170
171   with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
172     sock.connect(flags.socket_path.as_posix())
173     startup_connection, mojo_connection = (_ReceiveFDs(sock))
174
175   with _MaybeClosing(startup_connection), _MaybeClosing(mojo_connection):
176     cmd = args[:]
177     pass_fds = []
178     if startup_connection:
179       cmd.append('--cros-startup-data-fd=%d' % startup_connection.fileno())
180       pass_fds.append(startup_connection.fileno())
181     if mojo_connection:
182       cmd.append('--crosapi-mojo-platform-channel-handle=%d' %
183                  mojo_connection.fileno())
184       pass_fds.append(mojo_connection.fileno())
185
186     env = os.environ.copy()
187     if flags.root_env_setup:
188       username = 'chronos'
189       p = pwd.getpwnam(username)
190       uid = p.pw_uid
191       gid = p.pw_gid
192       groups = [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]
193       env['HOME'] = p.pw_dir
194       env['LOGNAME'] = username
195       env['USER'] = username
196
197       def fn():
198         return _PreExec(uid, gid, groups)
199     else:
200
201       def fn():
202         return None
203
204     proc = subprocess.Popen(cmd, pass_fds=pass_fds, preexec_fn=fn)
205
206   return proc.wait()
207
208
209 if __name__ == '__main__':
210   sys.exit(Main())