Add bounded Panama recovery actions

This commit is contained in:
Gabriel Brown
2026-08-18 10:35:18 -04:00
parent d855a26bd9
commit 2cc109f5e1
6 changed files with 596 additions and 24 deletions
@@ -11,6 +11,11 @@ Item {
property int actionActivationCount: 0
signal actionRequested(var check)
readonly property bool repairWorking: Health.repairingId === root.check.id
readonly property bool repairFailed: root.check.status !== "ok"
&& Health.lastRepair.checkId === root.check.id
&& (Health.lastRepair.accepted === false || Health.lastRepair.exitCode !== 0)
objectName: `health-check-row:${root.issue ? "issue" : "quiet"}:${root.check.id}`
implicitHeight: 62
@@ -38,6 +43,14 @@ Item {
return Theme.fgMuted;
}
function displayedStatus(): string {
if (root.repairWorking)
return "Working…";
if (root.repairFailed)
return "Repair failed";
return root.statusLabel(root.check.status);
}
Rectangle {
id: issueMark
@@ -91,7 +104,7 @@ Item {
Text {
objectName: `health-status-text:${root.issue ? "issue" : "quiet"}:${root.check.id}`
anchors.verticalCenter: parent.verticalCenter
text: root.statusLabel(root.check.status)
text: root.displayedStatus()
color: root.statusColor(root.check.status)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
@@ -102,8 +115,8 @@ Item {
objectName: `health-row-action:${root.issue ? "issue" : "quiet"}:${root.check.id}`
visible: root.check.action !== undefined
text: Health.repairingId === root.check.id ? "Working…" : (root.check.action?.label ?? "")
enabled: visible && Health.repairingId !== root.check.id && !Health.busy
text: root.check.action?.label ?? ""
enabled: visible && !root.repairWorking && !Health.busy
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
+174 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Read-only, redacted diagnostics for Panama-owned desktop functionality."""
"""Redacted diagnostics and bounded repairs for Panama-owned functionality."""
from __future__ import annotations
@@ -15,6 +15,7 @@ from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from types import MappingProxyType
from typing import Callable, Literal
Status = Literal["ok", "warning", "error", "unconfigured"]
@@ -73,6 +74,23 @@ class CommandResult:
stdout: str = ""
@dataclass(frozen=True)
class RepairResult:
check_id: str
accepted: bool
exit_code: int
message: str
def as_json(self) -> dict[str, object]:
return {
"schemaVersion": 1,
"checkId": self.check_id,
"accepted": self.accepted,
"exitCode": self.exit_code,
"message": self.message,
}
CHECK_ORDER = (
"desktop.hyprland", "desktop.quickshell", "desktop.notifications", "desktop.portals",
"desktop.hyprpaper", "desktop.hypridle", "desktop.vicinae", "input.pipewire",
@@ -90,6 +108,19 @@ SYSTEMCTL_COMMANDS = {
"nextcloud": ("systemctl", "--user", "is-active", "--quiet", "nextcloud.service"),
"rustdesk": ("systemctl", "--user", "is-active", "--quiet", "rustdesk.service"),
}
REPAIR_COMMANDS = MappingProxyType({
"desktop.hyprpaper": ("systemctl", "--user", "restart", "hyprpaper.service"),
"desktop.hypridle": ("systemctl", "--user", "restart", "hypridle.service"),
"desktop.vicinae": ("systemctl", "--user", "restart", "vicinae.service"),
"desktop.quickshell": ("panama-action", "restart-shell"),
})
RUNTIME_LINK_TARGETS = (
("hypr", Path("config/dot/hypr")),
("quickshell", Path("config/dot/quickshell")),
("uwsm", Path("config/dot/uwsm")),
("vicinae", Path("config/dot/vicinae")),
)
REPAIR_IDS = frozenset((*REPAIR_COMMANDS.keys(), "panama.runtime-links", "panama.vicinae-commands", "panama.caffeine"))
PROCESS_NAMES = ("quickshell", "vicinae", "hyprpaper", "hypridle")
VERSION_PATTERN = re.compile(r"\b\d+(?:\.\d+){0,3}(?:[-+._][A-Za-z0-9._-]+)?\b")
REVISION_PATTERN = re.compile(r"\b[0-9a-f]{7,40}\b", re.IGNORECASE)
@@ -174,6 +205,28 @@ def run_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None
return CommandResult("ok", completed.stdout)
def run_repair_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> tuple[int, str]:
"""Execute one authored repair argv and retain output only for strict parsing."""
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=config.timeout,
check=False,
env=config.command_env,
cwd=cwd,
)
except FileNotFoundError:
return 127, ""
except subprocess.TimeoutExpired:
return 124, ""
except OSError:
return 126, ""
exit_code = completed.returncode if 0 <= completed.returncode <= 255 else 1
return exit_code, completed.stdout
def executable_exists(name: str, config: DoctorConfig) -> bool:
return shutil.which(name, path=config.path) is not None
@@ -340,8 +393,16 @@ def check_calendar(config: DoctorConfig) -> Check:
def check_runtime_links(config: DoctorConfig) -> Check:
names = ("hypr", "quickshell", "uwsm", "vicinae")
if any(not (config.config_home / name).is_symlink() or not (config.config_home / name).exists() for name in names):
def valid_link(name: str, relative_source: Path) -> bool:
destination = config.config_home / name
source = config.root / relative_source
try:
return source.is_dir() and destination.is_symlink() \
and destination.resolve(strict=False) == source.resolve(strict=True)
except OSError:
return False
if any(not valid_link(name, relative_source) for name, relative_source in RUNTIME_LINK_TARGETS):
return Check("panama.runtime-links", "panama-tools", "Panama runtime links", "warning", "One or more Panama runtime links are unavailable.", Action("repair", "Repair runtime links"))
return Check("panama.runtime-links", "panama-tools", "Panama runtime links", "ok", "Panama runtime links are available.")
@@ -466,12 +527,121 @@ def snapshot(config: DoctorConfig) -> dict[str, object]:
return {"schemaVersion": 1, "generatedAt": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), "summary": {"status": overall, "healthy": counts["ok"], "warnings": counts["warning"], "errors": counts["error"], "unconfigured": counts["unconfigured"]}, "context": {"session": session, "versions": versions}, "checks": [check_json(check) for check in checks]}
def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult:
command = REPAIR_COMMANDS[check_id]
exit_code, _ = run_repair_command(command, config)
message = "Repair completed. A fresh health check will verify recovery." if exit_code == 0 \
else "The authored repair command could not be completed."
return RepairResult(check_id, True, exit_code, message)
def repair_runtime_links(config: DoctorConfig) -> RepairResult:
sources = [(name, config.root / relative_source) for name, relative_source in RUNTIME_LINK_TARGETS]
if any(not source.is_dir() for _, source in sources):
return RepairResult("panama.runtime-links", True, 1, "Tracked Panama link destinations are unavailable.")
try:
config.config_home.mkdir(parents=True, exist_ok=True)
except OSError:
return RepairResult("panama.runtime-links", True, 1, "Panama runtime links could not be accessed.")
blocked = False
failed = False
for name, source in sources:
destination = config.config_home / name
try:
if destination.is_symlink():
if destination.resolve(strict=False) == source.resolve(strict=True):
continue
destination.unlink()
destination.symlink_to(source, target_is_directory=True)
elif destination.exists():
# A regular file or directory is user-owned unless proven
# otherwise. Report it, but never replace it.
blocked = True
else:
destination.symlink_to(source, target_is_directory=True)
except OSError:
failed = True
if failed:
return RepairResult("panama.runtime-links", True, 1, "One or more Panama runtime links could not be recreated.")
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, 0, "Panama runtime links were recreated. A fresh health check will verify them.")
def repair_vicinae_commands(config: DoctorConfig) -> RepairResult:
helper = config.root / "setup/scripts/link-vicinae-scripts"
if not helper.is_file():
return RepairResult("panama.vicinae-commands", True, 127, "The authored Vicinae link helper is unavailable.")
exit_code, _ = run_repair_command((str(helper),), config, config.root)
message = "Panama commands were relinked. A fresh health check will verify them." if exit_code == 0 \
else "Panama commands could not be relinked."
return RepairResult("panama.vicinae-commands", True, exit_code, message)
def repair_caffeine(config: DoctorConfig) -> RepairResult:
list_command = ("systemd-inhibit", "--list", "--no-pager", "--no-legend")
list_exit, output = run_repair_command(list_command, config)
if list_exit != 0:
return RepairResult("panama.caffeine", True, list_exit, "Caffeine inhibitors could not be inspected.")
uid = str(os.getuid())
inhibitor_pids: list[str] = []
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:
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
if parts[6] != "Caffeine" or parts[7] != "block":
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:
return RepairResult("panama.caffeine", True, 0, "No duplicate Panama Caffeine inhibitors needed release.")
for pid in inhibitor_pids[1:]:
exit_code, _ = run_repair_command(("kill", "--", pid), config)
if exit_code != 0:
return RepairResult("panama.caffeine", True, exit_code, "A duplicate Panama Caffeine inhibitor could not be released.")
return RepairResult("panama.caffeine", True, 0, "Duplicate Panama Caffeine inhibitors were released. A fresh health check will verify recovery.")
def repair(check_id: str, config: DoctorConfig) -> RepairResult:
if check_id in REPAIR_COMMANDS:
return repair_authored_command(check_id, config)
if check_id == "panama.runtime-links":
return repair_runtime_links(config)
if check_id == "panama.vicinae-commands":
return repair_vicinae_commands(config)
if check_id == "panama.caffeine":
return repair_caffeine(config)
return RepairResult(check_id, False, 2, "This health check has no authored repair.")
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description="Read-only Panama system diagnostics")
parser = argparse.ArgumentParser(description="Panama system diagnostics and bounded repairs")
output = parser.add_mutually_exclusive_group()
output.add_argument("--json", action="store_true")
output.add_argument("--summary", action="store_true")
parser.add_argument("--repair", metavar="CHECK_ID")
args = parser.parse_args(argv)
if args.repair is not None:
if args.repair not in REPAIR_IDS or args.summary:
result = RepairResult(args.repair, False, 2, "This health check has no authored repair.")
else:
try:
result = repair(args.repair, config_from_environment())
except Exception:
result = RepairResult(args.repair, True, 1, "The authored repair could not be completed.")
print(json.dumps(result.as_json(), separators=(",", ":"), sort_keys=False))
return result.exit_code
result = snapshot(config_from_environment())
if args.summary:
summary = result["summary"]
+94 -8
View File
@@ -24,9 +24,10 @@ Singleton {
property var lastRepair: ({})
property string lastCopyResult: ""
property bool startupScanEnabled: true
property bool postRepairScanPending: false
readonly property bool actionable: root.status === "warning" || root.status === "error"
readonly property bool busy: scanProcess.running || repairProcess.running
readonly property bool busy: scanProcess.running || repairProcess.running || root.postRepairScanPending
readonly property string helperPath: Quickshell.env("PANAMA_HEALTH_HELPER")
|| Quickshell.shellDir + "/scripts/panama-doctor"
readonly property var statuses: ["ok", "warning", "error", "unconfigured"]
@@ -68,8 +69,25 @@ Singleton {
property string checkId: ""
property bool external: false
property string outputText: ""
property int exitCode: -1
property bool exited: false
property bool streamFinished: false
property bool settled: false
onExited: (exitCode, exitStatus) => root.finishRepair(exitCode, checkId, external)
stdout: StdioCollector {
onStreamFinished: {
repairProcess.outputText = this.text;
repairProcess.streamFinished = true;
root.settleRepair();
}
}
onExited: (exitCode, exitStatus) => {
repairProcess.exitCode = exitCode;
repairProcess.exited = true;
root.settleRepair();
}
}
Process {
@@ -226,26 +244,92 @@ Singleton {
root.lastError = "";
repairProcess.checkId = id;
repairProcess.external = external;
repairProcess.outputText = "";
repairProcess.exitCode = -1;
repairProcess.exited = false;
repairProcess.streamFinished = false;
repairProcess.settled = false;
repairProcess.exec([root.helperPath, "--repair", id, "--json"]);
return true;
}
function finishRepair(exitCode: int, id: string, external: bool): void {
const succeeded = exitCode === 0;
function settleRepair(): void {
if (repairProcess.settled || !repairProcess.exited || !repairProcess.streamFinished)
return;
repairProcess.settled = true;
root.finishRepair(
repairProcess.exitCode,
repairProcess.checkId,
repairProcess.external,
repairProcess.outputText
);
}
function finishRepair(exitCode: int, id: string, external: bool, text: string): void {
let candidate;
try {
candidate = JSON.parse(text.trim());
} catch (error) {
candidate = null;
}
const result = root.validRepairResult(candidate, id, exitCode)
? {
schemaVersion: 1,
checkId: candidate.checkId,
accepted: candidate.accepted,
exitCode: candidate.exitCode,
message: candidate.message
}
: {
schemaVersion: 1,
checkId: id,
accepted: false,
exitCode: exitCode,
message: "Panama returned an invalid repair response."
};
const failed = !result.accepted || result.exitCode !== 0;
root.repairingId = "";
root.lastRepair = ({ id: id, succeeded: succeeded });
if (!succeeded) {
root.lastError = "Panama could not repair this item. Try refreshing or use the recommended setup steps.";
if (external) {
root.lastRepair = result;
root.lastError = failed ? result.message : "";
if (failed && external && !failureNotification.running) {
failureNotification.exec([
"notify-send", "-a", "Panama", "-i", "dialog-error-symbolic",
"Panama action failed", "The requested health repair could not be completed."
]);
}
// Any refresh requested while the repair was running is satisfied by
// this one observed post-repair scan. Process exit alone never changes
// the accepted check rows.
root.queuedRefresh = false;
root.postRepairScanPending = true;
Qt.callLater(root.startPostRepairScan);
}
function startPostRepairScan(): void {
root.postRepairScanPending = false;
root.queuedRefresh = false;
root.refresh();
}
function validRepairResult(candidate: var, id: string, processExitCode: int): bool {
if (!root.plainObject(candidate))
return false;
const keys = Object.keys(candidate).sort();
const expectedKeys = ["accepted", "checkId", "exitCode", "message", "schemaVersion"];
if (keys.length !== expectedKeys.length
|| !keys.every((key, index) => key === expectedKeys[index]))
return false;
return candidate.schemaVersion === 1
&& candidate.checkId === id
&& typeof candidate.accepted === "boolean"
&& Number.isInteger(candidate.exitCode)
&& candidate.exitCode === processExitCode
&& typeof candidate.message === "string"
&& candidate.message.length > 0;
}
function copyReport(): bool {
if (copyProcess.running)
return false;
@@ -265,6 +349,8 @@ Singleton {
generation: root.generation,
acceptedGeneration: root.acceptedGeneration,
repairingId: root.repairingId,
lastRepair: root.lastRepair,
lastError: root.lastError,
checks: root.checks.map(check => check.id),
checkStates: root.checks.map(check => ({ id: check.id, status: check.status }))
};
+83 -7
View File
@@ -75,15 +75,26 @@ fixture_dir="$(mktemp -d /tmp/panama-health.XXXXXX)"
helper="$fixture_dir/panama-doctor"
copy_bin="$fixture_dir/bin"
copy_file="$fixture_dir/copied-report.json"
repair_mode_file="$fixture_dir/repair-mode"
repair_log="$fixture_dir/repair.log"
notification_log="$fixture_dir/notifications.log"
printf 'success\n' >"$repair_mode_file"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "%s\n" "$*" >>"$PANAMA_HEALTH_REPAIR_LOG"' \
'if [[ "$1" == "--json" ]]; then' \
' sleep 0.2' \
" printf '%s\\n' '$warning_snapshot'" \
' exit 0' \
'fi' \
'if [[ "$1" == "--repair" && "$2" == "panama.caffeine" && "$3" == "--json" ]]; then' \
' exit 0' \
' sleep 0.25' \
' case "$(cat "$PANAMA_HEALTH_REPAIR_MODE_FILE")" in' \
' success) printf "{\"schemaVersion\":1,\"checkId\":\"panama.caffeine\",\"accepted\":true,\"exitCode\":0,\"message\":\"Duplicate inhibitors were released.\"}\\n"; exit 0 ;;' \
' failed) printf "{\"schemaVersion\":1,\"checkId\":\"panama.caffeine\",\"accepted\":true,\"exitCode\":7,\"message\":\"Duplicate inhibitors could not be released.\"}\\n"; exit 7 ;;' \
' mismatch) printf "{\"schemaVersion\":1,\"checkId\":\"desktop.vicinae\",\"accepted\":true,\"exitCode\":0,\"message\":\"Wrong row.\"}\\n"; exit 0 ;;' \
' *) printf "not-json\\n"; exit 0 ;;' \
' esac' \
'fi' \
'exit 2' >"$helper"
chmod +x "$helper"
@@ -91,9 +102,16 @@ mkdir -p "$copy_bin"
printf '%s\n' \
'#!/usr/bin/env bash' \
'/usr/bin/cat > "$PANAMA_HEALTH_COPY_FILE"' >"$copy_bin/wl-copy"
chmod +x "$copy_bin/wl-copy"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "%s\n" "$*" >>"$PANAMA_HEALTH_NOTIFICATION_LOG"' >"$copy_bin/notify-send"
chmod +x "$copy_bin/wl-copy" "$copy_bin/notify-send"
run() { PATH="$copy_bin:$PATH" PANAMA_HEALTH_HELPER="$helper" PANAMA_HEALTH_COPY_FILE="$copy_file" qs -p "$harness" "$@"; }
run() {
PATH="$copy_bin:$PATH" PANAMA_HEALTH_HELPER="$helper" PANAMA_HEALTH_COPY_FILE="$copy_file" \
PANAMA_HEALTH_REPAIR_MODE_FILE="$repair_mode_file" PANAMA_HEALTH_REPAIR_LOG="$repair_log" \
PANAMA_HEALTH_NOTIFICATION_LOG="$notification_log" qs -p "$harness" "$@"
}
harness_pid=""
cleanup() {
@@ -103,6 +121,8 @@ cleanup() {
trap cleanup EXIT
PATH="$copy_bin:$PATH" PANAMA_HEALTH_HELPER="$helper" PANAMA_HEALTH_COPY_FILE="$copy_file" \
PANAMA_HEALTH_REPAIR_MODE_FILE="$repair_mode_file" PANAMA_HEALTH_REPAIR_LOG="$repair_log" \
PANAMA_HEALTH_NOTIFICATION_LOG="$notification_log" \
qs -p "$harness" --daemonize >/dev/null
for _ in $(seq 1 40); do
run ipc show 2>/dev/null | rg -q '^target health-test$' && break
@@ -165,25 +185,81 @@ done
jq -e '.busy == false and .generation == ($before + 2) and .queuedRefresh == false' --argjson before "$before_generation" \
>/dev/null <<<"$state" || fail "queued refresh did not run exactly once: $state"
printf 'success\n' >"$repair_mode_file"
repair_generation="$(jq -r .generation <<<"$state")"
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|| fail 'repairable check was refused'
run ipc call health-test queue >/dev/null
working_state="$(run ipc call health-test status)"
jq -e '.repairingId == "panama.caffeine" and .queuedRefresh == true
and (.checkStates[] | select(.id == "panama.caffeine") | .status) == "warning"' \
>/dev/null <<<"$working_state" || fail "repair did not retain the degraded row while working: $working_state"
for _ in $(seq 1 120); do
state="$(run ipc call health-test status)"
jq -e '.busy == false and .generation == ($before + 3)' --argjson before "$before_generation" \
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false' --argjson before "$repair_generation" \
>/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.busy == false and .generation == ($before + 3)' --argjson before "$before_generation" \
>/dev/null <<<"$state" || fail "accepted repair did not trigger one rescan: $state"
jq -e '.busy == false and .generation == ($before + 1) and .queuedRefresh == false
and .lastRepair == {schemaVersion:1, checkId:"panama.caffeine", accepted:true, exitCode:0, message:"Duplicate inhibitors were released."}
and (.checkStates[] | select(.id == "panama.caffeine") | .status) == "warning"' \
--argjson before "$repair_generation" >/dev/null <<<"$state" \
|| fail "accepted repair was trusted before exactly one observed rescan: $state"
# A syntactically valid command failure remains inline for Settings and still
# receives exactly one observed rescan.
printf 'failed\n' >"$repair_mode_file"
failure_generation="$(jq -r .generation <<<"$state")"
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|| fail 'second repairable check 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 "$failure_generation" \
>/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.lastRepair.checkId == "panama.caffeine"
and .lastRepair.accepted == true and .lastRepair.exitCode == 7
and .lastRepair.message == "Duplicate inhibitors could not be released."
and .generation == ($before + 1)' --argjson before "$failure_generation" \
>/dev/null <<<"$state" || fail "known repair failure was not retained inline: $state"
[[ ! -e "$notification_log" || ! -s "$notification_log" ]] \
|| fail 'Settings-originated repair emitted an external notification'
# A malformed or mismatched helper response is contained and cannot masquerade
# as recovery; it also schedules only one scan.
printf 'mismatch\n' >"$repair_mode_file"
mismatch_generation="$(jq -r .generation <<<"$state")"
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|| fail 'mismatch repair fixture 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 "$mismatch_generation" \
>/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.lastRepair.checkId == "panama.caffeine" and .lastRepair.accepted == false
and .lastRepair.exitCode == 0 and .generation == ($before + 1)' \
--argjson before "$mismatch_generation" >/dev/null <<<"$state" \
|| fail "mismatched repair JSON escaped containment: $state"
[[ "$(run ipc call health-test repair unknown.check)" == "false" ]] \
|| fail 'unknown check started a repair'
[[ "$(run ipc call health-test repair integration.calendar)" == "false" ]] \
|| fail 'non-repairable check started a repair'
state="$(run ipc call health-test status)"
jq -e '.repairingId == "" and .generation == ($before + 3)' --argjson before "$before_generation" \
jq -e '.repairingId == "" and .generation == ($before + 1)' --argjson before "$mismatch_generation" \
>/dev/null <<<"$state" || fail "rejected repair altered process state: $state"
python3 - "$service" <<'PY' || fail 'external repair failure notification is not bounded'
import sys
source = open(sys.argv[1], encoding="utf-8").read()
assert 'if (failed && external && !failureNotification.running)' in source
assert '"notify-send", "-a", "Panama", "-i", "dialog-error-symbolic"' in source
assert '"Panama action failed", "The requested health repair could not be completed."' in source
PY
trap - EXIT
cleanup
printf 'health service contract: PASS\n'
+35 -1
View File
@@ -114,6 +114,11 @@ cp -a "$repo_dir/config/dot/quickshell" "$config_path"
cat >"$helper" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "--repair" ]]; then
sleep 0.35
printf '{"schemaVersion":1,"checkId":"%s","accepted":true,"exitCode":7,"message":"The authored repair failed."}\n' "$2"
exit 7
fi
sleep 1.5
printf '%s\n' "$PANAMA_HEALTH_FIXTURE"
EOF
@@ -296,13 +301,42 @@ jq -e '
and (.focusChain | any(startswith("health-row-action:")))
' >/dev/null <<<"$settled_state" || fail "actual focus-chain traversal does not reach hero and row actions: $settled_state"
settled_heights="$(jq -c .rowHeights <<<"$settled_state")"
[[ "$(run ipc call health-ui-test request desktop.vicinae)" == "true" ]] \
|| fail 'inline repair fixture could not be requested'
working_repair_state="$(run ipc call health-ui-test state)"
jq -e '.renderedRows[] | select(.id == "desktop.vicinae") | .statusText == "Working…"' \
>/dev/null <<<"$working_repair_state" || fail "repair row did not show Working state: $working_repair_state"
[[ "$(jq -c .rowHeights <<<"$working_repair_state")" == "$settled_heights" ]] \
|| fail 'repair Working state changed row geometry'
for _ in $(seq 1 40); do
failed_repair_state="$(run ipc call health-ui-test state)"
jq -e '.renderedRows[] | select(.id == "desktop.vicinae") | .statusText == "Repair failed"' \
>/dev/null <<<"$failed_repair_state" && break
sleep 0.1
done
jq -e '.renderedRows[] | select(.id == "desktop.vicinae") | .statusText == "Repair failed"' \
>/dev/null <<<"$failed_repair_state" || fail "repair failure was not shown inline: $failed_repair_state"
[[ "$(jq -c .rowHeights <<<"$failed_repair_state")" == "$settled_heights" ]] \
|| fail 'repair failure changed row geometry'
[[ "$(jq -r '.renderedRows | map(.id) | unique | length' <<<"$failed_repair_state")" == 6 ]] \
|| fail 'repair state duplicated a health action row'
for _ in $(seq 1 40); do
failed_repair_state="$(run ipc call health-ui-test state)"
[[ "$(jq -r .checking <<<"$failed_repair_state")" == "false" ]] && break
sleep 0.1
done
[[ "$(jq -r .checking <<<"$failed_repair_state")" == "false" ]] \
|| fail 'post-repair scan did not settle before the next action'
[[ "$(run ipc call health-ui-test request desktop.quickshell)" == "true" ]] \
|| fail 'restart confirmation fixture could not be requested'
confirmation_state="$(run ipc call health-ui-test state)"
jq -e '
.confirmationVisible == true
and .confirmationId == "desktop.quickshell"
and .activatedRows == ["health-check-row:issue:desktop.quickshell"]
and (.activatedRows | index("health-check-row:issue:desktop.quickshell")) != null
and (.activatedRows | map(select(endswith(":desktop.quickshell"))) | length) == 1
' \
>/dev/null <<<"$confirmation_state" || fail 'Quickshell restart did not open confirmation sheet'
+193
View File
@@ -235,4 +235,197 @@ summary="$(run_doctor --summary)"
[[ "$summary" =~ ^Panama\ system\ health:\ (healthy|warning|error)\ \([0-9]+\ ok,\ [0-9]+\ warnings,\ [0-9]+\ errors,\ [0-9]+\ unconfigured\)$ ]] \
|| fail "summary is not concise: $summary"
# Repairs run against a second, disposable Panama root. Every process boundary
# records its argv, and every filesystem assertion is confined to this fixture.
repair_root="$fixture/repair-root"
repair_log="$runtime_dir/repair.log"
mkdir -p "$repair_root/config/dot" "$repair_root/setup/scripts"
for name in hypr quickshell uwsm vicinae; do
mkdir -p "$repair_root/config/dot/$name"
done
mv "$bin_dir/systemctl" "$bin_dir/systemctl-probe"
cat >"$bin_dir/systemctl" <<'EOF'
#!/usr/bin/bash
set -euo pipefail
if [[ "${1:-}" == "--user" && "${2:-}" == "restart" ]]; then
printf 'systemctl' >>"$XDG_RUNTIME_DIR/repair.log"
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
[[ ! -e "$XDG_RUNTIME_DIR/fail-repair" ]] || exit 5
exit 0
fi
exec "${0%/*}/systemctl-probe" "$@"
EOF
cat >"$bin_dir/panama-action" <<'EOF'
#!/usr/bin/bash
set -euo pipefail
printf 'panama-action' >>"$XDG_RUNTIME_DIR/repair.log"
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
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'
#!/usr/bin/bash
set -euo pipefail
printf 'systemd-inhibit' >>"$XDG_RUNTIME_DIR/repair.log"
printf '|%s' "$@" >>"$XDG_RUNTIME_DIR/repair.log"
printf '\n' >>"$XDG_RUNTIME_DIR/repair.log"
uid="$(/usr/bin/id -u)"
printf 'Panama %s fixture-user 4101 systemd-inhibit sleep:idle Caffeine block\n' "$uid"
printf 'Panama %s fixture-user 4102 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 %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"
EOF
cat >"$repair_root/setup/scripts/link-vicinae-scripts" <<'EOF'
#!/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"
run_repair() {
HOME="$home" \
PATH="$bin_dir" \
XDG_CURRENT_DESKTOP=Hyprland \
PANAMA_DOCTOR_ROOT="$repair_root" \
PANAMA_DOCTOR_HOME="$home" \
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
PANAMA_DOCTOR_STATE_HOME="$state_home" \
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
PANAMA_DOCTOR_PATH="$bin_dir" \
PANAMA_DOCTOR_TIMEOUT=0.2 \
/usr/bin/python3 "$doctor" "$@"
}
repair_output=""
repair_status=0
invoke_repair() {
set +e
repair_output="$(run_repair --repair "$1" --json)"
repair_status=$?
set -e
}
assert_repair_result() {
local id="$1" accepted="$2" exit_code="$3"
jq -e --arg id "$id" --argjson accepted "$accepted" --argjson exitCode "$exit_code" '
(keys | sort) == ["accepted", "checkId", "exitCode", "message", "schemaVersion"]
and .schemaVersion == 1
and .checkId == $id
and .accepted == $accepted
and .exitCode == $exitCode
and (.message | type == "string" and length > 0)
' >/dev/null <<<"$repair_output" || fail "invalid repair result for $id: $repair_output"
}
for repair_case in \
'desktop.hyprpaper|systemctl|--user|restart|hyprpaper.service' \
'desktop.hypridle|systemctl|--user|restart|hypridle.service' \
'desktop.vicinae|systemctl|--user|restart|vicinae.service' \
'desktop.quickshell|panama-action|restart-shell'; do
IFS='|' read -r repair_id executable arg1 arg2 arg3 <<<"$repair_case"
: >"$repair_log"
invoke_repair "$repair_id"
[[ "$repair_status" == 0 ]] || fail "$repair_id returned $repair_status"
assert_repair_result "$repair_id" true 0
expected="$executable|$arg1"
[[ -z "$arg2" ]] || expected+="|$arg2"
[[ -z "$arg3" ]] || expected+="|$arg3"
[[ "$(<"$repair_log")" == "$expected" ]] \
|| fail "$repair_id argv was not exact: $(<"$repair_log")"
done
# Known process failures still return complete JSON and preserve the command's
# exit status for the QML state machine.
touch "$runtime_dir/fail-repair"
: >"$repair_log"
invoke_repair desktop.vicinae
rm "$runtime_dir/fail-repair"
[[ "$repair_status" == 5 ]] || fail "failed repair returned $repair_status instead of 5"
assert_repair_result desktop.vicinae true 5
[[ "$(<"$repair_log")" == 'systemctl|--user|restart|vicinae.service' ]] \
|| fail 'failed repair changed the authored argv'
# The Vicinae repair executes only the authored setup helper with no arguments.
: >"$repair_log"
invoke_repair panama.vicinae-commands
[[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status"
assert_repair_result panama.vicinae-commands true 0
[[ "$(<"$repair_log")" == "link-vicinae-scripts|$repair_root/setup/scripts/link-vicinae-scripts" ]] \
|| fail "Vicinae command repair argv was not exact: $(<"$repair_log")"
# Runtime-link repair may replace only the four authored symlink names. Broken
# or absent links are recreated toward authored tracked destinations; regular
# files and directories remain untouched and make the result incomplete.
for name in hypr quickshell uwsm vicinae; do
path="$config_home/$name"
if [[ -e "$path" || -L "$path" ]]; then
mv "$path" "$fixture/pre-repair-$name"
fi
done
ln -s "$fixture/missing-hypr" "$config_home/hypr"
ln -s "$fixture/missing-quickshell" "$config_home/quickshell"
printf 'user-owned file\n' >"$config_home/uwsm"
mkdir "$config_home/vicinae"
ln -s "$fixture/untouched" "$config_home/not-panama"
: >"$repair_log"
invoke_repair panama.runtime-links
[[ "$repair_status" == 1 ]] || fail "blocked runtime-link repair returned $repair_status"
assert_repair_result panama.runtime-links true 1
[[ -L "$config_home/hypr" && "$(readlink "$config_home/hypr")" == "$repair_root/config/dot/hypr" ]] \
|| fail 'hypr link was not recreated toward its authored destination'
[[ -L "$config_home/quickshell" && "$(readlink "$config_home/quickshell")" == "$repair_root/config/dot/quickshell" ]] \
|| fail 'quickshell link was not recreated toward its authored destination'
[[ -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'
[[ -L "$config_home/not-panama" && "$(readlink "$config_home/not-panama")" == "$fixture/untouched" ]] \
|| fail 'runtime-link repair touched an unauthored link name'
[[ ! -s "$repair_log" ]] || fail 'runtime-link repair launched a process'
# Caffeine repair parses exact authored metadata, keeps the first valid lock,
# and releases only later exact matches.
: >"$repair_log"
invoke_repair panama.caffeine
[[ "$repair_status" == 0 ]] || fail "Caffeine repair returned $repair_status"
assert_repair_result panama.caffeine true 0
expected_caffeine=$'systemd-inhibit|--list|--no-pager|--no-legend\nkill|--|4102'
[[ "$(<"$repair_log")" == "$expected_caffeine" ]] \
|| fail "Caffeine repair did not preserve/filter exact inhibitors: $(<"$repair_log")"
# Rejected IDs are complete JSON, exit 2, and cause neither a process launch
# nor a filesystem mutation.
fixture_state() {
find "$config_home" -mindepth 1 -printf '%P|%y|%l\n' | sort | sha256sum | awk '{print $1}'
}
for rejected_id in unknown.check integration.home-assistant input.brightness \
desktop.notifications ../../escape 'desktop.vicinae;touch injected'; do
: >"$repair_log"
before_state="$(fixture_state)"
invoke_repair "$rejected_id"
[[ "$repair_status" == 2 ]] || fail "$rejected_id returned $repair_status instead of 2"
assert_repair_result "$rejected_id" false 2
[[ ! -s "$repair_log" ]] || fail "$rejected_id launched a process"
[[ "$(fixture_state)" == "$before_state" ]] || fail "$rejected_id mutated the filesystem"
done
printf 'panama doctor contract: PASS\n'