blob: 7838eb5d51c9780d6d03b5114b7fa57398be0dc0 [file]
#!/usr/bin/env python3
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Helper script to run the dev CRD multi-process host. Typical usage is:
$ remoting/tools/run_multi_process_host.py out/debug
The script will elevate itself and ask for your password.
"""
import argparse
import atexit
import hashlib
import os
import shutil
import signal
import socket
import subprocess
import sys
import time
def run_command(command):
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
print(
"Command failed with exit code "
f"{e.returncode}: {' '.join(command)}",
file=sys.stderr)
raise
def get_gdm_version():
try:
result = subprocess.run(["gdm3", "--version"],
capture_output=True,
text=True,
check=True)
# result.stdout: "GDM 38.0\n"
output = result.stdout.strip()
if output.startswith("GDM "):
return output[4:]
return output
except (subprocess.CalledProcessError, FileNotFoundError):
return None
def is_gdm_version_ge_49():
version_str = get_gdm_version()
if not version_str:
return False
try:
# Version might be "49.0" or just "49"
major_version = int(version_str.split(".")[0])
return major_version >= 49
except (ValueError, IndexError):
return False
def terminate_remote_sessions():
print("Cleaning up remote sessions...")
try:
result = subprocess.run(["loginctl", "list-sessions", "--no-legend"],
capture_output=True,
text=True,
check=True)
for line in result.stdout.splitlines():
parts = line.split()
if not parts:
continue
session_id = parts[0]
show_session_command = [
"loginctl", "show-session", session_id, "-p", "Remote", "-p",
"Type"
]
show_result = subprocess.run(show_session_command,
capture_output=True,
text=True,
check=False)
if show_result.returncode != 0:
continue
props = {}
for show_line in show_result.stdout.splitlines():
if "=" in show_line:
k, v = show_line.split("=", 1)
props[k] = v
if (props.get("Remote") == "yes"
and props.get("Type") in ["wayland", "x11"]):
print(f"Terminating remote session {session_id} "
f"(Type={props.get('Type')})...")
subprocess.run(["loginctl", "terminate-session", session_id],
check=False)
except subprocess.CalledProcessError as e:
print(f"Failed to list sessions: {e}", file=sys.stderr)
def handle_signal(signum, frame):
del signum, frame # Unused
# This will trigger atexit handlers.
sys.exit(0)
def get_file_trigger_state(path):
try:
st = os.stat(path)
return (st.st_mtime, st.st_ino)
except FileNotFoundError:
return None
def ensure_permissions(abs_out_dir, user_home, force=False):
remoting_host_path = os.path.join(abs_out_dir, "remoting_me2me_host")
remoting_core_path = os.path.join(abs_out_dir, "libremoting_core.so")
def check_permissions():
for user in [
"_crd_network", "_crd_peer_connection", "_crd_crashpad"
]:
try:
subprocess.run([
"sudo", "-u", user, "test", "-x", remoting_host_path
],
check=True,
capture_output=True)
subprocess.run([
"sudo", "-u", user, "test", "-r", remoting_core_path
],
check=True,
capture_output=True)
except subprocess.CalledProcessError:
return False
return True
print("Checking permissions...")
if not force and check_permissions():
print("System users have the right permissions.")
return
if force:
print("Forcing permission update...")
else:
print("System users do not have execute permissions. Setting ACLs...")
run_command([
"setfacl", "-R", "-m", "u:_crd_network:rx", "-m",
"u:_crd_peer_connection:rx", "-m", "u:_crd_crashpad:rx", "-m",
"g:Debian-gdm:rx", abs_out_dir
])
# Accounts also need to be granted read and executable permissions to
# all the parent directories.
current_dir = abs_out_dir
user_home_abs = os.path.abspath(user_home)
while not check_permissions():
parent_dir = os.path.dirname(current_dir)
if parent_dir == current_dir:
break
print(f"Setting ACLs on {parent_dir}...")
run_command([
"setfacl", "-m", "u:_crd_network:rx", "-m",
"u:_crd_peer_connection:rx", "-m", "u:_crd_crashpad:rx", "-m",
"g:Debian-gdm:rx", parent_dir
])
if parent_dir == user_home_abs:
break
current_dir = parent_dir
if check_permissions():
print("ACLs updated successfully.")
else:
print("ACLs updated but permissions still failing.", file=sys.stderr)
def user_main(out_dir, keep_sessions=False, set_permissions=False):
try:
run_command(["systemctl", "is-active", "--quiet", "gdm3"])
except subprocess.CalledProcessError:
print("gdm3 service is not running.")
print("Please make sure gdm3.service is unmasked and started:")
print(" sudo systemctl unmask gdm3.service")
print(" sudo systemctl start gdm3.service")
sys.exit(1)
print("Re-running script with sudo...")
script_path = os.path.abspath(__file__)
home_dir = os.path.expanduser("~")
cmd = [
"sudo", sys.executable, script_path, "--elevated", out_dir,
"--user-home", home_dir
]
if keep_sessions:
cmd.append("--keep-sessions")
if set_permissions:
cmd.append("--set-permissions")
run_command(cmd)
def root_main(out_dir,
user_home,
keep_sessions=False,
set_permissions=False):
abs_out_dir = os.path.abspath(out_dir)
host_config_path = "/etc/chrome-remote-desktop/host.json"
if not os.path.exists(host_config_path):
print(f"{host_config_path} not found. "
"Looking for the current user config...")
host_hash = hashlib.md5(socket.gethostname().encode()).hexdigest()
config_file = os.path.join(
user_home, f".config/chrome-remote-desktop/host#{host_hash}.json")
if os.path.exists(config_file):
os.makedirs(os.path.dirname(host_config_path), exist_ok=True)
shutil.copy2(config_file, host_config_path)
print(f"Copied {config_file} to {host_config_path}")
else:
print("No suitable user config found")
sys.exit(1)
print("Adding CRD system users...")
run_command(["adduser", "--system", "_crd_network"])
run_command(["adduser", "--system", "_crd_peer_connection"])
run_command(["adduser", "--system", "_crd_crashpad"])
remoting_host_path = os.path.join(abs_out_dir, "remoting_me2me_host")
remoting_core_path = os.path.join(abs_out_dir, "libremoting_core.so")
daemon_command = [remoting_host_path, "--type=daemon"]
# Initial state for monitoring
host_state = get_file_trigger_state(remoting_host_path)
core_state = get_file_trigger_state(remoting_core_path)
proc = None
def start_daemon():
nonlocal proc
print(f"Starting CRD host daemon: {' '.join(daemon_command)}")
proc = subprocess.Popen(daemon_command)
def stop_daemon():
nonlocal proc
if proc and proc.poll() is None:
print("Stopping CRD host daemon...")
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
print("Daemon did not terminate, killing...")
proc.kill()
proc.wait()
print("CRD host daemon stopped.")
proc = None
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGHUP, handle_signal)
if not keep_sessions:
atexit.register(terminate_remote_sessions)
if host_state is not None and core_state is not None:
ensure_permissions(abs_out_dir, user_home, force=set_permissions)
start_daemon()
else:
print("Binaries are missing. Waiting for them to be built...")
try:
while True:
if proc and proc.poll() is not None:
print(f"Host daemon exited with code {proc.returncode}")
break
current_host_state = get_file_trigger_state(remoting_host_path)
current_core_state = get_file_trigger_state(remoting_core_path)
changed = False
if current_host_state != host_state:
print(f"Detected change in {remoting_host_path}")
host_state = current_host_state
changed = True
if current_core_state != core_state:
print(f"Detected change in {remoting_core_path}")
core_state = current_core_state
changed = True
if changed:
stop_daemon()
if host_state is not None and core_state is not None:
print("Binaries updated/restored. Starting daemon...")
ensure_permissions(abs_out_dir,
user_home,
force=set_permissions)
start_daemon()
else:
print(
"Binaries are missing (likely rebuilding). Waiting for "
"restore...")
time.sleep(1)
except KeyboardInterrupt:
print("\nCaught KeyboardInterrupt. Stopping host daemon...")
finally:
stop_daemon()
def main():
parser = argparse.ArgumentParser(
description="Run dev CRD multi-process host.")
parser.add_argument("out_dir",
nargs="?",
help="Build output directory (e.g., out/debug)")
parser.add_argument(
"--keep-sessions",
action="store_true",
help=
"Skip terminating remote sessions on exit. Graphical sessions created "
"by CRD will be kept even after the host has exited. This allows for "
"testing session recovery behavior. Run this script as root with the "
"--terminate-sessions flag to terminate these sessions.")
parser.add_argument(
"--set-permissions",
action="store_true",
help=
"Forcibly run the setfacl command to set permissions, bypassing the "
"permission check. Useful if you modified dependencies (e.g. WebRTC) "
"that aren't checked by default.")
parser.add_argument(
"--terminate-sessions",
action="store_true",
help="Only terminate remote sessions and exit. "
"Must be run with sudo.")
# These should be set by the script itself.
parser.add_argument("--elevated",
action="store_true",
help=argparse.SUPPRESS)
parser.add_argument("--user-home", help=argparse.SUPPRESS)
args = parser.parse_args()
if args.terminate_sessions:
if os.geteuid() != 0:
print("Error: --terminate-sessions must be run with sudo.")
sys.exit(1)
terminate_remote_sessions()
sys.exit(0)
if not args.out_dir:
parser.error("out_dir is required when not using --terminate-sessions")
if not os.path.isdir(args.out_dir):
print(f"Error: Output directory not found: {args.out_dir}")
sys.exit(1)
if args.elevated:
if os.geteuid() != 0:
print("Error: Must be run as root in elevated mode.")
sys.exit(1)
if not args.user_home:
print("Error: --user-home is required in elevated mode.")
sys.exit(1)
root_main(args.out_dir, args.user_home, args.keep_sessions,
args.set_permissions)
else:
if os.geteuid() == 0:
print("Error: Do not run this script directly with sudo. "
"It will re-invoke itself.")
sys.exit(1)
user_main(args.out_dir, args.keep_sessions, args.set_permissions)
if __name__ == "__main__":
main()