feat: build CouchOS Melee appliance
Some checks failed
ci / verify (push) Has been cancelled

This commit is contained in:
jackmhny
2026-08-03 02:52:28 +02:00
commit 04ef2a0d74
42 changed files with 3671 additions and 0 deletions

135
scripts/backup_restore.sh Executable file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -Eeuo pipefail
MODE="${1:-}"
ARCHIVE_PATH="${2:-}"
COUCH_HOME="${COUCH_HOME:-/home/couch}"
STAMP="$(date +%Y%m%d-%H%M%S)"
usage() {
cat <<'EOF'
Usage:
./scripts/backup_restore.sh backup [archive.tar.gz]
./scripts/backup_restore.sh restore <archive.tar.gz>
EOF
}
if [[ -z "${MODE}" ]]; then
usage >&2
exit 1
fi
build_backup_manifest() {
python3 - <<'PY' "${COUCH_HOME}"
import os
import sys
from pathlib import Path
couch_home = Path(sys.argv[1])
roots = [
Path(".config/couchd/runtime.json"),
Path(".config/dolphin-emu"),
Path(".config/Slippi"),
Path(".local/share/dolphin-emu"),
Path("Slippi"),
]
excluded_dirs = tuple(
item.lower()
for item in (
".config/Slippi Launcher",
".local/share/accounts",
".local/share/sessions",
"Slippi/Replay",
"Slippi/Replays",
)
)
excluded_suffixes = (".slp", ".iso", ".gcm", ".wbfs", ".nkit.iso")
for root in roots:
source = couch_home / root
if not source.exists():
continue
if source.is_file():
print(str(root), end="\0")
continue
for current_root, dir_names, file_names in os.walk(source):
rel_root = Path(current_root).relative_to(couch_home)
dir_names[:] = [
name
for name in dir_names
if str(rel_root / name).lower() not in excluded_dirs
]
for file_name in file_names:
rel_path = rel_root / file_name
rel_lower = str(rel_path).lower()
if any(rel_lower == item or rel_lower.startswith(f"{item}/") for item in excluded_dirs):
continue
if rel_lower.endswith(excluded_suffixes):
continue
print(str(rel_path), end="\0")
PY
}
validate_restore_archive() {
python3 - <<'PY' "${ARCHIVE_PATH}" "${COUCH_HOME}"
import sys
import tarfile
from pathlib import Path
archive_path = Path(sys.argv[1])
couch_home = Path(sys.argv[2]).resolve(strict=False)
with tarfile.open(archive_path, "r:*") as archive:
for member in archive.getmembers():
member_path = Path(member.name)
if member_path.is_absolute():
raise SystemExit(f"unsafe archive entry: {member.name}")
if any(part == ".." for part in member_path.parts):
raise SystemExit(f"unsafe archive entry: {member.name}")
if member.isdev():
raise SystemExit(f"unsafe archive entry: {member.name}")
target_path = (couch_home / member_path).resolve(strict=False)
if couch_home != target_path and couch_home not in target_path.parents:
raise SystemExit(f"unsafe archive entry: {member.name}")
if member.issym() or member.islnk():
link_target = Path(member.linkname)
if link_target.is_absolute():
raise SystemExit(f"unsafe archive entry: {member.name}")
resolved_link = (target_path.parent / link_target).resolve(strict=False)
if couch_home != resolved_link and couch_home not in resolved_link.parents:
raise SystemExit(f"unsafe archive entry: {member.name}")
PY
}
case "${MODE}" in
backup)
ARCHIVE_PATH="${ARCHIVE_PATH:-${COUCH_HOME}/Slippi/couchos-backup-${STAMP}.tar.gz}"
umask 077
mapfile -d '' manifest < <(build_backup_manifest)
if [[ "${#manifest[@]}" -eq 0 ]]; then
printf 'no backup sources found under %s\n' "${COUCH_HOME}" >&2
exit 1
fi
tar -C "${COUCH_HOME}" -czf "${ARCHIVE_PATH}" \
--exclude='.config/couchd/token' \
"${manifest[@]}"
chmod 0600 "${ARCHIVE_PATH}"
printf 'backup written to %s\n' "${ARCHIVE_PATH}"
;;
restore)
if [[ -z "${ARCHIVE_PATH}" || ! -f "${ARCHIVE_PATH}" ]]; then
printf 'restore archive is required\n' >&2
exit 1
fi
if ! validation_error="$(validate_restore_archive 2>&1)"; then
printf '%s\n' "${validation_error}" >&2
exit 1
fi
tar -C "${COUCH_HOME}" -xzf "${ARCHIVE_PATH}"
printf 'restored from %s\n' "${ARCHIVE_PATH}"
;;
*)
usage >&2
exit 1
;;
esac

