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:
@@ -1,4 +1,56 @@
|
||||
<?php
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
// PURPOSE
|
||||
// Stale lock removal. Deletes one lock file from /tmp/unraid_locks so a script whose
|
||||
// previous run died without releasing its lock can be started again from the UI.
|
||||
//
|
||||
// OPERATIONAL MODEL
|
||||
// The manual escape hatch for the one failure the lock discipline cannot resolve on its
|
||||
// own. Scripts take a lock to prevent concurrent runs; a run killed by OOM, a reboot mid-
|
||||
// job, or kill -9 leaves the lock behind and every subsequent run refuses to start. This
|
||||
// endpoint is how a person clears that, and it is deliberately the only way — nothing
|
||||
// automatically reaps locks, because "the lock is old" and "the job is still running" are
|
||||
// not distinguishable from the file alone.
|
||||
//
|
||||
// DESIGN PRINCIPLES
|
||||
// Loads nothing.
|
||||
// This is the only endpoint in the api layer that requires no include. The operation is
|
||||
// one unlink in one fixed directory; pulling in the config layer to perform it would
|
||||
// add failure modes to something that must work when other things are broken.
|
||||
//
|
||||
// Deletes exactly one lock, never sweeps.
|
||||
// There is no clear-all. Releasing every lock at once would restart the concurrent runs
|
||||
// the locks exist to prevent.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// POST only. A GET cannot delete a lock, so a stray link, prefetch, or browser history
|
||||
// entry cannot release one.
|
||||
//
|
||||
// The filename is stripped and then pattern-matched, in that order.
|
||||
// basename() removes any directory component, and the surviving name must match
|
||||
// ^[a-zA-Z0-9_\-]+\.lock$ — no dots beyond the extension, no slashes, no traversal. The
|
||||
// two together mean the composed path cannot leave /tmp/unraid_locks, and the .lock
|
||||
// suffix means nothing but a lock file is a candidate in the first place.
|
||||
//
|
||||
// Deleting an absent lock is success, not an error.
|
||||
// file_exists() is checked and @unlink() suppressed, so two clicks, or a race with the
|
||||
// script releasing its own lock, both end with the lock gone and ok:true. The caller
|
||||
// wants the lock absent; it does not care who removed it.
|
||||
//
|
||||
// Scope is one fixed directory, hardcoded here.
|
||||
// /tmp/unraid_locks is a literal, not a config value, so no conf edit or empty variable
|
||||
// can redirect this delete somewhere else.
|
||||
//
|
||||
// REQUEST
|
||||
// POST file=<name>.lock
|
||||
//
|
||||
// RESPONSE
|
||||
// {"ok":true} lock removed, or already absent
|
||||
// {"ok":false,"error":string} wrong method or a filename that failed validation
|
||||
//
|
||||
// DEPENDS ON
|
||||
// nothing — /tmp/unraid_locks is written by the shell layer's locking helper
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
header('Content-Type: application/json');
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'POST only']);
|
||||
|
||||
Reference in New Issue
Block a user