336 lines
10 KiB
Bash
Executable File
336 lines
10 KiB
Bash
Executable File
#!/bin/sh
|
|
|
|
set -eu
|
|
|
|
GUI_CORE_SCRIPT_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
|
GUI_ENGINE="$GUI_CORE_SCRIPT_DIR/emacs-gui-verifier.el"
|
|
GUI_RECORDER_SOURCE="$GUI_CORE_SCRIPT_DIR/record-emacs-window.swift"
|
|
GUI_CAPTURE="$GUI_CORE_SCRIPT_DIR/capture-emacs-window.sh"
|
|
GUI_EMACS_APP=${EMACS_APP:-/Applications/Emacs.app}
|
|
GUI_EMACS_BIN="$GUI_EMACS_APP/Contents/MacOS/Emacs"
|
|
GUI_EMACSCLIENT="$GUI_EMACS_APP/Contents/MacOS/bin/emacsclient"
|
|
GUI_CODEX_ROOT=${CODEX_HOME:-${HOME}/.codex}
|
|
GUI_DYNAMIC_SKILL=${EMACS_DYNAMIC_UI_SKILL_DIR:-$GUI_CODEX_ROOT/skills/emacs-dynamic-ui-verification}
|
|
GUI_CHECKPOINT_EL="$GUI_DYNAMIC_SKILL/scripts/capture-checkpoint.el"
|
|
GUI_EVIDENCE_PY="$GUI_DYNAMIC_SKILL/scripts/evidence.py"
|
|
|
|
usage() {
|
|
echo "usage:" >&2
|
|
echo " $0 doctor" >&2
|
|
echo " $0 review RUN_DIR" >&2
|
|
echo " $0 run ADAPTER_EL ENTRY_FUNCTION [--initialize-function FUNCTION] [--run-dir DIR] [--load-path DIR]..." >&2
|
|
exit 2
|
|
}
|
|
|
|
require_file() {
|
|
[ -f "$1" ] || {
|
|
echo "required file is missing: $1" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
doctor() {
|
|
require_file "$GUI_EMACS_BIN"
|
|
require_file "$GUI_EMACSCLIENT"
|
|
require_file "$GUI_ENGINE"
|
|
require_file "$GUI_RECORDER_SOURCE"
|
|
require_file "$GUI_CAPTURE"
|
|
require_file "$GUI_CHECKPOINT_EL"
|
|
require_file "$GUI_EVIDENCE_PY"
|
|
command -v osascript >/dev/null 2>&1
|
|
xcrun --find swiftc >/dev/null 2>&1
|
|
command -v ffmpeg >/dev/null 2>&1
|
|
command -v python3 >/dev/null 2>&1
|
|
echo "EMACS-GUI-VERIFIER DOCTOR PASS"
|
|
}
|
|
|
|
review_run() {
|
|
[ "$#" -eq 1 ] || usage
|
|
GUI_REVIEW_DIR=$1
|
|
require_file "$GUI_REVIEW_DIR/manifest.jsonl"
|
|
require_file "$GUI_REVIEW_DIR/recording.mov"
|
|
python3 "$GUI_EVIDENCE_PY" finalize \
|
|
--run-dir "$GUI_REVIEW_DIR" --temporal-reviewed
|
|
}
|
|
|
|
GUI_DAEMON=""
|
|
GUI_DAEMON_PID=""
|
|
GUI_RECORDER_PID=""
|
|
GUI_DAEMON_STARTED=false
|
|
|
|
valid_pid() {
|
|
case $1 in
|
|
''|*[!0-9]*) return 1 ;;
|
|
*) [ "$1" -gt 1 ] ;;
|
|
esac
|
|
}
|
|
|
|
process_alive() {
|
|
valid_pid "$1" && kill -0 "$1" 2>/dev/null
|
|
}
|
|
|
|
wait_for_exit() {
|
|
GUI_WAIT_PID=$1
|
|
GUI_WAIT_LIMIT=$2
|
|
GUI_WAIT_ATTEMPT=0
|
|
while process_alive "$GUI_WAIT_PID"; do
|
|
[ "$GUI_WAIT_ATTEMPT" -lt "$GUI_WAIT_LIMIT" ] || return 1
|
|
sleep 0.1
|
|
GUI_WAIT_ATTEMPT=$((GUI_WAIT_ATTEMPT + 1))
|
|
done
|
|
}
|
|
|
|
force_owned_exit() {
|
|
GUI_FORCE_PID=$1
|
|
GUI_FORCE_LABEL=$2
|
|
process_alive "$GUI_FORCE_PID" || return 0
|
|
echo "forcing owned $GUI_FORCE_LABEL process to exit: $GUI_FORCE_PID" >&2
|
|
kill -TERM "$GUI_FORCE_PID" 2>/dev/null || return 1
|
|
if ! wait_for_exit "$GUI_FORCE_PID" 20; then
|
|
kill -KILL "$GUI_FORCE_PID" 2>/dev/null || return 1
|
|
wait_for_exit "$GUI_FORCE_PID" 10 || return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
stop_recorder() {
|
|
[ -n "$GUI_RECORDER_PID" ] || return 0
|
|
GUI_RECORDER_STOP_FAILED=0
|
|
if process_alive "$GUI_RECORDER_PID"; then
|
|
if ! kill -TERM "$GUI_RECORDER_PID"; then
|
|
GUI_RECORDER_STOP_FAILED=1
|
|
elif ! wait_for_exit "$GUI_RECORDER_PID" 50; then
|
|
GUI_RECORDER_STOP_FAILED=1
|
|
if ! force_owned_exit "$GUI_RECORDER_PID" "window recorder"; then
|
|
GUI_RECORDER_STOP_FAILED=1
|
|
fi
|
|
fi
|
|
fi
|
|
if ! wait "$GUI_RECORDER_PID" 2>/dev/null; then
|
|
GUI_RECORDER_STOP_FAILED=1
|
|
fi
|
|
GUI_RECORDER_PID=""
|
|
[ "$GUI_RECORDER_STOP_FAILED" -eq 0 ]
|
|
}
|
|
|
|
stop_daemon() {
|
|
[ "$GUI_DAEMON_STARTED" = true ] || return 0
|
|
GUI_DAEMON_STOP_FAILED=0
|
|
if ! "$GUI_EMACSCLIENT" -n -s "$GUI_DAEMON" -e '(kill-emacs 0)' \
|
|
>"$GUI_RUN_DIR/daemon-shutdown.log" 2>&1; then
|
|
GUI_DAEMON_STOP_FAILED=1
|
|
fi
|
|
if ! wait_for_exit "$GUI_DAEMON_PID" 50; then
|
|
GUI_DAEMON_STOP_FAILED=1
|
|
if ! force_owned_exit "$GUI_DAEMON_PID" "Emacs daemon"; then
|
|
GUI_DAEMON_STOP_FAILED=1
|
|
fi
|
|
fi
|
|
GUI_DAEMON_STARTED=false
|
|
GUI_DAEMON_PID=""
|
|
GUI_DAEMON=""
|
|
[ "$GUI_DAEMON_STOP_FAILED" -eq 0 ]
|
|
}
|
|
|
|
cleanup() {
|
|
GUI_CLEANUP_FAILED=0
|
|
if ! stop_recorder; then
|
|
echo "GUI recorder cleanup failed" >&2
|
|
GUI_CLEANUP_FAILED=1
|
|
fi
|
|
if ! stop_daemon; then
|
|
echo "GUI daemon cleanup failed" >&2
|
|
GUI_CLEANUP_FAILED=1
|
|
fi
|
|
[ "$GUI_CLEANUP_FAILED" -eq 0 ]
|
|
}
|
|
|
|
on_exit() {
|
|
GUI_MAIN_STATUS=$?
|
|
trap - EXIT HUP INT TERM
|
|
if ! cleanup; then
|
|
[ "$GUI_MAIN_STATUS" -ne 0 ] || GUI_MAIN_STATUS=1
|
|
fi
|
|
exit "$GUI_MAIN_STATUS"
|
|
}
|
|
|
|
trap on_exit EXIT
|
|
trap 'exit 130' HUP INT TERM
|
|
|
|
activate_emacs() {
|
|
osascript -e "tell application \"System Events\" to set frontmost of first application process whose unix id is $GUI_DAEMON_PID to true"
|
|
GUI_ACTIVATE_ATTEMPTS=0
|
|
while [ "$GUI_ACTIVATE_ATTEMPTS" -lt 30 ]; do
|
|
GUI_FRONTMOST=$(osascript -e \
|
|
'tell application "System Events" to get unix id of first application process whose frontmost is true')
|
|
if [ "$GUI_FRONTMOST" = "$GUI_DAEMON_PID" ]; then
|
|
# macOS fullscreen/Space animations continue after focus changes.
|
|
sleep 1
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
GUI_ACTIVATE_ATTEMPTS=$((GUI_ACTIVATE_ATTEMPTS + 1))
|
|
done
|
|
echo "Emacs did not become the frontmost application" >&2
|
|
exit 1
|
|
}
|
|
|
|
start_recorder() {
|
|
"$GUI_RECORDER" "$GUI_DAEMON_PID" "$GUI_RUN_DIR/recording.mov" \
|
|
"$GUI_RUN_DIR/recorder.ready" \
|
|
>"$GUI_RUN_DIR/recorder.log" 2>&1 &
|
|
GUI_RECORDER_PID=$!
|
|
GUI_RECORDER_ATTEMPTS=0
|
|
while [ "$GUI_RECORDER_ATTEMPTS" -lt 150 ]; do
|
|
if ! kill -0 "$GUI_RECORDER_PID" 2>/dev/null; then
|
|
echo "screen recorder exited before the scenario started" >&2
|
|
exit 1
|
|
fi
|
|
if [ -s "$GUI_RUN_DIR/recorder.ready" ]; then
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
GUI_RECORDER_ATTEMPTS=$((GUI_RECORDER_ATTEMPTS + 1))
|
|
done
|
|
echo "window recorder did not produce a verified frame" >&2
|
|
exit 1
|
|
}
|
|
|
|
parse_run_arguments() {
|
|
[ "$#" -ge 2 ] || usage
|
|
GUI_ADAPTER=$1
|
|
GUI_ENTRY=$2
|
|
shift 2
|
|
GUI_RUN_DIR=""
|
|
GUI_LOAD_PATHS=""
|
|
GUI_INITIALIZE_FUNCTION=""
|
|
while [ "$#" -gt 0 ]; do
|
|
case $1 in
|
|
--initialize-function)
|
|
[ "$#" -ge 2 ] || usage
|
|
GUI_INITIALIZE_FUNCTION=$2
|
|
shift 2
|
|
;;
|
|
--run-dir)
|
|
[ "$#" -ge 2 ] || usage
|
|
GUI_RUN_DIR=$2
|
|
shift 2
|
|
;;
|
|
--load-path)
|
|
[ "$#" -ge 2 ] || usage
|
|
case $2 in *:*)
|
|
echo "load path cannot contain a colon: $2" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
if [ -n "$GUI_LOAD_PATHS" ]; then
|
|
GUI_LOAD_PATHS="$GUI_LOAD_PATHS:$2"
|
|
else
|
|
GUI_LOAD_PATHS=$2
|
|
fi
|
|
shift 2
|
|
;;
|
|
*) usage ;;
|
|
esac
|
|
done
|
|
require_file "$GUI_ADAPTER"
|
|
if [ -z "$GUI_RUN_DIR" ]; then
|
|
GUI_RUN_DIR=$(mktemp -d /private/tmp/emacs-gui-verification.XXXXXX)
|
|
else
|
|
mkdir -p "$GUI_RUN_DIR"
|
|
fi
|
|
if [ -e "$GUI_RUN_DIR/manifest.jsonl" ] || [ -e "$GUI_RUN_DIR/recording.mov" ]; then
|
|
echo "run directory already contains GUI evidence: $GUI_RUN_DIR" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
run_adapter() {
|
|
doctor >/dev/null
|
|
GUI_RECORDER="$GUI_RUN_DIR/record-emacs-window"
|
|
xcrun swiftc -parse-as-library -warnings-as-errors -O \
|
|
"$GUI_RECORDER_SOURCE" -o "$GUI_RECORDER"
|
|
export ETAF_GUI_RUN_DIR="$GUI_RUN_DIR"
|
|
export ETAF_GUI_LOAD_PATHS="$GUI_LOAD_PATHS"
|
|
export ETAF_GUI_ENTRY="$GUI_ENTRY"
|
|
export ETAF_GUI_INITIALIZE_FUNCTION="$GUI_INITIALIZE_FUNCTION"
|
|
export SCREENCAPTURE="$GUI_CAPTURE"
|
|
GUI_DAEMON="emacs-gui-verify-$$"
|
|
"$GUI_EMACS_BIN" -Q --daemon="$GUI_DAEMON" \
|
|
--eval '(setq native-comp-jit-compilation nil load-prefer-newer t)'
|
|
GUI_DAEMON_STARTED=true
|
|
GUI_DAEMON_PID=$(
|
|
"$GUI_EMACSCLIENT" -n -s "$GUI_DAEMON" -e '(emacs-pid)'
|
|
)
|
|
valid_pid "$GUI_DAEMON_PID" || {
|
|
echo "Emacs daemon returned an invalid process id: $GUI_DAEMON_PID" >&2
|
|
exit 1
|
|
}
|
|
"$GUI_EMACSCLIENT" -n -s "$GUI_DAEMON" -c -e \
|
|
"(progn
|
|
(dolist (path (split-string (or (getenv \"ETAF_GUI_LOAD_PATHS\") \"\") path-separator t))
|
|
(add-to-list 'load-path path))
|
|
(load \"$GUI_CHECKPOINT_EL\" nil nil t)
|
|
(load \"$GUI_ENGINE\" nil nil t)
|
|
(load \"$GUI_ADAPTER\" nil nil t)
|
|
(when (fboundp 'tool-bar-mode) (tool-bar-mode -1))
|
|
(when (fboundp 'menu-bar-mode) (menu-bar-mode -1))
|
|
(when (fboundp 'scroll-bar-mode) (scroll-bar-mode -1))
|
|
(setenv \"ETAF_GUI_CAPTURE_PID\" (number-to-string (emacs-pid)))
|
|
(set-frame-parameter nil 'fullscreen 'fullboth)
|
|
(when-let* ((initialize (getenv \"ETAF_GUI_INITIALIZE_FUNCTION\"))
|
|
((> (length initialize) 0)))
|
|
(funcall (intern initialize)))
|
|
(select-frame-set-input-focus (selected-frame))
|
|
(message nil)
|
|
(redisplay t)
|
|
t)" >"$GUI_RUN_DIR/bootstrap.out"
|
|
activate_emacs
|
|
start_recorder
|
|
"$GUI_EMACSCLIENT" -n -s "$GUI_DAEMON" -e \
|
|
'(funcall (intern (getenv "ETAF_GUI_ENTRY")))' \
|
|
>"$GUI_RUN_DIR/scenario.out"
|
|
stop_recorder
|
|
require_file "$GUI_RUN_DIR/recording.mov"
|
|
[ -s "$GUI_RUN_DIR/recording.mov" ] || {
|
|
echo "screen recording is empty" >&2
|
|
exit 1
|
|
}
|
|
stop_daemon
|
|
|
|
set +e
|
|
python3 "$GUI_EVIDENCE_PY" finalize --run-dir "$GUI_RUN_DIR" \
|
|
>"$GUI_RUN_DIR/finalize.out" 2>&1
|
|
GUI_FINALIZE_STATUS=$?
|
|
set -e
|
|
cat "$GUI_RUN_DIR/finalize.out"
|
|
if [ "$GUI_FINALIZE_STATUS" -eq 1 ]; then
|
|
echo "GUI verification assertions failed: $GUI_RUN_DIR" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$GUI_FINALIZE_STATUS" -ne 2 ]; then
|
|
echo "fresh capture unexpectedly bypassed temporal review" >&2
|
|
exit 1
|
|
fi
|
|
echo "EMACS-GUI-VERIFIER CAPTURE COMPLETE verdict=INCOMPLETE entry=$GUI_ENTRY run-dir=$GUI_RUN_DIR"
|
|
}
|
|
|
|
[ "$#" -ge 1 ] || usage
|
|
GUI_COMMAND=$1
|
|
shift
|
|
|
|
case $GUI_COMMAND in
|
|
doctor)
|
|
[ "$#" -eq 0 ] || usage
|
|
doctor
|
|
;;
|
|
review)
|
|
review_run "$@"
|
|
;;
|
|
run)
|
|
parse_run_arguments "$@"
|
|
run_adapter
|
|
;;
|
|
*) usage ;;
|
|
esac
|