// 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') import * as utils from './utils'; import User from './user'; import DevstarAPIHandler from './devstar-api'; export default class RemoteContainer { private user: User; 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 { await this.firstConnect(host, hostname, username, port) .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 * @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 }); 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'); } 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); } }); }); } /** * 本地环境,保存项目的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 */ openRemoteFolder(host: string, port: number, username: string, path: string): void { console.log(`[RemoteContainer] openRemoteFolder called with:`, { host, port, username, path }); try { let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`); terminal.show(true); const command = `code --remote ssh-remote+${username}@${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}`); } } } /** * 打开项目(无须插件登录) * @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}`); } }