detector->detect($server)->supports('updates'); } /** * Number of pending package upgrades via a dry-run/metadata read (no root * needed), or NULL when it could not run (unsupported manager / errored) — so * the caller shows "unbekannt" rather than a misleading "0 / up to date". The * remote script emits a CLUSEV_OK sentinel only when the query actually ran. */ public function pendingUpdates(Server $server): ?int { $os = $this->detector->detect($server); if (! $os->managesPackages()) { return null; } $res = $this->fleet->runPlain($server, PackageManager::for($os)->pendingScript()); if (! str_contains($res['output'], 'CLUSEV_OK')) { return null; } return preg_match('/CLUSEV_PENDING=(\d+)/', $res['output'], $m) ? (int) $m[1] : null; } /** * Pending update count AND its security subset in one round-trip. Either value is NULL when the * query couldn't run (unsupported manager / errored) so the caller shows "unbekannt" rather than * a misleading 0. Security is best-effort per family; it is always ≤ pending. * * @return array{pending: ?int, security: ?int} */ public function updateCounts(Server $server): array { $os = $this->detector->detect($server); if (! $os->managesPackages()) { return ['pending' => null, 'security' => null]; } $res = $this->fleet->runPlain($server, PackageManager::for($os)->countsScript()); if (! str_contains($res['output'], 'CLUSEV_OK')) { return ['pending' => null, 'security' => null]; } $pending = preg_match('/CLUSEV_PENDING=(\d+)/', $res['output'], $m) ? (int) $m[1] : null; $security = preg_match('/CLUSEV_SECURITY=(\d+)/', $res['output'], $s) ? (int) $s[1] : null; // Security can never exceed total pending (guards a noisy grep). if ($pending !== null && $security !== null) { $security = min($security, $pending); } return ['pending' => $pending, 'security' => $security]; } /** * Refresh the index and apply all upgrades as root, using the host's package * manager. Long-running, so a 900s timeout. Output trimmed to the last ~400 * chars for a compact result panel. * * @return array{ok: bool, output: string} */ public function applyUpgrades(Server $server): array { $os = $this->detector->detect($server); if ($reason = $os->supports('updates')) { return ['ok' => false, 'output' => $reason]; } $res = $this->fleet->runPrivileged($server, PackageManager::for($os)->applyScript(), 900); return ['ok' => $res['ok'], 'output' => $this->tail($res['output'], 400)]; } /** Keep only the last $max characters of a (multi-line) output. */ private function tail(string $out, int $max): string { $out = trim($out); if (mb_strlen($out) <= $max) { return $out; } return '…'.mb_substr($out, -$max); } }