123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- #!/usr/bin/env vpython3
- #
- # Copyright 2020 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """Helps launch lacros-chrome with mojo connection established on Linux
- or Chrome OS. Use on Chrome OS is for dev purposes.
- The main use case is to be able to launch lacros-chrome in a debugger.
- Please first launch an ash-chrome in the background as usual except without
- the '--lacros-chrome-path' argument and with an additional
- '--lacros-mojo-socket-for-testing' argument pointing to a socket path:
- XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime ./out/ash/chrome \\
- --user-data-dir=/tmp/ash-chrome --enable-wayland-server \\
- --no-startup-window --enable-features=LacrosSupport \\
- --lacros-mojo-socket-for-testing=/tmp/lacros.sock
- Then, run this script with '-s' pointing to the same socket path used to
- launch ash-chrome, followed by a command one would use to launch lacros-chrome
- inside a debugger:
- EGL_PLATFORM=surfaceless XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime \\
- ./build/lacros/mojo_connection_lacros_launcher.py -s /tmp/lacros.sock
- gdb --args ./out/lacros-release/chrome --user-data-dir=/tmp/lacros-chrome
- """
- import argparse
- import array
- import contextlib
- import getpass
- import grp
- import os
- import pathlib
- import pwd
- import resource
- import socket
- import sys
- import subprocess
- _NUM_FDS_MAX = 3
- # contextlib.nullcontext is introduced in 3.7, while Python version on
- # CrOS is still 3.6. This is for backward compatibility.
- class NullContext:
- def __init__(self, enter_ret=None):
- self.enter_ret = enter_ret
- def __enter__(self):
- return self.enter_ret
- def __exit__(self, exc_type, exc_value, trace):
- pass
- def _ReceiveFDs(sock):
- """Receives FDs from ash-chrome that will be used to launch lacros-chrome.
- Args:
- sock: A connected unix domain socket.
- Returns:
- File objects for the mojo connection and maybe startup data file.
- """
- # This function is borrowed from with modifications:
- # https://docs.python.org/3/library/socket.html#socket.socket.recvmsg
- fds = array.array("i") # Array of ints
- # Along with the file descriptor, ash-chrome also sends the version in the
- # regular data.
- version, ancdata, _, _ = sock.recvmsg(
- 1, socket.CMSG_LEN(fds.itemsize * _NUM_FDS_MAX))
- for cmsg_level, cmsg_type, cmsg_data in ancdata:
- if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
- # There are three versions currently this script supports.
- # The oldest one: ash-chrome returns one FD, the mojo connection of
- # old bootstrap procedure (i.e., it will be BrowserService).
- # The middle one: ash-chrome returns two FDs, the mojo connection of
- # old bootstrap procedure, and the second for the start up data FD.
- # The newest one: ash-chrome returns three FDs, the mojo connection of
- # old bootstrap procedure, the second for the start up data FD, and
- # the third for another mojo connection of new bootstrap procedure.
- # TODO(crbug.com/1156033): Clean up the code to drop the support of
- # oldest one after M91.
- # TODO(crbug.com/1180712): Clean up the mojo procedure support of the
- # the middle one after M92.
- cmsg_len_candidates = [(i + 1) * fds.itemsize
- for i in range(_NUM_FDS_MAX)]
- assert len(cmsg_data) in cmsg_len_candidates, (
- 'CMSG_LEN is unexpected: %d' % (len(cmsg_data), ))
- fds.frombytes(cmsg_data[:])
- if version == b'\x00':
- assert len(fds) in (1, 2, 3), 'Expecting exactly 1, 2, or 3 FDs'
- legacy_mojo_fd = os.fdopen(fds[0])
- startup_fd = None if len(fds) < 2 else os.fdopen(fds[1])
- mojo_fd = None if len(fds) < 3 else os.fdopen(fds[2])
- elif version == b'\x01':
- assert len(fds) == 2, 'Expecting exactly 2 FDs'
- legacy_mojo_fd = None
- startup_fd = os.fdopen(fds[0])
- mojo_fd = os.fdopen(fds[1])
- elif version:
- raise AssertionError('Unknown version: \\x%s' % version.hex())
- else:
- raise AssertionError('Failed to receive startup message from ash-chrome. '
- 'Make sure you\'re logged in to Chrome OS.')
- return legacy_mojo_fd, startup_fd, mojo_fd
- def _MaybeClosing(fileobj):
- """Returns closing context manager, if given fileobj is not None.
- If the given fileobj is none, return nullcontext.
- """
- return (contextlib.closing if fileobj else NullContext)(fileobj)
- def _ApplyCgroups():
- """Applies cgroups used in ChromeOS to lacros chrome as well."""
- # Cgroup directories taken from ChromeOS session_manager job configuration.
- UI_FREEZER_CGROUP_DIR = '/sys/fs/cgroup/freezer/ui'
- UI_CPU_CGROUP_DIR = '/sys/fs/cgroup/cpu/ui'
- pid = os.getpid()
- with open(os.path.join(UI_CPU_CGROUP_DIR, 'tasks'), 'a') as f:
- f.write(str(pid) + '\n')
- with open(os.path.join(UI_FREEZER_CGROUP_DIR, 'cgroup.procs'), 'a') as f:
- f.write(str(pid) + '\n')
- def _PreExec(uid, gid, groups):
- """Set environment up for running the chrome binary."""
- # Nice and realtime priority values taken ChromeOSs session_manager job
- # configuration.
- resource.setrlimit(resource.RLIMIT_NICE, (40, 40))
- resource.setrlimit(resource.RLIMIT_RTPRIO, (10, 10))
- os.setgroups(groups)
- os.setgid(gid)
- os.setuid(uid)
- def Main():
- arg_parser = argparse.ArgumentParser()
- arg_parser.usage = __doc__
- arg_parser.add_argument(
- '-r',
- '--root-env-setup',
- action='store_true',
- help='Set typical cgroups and environment for chrome. '
- 'If this is set, this script must be run as root.')
- arg_parser.add_argument(
- '-s',
- '--socket-path',
- type=pathlib.Path,
- required=True,
- help='Absolute path to the socket that were used to start ash-chrome, '
- 'for example: "/tmp/lacros.socket"')
- flags, args = arg_parser.parse_known_args()
- assert 'XDG_RUNTIME_DIR' in os.environ
- assert os.environ.get('EGL_PLATFORM') == 'surfaceless'
- if flags.root_env_setup:
- # Check if we are actually root and error otherwise.
- assert getpass.getuser() == 'root', \
- 'Root required environment flag specified, but user is not root.'
- # Apply necessary cgroups to our own process, so they will be inherited by
- # lacros chrome.
- _ApplyCgroups()
- else:
- print('WARNING: Running chrome without appropriate environment. '
- 'This may affect performance test results. '
- 'Set -r and run as root to avoid this.')
- with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
- sock.connect(flags.socket_path.as_posix())
- legacy_mojo_connection, startup_connection, mojo_connection = (
- _ReceiveFDs(sock))
- with _MaybeClosing(legacy_mojo_connection), \
- _MaybeClosing(startup_connection), \
- _MaybeClosing(mojo_connection):
- cmd = args[:]
- pass_fds = []
- if legacy_mojo_connection:
- cmd.append('--mojo-platform-channel-handle=%d' %
- legacy_mojo_connection.fileno())
- pass_fds.append(legacy_mojo_connection.fileno())
- else:
- # TODO(crbug.com/1188020): This is for backward compatibility.
- # We should remove this after M93 lacros is spread enough.
- cmd.append('--mojo-platform-channel-handle=-1')
- if startup_connection:
- cmd.append('--cros-startup-data-fd=%d' % startup_connection.fileno())
- pass_fds.append(startup_connection.fileno())
- if mojo_connection:
- cmd.append('--crosapi-mojo-platform-channel-handle=%d' %
- mojo_connection.fileno())
- pass_fds.append(mojo_connection.fileno())
- env = os.environ.copy()
- if flags.root_env_setup:
- username = 'chronos'
- p = pwd.getpwnam(username)
- uid = p.pw_uid
- gid = p.pw_gid
- groups = [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]
- env['HOME'] = p.pw_dir
- env['LOGNAME'] = username
- env['USER'] = username
- def fn():
- return _PreExec(uid, gid, groups)
- else:
- def fn():
- return None
- proc = subprocess.Popen(cmd, pass_fds=pass_fds, preexec_fn=fn)
- return proc.wait()
- if __name__ == '__main__':
- sys.exit(Main())
|