// remote-container.ts 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') const { spawn } = require('child_process'); const net = require('net'); import * as utils from './utils'; import User from './user'; import DevstarAPIHandler from './devstar-api'; export default class RemoteContainer { private user: User; private sshProcesses?: Map; constructor(user: User) { this.user = user } public setUser(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) 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}"`; } } console.log(`[RemoteContainer] Sending command to terminal: ${command}`); terminal.sendText(command); } else { console.error(`[RemoteContainer] Failed to create or access terminal`); vscode.window.showErrorMessage('无法创建终端,请检查终端是否可用。'); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`[RemoteContainer] Error in remote environment:`, error); vscode.window.showErrorMessage(`远程环境操作失败: ${errorMessage}`); } } 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}`); vscode.window.showErrorMessage('首次连接容器失败,请检查网络和容器状态。'); } }) .catch(error => { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`[RemoteContainer] firstConnect failed:`, error); vscode.window.showErrorMessage(`首次连接容器时发生错误: ${errorMessage}`); }); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`[RemoteContainer] Error in local environment firstOpenProject:`, error); vscode.window.showErrorMessage(`打开项目失败: ${errorMessage}`); } } } /** * 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"), cancellable: false }, 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() // 上传公钥 console.log(`[RemoteContainer] Uploading public key`); const devstarAPIHandler = new DevstarAPIHandler() const uploadResult = await devstarAPIHandler.uploadUserPublicKey(this.user) if (uploadResult !== "ok") { throw new Error('Upload public key failed.') } console.log(`[RemoteContainer] Public key uploaded successfully`); } else { console.log(`[RemoteContainer] SSH keys already exist`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; 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秒 onKeyboardInteractive: ( _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) { 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 installVscodeServerScript = ` mkdir -p ~/.vscode-server/bin/${vscodeCommitId} && \\ if [ "$(ls -A ~/.vscode-server/bin/${vscodeCommitId})" ]; then echo "VSCode server already exists, installing extension only" ~/.vscode-server/bin/${vscodeCommitId}/bin/code-server --install-extension mengning.devstar else echo "Downloading and installing VSCode server" wget ${vscodeServerUrl} -O vscode-server-linux-x64.tar.gz && \\ mv vscode-server-linux-x64.tar.gz ~/.vscode-server/bin/${vscodeCommitId} && \\ cd ~/.vscode-server/bin/${vscodeCommitId} && \\ tar -xvzf vscode-server-linux-x64.tar.gz --strip-components 1 && \\ rm vscode-server-linux-x64.tar.gz && \\ ~/.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); console.error("[RemoteContainer] Installation stderr:", installResult.stderr); throw new Error(`Installation failed with exit code ${installResult.code}: ${installResult.stderr}`); } } else { 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`); // only connect successfully then save the host info console.log(`[RemoteContainer] Storing project SSH info`); await this.storeProjectSSHInfo(host, hostname, port, 'root') 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); } }); }); } /** * 从 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 * @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; } } } else { console.log(`[RemoteContainer] SSH config file does not exist, will create it`); } 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; } } } /** * 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`); } catch (portError) { console.warn(`[RemoteContainer] Port forwarding setup failed:`, portError); // 端口映射失败不应阻止主要功能 } } // 然后打开远程文件夹 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); 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(); } } /** * 打开项目(无须插件登录) * @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` console.log(`[RemoteContainer] Command: ${command}`); try { let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`); terminal.show(true); terminal.sendText(command); console.log(`[RemoteContainer] openProjectWithoutLogging completed successfully`); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; console.error(`[RemoteContainer] Error in openProjectWithoutLogging:`, error); vscode.window.showErrorMessage(`无登录打开项目失败: ${errorMessage}`); } }