diff --git a/Plugin/unraid/api/board.php b/Plugin/unraid/api/board.php index 11da439..855ae87 100644 --- a/Plugin/unraid/api/board.php +++ b/Plugin/unraid/api/board.php @@ -80,13 +80,9 @@ if (is_dir(LOG_DIR)) { $out['errors'] = array_slice($errors, 0, 20); // ── Partner reachability ─────────────────────────────────────────────────── -$cacheFile = '/tmp/vv_partner_cache.json'; -$cacheTtl = 30; -$partnerData = null; +$partnerData = vv_cache_read('board_partner', 30); -if (file_exists($cacheFile) && (time() - (int)filemtime($cacheFile)) < $cacheTtl) { - $partnerData = json_decode(file_get_contents($cacheFile), true); -} else { +if (!$partnerData) { // Discover partner hostname dynamically from master.conf (works for any number of hosts) $vars = vv_conf_vars(); $mine = vv_get_hostname(); @@ -113,7 +109,7 @@ if (file_exists($cacheFile) && (time() - (int)filemtime($cacheFile)) < $cacheTtl 'reachable' => $reached, 'latency' => $reached ? $elapsed : null, ]; - @file_put_contents($cacheFile, json_encode($partnerData)); + vv_cache_write('board_partner', $partnerData); } } $out['partner'] = $partnerData; diff --git a/Plugin/unraid/api/conf_toggle.php b/Plugin/unraid/api/conf_toggle.php index 5044bfd..b9779d9 100644 --- a/Plugin/unraid/api/conf_toggle.php +++ b/Plugin/unraid/api/conf_toggle.php @@ -2,6 +2,11 @@ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/scheduler.php'; +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['ok' => false, 'error' => 'POST only']); + exit; +} + $id = trim($_POST['id'] ?? ''); $enabled = ($_POST['enabled'] ?? '0') === '1'; diff --git a/Plugin/unraid/api/flag_toggle.php b/Plugin/unraid/api/flag_toggle.php index 9cdda4e..2d96f03 100644 --- a/Plugin/unraid/api/flag_toggle.php +++ b/Plugin/unraid/api/flag_toggle.php @@ -2,6 +2,11 @@ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/scheduler.php'; +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['ok' => false, 'error' => 'POST only']); + exit; +} + $name = trim($_POST['name'] ?? ''); $enabled = ($_POST['enabled'] ?? '0') === '1'; diff --git a/Plugin/unraid/api/monitor_remote.php b/Plugin/unraid/api/monitor_remote.php index db1b3fd..c941f11 100644 --- a/Plugin/unraid/api/monitor_remote.php +++ b/Plugin/unraid/api/monitor_remote.php @@ -3,18 +3,11 @@ header('Content-Type: application/json'); header('Cache-Control: no-cache, no-store'); require_once dirname(__DIR__) . '/include/monitor.php'; -$cacheFile = '/tmp/vv_cache_monitor_remote.json'; -$cacheTTL = 3600; - -if (!isset($_GET['live']) && file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) { - echo file_get_contents($cacheFile); - exit; +if (!isset($_GET['live'])) { + $cached = vv_cache_read('monitor_remote', 3600); + if ($cached) { echo json_encode($cached); exit; } } -$out = json_encode([ - 'remote_hosts' => vv_remote_hosts_stats(), - 'ts' => time(), -]); - -file_put_contents($cacheFile, $out); -echo $out; +$data = ['remote_hosts' => vv_remote_hosts_stats(), 'ts' => time()]; +vv_cache_write('monitor_remote', $data); +echo json_encode($data); diff --git a/Plugin/unraid/api/movescript.php b/Plugin/unraid/api/movescript.php index 0da03e7..1c9fc60 100644 --- a/Plugin/unraid/api/movescript.php +++ b/Plugin/unraid/api/movescript.php @@ -70,9 +70,10 @@ if ($toArray) { $newLines = $resultLines; } -if (file_put_contents($confPath, implode('', $newLines)) === false) { +if (!vv_write_conf_raw('master.conf', implode('', $newLines))) { echo json_encode(['ok' => false, 'error' => 'Write failed']); exit; } +vv_push_master_conf(); echo json_encode(['ok' => true]); diff --git a/Plugin/unraid/api/reorderarray.php b/Plugin/unraid/api/reorderarray.php index 89432d2..7e0cd94 100644 --- a/Plugin/unraid/api/reorderarray.php +++ b/Plugin/unraid/api/reorderarray.php @@ -101,9 +101,10 @@ $newBlockLines[] = $lines[$blockEnd]; // Replace the original block in $lines array_splice($lines, $blockStart, $blockEnd - $blockStart + 1, $newBlockLines); -if (file_put_contents($confPath, implode('', $lines)) === false) { +if (!vv_write_conf_raw('master.conf', implode('', $lines))) { echo json_encode(['ok' => false, 'error' => 'Write failed']); exit; } +vv_push_master_conf(); echo json_encode(['ok' => true]); diff --git a/Plugin/unraid/api/scheduler.php b/Plugin/unraid/api/scheduler.php index 18f561a..34312b7 100644 --- a/Plugin/unraid/api/scheduler.php +++ b/Plugin/unraid/api/scheduler.php @@ -2,6 +2,11 @@ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/scheduler.php'; +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + echo json_encode(['ok' => false, 'error' => 'POST only']); + exit; +} + // Batch save — all entries in one load/write/rebuild cycle if (!empty($_POST['batch'])) { $entries = json_decode($_POST['batch'], true) ?: []; diff --git a/Plugin/unraid/api/snapshot.php b/Plugin/unraid/api/snapshot.php index 85aebea..7ca81c1 100644 --- a/Plugin/unraid/api/snapshot.php +++ b/Plugin/unraid/api/snapshot.php @@ -3,61 +3,36 @@ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/media.php'; -// CPU% — delta from own state file so it doesn't conflict with monitor.php -$cpuPct = 0; -$cpuLine = ''; -foreach (file('/proc/stat') ?: [] as $line) { - if (strncmp($line, 'cpu ', 4) === 0) { $cpuLine = $line; break; } -} -if (preg_match('/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/', $cpuLine, $m)) { - $c = [(int)$m[1],(int)$m[2],(int)$m[3],(int)$m[4],(int)$m[5],(int)$m[6],(int)$m[7]]; - $sf = '/tmp/vv_snap_cpu.json'; - $p = file_exists($sf) ? (json_decode(file_get_contents($sf), true) ?: null) : null; - file_put_contents($sf, json_encode($c)); - if ($p && is_array($p)) { - $dt = array_sum($c) - array_sum($p); - $di = ($c[3] + $c[4]) - ($p[3] + $p[4]); - $cpuPct = $dt > 0 ? max(0, min(100, (int)round((1 - $di / $dt) * 100))) : 0; - } -} +// CPU% — shares /tmp/vv_cpu_stat.json with vv_cpu_per_core() so both read the same baseline +$cpuPct = vv_cpu_per_core()['overall'] ?? 0; // RAM% -$mem = []; -foreach (file('/proc/meminfo') ?: [] as $line) { - if (preg_match('/^(MemTotal|MemAvailable):\s+(\d+)/', $line, $m)) $mem[$m[1]] = (int)$m[2]; -} -$ramTotalMb = (int)(($mem['MemTotal'] ?? 0) / 1024); -$ramUsedMb = (int)((($mem['MemTotal'] ?? 0) - ($mem['MemAvailable'] ?? 0)) / 1024); +$res = vv_system_resources(); +$ramTotalMb = $res['ram_total_mb']; +$ramUsedMb = $ramTotalMb - $res['ram_free_mb']; $ramPct = $ramTotalMb > 0 ? (int)round($ramUsedMb / $ramTotalMb * 100) : 0; // Fallback state (fast file read, no exec) -$fallbackState = 'UNKNOWN'; -foreach (@file('/tmp/fallback_state.db') ?: [] as $line) { - if (preg_match('/^state=(.+)/', trim($line), $m)) { $fallbackState = trim($m[1]); break; } -} +$fbRaw = @file_get_contents('/tmp/fallback_state.db') ?: ''; +$fbData = vv_parse_kv_db($fbRaw); +$fallbackState = $fbData['state'] ?? 'UNKNOWN'; // Partner $partner = vv_partner_state(); $peers = array_values(array_filter($partner['hosts'], fn($h) => !$h['is_me'])); // Media sessions — cached 30s so the HTTP calls don't hold up every snapshot poll -$streamCount = 0; -$transcodeCount = 0; -$mediaCacheFile = '/tmp/vv_snap_media.json'; -$cacheMaxAge = 30; -$cacheValid = file_exists($mediaCacheFile) && (time() - filemtime($mediaCacheFile)) < $cacheMaxAge; -if ($cacheValid) { - $cached = json_decode(file_get_contents($mediaCacheFile), true) ?: []; -} else { - $media = vv_media_sessions(); - $cached = [ +$mediaCache = vv_cache_read('snap_media', 30); +if (!$mediaCache) { + $media = vv_media_sessions(); + $mediaCache = [ 'stream_count' => count($media['sessions']), 'transcode_count' => count(array_filter($media['sessions'], fn($s) => !empty($s['is_tc']))), ]; - file_put_contents($mediaCacheFile, json_encode($cached)); + vv_cache_write('snap_media', $mediaCache); } -$streamCount = (int)($cached['stream_count'] ?? 0); -$transcodeCount = (int)($cached['transcode_count'] ?? 0); +$streamCount = (int)($mediaCache['stream_count'] ?? 0); +$transcodeCount = (int)($mediaCache['transcode_count'] ?? 0); echo json_encode([ 'cpu_pct' => $cpuPct, diff --git a/Plugin/unraid/api/system.php b/Plugin/unraid/api/system.php index dc93c84..76d4d94 100644 --- a/Plugin/unraid/api/system.php +++ b/Plugin/unraid/api/system.php @@ -1,5 +1,11 @@ false, 'error' => 'POST only']); + exit; +} $body = json_decode(file_get_contents('php://input'), true) ?? []; $action = $body['action'] ?? ''; diff --git a/Plugin/unraid/include/arrs.php b/Plugin/unraid/include/arrs.php index 5715167..e6472b2 100644 --- a/Plugin/unraid/include/arrs.php +++ b/Plugin/unraid/include/arrs.php @@ -6,23 +6,11 @@ require_once __DIR__ . '/config.php'; // ── Conf helpers ────────────────────────────────────────────────────────────── function vv_arr_scalar(string $raw, string $key): string { - return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) - ? trim($m[1]) : ''; + return vv_parse_conf_scalar($raw, $key); } -// Returns all configured hosts found in master.conf as ['host1'=>'hostname', ...] -// Returns ['host1' => 'hostname', 'host2' => 'hostname', ...] from master.conf. -// Matches both HOST1="name" and HOST1_NAME="name" (either convention). function vv_arr_known_hosts(): array { - $vars = vv_conf_vars(); // already parses HOST1="val" correctly - $hosts = []; - foreach ($vars as $k => $v) { - if (preg_match('/^HOST(\d+)$/', $k, $m) && $v !== '') { - $hosts['host' . $m[1]] = $v; - } - } - ksort($hosts); - return $hosts ?: ['host1' => 'HOST1']; + return vv_known_hosts(); } function vv_arr_node_names(): array { diff --git a/Plugin/unraid/include/common.php b/Plugin/unraid/include/common.php index 154bc69..936faad 100644 --- a/Plugin/unraid/include/common.php +++ b/Plugin/unraid/include/common.php @@ -23,13 +23,10 @@ function vv_system_info(): array { $uptimeRaw = $os['uptime'] ?? ''; if (is_numeric($uptimeRaw)) { $uptimeSec = (int)$uptimeRaw; - $days = intdiv($uptimeSec, 86400); - $hours = intdiv($uptimeSec % 86400, 3600); - $mins = intdiv($uptimeSec % 3600, 60); - $uptime = ($days ? "{$days}d " : '') . ($hours ? "{$hours}h " : '') . "{$mins}m"; + $uptime = vv_format_uptime($uptimeSec); } else { $uptimeSec = 0; - $uptime = $uptimeRaw ?: '—'; + $uptime = $uptimeRaw ?: '—'; } $load = sys_getloadavg(); @@ -57,10 +54,7 @@ function vv_system_info(): array { if (preg_match('/^model name\s*:\s*(.+)/', $line, $m)) { $cpuModel = trim($m[1]); break; } } $uptimeSec = (int)explode(' ', @file_get_contents('/proc/uptime') ?: '0')[0]; - $days = intdiv($uptimeSec, 86400); - $hours = intdiv($uptimeSec % 86400, 3600); - $mins = intdiv($uptimeSec % 3600, 60); - $uptime = ($days ? "{$days}d " : '') . ($hours ? "{$hours}h " : '') . "{$mins}m"; + $uptime = vv_format_uptime($uptimeSec); $load = sys_getloadavg(); return [ @@ -159,7 +153,10 @@ function vv_cpu_per_core(): array { $stateFile = '/tmp/vv_cpu_stat.json'; $prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : []; - file_put_contents($stateFile, json_encode($raw)); + // Atomic write — concurrent fast/slow polls read a consistent snapshot + $tmp = $stateFile . '.tmp'; + file_put_contents($tmp, json_encode($raw)); + rename($tmp, $stateFile); $usage = function(array $c, ?array $p): int { if (!$p) return 0; @@ -298,7 +295,9 @@ function vv_network_stats(): array { $stateFile = '/tmp/vv_net_stat.json'; $now = ['rx' => $rxBytes, 'tx' => $txBytes, 'ts' => microtime(true)]; $prev = file_exists($stateFile) ? (json_decode(file_get_contents($stateFile), true) ?: []) : []; - file_put_contents($stateFile, json_encode($now)); + $tmp = $stateFile . '.tmp'; + file_put_contents($tmp, json_encode($now)); + rename($tmp, $stateFile); $rxRate = $txRate = 0; if (!empty($prev['ts']) && ($dt = $now['ts'] - $prev['ts']) > 0.1) { @@ -314,15 +313,15 @@ function vv_network_stats(): array { ) ?: ''); // External IP — curl ifconfig.me, cached 5 min so we don't hammer it - $extIpCache = '/tmp/vv_ext_ip.cache'; - $extIp = ''; - if (file_exists($extIpCache) && (time() - filemtime($extIpCache)) < 300) { - $extIp = trim(file_get_contents($extIpCache) ?: ''); + $extIp = ''; + $extData = vv_cache_read('ext_ip', 300); + if ($extData) { + $extIp = $extData['ip'] ?? ''; } else { $fetched = trim(shell_exec('curl -sf --max-time 4 https://ifconfig.me 2>/dev/null') ?: ''); if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $fetched)) { $extIp = $fetched; - file_put_contents($extIpCache, $extIp); + vv_cache_write('ext_ip', ['ip' => $extIp]); } } diff --git a/Plugin/unraid/include/config.php b/Plugin/unraid/include/config.php index 5d2e504..b91e9d0 100644 --- a/Plugin/unraid/include/config.php +++ b/Plugin/unraid/include/config.php @@ -312,3 +312,57 @@ function vv_cache_write(string $key, array $data): void { file_put_contents($tmp, json_encode($data)); rename($tmp, $f); } + +// ── Shared utility functions (used across include/ and api/ files) ──────────── + +// Format seconds into "2d 3h 15m". +function vv_format_uptime(int $seconds): string { + $d = intdiv($seconds, 86400); + $h = intdiv($seconds % 86400, 3600); + $m = intdiv($seconds % 3600, 60); + return ($d ? "{$d}d " : '') . ($h ? "{$h}h " : '') . "{$m}m"; +} + +// Parse a scalar value from raw conf text. Matches KEY="value" or KEY=value. +// Identical logic was previously duplicated as vv_arr_scalar / vv_wd_scalar / +// vv_fb_scalar / vv_media_conf_scalar — all reduce to this one regex. +function vv_parse_conf_scalar(string $raw, string $key): string { + return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) + ? trim($m[1]) : ''; +} + +// Parse a key=value state file (e.g. fallback_state.db, partnership_state.db). +// Returns ['key' => 'value', ...]. Lines without '=' are ignored. +function vv_parse_kv_db(string $text): array { + $out = []; + foreach (explode("\n", $text) as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') continue; + [$k, $v] = array_pad(explode('=', $line, 2), 2, ''); + if ($k !== '') $out[trim($k)] = trim($v); + } + return $out; +} + +// All configured hosts from master.conf as ['host1' => 'hostname', ...]. +// Canonical version — previously duplicated as vv_arr_known_hosts / vv_fb_known_hosts. +function vv_known_hosts(): array { + $vars = vv_conf_vars(); + $hosts = []; + foreach ($vars as $k => $v) { + if (preg_match('/^HOST(\d+)$/', $k, $m) && $v !== '') { + $hosts['host' . $m[1]] = $v; + } + } + ksort($hosts); + return $hosts ?: ['host1' => 'HOST1']; +} + +// Local LAN IP via routing table — static-cached per request. +// Previously duplicated in include/docker_folders.php and inline in include/docker.php. +function vv_local_ip(): string { + static $ip = null; + if ($ip !== null) return $ip; + $ip = trim(shell_exec("ip route get 8.8.8.8 2>/dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? ''); + return $ip; +} diff --git a/Plugin/unraid/include/docker_folders.php b/Plugin/unraid/include/docker_folders.php index cc076a1..c86c788 100644 --- a/Plugin/unraid/include/docker_folders.php +++ b/Plugin/unraid/include/docker_folders.php @@ -1,11 +1,5 @@ /dev/null | awk '/src/{for(i=1;i<=NF;i++)if(\$i==\"src\")print \$(i+1)}'") ?? ''); - if (!$ip) $ip = gethostbyname(gethostname()); - return $ip; -} +require_once __DIR__ . '/config.php'; function vv_container_webui(string $name, array $portMap): string { $template = '/boot/config/plugins/dockerMan/templates-user/my-' . $name . '.xml'; diff --git a/Plugin/unraid/include/fallback.php b/Plugin/unraid/include/fallback.php index 9cb407a..1de5a04 100644 --- a/Plugin/unraid/include/fallback.php +++ b/Plugin/unraid/include/fallback.php @@ -7,15 +7,11 @@ require_once __DIR__ . '/partnership.php'; // vv_pt_ssh(), vv_pt_ts_peers() // ── Conf parsers ────────────────────────────────────────────────────────────── function vv_fb_bash_array(string $raw, string $varname): array { - if (!preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*\(\s*(.*?)\s*\)/ms', $raw, $m)) - return []; - preg_match_all('/"([^"]*)"/', $m[1], $items); - return array_values(array_filter($items[1])); + return vv_parse_bash_array($raw, $varname); } function vv_fb_scalar(string $raw, string $varname): string { - return preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) - ? trim($m[1]) : ''; + return vv_parse_conf_scalar($raw, $varname); } // ── State file ──────────────────────────────────────────────────────────────── @@ -90,16 +86,7 @@ function vv_fb_covers(string $covering, string $remote, string $coveringRaw, str // ── Known hosts ─────────────────────────────────────────────────────────────── function vv_fb_known_hosts(): array { - $master = vv_read_conf_raw('master.conf'); - preg_match_all('/^\s*(HOST(\d+))(?:_NAME)?\s*=\s*["\']?(\S+?)["\']?\s*$/m', $master, $m); - $hosts = []; - foreach ($m[1] as $i => $key) { - $num = $m[2][$i]; - $name = trim($m[3][$i]); - $hosts['host' . $num] = $name; - } - ksort($hosts); - return $hosts ?: ['host1' => 'HOST1']; + return vv_known_hosts(); } // ── Main data builder ───────────────────────────────────────────────────────── diff --git a/Plugin/unraid/include/media.php b/Plugin/unraid/include/media.php index fd9ae85..b86c289 100644 --- a/Plugin/unraid/include/media.php +++ b/Plugin/unraid/include/media.php @@ -6,8 +6,7 @@ require_once __DIR__ . '/config.php'; // ── Conf reader ─────────────────────────────────────────────────────────────── function vv_media_conf_scalar(string $raw, string $key): string { - return preg_match('/^\s*' . preg_quote($key, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) - ? trim($m[1]) : ''; + return vv_parse_conf_scalar($raw, $key); } // ── Server list from host conf ──────────────────────────────────────────────── diff --git a/Plugin/unraid/include/watchdog.php b/Plugin/unraid/include/watchdog.php index 217577c..2a8a31d 100644 --- a/Plugin/unraid/include/watchdog.php +++ b/Plugin/unraid/include/watchdog.php @@ -7,10 +7,7 @@ require_once __DIR__ . '/partnership.php'; // ── Conf array parser (bash arrays) ────────────────────────────────────────── function vv_wd_bash_array(string $raw, string $varname): array { - if (!preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*\(\s*(.*?)\s*\)/ms', $raw, $m)) - return []; - preg_match_all('/"([^"]*)"/', $m[1], $items); - return array_values(array_filter($items[1])); + return vv_parse_bash_array($raw, $varname); } function vv_wd_bash_assoc(string $raw, string $varname): array { @@ -24,8 +21,7 @@ function vv_wd_bash_assoc(string $raw, string $varname): array { } function vv_wd_scalar(string $raw, string $varname): string { - return preg_match('/^\s*' . preg_quote($varname, '/') . '\s*=\s*"?([^"\n]*)"?/m', $raw, $m) - ? trim($m[1]) : ''; + return vv_parse_conf_scalar($raw, $varname); } // ── State file parsers ────────────────────────────────────────────────────────