• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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