tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mojo_connection_lacros_launcher.py (7380B)


      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/40735724): Clean up the code to drop the support of
     86      # oldest one after M91.
     87      # TODO(crbug.com/40170079): 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())