#!/usr/bin/env bash set -Eeuo pipefail MODE="${1:-quick}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}" ARTIFACT_DIR="${ARTIFACT_DIR:-${REPO_ROOT}/artifacts}" mkdir -p "${ARTIFACT_DIR}" LOG_FILE="${ARTIFACT_DIR}/preflight-${MODE}.log" STATE_FILE="${ARTIFACT_DIR}/five-reboots.jsonl" SOAK_FILE="${ARTIFACT_DIR}/soak-30m.json" FAILURES=0 COUCH_USER="couch" RUNTIME_CONFIG="/home/couch/.config/couchd/runtime.json" record() { local status="$1" shift printf '%s %s\n' "${status}" "$*" | tee -a "${LOG_FILE}" if [[ "${status}" == "FAIL" ]]; then FAILURES=$((FAILURES + 1)) fi } run_as_couch() { if [[ "${EUID}" -eq 0 ]]; then runuser -u "${COUCH_USER}" -- "$@" elif [[ "$(id -un)" == "${COUCH_USER}" ]]; then "$@" else return 1 fi } runtime_field() { local field="$1" python3 - <<'PY' "${RUNTIME_CONFIG}" "${field}" import json import sys from pathlib import Path path = Path(sys.argv[1]) field = sys.argv[2] data = json.loads(path.read_text(encoding="utf-8")) value = data.get(field) if value is None: raise SystemExit(1) print(value) PY } controller_snapshot() { local address address="$(runtime_field xbox_bluetooth_address 2>/dev/null || true)" if [[ -z "${address}" ]]; then printf '{"address": null, "paired": false, "trusted": false, "connected": false}' return 0 fi local output output="$(bluetoothctl info "${address}" 2>/dev/null || true)" python3 - <<'PY' "${address}" "${output}" import json import sys address = sys.argv[1] text = sys.argv[2] print(json.dumps({ "address": address, "paired": "Paired: yes" in text, "trusted": "Trusted: yes" in text, "connected": "Connected: yes" in text, })) PY } status_json() { python3 -m couchd --config "${RUNTIME_CONFIG}" status } finish() { if [[ "${FAILURES}" -gt 0 ]]; then exit 1 fi } quick_mode() { : >"${LOG_FILE}" if id "${COUCH_USER}" >/dev/null 2>&1; then record PASS "couch user exists" else record FAIL "couch user missing" fi if [[ "$(stat -c '%a' /home/jackmhny 2>/dev/null || true)" == "700" ]]; then record PASS "/home/jackmhny mode is 0700" else record FAIL "/home/jackmhny mode is not 0700" fi if run_as_couch bash -lc 'test ! -r /home/jackmhny && test ! -x /home/jackmhny'; then record PASS "couch cannot read or traverse /home/jackmhny" else record FAIL "couch can read or traverse /home/jackmhny or couch access test could not run" fi if id -nG "${COUCH_USER}" 2>/dev/null | tr ' ' '\n' | grep -Eq '^(sudo|adm)$'; then record FAIL "couch is still in sudo/adm" else record PASS "couch is not in sudo/adm" fi grep -Fq 'autologin-user=couch' /etc/lightdm/lightdm.conf.d/50-couchos.conf 2>/dev/null && \ grep -Fq 'autologin-session=xfce' /etc/lightdm/lightdm.conf.d/50-couchos.conf 2>/dev/null \ && record PASS "LightDM autologin is set to couch/XFCE" \ || record FAIL "LightDM autologin is not set to couch/XFCE" grep -Fq 'HandleLidSwitchExternalPower=ignore' /etc/systemd/logind.conf.d/50-couchos.conf 2>/dev/null \ && record PASS "logind ignores lid close on AC" \ || record FAIL "logind lid policy on AC is not configured" grep -Fq 'use_compositing" type="bool" value="false' /home/couch/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml 2>/dev/null \ && record PASS "XFCE compositing disabled" \ || record FAIL "XFCE compositing not disabled" grep -Fq 'do-not-disturb" type="bool" value="true' /home/couch/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-notifyd.xml 2>/dev/null \ && record PASS "notifications DND enabled" \ || record FAIL "notifications DND not enabled" grep -Fq 'dpms-enabled" type="bool" value="false' /home/couch/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-power-manager.xml 2>/dev/null \ && record PASS "power blanking/suspend disabled" \ || record FAIL "power blanking/suspend not disabled" if [[ -f "${RUNTIME_CONFIG}" ]]; then record PASS "runtime config present" else record FAIL "runtime config missing" fi if [[ -f /home/couch/.config/couchd/token && "$(stat -c '%a' /home/couch/.config/couchd/token)" == "600" ]]; then if [[ "$(tr -d '\r\n' /dev/null | awk '{print $4}' | grep -Fxq '100.64.0.15:8765'; then record PASS "couchd listens only on 100.64.0.15:8765" else record FAIL "couchd listener is not exactly 100.64.0.15:8765" fi local controller controller="$(controller_snapshot)" if python3 - <<'PY' "${controller}" import json import sys snapshot = json.loads(sys.argv[1]) if snapshot["address"] and snapshot["paired"] and snapshot["trusted"] and snapshot["connected"]: raise SystemExit(0) raise SystemExit(1) PY then record PASS "controller is paired/trusted/connected" else record FAIL "controller is not paired/trusted/connected" fi local status_payload if status_payload="$(status_json 2>/dev/null)"; then record PASS "couchd status command succeeded" if python3 - <<'PY' "${status_payload}" import json import sys payload = json.loads(sys.argv[1]) mode = payload.get("display", {}).get("mode") name = payload.get("display", {}).get("name") if name == "HDMI-1" and mode == "1920x1080@60.00": raise SystemExit(0) raise SystemExit(1) PY then record PASS "HDMI-1 active mode is 1920x1080@60.00" else record FAIL "HDMI-1 active mode is not 1920x1080@60.00" fi else record FAIL "couchd status command failed" fi if pactl info 2>/dev/null | grep -Eiq '^Default Sink: .*hdmi'; then record PASS "default audio sink is HDMI" else record FAIL "default audio sink is not HDMI" fi local couch_uid couch_uid="$(id -u "${COUCH_USER}" 2>/dev/null || true)" if [[ -n "${couch_uid}" ]] && XDG_RUNTIME_DIR="/run/user/${couch_uid}" run_as_couch systemctl --user is-active couchd.service >/dev/null 2>&1; then record PASS "couchd user service is active" else record FAIL "couchd user service is not active" fi if [[ -f /home/couch/.config/autostart/couchd.desktop && -f /home/couch/.config/autostart/couch-display-audio.desktop ]]; then record PASS "XFCE autostart entries exist for couch session units" else record FAIL "XFCE autostart entries for couch session units are missing" fi finish } five_reboots_mode() { : >"${LOG_FILE}" local boot_id boot_id="$(cat /proc/sys/kernel/random/boot_id)" local controller controller="$(controller_snapshot)" local status_payload status_payload="$(status_json 2>/dev/null || printf '{}')" python3 - <<'PY' "${STATE_FILE}" "${boot_id}" "${controller}" "${status_payload}" import json import sys from pathlib import Path path = Path(sys.argv[1]) boot_id = sys.argv[2] controller = json.loads(sys.argv[3]) status_payload = json.loads(sys.argv[4]) existing = [] if path.exists(): existing = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] if not any(item.get("boot_id") == boot_id for item in existing): existing.append({ "boot_id": boot_id, "timestamp": __import__("datetime").datetime.now().isoformat(timespec="seconds"), "controller": controller, "display_mode": status_payload.get("display", {}).get("mode"), "operator_login_observed": "SKIP", "operator_fps_observed": "SKIP", }) path.write_text("\n".join(json.dumps(item, sort_keys=True) for item in existing) + ("\n" if existing else ""), encoding="utf-8") print(len(existing)) PY local count count="$(python3 - <<'PY' "${STATE_FILE}" import json import sys from pathlib import Path path = Path(sys.argv[1]) print(len([json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()])) PY )" record PASS "recorded reboot observation for boot ${boot_id} (${count}/5 unique boots)" record SKIP "operator_login_observed remains SKIP until human session confirmation is recorded" record SKIP "operator_fps_observed remains SKIP until human 60 FPS confirmation is recorded" finish } soak_30m_start() { : >"${LOG_FILE}" python3 - <<'PY' "${SOAK_FILE}" "$(cat /proc/sys/kernel/random/boot_id)" "$(controller_snapshot)" import json import sys from datetime import datetime from pathlib import Path Path(sys.argv[1]).write_text(json.dumps({ "start_boot_id": sys.argv[2], "started_at": datetime.now().isoformat(timespec="seconds"), "controller_start": json.loads(sys.argv[3]), "operator_fps_observed": "SKIP", }, sort_keys=True) + "\n", encoding="utf-8") PY record PASS "30-minute soak started" record SKIP "operator_fps_observed remains SKIP until a human records it after the session" finish } soak_30m_finish() { : >"${LOG_FILE}" if [[ ! -f "${SOAK_FILE}" ]]; then record FAIL "soak-30m-start was not run" finish fi python3 - <<'PY' "${SOAK_FILE}" "$(cat /proc/sys/kernel/random/boot_id)" "$(controller_snapshot)" "${ARTIFACT_DIR}/soak-30m-finish.json" import json import sys from datetime import datetime from pathlib import Path start = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) finished_at = datetime.now() started_at = datetime.fromisoformat(start["started_at"]) elapsed = int((finished_at - started_at).total_seconds()) payload = { **start, "finish_boot_id": sys.argv[2], "finished_at": finished_at.isoformat(timespec="seconds"), "elapsed_seconds": elapsed, "controller_finish": json.loads(sys.argv[3]), } Path(sys.argv[4]).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(elapsed) PY local elapsed elapsed="$(python3 - <<'PY' "${ARTIFACT_DIR}/soak-30m-finish.json" import json import sys from pathlib import Path print(json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["elapsed_seconds"]) PY )" if [[ "${elapsed}" -ge 1800 ]]; then record PASS "30-minute soak elapsed ${elapsed} seconds" else record FAIL "30-minute soak only ran ${elapsed} seconds" fi record SKIP "operator_fps_observed remains SKIP until a human records the observed FPS stability" finish } case "${MODE}" in quick) quick_mode ;; five-reboots) five_reboots_mode ;; soak-30m-start) soak_30m_start ;; soak-30m-finish) soak_30m_finish ;; *) printf 'unknown mode: %s\n' "${MODE}" >&2 exit 1 ;; esac