Compare commits
2
Commits
2cc109f5e1
...
faa9a00716
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faa9a00716 | ||
|
|
8bfae70284 |
@@ -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()
|
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
|
||||||
if len(parts) < 2 or parts[0] != "Panama" or parts[1] != uid:
|
inhibitor_pids = list(dict.fromkeys(int(row[3]) for row in inhibitor_rows))
|
||||||
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:
|
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 = "";
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user