refactor🎨: (阅读代码):补充manager.ts的说明

master
yetao 3 weeks ago
parent b1f50318a8
commit c3737330b7

@ -172,6 +172,41 @@ export function activate(context: vscode.ExtensionContext) {
* @param handler - * @param handler -
* @returns * @returns
*/ */
/**
@startuml
start
:;
if (handler.promptOptions && (!arg || typeof arg === 'string')) then (yes)
:使 pickComplex arg;
endif
if (typeof arg === 'string') then (yes)
: handleString ;
stop
endif
if (!arg) then (yes)
:;
stop
endif
if (arg instanceof vscode.Uri) then (yes)
: handleUri ;
stop
endif
if ('handleInput' in arg) then (yes)
: handleTerminal ;
stop
endif
if ('client' in arg) then (yes)
: handleConnection ;
stop
endif
if ('name' in arg) then (yes)
: handleConfig ;
stop
endif
:;
stop
@enduml
*/
function registerCommandHandler(name: string, handler: CommandHandler) { function registerCommandHandler(name: string, handler: CommandHandler) {
/** /**
* *

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

Loading…
Cancel
Save