From 515ab4ff91bc503cde5d7f308c5ff2c93ec34301 Mon Sep 17 00:00:00 2001 From: yinxue <2643126914@qq.com> Date: Fri, 14 Nov 2025 15:25:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E5=AE=8C=E6=88=90=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E7=AB=AF=E5=8F=A3=E5=8F=B7=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/remote-container.ts | 241 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 236 insertions(+), 5 deletions(-) diff --git a/src/remote-container.ts b/src/remote-container.ts index 5343ee8..7babec8 100644 --- a/src/remote-container.ts +++ b/src/remote-container.ts @@ -5,6 +5,8 @@ import * as os from 'os'; import * as vscode from 'vscode'; import * as rd from 'readline' const { NodeSSH } = require('node-ssh') +const { spawn } = require('child_process'); +const net = require('net'); import * as utils from './utils'; import User from './user'; @@ -12,6 +14,7 @@ import DevstarAPIHandler from './devstar-api'; export default class RemoteContainer { private user: User; + private sshProcesses?: Map; constructor(user: User) { this.user = user @@ -93,7 +96,7 @@ export default class RemoteContainer { console.log(`[RemoteContainer] Running in local environment, attempting firstConnect`); try { //传入真实IP - await this.firstConnect(host, hostname, username, port) + await this.firstConnect(host, hostname, username, port, path) .then((res) => { if (res === 'success') { console.log(`[RemoteContainer] firstConnect succeeded, opening remote folder`); @@ -123,11 +126,11 @@ export default class RemoteContainer { * @param hostname ip * @param username * @param port + * @param projectPath 项目路径(用于查找devcontainer.json) * @returns 成功返回success */ - // connect with key - async firstConnect(host: string, hostname: string, username: string, port: number): Promise { - console.log(`[RemoteContainer] firstConnect called with:`, { host, hostname, username, port }); + async firstConnect(host: string, hostname: string, username: string, port: number, projectPath?: string): Promise { + console.log(`[RemoteContainer] firstConnect called with:`, { host, hostname, username, port, projectPath }); return new Promise(async (resolve, reject) => { const ssh = new NodeSSH(); @@ -232,6 +235,19 @@ export default class RemoteContainer { throw new Error('Failed to get VSCode commit ID'); } + // 设置端口映射(如果提供了项目路径) + if (projectPath) { + progress.report({ message: vscode.l10n.t("Setting up port mappings") }); + try { + await this.setupPortForwarding(hostname, port, projectPath); + console.log(`[RemoteContainer] Port mappings established successfully`); + } catch (portError) { + console.warn(`[RemoteContainer] Port forwarding setup failed:`, portError); + // 端口映射失败不应阻止主要功能 + vscode.window.showWarningMessage('端口映射设置失败,但容器连接已建立'); + } + } + await ssh.dispose(); console.log(`[RemoteContainer] SSH connection disposed`); @@ -255,6 +271,151 @@ export default class RemoteContainer { }); } + /** + * 从 devcontainer.json 中提取端口映射配置 + */ + private async getPortsAttributesFromDevContainer(ssh: any, containerPath: string): Promise { + try { + console.log(`[RemoteContainer] Looking for devcontainer.json at: ${containerPath}`); + + // 在容器中查找 devcontainer.json 文件 + const findResult = await ssh.execCommand(`find ${containerPath} -name "devcontainer.json" -type f`); + + if (findResult.code === 0 && findResult.stdout.trim()) { + const devcontainerPath = findResult.stdout.trim().split('\n')[0]; // 取第一个找到的文件 + console.log(`[RemoteContainer] Found devcontainer.json at: ${devcontainerPath}`); + + // 读取 devcontainer.json 内容 + const readResult = await ssh.execCommand(`cat ${devcontainerPath}`); + if (readResult.code === 0) { + const devcontainerConfig = JSON.parse(readResult.stdout); + console.log(`[RemoteContainer] Parsed devcontainer.json:`, devcontainerConfig); + + return devcontainerConfig.portsAttributes || {}; + } + } else { + console.log(`[RemoteContainer] No devcontainer.json found in ${containerPath}`); + } + } catch (error) { + console.error(`[RemoteContainer] Error reading devcontainer.json:`, error); + } + + return {}; + } + + /** + * 建立端口映射 + */ + private async setupPortForwarding(hostname: string, port: number, containerPath: string): Promise { + console.log(`[RemoteContainer] Setting up port forwarding for ${hostname}:${port}`); + + const ssh = new NodeSSH(); + + try { + // 连接到容器 + await ssh.connect({ + host: hostname, + username: 'root', + port: port, + privateKeyPath: this.user.getUserPrivateKeyPath(), + readyTimeout: 30000, + }); + + // 获取端口配置 + const portsAttributes = await this.getPortsAttributesFromDevContainer(ssh, containerPath); + console.log(`[RemoteContainer] Ports attributes found:`, portsAttributes); + + // 建立端口映射 + for (const [containerPortStr, attributes] of Object.entries(portsAttributes)) { + const containerPort = parseInt(containerPortStr); + if (!isNaN(containerPort)) { + // 查找可用的本地端口 + const localPort = await this.findAvailableLocalPort(); + + // 建立 SSH 端口转发 + await this.createSSHPortForward(hostname, port, containerPort, localPort); + + console.log(`[RemoteContainer] Port mapping established: localhost:${localPort} -> container:${containerPort}`); + + // 显示通知 + const label = (attributes as any).label || `Port ${containerPort}`; + vscode.window.showInformationMessage( + `端口映射: ${label} -> http://localhost:${localPort}` + ); + } + } + + await ssh.dispose(); + } catch (error) { + console.error(`[RemoteContainer] Error setting up port forwarding:`, error); + await ssh.dispose(); + throw error; + } + } + + /** + * 查找可用的本地端口 + */ + private async findAvailableLocalPort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.unref(); + server.on('error', reject); + server.listen(0, () => { + const port = server.address().port; + server.close(() => { + resolve(port); + }); + }); + }); + } + + /** + * 创建 SSH 端口转发 + */ + private async createSSHPortForward(hostname: string, sshPort: number, containerPort: number, localPort: number): Promise { + return new Promise((resolve, reject) => { + const sshArgs = [ + '-N', // 不执行远程命令 + '-L', // 本地端口转发 + `${localPort}:localhost:${containerPort}`, // 转发规则 + '-o', 'StrictHostKeyChecking=no', + '-o', 'UserKnownHostsFile=/dev/null', + '-p', sshPort.toString(), + '-i', this.user.getUserPrivateKeyPath(), + `root@${hostname}` + ]; + + const sshProcess = spawn('ssh', sshArgs); + + sshProcess.on('error', (error: Error) => { + console.error(`[RemoteContainer] SSH port forward error:`, error); + reject(error); + }); + + sshProcess.stdout.on('data', (data: Buffer) => { + console.log(`[RemoteContainer] SSH stdout:`, data.toString()); + }); + + sshProcess.stderr.on('data', (data: Buffer) => { + console.log(`[RemoteContainer] SSH stderr:`, data.toString()); + }); + + // 存储进程引用以便后续管理(可选) + if (!this.sshProcesses) { + this.sshProcesses = new Map(); + } + const key = `${hostname}:${sshPort}:${containerPort}`; + this.sshProcesses.set(key, sshProcess); + + // 等待一段时间确保连接建立 + setTimeout(() => { + console.log(`[RemoteContainer] SSH port forward established: ${localPort} -> ${hostname}:${containerPort}`); + resolve(); + }, 2000); + }); + } + /** * 本地环境,保存项目的ssh连接信息 * @param host @@ -312,10 +473,23 @@ export default class RemoteContainer { * * @host 表示project name */ - openRemoteFolder(host: string, port: number, username: string, path: string): void { + async openRemoteFolder(host: string, port: number, username: string, path: string): Promise { console.log(`[RemoteContainer] openRemoteFolder called with:`, { host, port, username, path }); try { + // 先设置端口映射 + const sshConfig = await this.getSSHConfig(host); + if (sshConfig) { + try { + await this.setupPortForwarding(sshConfig.hostname, port, path); + console.log(`[RemoteContainer] Port forwarding established for existing connection`); + } catch (portError) { + console.warn(`[RemoteContainer] Port forwarding setup failed:`, portError); + // 端口映射失败不应阻止主要功能 + } + } + + // 然后打开远程文件夹 let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`); terminal.show(true); @@ -331,6 +505,63 @@ export default class RemoteContainer { vscode.window.showErrorMessage(`打开远程文件夹失败: ${errorMessage}`); } } + + /** + * 从 SSH config 获取主机配置 + */ + private async getSSHConfig(host: string): Promise<{ hostname: string; port: number } | null> { + const sshConfigPath = path.join(os.homedir(), '.ssh', 'config'); + + if (!fs.existsSync(sshConfigPath)) { + return null; + } + + const configContent = fs.readFileSync(sshConfigPath, 'utf8'); + const lines = configContent.split('\n'); + + let currentHost = ''; + let hostname = ''; + let port = 22; // 默认端口 + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith('Host ')) { + currentHost = trimmed.substring(5).trim(); + } else if (currentHost === host) { + if (trimmed.startsWith('HostName ')) { + hostname = trimmed.substring(9).trim(); + } else if (trimmed.startsWith('Port ')) { + port = parseInt(trimmed.substring(5).trim()); + } + } + + if (hostname && currentHost === host) { + return { hostname, port }; + } + } + + return null; + } + + /** + * 清理端口转发(可选) + */ + public cleanupPortForwarding(): void { + if (!this.sshProcesses) return; + + console.log(`[RemoteContainer] Cleaning up ${this.sshProcesses.size} SSH port forwarding processes`); + + for (const [key, process] of this.sshProcesses.entries()) { + try { + process.kill(); + console.log(`[RemoteContainer] Killed SSH process: ${key}`); + } catch (error) { + console.warn(`[RemoteContainer] Failed to kill process ${key}:`, error); + } + } + this.sshProcesses.clear(); + } } /**