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=LacrosSupport \\ 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 29import argparse 30import array 31import contextlib 32import getpass 33import grp 34import os 35import pathlib 36import pwd 37import resource 38import socket 39import sys 40import 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. 48class 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 59def _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 107def _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 115def _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 127def _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 138def 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 209if __name__ == '__main__': 210 sys.exit(Main()) 211