# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import shlex
import subprocess
import time
from typing import List
from cca import cli
from cca import build
from cca import util
_CCA_OVERRIDE_PATH = "/etc/camera/cca"
_CCA_OVERRIDE_FEATURE = "CCALocalOverride"
_CHROME_DEV_CONF_PATH = "/etc/chrome_dev.conf"
def _local_override_enabled(device: str) -> bool:
chrome_dev_conf = util.check_output(
["ssh", device, "--", "cat", _CHROME_DEV_CONF_PATH])
# This is a simple heuristic that is not 100% accurate, since this only
# matches the feature name which can be in other irrevelant position in the
# file. This should be fine though since this is only used for developers
# and it's not expected to have the exact string match outside of
# --enable-features added by this script.
return _CCA_OVERRIDE_FEATURE in chrome_dev_conf
def _ensure_local_override_enabled(device: str, force: bool):
if _local_override_enabled(device):
return
util.run([
"ssh",
device,
"--",
f'echo "--enable-features={_CCA_OVERRIDE_FEATURE}"' +
f" >> {_CHROME_DEV_CONF_PATH}",
])
if not force:
prompt = input("Need to restart UI for deploy to take effect, " +
"do it now? (y/N): ").lower()
if prompt != "y":
print(
"Not restarting UI. " +
"`restart ui` on DUT manually for the change to take effect.")
return
util.run(["ssh", device, "--", "restart", "ui"])
# Script to reload all CSS on the page by appending a different search
# parameter to the URL each time this is run. Note that Date.now() has
# milliseconds accuracy, so in practice multiple run of the cca.py deploy
# script will have different search parameter.
_CSS_RELOAD_SCRIPT = """
for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
const url = new URL(link.href);
url.searchParams.set('cca-deploy-refresh', Date.now().toString());
link.href = url.toString();
}
console.log('All CSS reloaded');
"""
def _can_only_reload_css(changed_files: List[str]) -> bool:
for file in changed_files:
# Ignore deployed_version.js since this always change every deploy, and
# doesn't affect anything other than the startup console log and toast.
if file.endswith("/deployed_version.js"):
continue
# Ignore folders.
if file.endswith("/"):
continue
# .css change is okay.
if file.endswith(".css"):
continue
return False
return True
def _reload_cca(device: str, changed_files: List[str]):
try:
reload_script = "document.location.reload()"
if _can_only_reload_css(changed_files):
reload_script = _CSS_RELOAD_SCRIPT
util.run([
"ssh",
device,
"--",
"cca",
"open",
"&&",
"cca",
"eval",
shlex.quote(reload_script),
">",
"/dev/null",
])
except subprocess.CalledProcessError as e:
print("Failed to reload CCA on DUT, "
"please make sure that the DUT is logged in "
"and `cca setup` has been run on DUT.")
# Use a fixed temporary output folder for deploy, so incremental compilation
# works and deploy is faster.
_DEPLOY_OUTPUT_TEMP_DIR = "/tmp/cca-deploy-out"
def _rsync_to_device(device: str,
src: str,
target: str,
*,
extra_arguments: List[str] = []):
"""Returns list of files that are changed."""
cmd = [
"rsync",
"--recursive",
"--inplace",
"--delete",
"--mkpath",
"--times",
# rsync by default use source file permission masked by target file
# system umask while transferring new files, and since workstation
# defaults to have file not readable by others, this makes deployed
# file not readable by Chrome.
# Set --chmod=a+rX to rsync to fix this ('a' so it won't be affected by
# local umask, +r for read and +X for executable bit on folder), and
# set --perms so existing files that might have the wrong permission
# will have their permission fixed.
"--perms",
"--chmod=a+rX",
# Sets rsync output format to %n which prints file path that are
# changed. (By default rsync only copies file that have different size
# or modified time.)
"--out-format=%n",
*extra_arguments,
src,
f"{device}:{target}",
]
output = util.check_output(cmd)
return [os.path.join(target, file) for file in output.splitlines()]
@cli.command(
"deploy",
help="deploy to device",
description=(
"Deploy CCA to device. "
"This script only works if there's no .cc / .grd changes. "
"And please build Chrome at least once before running the command."),
)
@cli.option("board")
@cli.option("device")
@cli.option(
"--force",
help="Don't prompt for restarting Chrome.",
action="store_true",
)
@cli.option(
"--reload",
help="Try reloading CCA window after deploy. "
"Please run `cca setup` on DUT once before using this argument.",
action="store_true",
)
def cmd(board: str, device: str, force: bool, reload: bool):
cca_root = os.getcwd()
os.makedirs(_DEPLOY_OUTPUT_TEMP_DIR, exist_ok=True)
js_out_dir = os.path.join(_DEPLOY_OUTPUT_TEMP_DIR, "js")
build.generate_tsconfig(board)
util.run_node([
"typescript/bin/tsc",
"--outDir",
_DEPLOY_OUTPUT_TEMP_DIR,
"--noEmit",
"false",
# Makes compilation faster
"--incremental",
# For better debugging experience on DUT.
"--inlineSourceMap",
"--inlineSources",
# Makes devtools show TypeScript source with better path
"--sourceRoot",
"/",
# For easier developing / test cycle.
"--noUnusedLocals",
"false",
"--noUnusedParameters",
"false",
])
build.build_preload_images_js(js_out_dir)
# Note that although we always rerun tsc, when the JS inputs are not
# changed, tsc also doesn't change the output file's mtime, so rsync will
# correctly skip those unchanged files.
changed_files = _rsync_to_device(
device,
f"{js_out_dir}/",
f"{_CCA_OVERRIDE_PATH}/js/",
extra_arguments=["--exclude=tsconfig.tsbuildinfo"],
)
for dir in ["css", "images", "views", "sounds"]:
changed_files += _rsync_to_device(
device,
f"{os.path.join(cca_root, dir)}/",
f"{_CCA_OVERRIDE_PATH}/{dir}/",
)
current_time = time.strftime("%F %T%z")
util.run([
"ssh",
device,
"--",
"printf",
"%s",
shlex.quote(
f'export const DEPLOYED_VERSION = "cca.py deploy {current_time}";'
),
">",
f"{_CCA_OVERRIDE_PATH}/js/deployed_version.js",
])
_ensure_local_override_enabled(device, force)
if reload:
_reload_cca(device, changed_files)