#!/bin/bash # Round01 system-critical-window QA. It verifies Quick Switch stays below # rescue system window levels and closes when a system-critical window is detected. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=build-output-paths.sh source "$SCRIPT_DIR/build-output-paths.sh" APP="$BUILD_CURRENT_APP" REPORT="$BUILD_REPORT_ROOT/round01-system-critical-report.json" LOCK_DIR="$BUILD_LOCK_ROOT/round1-system-critical-fixture-qa.lock" FIXTURE_APP_COUNT="${ALIGNER_ROUND1_SYSTEM_CRITICAL_FIXTURE_APP_COUNT:-8}" FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_SYSTEM_CRITICAL_WINDOWS_PER_APP:-4}" REPORT_WAIT="${ALIGNER_ROUND1_SYSTEM_CRITICAL_REPORT_WAIT:-8.0}" DEBUG_AFTER="${ALIGNER_ROUND1_SYSTEM_CRITICAL_AFTER:-1.5}" APP_PID="" LOCK_HELD=0 fail() { echo "Round01 system-critical fixture QA failed: $*" >&2 exit 1 } acquire_lock() { if ! mkdir "$LOCK_DIR" 2>/dev/null; then fail "another round1-system-critical-fixture-qa.sh appears to be running; run packaging QA scripts serially" fi LOCK_HELD=1 } release_lock() { if [ "$LOCK_HELD" -eq 1 ]; then rmdir "$LOCK_DIR" 2>/dev/null || true LOCK_HELD=0 fi } aligner_pids_for_current_app() { ps -axo pid=,args= | while read -r pid command; do case "$command" in "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;; esac done } stop_current_aligner() { for pid in $(aligner_pids_for_current_app); do kill "$pid" 2>/dev/null || true done for _ in {1..30}; do [ -z "$(aligner_pids_for_current_app)" ] && return sleep 0.1 done fail "current Aligner app did not exit before QA" } cleanup() { release_lock if [ -n "${APP_PID:-}" ]; then kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true APP_PID="" fi stop_current_aligner } wait_for_visible_report() { /usr/bin/python3 - "$REPORT" "$REPORT_WAIT" <<'PY' import json import sys import time path = sys.argv[1] timeout = float(sys.argv[2]) deadline = time.monotonic() + timeout last_report = None while time.monotonic() < deadline: try: with open(path, "r", encoding="utf-8") as file: report = json.load(file) last_report = report if report.get("quickSwitchVisible") is True and report.get("overlayBelowRescueSystemWindows") is True: sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.1) if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print(f"visible report did not become ready within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } wait_for_closed_report() { /usr/bin/python3 - "$REPORT" "$REPORT_WAIT" <<'PY' import json import sys import time path = sys.argv[1] timeout = float(sys.argv[2]) deadline = time.monotonic() + timeout last_report = None while time.monotonic() < deadline: try: with open(path, "r", encoding="utf-8") as file: report = json.load(file) last_report = report if ( report.get("quickSwitchVisible") is False and report.get("lastSystemCriticalAction") == "closeOverlay" and report.get("lastDismissReason") == "systemCriticalWindow" ): sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.1) if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print(f"closed system-critical report did not become ready within {timeout:.1f}s", file=sys.stderr) sys.exit(1) PY } assert_report() { /usr/bin/python3 - "$REPORT" <<'PY' import json import sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as file: report = json.load(file) def require(condition, message): if not condition: print(message, file=sys.stderr) print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) sys.exit(1) require(report.get("quickSwitchVisible") is False, "Quick Switch must be closed after system critical detection") require(report.get("overlayBelowRescueSystemWindows") is True, "overlay level must stay below rescue system windows") require(report.get("lastSystemCriticalAction") == "closeOverlay", "system critical action must close overlay") require(report.get("lastSystemCriticalMonitorSource") == "debug", "debug system critical monitor source must be recorded") require(report.get("lastDismissReason") == "systemCriticalWindow", "dismiss reason must record system critical window") require(report.get("lastSystemCriticalWindowCount") == 1, "debug system critical fixture must report one critical window") require(report.get("systemCriticalDetectionCount") == 1, "system critical detection should fire once") require("SecurityAgent" in report.get("lastSystemCriticalWindowOwners", []), "critical window owner must be recorded") print(json.dumps({ "quickSwitchVisible": report.get("quickSwitchVisible"), "overlayLevel": report.get("overlayLevel"), "overlayMainMenuLevel": report.get("overlayMainMenuLevel"), "lastSystemCriticalAction": report.get("lastSystemCriticalAction"), "lastDismissReason": report.get("lastDismissReason"), "lastSystemCriticalWindowOwners": report.get("lastSystemCriticalWindowOwners") }, indent=2, ensure_ascii=False)) PY } trap cleanup EXIT acquire_lock stop_current_aligner "$SCRIPT_DIR/package-app.sh" >&2 rm -f "$REPORT" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-fixture-app-count="$FIXTURE_APP_COUNT" \ --round01-fixture-windows-per-app="$FIXTURE_WINDOWS_PER_APP" \ --round01-disable-screenshot-refresh \ --round01-debug-system-critical-after="$DEBUG_AFTER" \ --round01-quick-switch-report="$REPORT" & APP_PID=$! wait_for_visible_report swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2 wait_for_closed_report swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2 sleep 0.5 swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2 assert_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true APP_PID="" stop_current_aligner release_lock trap - EXIT