mojo_connection_lacros_launcher.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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. The main use case is to be able to launch lacros-chrome in a debugger.
  9. Please first launch an ash-chrome in the background as usual except without
  10. the '--lacros-chrome-path' argument and with an additional
  11. '--lacros-mojo-socket-for-testing' argument pointing to a socket path:
  12. XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime ./out/ash/chrome \\
  13. --user-data-dir=/tmp/ash-chrome --enable-wayland-server \\
  14. --no-startup-window --enable-features=LacrosSupport \\
  15. --lacros-mojo-socket-for-testing=/tmp/lacros.sock
  16. Then, run this script with '-s' pointing to the same socket path used to
  17. launch ash-chrome, followed by a command one would use to launch lacros-chrome
  18. inside a debugger:
  19. EGL_PLATFORM=surfaceless XDG_RUNTIME_DIR=/tmp/ash_chrome_xdg_runtime \\
  20. ./build/lacros/mojo_connection_lacros_launcher.py -s /tmp/lacros.sock
  21. gdb --args ./out/lacros-release/chrome --user-data-dir=/tmp/lacros-chrome
  22. """
  23. import argparse
  24. import array
  25. import contextlib
  26. import getpass
  27. import grp
  28. import os
  29. import pathlib
  30. import pwd
  31. import resource
  32. import socket
  33. import sys
  34. import subprocess
  35. _NUM_FDS_MAX = 3
  36. # contextlib.nullcontext is introduced in 3.7, while Python version on
  37. # CrOS is still 3.6. This is for backward compatibility.
  38. class NullContext:
  39. def __init__(self, enter_ret=None):
  40. self.enter_ret = enter_ret
  41. def __enter__(self):
  42. return self.enter_ret
  43. def __exit__(self, exc_type, exc_value, trace):
  44. pass
  45. def _ReceiveFDs(sock):
  46. """Receives FDs from ash-chrome that will be used to launch lacros-chrome.
  47. Args:
  48. sock: A connected unix domain socket.
  49. Returns:
  50. File objects for the mojo connection and maybe startup data file.
  51. """
  52. # This function is borrowed from with modifications:
  53. # https://docs.python.org/3/library/socket.html#socket.socket.recvmsg
  54. fds = array.array("i") # Array of ints
  55. # Along with the file descriptor, ash-chrome also sends the version in the
  56. # regular data.
  57. version, ancdata, _, _ = sock.recvmsg(
  58. 1, socket.CMSG_LEN(fds.itemsize * _NUM_FDS_MAX))
  59. for cmsg_level, cmsg_type, cmsg_data in ancdata:
  60. if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
  61. # There are three versions currently this script supports.
  62. # The oldest one: ash-chrome returns one FD, the mojo connection of
  63. # old bootstrap procedure (i.e., it will be BrowserService).
  64. # The middle one: ash-chrome returns two FDs, the mojo connection of
  65. # old bootstrap procedure, and the second for the start up data FD.
  66. # The newest one: ash-chrome returns three FDs, the mojo connection of
  67. # old bootstrap procedure, the second for the start up data FD, and
  68. # the third for another mojo connection of new bootstrap procedure.
  69. # TODO(crbug.com/1156033): Clean up the code to drop the support of
  70. # oldest one after M91.
  71. # TODO(crbug.com/1180712): Clean up the mojo procedure support of the
  72. # the middle one after M92.
  73. cmsg_len_candidates = [(i + 1) * fds.itemsize
  74. for i in range(_NUM_FDS_MAX)]
  75. assert len(cmsg_data) in cmsg_len_candidates, (
  76. 'CMSG_LEN is unexpected: %d' % (len(cmsg_data), ))
  77. fds.frombytes(cmsg_data[:])
  78. if version == b'\x00':
  79. assert len(fds) in (1, 2, 3), 'Expecting exactly 1, 2, or 3 FDs'
  80. legacy_mojo_fd = os.fdopen(fds[0])
  81. startup_fd = None if len(fds) < 2 else os.fdopen(fds[1])
  82. mojo_fd = None if len(fds) < 3 else os.fdopen(fds[2])
  83. elif version == b'\x01':
  84. assert len(fds) == 2, 'Expecting exactly 2 FDs'
  85. legacy_mojo_fd = None
  86. startup_fd = os.fdopen(fds[0])
  87. mojo_fd = os.fdopen(fds[1])
  88. elif version:
  89. raise AssertionError('Unknown version: \\x%s' % version.hex())
  90. else:
  91. raise AssertionError('Failed to receive startup message from ash-chrome. '
  92. 'Make sure you\'re logged in to Chrome OS.')
  93. return legacy_mojo_fd, startup_fd, mojo_fd
  94. def _MaybeClosing(fileobj):
  95. """Returns closing context manager, if given fileobj is not None.
  96. If the given fileobj is none, return nullcontext.
  97. """
  98. return (contextlib.closing if fileobj else NullContext)(fileobj)
  99. def _ApplyCgroups():
  100. """Applies cgroups used in ChromeOS to lacros chrome as well."""
  101. # Cgroup directories taken from ChromeOS session_manager job configuration.
  102. UI_FREEZER_CGROUP_DIR = '/sys/fs/cgroup/freezer/ui'
  103. UI_CPU_CGROUP_DIR = '/sys/fs/cgroup/cpu/ui'
  104. pid = os.getpid()
  105. with open(os.path.join(UI_CPU_CGROUP_DIR, 'tasks'), 'a') as f:
  106. f.write(str(pid) + '\n')
  107. with open(os.path.join(UI_FREEZER_CGROUP_DIR, 'cgroup.procs'), 'a') as f:
  108. f.write(str(pid) + '\n')
  109. def _PreExec(uid, gid, groups):
  110. """Set environment up for running the chrome binary."""
  111. # Nice and realtime priority values taken ChromeOSs session_manager job
  112. # configuration.
  113. resource.setrlimit(resource.RLIMIT_NICE, (40, 40))
  114. resource.setrlimit(resource.RLIMIT_RTPRIO, (10, 10))
  115. os.setgroups(groups)
  116. os.setgid(gid)
  117. os.setuid(uid)
  118. def Main():
  119. arg_parser = argparse.ArgumentParser()
  120. arg_parser.usage = __doc__
  121. arg_parser.add_argument(
  122. '-r',
  123. '--root-env-setup',
  124. action='store_true',
  125. help='Set typical cgroups and environment for chrome. '
  126. 'If this is set, this script must be run as root.')
  127. arg_parser.add_argument(
  128. '-s',
  129. '--socket-path',
  130. type=pathlib.Path,
  131. required=True,
  132. help='Absolute path to the socket that were used to start ash-chrome, '
  133. 'for example: "/tmp/lacros.socket"')
  134. flags, args = arg_parser.parse_known_args()
  135. assert 'XDG_RUNTIME_DIR' in os.environ
  136. assert os.environ.get('EGL_PLATFORM') == 'surfaceless'
  137. if flags.root_env_setup:
  138. # Check if we are actually root and error otherwise.
  139. assert getpass.getuser() == 'root', \
  140. 'Root required environment flag specified, but user is not root.'
  141. # Apply necessary cgroups to our own process, so they will be inherited by
  142. # lacros chrome.
  143. _ApplyCgroups()
  144. else:
  145. print('WARNING: Running chrome without appropriate environment. '
  146. 'This may affect performance test results. '
  147. 'Set -r and run as root to avoid this.')
  148. with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
  149. sock.connect(flags.socket_path.as_posix())
  150. legacy_mojo_connection, startup_connection, mojo_connection = (
  151. _ReceiveFDs(sock))
  152. with _MaybeClosing(legacy_mojo_connection), \
  153. _MaybeClosing(startup_connection), \
  154. _MaybeClosing(mojo_connection):
  155. cmd = args[:]
  156. pass_fds = []
  157. if legacy_mojo_connection:
  158. cmd.append('--mojo-platform-channel-handle=%d' %
  159. legacy_mojo_connection.fileno())
  160. pass_fds.append(legacy_mojo_connection.fileno())
  161. else:
  162. # TODO(crbug.com/1188020): This is for backward compatibility.
  163. # We should remove this after M93 lacros is spread enough.
  164. cmd.append('--mojo-platform-channel-handle=-1')
  165. if startup_connection:
  166. cmd.append('--cros-startup-data-fd=%d' % startup_connection.fileno())
  167. pass_fds.append(startup_connection.fileno())
  168. if mojo_connection:
  169. cmd.append('--crosapi-mojo-platform-channel-handle=%d' %
  170. mojo_connection.fileno())
  171. pass_fds.append(mojo_connection.fileno())
  172. env = os.environ.copy()
  173. if flags.root_env_setup:
  174. username = 'chronos'
  175. p = pwd.getpwnam(username)
  176. uid = p.pw_uid
  177. gid = p.pw_gid
  178. groups = [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]
  179. env['HOME'] = p.pw_dir
  180. env['LOGNAME'] = username
  181. env['USER'] = username
  182. def fn():
  183. return _PreExec(uid, gid, groups)
  184. else:
  185. def fn():
  186. return None
  187. proc = subprocess.Popen(cmd, pass_fds=pass_fds, preexec_fn=fn)
  188. return proc.wait()
  189. if __name__ == '__main__':
  190. sys.exit(Main())