|
|
|
@ -1,204 +1,399 @@
|
|
|
|
|
|
|
|
|
|
// 导入 common/fileSystemConfig 模块中的 FileSystemConfig 类型
|
|
|
|
|
import type { FileSystemConfig } from 'common/fileSystemConfig';
|
|
|
|
|
// 导入 common/webviewMessages 模块中的 Navigation 类型
|
|
|
|
|
import type { Navigation } from 'common/webviewMessages';
|
|
|
|
|
// 导入 vscode 模块,这是 VS Code 扩展 API 的入口点
|
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
// 从./config 模块中导入 getConfig、loadConfigs 和 LOADING_CONFIGS 函数或常量
|
|
|
|
|
import { getConfig, loadConfigs, LOADING_CONFIGS } from './config';
|
|
|
|
|
// 从./flags 模块中导入 getFlagBoolean 函数
|
|
|
|
|
import { getFlagBoolean } from './flags';
|
|
|
|
|
// 从./connection 模块中导入 Connection 和 ConnectionManager 类
|
|
|
|
|
import { Connection, ConnectionManager } from './connection';
|
|
|
|
|
// 从./logging 模块中导入 Logging 和 LOGGING_NO_STACKTRACE 常量
|
|
|
|
|
import { Logging, LOGGING_NO_STACKTRACE } from './logging';
|
|
|
|
|
// 从./pseudoTerminal 模块中导入 isSSHPseudoTerminal、replaceVariables 和 replaceVariablesRecursive 函数
|
|
|
|
|
import { isSSHPseudoTerminal, replaceVariables, replaceVariablesRecursive } from './pseudoTerminal';
|
|
|
|
|
// 导入./sshFileSystem 模块中的 SSHFileSystem 类型
|
|
|
|
|
import type { SSHFileSystem } from './sshFileSystem';
|
|
|
|
|
// 从./utils 模块中导入 catchingPromise 和 joinCommands 函数
|
|
|
|
|
import { catchingPromise, joinCommands } from './utils';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将命令参数转换为名称字符串
|
|
|
|
|
* @param arg - 命令参数,可以是字符串、FileSystemConfig 对象或 Connection 对象
|
|
|
|
|
* @returns 转换后的名称字符串
|
|
|
|
|
*/
|
|
|
|
|
function commandArgumentToName(arg?: string | FileSystemConfig | Connection): string {
|
|
|
|
|
// 如果参数不存在,则返回 'undefined'
|
|
|
|
|
if (!arg) return 'undefined';
|
|
|
|
|
// 如果参数是字符串,则直接返回该字符串
|
|
|
|
|
if (typeof arg === 'string') return arg;
|
|
|
|
|
// 如果参数是 Connection 对象,并且包含 'client' 属性,则返回 'Connection(...)' 格式的字符串
|
|
|
|
|
if ('client' in arg) return `Connection(${arg.actualConfig.name})`;
|
|
|
|
|
// 否则,返回 'FileSystemConfig(...)' 格式的字符串
|
|
|
|
|
return `FileSystemConfig(${arg.name})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SSHShellTaskOptions 接口定义了用于执行 SSH 命令的任务选项
|
|
|
|
|
* 它继承自 vscode.TaskDefinition 接口,并添加了一些特定的属性
|
|
|
|
|
*/
|
|
|
|
|
interface SSHShellTaskOptions extends vscode.TaskDefinition {
|
|
|
|
|
//远程主机的名称或 IP 地址
|
|
|
|
|
host: string;
|
|
|
|
|
//要在远程主机上执行的命令
|
|
|
|
|
command: string;
|
|
|
|
|
//命令执行的工作目录,可选
|
|
|
|
|
workingDirectory?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 定义一个接口,继承自 vscode.TerminalLink 并添加了一个可选的 uri 属性
|
|
|
|
|
* 这个接口用于表示终端链接的 URI
|
|
|
|
|
*/
|
|
|
|
|
interface TerminalLinkUri extends vscode.TerminalLink {
|
|
|
|
|
//链接的 URI,可选
|
|
|
|
|
uri?: vscode.Uri;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 定义一个类,实现了 vscode.TaskProvider 接口和 vscode.TerminalLinkProvider 接口
|
|
|
|
|
* 这个类用于管理 SSH 连接和执行命令任务
|
|
|
|
|
*/
|
|
|
|
|
export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider<TerminalLinkUri> {
|
|
|
|
|
//存储所有已创建的 SSHFileSystem 实例的数组
|
|
|
|
|
protected fileSystems: SSHFileSystem[] = [];
|
|
|
|
|
//存储正在创建的 SSHFileSystem 实例的 Promise 的对象
|
|
|
|
|
//这个对象的键是文件系统的名称,值是创建文件系统的 Promise
|
|
|
|
|
protected creatingFileSystems: { [name: string]: Promise<SSHFileSystem> } = {};
|
|
|
|
|
//一个公共的只读属性,用于管理连接的 ConnectionManager 实例
|
|
|
|
|
public readonly connectionManager = new ConnectionManager();
|
|
|
|
|
/**
|
|
|
|
|
* Manager 类负责管理 SSH 连接和文件系统
|
|
|
|
|
* 它监听工作区文件夹的变化,以便在必要时断开或连接 SSH 文件系统
|
|
|
|
|
* @constructor
|
|
|
|
|
* @param {vscode.ExtensionContext} context - 扩展上下文
|
|
|
|
|
*/
|
|
|
|
|
constructor(public readonly context: vscode.ExtensionContext) {
|
|
|
|
|
// In a multi-workspace environment, when the non-main folder gets removed,
|
|
|
|
|
// it might be one of ours, which we should then disconnect if it's
|
|
|
|
|
// the only one left for the given config (name)
|
|
|
|
|
// When one gets added, it gets connected on-demand (using stat() etc)
|
|
|
|
|
// 在多工作区环境中,当非主文件夹被移除时,
|
|
|
|
|
// 它可能是我们的文件夹之一,如果它是给定配置(名称)的唯一文件夹,我们应该断开它的连接
|
|
|
|
|
// 当一个文件夹被添加时,它会按需连接(使用 stat() 等)
|
|
|
|
|
vscode.workspace.onDidChangeWorkspaceFolders((e) => {
|
|
|
|
|
// 从 VSCode 工作区获取当前的 workspaceFolders,如果没有则默认为空数组
|
|
|
|
|
const { workspaceFolders = [] } = vscode.workspace;
|
|
|
|
|
// 遍历所有被移除的文件夹
|
|
|
|
|
e.removed.forEach(async (folder) => {
|
|
|
|
|
// 如果移除的文件夹 URI 方案不是 'ssh',则直接返回,不进行任何处理
|
|
|
|
|
if (folder.uri.scheme !== 'ssh') return;
|
|
|
|
|
// 检查是否存在其他 workspaceFolders 具有相同的 authority(即主机名)
|
|
|
|
|
if (workspaceFolders.find(f => f.uri.authority === folder.uri.authority)) return;
|
|
|
|
|
// 在 fileSystems 数组中查找是否存在与移除的文件夹 authority 匹配的 SSHFileSystem 实例
|
|
|
|
|
const fs = this.fileSystems.find(fs => fs.authority === folder.uri.authority);
|
|
|
|
|
// 如果找到了匹配的 SSHFileSystem 实例,则调用其 disconnect 方法来断开连接
|
|
|
|
|
if (fs) fs.disconnect();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 创建一个新的 SSH 文件系统
|
|
|
|
|
* @param {string} name - 文件系统的名称
|
|
|
|
|
* @param {FileSystemConfig} config - 文件系统的配置
|
|
|
|
|
* @returns {Promise<SSHFileSystem>} - 当文件系统创建完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async createFileSystem(name: string, config?: FileSystemConfig): Promise<SSHFileSystem> {
|
|
|
|
|
await LOADING_CONFIGS; // Prevent race condition on startup, and wait for any current config reload to finish
|
|
|
|
|
await LOADING_CONFIGS; // Prevent race condition on startup, and wait for any current config reload to finish 防止启动时的竞态条件,并等待当前配置重载完成
|
|
|
|
|
// 检查是否已经存在一个具有指定名称的 SSHFileSystem 实例
|
|
|
|
|
const existing = this.fileSystems.find(fs => fs.authority === name);
|
|
|
|
|
// 如果存在,则直接返回该实例
|
|
|
|
|
if (existing) return existing;
|
|
|
|
|
// 声明一个名为 con 的变量,用于存储即将创建的 Connection 实例
|
|
|
|
|
let con: Connection | undefined;
|
|
|
|
|
// 尝试从 creatingFileSystems 中获取已存在的 Promise,如果不存在则创建一个新的 Promise
|
|
|
|
|
return this.creatingFileSystems[name] ||= catchingPromise<SSHFileSystem>(async (resolve, reject) => {
|
|
|
|
|
// 如果没有提供配置,则从默认配置中获取
|
|
|
|
|
config ||= getConfig(name);
|
|
|
|
|
// 如果没有找到配置,则抛出错误
|
|
|
|
|
if (!config) throw new Error(`Couldn't find a configuration with the name '${name}'`);
|
|
|
|
|
// 创建一个新的连接
|
|
|
|
|
const con = await this.connectionManager.createConnection(name, config);
|
|
|
|
|
// 更新连接管理器中的连接信息
|
|
|
|
|
this.connectionManager.update(con, con => con.pendingUserCount++);
|
|
|
|
|
// 获取连接的实际配置
|
|
|
|
|
config = con.actualConfig;
|
|
|
|
|
// 导入 getSFTP 和 SSHFileSystem 模块
|
|
|
|
|
const { getSFTP } = await import('./connect');
|
|
|
|
|
const { SSHFileSystem } = await import('./sshFileSystem');
|
|
|
|
|
// 使用连接的实际配置创建 SFTP 会话
|
|
|
|
|
// Create the actual SFTP session (using the connection's actualConfig, otherwise it'll reprompt for passwords etc)
|
|
|
|
|
const sftp = await getSFTP(con.client, con.actualConfig);
|
|
|
|
|
// 创建一个新的 SSH 文件系统实例
|
|
|
|
|
const fs = new SSHFileSystem(name, sftp, con.actualConfig);
|
|
|
|
|
// 记录日志,表示已创建 SSH 文件系统并正在读取根目录
|
|
|
|
|
Logging.info`Created SSHFileSystem for ${name}, reading root directory...`;
|
|
|
|
|
// 更新连接管理器中的文件系统列表
|
|
|
|
|
this.connectionManager.update(con, con => con.filesystems.push(fs));
|
|
|
|
|
// 将新创建的文件系统添加到 fileSystems 数组中
|
|
|
|
|
this.fileSystems.push(fs);
|
|
|
|
|
// 从 creatingFileSystems 中删除该文件系统的 Promise
|
|
|
|
|
delete this.creatingFileSystems[name];
|
|
|
|
|
// 监听文件系统的关闭事件,当文件系统关闭时,从 fileSystems 数组中删除该文件系统,并更新连接管理器中的文件系统列表
|
|
|
|
|
fs.onClose(() => {
|
|
|
|
|
// 从 fileSystems 中移除已关闭的文件系统
|
|
|
|
|
this.fileSystems = this.fileSystems.filter(f => f !== fs);
|
|
|
|
|
// 更新连接管理器中的文件系统列表
|
|
|
|
|
this.connectionManager.update(con, con => con.filesystems = con.filesystems.filter(f => f !== fs));
|
|
|
|
|
});
|
|
|
|
|
// 刷新文件资源管理器,以显示新创建的文件系统
|
|
|
|
|
vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');
|
|
|
|
|
// con.client.once('close', hadError => !fs.closing && this.promptReconnect(name));
|
|
|
|
|
// 更新连接管理器中的连接信息
|
|
|
|
|
this.connectionManager.update(con, con => con.pendingUserCount--);
|
|
|
|
|
// Sanity check that we can access the home directory
|
|
|
|
|
// 检查是否可以访问主目录
|
|
|
|
|
const [flagCH] = getFlagBoolean('CHECK_HOME', true, config.flags);
|
|
|
|
|
if (flagCH) try {
|
|
|
|
|
// 构建主目录的 URI
|
|
|
|
|
const homeUri = vscode.Uri.parse(`ssh://${name}/${con.home}`);
|
|
|
|
|
// 获取主目录的状态
|
|
|
|
|
const stat = await fs.stat(homeUri);
|
|
|
|
|
// 如果主目录不是一个目录,则抛出错误
|
|
|
|
|
if (!(stat.type & vscode.FileType.Directory)) {
|
|
|
|
|
throw vscode.FileSystemError.FileNotADirectory(homeUri);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 构建错误消息
|
|
|
|
|
let message = `Couldn't read the home directory '${con.home}' on the server for SSH FS '${name}', this might be a sign of bad permissions`;
|
|
|
|
|
// 如果错误是文件系统错误,则更新错误消息
|
|
|
|
|
if (e instanceof vscode.FileSystemError) {
|
|
|
|
|
message = `The home directory '${con.home}' in SSH FS '${name}' is not a directory, this might be a sign of bad permissions`;
|
|
|
|
|
}
|
|
|
|
|
// 记录错误日志
|
|
|
|
|
Logging.error(e);
|
|
|
|
|
// 显示错误消息并提供选项
|
|
|
|
|
const answer = await vscode.window.showWarningMessage(message, 'Stop', 'Ignore');
|
|
|
|
|
// 如果用户选择停止,则拒绝 Promise 并抛出错误
|
|
|
|
|
if (answer === 'Okay') return reject(new Error('User stopped filesystem creation after unaccessible home directory error'));
|
|
|
|
|
}
|
|
|
|
|
// 解决 Promise,返回创建的文件系统实例
|
|
|
|
|
return resolve(fs);
|
|
|
|
|
}).catch((e) => {
|
|
|
|
|
}).catch((e) => { // 捕获 Promise 中的错误
|
|
|
|
|
// 如果存在连接,则更新连接管理器中的连接信息
|
|
|
|
|
if (con) this.connectionManager.update(con, con => con.pendingUserCount--); // I highly doubt resolve(fs) will error
|
|
|
|
|
// 如果错误为空,则删除 creatingFileSystems 中的 Promise 并断开连接
|
|
|
|
|
if (!e) {
|
|
|
|
|
delete this.creatingFileSystems[name];
|
|
|
|
|
this.commandDisconnect(name);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
// 记录错误日志
|
|
|
|
|
Logging.error`Error while connecting to SSH FS ${name}:\n${e}`;
|
|
|
|
|
// 显示错误消息并提供选项
|
|
|
|
|
vscode.window.showErrorMessage(`Error while connecting to SSH FS ${name}:\n${e.message}`, 'Retry', 'Configure', 'Ignore').then((chosen) => {
|
|
|
|
|
// 从 creatingFileSystems 中删除该文件系统的 Promise
|
|
|
|
|
delete this.creatingFileSystems[name];
|
|
|
|
|
// 根据用户的选择执行相应的操作
|
|
|
|
|
if (chosen === 'Retry') {
|
|
|
|
|
// 重试创建文件系统
|
|
|
|
|
this.createFileSystem(name).catch(() => { });
|
|
|
|
|
} else if (chosen === 'Configure') {
|
|
|
|
|
// 配置文件系统
|
|
|
|
|
this.commandConfigure(config || name);
|
|
|
|
|
} else {
|
|
|
|
|
// 断开文件系统的连接
|
|
|
|
|
this.commandDisconnect(name);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 抛出错误
|
|
|
|
|
throw e;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 创建一个新的终端实例
|
|
|
|
|
* @param {string} name - 终端的名称
|
|
|
|
|
* @param {FileSystemConfig | Connection} config - 文件系统的配置或连接对象
|
|
|
|
|
* @param {vscode.Uri} uri - URI 对象,指定终端的工作目录
|
|
|
|
|
* @returns {Promise<void>} - 当终端创建完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async createTerminal(name: string, config?: FileSystemConfig | Connection, uri?: vscode.Uri): Promise<void> {
|
|
|
|
|
const { createTerminal } = await import('./pseudoTerminal');
|
|
|
|
|
// Create connection (early so we have .actualConfig.root)
|
|
|
|
|
// 创建连接(提前创建以便我们有.actualConfig.root)
|
|
|
|
|
const con = (config && 'client' in config) ? config : await this.connectionManager.createConnection(config?.name || name, config);
|
|
|
|
|
// Create pseudo terminal
|
|
|
|
|
// 创建伪终端
|
|
|
|
|
// 更新连接管理器中的连接信息,将 pendingUserCount 加 1,表示有一个用户正在使用该连接
|
|
|
|
|
this.connectionManager.update(con, con => con.pendingUserCount++);
|
|
|
|
|
// 使用提供的连接和工作目录创建一个伪终端实例,并等待其完成
|
|
|
|
|
const pty = await createTerminal({ connection: con, workingDirectory: uri?.path || con.actualConfig.root });
|
|
|
|
|
// 当伪终端关闭时,更新连接管理器中的连接信息,从 terminals 数组中移除已关闭的伪终端
|
|
|
|
|
pty.onDidClose(() => this.connectionManager.update(con, con => con.terminals = con.terminals.filter(t => t !== pty)));
|
|
|
|
|
// 更新连接管理器中的连接信息,将新创建的伪终端添加到 terminals 数组中,并将 pendingUserCount 减 1
|
|
|
|
|
this.connectionManager.update(con, con => (con.terminals.push(pty), con.pendingUserCount--));
|
|
|
|
|
// Create and show the graphical representation
|
|
|
|
|
// 创建并显示图形表示
|
|
|
|
|
// 使用 vscode.window.createTerminal 方法创建一个新的终端实例,并传入一个包含终端名称和伪终端实例的对象
|
|
|
|
|
const terminal = vscode.window.createTerminal({ name, pty });
|
|
|
|
|
// 将新创建的终端实例赋值给伪终端实例的 terminal 属性,以便后续使用
|
|
|
|
|
pty.terminal = terminal;
|
|
|
|
|
// 调用终端实例的 show 方法,显示终端窗口
|
|
|
|
|
terminal.show();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 获取当前活动的文件系统列表
|
|
|
|
|
* @returns {readonly SSHFileSystem[]} - 活动的文件系统列表
|
|
|
|
|
*/
|
|
|
|
|
public getActiveFileSystems(): readonly SSHFileSystem[] {
|
|
|
|
|
return this.fileSystems;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 根据 URI 获取对应的 SSH 文件系统实例
|
|
|
|
|
* @param {vscode.Uri} uri - 要获取文件系统的 URI
|
|
|
|
|
* @returns {SSHFileSystem | null} - 找到的 SSH 文件系统实例,如果没有找到则返回 null
|
|
|
|
|
*/
|
|
|
|
|
public getFs(uri: vscode.Uri): SSHFileSystem | null {
|
|
|
|
|
// 在 fileSystems 数组中查找 authority 与给定 URI 的 authority 相匹配的 SSHFileSystem 实例
|
|
|
|
|
const fs = this.fileSystems.find(f => f.authority === uri.authority);
|
|
|
|
|
// 如果找到了匹配的文件系统实例,则返回该实例
|
|
|
|
|
if (fs) return fs;
|
|
|
|
|
// 如果没有找到匹配的文件系统实例,则返回 null
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 提示用户重新连接指定名称的 SSH 文件系统
|
|
|
|
|
* @param {string} name - 要重新连接的文件系统的名称
|
|
|
|
|
* @returns {Promise<void>} - 当提示完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async promptReconnect(name: string) {
|
|
|
|
|
// 尝试从默认配置中获取指定名称的配置
|
|
|
|
|
const config = getConfig(name);
|
|
|
|
|
// 如果没有找到配置,则直接返回
|
|
|
|
|
if (!config) return;
|
|
|
|
|
// 显示一个警告消息,提示用户 SSH 文件系统已断开连接,并提供忽略和断开连接的选项
|
|
|
|
|
const choice = await vscode.window.showWarningMessage(`SSH FS ${config.label || config.name} disconnected`, 'Ignore', 'Disconnect');
|
|
|
|
|
if (choice === 'Disconnect') this.commandDisconnect(name);
|
|
|
|
|
// 根据用户的选择执行相应的操作
|
|
|
|
|
if (choice === 'Disconnect') this.commandDisconnect(name); // 如果用户选择断开连接,则调用 commandDisconnect 方法断开指定名称的文件系统连接
|
|
|
|
|
}
|
|
|
|
|
/* TaskProvider */
|
|
|
|
|
/**
|
|
|
|
|
* 提供任务列表
|
|
|
|
|
* @param token - 可选的取消令牌
|
|
|
|
|
* @returns 任务数组,如果没有任务则返回空数组
|
|
|
|
|
*/
|
|
|
|
|
public provideTasks(token?: vscode.CancellationToken | undefined): vscode.ProviderResult<vscode.Task[]> {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 解析并执行一个任务
|
|
|
|
|
* @param {vscode.Task} task - 要解析的任务
|
|
|
|
|
* @param {vscode.CancellationToken | undefined} token - 可选的取消令牌
|
|
|
|
|
* @returns {Promise<vscode.Task>} - 解析后的任务
|
|
|
|
|
*/
|
|
|
|
|
public async resolveTask(task: vscode.Task, token?: vscode.CancellationToken | undefined): Promise<vscode.Task> {
|
|
|
|
|
return new vscode.Task(
|
|
|
|
|
task.definition, // Can't replace/modify this, otherwise we're not contributing to "this" task
|
|
|
|
|
// 任务定义,不能替换/修改这个,否则我们就不是在为“这个”任务做贡献
|
|
|
|
|
task.definition, // Can't replace/modify this, otherwise we're not contributing to "this" task 不能替换/修改这个,否则我们就不是在为“这个”任务做贡献
|
|
|
|
|
// 任务作用域为工作区
|
|
|
|
|
vscode.TaskScope.Workspace,
|
|
|
|
|
// 任务名称,格式为“SSH Task '任务名'”
|
|
|
|
|
`SSH Task '${task.name}'`,
|
|
|
|
|
// 任务类型为“ssh”
|
|
|
|
|
'ssh',
|
|
|
|
|
// 自定义执行器,用于执行任务
|
|
|
|
|
new vscode.CustomExecution(async (resolved: SSHShellTaskOptions) => {
|
|
|
|
|
const { createTerminal, createTextTerminal } = await import('./pseudoTerminal');
|
|
|
|
|
try {
|
|
|
|
|
// 检查任务描述中是否缺少“host”字段
|
|
|
|
|
if (!resolved.host) throw new Error('Missing field \'host\' in task description');
|
|
|
|
|
// 检查任务描述中是否缺少“command”字段
|
|
|
|
|
if (!resolved.command) throw new Error('Missing field \'command\' in task description');
|
|
|
|
|
// 根据主机名创建连接
|
|
|
|
|
const connection = await this.connectionManager.createConnection(resolved.host);
|
|
|
|
|
// 递归替换变量
|
|
|
|
|
resolved = await replaceVariablesRecursive(resolved, value => replaceVariables(value, connection.actualConfig));
|
|
|
|
|
let { command, workingDirectory } = resolved;
|
|
|
|
|
// 根据标志位确定是否使用 Windows 命令分隔符
|
|
|
|
|
const [useWinCmdSep] = getFlagBoolean('WINDOWS_COMMAND_SEPARATOR', connection.shellConfig.isWindows, connection.actualConfig.flags);
|
|
|
|
|
// 命令分隔符
|
|
|
|
|
const separator = useWinCmdSep ? ' && ' : '; ';
|
|
|
|
|
let { taskCommand = '$COMMAND' } = connection.actualConfig;
|
|
|
|
|
// 连接命令
|
|
|
|
|
taskCommand = joinCommands(taskCommand, separator)!;
|
|
|
|
|
// 如果命令中包含“$COMMAND”占位符,则替换为实际命令
|
|
|
|
|
if (taskCommand.includes('$COMMAND')) {
|
|
|
|
|
command = taskCommand.replace(/\$COMMAND/g, command);
|
|
|
|
|
} else {
|
|
|
|
|
// 如果命令中缺少“$COMMAND”占位符,则记录错误日志
|
|
|
|
|
const message = `The taskCommand '${taskCommand}' is missing the '$COMMAND' placeholder!`;
|
|
|
|
|
Logging.warning(message, LOGGING_NO_STACKTRACE);
|
|
|
|
|
// 输出错误信息
|
|
|
|
|
command = `echo "Missing '$COMMAND' placeholder"`;
|
|
|
|
|
}
|
|
|
|
|
// 如果存在工作目录,则将其转换为远程路径
|
|
|
|
|
//if (workingDirectory) workingDirectory = this.getRemotePath(config, workingDirectory);
|
|
|
|
|
// 更新连接管理器中的连接信息
|
|
|
|
|
this.connectionManager.update(connection, con => con.pendingUserCount++);
|
|
|
|
|
// 创建终端
|
|
|
|
|
const pty = await createTerminal({ command, workingDirectory, connection });
|
|
|
|
|
// 更新连接管理器中的连接信息
|
|
|
|
|
this.connectionManager.update(connection, con => (con.pendingUserCount--, con.terminals.push(pty)));
|
|
|
|
|
// 监听终端关闭事件
|
|
|
|
|
pty.onDidClose(() => this.connectionManager.update(connection,
|
|
|
|
|
con => con.terminals = con.terminals.filter(t => t !== pty)));
|
|
|
|
|
// 返回终端
|
|
|
|
|
return pty;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 如果发生错误,创建一个文本终端并输出错误信息
|
|
|
|
|
return createTextTerminal(`Error: ${e.message || e}`);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
/* TerminalLinkProvider */
|
|
|
|
|
/**
|
|
|
|
|
* 提供终端链接
|
|
|
|
|
* @param context - 终端链接上下文
|
|
|
|
|
* @param token - 取消令牌
|
|
|
|
|
* @returns 终端链接 URI 数组,如果没有链接则返回 undefined
|
|
|
|
|
*/
|
|
|
|
|
public provideTerminalLinks(context: vscode.TerminalLinkContext, token: vscode.CancellationToken): TerminalLinkUri[] | undefined {
|
|
|
|
|
// 从上下文中提取当前行和终端对象
|
|
|
|
|
const { line, terminal } = context;
|
|
|
|
|
// 从终端对象中获取创建选项
|
|
|
|
|
const { creationOptions } = terminal;
|
|
|
|
|
// 如果创建选项中没有 'pty' 属性,则返回
|
|
|
|
|
if (!('pty' in creationOptions)) return;
|
|
|
|
|
// 从创建选项中获取 'pty' 属性
|
|
|
|
|
const { pty } = creationOptions;
|
|
|
|
|
// 如果 'pty' 不是 SSH 伪终端,则返回
|
|
|
|
|
if (!isSSHPseudoTerminal(pty)) return;
|
|
|
|
|
// 从连接管理器中获取活动连接,并查找包含当前 'pty' 的连接
|
|
|
|
|
const conn = this.connectionManager.getActiveConnections().find(c => c.terminals.includes(pty));
|
|
|
|
|
// 如果没有找到连接,则返回
|
|
|
|
|
if (!conn) return; // Connection died, which means the terminal should also be closed already?
|
|
|
|
|
// 初始化一个数组来存储终端链接 URI
|
|
|
|
|
const links: TerminalLinkUri[] = [];
|
|
|
|
|
// 定义一个正则表达式来匹配路径
|
|
|
|
|
const PATH_REGEX = /\/\S+/g;
|
|
|
|
|
// 循环匹配当前行中的路径
|
|
|
|
|
while (true) {
|
|
|
|
|
// 使用正则表达式执行匹配
|
|
|
|
|
const match = PATH_REGEX.exec(line);
|
|
|
|
|
// 如果没有匹配到,则退出循环
|
|
|
|
|
if (!match) break;
|
|
|
|
|
// 提取匹配到的文件路径
|
|
|
|
|
let [filepath] = match;
|
|
|
|
|
// 如果文件路径以 '~' 开头,则将其替换为连接的主目录
|
|
|
|
|
if (filepath.startsWith('~')) filepath = conn.home + filepath.substring(1);
|
|
|
|
|
// 根据连接的实际配置名称和文件路径创建一个 URI
|
|
|
|
|
const uri = vscode.Uri.parse(`ssh://${conn.actualConfig.name}/${filepath}`);
|
|
|
|
|
// 将链接信息添加到数组中
|
|
|
|
|
links.push({
|
|
|
|
|
uri,
|
|
|
|
|
startIndex: match.index,
|
|
|
|
@ -206,91 +401,174 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider
|
|
|
|
|
tooltip: '[SSH FS] Open file',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// 返回找到的链接
|
|
|
|
|
return links;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 处理终端链接
|
|
|
|
|
* @param link - 终端链接 URI 对象
|
|
|
|
|
* @returns 当处理完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async handleTerminalLink(link: TerminalLinkUri): Promise<void> {
|
|
|
|
|
// 如果链接的 URI 不存在,则直接返回
|
|
|
|
|
if (!link.uri) return;
|
|
|
|
|
// 显示链接指向的文档
|
|
|
|
|
await vscode.window.showTextDocument(link.uri);
|
|
|
|
|
}
|
|
|
|
|
/* Commands (stuff for e.g. context menu for ssh-configs tree) */
|
|
|
|
|
/**
|
|
|
|
|
* 连接到指定的 SSH 文件系统
|
|
|
|
|
* @param {FileSystemConfig} config - 文件系统的配置
|
|
|
|
|
* @returns {Promise<void>} - 当连接完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async commandConnect(config: FileSystemConfig) {
|
|
|
|
|
// 记录接收到的连接命令信息
|
|
|
|
|
Logging.info`Command received to connect ${config.name}`;
|
|
|
|
|
// 获取当前工作区的文件夹
|
|
|
|
|
const folders = vscode.workspace.workspaceFolders!;
|
|
|
|
|
// 在工作区文件夹中查找是否已经存在指定名称的 SSH 文件系统
|
|
|
|
|
const folder = folders && folders.find(f => f.uri.scheme === 'ssh' && f.uri.authority === config.name);
|
|
|
|
|
// 如果已经存在,则刷新文件资源管理器
|
|
|
|
|
if (folder) return vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');
|
|
|
|
|
// 获取文件系统的根目录,默认值为 '/'
|
|
|
|
|
let { root = '/' } = config;
|
|
|
|
|
// 如果根目录以 '~' 开头,则将其替换为连接的主目录
|
|
|
|
|
if (root.startsWith('~')) {
|
|
|
|
|
const con = await this.connectionManager.createConnection(config.name, config);
|
|
|
|
|
root = con.home + root.substring(1);
|
|
|
|
|
}
|
|
|
|
|
// 如果根目录以 '/' 开头,则去除开头的 '/'
|
|
|
|
|
if (root.startsWith('/')) root = root.substring(1);
|
|
|
|
|
// 更新工作区文件夹,添加新的 SSH 文件系统
|
|
|
|
|
vscode.workspace.updateWorkspaceFolders(folders ? folders.length : 0, 0, {
|
|
|
|
|
// 使用 SSH URI 格式
|
|
|
|
|
uri: vscode.Uri.parse(`ssh://${config.name}/${root}`),
|
|
|
|
|
// 文件夹名称,格式为 "SSH FS - 标签或名称"
|
|
|
|
|
name: `SSH FS - ${config.label || config.name}`,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 断开与指定 SSH 文件系统的连接
|
|
|
|
|
* @param target - 要断开连接的文件系统的名称或连接对象
|
|
|
|
|
* @returns {Promise<void>} - 当断开连接完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public commandDisconnect(target: string | Connection) {
|
|
|
|
|
// 记录接收到的断开连接命令信息
|
|
|
|
|
Logging.info`Command received to disconnect ${commandArgumentToName(target)}`;
|
|
|
|
|
// 初始化一个数组来存储要断开连接的连接对象
|
|
|
|
|
let cons: Connection[];
|
|
|
|
|
// 如果目标是一个对象并且包含 'client' 属性,则将其视为一个连接对象
|
|
|
|
|
if (typeof target === 'object' && 'client' in target) {
|
|
|
|
|
// 将目标连接对象添加到数组中
|
|
|
|
|
cons = [target];
|
|
|
|
|
// 获取目标连接对象的实际配置名称
|
|
|
|
|
target = target.actualConfig.name;
|
|
|
|
|
} else {
|
|
|
|
|
// 如果目标是一个字符串,则获取所有活动连接中名称与目标匹配的连接对象
|
|
|
|
|
cons = this.connectionManager.getActiveConnections()
|
|
|
|
|
.filter(con => con.actualConfig.name === target);
|
|
|
|
|
}
|
|
|
|
|
// 遍历所有要断开连接的连接对象,调用连接管理器的 closeConnection 方法来关闭它们
|
|
|
|
|
for (const con of cons) this.connectionManager.closeConnection(con);
|
|
|
|
|
// 获取所有活动连接中名称与目标相同的其他连接对象
|
|
|
|
|
const others = this.connectionManager.getActiveConnections().filter(c => c.actualConfig.name === target);
|
|
|
|
|
// 如果没有其他相同名称的文件系统连接,则直接返回
|
|
|
|
|
if (others && others.some(c => c.filesystems.length)) return;
|
|
|
|
|
// No other filesystems of the same name left anymore, so remove all related workspace folders
|
|
|
|
|
// 如果没有其他相同名称的文件系统连接,则移除所有相关的工作区文件夹
|
|
|
|
|
const folders = vscode.workspace.workspaceFolders || [];
|
|
|
|
|
// 初始化一个变量来存储要移除的工作区文件夹的起始索引
|
|
|
|
|
let start: number = folders.length;
|
|
|
|
|
// 初始化一个数组来存储移除指定文件夹后剩余的工作区文件夹
|
|
|
|
|
let left: vscode.WorkspaceFolder[] = [];
|
|
|
|
|
// 遍历所有工作区文件夹
|
|
|
|
|
for (const folder of folders) {
|
|
|
|
|
// 如果文件夹的 URI 方案是 'ssh' 并且权限与目标名称相同,则更新起始索引
|
|
|
|
|
if (folder.uri.scheme === 'ssh' && folder.uri.authority === target) {
|
|
|
|
|
// 更新 start 变量,使其为当前 folder.index 和之前的 start 值中的较小值
|
|
|
|
|
start = Math.min(folder.index, start);
|
|
|
|
|
} else if (folder.index > start) {
|
|
|
|
|
// 如果文件夹的索引大于起始索引,则将其添加到剩余文件夹数组中
|
|
|
|
|
left.push(folder);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// 如果工作区文件夹的数量没有变化,则直接返回
|
|
|
|
|
if (folders.length === left.length) return;
|
|
|
|
|
// 使用 updateWorkspaceFolders 方法从工作区中移除指定的文件夹
|
|
|
|
|
vscode.workspace.updateWorkspaceFolders(start, folders.length - start, ...left);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 打开一个终端窗口,连接到指定的 SSH 文件系统
|
|
|
|
|
* @param target - 文件系统的配置或连接对象
|
|
|
|
|
* @param uri - 可选的 URI 对象,指定终端的工作目录
|
|
|
|
|
* @returns {Promise<void>} - 当终端创建完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async commandTerminal(target: FileSystemConfig | Connection, uri?: vscode.Uri) {
|
|
|
|
|
// 记录接收到的打开终端命令信息
|
|
|
|
|
Logging.info`Command received to open a terminal for ${commandArgumentToName(target)}${uri ? ` in ${uri}` : ''}`;
|
|
|
|
|
// 从目标对象中获取配置信息
|
|
|
|
|
const config = 'client' in target ? target.actualConfig : target;
|
|
|
|
|
try {
|
|
|
|
|
// 尝试创建一个终端
|
|
|
|
|
await this.createTerminal(config.label || config.name, target, uri);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 如果创建终端时发生错误,记录错误信息
|
|
|
|
|
Logging.error`Error while creating terminal:\n${e}`;
|
|
|
|
|
// 向用户显示错误信息,并提供重试和忽略选项
|
|
|
|
|
const choice = await vscode.window.showErrorMessage<vscode.MessageItem>(
|
|
|
|
|
`Couldn't start a terminal for ${config.name}: ${e.message || e}`,
|
|
|
|
|
{ title: 'Retry' }, { title: 'Ignore', isCloseAffordance: true });
|
|
|
|
|
// 如果用户选择重试,则再次调用 commandTerminal 方法
|
|
|
|
|
if (choice && choice.title === 'Retry') return this.commandTerminal(target, uri);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 配置指定的 SSH 文件系统连接
|
|
|
|
|
* @param target - 要配置的文件系统的名称或配置对象
|
|
|
|
|
* @returns {Promise<void>} - 当配置完成时解析的 Promise
|
|
|
|
|
*/
|
|
|
|
|
public async commandConfigure(target: string | FileSystemConfig) {
|
|
|
|
|
// 记录接收到的配置命令信息
|
|
|
|
|
Logging.info`Command received to configure ${typeof target === 'string' ? target : target.name}`;
|
|
|
|
|
// 如果目标是一个对象
|
|
|
|
|
if (typeof target === 'object') {
|
|
|
|
|
// 检查对象是否有 _location 或 _locations 属性
|
|
|
|
|
if (!target._location && !target._locations.length) {
|
|
|
|
|
// 如果没有,显示错误信息
|
|
|
|
|
vscode.window.showErrorMessage('Cannot configure a config-less connection!');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 打开设置界面,导航到编辑配置页面
|
|
|
|
|
this.openSettings({ config: target, type: 'editconfig' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 如果目标是一个字符串,将其转换为小写
|
|
|
|
|
target = target.toLowerCase();
|
|
|
|
|
// 加载所有配置
|
|
|
|
|
let configs = await loadConfigs();
|
|
|
|
|
// 过滤出名称与目标匹配的配置
|
|
|
|
|
configs = configs.filter(c => c.name === target);
|
|
|
|
|
// 如果没有找到匹配的配置
|
|
|
|
|
if (configs.length === 0) {
|
|
|
|
|
// 显示错误信息
|
|
|
|
|
vscode.window.showErrorMessage(`Found no matching configs for '${target}'`);
|
|
|
|
|
// 记录错误信息
|
|
|
|
|
return Logging.error`Unexpectedly found no matching configs for '${target}' in commandConfigure?`;
|
|
|
|
|
}
|
|
|
|
|
// 如果只有一个匹配的配置,直接使用它
|
|
|
|
|
const config = configs.length === 1 ? configs[0] : configs;
|
|
|
|
|
// 打开设置界面,导航到编辑配置页面
|
|
|
|
|
this.openSettings({ config, type: 'editconfig' });
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 打开设置界面
|
|
|
|
|
* @param navigation - 导航对象,用于指定要导航到的设置页面
|
|
|
|
|
* @returns 一个 Promise,当设置界面打开完成时解决
|
|
|
|
|
*/
|
|
|
|
|
public async openSettings(navigation?: Navigation) {
|
|
|
|
|
// 导入 webview 模块中的 open 和 navigate 函数
|
|
|
|
|
const { open, navigate } = await import('./webview');
|
|
|
|
|
// 如果提供了 navigation 对象,则导航到指定的设置页面,否则直接打开设置界面
|
|
|
|
|
return navigation ? navigate(navigation) : open();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|