1#!/usr/bin/env vpython3 2# 3# Copyright 2020 The Chromium Authors. All rights reserved. 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 os 33import pathlib 34import socket 35import sys 36import subprocess 37 38 39_NUM_FDS_MAX = 3 40 41 42# contextlib.nullcontext is introduced in 3.7, while Python version on 43# CrOS is still 3.6. This is for backward compatibility. 44class NullContext: 45 def __init__(self, enter_ret=None): 46 self.enter_ret = enter_ret 47 48 def __enter__(self): 49 return self.enter_ret 50 51 def __exit__(self, exc_type, exc_value, trace): 52 pass 53 54 55def _ReceiveFDs(sock): 56 """Receives FDs from ash-chrome that will be used to launch lacros-chrome. 57 58 Args: 59 sock: A connected unix domain socket. 60 61 Returns: 62 File objects for the mojo connection and maybe startup data file. 63 """ 64 # This function is borrowed from with modifications: 65 # https://docs.python.org/3/library/socket.html#socket.socket.recvmsg 66 fds = array.array("i") # Array of ints 67 # Along with the file descriptor, ash-chrome also sends the version in the 68 # regular data. 69 version, ancdata, _, _ = sock.recvmsg( 70 1, socket.CMSG_LEN(fds.itemsize * _NUM_FDS_MAX)) 71 for cmsg_level, cmsg_type, cmsg_data in ancdata: 72 if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: 73 # There are three versions currently this script supports. 74 # The oldest one: ash-chrome returns one FD, the mojo connection of 75 # old bootstrap procedure (i.e., it will be BrowserService). 76 # The middle one: ash-chrome returns two FDs, the mojo connection of 77 # old bootstrap procedure, and the second for the start up data FD. 78 # The newest one: ash-chrome returns three FDs, the mojo connection of 79 # old bootstrap procedure, the second for the start up data FD, and 80 # the third for another mojo connection of new bootstrap procedure. 81 # TODO(crbug.com/1156033): Clean up the code to drop the support of 82 # oldest one after M91. 83 # TODO(crbug.com/1180712): Clean up the mojo procedure support of the 84 # the middle one after M92. 85 cmsg_len_candidates = [(i + 1) * fds.itemsize 86 for i in range(_NUM_FDS_MAX)] 87 assert len(cmsg_data) in cmsg_len_candidates, ( 88 'CMSG_LEN is unexpected: %d' % (len(cmsg_data), )) 89 fds.frombytes(cmsg_data[:]) 90 91 if version == b'\x00': 92 assert len(fds) in (1, 2, 3), 'Expecting exactly 1, 2, or 3 FDs' 93 legacy_mojo_fd = os.fdopen(fds[0]) 94 startup_fd = None if len(fds) < 2 else os.fdopen(fds[1]) 95 mojo_fd = None if len(fds) < 3 else os.fdopen(fds[2]) 96 elif version == b'\x01': 97 assert len(fds) == 2, 'Expecting exactly 2 FDs' 98 legacy_mojo_fd = None 99 startup_fd = os.fdopen(fds[0]) 100 mojo_fd = os.fdopen(fds[1]) 101 else: 102 raise AssertionError('Unknown version: \\x%s' % version.encode('hex')) 103 return legacy_mojo_fd, startup_fd, mojo_fd 104 105 106def _MaybeClosing(fileobj): 107 """Returns closing context manager, if given fileobj is not None. 108 109 If the given fileobj is none, return nullcontext. 110 """ 111 return (contextlib.closing if fileobj else NullContext)(fileobj) 112 113 114def Main(): 115 arg_parser = argparse.ArgumentParser() 116 arg_parser.usage = __doc__ 117 arg_parser.add_argument( 118 '-s', 119 '--socket-path', 120 type=pathlib.Path, 121 required=True, 122 help='Absolute path to the socket that were used to start ash-chrome, ' 123 'for example: "/tmp/lacros.socket"') 124 flags, args = arg_parser.parse_known_args() 125 126 assert 'XDG_RUNTIME_DIR' in os.environ 127 assert os.environ.get('EGL_PLATFORM') == 'surfaceless' 128 129 with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: 130 sock.connect(flags.socket_path.as_posix()) 131 legacy_mojo_connection, startup_connection, mojo_connection = ( 132 _ReceiveFDs(sock)) 133 134 with _MaybeClosing(legacy_mojo_connection), \ 135 _MaybeClosing(startup_connection), \ 136 _MaybeClosing(mojo_connection): 137 cmd = args[:] 138 pass_fds = [] 139 if legacy_mojo_connection: 140 cmd.append('--mojo-platform-channel-handle=%d' % 141 legacy_mojo_connection.fileno()) 142 pass_fds.append(legacy_mojo_connection.fileno()) 143 else: 144 # TODO(crbug.com/1188020): This is for backward compatibility. 145 # We should remove this after M93 lacros is spread enough. 146 cmd.append('--mojo-platform-channel-handle=-1') 147 if startup_connection: 148 cmd.append('--cros-startup-data-fd=%d' % startup_connection.fileno()) 149 pass_fds.append(startup_connection.fileno()) 150 if mojo_connection: 151 cmd.append('--crosapi-mojo-platform-channel-handle=%d' % 152 mojo_connection.fileno()) 153 pass_fds.append(mojo_connection.fileno()) 154 proc = subprocess.Popen(cmd, pass_fds=pass_fds) 155 156 return proc.wait() 157 158 159if __name__ == '__main__': 160 sys.exit(Main()) 161