module_fn($module, 'table', $name); $settings = modules()->settings($module); $strictHostKey = !empty($settings['terminal_strict_hostkey']); $updateCmd = <<<'SH' if ! command -v apt-get >/dev/null 2>&1; then echo "__ERR__NO_APT"; exit 2; fi; if sudo -n apt update -qq >/dev/null 2>&1; then echo "__APT_UPDATE__=1"; else echo "__APT_UPDATE__=0"; fi; count=$(apt-get -s dist-upgrade 2>/dev/null | grep -c "^Inst "); echo "__COUNT__=$count" SH; $upgradeCmd = <<<'SH' 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 "__UPGRADE__=0"; exit 0; 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 "__UPGRADE__=0"; echo "__RAW__=NO_FETCH"; exit 0; fi; if [ "$current" != "$latest" ]; then echo "__UPGRADE__=1"; else echo "__UPGRADE__=0"; fi SH; $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 = ''; $updateErr = str_contains($updOutStr, '__ERR__NO_APT'); if ($updExit === 0 && !$updateErr) { if (preg_match('/^__COUNT__=(\d+)$/m', $updOutStr, $m)) { $updateCount = (int)$m[1]; } } $updatePreview = trim($updOutStr); if (strlen($updatePreview) > 1200) { $updatePreview = substr($updatePreview, 0, 1200); } [$upgExit, $upgOut, $upgErr] = runSshCommandCapture($host, $upgradeCmd, $strictHostKey, 25); $upgOutStr = (string)$upgOut; $upgErrStr = (string)$upgErr; $upgradeAvailable = null; $upgradeErr = str_contains($upgOutStr, '__ERR__'); if ($upgExit === 0 && !$upgradeErr) { if (preg_match('/^__UPGRADE__=(0|1)$/m', $upgOutStr, $m)) { $upgradeAvailable = $m[1] === '1'; } } $updErrVal = $updExit === 0 && !$updateErr ? null : trim($updErrStr ?: $updOutStr); $upgErrVal = $upgExit === 0 && !$upgradeErr ? 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 -o LogLevel=ERROR -o RequestTTY=no'; $target = escapeshellarg($user . '@' . $hostAddr); $remote = '/bin/sh -c ' . escapeshellarg($command); $cmd = 'ssh -T ' . $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); } }