【免费下载链接】obsidian-terminal
Integrate consoles, shells, and terminals.
obsidian-terminal 是一个 Obsidian 终端插件,它把真正的 shell 集成进笔记软件:在 macOS、Linux 与 Windows 上,它通过Python PTY 代理和ConPTY 控制主机在浏览器沙箱外托管伪终端(Pseudoterminal),再经 xterm.js 渲染出可交互的跨平台伪终端。本文拆解这套"前端—TS 中间层—Python 宿主"三层架构的核心机制,帮你理解浏览器类应用如何安全、可靠地驱动终端。
为什么浏览器里的终端需要一个 Python 代理?
Obsidian 基于 Electron,插件代码运行在浏览器渲染进程里,无法直接调用fork()、CreatePseudoConsole这类系统接口。终端的"灵魂"——伪终端设备(Unix PTY / Windows ConPTY)——必须由一个真正的操作系统进程持有。
obsidian-terminal 的解法非常直接:🐍
- 把 Python 源码直接打进插件 bundle,作为"虚拟模块"导入;
- 用
python -c <源码> <shell 命令>启动一个PTY 宿主进程,它创建伪终端并跑起目标 shell; - 宿主进程的 stdin/stdout 就是数据通道,另开一条控制通道传"调整窗口大小"这类指令;
- TypeScript 中间层负责把宿主的输出接到 xterm.js,把键盘输入写回宿主。
这个统一抽象定义在Pseudoterminal接口(pseudoterminal.ts),只暴露shell、kill、onExit、pipe、resize五个概念,Unix 与 Windows 各自实现,对上层完全透明。
Unix 侧:一个约 460 行的 Python PTY 代理
Unix 平台的宿主是 unix_pseudoterminal.py,逻辑精炼到可以通读全文。
pty.fork() 与非阻塞 master
main()调用pty.fork()一分为二:子进程execvp执行目标 shell,父进程成为"代理",持有 pty master 描述符并立即设为非阻塞(unix_pseudoterminal.py)。
关键设计在文件头注释里说得明白:"代理从不等待写入 pty"。因为如果 shell 正在等自己读输出,代理又阻塞在写 pty 上,两边就永久死锁。所以:
- pty 输出 → 代理 stdout,始终可读就走;
- 代理 stdin → pty 的输入放进一个有界缓冲区(
_INPUT_BUFFER_LIMIT = 65536字节,unix_pseudoterminal.py),写不进去就停下读 stdin,让背压自然传导回浏览器端。
selector 事件循环:读写复用一套回调
代理用DefaultSelector监听三个描述符(unix_pseudoterminal.py):
| 描述符 | 处理器 | 职责 |
|---|---|---|
| pty master | _PipePty | 读出 shell 输出转发到 stdout;在有排队输入时才注册EVENT_WRITE,PTY 有空闲就_flush() |
| stdin (fd 0) | _PipeStdin | 读宿主写来的键盘输入喂给 pty,缓冲区满时自动"停看" |
| 控制通道 (fd 3) | _ProcessCmdIO | 解析列数x行数指令,调整窗口尺寸 |
一个细节:macOS 上子进程丢弃输入队列或改变终端模式时,PTY 可能"变空"但 selector 不会上报可写,所以每轮循环都会强制retry()一次(unix_pseudoterminal.py)。
窗口大小调整:fd 3 上的 TIOCSWINSZ
控制通道是第 4 个 stdio 管道(fd 3)。TS 侧每次 resize 只写一行"120x30\n"这样的文本(pseudoterminal.ts),Python 侧解析后执行ioctl(TIOCSWINSZ),内核随即通知 shell 终端尺寸变化,触发SIGWINCH和重排(unix_pseudoterminal.py)。
优雅终止:按进程组发三档信号
宿主退出时先关掉 pty master(内核挂断会话),再对整个进程组按SIGHUP → SIGTERM → SIGKILL逐级施压,每档给 1~2 秒宽限,并用/proc或killpg(pgid, 0)轮询确认组内已无活进程(unix_pseudoterminal.py)。这样即使用户只杀了 shell,它的子孙进程(比如top)也不会变成孤儿。
Windows 侧:ConPTY 主机的握手、认证与进程收容
Windows 没有 POSIX PTY,obsidian-terminal 用ConPTY(Windows 10 1899+ 的伪控制台 API)实现同等能力。宿主 win32_conpty.py 约 3000 行,几乎全靠ctypes调用 Win32 API,亮点不少。
纯 ctypes 实现 CreatePseudoConsole
宿主用 ctypes 绑定conpty.dll的CreatePseudoConsole、ResizePseudoConsole(win32_conpty.py),并按微软winconpty.cpp的移植逻辑构造PSEUDOCONSOLE结构,让CreateProcessW把 shell 挂到控制台宿主上。
它还会优先使用 Windows Terminal 1.22+ 自带的控制台宿主OpenConsole.exe(响应更多终端回复序列),找不到或启动失败就自动回退到系统内置 conhost,并允许 ConPTY 整体失败时降级为纯 ConHost 后端(win32_conpty.py)。
NDJSON 控制管道 + token 认证握手
与 Unix 的"fd 3 写一行文本"不同,ConPTY 宿主使用命名管道\\.\pipe\obsidian-terminal-conpty-<随机>承载 NDJSON 控制消息(pseudoterminal.ts)。为什么需要管道和认证?因为命名管道是全局可见的,任何本地进程都能连上来发kill指令。
握手流程(win32_conpty.py):
- 宿主发送
hello(或预热的idle),携带 token 与hostPid、childPid; - 插件核对 token 后回发
op: "authenticate"响应,宿主限定 5 秒内必须收到,否则拒绝服务(win32_conpty.py); - 之后宿主才发
ready事件,并附上一句"宣誓"式认证字符串:create-pseudoconsole+authenticated-control-channel+job-object-assigned—— 明确宣告"已创建伪终端 + 控制通道已认证 + 已分配 Job 对象"(win32_conpty.py)。
token 本身只经环境变量OBSIDIAN_TERMINAL_CONPTY_TOKEN传递,绝不进命令行,避免被进程列表窥探(pseudoterminal.ts)。
挂起启动 + Job 对象:让"杀进程"永远可靠
宿主用CREATE_SUSPENDED创建 shell:进程先被冻住,分配进一个带JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE的 Job 对象,然后才发送hello报出子进程 PID,最后ResumeThread放行(win32_conpty.py)。
好处很实际:
- 冻结窗口内完成 Job 分配,杜绝"进程跑了但没管住"的竞态;
- 关闭 Job 即连带杀掉 shell 的全部后代,无需逐个
TerminateProcess; ready事件里报的 PID 就是真实运行的子进程,TS 侧可据此校验宿主身份(pseudoterminal.ts)。
数据泵与 resize
会话建立后,三个后台线程各司其职:conpty-output把伪终端输出泵到宿主 stdout,conpty-input把宿主 stdin 写进伪终端输入管道,conpty-control循环处理resize/kill等操作(win32_conpty.py)。
值得注意的是输出泵直接用裸描述符os.write写盘,注释解释了原因:线程在解释器收尾阶段持有sys.stdout.buffer的锁会直接让 CPython 崩溃——这类细节正是"Python 当系统级宿主"的必修课。resize 则通过控制管道的resize操作执行ResizePseudoConsole;若运行在 OpenConsole 宿主上,改发信号管数据包包给它(win32_conpty.py)。
TypeScript 中间层:xterm.js 接入与背压控制
宿主输出只是字节流,真正"像终端"的是 xterm.js 渲染与输入回传,核心函数是pipeShellToTerminal(pseudoterminal.ts),它做了三件关键的事:
- 切片写入:
writeTerminalSliced把大输出切成小块写 xterm,因为 xterm 每次Terminal.write是不可打断的任务,切片才能让渲染帧穿插其间,防止cat big.log时界面卡死; - 高/低水位背压:
createTerminalOutputBackpressure统计"已写入但未解析"的字节数,超过高水位就暂停来源流,低于低水位再恢复,避免浏览器内存被输出洪水冲爆(pseudoterminal.ts); - 输入回传:
terminal.onData把 xterm 解析后的按键数据写回shell.stdin,shell 退出时自动解绑。
此外,终端外观选项、多标签、历史保存等功能都建立在这套管道之上,配置入口在 settings-data.ts,可定制的终端行为详见 emulator.ts。
动手跑起来:环境要求与排错要点
想亲自验证这套跨平台伪终端实现,环境要求如下:
| 平台 | 要求 | 说明 |
|---|---|---|
| macOS / Linux | Python ≥ 3.9 | 按 profile 配置解释器,点"Check"校验 |
| Windows | Python 可选 | 默认 ConPTY 后端无需 pip 包;ConHost 后端的 resizer 需要psutil、pywinctl(win32_resizer.py) |
Windows 上若 Python 不可用、宿主握手超时或 ConPTY 启动失败,插件会自动以 ConHost 模式打开并提示错误;装好 Python 后在插件设置里点"Recheck Python"即可重试 ConPTY(README.md)。相关行为还有专门的自愈脚本 win32-doctor.ts,以及测试用例 test_unix_pseudoterminal.py、test_win32_conpty.py 可对照阅读。
小结
obsidian-terminal 用两个"小而全"的 Python 宿主解决了浏览器应用驱动终端的三个核心难题:
- 能力边界:PTY/ConPTY 系统调用交给 OS 子进程,浏览器端只传字节;
- 背压与死锁:Unix 侧用非阻塞 master + 有界输入缓冲,Windows 侧用管道泵 + TS 端高低水位,双向流控;
- 安全与可靠性:命名管道 token 认证、挂起启动 + Job 收容、三级信号终止,让"跨进程杀终端"和"防本地伪造指令"都有确定答案。
这套"前端渲染、TS 编排、Python 托管"的分工,对任何想在 Web 容器里做终端的项目都是可直接借鉴的完整范式。
【免费下载链接】obsidian-terminal
Integrate consoles, shells, and terminals.
相关推荐
集成终端是如何工作的?obsidian-terminal的xterm.js渲染与伪终端(PTY)架构完整解析
集成终端是如何工作的?obsidian terminal的xterm.js渲染与伪终端(PTY)架构完整解析 obsidian terminal 是一款 Obs
MiniTerm 深度解析:在 Windows Terminal 仓库中用 ConPTY 伪控制台 API 构建 C 迷你终端
MiniTerm 深度解析:在 Windows Terminal 仓库中用 ConPTY 伪控制台 API 构建 C 迷你终端 本文基于 Windows Ter
桌面应用CLIPty4J - Java实现的伪终端(PTY)库
Pty4J Java实现的伪终端 PTY 库 简介 Pty4J是一个官方JetBrains支持的开源项目,它为Java提供了一个功能强大的伪终端 PTY 实现。
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考