62
scripts/check.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
run_step() {
local label="$1"
shift
printf '[check] %s\n' "${label}"
"$@"
}
secret_scan() {
python3 - "${REPO_ROOT}" <<'PY'
import re
import sys
from pathlib import Path
root = Path(sys.argv[1])
rom_suffixes = (".iso", ".gcm", ".wbfs", ".nkit.iso")
secret_pattern = re.compile(
r"BEGIN [A-Z ]+PRIVATE KEY|Authorization:\s*Bearer\s+[A-Za-z0-9._~-]{20,}"
)
findings: list[str] = []
for path in root.rglob("*"):
if not path.is_file() or any(part in {".git", "__pycache__", "artifacts"} for part in path.parts):
continue
relative = path.relative_to(root)
if path.name.lower().endswith(rom_suffixes):
findings.append(f"ROM file: {relative}")
continue
if relative in {Path("templates/couchd.token.example"), Path("scripts/check.sh")}:
continue
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
if secret_pattern.search(text):
findings.append(f"possible secret: {relative}")
if findings:
print("\n".join(findings))
raise SystemExit(1)
PY
}
run_step "unit tests" python3 -m unittest discover -v
run_step "setup shell tests" bash "${REPO_ROOT}/tests/test_setup_modes.sh"
run_step "shell syntax" bash -n "${REPO_ROOT}/setup.sh" "${REPO_ROOT}"/scripts/*.sh
run_step "python compile" python3 -m py_compile "${REPO_ROOT}"/src/couchd/*.py "${REPO_ROOT}"/tests/*.py
if command -v node >/dev/null 2>&1; then
run_step "dashboard JS syntax" node --check "${REPO_ROOT}/dashboard/app.js"
else
printf '[check] dashboard JS syntax skipped: node not installed\n'
fi
if git -C "${REPO_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
run_step "git diff check" git -C "${REPO_ROOT}" diff --check
else
printf '[check] git diff check skipped: deployed tree has no .git directory\n'
fi
run_step "secret and ROM scan" secret_scan

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
set -Eeuo pipefail
log() {
printf '[display-audio] %s\n' "$*"
}
warn() {
printf '[display-audio] WARN: %s\n' "$*" >&2
logger -t couch-display-audio -- "$*" 2>/dev/null || true
}
pick_hdmi_refresh() {
awk '
$1 == "HDMI-1" && $2 == "connected" { in_hdmi=1; next }
in_hdmi && $0 !~ /^[[:space:]]/ { exit }
in_hdmi && $1 == "1920x1080" {
for (i = 2; i <= NF; i++) {
gsub(/[+*]/, "", $i)
if ($i ~ /^[0-9]+(\.[0-9]+)?$/) {
value = $i + 0
if (value > best) {
best = value
best_text = $i
}
}
}
}
END {
if (best_text != "") {
print best_text
}
}
'
}
choose_hdmi_profile() {
pactl list cards | awk '
/^Card #[0-9]+/ { card="" }
/^\tName: / { sub(/^\tName: /, "", $0); card=$0 }
/^\t\toutput:hdmi/ && /available: yes/ {
profile=$1
sub(/:$/, "", profile)
print card "\t" profile
exit
}
'
}
choose_hdmi_sink() {
pactl list short sinks | awk '$2 ~ /\.hdmi/ { print $2; exit }'
}
xrandr_output="$(xrandr --query)"
if ! grep -q '^HDMI-1 connected' <<<"${xrandr_output}"; then
warn "HDMI-1 is not connected; deferring display/audio switch"
exit 0
fi
refresh="$(printf '%s\n' "${xrandr_output}" | pick_hdmi_refresh)"
if [[ -z "${refresh}" ]]; then
warn "HDMI-1 is connected but no 1920x1080 refresh was discovered"
exit 0
fi
log "setting HDMI-1 primary at 1920x1080@${refresh} and disabling eDP-1"
xrandr --output HDMI-1 --primary --mode 1920x1080 --rate "${refresh}" --output eDP-1 --off
profile_line="$(choose_hdmi_profile || true)"
if [[ -z "${profile_line}" ]]; then
warn "HDMI audio sink/profile not available yet; display configured and audio switch deferred"
exit 0
fi
card_name="${profile_line%%$'\t'*}"
profile_name="${profile_line#*$'\t'}"
log "selecting HDMI audio profile ${profile_name} on ${card_name}"
pactl set-card-profile "${card_name}" "${profile_name}"
pactl list short sinks >/dev/null 2>&1 || true
sink_name="$(choose_hdmi_sink || true)"
if [[ -z "${sink_name}" ]]; then
warn "HDMI sink still unavailable after profile switch; audio switch deferred"
exit 0
fi
pactl set-default-sink "${sink_name}"

8
scripts/discover_controller.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ADDRESS="${1:-C8:3F:26:12:69:7B}"
bluetoothctl info "${ADDRESS}" || true
printf '\nKnown-safe template: templates/controllers/xbox-standard-controller.profile.ini\n'
printf 'Do not fill in SDL GUIDs or Dolphin device IDs until you confirm them on this machine.\n'

77
scripts/install_slippi.sh Executable file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -Eeuo pipefail
API_URL="${SLIPPI_RELEASE_API_URL:-https://api.github.com/repos/project-slippi/slippi-launcher/releases/latest}"
INSTALL_DIR="${SLIPPI_INSTALL_DIR:-/home/couch/Applications}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
log() {
printf '[install_slippi] %s\n' "$*"
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
printf 'missing required command: %s\n' "$1" >&2
exit 1
}
}
require_cmd curl
require_cmd jq
require_cmd install
mkdir -p "${INSTALL_DIR}"
validate_asset_url() {
local url="$1"
local expected_name_regex="$2"
if [[ ! "${url}" =~ ^https://github\.com/project-slippi/slippi-launcher/releases/download/ ]]; then
printf 'unexpected asset host or path: %s\n' "${url}" >&2
exit 1
fi
local name
name="$(basename "${url}")"
if [[ ! "${name}" =~ ${expected_name_regex} ]]; then
printf 'unexpected asset name: %s\n' "${name}" >&2
exit 1
fi
}
release_json="${TMP_DIR}/release.json"
curl -fsSL "${API_URL}" -o "${release_json}"
appimage_url="$(jq -r '[.assets[] | select(.name | test("^Slippi-Launcher-.*x86_64\\.AppImage$")) | .browser_download_url] | first // empty' "${release_json}")"
checksum_url="$(jq -r '[.assets[] | select(.name | test("(sha256|checksums?)(\\.txt)?$"; "i")) | .browser_download_url] | first // empty' "${release_json}")"
if [[ -z "${appimage_url}" ]]; then
printf 'could not find official Slippi Launcher AppImage asset\n' >&2
exit 1
fi
validate_asset_url "${appimage_url}" '^Slippi-Launcher-.*x86_64\.AppImage$'
appimage_name="$(basename "${appimage_url}")"
appimage_path="${TMP_DIR}/${appimage_name}"
curl -fsSL "${appimage_url}" -o "${appimage_path}"
if [[ -n "${checksum_url}" ]]; then
validate_asset_url "${checksum_url}" '^(sha256|checksums?)(\.[A-Za-z0-9]+)?$'
checksum_path="${TMP_DIR}/checksums.txt"
curl -fsSL "${checksum_url}" -o "${checksum_path}"
if grep -F "${appimage_name}" "${checksum_path}" >/dev/null 2>&1; then
(
cd "${TMP_DIR}"
grep -F "${appimage_name}" "${checksum_path}" | sha256sum --check --status
)
log "checksum verified for ${appimage_name}"
else
printf 'checksum asset present but does not include %s\n' "${appimage_name}" >&2
exit 1
fi
else
log "no upstream checksum asset published for this release; upstream verification unavailable"
fi
install -m 0755 "${appimage_path}" "${INSTALL_DIR}/${appimage_name}"
ln -sfn "${INSTALL_DIR}/${appimage_name}" "${INSTALL_DIR}/SlippiLauncher.AppImage"
log "installed ${INSTALL_DIR}/${appimage_name}"

9
scripts/launch_melee.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_PATH="${COUCHOS_CONFIG:-/home/couch/.config/couchd/runtime.json}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
exec python3 -m couchd --config "${CONFIG_PATH}" launch-melee

9
scripts/open_slippi.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_PATH="${COUCHOS_CONFIG:-/home/couch/.config/couchd/runtime.json}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
exec python3 -m couchd --config "${CONFIG_PATH}" open-slippi

346
scripts/preflight.sh Executable file
View File

@@ -0,0 +1,346 @@
#!/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' </home/couch/.config/couchd/token)" == "replace-with-long-random-token" ]]; then
record FAIL "token still uses the template placeholder"
else
record PASS "token mode is 0600 and not placeholder"
fi
else
record FAIL "token missing or not 0600"
fi
local bind_address
bind_address="$(runtime_field bind_address 2>/dev/null || true)"
if [[ "${bind_address}" == "100.64.0.15" ]]; then
record PASS "runtime bind address is 100.64.0.15"
else
record FAIL "runtime bind address is not 100.64.0.15"
fi
if ss -ltn sport = :8765 2>/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

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -Eeuo pipefail
COUCH_HOME="${COUCH_HOME:-/home/couch}"
REPLAY_DIR="${COUCH_HOME}/Slippi"
CHECKLIST="${REPLAY_DIR}/FIRST_RUN_CHECKLIST.md"
install -d -m 0700 "${REPLAY_DIR}"
cat <<EOF
Created/validated ${REPLAY_DIR}.
Manual first-run checklist:
1. Launch Slippi Launcher and install/update Slippi Dolphin.
2. Point Dolphin at your own NTSC 1.02 Melee image.
3. In Dolphin graphics, set Vulkan, fullscreen, 4:3, internal resolution 2x, and V-Sync off.
4. Confirm replays save under ${REPLAY_DIR}.
5. Create and save the named Xbox controller profile after device discovery.
Reference template:
${CHECKLIST}
EOF

9
scripts/stop_game.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_PATH="${COUCHOS_CONFIG:-/home/couch/.config/couchd/runtime.json}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}"
exec python3 -m couchd --config "${CONFIG_PATH}" stop