refactor🎨: (阅读代码):shellConfig.ts增加注释

master
yetao 2 weeks ago
parent 186042f3b3
commit 54a853f590

@ -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";
elif [ ! -n "$KELVIN_SSHFS_CMD_PATH" ]; then
// 如果环境变量 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);
return path ? [
// 如果脚本文件路径存在,则返回设置 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
const embedSubstitutionsBackticks = (command: TemplateStringsArray, ...substitutions: (string | number)[]): string =>
/**
* 使
* @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]) + '"';
const embedSubstitutionsFish = (command: TemplateStringsArray, ...substitutions: (string | number)[]): string =>
/**
* 使 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]) + '"';
const embedSubstitutionsPowershell = (command: TemplateStringsArray, ...substitutions: (string | number)[]): string =>
/**
* 使 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: '???' };
}
}

Loading…
Cancel
Save