|
|
@ -1,14 +1,26 @@
|
|
|
|
|
|
|
|
// 导入 common/fileSystemConfig 模块中的 EnvironmentVariable 和 FileSystemConfig 类型
|
|
|
|
import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemConfig';
|
|
|
|
import type { EnvironmentVariable, FileSystemConfig } from 'common/fileSystemConfig';
|
|
|
|
|
|
|
|
// 导入 path 模块的 posix 版本,用于处理路径
|
|
|
|
import { posix as path } from 'path';
|
|
|
|
import { posix as path } from 'path';
|
|
|
|
|
|
|
|
// 导入 readline 模块,用于处理逐行读取数据
|
|
|
|
import * as readline from 'readline';
|
|
|
|
import * as readline from 'readline';
|
|
|
|
|
|
|
|
// 导入 ssh2 模块中的 Client 和 ClientChannel 类型
|
|
|
|
import type { Client, ClientChannel } from 'ssh2';
|
|
|
|
import type { Client, ClientChannel } from 'ssh2';
|
|
|
|
|
|
|
|
// 导入 vscode 模块,用于 VSCode 插件开发
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
|
|
|
// 导入 config 模块中的 configMatches 和 loadConfigs 函数
|
|
|
|
import { configMatches, loadConfigs } from './config';
|
|
|
|
import { configMatches, loadConfigs } from './config';
|
|
|
|
|
|
|
|
// 导入 flags 模块中的 getFlag 和 getFlagBoolean 函数
|
|
|
|
import { getFlag, getFlagBoolean } from './flags';
|
|
|
|
import { getFlag, getFlagBoolean } from './flags';
|
|
|
|
|
|
|
|
// 导入 logging 模块中的 Logging 和 LOGGING_NO_STACKTRACE 常量
|
|
|
|
import { Logging, LOGGING_NO_STACKTRACE } from './logging';
|
|
|
|
import { Logging, LOGGING_NO_STACKTRACE } from './logging';
|
|
|
|
|
|
|
|
// 导入 pseudoTerminal 模块中的 SSHPseudoTerminal 类型
|
|
|
|
import type { SSHPseudoTerminal } from './pseudoTerminal';
|
|
|
|
import type { SSHPseudoTerminal } from './pseudoTerminal';
|
|
|
|
|
|
|
|
// 导入 shellConfig 模块中的 calculateShellConfig、KNOWN_SHELL_CONFIGS、ShellConfig、tryCommand 和 tryEcho 函数
|
|
|
|
import { calculateShellConfig, KNOWN_SHELL_CONFIGS, ShellConfig, tryCommand, tryEcho } from './shellConfig';
|
|
|
|
import { calculateShellConfig, KNOWN_SHELL_CONFIGS, ShellConfig, tryCommand, tryEcho } from './shellConfig';
|
|
|
|
|
|
|
|
// 导入 sshFileSystem 模块中的 SSHFileSystem 类型
|
|
|
|
import type { SSHFileSystem } from './sshFileSystem';
|
|
|
|
import type { SSHFileSystem } from './sshFileSystem';
|
|
|
|
|
|
|
|
// 导入 utils 模块中的 mergeEnvironment 和 toPromise 函数
|
|
|
|
import { mergeEnvironment, toPromise } from './utils';
|
|
|
|
import { mergeEnvironment, toPromise } from './utils';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
@ -41,95 +53,183 @@ export interface Connection {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class ConnectionManager {
|
|
|
|
export class ConnectionManager {
|
|
|
|
|
|
|
|
// 定义事件发射器,用于在连接添加时触发事件
|
|
|
|
protected onConnectionAddedEmitter = new vscode.EventEmitter<Connection>();
|
|
|
|
protected onConnectionAddedEmitter = new vscode.EventEmitter<Connection>();
|
|
|
|
|
|
|
|
// 定义事件发射器,用于在连接移除时触发事件
|
|
|
|
protected onConnectionRemovedEmitter = new vscode.EventEmitter<Connection>();
|
|
|
|
protected onConnectionRemovedEmitter = new vscode.EventEmitter<Connection>();
|
|
|
|
|
|
|
|
// 定义事件发射器,用于在连接更新时触发事件
|
|
|
|
protected onConnectionUpdatedEmitter = new vscode.EventEmitter<Connection>();
|
|
|
|
protected onConnectionUpdatedEmitter = new vscode.EventEmitter<Connection>();
|
|
|
|
|
|
|
|
// 定义事件发射器,用于在待处理连接状态改变时触发事件
|
|
|
|
protected onPendingChangedEmitter = new vscode.EventEmitter<void>();
|
|
|
|
protected onPendingChangedEmitter = new vscode.EventEmitter<void>();
|
|
|
|
|
|
|
|
// 存储当前已建立的连接
|
|
|
|
protected connections: Connection[] = [];
|
|
|
|
protected connections: Connection[] = [];
|
|
|
|
|
|
|
|
// 存储待处理的连接,每个连接对应一个 Promise 和一个可选的 FileSystemConfig
|
|
|
|
protected pendingConnections: { [name: string]: [Promise<Connection>, FileSystemConfig | undefined] } = {};
|
|
|
|
protected pendingConnections: { [name: string]: [Promise<Connection>, FileSystemConfig | undefined] } = {};
|
|
|
|
/** Fired when a connection got added (and finished connecting) */
|
|
|
|
/** Fired when a connection got added (and finished connecting) */
|
|
|
|
|
|
|
|
// 当有连接被添加(并且完成连接)时触发的事件
|
|
|
|
public readonly onConnectionAdded = this.onConnectionAddedEmitter.event;
|
|
|
|
public readonly onConnectionAdded = this.onConnectionAddedEmitter.event;
|
|
|
|
/** Fired when a connection got removed */
|
|
|
|
/** Fired when a connection got removed */
|
|
|
|
|
|
|
|
// 当有连接被移除时触发的事件
|
|
|
|
public readonly onConnectionRemoved = this.onConnectionRemovedEmitter.event;
|
|
|
|
public readonly onConnectionRemoved = this.onConnectionRemovedEmitter.event;
|
|
|
|
/** Fired when a connection got updated (terminal added/removed, ...) */
|
|
|
|
/** Fired when a connection got updated (terminal added/removed, ...) */
|
|
|
|
|
|
|
|
// 当有连接被更新(终端添加/移除等)时触发的事件
|
|
|
|
public readonly onConnectionUpdated = this.onConnectionUpdatedEmitter.event;
|
|
|
|
public readonly onConnectionUpdated = this.onConnectionUpdatedEmitter.event;
|
|
|
|
/** Fired when a pending connection gets added/removed */
|
|
|
|
/** Fired when a pending connection gets added/removed */
|
|
|
|
|
|
|
|
// 当待处理连接状态改变时触发的事件
|
|
|
|
public readonly onPendingChanged = this.onPendingChangedEmitter.event;
|
|
|
|
public readonly onPendingChanged = this.onPendingChangedEmitter.event;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 获取指定名称的活动连接,如果提供了配置,则根据配置匹配连接
|
|
|
|
|
|
|
|
* @param name 连接的名称
|
|
|
|
|
|
|
|
* @param config 可选的文件系统配置
|
|
|
|
|
|
|
|
* @returns 匹配的连接,如果没有找到则返回 undefined
|
|
|
|
|
|
|
|
*/
|
|
|
|
public getActiveConnection(name: string, config?: FileSystemConfig): Connection | undefined {
|
|
|
|
public getActiveConnection(name: string, config?: FileSystemConfig): Connection | undefined {
|
|
|
|
|
|
|
|
// 如果提供了配置,则使用 configMatches 函数查找匹配的连接
|
|
|
|
if (config) return this.connections.find(con => configMatches(con.config, config));
|
|
|
|
if (config) return this.connections.find(con => configMatches(con.config, config));
|
|
|
|
|
|
|
|
// 将名称转换为小写,以便进行不区分大小写的比较
|
|
|
|
name = name.toLowerCase();
|
|
|
|
name = name.toLowerCase();
|
|
|
|
|
|
|
|
// 在连接列表中查找名称匹配的连接
|
|
|
|
return this.connections.find(con => con.config.name === name);
|
|
|
|
return this.connections.find(con => con.config.name === name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 获取所有活动连接
|
|
|
|
|
|
|
|
* @returns 活动连接的数组
|
|
|
|
|
|
|
|
*/
|
|
|
|
public getActiveConnections(): Connection[] {
|
|
|
|
public getActiveConnections(): Connection[] {
|
|
|
|
|
|
|
|
// 使用扩展运算符将 this.connections 数组展开并复制到一个新数组中,以避免直接返回原数组的引用
|
|
|
|
return [...this.connections];
|
|
|
|
return [...this.connections];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 获取所有待处理连接
|
|
|
|
|
|
|
|
* @returns 待处理连接的名称和配置的数组
|
|
|
|
|
|
|
|
*/
|
|
|
|
public getPendingConnections(): [string, FileSystemConfig | undefined][] {
|
|
|
|
public getPendingConnections(): [string, FileSystemConfig | undefined][] {
|
|
|
|
|
|
|
|
// 使用 Object.keys 方法获取 this.pendingConnections 对象的所有键,即待处理连接的名称
|
|
|
|
return Object.keys(this.pendingConnections).map(name => [name, this.pendingConnections[name][1]]);
|
|
|
|
return Object.keys(this.pendingConnections).map(name => [name, this.pendingConnections[name][1]]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 创建一个命令终端,用于执行远程命令
|
|
|
|
|
|
|
|
* @param client SSH 客户端
|
|
|
|
|
|
|
|
* @param shellConfig shell 配置
|
|
|
|
|
|
|
|
* @param authority 权限
|
|
|
|
|
|
|
|
* @param debugLogging 是否启用调试日志
|
|
|
|
|
|
|
|
* @returns 命令终端的路径
|
|
|
|
|
|
|
|
*/
|
|
|
|
protected async _createCommandTerminal(client: Client, shellConfig: ShellConfig, authority: string, debugLogging: boolean): Promise<string> {
|
|
|
|
protected async _createCommandTerminal(client: Client, shellConfig: ShellConfig, authority: string, debugLogging: boolean): Promise<string> {
|
|
|
|
|
|
|
|
// 使用 authority 参数创建一个日志记录器,用于记录命令终端的相关信息
|
|
|
|
const logging = Logging.scope(`CmdTerm(${authority})`);
|
|
|
|
const logging = Logging.scope(`CmdTerm(${authority})`);
|
|
|
|
|
|
|
|
// 检查 shellConfig 对象是否支持嵌入替换,如果不支持则抛出错误
|
|
|
|
if (!shellConfig.embedSubstitutions) throw new Error(`Shell '${shellConfig.shell}' does not support embedding substitutions`);
|
|
|
|
if (!shellConfig.embedSubstitutions) throw new Error(`Shell '${shellConfig.shell}' does not support embedding substitutions`);
|
|
|
|
|
|
|
|
// 使用 client.shell 方法创建一个 shell 通道,并等待其完成
|
|
|
|
const shell = await toPromise<ClientChannel>(cb => client.shell({}, cb));
|
|
|
|
const shell = await toPromise<ClientChannel>(cb => client.shell({}, cb));
|
|
|
|
|
|
|
|
// 记录调试信息,显示即将发送的 TTY 命令
|
|
|
|
logging.debug(`TTY COMMAND: ${`echo ${shellConfig.embedSubstitutions`::sshfs:${'echo TTY'}:${'tty'}`}\n`}`);
|
|
|
|
logging.debug(`TTY COMMAND: ${`echo ${shellConfig.embedSubstitutions`::sshfs:${'echo TTY'}:${'tty'}`}\n`}`);
|
|
|
|
|
|
|
|
// 将 TTY 命令发送到 shell 通道中
|
|
|
|
shell.write(`echo ${shellConfig.embedSubstitutions`::sshfs:${'echo TTY'}:${'tty'}`}\n`);
|
|
|
|
shell.write(`echo ${shellConfig.embedSubstitutions`::sshfs:${'echo TTY'}:${'tty'}`}\n`);
|
|
|
|
|
|
|
|
// 创建一个 Promise,用于处理命令终端返回的路径
|
|
|
|
return new Promise((resolvePath, rejectPath) => {
|
|
|
|
return new Promise((resolvePath, rejectPath) => {
|
|
|
|
|
|
|
|
// 设置一个超时时间,如果在 10 秒内没有获取到命令路径,则抛出错误
|
|
|
|
setTimeout(() => rejectPath(new Error('Timeout fetching command path')), 10e3);
|
|
|
|
setTimeout(() => rejectPath(new Error('Timeout fetching command path')), 10e3);
|
|
|
|
|
|
|
|
// 使用 readline 模块创建一个接口,用于读取命令终端的输出
|
|
|
|
const rl = readline.createInterface(shell);
|
|
|
|
const rl = readline.createInterface(shell);
|
|
|
|
|
|
|
|
// 监听 shell 的 error 事件,如果发生错误,则拒绝 Promise
|
|
|
|
shell.once('error', rejectPath);
|
|
|
|
shell.once('error', rejectPath);
|
|
|
|
|
|
|
|
// 监听 shell 的 close 事件,如果 shell 关闭,则拒绝 Promise
|
|
|
|
shell.once('close', () => rejectPath());
|
|
|
|
shell.once('close', () => rejectPath());
|
|
|
|
|
|
|
|
// 监听 readline 接口的 'line' 事件,当接收到一行数据时,执行回调函数
|
|
|
|
rl.on('line', async line => {
|
|
|
|
rl.on('line', async line => {
|
|
|
|
|
|
|
|
// 如果启用了调试日志,则记录接收到的行数据
|
|
|
|
if (debugLogging) logging.debug('<< ' + line);
|
|
|
|
if (debugLogging) logging.debug('<< ' + line);
|
|
|
|
|
|
|
|
// 使用正则表达式匹配接收到的行数据,提取命令和参数
|
|
|
|
const [, prefix, cmd, args] = line.match(/(.*?)::sshfs:(\w+):(.*)$/) || [];
|
|
|
|
const [, prefix, cmd, args] = line.match(/(.*?)::sshfs:(\w+):(.*)$/) || [];
|
|
|
|
|
|
|
|
// 如果没有匹配到命令或前缀以 echo 结尾,则忽略该行数据
|
|
|
|
if (!cmd || prefix.endsWith('echo ')) return;
|
|
|
|
if (!cmd || prefix.endsWith('echo ')) return;
|
|
|
|
|
|
|
|
// 根据命令类型进行不同的处理
|
|
|
|
switch (cmd) {
|
|
|
|
switch (cmd) {
|
|
|
|
|
|
|
|
// 如果接收到的命令是 TTY,则记录日志并解析出 TTY 路径
|
|
|
|
case 'TTY':
|
|
|
|
case 'TTY':
|
|
|
|
|
|
|
|
// 记录接收到 TTY 路径的信息
|
|
|
|
logging.info('Got TTY path: ' + args);
|
|
|
|
logging.info('Got TTY path: ' + args);
|
|
|
|
|
|
|
|
// 解析出的 TTY 路径作为参数,调用 resolvePath 方法来解决 Promise
|
|
|
|
resolvePath(args);
|
|
|
|
resolvePath(args);
|
|
|
|
|
|
|
|
// 跳出 switch 语句
|
|
|
|
break;
|
|
|
|
break;
|
|
|
|
|
|
|
|
// 如果接收到的命令是 code,则解析命令参数并执行相应操作
|
|
|
|
case 'code':
|
|
|
|
case 'code':
|
|
|
|
|
|
|
|
// 使用 ::: 分割 args,获取 pwd 和 target
|
|
|
|
let [pwd, target] = args.split(':::');
|
|
|
|
let [pwd, target] = args.split(':::');
|
|
|
|
|
|
|
|
// 如果 pwd 或 target 为空,则记录错误日志并返回
|
|
|
|
if (!pwd || !target) {
|
|
|
|
if (!pwd || !target) {
|
|
|
|
logging.error`Malformed 'code' command args: ${args}`;
|
|
|
|
logging.error`Malformed 'code' command args: ${args}`;
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 去除 pwd 和 target 前后的空格
|
|
|
|
pwd = pwd.trim();
|
|
|
|
pwd = pwd.trim();
|
|
|
|
target = target.trim();
|
|
|
|
target = target.trim();
|
|
|
|
|
|
|
|
// 记录接收到的打开文件命令的信息
|
|
|
|
logging.info`Received command to open '${target}' while in '${pwd}'`;
|
|
|
|
logging.info`Received command to open '${target}' while in '${pwd}'`;
|
|
|
|
|
|
|
|
// 如果 target 以 / 开头,则 absolutePath 为 target,否则为 pwd 和 target 的拼接
|
|
|
|
const absolutePath = target.startsWith('/') ? target : path.join(pwd, target);
|
|
|
|
const absolutePath = target.startsWith('/') ? target : path.join(pwd, target);
|
|
|
|
|
|
|
|
// 使用 authority 和 absolutePath 构建 URI
|
|
|
|
const uri = vscode.Uri.parse(`ssh://${authority}/${absolutePath}`);
|
|
|
|
const uri = vscode.Uri.parse(`ssh://${authority}/${absolutePath}`);
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 获取 URI 的状态
|
|
|
|
const stat = await vscode.workspace.fs.stat(uri);
|
|
|
|
const stat = await vscode.workspace.fs.stat(uri);
|
|
|
|
|
|
|
|
// 如果是目录,则更新工作区文件夹
|
|
|
|
if (stat.type & vscode.FileType.Directory) {
|
|
|
|
if (stat.type & vscode.FileType.Directory) {
|
|
|
|
await vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, 0, { uri });
|
|
|
|
await vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, 0, { uri });
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 如果是文件,则显示文件
|
|
|
|
await vscode.window.showTextDocument(uri);
|
|
|
|
await vscode.window.showTextDocument(uri);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
// 如果是文件系统错误
|
|
|
|
if (e instanceof vscode.FileSystemError) {
|
|
|
|
if (e instanceof vscode.FileSystemError) {
|
|
|
|
|
|
|
|
// 如果是文件未找到错误
|
|
|
|
if (e.code === 'FileNotFound') {
|
|
|
|
if (e.code === 'FileNotFound') {
|
|
|
|
|
|
|
|
// 记录文件未找到的警告日志
|
|
|
|
logging.warning(`File '${absolutePath}' not found, prompting to create empty file`);
|
|
|
|
logging.warning(`File '${absolutePath}' not found, prompting to create empty file`);
|
|
|
|
|
|
|
|
// 显示文件未找到的警告消息,并提供创建文件的选项
|
|
|
|
const choice = await vscode.window.showWarningMessage(`File '${absolutePath}' not found, create it?`, { modal: true }, 'Yes');
|
|
|
|
const choice = await vscode.window.showWarningMessage(`File '${absolutePath}' not found, create it?`, { modal: true }, 'Yes');
|
|
|
|
|
|
|
|
// 如果用户选择不创建文件,则返回
|
|
|
|
if (choice !== 'Yes') return;
|
|
|
|
if (choice !== 'Yes') return;
|
|
|
|
try { await vscode.workspace.fs.writeFile(uri, Buffer.of()); } catch (e) {
|
|
|
|
try {
|
|
|
|
|
|
|
|
// 尝试创建空文件
|
|
|
|
|
|
|
|
await vscode.workspace.fs.writeFile(uri, Buffer.of());
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
// 记录创建文件失败的错误日志
|
|
|
|
logging.error(e);
|
|
|
|
logging.error(e);
|
|
|
|
|
|
|
|
// 显示创建文件失败的错误消息
|
|
|
|
vscode.window.showErrorMessage(`Failed to create an empty file at '${absolutePath}'`);
|
|
|
|
vscode.window.showErrorMessage(`Failed to create an empty file at '${absolutePath}'`);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 显示创建的文件
|
|
|
|
await vscode.window.showTextDocument(uri);
|
|
|
|
await vscode.window.showTextDocument(uri);
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 显示其他文件系统错误
|
|
|
|
vscode.window.showErrorMessage(`Error opening ${absolutePath}: ${e.name.replace(/ \(FileSystemError\)/g, '')}`);
|
|
|
|
vscode.window.showErrorMessage(`Error opening ${absolutePath}: ${e.name.replace(/ \(FileSystemError\)/g, '')}`);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// 显示其他错误
|
|
|
|
vscode.window.showErrorMessage(`Error opening ${absolutePath}: ${e.message || e}`);
|
|
|
|
vscode.window.showErrorMessage(`Error opening ${absolutePath}: ${e.message || e}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
|
|
|
|
// 如果接收到的命令不是 TTY 或 code,则记录错误日志
|
|
|
|
default:
|
|
|
|
default:
|
|
|
|
|
|
|
|
// 记录接收到未知命令的错误信息
|
|
|
|
logging.error`Unrecognized command ${cmd} with args: ${args}`;
|
|
|
|
logging.error`Unrecognized command ${cmd} with args: ${args}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 创建一个新的连接
|
|
|
|
|
|
|
|
* @param name 连接的名称
|
|
|
|
|
|
|
|
* @param config 可选的文件系统配置
|
|
|
|
|
|
|
|
* @returns 一个 Promise,解析为新创建的连接对象
|
|
|
|
|
|
|
|
*/
|
|
|
|
protected async _createConnection(name: string, config?: FileSystemConfig): Promise<Connection> {
|
|
|
|
protected async _createConnection(name: string, config?: FileSystemConfig): Promise<Connection> {
|
|
|
|
const logging = Logging.scope(`createConnection(${name},${config ? 'config' : 'undefined'})`);
|
|
|
|
const logging = Logging.scope(`createConnection(${name},${config ? 'config' : 'undefined'})`);
|
|
|
|
logging.info`Creating a new connection for '${name}'`;
|
|
|
|
logging.info`Creating a new connection for '${name}'`;
|
|
|
|