diff --git a/.gitea/workflows/devstar-vscode-release.yaml b/.gitea/workflows/devstar-vscode-release.yaml new file mode 100644 index 0000000..d174496 --- /dev/null +++ b/.gitea/workflows/devstar-vscode-release.yaml @@ -0,0 +1,59 @@ +name: CI/CD Pipeline for DevStar Extension +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + container: + image: gitea/runner-images:ubuntu-latest + steps: + - name: 拉取代码 + uses: https://devstar.cn/actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 安装依赖 + run: | + npm install + + - name: 构建插件 + run: | + webpack --mode production && vsce package + + - name: 上传构建产物 + uses: https://devstar.cn/actions/upload-artifact@v4 + with: + name: devstar-extension + path: "*.vsix" + + publish: + needs: build + runs-on: ubuntu-latest + if: gitea.ref == 'refs/heads/main' + + steps: + - name: 拉取代码 + uses: https://devstar.cn/actions/checkout@v4 + + - name: 安装依赖 + run: | + npm install + + - name: 发布到 VSCode 市场 + run: vsce publish + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + + - name: 更新版本标签 + run: | + version=$(node -p "require('./package.json').version") + git config --local user.email "ci@devstar.cn" + git config --local user.name "DevStar CI" + git tag -a "v$version" -m "Release version $version" + git push origin "v$version" \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index ab83ac7..2c040e1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -178,6 +178,11 @@ export class DevStarExtension { vscode.commands.registerCommand('devstar.showHome', () => this.dsHome.toggle() ), + vscode.commands.registerCommand('devstar.showPortMappings', () => { + // 这里需要根据当前活动连接获取hostname和port + // 简化实现:显示所有活动的端口映射 + this.remoteContainer.showPortMappingsInOutputChannel('current', 0); + }), vscode.commands.registerCommand('devstar.clean', () => { // 先清除ssh key if (fs.existsSync(this.user.getUserPrivateKeyPath())) { diff --git a/src/remote-container.ts b/src/remote-container.ts index 7babec8..02890cd 100644 --- a/src/remote-container.ts +++ b/src/remote-container.ts @@ -3,8 +3,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as vscode from 'vscode'; -import * as rd from 'readline' -const { NodeSSH } = require('node-ssh') +import * as rd from 'readline'; +const { NodeSSH } = require('node-ssh'); const { spawn } = require('child_process'); const net = require('net'); @@ -15,68 +15,56 @@ import DevstarAPIHandler from './devstar-api'; export default class RemoteContainer { private user: User; private sshProcesses?: Map; + private portMappings: Map> = new Map(); + private statusBarItems?: Map; constructor(user: User) { - this.user = user + this.user = user; } public setUser(user: User) { - this.user = user + this.user = user; } /** * 第一次打开远程项目 - * - * 远程环境,先创建local窗口,在通过命令行调用url打开(目前,仅支持vscode协议) - * @param host 项目名称 - * @param hostname ip - * @param port - * @param username - * @param path - * @param context 用于支持远程项目环境 */ async firstOpenProject(host: string, hostname: string, port: number, username: string, path: string, context: vscode.ExtensionContext) { console.log(`[RemoteContainer] firstOpenProject called with:`, { host, hostname, port, username, path }); - + if (vscode.env.remoteName) { // 远程环境 console.log(`[RemoteContainer] Running in remote environment: ${vscode.env.remoteName}`); - + try { await vscode.commands.executeCommand('workbench.action.terminal.newLocal'); const terminal = vscode.window.terminals[vscode.window.terminals.length - 1]; - - if (terminal) { - let devstarDomain: string | undefined = context.globalState.get("devstarDomain_" + vscode.env.sessionId) - if (devstarDomain == undefined || devstarDomain == "") - devstarDomain = undefined - // vscode协议 - // 根据系统+命令行版本确定命令 - const semver = require('semver') - const powershellVersion = context.globalState.get('powershellVersion') - const powershell_semver_compatible_version = semver.coerce(powershellVersion) + if (terminal) { + let devstarDomain: string | undefined = context.globalState.get("devstarDomain_" + vscode.env.sessionId); + if (devstarDomain == undefined || devstarDomain == "") { + devstarDomain = undefined; + } + + const semver = require('semver'); + const powershellVersion = context.globalState.get('powershellVersion'); + const powershell_semver_compatible_version = semver.coerce(powershellVersion); let command = ''; if (devstarDomain === undefined) { - // 不传递devstarDomain if (powershellVersion === undefined) { command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}"`; } else if (semver.satisfies(powershell_semver_compatible_version, ">=5.1.26100")) { - // win & powershell >= 5.1.26100.0 command = `code --new-window ; code --% --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}"`; } else { - // win & powershell < 5.1.26100.0 command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}"`; } } else { if (powershellVersion === undefined) { command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}&devstar_domain=${devstarDomain}"`; } else if (semver.satisfies(powershell_semver_compatible_version, ">=5.1.26100")) { - // win & powershell >= 5.1.26100.0 command = `code --new-window ; code --% --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}&devstar_domain=${devstarDomain}"`; } else { - // win & powershell < 5.1.26100.0 command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}&devstar_domain=${devstarDomain}"`; } } @@ -95,12 +83,10 @@ export default class RemoteContainer { } else { console.log(`[RemoteContainer] Running in local environment, attempting firstConnect`); try { - //传入真实IP await this.firstConnect(host, hostname, username, port, path) .then((res) => { if (res === 'success') { console.log(`[RemoteContainer] firstConnect succeeded, opening remote folder`); - // only success then open folder this.openRemoteFolder(host, port, username, path); } else { console.error(`[RemoteContainer] firstConnect returned: ${res}`); @@ -122,19 +108,13 @@ export default class RemoteContainer { /** * local environment,第一次连接其他项目 - * @param host 项目名称 - * @param hostname ip - * @param username - * @param port - * @param projectPath 项目路径(用于查找devcontainer.json) - * @returns 成功返回success */ 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(); - + vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t("Installing vscode-server and devstar extension in container"), @@ -142,18 +122,16 @@ export default class RemoteContainer { }, async (progress) => { try { console.log(`[RemoteContainer] Checking SSH keys existence`); - - // 检查公私钥是否存在,如果不存在,需要创建 + if (!this.user.existUserPrivateKey() || !this.user.existUserPublicKey()) { console.log(`[RemoteContainer] SSH keys not found, creating new keys`); - await this.user.createUserSSHKey() - - // 上传公钥 + await this.user.createUserSSHKey(); + console.log(`[RemoteContainer] Uploading public key`); - const devstarAPIHandler = new DevstarAPIHandler() - const uploadResult = await devstarAPIHandler.uploadUserPublicKey(this.user) + const devstarAPIHandler = new DevstarAPIHandler(); + const uploadResult = await devstarAPIHandler.uploadUserPublicKey(this.user); if (uploadResult !== "ok") { - throw new Error('Upload public key failed.') + throw new Error('Upload public key failed.'); } console.log(`[RemoteContainer] Public key uploaded successfully`); } else { @@ -161,44 +139,41 @@ export default class RemoteContainer { } } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; - console.error("[RemoteContainer] Failed to first connect container - SSH key setup: ", error) + console.error("[RemoteContainer] Failed to first connect container - SSH key setup: ", error); reject(error); return; } - // 本地环境 try { console.log(`[RemoteContainer] Attempting SSH connection to ${hostname}:${port} as ${username}`); - - // connect with key + await ssh.connect({ host: hostname, username: 'root', port: port, privateKeyPath: this.user.getUserPrivateKeyPath(), - readyTimeout: 30000, // 增加超时时间到30秒 + readyTimeout: 30000, onKeyboardInteractive: ( - _name: string, - _instructions: string, - _instructionsLang: string, - _prompts: any[], + _name: string, + _instructions: string, + _instructionsLang: string, + _prompts: any[], finish: (responses: string[]) => void ) => { console.log(`[RemoteContainer] Keyboard interactive authentication required`); finish([]); } }); - + console.log(`[RemoteContainer] SSH connection established successfully`); progress.report({ message: vscode.l10n.t("Connected! Start installation") }); - // install vscode-server and devstar extension console.log(`[RemoteContainer] Getting VSCode commit ID`); - const vscodeCommitId = await utils.getVsCodeCommitId() - - if ("" != vscodeCommitId) { + const vscodeCommitId = await utils.getVsCodeCommitId(); + + if ("" !== vscodeCommitId) { console.log(`[RemoteContainer] VSCode commit ID: ${vscodeCommitId}`); - const vscodeServerUrl = `https://vscode.download.prss.microsoft.com/dbazure/download/stable/${vscodeCommitId}/vscode-server-linux-x64.tar.gz` + const vscodeServerUrl = `https://vscode.download.prss.microsoft.com/dbazure/download/stable/${vscodeCommitId}/vscode-server-linux-x64.tar.gz`; const installVscodeServerScript = ` mkdir -p ~/.vscode-server/bin/${vscodeCommitId} && \\ if [ "$(ls -A ~/.vscode-server/bin/${vscodeCommitId})" ]; then @@ -214,17 +189,17 @@ export default class RemoteContainer { ~/.vscode-server/bin/${vscodeCommitId}/bin/code-server --install-extension mengning.devstar fi `; - + console.log(`[RemoteContainer] Executing installation script`); const installResult = await ssh.execCommand(installVscodeServerScript); - + if (installResult.code === 0) { console.log("[RemoteContainer] VSCode server and extension installed successfully"); console.log("[RemoteContainer] Installation stdout:", installResult.stdout); if (installResult.stderr) { console.warn("[RemoteContainer] Installation stderr:", installResult.stderr); } - + vscode.window.showInformationMessage(vscode.l10n.t('Installation completed!')); } else { console.error("[RemoteContainer] Installation failed with code:", installResult.code); @@ -235,34 +210,26 @@ export default class RemoteContainer { throw new Error('Failed to get VSCode commit ID'); } - // 设置端口映射(如果提供了项目路径) + // 移除端口映射设置,移到 openRemoteFolder 中 + // 只记录项目路径信息,供后续使用 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('端口映射设置失败,但容器连接已建立'); - } + console.log(`[RemoteContainer] Project path recorded for port forwarding: ${projectPath}`); + // 这里可以存储项目路径信息,但不在第一次连接时建立端口映射 } await ssh.dispose(); console.log(`[RemoteContainer] SSH connection disposed`); - // only connect successfully then save the host info console.log(`[RemoteContainer] Storing project SSH info`); - await this.storeProjectSSHInfo(host, hostname, port, 'root') + await this.storeProjectSSHInfo(host, hostname, port, 'root'); - resolve('success') + resolve('success'); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error('[RemoteContainer] Failed to install vscode-server and extension: ', error); try { await ssh.dispose(); } catch (disposeError) { - const disposeErrorMessage = disposeError instanceof Error ? disposeError.message : '未知错误'; console.error('[RemoteContainer] Error disposing SSH connection: ', disposeError); } reject(error); @@ -274,24 +241,30 @@ export default class RemoteContainer { /** * 从 devcontainer.json 中提取端口映射配置 */ - private async getPortsAttributesFromDevContainer(ssh: any, containerPath: string): Promise { + private async getPortsConfigFromDevContainer(ssh: any, containerPath: string): Promise<{ + portsAttributes: any; + forwardPorts?: number[]; + otherPortsAttributes?: any; + }> { 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]; // 取第一个找到的文件 + 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 || {}; + + return { + portsAttributes: devcontainerConfig.portsAttributes || {}, + forwardPorts: devcontainerConfig.forwardPorts, + otherPortsAttributes: devcontainerConfig.otherPortsAttributes + }; } } else { console.log(`[RemoteContainer] No devcontainer.json found in ${containerPath}`); @@ -299,64 +272,67 @@ export default class RemoteContainer { } catch (error) { console.error(`[RemoteContainer] Error reading devcontainer.json:`, error); } - - return {}; + + return { portsAttributes: {} }; } /** - * 建立端口映射 + * 查找可用的本地端口 - 优先使用相同端口 */ - 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(containerPort: number): Promise { + // 首先尝试使用相同的端口号 + if (await this.isPortAvailable(containerPort)) { + console.log(`[RemoteContainer] Using same port: ${containerPort}`); + return containerPort; } + + // 如果相同端口被占用,尝试在附近查找可用端口 + console.log(`[RemoteContainer] Port ${containerPort} is occupied, finding alternative...`); + + // 在 containerPort ± 10 范围内查找 + for (let offset = 1; offset <= 10; offset++) { + // 尝试 containerPort + offset + const port1 = containerPort + offset; + if (port1 < 65536 && await this.isPortAvailable(port1)) { + console.log(`[RemoteContainer] Using alternative port: ${port1}`); + return port1; + } + + // 尝试 containerPort - offset + const port2 = containerPort - offset; + if (port2 > 0 && await this.isPortAvailable(port2)) { + console.log(`[RemoteContainer] Using alternative port: ${port2}`); + return port2; + } + } + + // 如果附近端口都被占用,使用随机端口 + console.log(`[RemoteContainer] No nearby ports available, using random port`); + return this.findRandomAvailablePort(); } /** - * 查找可用的本地端口 + * 检查端口是否可用 */ - private async findAvailableLocalPort(): Promise { + private async isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + server.unref(); + server.on('error', () => { + resolve(false); + }); + server.listen(port, () => { + server.close(() => { + resolve(true); + }); + }); + }); + } + + /** + * 查找随机可用端口 + */ + private async findRandomAvailablePort(): Promise { return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); @@ -370,45 +346,127 @@ export default class RemoteContainer { }); } + /** + * 建立端口映射 - 根据 devcontainer.json 配置 + */ + private async setupPortForwarding(hostname: string, port: number, containerPath: string): Promise { + console.log(`[RemoteContainer] Setting up port forwarding for ${hostname}:${port}`); + + const ssh = new NodeSSH(); + const portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }> = []; + + try { + await ssh.connect({ + host: hostname, + username: 'root', + port: port, + privateKeyPath: this.user.getUserPrivateKeyPath(), + readyTimeout: 30000, + }); + + // 获取完整的端口配置 + const portsConfig = await this.getPortsConfigFromDevContainer(ssh, containerPath); + console.log(`[RemoteContainer] Ports config found:`, portsConfig); + + // 1. 首先处理 forwardPorts(显式指定的端口) + if (portsConfig.forwardPorts && portsConfig.forwardPorts.length > 0) { + for (const containerPort of portsConfig.forwardPorts) { + const localPort = await this.findAvailableLocalPort(containerPort); + await this.createSSHPortForward(hostname, port, containerPort, localPort); + + portMappings.push({ + containerPort, + localPort, + label: `Port ${containerPort}`, + source: 'forwardPorts' + }); + + console.log(`[RemoteContainer] ForwardPorts mapping: localhost:${localPort} -> container:${containerPort}`); + } + } + + // 2. 处理 portsAttributes 配置的端口 + for (const [containerPortStr, attributes] of Object.entries(portsConfig.portsAttributes)) { + const containerPort = parseInt(containerPortStr); + if (!isNaN(containerPort)) { + // 检查是否已经在 forwardPorts 中处理过 + const alreadyMapped = portMappings.some(m => m.containerPort === containerPort); + if (!alreadyMapped) { + const localPort = await this.findAvailableLocalPort(containerPort); + await this.createSSHPortForward(hostname, port, containerPort, localPort); + + const label = (attributes as any).label || `Port ${containerPort}`; + portMappings.push({ + containerPort, + localPort, + label, + source: 'portsAttributes' + }); + + console.log(`[RemoteContainer] PortsAttributes mapping: localhost:${localPort} -> container:${containerPort}`); + } + } + } + + await ssh.dispose(); + + // 存储端口映射信息 + const mappingKey = `${hostname}:${port}`; + this.portMappings.set(mappingKey, portMappings); + + // 显示映射汇总信息 + if (portMappings.length > 0) { + this.showPortMappingsSummary(portMappings); + + // 注册命令以便后续查看 + this.registerPortMappingsCommands(mappingKey, portMappings); + } + + } catch (error) { + console.error(`[RemoteContainer] Error setting up port forwarding:`, error); + await ssh.dispose(); + throw error; + } + } + /** * 创建 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}`, // 转发规则 + '-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(); @@ -416,29 +474,142 @@ export default class RemoteContainer { }); } + /** + * 显示端口映射汇总信息 + */ + private showPortMappingsSummary(portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }>): void { + let message = `🎯 已建立 ${portMappings.length} 个端口映射:\n\n`; + + portMappings.forEach(mapping => { + const samePort = mapping.containerPort === mapping.localPort ? " ✅" : " 🔄"; + message += `• ${mapping.label}\n`; + message += ` 容器端口: ${mapping.containerPort} → 本地端口: ${mapping.localPort}${samePort}\n`; + message += ` 访问地址: http://localhost:${mapping.localPort}\n`; + message += ` 配置来源: ${mapping.source}\n\n`; + }); + + vscode.window.showInformationMessage(message, '复制映射信息', '在浏览器中打开', '查看详细信息') + .then(selection => { + if (selection === '复制映射信息') { + const copyText = portMappings.map(m => + `${m.label}: http://localhost:${m.localPort} (容器端口: ${m.containerPort})` + ).join('\n'); + vscode.env.clipboard.writeText(copyText); + vscode.window.showInformationMessage('端口映射信息已复制到剪贴板'); + } else if (selection === '在浏览器中打开' && portMappings.length > 0) { + // 打开第一个映射的地址 + vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${portMappings[0].localPort}`)); + } else if (selection === '查看详细信息') { + this.showPortMappingsInOutputChannel('current', 0); + } + }); + } + + /** + * 注册端口映射查看命令 + */ + private registerPortMappingsCommands(mappingKey: string, portMappings: Array<{containerPort: number, localPort: number, label: string, source: string}>): void { + // 注册一个全局命令来显示端口映射信息 + const showPortMappingsCommand = `devstar.showPortMappings.${mappingKey.replace(/[^a-zA-Z0-9]/g, '_')}`; + + vscode.commands.registerCommand(showPortMappingsCommand, () => { + this.showPortMappingsSummary(portMappings); + }); + + // 在状态栏显示端口映射信息 + this.createStatusBarItem(mappingKey, portMappings); + } + + /** + * 创建状态栏项目 + */ + private createStatusBarItem(mappingKey: string, portMappings: Array<{containerPort: number, localPort: number, label: string, source: string}>): void { + if (portMappings.length === 0) return; + + const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100); + + // 显示端口数量 + statusBarItem.text = `$(plug) ${portMappings.length} Ports`; + statusBarItem.tooltip = `点击查看 ${portMappings.length} 个端口映射详情`; + + // 添加命令 + statusBarItem.command = `devstar.showPortMappings.${mappingKey.replace(/[^a-zA-Z0-9]/g, '_')}`; + + statusBarItem.show(); + + // 存储状态栏项目以便后续清理 + if (!this.statusBarItems) { + this.statusBarItems = new Map(); + } + this.statusBarItems.set(mappingKey, statusBarItem); + } + + /** + * 获取当前活动的端口映射信息 + */ + public getActivePortMappings(hostname: string, port: number): Array<{containerPort: number, localPort: number, label: string, source: string}> { + const mappingKey = `${hostname}:${port}`; + return this.portMappings.get(mappingKey) || []; + } + + /** + * 显示端口映射面板 + */ + public showPortMappingsPanel(hostname: string, port: number): void { + const mappings = this.getActivePortMappings(hostname, port); + if (mappings.length === 0) { + vscode.window.showInformationMessage('当前没有活动的端口映射'); + return; + } + + this.showPortMappingsSummary(mappings); + } + + /** + * 在输出通道显示详细的端口映射信息 + */ + public showPortMappingsInOutputChannel(hostname: string, port: number): void { + const mappings = this.getActivePortMappings(hostname, port); + if (mappings.length === 0) { + vscode.window.showInformationMessage('当前没有活动的端口映射'); + return; + } + + const outputChannel = vscode.window.createOutputChannel('DevStar Port Mappings'); + outputChannel.show(); + + outputChannel.appendLine(`🎯 端口映射信息 - ${hostname}:${port}`); + outputChannel.appendLine('='.repeat(50)); + + mappings.forEach((mapping, index) => { + outputChannel.appendLine(`${index + 1}. ${mapping.label}`); + outputChannel.appendLine(` 容器端口: ${mapping.containerPort}`); + outputChannel.appendLine(` 本地端口: ${mapping.localPort}`); + outputChannel.appendLine(` 访问地址: http://localhost:${mapping.localPort}`); + outputChannel.appendLine(` 配置来源: ${mapping.source}`); + outputChannel.appendLine(''); + }); + + outputChannel.appendLine('💡 提示: 您可以在浏览器中访问上述本地端口来访问容器中的服务'); + } + /** * 本地环境,保存项目的ssh连接信息 - * @param host - * @param hostname - * @param port - * @param username */ async storeProjectSSHInfo(host: string, hostname: string, port: number, username: string): Promise { console.log(`[RemoteContainer] storeProjectSSHInfo called with:`, { host, hostname, port, username }); - + const sshConfigPath = path.join(os.homedir(), '.ssh', 'config'); console.log(`[RemoteContainer] SSH config path: ${sshConfigPath}`); - - // check if the host and related info exist in local ssh config file before saving + let canAppendSSHConfig = true; if (fs.existsSync(sshConfigPath)) { console.log(`[RemoteContainer] SSH config file exists, checking for existing host`); - + const reader = rd.createInterface(fs.createReadStream(sshConfigPath)); for await (const line of reader) { if (line.includes(`Host ${host}`)) { - // the container ssh info exists console.log(`[RemoteContainer] Host ${host} already exists in SSH config`); canAppendSSHConfig = false; break; @@ -449,18 +620,16 @@ export default class RemoteContainer { } if (canAppendSSHConfig) { - // save the host to the local ssh config file const privateKeyPath = this.user.getUserPrivateKeyPath(); console.log(`[RemoteContainer] Using private key path: ${privateKeyPath}`); - + const newSShConfigContent = `\nHost ${host}\n HostName ${hostname}\n Port ${port}\n User ${username}\n PreferredAuthentications publickey\n IdentityFile ${privateKeyPath}\n `; - + try { fs.writeFileSync(sshConfigPath, newSShConfigContent, { encoding: 'utf8', flag: 'a' }); console.log('[RemoteContainer] Host registered in local ssh config'); } catch (error) { - const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error('[RemoteContainer] Failed to write SSH config:', error); throw error; } @@ -469,36 +638,39 @@ export default class RemoteContainer { /** * local env - * 仅支持已经成功连接,并在ssh config file中存储ssh信息的项目连接。 - * - * @host 表示project name */ 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`); + + // 延迟显示端口映射信息,确保用户能看到 + setTimeout(() => { + this.showPortMappingsPanel(sshConfig.hostname, port); + }, 3000); + } catch (portError) { console.warn(`[RemoteContainer] Port forwarding setup failed:`, portError); - // 端口映射失败不应阻止主要功能 + vscode.window.showWarningMessage('端口映射设置失败,但容器连接已建立'); } } - + // 然后打开远程文件夹 let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`); terminal.show(true); - + const command = `code --remote ssh-remote+root@${host}:${port} ${path} --reuse-window`; console.log(`[RemoteContainer] Sending command to terminal: ${command}`); - - // 在原窗口打开 + terminal.sendText(command); console.log(`[RemoteContainer] Command sent successfully`); + } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`[RemoteContainer] Error in openRemoteFolder:`, error); @@ -511,21 +683,21 @@ export default class RemoteContainer { */ 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; // 默认端口 - + 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) { @@ -535,48 +707,71 @@ export default class RemoteContainer { port = parseInt(trimmed.substring(5).trim()); } } - + if (hostname && currentHost === host) { return { hostname, port }; } } - + return null; } /** - * 清理端口转发(可选) + * 清理端口映射和相关UI */ - public cleanupPortForwarding(): void { - if (!this.sshProcesses) return; + public cleanupPortForwarding(hostname?: string, port?: number): void { + // 清理SSH进程 + if (this.sshProcesses) { + 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(); + } - 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); + // 清理状态栏项目 + if (this.statusBarItems) { + if (hostname && port) { + const mappingKey = `${hostname}:${port}`; + const statusBarItem = this.statusBarItems.get(mappingKey); + if (statusBarItem) { + statusBarItem.dispose(); + this.statusBarItems.delete(mappingKey); + } + } else { + // 清理所有状态栏项目 + for (const [_, statusBarItem] of this.statusBarItems) { + statusBarItem.dispose(); + } + this.statusBarItems.clear(); } } - this.sshProcesses.clear(); + + // 清理端口映射信息 + if (hostname && port) { + const mappingKey = `${hostname}:${port}`; + this.portMappings.delete(mappingKey); + } else { + this.portMappings.clear(); + } } } /** * 打开项目(无须插件登录) - * @param hostname 表示ip - * @param port - * @param username - * @param path */ export async function openProjectWithoutLogging(hostname: string, port: number, username: string, path: string): Promise { console.log(`[RemoteContainer] openProjectWithoutLogging called with:`, { hostname, port, username, path }); - - const command = `code --remote ssh-remote+${username}@${hostname}:${port} ${path} --reuse-window` + + const command = `code --remote ssh-remote+${username}@${hostname}:${port} ${path} --reuse-window`; console.log(`[RemoteContainer] Command: ${command}`); - + try { let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`); terminal.show(true);