counts, storage usage, engine version, ping status * autostart_queue GET -> the autostart list in start order, each entry's * configured delay, and its last-known outcome (reusing * plugin/sbin/podman-autostart.sh's own failure-counter * files rather than tracking a second copy of that state) */ declare(strict_types=1); require __DIR__ . '/../include/bootstrap.php'; $action = $_GET['action'] ?? ''; switch ($action) { case 'summary': podman_json_response(system_summary($client, $podmanConfig)); break; case 'autostart_queue': podman_json_response(system_autostart_queue($client, $podmanConfig)); break; default: podman_json_error("Unknown action '{$action}'", 400); } /** @return array */ function system_summary(PodmanClient $client, PodmanConfig $config): array { if (!$client->ping()) { return [ 'reachable' => false, 'socketPath' => $config->socketPath, ]; } $containers = $client->listContainers(true); $running = 0; foreach ($containers as $c) { if (strtolower((string) ($c['State'] ?? '')) === 'running') { $running++; } } $pods = $client->listPods(); $images = $client->listImages(); $volumes = $client->listVolumes(); $networks = $client->listNetworks(); $info = $client->info(); $df = $client->systemDf(); $imagesSize = 0; foreach (($df['Images'] ?? []) as $img) { $imagesSize += (int) ($img['Size'] ?? 0); } $host = $info['host'] ?? []; $store = $info['store'] ?? []; $meminfo = podman_read_meminfo(); // libpod's host.memFree/swapFree are the kernel's raw "free" counters — // they exclude reclaimable buffers/cache, so on a host that's been up a // while they make used memory look dramatically higher than reality // (found live: 67% "used" here vs. the 19% `free -h` actually reports). // /proc/meminfo's MemAvailable is the same estimate `free -h`'s // "available" column and most monitoring tools use, so read it // directly instead of trusting libpod's numbers for this. $memTotal = $meminfo['MemTotal'] ?? (int) ($host['memTotal'] ?? 0); $memAvailable = $meminfo['MemAvailable'] ?? (int) ($host['memFree'] ?? 0); $swapTotal = $meminfo['SwapTotal'] ?? (int) ($host['swapTotal'] ?? 0); $swapFree = $meminfo['SwapFree'] ?? (int) ($host['swapFree'] ?? 0); $graphAllocated = (int) ($store['graphRootAllocated'] ?? 0); $graphUsed = (int) ($store['graphRootUsed'] ?? 0); return [ 'reachable' => true, 'socketPath' => $config->socketPath, // libpod's /info nests the version block under lowercase "version" // (unlike most other libpod endpoints, which are PascalCase // throughout) — verified live against a real podman system service. 'podmanVersion' => $info['version']['Version'] ?? null, 'containers' => [ 'total' => count($containers), 'running' => $running, 'stopped' => count($containers) - $running, ], 'pods' => count($pods), 'images' => count($images), 'volumes' => count($volumes), 'networks' => count($networks), 'storage' => [ 'imagesSizeBytes' => $imagesSize, 'imagesSizeFormatted' => podman_format_bytes($imagesSize), 'graphUsedBytes' => $graphUsed, 'graphAllocatedBytes' => $graphAllocated, 'graphUsedFormatted' => podman_format_bytes($graphUsed), 'graphAllocatedFormatted' => podman_format_bytes($graphAllocated), 'graphUsedPercent' => $graphAllocated > 0 ? round($graphUsed / $graphAllocated * 100, 1) : null, ], 'host' => [ 'cpuCount' => (int) ($host['cpus'] ?? 0), 'cpuPercent' => podman_read_cpu_percent() ?? (isset($host['cpuUtilization']['idlePercent']) ? round(100 - (float) $host['cpuUtilization']['idlePercent'], 1) : null), 'memUsedBytes' => max(0, $memTotal - $memAvailable), 'memTotalBytes' => $memTotal, 'memUsedFormatted' => podman_format_bytes(max(0, $memTotal - $memAvailable)), 'memTotalFormatted' => podman_format_bytes($memTotal), 'memPercent' => $memTotal > 0 ? round(($memTotal - $memAvailable) / $memTotal * 100, 1) : null, 'swapUsedBytes' => max(0, $swapTotal - $swapFree), 'swapTotalBytes' => $swapTotal, 'uptime' => (string) ($host['uptime'] ?? ''), ], ]; } /** * The Dashboard's "Autostart Queue" table: same list/order Settings already * reads via autostart_read() in settings.php, enriched with each entry's * configured post-start delay and its last-known outcome — derived from * plugin/sbin/podman-autostart.sh's own per-container failure-counter * files (under autostart-failures/) plus the container's actual * current state, rather than a second, separately-tracked history. * * @return array */ function system_autostart_queue(PodmanClient $client, PodmanConfig $config): array { if (!is_readable($config->autostartFile)) { return ['entries' => []]; } $names = []; foreach (file($config->autostartFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $line = trim(preg_replace('/#.*$/', '', $line) ?? ''); if ($line !== '') { $names[] = $line; } } if (!$names) { return ['entries' => []]; } $delays = []; if (is_readable($config->autostartDelayFile)) { foreach (file($config->autostartDelayFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { if (preg_match('/^([^=]+)=(\d+)$/', trim($line), $m)) { $delays[$m[1]] = (int) $m[2]; } } } // Mirrors podman-autostart.sh's own MAX_CONSECUTIVE_FAILURES — that // script is what actually decides when a container is Safe-Mode-paused; // this only needs to agree on the same threshold to describe it // accurately, not to make the decision itself. $maxFailures = 3; $failuresDir = $config->bootDir . '/autostart-failures'; $stateByName = []; foreach ($client->listContainers(true) as $c) { foreach (($c['Names'] ?? []) as $n) { $stateByName[ltrim((string) $n, '/')] = strtolower((string) ($c['State'] ?? '')); } } $entries = []; foreach ($names as $i => $name) { $failCount = 0; $failFile = $failuresDir . '/' . $name; if (is_readable($failFile)) { $failCount = (int) trim((string) file_get_contents($failFile)); } if ($failCount >= $maxFailures) { $status = 'safe-mode'; $label = "Safe-Mode ({$failCount} failures)"; } elseif ($failCount > 0) { $status = 'failed'; $label = "Failed ({$failCount}/{$maxFailures})"; } elseif (($stateByName[$name] ?? '') === 'running') { $status = 'started'; $label = 'Started'; } elseif (array_key_exists($name, $stateByName)) { $status = 'stopped'; $label = 'Not running'; } else { $status = 'unknown'; $label = 'Container not found'; } $entries[] = [ 'position' => $i + 1, 'name' => $name, 'delaySeconds' => $delays[$name] ?? 0, 'status' => $status, 'statusLabel' => $label, ]; } return ['entries' => $entries]; } /** * Reads the fields we need out of /proc/meminfo directly, in bytes. * ajax/*.php runs as PHP-FPM on the Unraid host itself (not inside a * container), so this is just a local file read — no shelling out needed. * * @return array */ function podman_read_meminfo(): array { $lines = @file('/proc/meminfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $out = []; foreach ($lines as $line) { if (preg_match('/^(MemTotal|MemAvailable|SwapTotal|SwapFree):\s*(\d+)\s*kB$/', $line, $m)) { $out[$m[1]] = ((int) $m[2]) * 1024; } } return $out; } /** * Live CPU usage via the classic /proc/stat delta technique: libpod's own * /info.host.cpuUtilization turned out to be a value computed once and * never resampled (verified live — three /info calls several seconds * apart returned byte-identical numbers), which is why the Dashboard's * CPU tile never moved. A single instantaneous read of /proc/stat can't * give a percentage on its own either (its counters are cumulative * jiffies since boot) — it needs two samples to diff. Since the Dashboard * already re-fetches this endpoint every ~2s via auto-refresh, the * previous sample is persisted to a small state file and diffed against * the current one on each call, exactly like `top`/`htop` do between * their own refresh ticks. Returns null (falls back to libpod's static * figure) only on the very first call, before any previous sample exists. */ function podman_read_cpu_percent(): ?float { $stat = @file_get_contents('/proc/stat'); if ($stat === false || !preg_match('/^cpu\s+(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+)/m', $stat, $m)) { return null; } [, $user, $nice, $system, $idle, $iowait, $irq, $softirq, $steal] = array_map('intval', $m); $total = $user + $nice + $system + $idle + $iowait + $irq + $softirq + $steal; $idleAll = $idle + $iowait; $stateFile = '/var/tmp/podman-cpu-sample'; $prevRaw = @file_get_contents($stateFile); @file_put_contents($stateFile, $total . ' ' . $idleAll, LOCK_EX); if ($prevRaw === false || !preg_match('/^(\d+) (\d+)$/', trim($prevRaw), $pm)) { return null; } $deltaTotal = $total - (int) $pm[1]; $deltaIdle = $idleAll - (int) $pm[2]; if ($deltaTotal <= 0) { return null; } return round((1 - $deltaIdle / $deltaTotal) * 100, 1); }