Files
devstar_plugin/src/remote-container.ts

590 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<string, any>;
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<string> {
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<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]; // 取第一个找到的文件
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<void> {
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<number> {
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<void> {
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<void> {
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<void> {
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<void> {
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}`);
}
}