CH-J Tools通过 SSH 管理服务器
菜单

远程编辑器的保存与延迟监测

下载文件

本译文正在进行语言审校。如有不明确之处,请对照英文原文。

根本原因

以前的编辑器会把工作文件上传到目标文件的父目录,而在 /etc 或 /var 下,SSH 账户通常没有该目录的写入权限。SFTP 重命名既不提供经授权的 sudo 路径,也没有基准冲突检查、安全元数据验证或可靠的覆盖语义。其错误处理程序会删除临时副本,即使在无法确定操作是否完成时,也会移除可用于恢复的材料。

会话状态以前只显示连接状态。没有任何 ICMP 或经身份验证的 SSH 响应测量为终端指示器提供数据。

保存协议

  1. 一个固定的 SSH 辅助程序使用基于描述符的相对操作,并对每个路径组件使用 O_NOFOLLOW,打开一个普通的单链接文件。它会记录内容、SHA-256 哈希、设备/inode、UID、GID、完整的权限位、纳秒级的修改/变更时间以及所有可读的扩展属性。Core 会验证哈希,并以严格的 UTF-8 解码,同时保留 BOM。每个文档都会获得一个不透明的 editId,其作用域限定于对应的插件、终端会话和服务器身份。
  2. 一个非特权的准备操作通过 pwd.getpwuid(os.getuid()) 解析已认证账户的主目录,以 0700 模式创建 $HOME/ch-j-sm/,并创建一个私有的、独占的恢复日志。不安全的主目录、作为符号链接的工作区或由其他账户拥有的工作区都会被拒绝。
  3. Core 通过 SFTP 上传一个隐藏的、随机 128 位的 .tmp 工作文件,使用独占创建(wx)、0600 模式以及六十秒的通道/上传超时。日志包含目标、原始基准和预期的工作文件哈希;不包含任何凭据。
  4. 一次 SSH exec 完成整个最终处理,可以选择通过现有的 sudo 执行器运行。辅助程序会安全地打开私有工作副本、计算其哈希并将其刷新到磁盘。它会锁定原始 inode,检查其基准,在目标旁创建一个独占的目标文件,写入全部内容,应用并验证元数据,再次检查原始文件,然后刷新替换文件。
  5. 在同一目录中使用 os.replace 以原子方式替换目标。父目录会被刷新,并验证最终路径、哈希、inode 和元数据。在替换完成之前,原始路径始终可用。工作文件永远不会跨文件系统移动,也永远不会在最终处理过程中被删除。
  6. 在 Core 收到经过验证的成功结果后,会由一个单独的清理步骤再次检查最终内容和安全元数据,然后才删除工作文件和日志。即使清理失败,也会报告已确认的保存,并附带 recoveryAvailable: true。

上传失败、sudo 失败、冲突、元数据错误或替换失败时,会保留原始文件以及所有已暂存的材料。未完成的上传会被如实标识,无法作为完整的已编辑文档恢复。SSH 通信中断、缺少退出状态、超时以及意外的最终处理结果都会被报告为未知,绝不会被报告为已保存。对于结果不确定的文档,Core 会阻止再次保存;不会自动重试替换。

元数据、权限与安全边界

辅助程序会保留并验证 UID、GID 以及所有 POSIX 权限位,包括可执行、setuid、setgid 和 sticky 位。它会逐字节复现 Linux 扩展属性,包括 POSIX ACL、capabilities 和 SELinux 标签。所有权和文件内容会先于安全属性设置,因为写入/chown 可能会清除 capabilities 或特殊权限位。原始文件中不存在、但目标文件继承而来的属性会被移除。修改时间反映新的写入,不会从原始文件复制。

无法读取或无法表示的属性会中止操作;元数据不会被悄悄丢弃。IMA/EVM 签名会被明确拒绝,因为复制与内容/inode 绑定的完整性签名会使其失效。目标路径或主目录路径中的符号链接、硬链接文件以及非普通文件都会被拒绝。不使用任何原地覆盖或较弱的 SFTP 覆盖回退方式。

普通的可写保存以 SSH 账户身份运行。受保护的保存需要显式指定 { sudo: true };对于无需密码的 sudo,sudo 密码是可选的。密码经过验证后只通过通道的 stdin 传递,绝不会出现在命令、日志、工作文件、记录器或远程错误中。root SSH 会话使用现有的直接 root 执行器。授权失败时,原始文件和工作副本保持不变。所有以 root 身份执行的保存(直接 root SSH 或 sudo)还会拒绝由其他账户拥有或可写、或带有访问 ACL 的目标目录/上级目录,而不会信任由其他账户控制的特权路径名。

固定的辅助程序以 python3 -I -B 运行:它会忽略用户的 Python 路径、工作目录和用户 site-packages,并且不写入字节码。路径和 JSON 参数在 Core 中进行 shell 转义。插件永远不会获得任意的 SSH exec 或 SFTP 句柄。现有的沙盒、CSP、清单权限、主机密钥验证和更新验证仍然有效。

Plugin API 与剩余的 Monaco 集成

