No flatpak would launch. Every one of them failed in bwrap with "Can't find
source path /run/user/1000/doc/by-app/<id>", because xdg-document-portal's fuse
mount was gone -- /run/user/1000/doc was a plain empty directory. That mount is
bound into every sandbox bwrap builds, so losing it takes out all 34 flatpaks at
once, never a subset.
It had been gone for three days. The shipped unit is Type=dbus with Restart=no,
so nothing retries it on failure: after it exited 21 it came back only because a
flatpak called its bus name two seconds later, and that activation landed on the
dying instance's mountpoint and came up with no mount at all. systemctl reports
active (running) either way -- the fusermount3 helper is still sitting there as a
child, in this case for two and a half days without ever completing the mount.
Nothing running notices, which is what makes it so quiet. A sandbox needs the
mount only while it is being constructed, so everything already open keeps
working and the symptom arrives whenever you next open a flatpak you had not
opened yet. Here that was three days later, and it presented as "gearlever is
missing" -- an application that was installed, healthy, and entirely innocent.
Two changes, because there are two failures: it does not recover, and nothing
says so.
The drop-in clears the mountpoint before each start, so an activation that
follows a crash lands on clean ground. ExecStartPre rather than ExecStopPost
precisely because nothing restarts this unit -- the next start is whenever
something next wants it, and that is the moment that has to be safe. `-` because
a clean start has nothing to unmount and fusermount3 exits 1 saying so.
/etc/systemd/user rather than a per-user drop-in so it covers every account, and
change-settings reloads the user manager so it applies without a re-login.
The check asks the mount table whether $XDG_RUNTIME_DIR/doc is mounted
fuse.portal. Deliberately not a service probe, and deliberately not folded into
desktop.portals: that one asks about xdg-desktop-portal, a different service
which was up and healthy throughout. Service state is exactly the question that
lied here, so asking it again in a new place would have been no check at all.
Warning carries a restart repair, verified end to end rather than assumed.
The mount table is injectable, like every other path this script reads, so the
contract covers unmounted, wrong-filesystem-at-the-right-path, and unreadable
against written fixtures rather than against whatever /proc happens to say --
coupling the test to this machine's live flatpak state is the same mistake in
miniature. Stubbing the check to always return ok fails the contract, which was
confirmed rather than hoped.
What is not fixed is the crash itself: one occurrence, and restarting the
service to get the desktop working destroyed the evidence. The exit was 21, it
landed 21 seconds after xdg-desktop-portal restarted, and that is one sample and
not a theory. What this buys is that the next one is a two-second blip the
doctor names, rather than three silent days.
Second time for this bug. ac231ee found the same dead mount in August while
chasing "can't open Bitwarden", fixed it by hand, and recorded it as "not a
config issue, so nothing to commit there". That judgement is why it was paid for
twice, and it is the part most worth writing down.
807 lines
36 KiB
Bash
Executable File
807 lines
36 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The doctor is deliberately exercised through its command boundary. The
|
|
# fixture commands include sensitive-looking output so this test proves the
|
|
# report only retains explicitly parsed, non-sensitive observations.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
doctor="$repo_dir/config/dot/quickshell/scripts/panama-doctor"
|
|
theme_helper="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps"
|
|
fixture_root="$repo_dir/tests/quickshell/fixtures/doctor"
|
|
|
|
fail() {
|
|
printf 'panama doctor contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
fixture="$(mktemp -d /tmp/panama-doctor.XXXXXX)"
|
|
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"
|
|
config_home="$home/.config"
|
|
state_home="$home/.local/state"
|
|
runtime_dir="$fixture/runtime"
|
|
bin_dir="$fixture/bin"
|
|
data_home="$home/.local/share"
|
|
|
|
mkdir -p "$config_home" "$state_home" "$runtime_dir" "$bin_dir" "$data_home/vicinae/scripts"
|
|
|
|
# A mount table with the document portal present, which is the healthy state the
|
|
# rest of this file assumes. Written rather than read from /proc so the contract
|
|
# does not pass or fail on whether this machine happens to have flatpak working
|
|
# at the moment -- which is exactly the coupling that let a three-day outage go
|
|
# unnoticed. The gvfs line is there so the check has to match on the mount point
|
|
# and filesystem type rather than on being handed a file with one entry.
|
|
mountinfo="$fixture/mountinfo"
|
|
cat >"$mountinfo" <<EOF
|
|
749 54 0:87 / $runtime_dir rw,nosuid,nodev,relatime shared:933 - tmpfs tmpfs rw,size=3196364k,mode=700
|
|
1060 749 0:108 / $runtime_dir/gvfs rw,nosuid,nodev,relatime shared:1188 - fuse.gvfsd-fuse gvfsd-fuse rw,user_id=1000
|
|
29 749 0:86 / $runtime_dir/doc rw,nosuid,nodev,relatime shared:696 - fuse.portal portal rw,user_id=1000,group_id=1000
|
|
EOF
|
|
cp "$fixture_root/bin/"* "$bin_dir/"
|
|
chmod +x "$bin_dir"/*
|
|
|
|
# These are intentionally tiny stand-ins for authored executable probes. The
|
|
# named fixture scripts above cover probes whose output needs branch coverage.
|
|
for tool in hyprctl wl-paste grim tesseract kitty nextcloud rustdesk kdeconnect-cli; do
|
|
cat >"$bin_dir/$tool" <<'EOF'
|
|
#!/usr/bin/bash
|
|
case "${0##*/}" in
|
|
hyprctl) printf 'Hyprland 0.50.0\n' ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$bin_dir/$tool"
|
|
done
|
|
|
|
cat >"$bin_dir/flatpak" <<'EOF'
|
|
#!/usr/bin/bash
|
|
if [[ "${1:-}" == "info" && "${2:-}" == "app.bluebubbles.BlueBubbles" \
|
|
&& "${PANAMA_DOCTOR_FIXTURE_BLUEBUBBLES:-installed}" == "installed" ]]; then
|
|
printf 'BlueBubbles fixture-secret-token\n'
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
EOF
|
|
chmod +x "$bin_dir/flatpak"
|
|
|
|
cat >"$bin_dir/calendar-agenda" <<'EOF'
|
|
#!/usr/bin/bash
|
|
if [[ "${1:-}" != "probe" ]]; then
|
|
exit 2
|
|
fi
|
|
case "${PANAMA_DOCTOR_FIXTURE_CALENDAR:-ready}" in
|
|
ready) printf '{"eds":true,"sourceRegistry":true,"enabledSources":2,"event":"fixture clipboard body"}\n' ;;
|
|
malformed) printf 'calendar AA:BB:CC:DD:EE:FF\n' ;;
|
|
timeout) /usr/bin/sleep 2; printf '{"enabledSources":2}\n' ;;
|
|
*) printf '{"eds":true,"sourceRegistry":true,"enabledSources":0}\n' ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$bin_dir/calendar-agenda"
|
|
|
|
cat >"$bin_dir/panama-brightness" <<'EOF'
|
|
#!/usr/bin/bash
|
|
case "${PANAMA_DOCTOR_FIXTURE_BRIGHTNESS:-ready}" in
|
|
ready) printf '{"displays":[{"connector":"AA:BB:CC:DD:EE:FF"}],"error":""}\n' ;;
|
|
denied) printf '{"displays":[],"error":"fixture-secret-token"}\n' ;;
|
|
malformed) printf 'fixture clipboard body\n' ;;
|
|
esac
|
|
EOF
|
|
chmod +x "$bin_dir/panama-brightness"
|
|
|
|
mkdir -p "$config_home/autostart"
|
|
touch "$config_home/autostart/Nextcloud.desktop"
|
|
for name in hypr quickshell uwsm vicinae; do
|
|
ln -s "$repo_dir/config/dot/$name" "$config_home/$name"
|
|
done
|
|
ln -s "$repo_dir/config/local/share/vicinae/scripts" "$data_home/vicinae/scripts/panama"
|
|
mkdir -p "$state_home/panama"
|
|
printf '%s\n' 'background { path = /fixture-secret-wallpaper.jpg }' \
|
|
>"$state_home/panama/hyprlock.conf"
|
|
printf '%s\n' '{"generated":true,"path":"/fixture-secret-wallpaper.jpg","fallback":false,"error":""}' \
|
|
>"$state_home/panama/hyprlock-status.json"
|
|
|
|
run_doctor() {
|
|
HOME="$home" \
|
|
PATH="$bin_dir" \
|
|
XDG_CURRENT_DESKTOP=Hyprland \
|
|
PANAMA_HOME_ASSISTANT_URL='https://fixture.invalid' \
|
|
PANAMA_HOME_ASSISTANT_TOKEN='fixture-secret-token' \
|
|
PANAMA_DOCTOR_ROOT="$repo_dir" \
|
|
PANAMA_DOCTOR_HOME="$home" \
|
|
PANAMA_DOCTOR_CONFIG_HOME="$config_home" \
|
|
PANAMA_DOCTOR_STATE_HOME="$state_home" \
|
|
PANAMA_DOCTOR_RUNTIME_DIR="$runtime_dir" \
|
|
PANAMA_DOCTOR_MOUNTINFO="${PANAMA_DOCTOR_MOUNTINFO:-$mountinfo}" \
|
|
PANAMA_DOCTOR_PATH="$bin_dir" \
|
|
PANAMA_DOCTOR_SCRIPTS_DIR="$bin_dir" \
|
|
PANAMA_DOCTOR_TIMEOUT="${PANAMA_DOCTOR_TIMEOUT:-0.2}" \
|
|
/usr/bin/python3 "$doctor" "$@"
|
|
}
|
|
|
|
expected_order=$'desktop.hyprland\ndesktop.quickshell\ndesktop.notifications\ndesktop.portals\ndesktop.document-portal\ndesktop.hyprpaper\ndesktop.hypridle\ndesktop.hyprlock\ndesktop.vicinae\ninput.pipewire\ninput.clipboard\ninput.wallpaper\ninput.capture\ninput.ocr\ninput.brightness\nintegration.nextcloud\nintegration.rustdesk\nintegration.kdeconnect\nintegration.bluebubbles\nintegration.home-assistant\nintegration.calendar\npanama.updates\npanama.runtime-links\npanama.vicinae-commands\npanama.selected-terminal\npanama.selected-launcher\npanama.processes\npanama.caffeine'
|
|
|
|
assert_schema_and_redaction() {
|
|
local snapshot="$1"
|
|
jq -e '.schemaVersion == 1
|
|
and (.generatedAt | type == "string")
|
|
and (.summary.status | IN("healthy", "warning", "error"))
|
|
and (.context.session | IN("hyprland", "other"))
|
|
and (.context.versions | type == "array")
|
|
and ([.checks[].id] | length == 28)
|
|
and ([.checks[].id] | unique | length == 28)
|
|
and ([.checks[].status] | all(IN("ok", "warning", "error", "unconfigured")))' \
|
|
>/dev/null <<<"$snapshot" || fail "invalid schema: $snapshot"
|
|
[[ "$(jq -r '.checks[].id' <<<"$snapshot")" == "$expected_order" ]] \
|
|
|| fail "checks are not in the authored order"
|
|
! grep -Fq 'fixture-secret-token' <<<"$snapshot" \
|
|
|| fail 'report exposed a fixture secret'
|
|
! grep -Fq 'fixture clipboard body' <<<"$snapshot" \
|
|
|| fail 'report exposed clipboard or calendar content'
|
|
! grep -Fq 'AA:BB:CC:DD:EE:FF' <<<"$snapshot" \
|
|
|| fail 'report exposed a device address'
|
|
}
|
|
|
|
check_status() {
|
|
local snapshot="$1" id="$2" expected="$3"
|
|
[[ "$(jq -r --arg id "$id" '.checks[] | select(.id == $id) | .status' <<<"$snapshot")" == "$expected" ]] \
|
|
|| fail "$id did not report $expected: $snapshot"
|
|
}
|
|
|
|
snapshot="$(run_doctor --json)"
|
|
assert_schema_and_redaction "$snapshot"
|
|
check_status "$snapshot" panama.vicinae-commands ok
|
|
check_status "$snapshot" desktop.hyprlock ok
|
|
|
|
printf '%s\n' '{"generated":false,"path":"/fixture-secret-wallpaper.jpg","fallback":true,"error":"fixture-secret-token"}' \
|
|
>"$state_home/panama/hyprlock-status.json"
|
|
# A linked source checkout contains the template; installation renders the
|
|
# ignored machine-local fallback beside it. Model that installed state without
|
|
# writing an untracked file into the worktree under test.
|
|
rm "$config_home/hypr"
|
|
mkdir "$config_home/hypr"
|
|
cp "$repo_dir/config/dot/hypr/hyprlock.conf.template" \
|
|
"$config_home/hypr/hyprlock.conf.template"
|
|
HOME="$home" XDG_CONFIG_HOME="$config_home" "$theme_helper" dark >/dev/null
|
|
fallback_lock="$(run_doctor --json)"
|
|
check_status "$fallback_lock" desktop.hyprlock warning
|
|
assert_schema_and_redaction "$fallback_lock"
|
|
|
|
rm -rf "$config_home/hypr"
|
|
missing_lock="$(run_doctor --json)"
|
|
check_status "$missing_lock" desktop.hyprlock error
|
|
assert_schema_and_redaction "$missing_lock"
|
|
ln -s "$repo_dir/config/dot/hypr" "$config_home/hypr"
|
|
printf '%s\n' '{"generated":true,"path":"/fixture-secret-wallpaper.jpg","fallback":false,"error":""}' \
|
|
>"$state_home/panama/hyprlock-status.json"
|
|
|
|
# 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.
|
|
check_status "$snapshot" desktop.hyprpaper ok
|
|
|
|
# Arbitrary parent environment values are not propagated into probes.
|
|
sealed_environment="$(PANAMA_DOCTOR_FIXTURE_PROBE_SECRET=fixture-secret-token run_doctor --json)"
|
|
assert_schema_and_redaction "$sealed_environment"
|
|
check_status "$sealed_environment" desktop.hyprpaper ok
|
|
|
|
# An OS-level launch failure is contained as a check result, never a failed
|
|
# doctor invocation or a partial snapshot.
|
|
chmod 0644 "$bin_dir/systemctl"
|
|
if ! launch_failure="$(run_doctor --json)"; then
|
|
chmod +x "$bin_dir/systemctl"
|
|
fail 'launch failure prevented the doctor from emitting JSON'
|
|
fi
|
|
chmod +x "$bin_dir/systemctl"
|
|
assert_schema_and_redaction "$launch_failure"
|
|
check_status "$launch_failure" desktop.hyprpaper error
|
|
|
|
# A missing required executable is an error rather than a crash.
|
|
mv "$bin_dir/qs" "$bin_dir/qs.off"
|
|
missing_qs="$(run_doctor --json)"
|
|
check_status "$missing_qs" desktop.quickshell error
|
|
mv "$bin_dir/qs.off" "$bin_dir/qs"
|
|
|
|
# Optional integrations stay neutral until the user configures them.
|
|
rm "$config_home/autostart/Nextcloud.desktop"
|
|
unconfigured_nextcloud="$(run_doctor --json)"
|
|
check_status "$unconfigured_nextcloud" integration.nextcloud unconfigured
|
|
touch "$config_home/autostart/Nextcloud.desktop"
|
|
|
|
# A configured integration whose process isn't running is actionable with an
|
|
# authored label, never an application name or command derived from probe
|
|
# output. Nextcloud has no systemd unit behind it, so this is a process
|
|
# check (PANAMA_DOCTOR_FIXTURE_PROCESSES), not a service check.
|
|
stopped_nextcloud="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=nextcloud:missing run_doctor --json)"
|
|
check_status "$stopped_nextcloud" integration.nextcloud warning
|
|
jq -e '.checks[] | select(.id == "integration.nextcloud")
|
|
| .action == {kind:"open", label:"Open Nextcloud", confirm:false}' \
|
|
>/dev/null <<<"$stopped_nextcloud" || fail 'Nextcloud action was not authored'
|
|
|
|
# The document portal is judged by the mount, never by the service.
|
|
#
|
|
# This is the check's whole reason for existing. The service can be active and
|
|
# the mount gone -- that is precisely what happened, twice -- so a fixture that
|
|
# keeps every service healthy and removes only the mount has to fail. If this
|
|
# ever passes, the check has drifted back to asking systemd how it feels.
|
|
grep -v ' fuse.portal ' "$mountinfo" >"$fixture/mountinfo-unmounted"
|
|
unmounted_portal="$(PANAMA_DOCTOR_MOUNTINFO="$fixture/mountinfo-unmounted" run_doctor --json)"
|
|
check_status "$unmounted_portal" desktop.document-portal warning
|
|
jq -e '.checks[] | select(.id == "desktop.document-portal")
|
|
| .action == {kind:"repair", label:"Restart the document portal", confirm:false}' \
|
|
>/dev/null <<<"$unmounted_portal" || fail 'document portal repair action was not authored'
|
|
|
|
# A mount at the right path of the wrong type is not the document portal. The
|
|
# stale mountpoint left behind by a failed activation is a plain directory, but
|
|
# matching on path alone would call any future mount there healthy.
|
|
sed 's/ fuse.portal portal / fuse.gvfsd-fuse gvfsd-fuse /' "$mountinfo" >"$fixture/mountinfo-wrongtype"
|
|
wrong_type_portal="$(PANAMA_DOCTOR_MOUNTINFO="$fixture/mountinfo-wrongtype" run_doctor --json)"
|
|
check_status "$wrong_type_portal" desktop.document-portal warning
|
|
|
|
# An unreadable mount table is an error, not a silent pass and not a crash.
|
|
missing_mountinfo="$(PANAMA_DOCTOR_MOUNTINFO="$fixture/no-such-mountinfo" run_doctor --json)"
|
|
check_status "$missing_mountinfo" desktop.document-portal error
|
|
|
|
# DDC errors are classified without retaining connectors or bus addresses.
|
|
denied_brightness="$(PANAMA_DOCTOR_FIXTURE_BRIGHTNESS=denied run_doctor --json)"
|
|
check_status "$denied_brightness" input.brightness warning
|
|
jq -e '.checks[] | select(.id == "input.brightness")
|
|
| .action == {kind:"instructions", label:"View setup instructions", confirm:false, target:"ddc-permissions"}' \
|
|
>/dev/null <<<"$denied_brightness" || fail 'DDC instructions were not authored'
|
|
|
|
# A bounded probe timeout becomes a result, never a helper failure.
|
|
timed_calendar="$(PANAMA_DOCTOR_FIXTURE_CALENDAR=timeout PANAMA_DOCTOR_TIMEOUT=0.05 run_doctor --json)"
|
|
check_status "$timed_calendar" integration.calendar warning
|
|
jq -e '.checks[] | select(.id == "integration.calendar")
|
|
| .action == {kind:"open", label:"Open Date & Time", confirm:false, target:"datetime"}' \
|
|
>/dev/null <<<"$timed_calendar" || fail 'calendar action was not authored'
|
|
|
|
# Exact Panama/Caffeine inhibitor rows detect duplicates without exposing PIDs.
|
|
duplicated_caffeine="$(PANAMA_DOCTOR_FIXTURE_CAFFEINE=duplicate run_doctor --json)"
|
|
check_status "$duplicated_caffeine" panama.caffeine warning
|
|
jq -e '.checks[] | select(.id == "panama.caffeine")
|
|
| .action == {kind:"repair", label:"Release duplicate inhibitors", confirm:false}' \
|
|
>/dev/null <<<"$duplicated_caffeine" || fail 'Caffeine repair action was not authored'
|
|
! jq -r '.checks[] | select(.id == "panama.caffeine") | .detail' <<<"$duplicated_caffeine" | grep -Eq '[0-9]{3,}' \
|
|
|| fail 'Caffeine detail exposed inhibitor PIDs'
|
|
|
|
# Process counts use only exact authored names and never expose command lines or PIDs.
|
|
duplicated_processes="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=qs:duplicate run_doctor --json)"
|
|
check_status "$duplicated_processes" panama.processes warning
|
|
! jq -r '.checks[] | select(.id == "panama.processes") | .detail' <<<"$duplicated_processes" | grep -Eq '[0-9]{3,}' \
|
|
|| fail 'process detail exposed a PID'
|
|
|
|
missing_quickshell_process="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=qs:missing run_doctor --json)"
|
|
check_status "$missing_quickshell_process" panama.processes error
|
|
|
|
# Invalid output for a non-Quickshell authored process is not a normal zero
|
|
# count that can be hidden by the running Quickshell process.
|
|
malformed_processes="$(PANAMA_DOCTOR_FIXTURE_PROCESSES=hyprpaper:malformed run_doctor --json)"
|
|
check_status "$malformed_processes" panama.processes warning
|
|
|
|
# Panama/Caffeine-shaped rows that do not satisfy the fixed inhibitor schema
|
|
# are unavailable rather than reported as a healthy no-inhibitor state.
|
|
malformed_caffeine="$(PANAMA_DOCTOR_FIXTURE_CAFFEINE=malformed run_doctor --json)"
|
|
check_status "$malformed_caffeine" panama.caffeine warning
|
|
|
|
# A decoding error raised inside a concurrent probe is converted to a complete
|
|
# snapshot rather than escaping from Future.result().
|
|
if ! invalid_probe="$(PANAMA_DOCTOR_FIXTURE_BUS=invalid-utf8 run_doctor --json)"; then
|
|
fail 'unexpected probe exception prevented the doctor from emitting JSON'
|
|
fi
|
|
assert_schema_and_redaction "$invalid_probe"
|
|
check_status "$invalid_probe" desktop.portals warning
|
|
|
|
# Configured Home Assistant failures route to the exact authored Settings page.
|
|
rm "$config_home/quickshell"
|
|
mkdir -p "$config_home/quickshell/scripts"
|
|
home_assistant_failure="$(run_doctor --json)"
|
|
check_status "$home_assistant_failure" integration.home-assistant warning
|
|
jq -e '.checks[] | select(.id == "integration.home-assistant")
|
|
| .action == {kind:"open", label:"Open Home settings", confirm:false, target:"home-phone"}' \
|
|
>/dev/null <<<"$home_assistant_failure" || fail 'Home Assistant action was not routed to home-phone'
|
|
|
|
# Invalid probe text is contained in its own check and never copied to JSON.
|
|
malformed_calendar="$(PANAMA_DOCTOR_FIXTURE_CALENDAR=malformed run_doctor --json)"
|
|
check_status "$malformed_calendar" integration.calendar warning
|
|
assert_schema_and_redaction "$malformed_calendar"
|
|
|
|
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="$home/.local/share/Panama"
|
|
repair_log="$runtime_dir/repair.log"
|
|
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
|
|
mkdir -p "$repair_root/config/dot/$name"
|
|
done
|
|
cp "$repo_dir/setup/scripts/link-vicinae-scripts" "$repair_root/setup/scripts/link-vicinae-scripts"
|
|
|
|
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/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"
|
|
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)"
|
|
mode="$(<"$XDG_RUNTIME_DIR/caffeine-mode")"
|
|
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 '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
|
|
|
|
chmod +x "$bin_dir/systemctl" "$bin_dir/panama-action" \
|
|
"$bin_dir/systemd-inhibit" "$repair_root/setup/scripts/link-vicinae-scripts"
|
|
|
|
run_repair() {
|
|
HOME="$home" \
|
|
PATH="$bin_dir:/usr/bin" \
|
|
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:/usr/bin" \
|
|
PANAMA_DOCTOR_SCRIPTS_DIR="$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 real authored Vicinae helper converges the exact child link diagnosed by
|
|
# 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
|
|
[[ "$repair_status" == 0 ]] || fail "Vicinae command repair returned $repair_status"
|
|
assert_repair_result panama.vicinae-commands true 0
|
|
after_vicinae_repair="$(run_repair --json)"
|
|
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 absent links or symlinks whose lexical
|
|
# target proves Panama ownership. Every other object remains untouched.
|
|
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 "$repair_root/config/dot/hypr" "$config_home/hypr"
|
|
correct_inode="$(stat -c %i "$config_home/hypr")"
|
|
ln -s "$repair_root/config/dot/quickshell" "$config_home/uwsm"
|
|
ln -s "$fixture/external-broken-link" "$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 '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" ]] \
|
|
|| fail 'absent quickshell link was not created'
|
|
[[ -L "$config_home/uwsm" && "$(readlink "$config_home/uwsm")" == "$repair_root/config/dot/uwsm" ]] \
|
|
|| fail 'provably Panama-owned stale link was not repaired'
|
|
[[ -L "$config_home/vicinae" && "$(readlink "$config_home/vicinae")" == "$fixture/external-broken-link" ]] \
|
|
|| fail 'external broken symlink was replaced'
|
|
[[ -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'
|
|
|
|
# Regular files and directories also remain untouched.
|
|
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"
|
|
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\nsystemd-inhibit|--list|--no-pager|--no-legend'
|
|
[[ "$(<"$repair_log")" == "$expected_caffeine" ]] \
|
|
|| 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
|
|
# nor a filesystem mutation.
|
|
fixture_state() {
|
|
/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 \
|
|
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'
|