Document the PHP api layer and fix what documenting it exposed

Writing down what each endpoint actually guarantees made the places it
didn't obvious — shell arguments reaching a crontab or a bash -c
unescaped, master.conf written without tmp+rename, and conf edits that
could be saved without ever being parsed.
This commit is contained in:
Gmer4Lfe
2026-08-02 10:11:39 -04:00
parent 6a959fb5e4
commit 987313e7dc
55 changed files with 3972 additions and 95 deletions
+84
View File
@@ -1,4 +1,88 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Job killer. Terminates a running job, clears the lock files it left behind, and corrects
// its stat file — the stop button on the scheduler page.
//
// OPERATIONAL MODEL
// Escalating termination, then cleanup. SIGTERM to the whole process group, up to three
// seconds to exit, SIGKILL if it did not. The group is the target rather than the pid
// because a Varaverk script is mostly other processes — rsync, ssh, curl, docker — and
// killing only the parent would orphan every one of them still holding the resources the
// next run needs.
//
// The stat file is the job's own record, and a killed job never gets to correct it. This
// endpoint does that on its behalf, which is what stops the scheduler page from showing a
// spinner for a job that no longer exists.
//
// DESIGN PRINCIPLES
// Kills the group, falls back to the tree.
// `ps -o pgid=` resolves the process group; when that fails the fallback is pkill -P
// plus the pid itself. Two strategies, because a job whose runner already exited can
// leave children whose group id is no longer discoverable from the recorded pid.
//
// Waits before escalating.
// Six 500ms checks between TERM and KILL. Scripts have cleanup handlers — releasing
// locks, finishing a write, unmounting — and killing immediately would skip exactly the
// work that makes the next run safe.
//
// Reports honestly when the kill failed.
// ok mirrors whether the process is actually gone. A process in uninterruptible sleep
// survives SIGKILL, and the response says so rather than claiming success and leaving
// the user to discover the job is still running.
//
// OPERATIONAL SAFEGUARDS
// The job id is validated, and the pid is an integer before it reaches a shell.
// ^[a-zA-Z0-9_./\-]+\.sh$ with an explicit '..' check on the id, and (int) casts on
// every pid — the recorded one and each lock's owner — before interpolation. The signal
// commands are the only shell in this file and none of them can carry anything but a
// number.
//
// pid < 2 is refused.
// A zero, missing, or malformed pid in the stat file would make `kill -TERM -0` signal
// the caller's own process group — the web server. Rejecting anything below 2 also
// excludes init.
//
// A job that is not running is a no-op success.
// Both the missing-stat-file and status-not-running paths exit before any signal is
// sent, so pressing stop twice cannot kill an unrelated process that has since been
// assigned the recorded pid.
//
// The pid is only cleared from the stat file if the process is confirmed gone.
// D-state processes survive SIGKILL. Clearing the pid there would lose the only handle
// anyone has on a process that is still holding locks, and would report the job stopped
// while it continues to run.
//
// Lock clearing is bounded to the lock directory and to dead owners.
// glob over /tmp/unraid_locks/*.lock — a hardcoded literal, not a config value — and a
// lock is removed only when its recorded owner is this pid or is no longer in /proc. A
// lock held by a live, unrelated process is never touched.
//
// Deliberate side effect: stale locks from other jobs are reaped too.
// The dead-owner test is not scoped to this job, so one stop clears every abandoned
// lock on the host. That is intentional — a lock whose owner does not exist is by
// definition stale, and leaving it to be found later means a future run refuses to
// start for no reason.
//
// The name-based fallback covers pid rotation.
// A lock file named after the script is removed regardless of its recorded owner,
// because a rotated pid can make a genuinely stale lock look live.
//
// REQUEST
// POST id=<Category/name.sh>
//
// RESPONSE
// {"ok":true,"killed":true,"locks":["…"],"error":null}
// {"ok":true,"msg":"Not running"}
// {"ok":false,"killed":false,"locks":[…],
// "error":"Process still alive after SIGKILL (D-state) — lock may persist"}
// {"ok":false,"error":"Invalid id"|"No stat file — script may not be running"
// |"No valid PID in stat file"}
//
// DEPENDS ON
// include/scheduler.php vv_job_stat_path()
// /tmp/unraid_locks lock files written by common.sh's locking helper
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/scheduler.php';