current settings + autostart list + package versions * save POST {"storagePath": "...", "storageImageSizeGb": 20, * "enabled": true, "stopTimeoutSeconds": 10} * autostart_save POST {"names": ["postgres", "nextcloud", ...]} * service_status GET -> {"running": bool, "output": "..."} * service_start POST -> {"running": bool, "output": "..."} * service_stop POST -> {"running": bool, "output": "..."} * service_restart POST -> {"running": bool, "output": "..."} */ declare(strict_types=1); require __DIR__ . '/../include/bootstrap.php'; const RC_PODMAN = '/etc/rc.d/rc.podman'; $action = $_GET['action'] ?? ''; switch ($action) { case 'get': podman_json_response(settings_get($podmanConfig)); break; case 'save': $body = podman_read_json_body(); settings_save($podmanConfig, $body); podman_json_response(['status' => 'saved']); break; case 'autostart_save': $body = podman_read_json_body(); $names = $body['names'] ?? null; if (!is_array($names)) { podman_json_error('Missing names array in request body', 400); } autostart_save($podmanConfig, $names); podman_json_response(['status' => 'saved']); break; case 'service_status': podman_json_response(rc_podman('status', 15)); break; case 'service_start': podman_json_response(rc_podman('start', 120)); break; case 'service_stop': podman_json_response(rc_podman('stop', 120)); break; case 'service_restart': podman_json_response(rc_podman('restart', 120)); break; default: podman_json_error("Unknown action '{$action}'", 400); } /** * Shells out to /etc/rc.d/rc.podman — the plugin's own real * start/stop/status script, the SAME one the array-start event hook and * a terminal `rc.podman status` use (see plugin/rc.d/rc.podman's header * comment). This exists specifically so a fresh install where podman * failed to start (e.g. no cache pool configured yet, see * podman-storage.sh's "does not exist or is not mounted" error) can be * diagnosed and retried from the WebUI itself — no SSH/terminal access * needed, which is exactly what was missing when this was first needed * live (a fresh install on a different Unraid box with nobody able to * reach a terminal to run `rc.podman start` by hand). * * @return array{running: bool, output: string} */ function rc_podman(string $verb, int $timeoutSeconds): array { $output = run_rc_podman($verb, $timeoutSeconds); // Only `rc.podman status` prints the "service: running // (pid ..., socket ...)" line this regex looks for — start/stop/ // restart's OWN messages are worded differently ("start: already // running (pid ...)", "stop: stopped", ...), so relying on THIS same // regex against THEIR output silently reported "not running" right // after a successful start (found live: a start that printed "start: // already running" turned the status chip red). Always running a // fresh `status` afterward — regardless of which verb was actually // requested — is the one output format this check can trust. $statusOutput = $verb === 'status' ? $output : run_rc_podman('status', 15); $running = (bool) preg_match('/service:\s+running/', $statusOutput); return ['running' => $running, 'output' => $output]; } function run_rc_podman(string $verb, int $timeoutSeconds): string { $descriptors = [1 => ['pipe', 'w'], 2 => ['pipe', 'w']]; $process = proc_open([RC_PODMAN, $verb], $descriptors, $pipes); if (!is_resource($process)) { podman_json_error("Could not run rc.podman {$verb}", 500); } stream_set_timeout($pipes[1], $timeoutSeconds); $stdout = stream_get_contents($pipes[1]) ?: ''; $stderr = stream_get_contents($pipes[2]) ?: ''; fclose($pipes[1]); fclose($pipes[2]); proc_close($process); return trim($stdout . $stderr); } /** @return array */ function settings_get(PodmanConfig $config): array { return [ 'storagePath' => $config->storagePath, 'storageImageSizeGb' => $config->storageImageSizeGb, 'enabled' => $config->enabled, 'stopTimeoutSeconds' => $config->stopTimeoutSeconds, 'autostart' => autostart_read($config), 'packageVersions' => installed_package_versions(), ]; } /** @param array $input */ function settings_save(PodmanConfig $config, array $input): void { $storagePath = isset($input['storagePath']) ? (string) $input['storagePath'] : $config->storagePath; if (!storage_path_is_safe($storagePath)) { podman_json_error( "storagePath ({$storagePath}) is under /mnt/user (FUSE/shfs). " . 'The overlay storage driver needs a real mounted filesystem — use a cache pool or a specific disk path instead.', 400 ); } $lines = [ '# Rewritten by the unraid-podman WebUI (ajax/settings.php).', '# Applies on the next "rc.podman restart" — see plugin/rc.d/rc.podman.', 'STORAGE_PATH="' . $storagePath . '"', 'STORAGE_IMAGE_SIZE_GB="' . (int) ($input['storageImageSizeGb'] ?? $config->storageImageSizeGb) . '"', 'PODMAN_ENABLED="' . ((bool) ($input['enabled'] ?? $config->enabled) ? 'yes' : 'no') . '"', 'STOP_TIMEOUT="' . (int) ($input['stopTimeoutSeconds'] ?? $config->stopTimeoutSeconds) . '"', 'CONFIG_SCHEMA_VERSION="1"', '', ]; $target = $config->bootDir . '/podman.cfg'; if (file_put_contents($target, implode("\n", $lines), LOCK_EX) === false) { podman_json_error("Could not write {$target} — check permissions on /boot/config/plugins/podman/", 500); } } /** * Mirrors plugin/sbin/podman-common.sh's podman_storage_path_is_safe() — * kept in sync deliberately (both reject the same /mnt/user prefix, for * the same reason) rather than shelling out to the bash version, since * this is a one-line string check, not worth a process spawn for. */ function storage_path_is_safe(string $path): bool { return $path !== '/mnt/user' && !str_starts_with($path, '/mnt/user/'); } /** @return array */ function autostart_read(PodmanConfig $config): array { if (!is_readable($config->autostartFile)) { return []; } $lines = file($config->autostartFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $names = []; foreach ($lines as $line) { $line = trim(preg_replace('/#.*$/', '', $line) ?? ''); if ($line !== '') { $names[] = $line; } } return $names; } /** @param array $names */ function autostart_save(PodmanConfig $config, array $names): void { $lines = array_map(static fn($n) => (string) $n, $names); $content = implode("\n", $lines) . (count($lines) > 0 ? "\n" : ''); if (file_put_contents($config->autostartFile, $content, LOCK_EX) === false) { podman_json_error("Could not write {$config->autostartFile}", 500); } } /** * Reads /usr/local/share/unraid-podman/installed-versions.env — the same * manifest plugin/sbin/podman-verify-packages.sh and * podman-update-packages.sh use (see plugin/podman.plg's postinstall step, * which generates it) — so Settings shows exactly what those tools would * report, not a second, possibly-diverging source of truth. * * @return array */ function installed_package_versions(): array { $path = '/usr/local/share/unraid-podman/installed-versions.env'; if (!is_readable($path)) { return []; } $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $out = []; foreach ($lines as $line) { $line = trim($line); if ($line === '' || str_starts_with($line, '#')) { continue; } if (preg_match('/^([A-Z_][A-Z0-9_]*)="?([^"]*)"?$/', $line, $m)) { $out[$m[1]] = $m[2]; } } return $out; }