diff --git a/bin/omarchy-theme-set-browser b/bin/omarchy-theme-set-browser index 612e586f..d04be45d 100755 --- a/bin/omarchy-theme-set-browser +++ b/bin/omarchy-theme-set-browser @@ -12,25 +12,83 @@ if [[ -f $CHROMIUM_THEME ]]; then THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)") fi -refresh_running_browser() { - local process="$1" - local command="$2" - local pgrep_args="${3:--x}" +# Tests override this with a temporary directory list; production uses the +# fixed trust-root set from browser-policy.sh. +POLICY_DIRS=("${BROWSER_POLICY_MANAGED_DIRS[@]}") +if [[ -n ${OMARCHY_BROWSER_POLICY_DIRS:-} ]]; then + POLICY_DIRS=() + while IFS= read -r dir; do + POLICY_DIRS+=("$dir") + done <<< "$OMARCHY_BROWSER_POLICY_DIRS" +fi - if omarchy-cmd-present "$command" && pgrep $pgrep_args "$process" >/dev/null; then - "$command" --refresh-platform-policy --no-startup-window &>/dev/null - fi +# A managed color.json is already correct only when it is a regular, root-owned +# file with mode 0644 containing exactly the canonical JSON for the requested +# color. +file_is_canonical() { + local file=$1 + [[ -f $file && ! -L $file ]] || return 1 + [[ $(<"$file") == "$expected_json" ]] || return 1 + [[ $(stat -c '%U:%G %a' "$file") == "root:root 644" ]] } -failed=0 -omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1 +# Skip the privileged policy write and browser refreshes entirely when every +# existing managed color.json is already canonical. The writer only touches +# directories that exist, so a browser installed since then has no color.json +# yet and takes the write. +expected_json='{"BrowserThemeColor": "'$THEME_HEX_COLOR'", "BrowserColorScheme": "device"}' +saw_any=false +all_unchanged=true +for dir in "${POLICY_DIRS[@]}"; do + [[ -d $dir && ! -L $dir ]] || continue + saw_any=true -refresh_running_browser chromium chromium -refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome -refresh_running_browser msedge microsoft-edge-stable -refresh_running_browser brave brave + if ! file_is_canonical "$dir/color.json"; then + all_unchanged=false + break + fi +done +$saw_any && $all_unchanged && exit 0 + +failed=0 +# A script's background job reads /dev/null unless told otherwise, which would +# turn the writer's terminal sudo prompt into a pkexec dialog. Keep stdin. +omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" <&0 & +policy_pid=$! + +# While the policy writes, find the running browsers with one process table +# read. A pgrep per browser rescans /proc each time, ~40ms apiece. +declare -A running_names=() +running_args="" +while read -r name args; do + running_names[$name]=1 + running_args+="$args"$'\n' +done < <(ps -eo comm=,args=) + +# Chrome installs as either binary name, so take whichever exists. +chrome=google-chrome-stable +omarchy-cmd-present "$chrome" || chrome=google-chrome + +browsers_to_refresh=() +for browser in chromium:chromium "chrome:$chrome" msedge:microsoft-edge-stable brave:brave; do + [[ -n ${running_names[${browser%%:*}]:-} ]] && omarchy-cmd-present "${browser#*:}" && browsers_to_refresh+=("${browser#*:}") +done # Match on the binary path: the running process is named plain "brave", and a -# bare -f brave-origin pattern would also match the installer's own terminal. -refresh_running_browser /opt/brave-origin-bin/ brave-origin -f +# bare brave-origin pattern would also match the installer's own terminal. +[[ $running_args == *"/opt/brave-origin-bin/"* ]] && omarchy-cmd-present brave-origin && browsers_to_refresh+=(brave-origin) + +wait "$policy_pid" || failed=1 + +# Refresh each running browser in parallel, so one slow launch doesn't +# serialize the rest. +refresh_pids=() +for command in "${browsers_to_refresh[@]}"; do + "$command" --refresh-platform-policy --no-startup-window &>/dev/null & + refresh_pids+=("$!") +done + +for pid in "${refresh_pids[@]}"; do + wait "$pid" +done exit "$failed" diff --git a/test/shell.d/browser-policy-sudoers-test.sh b/test/shell.d/browser-policy-sudoers-test.sh index 3ddf3757..e9c3244d 100755 --- a/test/shell.d/browser-policy-sudoers-test.sh +++ b/test/shell.d/browser-policy-sudoers-test.sh @@ -133,6 +133,13 @@ fi setter_bin="$test_tmp/setter-bin" mkdir -p "$setter_bin" +# The setter now skips its privileged policy write when every managed +# color.json already matches. Give it a fixed out-of-range fixture so every +# theme below takes the writer path. +policy_tmp="$test_tmp/policies" +mkdir -p "$policy_tmp/a" +printf '{"BrowserThemeColor": "#ff0000", "BrowserColorScheme": "device"}\n' > "$policy_tmp/a/color.json" + cat >"$setter_bin/omarchy-theme-set-browser-policy" <<'SH' #!/bin/bash printf '%s\n' "$*" >"$COLOR_LOG" @@ -158,6 +165,7 @@ color_for_theme() { fi HOME="$setter_home" COLOR_LOG="$test_tmp/color" PATH="$setter_bin:$stub_bin:$PATH" \ + OMARCHY_BROWSER_POLICY_DIRS="$policy_tmp/a" \ OMARCHY_PATH="$ROOT" bash "$setter" /dev/null 2>&1 || true cat "$test_tmp/color" } diff --git a/test/shell.d/theme-set-browser-test.sh b/test/shell.d/theme-set-browser-test.sh new file mode 100755 index 00000000..1a84302e --- /dev/null +++ b/test/shell.d/theme-set-browser-test.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +TMPDIR=$(mktemp -d) +TMP_BIN="$TMPDIR/bin" +CALL_LOG="$TMPDIR/calls.txt" +trap 'rm -rf "$TMPDIR"' EXIT + +mkdir -p "$TMP_BIN" + +# No browser is running unless a case says so, so the policy cases never +# refresh a real browser on the host. +cat > "$TMP_BIN/ps" <<'FAKE' +#!/bin/bash +exit 0 +FAKE +chmod +x "$TMP_BIN/ps" + +# Stub the privileged writer so we can observe whether the setter calls it. +cat > "$TMP_BIN/omarchy-theme-set-browser-policy" <<'FAKE' +#!/bin/bash +printf '%s\n' "$*" >> "${CALL_LOG:?}" +exit 0 +FAKE +chmod +x "$TMP_BIN/omarchy-theme-set-browser-policy" + +# A policy directory that already carries the target color, so the setter can +# skip everything. +policy_tmp="$TMPDIR/policies" +mkdir -p "$policy_tmp" +printf '{"BrowserThemeColor": "#1c2027", "BrowserColorScheme": "device"}\n' > "$policy_tmp/color.json" + +# A test cannot make a root-owned file, so stat reports the owner the writer +# leaves behind, or a user-owned one. +stat_bin="$TMPDIR/stat-bin" +mkdir -p "$stat_bin" +cat >"$stat_bin/stat" <<'FAKE' +#!/bin/bash +printf '%s\n' "${STAT_OWNER:?}" +FAKE +chmod +x "$stat_bin/stat" + +# No theme file -> fallback color #1c2027, which matches the fixture. +HOME="$TMPDIR" PATH="$stat_bin:$TMP_BIN:$ROOT/bin:$PATH" OMARCHY_PATH="$ROOT" STAT_OWNER="root:root 644" \ + OMARCHY_BROWSER_POLICY_DIRS="$policy_tmp" CALL_LOG="$CALL_LOG" \ + bash "$ROOT/bin/omarchy-theme-set-browser" >/dev/null + +if [[ -e $CALL_LOG ]]; then + fail "setter skipped the privileged write when color.json already matches" +fi +pass "setter skips the privileged write when color.json already matches" + +# The same color in a file the writer did not leave behind is not trusted. +HOME="$TMPDIR" PATH="$stat_bin:$TMP_BIN:$ROOT/bin:$PATH" OMARCHY_PATH="$ROOT" STAT_OWNER="user:user 666" \ + OMARCHY_BROWSER_POLICY_DIRS="$policy_tmp" CALL_LOG="$CALL_LOG" \ + bash "$ROOT/bin/omarchy-theme-set-browser" >/dev/null + +if [[ ! -e $CALL_LOG ]]; then + fail "setter rewrites a matching color.json that is not root-owned 0644" +fi +pass "setter rewrites a matching color.json that is not root-owned 0644" + +# Now point it at a policy dir whose color.json does not match. +rm -f "$CALL_LOG" +printf '{"BrowserThemeColor": "#ff0000"}\n' > "$policy_tmp/color.json" + +HOME="$TMPDIR" PATH="$TMP_BIN:$ROOT/bin:$PATH" OMARCHY_PATH="$ROOT" \ + OMARCHY_BROWSER_POLICY_DIRS="$policy_tmp" CALL_LOG="$CALL_LOG" \ + bash "$ROOT/bin/omarchy-theme-set-browser" >/dev/null + +if [[ ! -e $CALL_LOG ]]; then + fail "setter invoked the privileged writer when color.json mismatched" +fi +pass "setter invokes the privileged writer when color.json mismatches" + +# No managed policy dirs at all must not vacuously skip the writer. +rm -f "$CALL_LOG" + +HOME="$TMPDIR" PATH="$TMP_BIN:$ROOT/bin:$PATH" OMARCHY_PATH="$ROOT" \ + OMARCHY_BROWSER_POLICY_DIRS="$TMPDIR/nonexistent" CALL_LOG="$CALL_LOG" \ + bash "$ROOT/bin/omarchy-theme-set-browser" >/dev/null + +if [[ ! -e $CALL_LOG ]]; then + fail "setter invoked the privileged writer when no managed dirs exist" +fi +pass "setter invokes the privileged writer when no managed dirs exist" + +# Refreshes go to exactly the running browsers. ps is stubbed with a process +# table; each browser command logs its refresh. +browser_bin="$TMPDIR/browser-bin" +mkdir -p "$browser_bin" +cat >"$browser_bin/ps" <<'FAKE' +#!/bin/bash +printf '%s\n' \ + 'chromium /usr/lib/chromium/chromium --type=renderer' \ + 'chrome /opt/google/chrome/chrome' \ + 'brave /opt/brave-origin-bin/brave --profile' \ + 'bash bash -c pgrep -f brave' +FAKE +for command in chromium google-chrome microsoft-edge-stable brave brave-origin; do + printf '#!/bin/bash\nprintf "%%s\\n" "%s" >>"$REFRESH_LOG"\n' "$command" >"$browser_bin/$command" +done +# Only the stubs count as installed, so a real browser on the host, such as +# google-chrome-stable, is neither preferred nor launched. +cat >"$browser_bin/omarchy-cmd-present" <<'FAKE' +#!/bin/bash +[[ -x ${BROWSER_BIN:?}/$1 ]] +FAKE +chmod +x "$browser_bin"/* + +printf '{"BrowserThemeColor": "#ff0000"}\n' >"$policy_tmp/color.json" +refresh_log="$TMPDIR/refreshes" +HOME="$TMPDIR" PATH="$browser_bin:$TMP_BIN:$ROOT/bin:$PATH" OMARCHY_PATH="$ROOT" \ + OMARCHY_BROWSER_POLICY_DIRS="$policy_tmp" CALL_LOG="$CALL_LOG" REFRESH_LOG="$refresh_log" BROWSER_BIN="$browser_bin" \ + bash "$ROOT/bin/omarchy-theme-set-browser" >/dev/null + +refreshed=$(sort "$refresh_log" | tr '\n' ' ') +# Chrome running as plain google-chrome is refreshed through that name; Edge is +# installed but not running; brave-origin matches on its binary path, and the +# running "brave" process refreshes plain brave too. +[[ $refreshed == "brave brave-origin chromium google-chrome " ]] || + fail "setter refreshes exactly the running browsers (got: $refreshed)" +pass "setter refreshes exactly the running browsers" + +grep -q '<&0 &$' "$ROOT/bin/omarchy-theme-set-browser" || + fail "setter keeps stdin for the backgrounded policy writer" +pass "setter keeps stdin for the backgrounded policy writer"