Files
Varaverk/Plugin/unraid/api/system.php
T
Gmer4Lfe ab794db0ab Stop the dashboard polling harder than its data can change
Every poll after the first asked for ?live=1, so the page paid a full collection — partner SSH
timeouts included — every two seconds while the tmpfs cache it was built around went unused.
Polls are now guarded against overlap and stop while the tab is hidden, repeated failures say so
instead of leaving the last good reading on screen, and two functions nothing called are gone.
2026-08-07 10:23:13 -04:00

106 lines
5.5 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// Host power control. Stops the array, shuts the machine down, or reboots it. The three
// most destructive operations the plugin can perform, deliberately isolated in one small
// file rather than folded into a general-purpose action endpoint.
//
// STATUS
// No UI caller. pages/monitor.php held the only one — a vvArrayAction() that no button on any
// tab invoked — and it was removed 2026-08-07 rather than left as an unreachable handler for
// these three commands. The endpoint is kept because it is complete and correct, and because
// power control is a thing this plugin will plausibly want; wiring it up is adding buttons, not
// writing an endpoint. Anything added here must keep the guarantees below intact.
//
// OPERATIONAL MODEL
// The command is detached and the response returns immediately. A shutdown kills the web
// server that is serving this request, so waiting on the child would mean the browser sees
// a connection reset rather than a result. Backgrounding is what lets the UI acknowledge
// the action before the host stops answering.
//
// ok:true therefore means "accepted and dispatched", not "completed". Nothing can report
// the completion of an operation that ends the process reporting it.
//
// DESIGN PRINCIPLES
// Three fixed commands, chosen by name.
// The action string selects among three compiled-in literals through a match. No part
// of the request is ever interpolated into the command, so there is no injection
// surface to protect — the request cannot express a command that is not one of these
// three.
//
// Reads its body as JSON, not as a form.
// php://input is parsed directly, because POST bodies are unreliable on this Unraid
// PHP setup and reading the raw stream sidesteps the form parser entirely.
//
// No scheduling, no delay, no cancel.
// There is nothing to cancel because there is no window in which to cancel it. A delayed
// shutdown with a cancel path would need state, and state that can be wrong about
// whether a host is about to power off is worse than no state.
//
// OPERATIONAL SAFEGUARDS
// POST only, checked first.
// A GET cannot power off the host, so no link, prefetch, bookmark, or browser history
// entry can reach these commands.
//
// Strict allowlist with strict comparison.
// in_array($action, ['stop','shutdown','restart'], true) — the third argument matters.
// Without it PHP's loose comparison would accept values that are not these strings, and
// the match below would then have no arm for them.
//
// The match has no default arm, and that is safe only because of the check above.
// An unmatched action would raise \UnhandledMatchError, which is a hard failure rather
// than a wrong command — but it is unreachable, because the allowlist already rejected
// everything that is not one of the three. The two guards are deliberately redundant.
//
// The audit line is written before the command is dispatched, not after.
// Ordering is the whole point: after a shutdown there is no "after". Timestamp, action
// and originating IP land in actions.log with LOCK_EX while the machine is still
// running, so a power event always has a record of who asked for it.
//
// The audit write is best-effort and never blocks the action.
// @ and FILE_APPEND — a full or read-only flash must not be able to prevent a shutdown.
// Losing the log line is the lesser failure; the log is gitignored and local by design.
//
// REQUEST
// POST {"action":"stop"} stop the array (mdcmd stop)
// POST {"action":"shutdown"} power off the host
// POST {"action":"restart"} reboot the host
//
// RESPONSE
// {"ok":true} accepted and dispatched — not completed
// {"ok":false,"error":"POST only"|"invalid action"}
//
// DEPENDS ON
// include/config.php SCRIPTS_DIR (audit log location)
// /usr/local/sbin/mdcmd array control
// /sbin/shutdown host power control
// ═══════════════════════════════════════════════════════════════════════════════════════════════
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'POST only']);
exit;
}
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $body['action'] ?? '';
$allowed = ['stop', 'shutdown', 'restart'];
if (!in_array($action, $allowed, true)) {
echo json_encode(['ok' => false, 'error' => 'invalid action']);
exit;
}
$cmd = match($action) {
'stop' => '/usr/local/sbin/mdcmd stop',
'shutdown' => '/sbin/shutdown -h now',
'restart' => '/sbin/shutdown -r now',
};
$logLine = date('Y-m-d H:i:s') . " action={$action} ip=" . ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . "\n";
@file_put_contents(SCRIPTS_DIR . '/actions.log', $logLine, FILE_APPEND | LOCK_EX);
exec($cmd . ' > /dev/null 2>&1 &');
echo json_encode(['ok' => true]);