module_fn($module, 'table', $name); $settings = modules()->settings($module); $strictHostKey = !empty($settings['terminal_strict_hostkey']); $updateCmd = "sh -lc 'if ! command -v apt-get >/dev/null 2>&1; then echo \"__ERR__NO_APT\"; exit 2; fi; if sudo apt update -qq >/dev/null 2>&1; then count=$(apt-get -s dist-upgrade | grep -c \"^Inst \"); if [ \"$count\" -gt 0 ]; then echo \"__UPDATE__=1\"; else echo \"__UPDATE__=0\"; fi; echo \"__COUNT__=$count\"; else echo \"__ERR__APT_UPDATE\"; exit 3; fi'"; $upgradeCmd = "sh -lc 'id=\"$(. /etc/os-release 2>/dev/null && echo \"${ID:-}\")\"; current=\"$(. /etc/os-release 2>/dev/null && echo \"${VERSION_CODENAME:-}\")\"; if [ \"$id\" != \"debian\" ] || [ -z \"$current\" ]; then echo \"__ERR__NO_CODENAME\"; exit 2; fi; latest=\"$( (command -v curl >/dev/null 2>&1 && curl -fsSL https://deb.debian.org/debian/dists/stable/Release) || (command -v wget >/dev/null 2>&1 && wget -qO- https://deb.debian.org/debian/dists/stable/Release) )\"; latest=\"$(printf \"%s\" \"$latest\" | awk -F\": \" \"/^Codename:/{print $2}\")\"; if [ -z \"$latest\" ]; then echo \"__ERR__NO_LATEST\"; exit 3; fi; if [ \"$current\" != \"$latest\" ]; then echo \"__UPGRADE__=1\"; else echo \"__UPGRADE__=0\"; fi'"; $driver = (string)$pdo->getAttribute(PDO::ATTR_DRIVER_NAME); $nowExpr = $driver === 'pgsql' ? 'NOW()' : "DATETIME('now')"; $hosts = $pdo->query('SELECT * FROM ' . $table('hosts'))->fetchAll(PDO::FETCH_ASSOC); foreach ($hosts as $host) { $id = (int)($host['id'] ?? 0); if ($id <= 0) continue; [$updExit, $updOut, $updErr] = runSshCommandCapture($host, $updateCmd, $strictHostKey, 20); $updOutStr = (string)$updOut; $updErrStr = (string)$updErr; $updateCount = null; $updatePreview = ''; if ($updExit === 0 && !str_contains($updOutStr, '__ERR__')) { if (preg_match('/^__COUNT__=(\d+)$/m', $updOutStr, $m)) { $updateCount = (int)$m[1]; } } [$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25); $upgOutStr = (string)$upgOut; $upgErrStr = (string)$upgErr; $upgradeAvailable = null; if ($upgExit === 0 && !str_contains($upgOutStr, '__ERR__')) { if (preg_match('/^__UPGRADE__=(0|1)$/m', $upgOutStr, $m)) { $upgradeAvailable = $m[1] === '1'; } } $updErrVal = $updExit === 0 && !str_contains($updOutStr, '__ERR__') ? null : trim($updErrStr ?: $updOutStr); $upgErrVal = $upgExit === 0 && !str_contains($upgOutStr, '__ERR__') ? null : trim($upgErrStr ?: $upgOutStr); $stmt = $pdo->prepare( 'UPDATE ' . $table('hosts') . ' SET update_checked_at = ' . $nowExpr . ', update_count = :update_count, update_preview = :update_preview, update_error = :update_error, upgrade_available = :upgrade_available, upgrade_raw = :upgrade_raw, upgrade_error = :upgrade_error WHERE id = :id' ); $stmt->execute([ 'update_count' => $updateCount, 'update_preview' => $updatePreview !== '' ? $updatePreview : null, 'update_error' => $updErrVal, 'upgrade_available' => $upgradeAvailable === null ? null : ($upgradeAvailable ? 1 : 0), 'upgrade_raw' => $upgExit === 0 ? trim($upgOutStr) : null, 'upgrade_error' => $upgErrVal, 'id' => $id, ]); } echo "OK\n"; function runSshCommandCapture(array $host, string $command, bool $strictHostKey, int $timeoutSec): array { $hostAddr = (string)($host['host'] ?? ''); $user = (string)($host['username'] ?? ''); $port = (int)($host['port'] ?? 22); $authType = (string)($host['auth_type'] ?? 'key'); $keyPath = (string)($host['key_path'] ?? ''); $password = (string)($host['password'] ?? ''); $opts = $strictHostKey ? '-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts' : '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'; $opts .= ' -o ConnectTimeout=6 -o NumberOfPasswordPrompts=1'; $target = escapeshellarg($user . '@' . $hostAddr); $remote = '/bin/bash -lc ' . escapeshellarg($command); $cmd = 'ssh ' . $opts . ' -p ' . (int)$port . ' '; if ($authType === 'key' && $keyPath !== '') { $cmd .= '-i ' . escapeshellarg($keyPath) . ' -o BatchMode=yes '; } elseif ($authType === 'key') { $cmd .= '-o BatchMode=yes '; } $cmd .= $target . ' -- ' . $remote; if ($authType === 'pass' && $password !== '') { $cmd = 'sshpass -p ' . escapeshellarg($password) . ' ' . $cmd; } $descriptors = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($cmd, $descriptors, $pipes); if (!is_resource($process)) { return [255, '', 'proc_open failed']; } stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $out = ''; $err = ''; $start = time(); while (true) { $status = proc_get_status($process); $out .= stream_get_contents($pipes[1]); $err .= stream_get_contents($pipes[2]); if (!$status['running']) { $exit = (int)$status['exitcode']; proc_close($process); return [$exit, $out, $err]; } if (time() - $start > $timeoutSec) { proc_terminate($process, 9); proc_close($process); return [124, $out, $err]; } usleep(100000); } }