@ -1,92 +1,207 @@
// 导入 EnvironmentVariable 和 FileSystemConfig 类型,用于定义环境变量和文件系统配置
import type { EnvironmentVariable , FileSystemConfig } from 'common/fileSystemConfig' ;
// 导入 path 模块,用于处理路径
import * as path from 'path' ;
// 导入 ClientChannel 和 PseudoTtyOptions 类型,用于定义客户端通道和伪终端选项
import type { ClientChannel , PseudoTtyOptions } from 'ssh2' ;
// 导入 vscode 模块,用于访问 VS Code 的 API
import * as vscode from 'vscode' ;
// 导入 getFlagBoolean 函数,用于获取布尔类型的标志
import { getFlagBoolean } from './flags' ;
// 导入 Connection 类型,用于定义连接
import type { Connection } from './connection' ;
// 导入 Logging 和 LOGGING_NO_STACKTRACE 常量,用于日志记录
import { Logging , LOGGING_NO_STACKTRACE } from './logging' ;
// 导入 environmentToExportString、joinCommands、mergeEnvironment 和 toPromise 函数,用于处理环境变量、命令拼接、环境合并和 Promise 转换
import { environmentToExportString , joinCommands , mergeEnvironment , toPromise } from './utils' ;
// 定义伪终端的高度和宽度
const [ HEIGHT , WIDTH ] = [ 480 , 640 ] ;
// 定义伪终端的选项对象
const PSEUDO_TTY_OPTIONS : Partial < PseudoTtyOptions > = {
height : HEIGHT , width : WIDTH , term : 'xterm-256color' ,
// 伪终端的高度
height : HEIGHT ,
// 伪终端的宽度
width : WIDTH ,
// 伪终端的类型
term : 'xterm-256color' ,
} ;
/ * *
* 定 义 一 个 SSH 伪 终 端 接 口 , 继 承 自 vscode . Pseudoterminal 接 口
* 该 接 口 扩 展 了 一 些 额 外 的 属 性 和 方 法 , 用 于 处 理 SSH 连 接 相 关 的 操 作
* /
export interface SSHPseudoTerminal extends vscode . Pseudoterminal {
onDidClose : vscode.Event < number > ; // Redeclaring that it isn't undefined
/ * *
* 当 终 端 关 闭 时 触 发 的 事 件
* 该 事 件 会 返 回 一 个 数 字 , 表 示 终 端 关 闭 的 原 因
* /
onDidClose : vscode.Event < number > ; // 重新声明它不是未定义的
/ * *
* 当 终 端 打 开 时 触 发 的 事 件
* 该 事 件 不 会 返 回 任 何 值
* /
onDidOpen : vscode.Event < void > ;
handleInput ( data : string ) : void ; // We don't support/need read-only terminals for now
/ * *
* 处 理 输 入 数 据 的 方 法
* 该 方 法 会 接 收 一 个 字 符 串 , 表 示 用 户 输 入 的 数 据
* 我 们 目 前 不 支 持 只 读 终 端
* /
handleInput ( data : string ) : void ; // 我们现在不支持/不需要只读终端
/ * *
* 终 端 的 状 态
* 可 能 的 值 有 : 'opening' ( 正 在 打 开 ) 、 'open' ( 已 打 开 ) 、 'closed' ( 已 关 闭 ) 、 'wait-to-close' ( 等 待 关 闭 )
* /
status : 'opening' | 'open' | 'closed' | 'wait-to-close' ;
/ * *
* 与 终 端 关 联 的 连 接 对 象
* 该 对 象 包 含 了 与 SSH 服 务 器 建 立 连 接 所 需 的 信 息
* /
connection : Connection ;
/** Could be undefined if it only gets created during psy.open() instead of beforehand */
/ * *
* 可 选 的 客 户 端 通 道 对 象
* 如 果 在 调 用 createTerminal 之 前 没 有 创 建 , 则 该 属 性 可 能 为 undefined
* /
channel? : ClientChannel ;
/** Either set by the code calling createTerminal, otherwise "calculated" and hopefully found */
/ * *
* 可 选 的 vscode . Terminal 对 象
* 该 对 象 可 能 是 在 调 用 createTerminal 时 设 置 的 , 否 则 会 根 据 需 要 “ 计 算 ” 并 找 到
* /
terminal? : vscode.Terminal ;
}
/ * *
* 检 查 给 定 的 终 端 对 象 是 否 是 SSHPseudoTerminal 类 型 。
*
* @param terminal - 要 检 查 的 终 端 对 象 。
* @returns 如 果 终 端 对 象 是 SSHPseudoTerminal 类 型 , 则 返 回 true , 否 则 返 回 false 。
* /
export function isSSHPseudoTerminal ( terminal : vscode.Pseudoterminal ) : terminal is SSHPseudoTerminal {
// 将终端对象强制转换为 SSHPseudoTerminal 类型
const term = terminal as SSHPseudoTerminal ;
// 检查转换后的对象是否具有 connection、status 和 handleInput 属性
return ! ! ( term . connection && term . status && term . handleInput ) ;
}
/ * *
* 定 义 了 创 建 终 端 时 可 以 配 置 的 选 项 。
* /
export interface TerminalOptions {
/ * *
* 与 终 端 关 联 的 连 接 对 象 。
* 该 对 象 包 含 了 与 SSH 服 务 器 建 立 连 接 所 需 的 信 息 。
* /
connection : Connection ;
/ * *
* 环 境 变 量 数 组 。
* 这 些 变 量 将 被 设 置 在 远 程 终 端 的 环 境 中 。
* /
environment? : EnvironmentVariable [ ] ;
/** If absent, this defaults to config.root if present, otherwise whatever the remote shell picks as default */
/ * *
* 工 作 目 录 。
* 如 果 不 存 在 , 默 认 值 为 配 置 中 的 根 目 录 ( 如 果 存 在 ) , 否 则 由 远 程 shell 选 择 默 认 目 录 。
* /
workingDirectory? : string ;
/** The command to run in the remote shell. If undefined, a (regular interactive) shell is started instead by running $SHELL*/
/ * *
* 要 在 远 程 shell 中 运 行 的 命 令 。
* 如 果 未 定 义 , 则 通 过 运 行 $SHELL 启 动 一 个 ( 常 规 交 互 式 ) shell 。
* /
command? : string ;
}
/ * *
* 替 换 字 符 串 中 的 变 量 。
*
* @param value - 要 替 换 变 量 的 字 符 串 。
* @param config - 文 件 系 统 配 置 对 象 。
* @returns 替 换 变 量 后 的 字 符 串 。
* /
export function replaceVariables ( value : string , config : FileSystemConfig ) : string {
return value . replace ( /\$\{(.*?)\}/g , ( str , match : string ) = > {
// 如果变量不是以 "remote" 开头,则直接返回原字符串
if ( ! match . startsWith ( 'remote' ) ) return str ; // Our variables always start with "remote"
// https://github.com/microsoft/vscode/blob/bebd06640734c37f6d5f1a82b13297ce1d297dd1/src/vs/workbench/services/configurationResolver/common/variableResolver.ts#L156
// 从变量中提取键和参数
const [ key , argument ] = match . split ( ':' ) as [ string , string ? ] ;
// 获取当前文件的 URI
const getFilePath = ( ) : vscode . Uri = > {
// 获取当前活动编辑器的文档 URI
const uri = vscode . window . activeTextEditor ? . document ? . uri ;
// 如果 URI 存在且协议为 ssh, 则返回该 URI
if ( uri && uri . scheme === 'ssh' ) return uri ;
// 如果 URI 存在但协议不是 ssh, 则抛出错误
if ( uri ) throw new Error ( ` Variable ${ str } : Active editor is not a ssh:// file ` ) ;
// 如果 URI 不存在,则抛出错误
throw new Error ( ` Variable ${ str } can not be resolved. Please open an editor. ` ) ;
}
// 获取当前文件所在的工作区文件夹的 URI
const getFolderPathForFile = ( ) : vscode . Uri = > {
// 获取当前文件的 URI
const filePath = getFilePath ( ) ;
// 从工作区中获取包含该文件的文件夹的 URI
const uri = vscode . workspace . getWorkspaceFolder ( filePath ) ? . uri ;
// 如果找到了文件夹,则返回其 URI
if ( uri ) return uri ;
// 如果没有找到文件夹,则抛出错误
throw new Error ( ` Variable ${ str } : can not find workspace folder of ' ${ filePath } '. ` ) ;
}
// 获取工作区中的所有文件夹
const { workspaceFolders = [ ] } = vscode . workspace ;
// 过滤出所有以 "ssh" 开头的文件夹
const sshFolders = workspaceFolders . filter ( ws = > ws . uri . scheme === 'ssh' ) ;
// 如果只有一个 "ssh" 文件夹,则返回该文件夹,否则返回 undefined
const sshFolder = sshFolders . length === 1 ? sshFolders [ 0 ] : undefined ;
// 获取指定参数的文件夹 URI
const getFolderUri = ( ) : vscode . Uri = > {
// 获取当前工作区的所有文件夹,默认值为空数组
const { workspaceFolders = [ ] } = vscode . workspace ;
// 如果参数存在
if ( argument ) {
// 在工作区文件夹中查找名称与参数匹配的文件夹
const uri = workspaceFolders . find ( ws = > ws . name === argument ) ? . uri ;
// 如果找到了匹配的文件夹,并且其 URI 协议为 ssh, 则返回该 URI
if ( uri && uri . scheme === 'ssh' ) return uri ;
// 如果找到了匹配的文件夹,但 URI 协议不是 ssh, 则抛出错误
if ( uri ) throw new Error ( ` Variable ${ str } : Workspace folder ' ${ argument } ' is not a ssh:// folder ` ) ;
// 如果没有找到匹配的文件夹,则抛出错误
throw new Error ( ` Variable ${ str } can not be resolved. No such folder ' ${ argument } '. ` ) ;
}
// 如果存在唯一的 ssh 文件夹,则返回其 URI
if ( sshFolder ) return sshFolder . uri ;
// 如果存在多个 ssh 文件夹,则抛出错误
if ( sshFolders . length > 1 ) {
throw new Error ( ` Variable ${ str } can not be resolved in a multi ssh:// folder workspace. Scope this variable using ':' and a workspace folder name. ` ) ;
}
// 如果没有找到 ssh 文件夹,则抛出错误
throw new Error ( ` Variable ${ str } can not be resolved. Please open an ssh:// folder. ` ) ;
} ;
// 根据不同的键,返回不同的路径信息
switch ( key ) {
// 处理 remoteWorkspaceRoot 和 remoteWorkspaceFolder 变量,返回工作区根目录或文件夹的路径
case 'remoteWorkspaceRoot' :
case 'remoteWorkspaceFolder' :
return getFolderUri ( ) . path ;
// 处理 remoteWorkspaceRootFolderName 和 remoteWorkspaceFolderBasename 变量,返回工作区根目录或文件夹的名称
case 'remoteWorkspaceRootFolderName' :
case 'remoteWorkspaceFolderBasename' :
return path . basename ( getFolderUri ( ) . path ) ;
// 处理 remoteFile 变量,返回当前文件的路径
case 'remoteFile' :
return getFilePath ( ) . path ;
// 处理 remoteFileWorkspaceFolder 变量,返回当前文件所在工作区文件夹的路径
case 'remoteFileWorkspaceFolder' :
return getFolderPathForFile ( ) . path ;
// 处理 remoteRelativeFile 变量,返回当前文件相对于工作区根目录或指定文件夹的路径
case 'remoteRelativeFile' :
if ( sshFolder || argument )
return path . relative ( getFolderUri ( ) . path , getFilePath ( ) . path ) ;
return getFilePath ( ) . path ;
// 处理 remoteRelativeFileDirname 变量,返回当前文件所在目录相对于工作区根目录或指定文件夹的路径
case 'remoteRelativeFileDirname' : {
const dirname = path . dirname ( getFilePath ( ) . path ) ;
if ( sshFolder || argument ) {
@ -95,21 +210,28 @@ export function replaceVariables(value: string, config: FileSystemConfig): strin
}
return dirname ;
}
// 处理 remoteFileDirname 变量,返回当前文件所在目录的路径
case 'remoteFileDirname' :
return path . dirname ( getFilePath ( ) . path ) ;
// 处理 remoteFileExtname 变量,返回当前文件的扩展名
case 'remoteFileExtname' :
return path . extname ( getFilePath ( ) . path ) ;
// 处理 remoteFileBasename 变量,返回当前文件的文件名
case 'remoteFileBasename' :
return path . basename ( getFilePath ( ) . path ) ;
// 处理 remoteFileBasenameNoExtension 变量,返回当前文件的文件名(不包含扩展名)
case 'remoteFileBasenameNoExtension' : {
const basename = path . basename ( getFilePath ( ) . path ) ;
return ( basename . slice ( 0 , basename . length - path . extname ( basename ) . length ) ) ;
}
// 处理 remoteFileDirnameBasename 变量,返回当前文件所在目录的文件名
case 'remoteFileDirnameBasename' :
return path . basename ( path . dirname ( getFilePath ( ) . path ) ) ;
// 处理 remotePathSeparator 变量, 返回路径分隔符( POSIX 风格)
case 'remotePathSeparator' :
// Not sure if we even need/want this variable, but sure
return path . posix . sep ;
// 如果变量未被识别,则记录警告并返回原始字符串
default :
const msg = ` Unrecognized task variable ' ${ str } ' starting with 'remote', ignoring ` ;
Logging . warning ( msg , LOGGING_NO_STACKTRACE ) ;
@ -119,174 +241,348 @@ export function replaceVariables(value: string, config: FileSystemConfig): strin
} ) ;
}
/ * *
* 递 归 地 替 换 对 象 中 的 变 量 。
*
* @param object - 要 替 换 变 量 的 对 象 。
* @param handler - 用 于 替 换 变 量 的 函 数 。
* @returns 替 换 变 量 后 的 对 象 。
* /
export async function replaceVariablesRecursive < T > ( object : T , handler : ( value : string ) = > string | Promise < string > ) : Promise < T > {
// 如果对象是字符串,则直接调用处理函数进行替换
if ( typeof object === 'string' ) return handler ( object ) as any ;
// 如果对象是数组,则递归地替换数组中的每个元素
if ( Array . isArray ( object ) ) return object . map ( v = > this . replaceVariablesRecursive ( v , handler ) ) as any ;
// 如果对象是对象且不是正则表达式或日期对象,则递归地替换对象中的每个键值对
if ( typeof object == 'object' && object !== null && ! ( object instanceof RegExp ) && ! ( object instanceof Date ) ) {
// ^ Same requirements VS Code applies: https://github.com/microsoft/vscode/blob/bebd06640734c37f6d5f1a82b13297ce1d297dd1/src/vs/base/common/types.ts#L34
const result : any = { } ;
for ( let key in object ) {
// 递归地替换键和值
const value = await replaceVariablesRecursive ( object [ key ] , handler ) ;
key = await replaceVariablesRecursive ( key , handler ) ;
result [ key ] = value ;
}
return result ;
}
// 如果对象不符合上述条件,则直接返回原对象
return object ;
}
/ * *
* 创 建 一 个 SSH 伪 终 端 。
*
* @param options - 终 端 选 项 。
* @returns 一 个 Promise , 成 功 时 解 析 为 SSHPseudoTerminal 实 例 。
* @throws 如 果 创 建 终 端 失 败 , 则 抛 出 错 误 。
* /
export async function createTerminal ( options : TerminalOptions ) : Promise < SSHPseudoTerminal > {
// 从 options 中提取 connection 对象
const { connection } = options ;
// 从 connection 中提取 actualConfig、client 和 shellConfig 对象
const { actualConfig , client , shellConfig } = connection ;
// 创建一个用于发送数据的事件发射器
const onDidWrite = new vscode . EventEmitter < string > ( ) ;
// 创建一个用于接收关闭事件的事件发射器
const onDidClose = new vscode . EventEmitter < number > ( ) ;
// 创建一个用于接收打开事件的事件发射器
const onDidOpen = new vscode . EventEmitter < void > ( ) ;
// 初始化终端对象为 undefined
let terminal : vscode.Terminal | undefined ;
// Won't actually open the remote terminal until pseudo.open(dims) is called
// 创建一个 SSH 伪终端实例
const pseudo : SSHPseudoTerminal = {
// 终端的当前状态,初始值为 'opening'
status : 'opening' ,
// 终端的连接对象
connection ,
// 用于发送数据的事件发射器
onDidWrite : onDidWrite.event ,
// 用于接收关闭事件的事件发射器
onDidClose : onDidClose.event ,
// 用于接收打开事件的事件发射器
onDidOpen : onDidOpen.event ,
close() {
// 获取伪终端的当前状态和连接通道
const { channel , status } = pseudo ;
// 如果伪终端已经关闭,则直接返回
if ( status === 'closed' ) return ;
// 如果连接通道存在,则发送关闭信号
if ( channel ) {
// 更新伪终端状态为关闭
pseudo . status = 'closed' ;
// 发送 INT 信号
channel . signal ! ( 'INT' ) ;
// 发送 SIGINT 信号
channel . signal ! ( 'SIGINT' ) ;
// 写入控制字符 \x03, 通常用于中断进程
channel . write ( '\x03' ) ;
// 关闭连接通道
channel . close ( ) ;
// 将伪终端的连接通道设置为 undefined
pseudo . channel = undefined ;
}
// 如果伪终端的状态为 'wait-to-close',则执行清理操作
if ( status === 'wait-to-close' ) {
// 释放终端资源
pseudo . terminal ? . dispose ( ) ;
// 将终端设置为 undefined
pseudo . terminal = undefined ;
// 更新伪终端状态为关闭
pseudo . status = 'closed' ;
// 触发关闭事件
onDidClose . fire ( 0 ) ;
}
} ,
/ * *
* 打 开 SSH 伪 终 端 。
*
* @param dims - 终 端 的 尺 寸 信 息 。
* @remarks
* 这 个 方 法 会 尝 试 打 开 一 个 远 程 终 端 会 话 , 并 在 成 功 时 触 发 onDidOpen 事 件 。
* 如 果 终 端 已 经 处 于 打 开 状 态 , 此 方 法 将 不 会 执 行 任 何 操 作 。
* /
async open ( dims ) {
// 触发 onDidWrite 事件,发送正在连接的消息
onDidWrite . fire ( ` Connecting to ${ actualConfig . label || actualConfig . name } ... \ r \ n ` ) ;
try {
// 检查是否需要使用 Windows 命令分隔符
const [ useWinCmdSep ] = getFlagBoolean ( 'WINDOWS_COMMAND_SEPARATOR' , shellConfig . isWindows , actualConfig . flags ) ;
// 根据是否使用 Windows 命令分隔符设置分隔符
const separator = useWinCmdSep ? ' && ' : '; ' ;
// 初始化命令列表
let commands : string [ ] = [ ] ;
// 设置默认的 shell 命令
let SHELL = '$SHELL' ;
// 如果是 Windows 环境,则使用指定的 shell
if ( shellConfig . isWindows ) SHELL = shellConfig . shell ;
// Add exports for environment variables if needed
// 合并连接的环境变量和选项中的环境变量
const env = mergeEnvironment ( connection . environment , options . environment ) ;
// 将环境变量导出为字符串并添加到命令列表中
commands . push ( environmentToExportString ( env , shellConfig . setEnv ) ) ;
// Beta feature to add a "code <file>" command in terminals to open the file locally
// 如果启用了远程命令功能,则执行相应的操作
if ( getFlagBoolean ( 'REMOTE_COMMANDS' , false , actualConfig . flags ) [ 0 ] && shellConfig . setupRemoteCommands ) {
// 获取远程命令配置并执行
const rcCmds = await shellConfig . setupRemoteCommands ( connection ) ;
// 如果有远程命令,则将其添加到命令列表中
if ( rcCmds ? . length ) commands . push ( joinCommands ( rcCmds , separator ) ! ) ;
}
// Push the actual command or (default) shell command with replaced variables
// 如果用户指定了命令,则将其添加到命令列表中
if ( options . command ) {
// 使用实际配置中的变量替换命令中的变量,并将结果添加到命令列表中
commands . push ( replaceVariables ( options . command . replace ( /$SHELL/g , SHELL ) , actualConfig ) ) ;
} else {
}
// 如果用户没有指定命令,则使用配置中的终端命令
else {
// 将实际配置中的终端命令使用分隔符连接起来
const tc = joinCommands ( actualConfig . terminalCommand , separator ) ;
// 如果连接后的终端命令存在,则使用它,否则使用默认的 shell 命令
let cmd = tc ? replaceVariables ( tc . replace ( /$SHELL/g , SHELL ) , actualConfig ) : SHELL ;
// 将最终确定的命令添加到命令列表中
commands . push ( cmd ) ;
}
// There isn't a proper way of setting the working directory, but this should work in most cases
// 从选项中获取工作目录,如果没有则使用实际配置中的根目录
let { workingDirectory } = options ;
// 如果用户没有指定工作目录,则使用实际配置中的根目录
workingDirectory = workingDirectory || actualConfig . root ;
// 使用分隔符连接命令列表,生成最终的命令字符串
let cmd = joinCommands ( commands , separator ) ! ;
// 检查是否指定了工作目录
if ( workingDirectory ) {
// 如果命令字符串中包含 ${workingDirectory},则替换为实际的工作目录
if ( cmd . includes ( '${workingDirectory}' ) ) {
// 将命令字符串中的 ${workingDirectory} 替换为实际的工作目录
cmd = cmd . replace ( /\${workingDirectory}/g , workingDirectory ) ;
} else {
// TODO: Maybe replace with `connection.home`? Especially with Windows not supporting ~
}
// TODO: Maybe replace with `connection.home`? Especially with Windows not supporting ~
// 如果命令字符串中不包含 ${workingDirectory}
else {
// 对于 Windows 系统,不支持 ~ 作为工作目录的前缀,因此抛出错误
if ( workingDirectory . startsWith ( '~' ) ) {
if ( shellConfig . isWindows )
// 在 Windows 系统中,不支持 ~ 作为工作目录的前缀,因此抛出错误
throw new Error ( ` Working directory ' ${ workingDirectory } ' starts with ~ for a Windows shell ` ) ;
// So `cd "~/a/b/..." apparently doesn't work, but `~/"a/b/..."` does
// `"~"` would also fail but `~/""` works fine it seems
// 在非 Windows 系统中,将 ~ 替换为用户的主目录
workingDirectory = ` ~/" ${ workingDirectory . slice ( 2 ) } " ` ;
} else {
}
// 如果工作目录不是以 / 开头(在 Windows 系统中),则将其包围在双引号中
else {
// 如果是 Windows 系统,并且工作目录以 / 开头,后面跟着一个字母和冒号(例如 /C:)
if ( shellConfig . isWindows && workingDirectory . match ( /^\/[a-zA-Z]:/ ) )
// 去掉工作目录最前面的斜杠
workingDirectory = workingDirectory . slice ( 1 ) ;
// 将工作目录包围在双引号中
workingDirectory = ` " ${ workingDirectory } " ` ;
}
// 将 cd 命令和原命令连接起来,形成新的命令字符串
cmd = joinCommands ( [ ` cd ${ workingDirectory } ` , . . . commands ] , separator ) ! ;
}
} else {
}
// 如果没有指定工作目录
else {
// 从命令字符串中移除 ${workingDirectory}
cmd = cmd . replace ( /\${workingDirectory}/g , '' ) ;
}
const pseudoTtyOptions : Partial < PseudoTtyOptions > = { . . . PSEUDO_TTY_OPTIONS , cols : dims?.columns , rows : dims?.rows } ;
// 根据提供的维度( dims) 创建一个伪终端选项对象( pseudoTtyOptions) , 其中包含列数( cols) 和行数( rows)
const pseudoTtyOptions : Partial < PseudoTtyOptions > = { . . . PSEUDO_TTY_OPTIONS , cols : dims?.columns , rows : dims?.rows } ;
// 记录一条调试信息,指示正在为指定的连接配置名称启动 shell, 并显示要执行的命令( cmd)
Logging . debug ( ` Starting shell for ${ connection . actualConfig . name } : ${ cmd } ` ) ;
// 使用提供的命令( cmd) 和伪终端选项( pseudoTtyOptions) 在客户端上执行一个命令, 并等待结果
const channel = await toPromise < ClientChannel | undefined > ( cb = > client . exec ( cmd , { pty : pseudoTtyOptions } , cb ) ) ;
// 如果没有成功创建通道( channel) , 则抛出一个错误, 指示无法创建远程终端
if ( ! channel ) throw new Error ( 'Could not create remote terminal' ) ;
// 将创建的通道( channel) 分配给伪终端对象( pseudo) 的 channel 属性
pseudo . channel = channel ;
// 获取当前时间,作为命令执行的开始时间
const startTime = Date . now ( ) ;
// 监听通道的退出事件
channel . once ( 'exit' , ( code , signal , _ , description ) = > {
// 记录一条调试信息,指示终端会话已关闭,并包含退出代码、信号、描述和伪终端的状态
Logging . debug ` Terminal session closed: ${ { code , signal , description , status : pseudo.status } } ` ;
// 如果退出代码存在,并且终端在一秒内失败(如果这不是一个任务),则保持终端打开,以便用户看到错误
if ( code && ( Date . now ( ) < startTime + 1000 ) && ! options . command ) {
// Terminal failed within a second, let's keep it open for the user to see the error (if this isn't a task)
// 向终端写入错误代码和可能的信号信息
onDidWrite . fire ( ` Got error code ${ code } ${ signal ? ` with signal ${ signal } ` : '' } \ r \ n ` ) ;
// 如果有额外的描述信息,也将其写入终端
if ( description ) onDidWrite . fire ( ` Extra info: ${ description } \ r \ n ` ) ;
// 提示用户按任意键关闭终端
onDidWrite . fire ( 'Press a key to close the terminal\r\n' ) ;
// 提示用户可能还有更多的标准输出/标准错误信息
onDidWrite . fire ( 'Possible more stdout/stderr below:\r\n' ) ;
// 将伪终端的状态设置为等待关闭
pseudo . status = 'wait-to-close' ;
} else {
// 触发终端关闭事件,并传递退出代码
onDidClose . fire ( code || 0 ) ;
// 将伪终端的状态设置为关闭
pseudo . status = 'closed' ;
}
} ) ;
// 监听通道的可读事件
channel . once ( 'readable' , ( ) = > {
// Inform others (e.g. createTaskTerminal) that the terminal is ready to be used
// 如果伪终端的状态是“opening”, 则将其状态更新为“open”
if ( pseudo . status === 'opening' ) pseudo . status = 'open' ;
// 触发 onDidOpen 事件,表示终端已打开
onDidOpen . fire ( ) ;
} ) ;
// 监听通道的数据事件
channel . on ( 'data' , chunk = > onDidWrite . fire ( chunk . toString ( ) ) ) ;
// 监听通道的标准错误数据事件
channel . stderr ! . on ( 'data' , chunk = > onDidWrite . fire ( chunk . toString ( ) ) ) ;
// TODO: ^ Keep track of stdout's color, switch to red, output, then switch back?
} catch ( e ) {
// 记录一条错误信息,指示在启动 SSH 终端时发生了错误,并包含错误的详细信息
Logging . error ` Error starting SSH terminal: \ n ${ e } ` ;
// 将错误信息写入终端,以便用户看到
onDidWrite . fire ( ` Error starting SSH terminal: \ r \ n ${ e } \ r \ n ` ) ;
// 触发终端关闭事件,并传递退出代码 1, 表示终端因错误而关闭
onDidClose . fire ( 1 ) ;
// 将伪终端的状态设置为“closed”, 表示终端已关闭
pseudo . status = 'closed' ;
// 如果存在通道,则销毁它,以释放资源
pseudo . channel ? . destroy ( ) ;
// 将通道设置为 undefined, 表明当前没有活动的通道
pseudo . channel = undefined ;
}
} ,
get terminal ( ) : vscode . Terminal | undefined {
return terminal || = vscode . window . terminals . find ( t = > 'pty' in t . creationOptions && t . creationOptions . pty === pseudo ) ;
} ,
set terminal ( term : vscode.Terminal | undefined ) {
terminal = term ;
} ,
setDimensions ( dims ) {
pseudo . channel ? . setWindow ! ( dims . rows , dims . columns , HEIGHT , WIDTH ) ;
} ,
handleInput ( data ) {
if ( pseudo . status === 'wait-to-close' ) return pseudo . close ( ) ;
pseudo . channel ? . write ( data ) ;
} ,
} ,
/ * *
* 获 取 与 伪 终 端 关 联 的 vscode . Terminal 实 例 。
* 如 果 实 例 不 存 在 , 则 在 窗 口 的 终 端 列 表 中 查 找 。
* @returns { vscode . Terminal | undefined } 找 到 的 终 端 实 例 , 如 果 没 有 找 到 则 返 回 undefined 。
* /
get terminal ( ) : vscode . Terminal | undefined {
return terminal || = vscode . window . terminals . find ( t = > 'pty' in t . creationOptions && t . creationOptions . pty === pseudo ) ;
} ,
/ * *
* 设 置 与 伪 终 端 关 联 的 vscode . Terminal 实 例 。
* @param { vscode . Terminal | undefined } term - 要 设 置 的 终 端 实 例 , 如 果 为 undefined , 则 清 除 当 前 关 联 。
* /
set terminal ( term : vscode.Terminal | undefined ) {
terminal = term ;
} ,
/ * *
* 设 置 伪 终 端 的 尺 寸 。
* @param { Object } dims - 包 含 行 数 和 列 数 的 对 象 。
* /
setDimensions ( dims ) {
pseudo . channel ? . setWindow ! ( dims . rows , dims . columns , HEIGHT , WIDTH ) ;
} ,
/ * *
* 处 理 输 入 到 伪 终 端 的 数 据 。
* 如 果 伪 终 端 的 状 态 是 'wait-to-close' , 则 关 闭 伪 终 端 。
* 否 则 , 将 数 据 写 入 伪 终 端 的 通 道 。
* @param { string } data - 要 写 入 的 输 入 数 据 。
* /
handleInput ( data ) {
if ( pseudo . status === 'wait-to-close' ) return pseudo . close ( ) ;
pseudo . channel ? . write ( data ) ;
} ,
} ;
return pseudo ;
}
/ * *
* 定 义 一 个 文 本 终 端 接 口 , 继 承 自 vscode . Pseudoterminal 接 口
* /
export interface TextTerminal extends vscode . Pseudoterminal {
/ * *
* 向 终 端 写 入 文 本
* @param text - 要 写 入 的 文 本
* /
write ( text : string ) : void ;
/ * *
* 关 闭 终 端
* @param code - 退 出 代 码 , 可 选 参 数
* /
close ( code? : number ) : void ;
onDidClose : vscode.Event < number > ; // Redeclaring that it isn't undefined
/ * *
* 当 终 端 关 闭 时 触 发 的 事 件
* @type { vscode . Event < number > } - 退 出 代 码
* /
onDidClose : vscode.Event < number > ; // 重新声明它不是未定义的 Redeclaring that it isn't undefined
/ * *
* 当 终 端 打 开 时 触 发 的 事 件
* @type { vscode . Event < void > } - 无 参 数
* /
onDidOpen : vscode.Event < void > ;
}
/ * *
* 创 建 一 个 文 本 终 端 实 例
* @param initialText - 初 始 化 时 要 写 入 终 端 的 文 本 , 可 选 参 数
* @returns { TextTerminal } - 创 建 的 文 本 终 端 实 例
* /
export function createTextTerminal ( initialText? : string ) : TextTerminal {
// 创建一个事件发射器,用于在终端中写入文本
const onDidWrite = new vscode . EventEmitter < string > ( ) ;
// 创建一个事件发射器,用于在终端关闭时触发
const onDidClose = new vscode . EventEmitter < number > ( ) ;
// 创建一个事件发射器,用于在终端打开时触发
const onDidOpen = new vscode . EventEmitter < void > ( ) ;
return {
// 向终端写入文本
write : onDidWrite.fire.bind ( onDidWrite ) ,
// 关闭终端
close : onDidClose.fire.bind ( onDidClose ) ,
// 当有文本写入终端时触发的事件
onDidWrite : onDidWrite.event ,
// 当终端关闭时触发的事件
onDidClose : onDidClose.event ,
// 当终端打开时触发的事件
onDidOpen : onDidOpen.event ,
// 打开终端,如果提供了 initialText, 则将其写入终端
open : ( ) = > initialText && ( onDidWrite . fire ( initialText + '\r\n' ) , onDidClose . fire ( 1 ) ) ,
} ;
}