单独分发的 Monaco/File Manager UI 不在此检出中。其 UI 集成、sudo 提示、未保存缓冲区的处理和翻译无法在这里实现或测试。内置的插件源代码是 Hash & Checksum 插件。没有虚构任何替代的 File Manager 源代码。

现有方法仍然可用。新增的方法/选项由沙盒化的 preload 和经过权限检查的运行时提供:

const opened = await chjPlugin.files.readText(sessionId, path);
// For a protected read, explicitly pass { sudo: true, sudoPassword }.

const result = await chjPlugin.files.saveText(sessionId, path, editor.getValue(), {
  editId: opened.editId,
  sudo: true,           // Include only after the user authorizes elevation.
  sudoPassword         // Omit for passwordless sudo; never persist this value.
});

if (result.ok && result.value.status === "saved") {
  // Mark this exact submitted editor version as saved. Later edits stay dirty.
} else {
  // Keep the Monaco model dirty. Translate result.error.code/status.
  // result.error.recoveryId/recoveryPath identify retained working material.
}

await chjPlugin.files.closeText(opened.editId);
const recovery = await chjPlugin.files.listRecovery(sessionId);
const copy = await chjPlugin.files.readRecovery(sessionId, recoveryId);
// Review copy.status and copy.text; open a fresh document before another save.
await chjPlugin.files.cleanupRecovery(sessionId, recoveryId);
// Cleanup rejects anything whose target is not currently confirmed.

当检查目标需要提升权限时,恢复方法可接受一个可选的最终参数 { sudo: true, sudoPassword }。即使通过 sudo 执行,它们也会保留已认证用户的工作区。日志在应用重启和 SSH 重新连接后依然存在;恢复永远不会自动重复最终处理。在显式保存恢复出的缓冲区之前,请先将其与重新打开的目标进行比对。

files.read 保护读取、检查和句柄释放。files.write 保护保存和恢复清理。对于现有插件,writeText 在失败时仍会抛出拒绝;saveText 则返回结构化的 { ok, value/error } 封装,因为 Electron 会丢弃被拒绝的 Error 对象上的自定义属性。旧式的三参数 writeText 可以使用单个已打开文档的基准。同一路径上存在多个活动文档时需要 editId;含糊的旧式调用会安全地失败,而不会选用另一个文档的基准。插件应释放不再使用的句柄。

外部 UI 必须立即显示保存状态,在失败时保持缓冲区为未保存状态,仅在出现权限/身份验证失败后才提供 sudo,在不覆盖的情况下显示冲突,并在连接中断或结果未知后提供恢复。它必须以捷克语、德语和英语翻译这些状态。建议的状态文本:

状态 捷克语 德语 英语
正在保存 Ukládání… Wird gespeichert… Saving…
已确认 Uloženo Gespeichert Saved
失败 Uložení selhalo Speichern fehlgeschlagen Save failed
需要 sudo Vyžadováno oprávnění sudo sudo-Autorisierung erforderlich Sudo authorization required
冲突 Původní soubor byl změněn Originaldatei wurde geändert Original file changed
连接已断开 Spojení bylo přerušeno Verbindung unterbrochen Connection lost
未知 Výsledek uložení není znám Speicherergebnis unbekannt Save result unknown
恢复 Obnova je dostupná Wiederherstellung verfügbar Recovery available

延迟的定义与生命周期

Ping 是向 SSH 所用的已解析 IP 发送 ICMP echo 请求、并由客户端实际收到回复所报告的延迟。Core 以固定的参数数组异步调用平台的 ping 可执行文件,不使用 shell,也不要求以管理员身份启动。Linux、macOS(包括 ping6)和 Windows 的参数分别选择。ICMP 被阻止、缺少可执行文件、超时、输出无效,或者 Windows 只给出上限值的回复(例如 <1ms)时,都会产生“不可用”的值,而不是编造一个数字。

SSH RTT 是通过现有的已认证 SSH 客户端执行固定的 true exec 请求所耗费的单调 performance.now() 时长。它包括通道建立、服务器处理和响应传递;它既不是纯粹的网络 RTT,也不是 SSH 登录时长。已翻译的指示器提示会解释这一区别。

终端一连接,监测就会开始。各项探测并发且异步地运行,请求/进程超时为五秒。下一次采样安排在上一次采样完成后十二秒进行,以避免重叠。断开连接会中止 ICMP、清除计时器、取消待处理的 SSH 操作并清空数值。记录身份检查可防止旧连接在重新连接后继续发布数据。ICMP 失败永远不会改变 SSH 连接状态。

延迟数据包含在现有的 state 事件 / ssh:state IPC 载荷中:

latency: { pingMs: 24.1, sshRttMs: 31.25, measuredAt: "…" }

不可用的数值为 null。紧凑的终端指示器使用毫秒、本地化的“不可用”文本以及捷克语/德语/英语标签。断开连接时它会隐藏。不需要额外的渲染器执行能力或 IPC 请求。

验证与运行限制

用户要求在现有工作树(当前为 feature/hash-checksum-plugin)中保留更改;没有切换分支,也没有进行无关的清理。

此任务中更改的文件