logger = Logger::getInstance(); } public function connect(array $server, bool $forceReconnect = false): bool { if ($this->connection !== null && !$forceReconnect) { return $this->connection->isConnected(); } $this->serverConfig = $server; $config = App::getConfig()['ssh']; try { $host = $server['ip_address'] ?? $server['hostname']; $port = (int) ($server['ssh_port'] ?? 22); $this->connection = new SSH2($host, $port, $config['timeout'] ?? 30); if (!empty($server['ssh_key'])) { $key = PublicKeyLoader::load($server['ssh_key']); $result = $this->connection->login($server['ssh_user'], $key); } elseif (!empty($server['ssh_password'])) { $result = $this->connection->login($server['ssh_user'], $server['ssh_password']); } else { throw new \RuntimeException('No authentication method configured'); } if (!$result) { throw new \RuntimeException('SSH authentication failed for ' . $host); } if ($config['keepalive_interval'] ?? 0 > 0) { $this->connection->setKeepAlive($config['keepalive_interval']); } $this->logger->info("SSH connection established to {$host}"); return true; } catch (\Throwable $e) { $this->logger->error("SSH connection failed: " . $e->getMessage(), [ 'host' => $host ?? 'unknown', 'port' => $port ?? 22, ]); $this->connection = null; return false; } } public function disconnect(): void { if ($this->connection !== null) { $this->connection->disconnect(); $this->connection = null; } } public function exec(string $command, int $timeout = 0, bool $pty = false): array { if ($this->connection === null || !$this->connection->isConnected()) { throw new \RuntimeException('No active SSH connection'); } $timeout = $timeout ?: (App::getConfig()['ssh']['command_timeout'] ?? 60); $this->connection->setTimeout($timeout); if ($pty) { $command .= '; echo "___EXIT___:$?"'; } $output = $this->connection->exec($command, null, $pty); $exitStatus = $this->connection->getExitStatus(); $stderr = $this->connection->getStdError() ?? ''; if ($pty && ($exitStatus === false || $exitStatus === null)) { if (preg_match('/___EXIT___:(\d+)/', $output, $m)) { $exitStatus = (int) $m[1]; $output = str_replace($m[0] . "\n", '', $output); $output = str_replace($m[0], '', $output); } } $this->logger->info("SSH command executed", [ 'command' => $command, 'exit_status' => $exitStatus, ]); return [ 'output' => $output, 'exit_status' => $exitStatus, 'stderr' => $stderr, ]; } public function write(string $data): void { if ($this->connection !== null && $this->connection->isConnected()) { $this->connection->write($data); } } public function read(string $expected = '', int $mode = SSH2::READ_REGEX): string { if ($this->connection === null || !$this->connection->isConnected()) { throw new \RuntimeException('No active SSH connection'); } return $this->connection->read($expected, $mode); } public function getInteractiveShell(): ?SSH2 { return $this->connection; } public function testConnection(array $server): bool { try { $result = $this->connect($server, true); if ($result) { $this->disconnect(); return true; } return false; } catch (\Throwable $e) { $this->logger->warning("SSH connection test failed", ['error' => $e->getMessage()]); return false; } } public function getSystemInfo(): array { $commands = [ 'hostname' => 'hostname', 'os' => 'cat /etc/os-release | head -1', 'kernel' => 'uname -r', 'uptime' => 'uptime -p', 'cpu_model' => 'cat /proc/cpuinfo | grep "model name" | head -1', 'cpu_cores' => 'nproc', 'mem_total' => 'free -b | grep Mem | awk \'{print $2}\'', 'mem_used' => "free -b | grep Mem | awk '{print $3}'", 'mem_avail' => "free -b | grep Mem | awk '{print $7}'", 'disk_total' => "df -B1 / | tail -1 | awk '{print $2}'", 'disk_used' => "df -B1 / | tail -1 | awk '{print $3}'", 'disk_avail' => "df -B1 / | tail -1 | awk '{print $4}'", 'disk_percent' => "df / | tail -1 | awk '{print $5}' | sed 's/%//'", ]; $info = []; foreach ($commands as $key => $cmd) { $result = $this->exec($cmd); $info[$key] = trim($result['output']); } return $info; } public function __destruct() { $this->disconnect(); } }