|
|
|
@ -1,61 +1,140 @@
|
|
|
|
|
// 导入 path 模块,用于处理和转换文件路径
|
|
|
|
|
import { posix as path } from 'path';
|
|
|
|
|
// 导入 ssh2 模块中的 Client、ClientChannel 和 SFTP 类型,用于 SSH 连接和文件传输
|
|
|
|
|
import type { Client, ClientChannel, SFTP } from 'ssh2';
|
|
|
|
|
// 导入 connection 模块中的 Connection 类型,用于表示 SSH 连接
|
|
|
|
|
import type { Connection } from './connection';
|
|
|
|
|
// 导入 logging 模块中的 Logger、Logging 和 LOGGING_NO_STACKTRACE 常量,用于日志记录
|
|
|
|
|
import { Logger, Logging, LOGGING_NO_STACKTRACE } from './logging';
|
|
|
|
|
// 导入 utils 模块中的 toPromise 函数,用于将回调函数转换为 Promise
|
|
|
|
|
import { toPromise } from './utils';
|
|
|
|
|
|
|
|
|
|
// 定义一个 Shell 脚本,用于在 SSH FS 环境中通过 VS Code 打开文件或文件夹
|
|
|
|
|
const SCRIPT_COMMAND_CODE = `#!/bin/sh
|
|
|
|
|
// 如果参数数量不等于 1,或者参数是 help、--help、-h 或 -?,则打印帮助信息
|
|
|
|
|
if [ "$#" -ne 1 ] || [ $1 = "help" ] || [ $1 = "--help" ] || [ $1 = "-h" ] || [ $1 = "-?" ]; then
|
|
|
|
|
echo "Usage:";
|
|
|
|
|
echo " code <path_to_existing_file> Will make VS Code open the file";
|
|
|
|
|
echo " code <path_to_existing_folder> Will make VS Code add the folder as an additional workspace folder";
|
|
|
|
|
echo " code <path_to_nonexisting_file> Will prompt VS Code to create an empty file, then open it afterwards";
|
|
|
|
|
// 如果环境变量 KELVIN_SSHFS_CMD_PATH 不存在,则打印错误信息
|
|
|
|
|
elif [! -n "$KELVIN_SSHFS_CMD_PATH" ]; then
|
|
|
|
|
echo "Not running in a terminal spawned by SSH FS? Failed to sent!"
|
|
|
|
|
// 如果 KELVIN_SSHFS_CMD_PATH 是一个字符设备文件(通常是一个终端设备),则将命令写入该文件
|
|
|
|
|
elif [ -c "$KELVIN_SSHFS_CMD_PATH" ]; then
|
|
|
|
|
echo "::sshfs:code:$(pwd):::$1" >> $KELVIN_SSHFS_CMD_PATH;
|
|
|
|
|
echo "Command sent to SSH FS extension";
|
|
|
|
|
// 如果 KELVIN_SSHFS_CMD_PATH 不是一个字符设备文件,则打印错误信息
|
|
|
|
|
else
|
|
|
|
|
echo "Missing command shell pty of SSH FS extension? Failed to sent!"
|
|
|
|
|
fi
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 定义一个远程命令初始化器类型,它可以是一个函数、字符串、字符串数组或 Promise,
|
|
|
|
|
* 用于初始化远程命令。
|
|
|
|
|
* @typeparam connection - 连接对象类型。
|
|
|
|
|
* @returns 初始化远程命令的结果,可以是 void、字符串、字符串数组或 Promise。
|
|
|
|
|
*/
|
|
|
|
|
type RemoteCommandInitializer = (connection: Connection) => void
|
|
|
|
|
| string | string[] | undefined
|
|
|
|
|
| Promise<void | string | string[] | undefined>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 确保文件被缓存到远程服务器上
|
|
|
|
|
* @param connection - 连接对象
|
|
|
|
|
* @param key - 文件的键
|
|
|
|
|
* @param path - 文件的路径
|
|
|
|
|
* @param content - 文件的内容
|
|
|
|
|
* @param sftp - SFTP 对象
|
|
|
|
|
* @returns 一个元组,包含文件是否被写入的布尔值和文件的路径
|
|
|
|
|
*/
|
|
|
|
|
async function ensureCachedFile(connection: Connection, key: string, path: string, content: string, sftp?: SFTP):
|
|
|
|
|
Promise<[written: boolean, path: string | null]> {
|
|
|
|
|
// 从连接的缓存中获取 rc_files 对象,如果不存在则初始化为空对象
|
|
|
|
|
const rc_files: Record<string, string> = connection.cache.rc_files ||= {};
|
|
|
|
|
// 如果文件已经存在于缓存中,则返回 false 和文件的路径
|
|
|
|
|
if (rc_files[key]) return [false, rc_files[key]];
|
|
|
|
|
try {
|
|
|
|
|
// 如果没有提供 SFTP 对象,则从连接的客户端中获取
|
|
|
|
|
sftp ||= await toPromise<SFTP>(cb => connection.client.sftp(cb));
|
|
|
|
|
// 使用 SFTP 对象将文件内容写入到指定路径,并设置文件权限为 0o755
|
|
|
|
|
await toPromise(cb => sftp!.writeFile(path, content, { mode: 0o755 }, cb));
|
|
|
|
|
// 将文件路径保存到缓存中,并返回 true 和文件路径
|
|
|
|
|
return [true, rc_files[key] = path];
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 如果写入文件失败,记录错误信息并返回 false 和 null
|
|
|
|
|
Logging.error`Failed to write ${key} file to '${path}':\n${e}`;
|
|
|
|
|
return [false, null];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化远程命令的 PATH 环境变量
|
|
|
|
|
* @param connection - 连接对象
|
|
|
|
|
* @returns 一个字符串数组,包含设置 PATH 环境变量的命令
|
|
|
|
|
*/
|
|
|
|
|
async function rcInitializePATH(connection: Connection): Promise<string[] | string> {
|
|
|
|
|
// 定义一个目录路径,用于存放远程命令脚本
|
|
|
|
|
const dir = `/tmp/.Kelvin_sshfs.RcBin.${connection.actualConfig.username || Date.now()}`;
|
|
|
|
|
// 从连接对象中获取 SFTP 客户端
|
|
|
|
|
const sftp = await toPromise<SFTP>(cb => connection.client.sftp(cb));
|
|
|
|
|
// 尝试在远程服务器上创建指定目录,如果目录已存在则忽略错误
|
|
|
|
|
await toPromise(cb => sftp!.mkdir(dir, { mode: 0o755 }, cb)).catch(() => { });
|
|
|
|
|
// 确保脚本文件被缓存到远程服务器上,并获取其路径
|
|
|
|
|
const [, path] = await ensureCachedFile(connection, 'CmdCode', `${dir}/code`, SCRIPT_COMMAND_CODE, sftp);
|
|
|
|
|
// 如果脚本文件路径存在,则返回设置 PATH 环境变量的命令
|
|
|
|
|
return path? [
|
|
|
|
|
connection.shellConfig.setEnv('PATH', `${dir}:$PATH`),
|
|
|
|
|
] : 'echo "An error occured while adding REMOTE_COMMANDS support"';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 定义了一个 Shell 配置对象的接口。
|
|
|
|
|
* 该接口包含了 shell 的名称、是否为 Windows 系统、设置环境变量的方法、初始化远程命令的方法以及嵌入替换的方法。
|
|
|
|
|
*/
|
|
|
|
|
export interface ShellConfig {
|
|
|
|
|
/**
|
|
|
|
|
* shell 的名称。
|
|
|
|
|
*/
|
|
|
|
|
shell: string;
|
|
|
|
|
/**
|
|
|
|
|
* 是否为 Windows 系统。
|
|
|
|
|
*/
|
|
|
|
|
isWindows: boolean;
|
|
|
|
|
/**
|
|
|
|
|
* 设置环境变量的方法。
|
|
|
|
|
* @param key - 环境变量的键。
|
|
|
|
|
* @param value - 环境变量的值。
|
|
|
|
|
* @returns 设置环境变量后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
setEnv(key: string, value: string): string;
|
|
|
|
|
/**
|
|
|
|
|
* 初始化远程命令的方法。
|
|
|
|
|
* @param connection - 连接对象。
|
|
|
|
|
* @returns 初始化远程命令的结果,可以是 void、字符串、字符串数组或 Promise。
|
|
|
|
|
*/
|
|
|
|
|
setupRemoteCommands?: RemoteCommandInitializer;
|
|
|
|
|
/**
|
|
|
|
|
* 嵌入替换的方法。
|
|
|
|
|
* @param command - 模板字符串数组。
|
|
|
|
|
* @param substitutions - 替换的字符串或数字。
|
|
|
|
|
* @returns 嵌入替换后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
embedSubstitutions?(command: TemplateStringsArray, ...substitutions: (string | number)[]): string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 定义一个对象,用于存储已知的 Shell 配置
|
|
|
|
|
export const KNOWN_SHELL_CONFIGS: Record<string, ShellConfig> = {}; {
|
|
|
|
|
/**
|
|
|
|
|
* 添加一个新的 Shell 配置到已知的 Shell 配置列表中。
|
|
|
|
|
* @param shell - Shell 的名称。
|
|
|
|
|
* @param setEnv - 设置环境变量的函数。
|
|
|
|
|
* @param setupRemoteCommands - 初始化远程命令的函数。
|
|
|
|
|
* @param embedSubstitutions - 嵌入替换的函数。
|
|
|
|
|
* @param isWindows - 是否为 Windows 系统。
|
|
|
|
|
* @returns 无返回值。
|
|
|
|
|
*/
|
|
|
|
|
const add = (shell: string,
|
|
|
|
|
setEnv: (key: string, value: string) => string,
|
|
|
|
|
setupRemoteCommands?: RemoteCommandInitializer,
|
|
|
|
@ -64,104 +143,232 @@ export const KNOWN_SHELL_CONFIGS: Record<string, ShellConfig> = {}; {
|
|
|
|
|
KNOWN_SHELL_CONFIGS[shell] = { shell, setEnv, setupRemoteCommands, embedSubstitutions, isWindows };
|
|
|
|
|
}
|
|
|
|
|
// Ways to set an environment variable
|
|
|
|
|
/**
|
|
|
|
|
* 设置环境变量的方法,用于导出环境变量。
|
|
|
|
|
* @param key - 环境变量的键。
|
|
|
|
|
* @param value - 环境变量的值。
|
|
|
|
|
* @returns 设置环境变量后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const setEnvExport = (key: string, value: string) => `export ${key}=${value}`;
|
|
|
|
|
/**
|
|
|
|
|
* 设置环境变量的方法,用于设置全局环境变量。
|
|
|
|
|
* @param key - 环境变量的键。
|
|
|
|
|
* @param value - 环境变量的值。
|
|
|
|
|
* @returns 设置环境变量后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const setEnvSetGX = (key: string, value: string) => `set -gx ${key} ${value}`;
|
|
|
|
|
/**
|
|
|
|
|
* 设置环境变量的方法,用于设置环境变量。
|
|
|
|
|
* @param key - 环境变量的键。
|
|
|
|
|
* @param value - 环境变量的值。
|
|
|
|
|
* @returns 设置环境变量后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const setEnvSetEnv = (key: string, value: string) => `setenv ${key} ${value}`;
|
|
|
|
|
/**
|
|
|
|
|
* 设置环境变量的方法,用于设置 PowerShell 环境变量。
|
|
|
|
|
* @param key - 环境变量的键。
|
|
|
|
|
* @param value - 环境变量的值。
|
|
|
|
|
* @returns 设置环境变量后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const setEnvPowerShell = (key: string, value: string) => `$env:${key}="${value}"`;
|
|
|
|
|
/**
|
|
|
|
|
* 设置环境变量的方法,用于设置命令行环境变量。
|
|
|
|
|
* @param key - 环境变量的键。
|
|
|
|
|
* @param value - 环境变量的值。
|
|
|
|
|
* @returns 设置环境变量后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const setEnvSet = (key: string, value: string) => `set ${key}=${value}`;
|
|
|
|
|
|
|
|
|
|
// Ways to embed a substitution
|
|
|
|
|
/**
|
|
|
|
|
* 使用反引号嵌入替换字符串的方法。
|
|
|
|
|
* @param command - 模板字符串数组。
|
|
|
|
|
* @param substitutions - 替换的字符串或数字。
|
|
|
|
|
* @returns 嵌入替换后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const embedSubstitutionsBackticks = (command: TemplateStringsArray,...substitutions: (string | number)[]): string =>
|
|
|
|
|
'"' + substitutions.reduce((str, sub, i) => `${str}\`${sub}\`${command[i + 1]}`, command[0]) + '"';
|
|
|
|
|
/**
|
|
|
|
|
* 使用 Fish 脚本嵌入替换字符串的方法。
|
|
|
|
|
* @param command - 模板字符串数组。
|
|
|
|
|
* @param substitutions - 替换的字符串或数字。
|
|
|
|
|
* @returns 嵌入替换后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const embedSubstitutionsFish = (command: TemplateStringsArray,...substitutions: (string | number)[]): string =>
|
|
|
|
|
substitutions.reduce((str, sub, i) => `${str}"(${sub})"${command[i + 1]}`, '"' + command[0]) + '"';
|
|
|
|
|
/**
|
|
|
|
|
* 使用 PowerShell 嵌入替换字符串的方法。
|
|
|
|
|
* @param command - 模板字符串数组。
|
|
|
|
|
* @param substitutions - 替换的字符串或数字。
|
|
|
|
|
* @returns 嵌入替换后的字符串。
|
|
|
|
|
*/
|
|
|
|
|
const embedSubstitutionsPowershell = (command: TemplateStringsArray,...substitutions: (string | number)[]): string =>
|
|
|
|
|
substitutions.reduce((str, sub, i) => `${str}$(${sub})${command[i + 1]}`, '"' + command[0]) + '"';
|
|
|
|
|
// Register the known shells
|
|
|
|
|
// 添加 sh 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('sh', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 bash 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('bash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 rbash 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('rbash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 ash 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('ash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 dash 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('dash', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 ksh 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('ksh', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 zsh 外壳配置,使用 setEnvExport 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('zsh', setEnvExport, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 fish 外壳配置,使用 setEnvSetGX 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsFish 函数嵌入替换字符串
|
|
|
|
|
add('fish', setEnvSetGX, rcInitializePATH, embedSubstitutionsFish); // https://fishshell.com/docs/current/tutorial.html#autoloading-functions
|
|
|
|
|
// 添加 csh 外壳配置,使用 setEnvSetEnv 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('csh', setEnvSetEnv, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 tcsh 外壳配置,使用 setEnvSetEnv 函数设置环境变量,使用 rcInitializePATH 函数初始化远程命令,使用 embedSubstitutionsBackticks 函数嵌入替换字符串
|
|
|
|
|
add('tcsh', setEnvSetEnv, rcInitializePATH, embedSubstitutionsBackticks);
|
|
|
|
|
// 添加 powershell 外壳配置,使用 setEnvPowerShell 函数设置环境变量,使用 embedSubstitutionsPowershell 函数嵌入替换字符串,isWindows 设置为 true
|
|
|
|
|
add('powershell', setEnvPowerShell, undefined, embedSubstitutionsPowershell, true); // experimental
|
|
|
|
|
// 添加 cmd.exe 外壳配置,使用 setEnvSet 函数设置环境变量,isWindows 设置为 true
|
|
|
|
|
add('cmd.exe', setEnvSet, undefined, undefined, true); // experimental
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 在 SSH 客户端上尝试执行指定的命令,并返回命令的输出或 null。
|
|
|
|
|
* 如果命令执行失败,将抛出一个包含错误信息的 Error 对象。
|
|
|
|
|
* @param ssh - SSH 客户端实例。
|
|
|
|
|
* @param command - 要执行的命令字符串。
|
|
|
|
|
* @returns 命令的标准输出字符串,如果命令执行成功;否则返回 null。
|
|
|
|
|
* @throws Error 如果命令执行失败,将抛出一个包含错误信息的 Error 对象。
|
|
|
|
|
*/
|
|
|
|
|
export async function tryCommand(ssh: Client, command: string): Promise<string | null> {
|
|
|
|
|
// 使用 ssh.exec 方法执行命令,并将结果转换为 Promise<ClientChannel> 类型
|
|
|
|
|
const exec = await toPromise<ClientChannel>(cb => ssh.exec(command, cb));
|
|
|
|
|
// 初始化一个包含两个空字符串的数组,用于存储命令的标准输出和标准错误输出
|
|
|
|
|
const output = ['', ''] as [string, string];
|
|
|
|
|
// 监听 exec 事件,当有数据输出时,将数据添加到 output 数组的第一个元素中
|
|
|
|
|
exec.on('data', (chunk: any) => output[0] += chunk);
|
|
|
|
|
// 监听 exec.stderr 事件,当有错误数据输出时,将数据添加到 output 数组的第二个元素中
|
|
|
|
|
exec.stderr!.on('data', (chunk: any) => output[1] += chunk);
|
|
|
|
|
// 等待 exec 事件结束,并处理可能的错误
|
|
|
|
|
await toPromise(cb => {
|
|
|
|
|
exec.once('error', cb);
|
|
|
|
|
exec.once('close', cb);
|
|
|
|
|
}).catch(e => {
|
|
|
|
|
// 如果捕获到的错误不是数字类型,则重新抛出该错误
|
|
|
|
|
if (typeof e !== 'number') throw e;
|
|
|
|
|
// 否则,抛出一个新的 Error 对象,包含命令失败的退出码和错误信息
|
|
|
|
|
throw new Error(`Command '${command}' failed with exit code ${e}${output[1] ? `:\n${output[1].trim()}` : ''}`);
|
|
|
|
|
});
|
|
|
|
|
// 如果命令没有产生标准输出
|
|
|
|
|
if (!output[0]) {
|
|
|
|
|
// 如果也没有标准错误输出,则返回 null
|
|
|
|
|
if (!output[1]) return null;
|
|
|
|
|
// 否则,抛出一个新的 Error 对象,包含命令失败的错误信息
|
|
|
|
|
throw new Error(`Command '${command}' only produced stderr:\n${output[1].trim()}`);
|
|
|
|
|
}
|
|
|
|
|
// 返回命令的标准输出
|
|
|
|
|
return output[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 在 SSH 客户端上尝试执行一个带有变量替换的 echo 命令,并返回变量的值或 null。
|
|
|
|
|
* 如果 shell 配置不支持嵌入替换,则抛出一个错误。
|
|
|
|
|
* @param ssh - SSH 客户端实例。
|
|
|
|
|
* @param shellConfig - Shell 配置对象。
|
|
|
|
|
* @param variable - 要替换的变量名。
|
|
|
|
|
* @returns 变量的值,如果命令执行成功;否则返回 null。
|
|
|
|
|
* @throws Error 如果 shell 配置不支持嵌入替换,则抛出一个错误。
|
|
|
|
|
*/
|
|
|
|
|
export async function tryEcho(ssh: Client, shellConfig: ShellConfig, variable: string): Promise<string | null> {
|
|
|
|
|
// 检查 shellConfig 是否支持嵌入替换,如果不支持则抛出错误
|
|
|
|
|
if (!shellConfig.embedSubstitutions) throw new Error(`Shell '${shellConfig.shell}' does not support embedding substitutions`);
|
|
|
|
|
// 生成一个唯一的数字,用于标记 echo 命令的输出
|
|
|
|
|
const uniq = Date.now() % 1e5;
|
|
|
|
|
// 使用 tryCommand 函数执行 echo 命令,并传入嵌入替换的字符串
|
|
|
|
|
const output = await tryCommand(ssh, `echo ${shellConfig.embedSubstitutions`::${'echo ' + uniq}:echo_result:${`echo ${variable}`}:${'echo ' + uniq}::`}`);
|
|
|
|
|
// 如果输出存在,则使用正则表达式匹配变量的值,并返回匹配结果的第一个捕获组
|
|
|
|
|
return output?.match(`::${uniq}:echo_result:(.*?):${uniq}::`)?.[1] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取远程 SSH 客户端上 PowerShell 的版本号。
|
|
|
|
|
* @param client - SSH 客户端实例。
|
|
|
|
|
* @returns PowerShell 的版本号,如果获取成功;否则返回 null。
|
|
|
|
|
*/
|
|
|
|
|
async function getPowershellVersion(client: Client): Promise<string | null> {
|
|
|
|
|
// 尝试执行命令获取 PowerShell 版本信息
|
|
|
|
|
const version = await tryCommand(client, 'echo $PSversionTable.PSVersion.ToString()').catch(e => {
|
|
|
|
|
// 如果命令执行失败,记录错误信息并返回 null
|
|
|
|
|
console.error(e);
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
// 如果获取到版本信息,使用正则表达式匹配版本号
|
|
|
|
|
return version?.match(/\d+\.\d+\.\d+\.\d+/)?.[0] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取远程 SSH 客户端上 Windows 操作系统的版本号。
|
|
|
|
|
* @param client - SSH 客户端实例。
|
|
|
|
|
* @returns Windows 操作系统的版本号,如果获取成功;否则返回 null。
|
|
|
|
|
*/
|
|
|
|
|
async function getWindowsVersion(client: Client): Promise<string | null> {
|
|
|
|
|
// 尝试执行命令获取 Windows 版本信息
|
|
|
|
|
const version = await tryCommand(client, 'systeminfo | findstr /BC:"OS Version"').catch(e => {
|
|
|
|
|
// 如果命令执行失败,记录错误信息并返回 null
|
|
|
|
|
console.error(e);
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
// 如果获取到版本信息,使用正则表达式匹配版本号
|
|
|
|
|
const match = version?.trim().match(/^OS Version:[ \t]+(.*)$/);
|
|
|
|
|
// 返回匹配结果的第一个捕获组,即版本号
|
|
|
|
|
return match?.[1] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算并返回远程 SSH 客户端的 Shell 配置。
|
|
|
|
|
* @param client - SSH 客户端实例。
|
|
|
|
|
* @param logging - 日志记录器,可选参数。
|
|
|
|
|
* @returns 计算出的 Shell 配置对象。
|
|
|
|
|
* @throws Error 如果计算过程中发生错误,将抛出一个包含错误信息的 Error 对象。
|
|
|
|
|
*/
|
|
|
|
|
export async function calculateShellConfig(client: Client, logging?: Logger): Promise<ShellConfig> {
|
|
|
|
|
try {
|
|
|
|
|
// 尝试执行命令获取 Shell 信息
|
|
|
|
|
const shellStdout = await tryCommand(client, 'echo :::SHELL:$SHELL:SHELL:::');
|
|
|
|
|
// 从输出中提取 Shell 名称
|
|
|
|
|
const shell = shellStdout?.match(/:::SHELL:([^$].*?):SHELL:::/)?.[1];
|
|
|
|
|
if (!shell) {
|
|
|
|
|
// 如果未获取到 Shell 名称,尝试获取 PowerShell 版本信息
|
|
|
|
|
const psVersion = await getPowershellVersion(client);
|
|
|
|
|
if (psVersion) {
|
|
|
|
|
logging?.debug(`Detected PowerShell version ${psVersion}`);
|
|
|
|
|
// 返回 PowerShell 的 Shell 配置
|
|
|
|
|
return { ...KNOWN_SHELL_CONFIGS['powershell'], shell: 'PowerShell' };
|
|
|
|
|
}
|
|
|
|
|
// 如果未获取到 PowerShell 版本信息,尝试获取 Windows 版本信息
|
|
|
|
|
const windowsVersion = await getWindowsVersion(client);
|
|
|
|
|
if (windowsVersion) {
|
|
|
|
|
logging?.debug(`Detected Command Prompt for Windows ${windowsVersion}`);
|
|
|
|
|
// 返回 cmd.exe 的 Shell 配置
|
|
|
|
|
return { ...KNOWN_SHELL_CONFIGS['cmd.exe'], shell: 'cmd.exe' };
|
|
|
|
|
}
|
|
|
|
|
// 如果既未获取到 Shell 名称,也未获取到 PowerShell 或 Windows 版本信息,记录错误并抛出异常
|
|
|
|
|
if (shellStdout) logging?.error(`Could not get $SHELL from following output:\n${shellStdout}`, LOGGING_NO_STACKTRACE);
|
|
|
|
|
throw new Error('Could not get $SHELL');
|
|
|
|
|
}
|
|
|
|
|
// 检查是否存在已知的 Shell 配置
|
|
|
|
|
const known = KNOWN_SHELL_CONFIGS[path.basename(shell)];
|
|
|
|
|
if (known) {
|
|
|
|
|
logging?.debug(`Detected known $SHELL '${shell}' (${known.shell})`);
|
|
|
|
|
// 返回已知的 Shell 配置
|
|
|
|
|
return known;
|
|
|
|
|
} else {
|
|
|
|
|
logging?.warning(`Unrecognized $SHELL '${shell}', using default ShellConfig instead`);
|
|
|
|
|
// 返回默认的 Shell 配置
|
|
|
|
|
return { ...KNOWN_SHELL_CONFIGS['sh'], shell };
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 如果发生错误,记录错误并返回默认的 Shell 配置
|
|
|
|
|
logging && logging.error`Error calculating ShellConfig: ${e}`;
|
|
|
|
|
return { ...KNOWN_SHELL_CONFIGS['sh'], shell: '???' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|