Rebuild the Fallback tab around what the daemon is actually doing, not what its last state file said
This commit is contained in:
@@ -115,6 +115,63 @@ function vv_fb_remote_running(string $ip, string $sshKey): array {
|
||||
return array_values(array_filter(explode("\n", trim($out))));
|
||||
}
|
||||
|
||||
|
||||
// ── Daemon / process state ────────────────────────────────────────────────────
|
||||
//
|
||||
// The page had no way to say whether fallback.sh was running at all, which is the first thing
|
||||
// anyone looking at this tab wants to know — every state below is written BY that daemon, so a
|
||||
// stale NORMAL from a process that died days ago read exactly like a healthy one.
|
||||
//
|
||||
// Mode matters as much as liveness. A --dry-run instance takes the same `fallback` lock as the
|
||||
// real daemon, so the lock alone cannot tell them apart; the cmdline can, and the per-PID
|
||||
// dry-run state copy is a second confirmation.
|
||||
function vv_fb_proc(string $lockName): array {
|
||||
$lockFile = '/tmp/unraid_locks/' . $lockName . '.lock';
|
||||
$out = ['running' => false, 'pid' => null, 'mode' => null, 'since' => null, 'stale_lock' => false];
|
||||
|
||||
if (!is_file($lockFile)) return $out;
|
||||
|
||||
$pid = (int)strtok((string)@file_get_contents($lockFile), ':');
|
||||
// A lock whose PID is gone is not "running" — it is residue from a SIGKILL or a power cut,
|
||||
// and saying so is the difference between "stop it" and "clear it".
|
||||
if ($pid <= 0 || !is_dir("/proc/$pid")) {
|
||||
$out['stale_lock'] = true;
|
||||
$out['pid'] = $pid ?: null;
|
||||
return $out;
|
||||
}
|
||||
|
||||
$cmd = (string)@file_get_contents("/proc/$pid/cmdline");
|
||||
$args = explode("\0", $cmd);
|
||||
$out['running'] = true;
|
||||
$out['pid'] = $pid;
|
||||
$out['mode'] = in_array('--dry-run', $args, true) || in_array('-n', $args, true) ? 'dry-run' : 'live';
|
||||
$st = @stat("/proc/$pid");
|
||||
if ($st) $out['since'] = (int)$st['mtime'];
|
||||
return $out;
|
||||
}
|
||||
|
||||
// Per-host daemon state. Local reads /proc directly; a partner is asked over the same SSH the
|
||||
// rest of this file already uses, in one call rather than three.
|
||||
function vv_fb_remote_proc(string $ip, string $sshKey): array {
|
||||
$cmd = 'for L in fallback fallback_test; do F=/tmp/unraid_locks/$L.lock; '
|
||||
. 'if [ -f "$F" ]; then P=$(cut -d: -f1 "$F"); '
|
||||
. 'if [ -d "/proc/$P" ]; then M=live; tr "\\0" " " < /proc/$P/cmdline | grep -q -- "--dry-run" && M=dry-run; '
|
||||
. 'echo "$L:running:$P:$M"; else echo "$L:stale:$P:"; fi; else echo "$L:none::"; fi; done';
|
||||
$out = vv_pt_ssh($ip, $sshKey, $cmd);
|
||||
$res = ['fallback' => ['running' => false, 'pid' => null, 'mode' => null, 'stale_lock' => false],
|
||||
'fallback_test' => ['running' => false, 'pid' => null, 'mode' => null, 'stale_lock' => false]];
|
||||
foreach (explode("\n", trim((string)$out)) as $line) {
|
||||
$p = explode(':', trim($line));
|
||||
if (count($p) < 4 || !isset($res[$p[0]])) continue;
|
||||
if ($p[1] === 'running') {
|
||||
$res[$p[0]] = ['running' => true, 'pid' => (int)$p[2], 'mode' => $p[3] ?: 'live', 'stale_lock' => false];
|
||||
} elseif ($p[1] === 'stale') {
|
||||
$res[$p[0]]['stale_lock'] = true;
|
||||
$res[$p[0]]['pid'] = (int)$p[2] ?: null;
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
// ── Covers — what a node runs for the other when it's down ───────────────────
|
||||
|
||||
function vv_fb_covers(string $covering, string $remote, string $coveringRaw, string $remoteRaw): array {
|
||||
@@ -207,14 +264,51 @@ function vv_fb_all(): array {
|
||||
break; // 2-node only
|
||||
}
|
||||
|
||||
// Daemon liveness per node. Everything in $state was written by this process — without
|
||||
// it a NORMAL left behind by a daemon that died days ago is indistinguishable from a
|
||||
// NORMAL being refreshed every 30 seconds.
|
||||
if ($isMe) {
|
||||
$proc = vv_fb_proc('fallback');
|
||||
$procTest = vv_fb_proc('fallback_test');
|
||||
} elseif ($ip && $mySshKey && $ts['online']) {
|
||||
$rp = vv_fb_remote_proc($ip, $mySshKey);
|
||||
$proc = $rp['fallback'];
|
||||
$procTest = $rp['fallback_test'];
|
||||
} else {
|
||||
$proc = ['running' => null, 'pid' => null, 'mode' => null, 'stale_lock' => false];
|
||||
$procTest = ['running' => null, 'pid' => null, 'mode' => null, 'stale_lock' => false];
|
||||
}
|
||||
|
||||
// How fresh the state actually is. The daemon rewrites its file every check interval, so
|
||||
// an age far past that interval means it is wedged even while the process still exists.
|
||||
$stateAge = null;
|
||||
if ($isMe) {
|
||||
$sp = STATE_DIR . '/fallback_state.db';
|
||||
if (is_file($sp)) $stateAge = time() - (int)@filemtime($sp);
|
||||
}
|
||||
|
||||
// "Reachable" is three separate facts and one boolean hid which had failed.
|
||||
$reach = [
|
||||
'tailscale' => $isMe ? true : ($ts['online'] === true),
|
||||
'ip' => $isMe ? null : $ip,
|
||||
'ssh' => $isMe ? true : ($running !== [] || ($proc['running'] !== null)),
|
||||
'state_file' => ($state['state'] ?? 'UNKNOWN') !== 'UNKNOWN',
|
||||
];
|
||||
|
||||
$nodes[] = [
|
||||
'slot' => $slot,
|
||||
'id' => strtoupper($slot),
|
||||
'hostname' => $hostname,
|
||||
'is_me' => $isMe,
|
||||
'ts_online' => $ts['online'],
|
||||
'ts_ip' => $ip,
|
||||
'state' => $state,
|
||||
'state_age' => $stateAge,
|
||||
'running' => $running,
|
||||
'running_count' => count($running),
|
||||
'proc' => $proc,
|
||||
'proc_test' => $procTest,
|
||||
'reach' => $reach,
|
||||
'covers' => $covers,
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user