#!/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
|