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.
67 lines
3.6 KiB
PHP
67 lines
3.6 KiB
PHP
<?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']);
|
|
exit;
|
|
}
|
|
$file = basename($_POST['file'] ?? '');
|
|
if (!$file || !preg_match('/^[a-zA-Z0-9_\-]+\.lock$/', $file)) {
|
|
echo json_encode(['ok' => false, 'error' => 'Invalid filename']);
|
|
exit;
|
|
}
|
|
$path = '/tmp/unraid_locks/' . $file;
|
|
if (file_exists($path)) @unlink($path);
|
|
echo json_encode(['ok' => true]);
|