1 #!/usr/bin/env vpython3
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.
9 The main use case is to be able to launch lacros-chrome in a debugger.
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:
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
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
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
46 # contextlib.nullcontext is introduced in 3.7, while Python version on
47 # CrOS is still 3.6. This is for backward compatibility.
49 def __init__(self, enter_ret=None):
50 self.enter_ret = enter_ret
55 def __exit__(self, exc_type, exc_value, trace):
59 def _ReceiveFDs(sock):
60 """Receives FDs from ash-chrome that will be used to launch lacros-chrome.
63 sock: A connected unix domain socket.
66 File objects for the mojo connection and maybe startup data file.
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
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[:])
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])
100 raise AssertionError('Unknown version: \\x%s' % version.hex())
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
107 def _MaybeClosing(fileobj):
108 """Returns closing context manager, if given fileobj is not None.
110 If the given fileobj is none, return nullcontext.
112 return (contextlib.closing if fileobj else NullContext)(fileobj)
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'
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')
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
131 resource.setrlimit(resource.RLIMIT_NICE, (40, 40))
132 resource.setrlimit(resource.RLIMIT_RTPRIO, (10, 10))
139 arg_parser = argparse.ArgumentParser()
140 arg_parser.usage = __doc__
141 arg_parser.add_argument(
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(
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()
156 assert 'XDG_RUNTIME_DIR' in os.environ
157 assert os.environ.get('EGL_PLATFORM') == 'surfaceless'
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
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.')
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))
175 with _MaybeClosing(startup_connection), _MaybeClosing(mojo_connection):
178 if startup_connection:
179 cmd.append('--cros-startup-data-fd=%d' % startup_connection.fileno())
180 pass_fds.append(startup_connection.fileno())
182 cmd.append('--crosapi-mojo-platform-channel-handle=%d' %
183 mojo_connection.fileno())
184 pass_fds.append(mojo_connection.fileno())
186 env = os.environ.copy()
187 if flags.root_env_setup:
189 p = pwd.getpwnam(username)
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
198 return _PreExec(uid, gid, groups)
204 proc = subprocess.Popen(cmd, pass_fds=pass_fds, preexec_fn=fn)
209 if __name__ == '__main__':