2 Commits
Author SHA1 Message Date
Gabriel Brown faa9a00716 Close Panama recovery race windows 2026-08-18 11:11:53 -04:00
Gabriel Brown 8bfae70284 Harden bounded Panama recovery actions 2026-08-18 10:58:48 -04:00
4 changed files with 680 additions and 88 deletions
+239 -36
View File
@@ -5,9 +5,13 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import ctypes
import errno
import json import json
import os import os
import re import re
import secrets
import signal
import shutil import shutil
import subprocess import subprocess
import sys import sys
@@ -21,6 +25,11 @@ from typing import Callable, Literal
Status = Literal["ok", "warning", "error", "unconfigured"] Status = Literal["ok", "warning", "error", "unconfigured"]
Group = Literal["desktop-foundation", "input-media", "integrations", "panama-tools"] Group = Literal["desktop-foundation", "input-media", "integrations", "panama-tools"]
ActionKind = Literal["repair", "open", "instructions"] ActionKind = Literal["repair", "open", "instructions"]
InhibitorRow = tuple[str, str, str, str, str, str, str, str]
AT_FDCWD = -100
RENAME_NOREPLACE = 1
RENAME_EXCHANGE = 2
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -409,8 +418,13 @@ def check_runtime_links(config: DoctorConfig) -> Check:
def check_vicinae_commands(config: DoctorConfig) -> Check: def check_vicinae_commands(config: DoctorConfig) -> Check:
source = config.root / "config/local/share/vicinae/scripts" source = config.root / "config/local/share/vicinae/scripts"
installed = config.home / ".local/share/vicinae/scripts" installed = config.home / ".local/share/vicinae/scripts/panama"
if source.is_dir() and installed.is_symlink() and installed.exists(): try:
linked = source.is_dir() and installed.is_symlink() \
and installed.resolve(strict=False) == source.resolve(strict=True)
except OSError:
linked = False
if linked:
return Check("panama.vicinae-commands", "panama-tools", "Panama commands", "ok", "Panama Vicinae commands are linked.") return Check("panama.vicinae-commands", "panama-tools", "Panama commands", "ok", "Panama Vicinae commands are linked.")
return Check("panama.vicinae-commands", "panama-tools", "Panama commands", "warning", "Panama Vicinae commands are not linked.", Action("repair", "Repair command link")) return Check("panama.vicinae-commands", "panama-tools", "Panama commands", "warning", "Panama Vicinae commands are not linked.", Action("repair", "Repair command link"))
@@ -445,20 +459,10 @@ def check_caffeine(config: DoctorConfig) -> Check:
result = run_command(("systemd-inhibit", "--list", "--no-pager", "--no-legend"), config) result = run_command(("systemd-inhibit", "--list", "--no-pager", "--no-legend"), config)
if result.state != "ok": if result.state != "ok":
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe is unavailable.") return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe is unavailable.")
uid = str(os.getuid()) inhibitor_rows = parse_caffeine_rows(result.stdout, str(os.getuid()))
inhibitors = 0 if inhibitor_rows is None:
malformed = False
for line in result.stdout.splitlines():
parts = line.split()
relevant = len(parts) >= 2 and parts[0] == "Panama" and parts[1] == uid and "Caffeine" in parts
if not relevant:
continue
if len(parts) >= 8 and parts[3].isdecimal() and parts[-2:] == ["Caffeine", "block"]:
inhibitors += 1
else:
malformed = True
if malformed:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe returned an invalid result.") return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe returned an invalid result.")
inhibitors = len(dict.fromkeys(int(row[3]) for row in inhibitor_rows))
if inhibitors > 1: if inhibitors > 1:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Duplicate Panama Caffeine inhibitors detected.", Action("repair", "Release duplicate inhibitors")) return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Duplicate Panama Caffeine inhibitors detected.", Action("repair", "Release duplicate inhibitors"))
if inhibitors == 1: if inhibitors == 1:
@@ -535,10 +539,138 @@ def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult
return RepairResult(check_id, True, exit_code, message) return RepairResult(check_id, True, exit_code, message)
def lexical_path(path: Path) -> Path:
"""Normalize dot segments without following any filesystem symlink."""
return Path(os.path.abspath(os.fspath(path)))
def lexical_link_target(destination: Path) -> Path:
target = Path(os.readlink(destination))
return lexical_path(target if target.is_absolute() else destination.parent / target)
def renameat2(source: Path, destination: Path, flags: int) -> None:
"""Call Linux renameat2 with fixed flags selected by authored code."""
libc = ctypes.CDLL(None, use_errno=True)
function = getattr(libc, "renameat2", None)
if function is None:
raise OSError(errno.ENOSYS, "renameat2 is unavailable")
function.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
function.restype = ctypes.c_int
result = function(
AT_FDCWD,
os.fsencode(source),
AT_FDCWD,
os.fsencode(destination),
flags,
)
if result != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), destination)
def rename_exchange(source: Path, destination: Path) -> None:
renameat2(source, destination, RENAME_EXCHANGE)
def rename_noreplace(source: Path, destination: Path) -> None:
renameat2(source, destination, RENAME_NOREPLACE)
def create_symlink_candidate(destination: Path, source: Path) -> Path:
"""Create one unpredictable authored sibling candidate symlink."""
for _ in range(32):
candidate = destination.with_name(
f".panama-link-{destination.name}-{os.getpid()}-{secrets.token_hex(8)}"
)
try:
os.symlink(source, candidate, target_is_directory=True)
return candidate
except FileExistsError:
continue
raise OSError("Could not allocate an authored temporary link")
def cleanup_candidate(candidate: Path) -> None:
try:
candidate.unlink()
except FileNotFoundError:
pass
def install_absent_symlink(destination: Path, source: Path) -> Literal["repaired", "blocked", "failed"]:
candidate = create_symlink_candidate(destination, source)
try:
try:
rename_noreplace(candidate, destination)
except FileExistsError:
return "blocked"
except OSError:
return "failed"
return "repaired"
finally:
cleanup_candidate(candidate)
def exchange_owned_symlink(
destination: Path,
source: Path,
authored_sources: frozenset[Path],
) -> Literal["repaired", "blocked", "failed"]:
"""Exchange first, then validate the exact object removed from destination."""
candidate = create_symlink_candidate(destination, source)
exchanged = False
rolled_back = False
try:
try:
rename_exchange(candidate, destination)
exchanged = True
except OSError:
return "failed"
try:
old_is_authored = candidate.is_symlink() \
and lexical_link_target(candidate) in authored_sources
except OSError:
old_is_authored = False
if old_is_authored:
cleanup_candidate(candidate)
return "repaired"
try:
rename_exchange(candidate, destination)
rolled_back = True
except OSError:
# The displaced object remains at the unpredictable candidate path;
# never unlink it when rollback could not restore ownership.
return "failed"
try:
restored_candidate_is_ours = candidate.is_symlink() \
and lexical_link_target(candidate) == source
except OSError:
restored_candidate_is_ours = False
if not restored_candidate_is_ours:
return "failed"
cleanup_candidate(candidate)
return "blocked"
finally:
if not exchanged or rolled_back:
try:
if candidate.is_symlink() and lexical_link_target(candidate) == source:
cleanup_candidate(candidate)
except OSError:
pass
def repair_runtime_links(config: DoctorConfig) -> RepairResult: def repair_runtime_links(config: DoctorConfig) -> RepairResult:
sources = [(name, config.root / relative_source) for name, relative_source in RUNTIME_LINK_TARGETS] root = lexical_path(config.root)
sources = [(name, lexical_path(config.root / relative_source)) for name, relative_source in RUNTIME_LINK_TARGETS]
if any(not source.is_dir() for _, source in sources): if any(not source.is_dir() for _, source in sources):
return RepairResult("panama.runtime-links", True, 1, "Tracked Panama link destinations are unavailable.") return RepairResult("panama.runtime-links", True, 1, "Tracked Panama link destinations are unavailable.")
if any(not source.is_relative_to(root) for _, source in sources):
return RepairResult("panama.runtime-links", True, 1, "Tracked Panama link destinations are invalid.")
authored_sources = frozenset(source for _, source in sources)
try: try:
config.config_home.mkdir(parents=True, exist_ok=True) config.config_home.mkdir(parents=True, exist_ok=True)
@@ -551,23 +683,30 @@ def repair_runtime_links(config: DoctorConfig) -> RepairResult:
destination = config.config_home / name destination = config.config_home / name
try: try:
if destination.is_symlink(): if destination.is_symlink():
if destination.resolve(strict=False) == source.resolve(strict=True): current_target = lexical_link_target(destination)
if current_target == source:
continue continue
destination.unlink() if current_target not in authored_sources:
destination.symlink_to(source, target_is_directory=True) blocked = True
continue
outcome = exchange_owned_symlink(destination, source, authored_sources)
blocked = blocked or outcome == "blocked"
failed = failed or outcome == "failed"
elif destination.exists(): elif destination.exists():
# A regular file or directory is user-owned unless proven # A regular file or directory is user-owned unless proven
# otherwise. Report it, but never replace it. # otherwise. Report it, but never replace it.
blocked = True blocked = True
else: else:
destination.symlink_to(source, target_is_directory=True) outcome = install_absent_symlink(destination, source)
blocked = blocked or outcome == "blocked"
failed = failed or outcome == "failed"
except OSError: except OSError:
failed = True failed = True
if failed: if failed:
return RepairResult("panama.runtime-links", True, 1, "One or more Panama runtime links could not be recreated.") return RepairResult("panama.runtime-links", True, 1, "One or more Panama runtime links could not be recreated.")
if blocked: if blocked:
return RepairResult("panama.runtime-links", True, 1, "A user-owned file or directory is blocking a Panama runtime link.") return RepairResult("panama.runtime-links", True, 1, "A user-owned runtime path is blocking a Panama link.")
return RepairResult("panama.runtime-links", True, 0, "Panama runtime links were recreated. A fresh health check will verify them.") return RepairResult("panama.runtime-links", True, 0, "Panama runtime links were recreated. A fresh health check will verify them.")
@@ -581,6 +720,57 @@ def repair_vicinae_commands(config: DoctorConfig) -> RepairResult:
return RepairResult("panama.vicinae-commands", True, exit_code, message) return RepairResult("panama.vicinae-commands", True, exit_code, message)
def parse_caffeine_rows(output: str, uid: str) -> list[InhibitorRow] | None:
inhibitor_rows: list[InhibitorRow] = []
for line in output.splitlines():
parts = line.split()
if len(parts) < 2 or parts[0] != "Panama" or parts[1] != uid:
continue
if len(parts) != 8:
if "Caffeine" in parts:
return None
continue
if parts[6] != "Caffeine" or parts[7] != "block":
continue
if not parts[3].isdecimal():
return None
inhibitor_rows.append(tuple(parts))
return inhibitor_rows
def close_pidfds(pidfds: list[int]) -> None:
for pidfd in pidfds:
try:
os.close(pidfd)
except OSError:
pass
def signal_caffeine_pidfds(
pidfds: list[int],
sender: Callable[..., None] | None = None,
) -> Literal["released", "preflight-failed", "incomplete"]:
send = sender or signal.pidfd_send_signal
for pidfd in pidfds:
try:
send(pidfd, 0, None, 0)
except (OSError, ValueError):
return "preflight-failed"
incomplete = False
for pidfd in pidfds:
try:
send(pidfd, signal.SIGTERM, None, 0)
except ProcessLookupError:
continue
except OSError as error:
if error.errno != errno.ESRCH:
incomplete = True
except ValueError:
incomplete = True
return "incomplete" if incomplete else "released"
def repair_caffeine(config: DoctorConfig) -> RepairResult: def repair_caffeine(config: DoctorConfig) -> RepairResult:
list_command = ("systemd-inhibit", "--list", "--no-pager", "--no-legend") list_command = ("systemd-inhibit", "--list", "--no-pager", "--no-legend")
list_exit, output = run_repair_command(list_command, config) list_exit, output = run_repair_command(list_command, config)
@@ -588,26 +778,39 @@ def repair_caffeine(config: DoctorConfig) -> RepairResult:
return RepairResult("panama.caffeine", True, list_exit, "Caffeine inhibitors could not be inspected.") return RepairResult("panama.caffeine", True, list_exit, "Caffeine inhibitors could not be inspected.")
uid = str(os.getuid()) uid = str(os.getuid())
inhibitor_pids: list[str] = [] inhibitor_rows = parse_caffeine_rows(output, uid)
for line in output.splitlines(): if inhibitor_rows is None:
parts = line.split()
if len(parts) < 2 or parts[0] != "Panama" or parts[1] != uid:
continue
if len(parts) != 8:
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.") return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
if parts[6] != "Caffeine" or parts[7] != "block": inhibitor_pids = list(dict.fromkeys(int(row[3]) for row in inhibitor_rows))
continue
if not parts[3].isdecimal():
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
inhibitor_pids.append(parts[3])
if len(inhibitor_pids) <= 1: if len(inhibitor_pids) <= 1:
return RepairResult("panama.caffeine", True, 0, "No duplicate Panama Caffeine inhibitors needed release.") return RepairResult("panama.caffeine", True, 0, "No duplicate Panama Caffeine inhibitors needed release.")
for pid in inhibitor_pids[1:]: duplicates = inhibitor_pids[1:]
exit_code, _ = run_repair_command(("kill", "--", pid), config) if not hasattr(os, "pidfd_open") or not hasattr(signal, "pidfd_send_signal"):
if exit_code != 0: return RepairResult("panama.caffeine", True, 1, "Safe Caffeine inhibitor release is unavailable on this system.")
return RepairResult("panama.caffeine", True, exit_code, "A duplicate Panama Caffeine inhibitor could not be released.")
pidfds: list[int] = []
try:
try:
pidfds = [os.pidfd_open(pid, 0) for pid in duplicates]
except (OSError, ValueError):
return RepairResult("panama.caffeine", True, 1, "A duplicate inhibitor changed before it could be safely released.")
second_exit, second_output = run_repair_command(list_command, config)
if second_exit != 0:
return RepairResult("panama.caffeine", True, second_exit, "Caffeine inhibitors could not be revalidated; nothing was released.")
second_rows = parse_caffeine_rows(second_output, uid)
if second_rows is None or second_rows != inhibitor_rows:
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata changed; nothing was released.")
signal_outcome = signal_caffeine_pidfds(pidfds)
if signal_outcome == "preflight-failed":
return RepairResult("panama.caffeine", True, 1, "A duplicate inhibitor changed before it could be safely released.")
if signal_outcome == "incomplete":
return RepairResult("panama.caffeine", True, 1, "One or more duplicate inhibitors could not be released.")
finally:
close_pidfds(pidfds)
return RepairResult("panama.caffeine", True, 0, "Duplicate Panama Caffeine inhibitors were released. A fresh health check will verify recovery.") return RepairResult("panama.caffeine", True, 0, "Duplicate Panama Caffeine inhibitors were released. A fresh health check will verify recovery.")
@@ -117,6 +117,8 @@ Singleton {
} }
function refresh(): bool { function refresh(): bool {
if (root.postRepairScanPending)
return false;
if (scanProcess.running || repairProcess.running) { if (scanProcess.running || repairProcess.running) {
root.queuedRefresh = true; root.queuedRefresh = true;
return false; return false;
@@ -239,6 +241,8 @@ Singleton {
const check = root.checks.find(candidate => candidate.id === id); const check = root.checks.find(candidate => candidate.id === id);
if (!check || !check.action || check.action.kind !== "repair") if (!check || !check.action || check.action.kind !== "repair")
return false; return false;
if (external && check.action.confirm)
return false;
root.repairingId = id; root.repairingId = id;
root.lastError = ""; root.lastError = "";
+87 -3
View File
@@ -7,10 +7,22 @@
set -euo pipefail set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
harness="$repo_dir/config/dot/quickshell/health-harness.qml" source_harness="$repo_dir/config/dot/quickshell/health-harness.qml"
service="$repo_dir/config/dot/quickshell/services/Health.qml" service="$repo_dir/config/dot/quickshell/services/Health.qml"
shell="$repo_dir/config/dot/quickshell/shell.qml" shell="$repo_dir/config/dot/quickshell/shell.qml"
warning_snapshot='{"schemaVersion":1,"generatedAt":"2026-08-18T00:00:00Z","summary":{"status":"warning","healthy":0,"warnings":2,"errors":0,"unconfigured":0},"context":{"session":"hyprland","versions":[{"id":"quickshell","version":"0.3.0"}]},"checks":[{"id":"integration.calendar","group":"integrations","title":"Calendar","status":"warning","detail":"Calendar probe timed out.","action":{"kind":"open","label":"Open Date & Time","confirm":false,"target":"datetime"}},{"id":"panama.caffeine","group":"panama-tools","title":"Caffeine","status":"warning","detail":"Duplicate inhibitors are active.","action":{"kind":"repair","label":"Release duplicate inhibitors","confirm":false}}]}' warning_snapshot='{"schemaVersion":1,"generatedAt":"2026-08-18T00:00:00Z","summary":{"status":"warning","healthy":0,"warnings":2,"errors":0,"unconfigured":0},"context":{"session":"hyprland","versions":[{"id":"quickshell","version":"0.3.0"}]},"checks":[{"id":"integration.calendar","group":"integrations","title":"Calendar","status":"warning","detail":"Calendar probe timed out.","action":{"kind":"open","label":"Open Date & Time","confirm":false,"target":"datetime"}},{"id":"panama.caffeine","group":"panama-tools","title":"Caffeine","status":"warning","detail":"Duplicate inhibitors are active.","action":{"kind":"repair","label":"Release duplicate inhibitors","confirm":false}}]}'
confirm_snapshot="$(jq -c '
.summary.status = "error"
| .summary.errors = 1
| .checks += [{
id: "desktop.quickshell",
group: "desktop-foundation",
title: "Quickshell",
status: "error",
detail: "Panama shell needs to restart.",
action: {kind: "repair", label: "Restart Panama", confirm: true}
}]
' <<<"$warning_snapshot")"
projection_snapshot="$(jq -c ' projection_snapshot="$(jq -c '
.fixtureSecret = "fixture-secret" .fixtureSecret = "fixture-secret"
| .summary.fixtureSecret = "fixture-secret" | .summary.fixtureSecret = "fixture-secret"
@@ -33,7 +45,7 @@ fail() {
} }
[[ -f "$service" ]] || fail 'Health.qml is missing' [[ -f "$service" ]] || fail 'Health.qml is missing'
[[ -f "$harness" ]] || fail 'health harness is missing' [[ -f "$source_harness" ]] || fail 'health harness is missing'
[[ -f "$shell" ]] || fail 'shell.qml is missing' [[ -f "$shell" ]] || fail 'shell.qml is missing'
# shell.qml is not started here: it is the active desktop shell. Keep this # shell.qml is not started here: it is the active desktop shell. Keep this
@@ -72,6 +84,33 @@ if keys != ["summary", "busy", "generation", "acceptedGeneration", "checks"]:
PY PY
fixture_dir="$(mktemp -d /tmp/panama-health.XXXXXX)" fixture_dir="$(mktemp -d /tmp/panama-health.XXXXXX)"
config_path="$fixture_dir/quickshell"
cp -a "$repo_dir/config/dot/quickshell" "$config_path"
harness="$config_path/health-harness.qml"
python3 - "$harness" <<'PY'
import sys
path = sys.argv[1]
source = open(path, encoding="utf-8").read()
needle = ' function repair(id: string): bool { return Health.repair(id, false); }\n'
replacement = needle + ''' function externalRepair(id: string): bool { return Health.repair(id, true); }
function pendingRefreshRace(): string {
const before = Health.generation;
Health.finishRepair(0, "panama.caffeine", false, JSON.stringify({
schemaVersion: 1,
checkId: "panama.caffeine",
accepted: true,
exitCode: 0,
message: "Fixture repair completed."
}));
const accepted = Health.refresh();
return JSON.stringify({ accepted: accepted, before: before });
}
'''
if needle not in source:
raise SystemExit("health harness repair seam is missing")
open(path, "w", encoding="utf-8").write(source.replace(needle, replacement))
PY
helper="$fixture_dir/panama-doctor" helper="$fixture_dir/panama-doctor"
copy_bin="$fixture_dir/bin" copy_bin="$fixture_dir/bin"
copy_file="$fixture_dir/copied-report.json" copy_file="$fixture_dir/copied-report.json"
@@ -96,6 +135,11 @@ printf '%s\n' \
' *) printf "not-json\\n"; exit 0 ;;' \ ' *) printf "not-json\\n"; exit 0 ;;' \
' esac' \ ' esac' \
'fi' \ 'fi' \
'if [[ "$1" == "--repair" && "$2" == "desktop.quickshell" && "$3" == "--json" ]]; then' \
' sleep 0.25' \
' printf "{\"schemaVersion\":1,\"checkId\":\"desktop.quickshell\",\"accepted\":true,\"exitCode\":0,\"message\":\"Panama shell restart was requested.\"}\\n"' \
' exit 0' \
'fi' \
'exit 2' >"$helper" 'exit 2' >"$helper"
chmod +x "$helper" chmod +x "$helper"
mkdir -p "$copy_bin" mkdir -p "$copy_bin"
@@ -243,12 +287,52 @@ jq -e '.lastRepair.checkId == "panama.caffeine" and .lastRepair.accepted == fals
--argjson before "$mismatch_generation" >/dev/null <<<"$state" \ --argjson before "$mismatch_generation" >/dev/null <<<"$state" \
|| fail "mismatched repair JSON escaped containment: $state" || fail "mismatched repair JSON escaped containment: $state"
# A refresh arriving after repair settlement but before the deferred mandatory
# scan is coalesced into that scan instead of starting an extra generation.
pending_race="$(run ipc call health-test pendingRefreshRace)"
jq -e '.accepted == false' >/dev/null <<<"$pending_race" \
|| fail "refresh escaped the post-repair pending window: $pending_race"
pending_generation="$(jq -r .before <<<"$pending_race")"
for _ in $(seq 1 120); do
state="$(run ipc call health-test status)"
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false' \
--argjson before "$pending_generation" >/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false' \
--argjson before "$pending_generation" >/dev/null <<<"$state" \
|| fail "pending-window refresh created duplicate scans: $state"
# External IPC cannot bypass an authored confirmation. The same current row is
# still repairable through Settings' external=false path after UI confirmation.
confirm_generation="$(jq -r .generation <<<"$state")"
[[ "$(run ipc call health-test accept "$confirm_snapshot" "$confirm_generation")" == "true" ]] \
|| fail 'confirmation fixture was rejected'
before_repair_lines="$(wc -l <"$repair_log")"
[[ "$(run ipc call health-test externalRepair desktop.quickshell)" == "false" ]] \
|| fail 'external repair bypassed confirmation'
[[ "$(wc -l <"$repair_log")" == "$before_repair_lines" ]] \
|| fail 'external confirmation rejection started a process'
[[ "$(run ipc call health-test repair desktop.quickshell)" == "true" ]] \
|| fail 'confirmed Settings repair was refused'
for _ in $(seq 1 120); do
state="$(run ipc call health-test status)"
jq -e '.busy == false and .generation == ($before + 1)' \
--argjson before "$confirm_generation" >/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.lastRepair == {schemaVersion:1, checkId:"desktop.quickshell", accepted:true, exitCode:0, message:"Panama shell restart was requested."}
and .generation == ($before + 1)' --argjson before "$confirm_generation" \
>/dev/null <<<"$state" || fail "confirmed Settings repair did not complete safely: $state"
[[ "$(grep -Fc -- '--repair desktop.quickshell --json' "$repair_log")" == 1 ]] \
|| fail 'confirmed Settings repair did not start exactly one repair process'
[[ "$(run ipc call health-test repair unknown.check)" == "false" ]] \ [[ "$(run ipc call health-test repair unknown.check)" == "false" ]] \
|| fail 'unknown check started a repair' || fail 'unknown check started a repair'
[[ "$(run ipc call health-test repair integration.calendar)" == "false" ]] \ [[ "$(run ipc call health-test repair integration.calendar)" == "false" ]] \
|| fail 'non-repairable check started a repair' || fail 'non-repairable check started a repair'
state="$(run ipc call health-test status)" state="$(run ipc call health-test status)"
jq -e '.repairingId == "" and .generation == ($before + 1)' --argjson before "$mismatch_generation" \ jq -e '.repairingId == "" and .generation == ($before + 1)' --argjson before "$confirm_generation" \
>/dev/null <<<"$state" || fail "rejected repair altered process state: $state" >/dev/null <<<"$state" || fail "rejected repair altered process state: $state"
python3 - "$service" <<'PY' || fail 'external repair failure notification is not bounded' python3 - "$service" <<'PY' || fail 'external repair failure notification is not bounded'
+349 -48
View File
@@ -16,7 +16,15 @@ fail() {
} }
fixture="$(mktemp -d /tmp/panama-doctor.XXXXXX)" fixture="$(mktemp -d /tmp/panama-doctor.XXXXXX)"
trap 'rm -rf "$fixture"' EXIT child_pids=()
cleanup() {
for pid in "${child_pids[@]}"; do
kill "$pid" >/dev/null 2>&1 || true
wait "$pid" >/dev/null 2>&1 || true
done
rm -rf "$fixture"
}
trap cleanup EXIT
home="$fixture/home" home="$fixture/home"
config_home="$home/.config" config_home="$home/.config"
@@ -25,7 +33,7 @@ runtime_dir="$fixture/runtime"
bin_dir="$fixture/bin" bin_dir="$fixture/bin"
data_home="$home/.local/share" data_home="$home/.local/share"
mkdir -p "$config_home" "$state_home" "$runtime_dir" "$bin_dir" "$data_home/vicinae" mkdir -p "$config_home" "$state_home" "$runtime_dir" "$bin_dir" "$data_home/vicinae/scripts"
cp "$fixture_root/bin/"* "$bin_dir/" cp "$fixture_root/bin/"* "$bin_dir/"
chmod +x "$bin_dir"/* chmod +x "$bin_dir"/*
@@ -81,7 +89,7 @@ touch "$config_home/autostart/nextcloud.desktop"
for name in hypr quickshell uwsm vicinae; do for name in hypr quickshell uwsm vicinae; do
ln -s "$repo_dir/config/dot/$name" "$config_home/$name" ln -s "$repo_dir/config/dot/$name" "$config_home/$name"
done done
ln -s "$repo_dir/config/local/share/vicinae/scripts" "$data_home/vicinae/scripts" ln -s "$repo_dir/config/local/share/vicinae/scripts" "$data_home/vicinae/scripts/panama"
run_doctor() { run_doctor() {
HOME="$home" \ HOME="$home" \
@@ -130,6 +138,20 @@ check_status() {
snapshot="$(run_doctor --json)" snapshot="$(run_doctor --json)"
assert_schema_and_redaction "$snapshot" assert_schema_and_redaction "$snapshot"
check_status "$snapshot" panama.vicinae-commands ok
# The diagnostic follows the actual installer contract: the scripts parent is
# a directory and only its Panama child is an authored link.
rm "$data_home/vicinae/scripts/panama"
unlinked_vicinae="$(run_doctor --json)"
check_status "$unlinked_vicinae" panama.vicinae-commands warning
PANAMA_PATH="$repo_dir" VICINAE_DATA_DIR="$data_home/vicinae" HOME="$home" \
PATH="$bin_dir:/usr/bin" "$repo_dir/setup/scripts/link-vicinae-scripts"
relinked_vicinae="$(run_doctor --json)"
check_status "$relinked_vicinae" panama.vicinae-commands ok
[[ -L "$data_home/vicinae/scripts/panama" \
&& "$(readlink "$data_home/vicinae/scripts/panama")" == "$repo_dir/config/local/share/vicinae/scripts" ]] \
|| fail 'authored Vicinae helper did not create the diagnosed child link'
# A healthy systemd-backed service stays healthy. # A healthy systemd-backed service stays healthy.
check_status "$snapshot" desktop.hyprpaper ok check_status "$snapshot" desktop.hyprpaper ok
@@ -237,12 +259,14 @@ summary="$(run_doctor --summary)"
# Repairs run against a second, disposable Panama root. Every process boundary # Repairs run against a second, disposable Panama root. Every process boundary
# records its argv, and every filesystem assertion is confined to this fixture. # records its argv, and every filesystem assertion is confined to this fixture.
repair_root="$fixture/repair-root" repair_root="$home/.local/share/Panama"
repair_log="$runtime_dir/repair.log" repair_log="$runtime_dir/repair.log"
mkdir -p "$repair_root/config/dot" "$repair_root/setup/scripts" mkdir -p "$repair_root/config/dot" "$repair_root/config/local/share/vicinae/scripts" \
"$repair_root/setup/scripts"
for name in hypr quickshell uwsm vicinae; do for name in hypr quickshell uwsm vicinae; do
mkdir -p "$repair_root/config/dot/$name" mkdir -p "$repair_root/config/dot/$name"
done done
cp "$repo_dir/setup/scripts/link-vicinae-scripts" "$repair_root/setup/scripts/link-vicinae-scripts"
mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe" mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe"
cat >"$bin_dir/systemctl" <<'EOF' cat >"$bin_dir/systemctl" <<'EOF'
@@ -266,51 +290,59 @@ printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log" printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
EOF EOF
cat >"$bin_dir/kill" <<'EOF'
#!/usr/bin/bash
set -euo pipefail
printf 'kill' >>"$XDG_RUNTIME_DIR/repair.log"
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
EOF
cat >"$bin_dir/systemd-inhibit" <<'EOF' cat >"$bin_dir/systemd-inhibit" <<'EOF'
#!/usr/bin/bash #!/usr/bin/bash
set -euo pipefail set -euo pipefail
printf 'systemd-inhibit' >>"$XDG_RUNTIME_DIR/repair.log" printf 'systemd-inhibit' >>"$XDG_RUNTIME_DIR/repair.log"
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log" printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log" printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
count_file="$XDG_RUNTIME_DIR/caffeine-list-count"
count=0
[[ ! -f "$count_file" ]] || read -r count <"$count_file"
count=$((count + 1))
printf '%s\n' "$count" >"$count_file"
read -r preserved duplicate <"$XDG_RUNTIME_DIR/caffeine-pids"
uid="$(/usr/bin/id -u)" uid="$(/usr/bin/id -u)"
printf 'Panama %s fixture-user 4101 systemd-inhibit sleep:idle Caffeine block\n' "$uid" mode="$(<"$XDG_RUNTIME_DIR/caffeine-mode")"
printf 'Panama %s fixture-user 4102 systemd-inhibit sleep:idle Caffeine block\n' "$uid" if [[ "$mode" == disappear && "$count" -ge 2 ]]; then
/usr/bin/touch "$XDG_RUNTIME_DIR/release-disappearing-pid"
for _ in $(/usr/bin/seq 1 100); do
[[ ! -e "/proc/$duplicate" ]] && break
/usr/bin/sleep 0.01
done
fi
preserved_comm=systemd-inhibit
if [[ "$mode" == preserve-altered && "$count" -ge 2 ]]; then
preserved_comm=changed-command
fi
printf 'Panama %s fixture-user %s %s sleep:idle Caffeine block\n' "$uid" "$preserved" "$preserved_comm"
if [[ "$mode" != multiplicity || "$count" -lt 2 ]]; then
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$preserved"
fi
if [[ "$mode" == altered && "$count" -ge 2 ]]; then
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Other block\n' "$uid" "$duplicate"
else
printf 'Panama %s fixture-user %s systemd-inhibit sleep:idle Caffeine block\n' "$uid" "$duplicate"
fi
printf 'Other %s fixture-user 4999 systemd-inhibit sleep:idle Caffeine block\n' "$uid" printf 'Other %s fixture-user 4999 systemd-inhibit sleep:idle Caffeine block\n' "$uid"
printf 'Panama 99999 fixture-user 4998 systemd-inhibit sleep:idle Caffeine block\n' printf 'Panama 99999 fixture-user 4998 systemd-inhibit sleep:idle Caffeine block\n'
printf 'Panama %s fixture-user 4997 systemd-inhibit sleep:idle Other block\n' "$uid" printf 'Panama %s fixture-user 4997 systemd-inhibit sleep:idle Other block\n' "$uid"
printf 'Panama %s fixture-user 4996 systemd-inhibit sleep:idle Caffeine delay\n' "$uid" printf 'Panama %s fixture-user 4996 systemd-inhibit sleep:idle Caffeine delay\n' "$uid"
EOF EOF
cat >"$repair_root/setup/scripts/link-vicinae-scripts" <<'EOF' chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" \
#!/usr/bin/bash
set -euo pipefail
printf 'link-vicinae-scripts|%s' "$0" >>"$XDG_RUNTIME_DIR/repair.log"
if (( $# > 0 )); then
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
fi
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
EOF
chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" "$bin_dir/kill" \
"$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts" "$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts"
run_repair() { run_repair() {
HOME="$home" \ HOME="$home" \
PATH="$bin_dir" \ PATH="$bin_dir:/usr/bin" \
XDG_CURRENT_DESKTOP=Hyprland \ XDG_CURRENT_DESKTOP=Hyprland \
PANAMA_DOCTOR_ROOT="$repair_root" \ PANAMA_DOCTOR_ROOT="$repair_root" \
PANAMA_DOCTOR_HOME="$home" \ PANAMA_DOCTOR_HOME="$home" \
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \ PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
PANAMA_DOCTOR_STATE_HOME="$state_home" \ PANAMA_DOCTOR_STATE_HOME="$state_home" \
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \ PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
PANAMA_DOCTOR_PATH="$bin_dir" \ PANAMA_DOCTOR_PATH="$bin_dir:/usr/bin" \
PANAMA_DOCTOR_TIMEOUT=0.2 \ PANAMA_DOCTOR_TIMEOUT=0.2 \
/usr/bin/python3 "$doctor" "$@" /usr/bin/python3 "$doctor" "$@"
} }
@@ -364,58 +396,327 @@ assert_repair_result desktop.vicinae true 5
[[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \ [[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \
|| fail 'failed repair changed the authored argv' || fail 'failed repair changed the authored argv'
# The Vicinae repair executes only the authored setup helper with no arguments. # The real authored Vicinae helper converges the exact child link diagnosed by
: >"$repair_log" # panama-doctor under the isolated HOME.
rm -f "$data_home/vicinae/scripts/panama"
before_vicinae_repair="$(run_repair --json)"
check_status "$before_vicinae_repair" panama.vicinae-commands warning
invoke_repair panama.vicinae-commands invoke_repair panama.vicinae-commands
[[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status" [[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status"
assert_repair_result panama.vicinae-commands true 0 assert_repair_result panama.vicinae-commands true 0
[[ "$(<"$repair_log")" == "link-vicinae-scripts|$repair_root/setup/scripts/link-vicinae-scripts" ]] \ after_vicinae_repair="$(run_repair --json)"
|| fail "Vicinae command repair argv was not exact: $(<"$repair_log")" check_status "$after_vicinae_repair" panama.vicinae-commands ok
[[ -L "$data_home/vicinae/scripts/panama" \
&& "$(readlink "$data_home/vicinae/scripts/panama")" == "$repair_root/config/local/share/vicinae/scripts" ]] \
|| fail 'Vicinae repair did not install the diagnosed child link'
# Runtime-link repair may replace only the four authored symlink names. Broken # Runtime-link repair may replace only absent links or symlinks whose lexical
# or absent links are recreated toward authored tracked destinations; regular # target proves Panama ownership. Every other object remains untouched.
# files and directories remain untouched and make the result incomplete.
for name in hypr quickshell uwsm vicinae; do for name in hypr quickshell uwsm vicinae; do
path="$config_home/$name" path="$config_home/$name"
if [[ -e "$path" || -L "$path" ]]; then if [[ -e "$path" || -L "$path" ]]; then
mv "$path" "$fixture/pre-repair-$name" mv "$path" "$fixture/pre-repair-$name"
fi fi
done done
ln -s "$fixture/missing-hypr" "$config_home/hypr" ln -s "$repair_root/config/dot/hypr" "$config_home/hypr"
ln -s "$fixture/missing-quickshell" "$config_home/quickshell" correct_inode="$(stat -c %i "$config_home/hypr")"
printf 'user-owned file\n' >"$config_home/uwsm" ln -s "$repair_root/config/dot/quickshell" "$config_home/uwsm"
mkdir "$config_home/vicinae" ln -s "$fixture/external-broken-link" "$config_home/vicinae"
ln -s "$fixture/untouched" "$config_home/not-panama" ln -s "$fixture/untouched" "$config_home/not-panama"
: >"$repair_log" : >"$repair_log"
invoke_repair panama.runtime-links invoke_repair panama.runtime-links
[[ "$repair_status" == 1 ]] || fail "blocked runtime-link repair returned $repair_status" [[ "$repair_status" == 1 ]] || fail "blocked runtime-link repair returned $repair_status"
assert_repair_result panama.runtime-links true 1 assert_repair_result panama.runtime-links true 1
[[ -L "$config_home/hypr" && "$(readlink "$config_home/hypr")" == "$repair_root/config/dot/hypr" ]] \ [[ -L "$config_home/hypr" && "$(readlink "$config_home/hypr")" == "$repair_root/config/dot/hypr" ]] \
|| fail 'hypr link was not recreated toward its authored destination' || fail 'correct runtime link changed'
[[ "$(stat -c %i "$config_home/hypr")" == "$correct_inode" ]] \
|| fail 'correct runtime link was replaced instead of left untouched'
[[ -L "$config_home/quickshell" && "$(readlink "$config_home/quickshell")" == "$repair_root/config/dot/quickshell" ]] \ [[ -L "$config_home/quickshell" && "$(readlink "$config_home/quickshell")" == "$repair_root/config/dot/quickshell" ]] \
|| fail 'quickshell link was not recreated toward its authored destination' || fail 'absent quickshell link was not created'
[[ -f "$config_home/uwsm" && "$(<"$config_home/uwsm")" == 'user-owned file' ]] \ [[ -L "$config_home/uwsm" && "$(readlink "$config_home/uwsm")" == "$repair_root/config/dot/uwsm" ]] \
|| fail 'runtime-link repair replaced a regular file' || fail 'provably Panama-owned stale link was not repaired'
[[ -d "$config_home/vicinae" && ! -L "$config_home/vicinae" ]] \ [[ -L "$config_home/vicinae" && "$(readlink "$config_home/vicinae")" == "$fixture/external-broken-link" ]] \
|| fail 'runtime-link repair replaced a user-owned directory' || fail 'external broken symlink was replaced'
[[ -L "$config_home/not-panama" && "$(readlink "$config_home/not-panama")" == "$fixture/untouched" ]] \ [[ -L "$config_home/not-panama" && "$(readlink "$config_home/not-panama")" == "$fixture/untouched" ]] \
|| fail 'runtime-link repair touched an unauthored link name' || fail 'runtime-link repair touched an unauthored link name'
[[ ! -s "$repair_log" ]] || fail 'runtime-link repair launched a process' [[ ! -s "$repair_log" ]] || fail 'runtime-link repair launched a process'
# Caffeine repair parses exact authored metadata, keeps the first valid lock, # Regular files and directories also remain untouched.
# and releases only later exact matches. rm "$config_home/vicinae"
rm "$config_home/uwsm"
printf 'user-owned file\n' >"$config_home/uwsm"
mkdir "$config_home/vicinae"
invoke_repair panama.runtime-links
[[ "$repair_status" == 1 ]] || fail 'file/directory blockers did not make repair incomplete'
[[ -f "$config_home/uwsm" && "$(<"$config_home/uwsm")" == 'user-owned file' ]] \
|| fail 'runtime-link repair replaced a regular file'
[[ -d "$config_home/vicinae" && ! -L "$config_home/vicinae" ]] \
|| fail 'runtime-link repair replaced a user-owned directory'
# An injected exchange failure occurs after the authored candidate symlink is
# made; the original link must still be intact.
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/atomic-config" <<'PY' \
|| fail 'atomic replacement failure did not preserve the original link'
import importlib.util
import importlib.machinery
import os
import sys
from pathlib import Path
doctor_path, root_text, config_text = sys.argv[1:]
loader = importlib.machinery.SourceFileLoader("panama_doctor_contract", doctor_path)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
loader.exec_module(module)
root = Path(root_text)
config_home = Path(config_text)
config_home.mkdir(parents=True)
destination = config_home / "hypr"
original = root / "config/dot/quickshell"
destination.symlink_to(original, target_is_directory=True)
config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
real_exchange = module.rename_exchange
module.rename_exchange = lambda source, target: (_ for _ in ()).throw(OSError("fixture exchange failure"))
try:
result = module.repair_runtime_links(config)
finally:
module.rename_exchange = real_exchange
assert result.exit_code == 1
assert destination.is_symlink()
assert os.readlink(destination) == str(original)
assert not list(config_home.glob(".panama-link-*"))
PY
# A deterministic swap at the ownership/replacement boundary must be detected
# from the exchanged-out object and rolled back, preserving the external link.
/usr/bin/python3 - "$doctor" "$repair_root" "$fixture/toctou-config" "$fixture/external-race-target" <<'PY' \
|| fail 'runtime-link exchange did not restore a boundary-swapped external link'
import importlib.machinery
import importlib.util
import os
import sys
from pathlib import Path
doctor_path, root_text, config_text, external_text = sys.argv[1:]
loader = importlib.machinery.SourceFileLoader("panama_doctor_toctou", doctor_path)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
loader.exec_module(module)
root = Path(root_text)
config_home = Path(config_text)
config_home.mkdir(parents=True)
for name, relative in module.RUNTIME_LINK_TARGETS:
(config_home / name).symlink_to(root / relative, target_is_directory=True)
destination = config_home / "uwsm"
destination.unlink()
destination.symlink_to(root / "config/dot/quickshell", target_is_directory=True)
external = Path(external_text)
real_exchange = module.rename_exchange
first = True
def race_exchange(candidate, target):
global first
if first:
first = False
target.unlink()
target.symlink_to(external, target_is_directory=True)
real_exchange(candidate, target)
module.rename_exchange = race_exchange
config = module.DoctorConfig(root, config_home.parent, config_home, config_home.parent / "state", config_home.parent / "runtime", "", 0.2)
try:
result = module.repair_runtime_links(config)
finally:
module.rename_exchange = real_exchange
assert result.exit_code == 1
assert destination.is_symlink()
assert os.readlink(destination) == str(external)
assert not list(config_home.glob(".panama-link-*"))
PY
# Caffeine repair deduplicates rows, pins each distinct duplicate with a
# pidfd, revalidates authored metadata, and signals only the duplicate.
/usr/bin/sleep 30 &
preserved_pid=$!
child_pids+=("$preserved_pid")
/usr/bin/sleep 30 &
duplicate_pid=$!
child_pids+=("$duplicate_pid")
printf '%s %s\n' "$preserved_pid" "$duplicate_pid" >"$runtime_dir/caffeine-pids"
printf 'dedupe\n' >"$runtime_dir/caffeine-mode"
rm -f "$runtime_dir/caffeine-list-count"
: >"$repair_log" : >"$repair_log"
invoke_repair panama.caffeine invoke_repair panama.caffeine
[[ "$repair_status" == 0 ]] || fail "Caffeine repair returned $repair_status" [[ "$repair_status" == 0 ]] || fail "Caffeine repair returned $repair_status"
assert_repair_result panama.caffeine true 0 assert_repair_result panama.caffeine true 0
expected_caffeine=$'systemd-inhibit|--list|--no-pager|--no-legend\nkill|--|4102' expected_caffeine=$'systemd-inhibit|--list|--no-pager|--no-legend\nsystemd-inhibit|--list|--no-pager|--no-legend'
[[ "$(<"$repair_log")" == "$expected_caffeine" ]] \ [[ "$(<"$repair_log")" == "$expected_caffeine" ]] \
|| fail "Caffeine repair did not preserve/filter exact inhibitors: $(<"$repair_log")" || fail "Caffeine repair did not preserve/filter exact inhibitors: $(<"$repair_log")"
kill -0 "$preserved_pid" >/dev/null 2>&1 || fail 'repeated inhibitor rows killed the preserved process'
for _ in $(seq 1 40); do
kill -0 "$duplicate_pid" >/dev/null 2>&1 || break
sleep 0.05
done
! kill -0 "$duplicate_pid" >/dev/null 2>&1 || fail 'distinct duplicate inhibitor was not terminated'
# Changed second-list metadata invalidates the candidate before any signal.
/usr/bin/sleep 30 &
altered_preserved=$!
child_pids+=("$altered_preserved")
/usr/bin/sleep 30 &
altered_duplicate=$!
child_pids+=("$altered_duplicate")
printf '%s %s\n' "$altered_preserved" "$altered_duplicate" >"$runtime_dir/caffeine-pids"
printf 'altered\n' >"$runtime_dir/caffeine-mode"
rm -f "$runtime_dir/caffeine-list-count"
invoke_repair panama.caffeine
[[ "$repair_status" == 1 ]] || fail 'altered inhibitor metadata was not safely refused'
assert_repair_result panama.caffeine true 1
kill -0 "$altered_preserved" >/dev/null 2>&1 || fail 'metadata refusal signaled the preserved process'
kill -0 "$altered_duplicate" >/dev/null 2>&1 || fail 'metadata refusal signaled the candidate process'
# Changing metadata on the preserved row is also a full-identity mismatch,
# even though every duplicate PID remains present.
/usr/bin/sleep 30 &
preserve_changed_keep=$!
child_pids+=("$preserve_changed_keep")
/usr/bin/sleep 30 &
preserve_changed_duplicate=$!
child_pids+=("$preserve_changed_duplicate")
printf '%s %s\n' "$preserve_changed_keep" "$preserve_changed_duplicate" >"$runtime_dir/caffeine-pids"
printf 'preserve-altered\n' >"$runtime_dir/caffeine-mode"
rm -f "$runtime_dir/caffeine-list-count"
invoke_repair panama.caffeine
[[ "$repair_status" == 1 ]] || fail 'preserved-row metadata change was not safely refused'
assert_repair_result panama.caffeine true 1
kill -0 "$preserve_changed_keep" >/dev/null 2>&1 || fail 'preserved-row mismatch signaled the preserved process'
kill -0 "$preserve_changed_duplicate" >/dev/null 2>&1 || fail 'preserved-row mismatch signaled the duplicate process'
# A repeated exact row disappearing between lists changes multiplicity and is
# refused before signaling any pinned duplicate.
/usr/bin/sleep 30 &
multiplicity_keep=$!
child_pids+=("$multiplicity_keep")
/usr/bin/sleep 30 &
multiplicity_duplicate=$!
child_pids+=("$multiplicity_duplicate")
printf '%s %s\n' "$multiplicity_keep" "$multiplicity_duplicate" >"$runtime_dir/caffeine-pids"
printf 'multiplicity\n' >"$runtime_dir/caffeine-mode"
rm -f "$runtime_dir/caffeine-list-count"
invoke_repair panama.caffeine
[[ "$repair_status" == 1 ]] || fail 'inhibitor row multiplicity change was not safely refused'
assert_repair_result panama.caffeine true 1
kill -0 "$multiplicity_keep" >/dev/null 2>&1 || fail 'multiplicity mismatch signaled the preserved process'
kill -0 "$multiplicity_duplicate" >/dev/null 2>&1 || fail 'multiplicity mismatch signaled the duplicate process'
# The production pidfd release function preflights every candidate before any
# SIGTERM. A refused second preflight leaves both disposable children alive.
/usr/bin/sleep 30 &
preflight_first=$!
child_pids+=("$preflight_first")
/usr/bin/sleep 30 &
preflight_second=$!
child_pids+=("$preflight_second")
/usr/bin/python3 - "$doctor" "$preflight_first" "$preflight_second" <<'PY' \
|| fail 'pidfd preflight failure signaled a disposable duplicate'
import errno
import importlib.machinery
import importlib.util
import os
import signal
import sys
doctor_path = sys.argv[1]
pids = [int(value) for value in sys.argv[2:]]
loader = importlib.machinery.SourceFileLoader("panama_doctor_preflight", doctor_path)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
loader.exec_module(module)
pidfds = [os.pidfd_open(pid, 0) for pid in pids]
calls = []
def sender(pidfd, sig, siginfo, flags):
calls.append(sig)
if sig == 0 and pidfd == pidfds[1]:
raise PermissionError(errno.EPERM, "fixture preflight refusal")
signal.pidfd_send_signal(pidfd, sig, siginfo, flags)
try:
outcome = module.signal_caffeine_pidfds(pidfds, sender)
finally:
for pidfd in pidfds:
os.close(pidfd)
assert outcome == "preflight-failed"
assert calls == [0, 0]
for pid in pids:
os.kill(pid, 0)
PY
kill -0 "$preflight_first" >/dev/null 2>&1 || fail 'preflight refusal killed the first duplicate'
kill -0 "$preflight_second" >/dev/null 2>&1 || fail 'preflight refusal killed the second duplicate'
# A candidate that disappears after pidfd acquisition and second-list request
# is a safe failure; an unrelated disposable process must remain untouched.
/usr/bin/sleep 30 &
unrelated_pid=$!
child_pids+=("$unrelated_pid")
(
/usr/bin/sleep 30 &
disappearing_pid=$!
trap 'kill "$disappearing_pid" >/dev/null 2>&1 || true; wait "$disappearing_pid" >/dev/null 2>&1 || true' EXIT
printf '%s\n' "$disappearing_pid" >"$runtime_dir/disappearing-pid"
while [[ ! -e "$runtime_dir/release-disappearing-pid" ]]; do
/usr/bin/sleep 0.01
done
kill "$disappearing_pid"
wait "$disappearing_pid" >/dev/null 2>&1 || true
trap - EXIT
) &
disappearance_controller=$!
child_pids+=("$disappearance_controller")
for _ in $(seq 1 100); do
[[ -s "$runtime_dir/disappearing-pid" ]] && break
sleep 0.01
done
[[ -s "$runtime_dir/disappearing-pid" ]] || fail 'disappearing PID fixture did not start'
disappearing_pid="$(<"$runtime_dir/disappearing-pid")"
printf '%s %s\n' "$altered_preserved" "$disappearing_pid" >"$runtime_dir/caffeine-pids"
printf 'disappear\n' >"$runtime_dir/caffeine-mode"
rm -f "$runtime_dir/caffeine-list-count"
invoke_repair panama.caffeine
[[ "$repair_status" == 1 ]] || fail 'disappeared inhibitor PID was not safely refused'
assert_repair_result panama.caffeine true 1
wait "$disappearance_controller"
kill -0 "$unrelated_pid" >/dev/null 2>&1 || fail 'PID disappearance signaled an unrelated process'
# Rejected IDs are complete JSON, exit 2, and cause neither a process launch # Rejected IDs are complete JSON, exit 2, and cause neither a process launch
# nor a filesystem mutation. # nor a filesystem mutation.
fixture_state() { fixture_state() {
find "$config_home" -mindepth 1 -printf '%P|%y|%l\n' | sort | sha256sum | awk '{print $1}' /usr/bin/python3 - "$fixture" <<'PY'
import hashlib
import os
import stat
import sys
from pathlib import Path
root = Path(sys.argv[1])
digest = hashlib.sha256()
for path in sorted(root.rglob("*"), key=lambda item: os.fsencode(str(item.relative_to(root)))):
relative = os.fsencode(str(path.relative_to(root)))
metadata = path.lstat()
digest.update(relative + b"\0" + oct(stat.S_IMODE(metadata.st_mode)).encode() + b"\0")
if path.is_symlink():
digest.update(b"link\0" + os.fsencode(os.readlink(path)) + b"\0")
elif path.is_file():
digest.update(b"file\0" + hashlib.sha256(path.read_bytes()).digest())
elif path.is_dir():
digest.update(b"dir\0")
else:
digest.update(b"other\0")
print(digest.hexdigest())
PY
} }
for rejected_id in unknown.check integration.home-assistant input.brightness \ for rejected_id in unknown.check integration.home-assistant input.brightness \
desktop.notifications ../../escape 'desktop.vicinae;touch injected'; do desktop.notifications ../../escape 'desktop.vicinae;touch injected'; do