总体概览
这三个入口都通过同一条 Tailscale 隧道到达 Box,最终都落到同一批驱动程序上。但在 Box 内部,它们走的路径不同。 通过 Box API 驱动 Net 的命令直接 POST 到端口 9000,lager supply 就是其中之一。
lager python 则把脚本上传到端口 5000 上的执行服务。该服务把脚本作为子进程运行,子进程可以完整访问 lager.* 硬件库。
CI 运行器就是一台临时的开发者计算机。它使用与所运行命令相同的路径。
术语
Lager Box 内部结构
lager 的 Docker 容器(用 --restart always 启动)运行全部服务。这些服务是平级的进程,而不是一条流水线。一个启动脚本逐个启动它们,并在它们退出时重启它们。
其中两方会调用端口 8080 上的硬件服务。第一方是端口 9000 上的 Box API。第二方是执行服务启动的每个用户脚本。两者都在自己的进程中解析 Net 名称,然后 POST 到 /invoke。调试服务和 MCP 服务是独立的。
硬件服务是仪器 VISA 会话的唯一拥有者。自行打开会话的调用方会与它争抢该 USB 设备。
只有当仪器不是 VISA 仪器时,脚本才会直接驱动它。直连驱动包括 LabJack、USB-202、
FT232H、Aardvark、Joulescope 和 PPK2。电源、示波器、电池模拟器、电子负载和太阳能模拟器与其他调用方一样,都经过 /invoke。
每个进程各自持有自己的
NetsCache。 它是每个解释器一个的单例,而不是整台 Box 一个。
Box API、硬件服务、调试服务和 MCP 服务各持有一个,每个 lager python 子进程也是如此。每份副本都读取 saved_nets.json,并根据该文件的 mtime 失效。因此这些副本会自行收敛。由此产生三个后果:每个进程都要自己承担第一次读取的开销;在一次写入和下一次读取之间,两个进程可能短暂地不一致;一个崩溃并重启的服务会带着冷缓存回来。端口一览
是否暴露这一列描述的是发布端口的 Box,这是默认行为。用
start_box.sh --no-publish
(或 LAGER_NO_PUBLISH=1)启动的 Box 不发布其中任何端口。所有服务仍然在容器内监听,
lagernet Docker 网络仍然可以访问它们,主机端口则由反向代理拥有。此时没有任何标记为
“是” 的行会在 <box-ip>:<port> 上响应。端口 22 是例外。SSH 是主机自己的守护进程,而不是容器发布的端口,因此 --no-publish
对它没有影响。设置了 lager box-config network-mode host 的 Box 同样不发布端口。在那种模式下,容器直接在主机上绑定端口,由主机防火墙管辖它们。为什么有两个 HTTP 端口
Box 在:9000 和 :5000 上都响应,这个划分是历史原因造成的,而不是功能上的区别。
:9000 是 Box API,也是主要端口。Net 的元数据、仪器发现、Box 锁定、文件下载和版本报告都走这里。
:5000 是更早的脚本上传路径。lager python 仍然用它把脚本发送到 Box,以及停止正在运行的脚本。不会再往它上面添加新功能。
有些状态在两个端口上都能查到。锁状态就是其中之一:Box 在每个服务器上都暴露它,而 CLI 从
:9000 读取。
请让这两个端口都能通过您的 VPN 访问。只发布 :9000 的 Box 能响应 lager nets 和
lager hello,但 lager python 会失败。
可选的控制平面集成
Lager Box 会从一个密钥目录/etc/lager/authorized_keys.d/ 发布 SSH 密钥。因此,外部控制平面可以在无人手动输入 SSH 命令的情况下开通访问权限。把一个 <name>.pub 文件放进去,该密钥会在大约五秒内进入 Box 账户的 ~/.ssh/authorized_keys。Box 把 /etc/lager
绑定挂载到运行时容器中,因此控制平面可以从容器内部写入该文件。它就是这样在还没有任何 SSH
访问权限时完成自举的。
start_box.sh 只拥有 authorized_keys 中位于它的 # BEGIN LAGER MANAGED KEYS 和
# END LAGER MANAGED KEYS 标记之间的区域。它每一轮都会根据密钥目录重建该区域。由此产生两个后果:
-
删除一个
.pub就是吊销该密钥。 没有别的方法能做到;在标记区域内手动编辑authorized_keys,会在下一轮被撤销。 -
通过其他方式安装的密钥不受影响。
ssh-copy-id和 cloud-init 追加在标记区域之外,start_box.sh会原样保留这些行。任何其他管理该文件的系统都必须使用自己独立的一对标记。共用同一对标记的两个管理者,每一轮都会重建对方的区域。 -
被保留不等于持久。
start_box.sh保护零散的行不被它自己的重建删除,但它无法保护那一行不被别人删除。 第二个密钥管理者会根据它自己的来源重建authorized_keys。它只保留自己的标记区域,因此会丢弃所有零散的行。随后start_box.sh只会根据密钥目录重建它自己的区域。从未进入过该目录的密钥不会回来。 因此,lager ssh-setup、lager update和lager install两件事都做:它们追加公钥,同时把它以lager-box-<user>-<host>.pub的名称写入密钥目录。任何希望所装密钥能够留存的工具,都必须这样做。
Net 抽象
Net 是把 CLI 命令与物理硬件细节解耦的核心抽象。psu1 背后的记录是这样的:
psu1 的命令都继续有效。
受支持的 Net 类型
执行流程
CLI 命令的执行
lager supply psu1 voltage 3.3 --yes 的逐步数据路径:
硬件服务拥有并缓存每台物理设备的驱动程序。它在按设备的锁下串行化访问。因此,对 Box API 的并发请求不会在同一台仪器上交错进行 I/O。
自定义脚本的执行(lager python)
lager python 命令把用户编写的 Python 脚本上传到端口 5000 上的执行服务。这条路径与上面的 Box API 命令不同。该服务把脚本作为独立的子进程运行,子进程有自己的解释器和自己的缓存。
接下来会发生什么,取决于仪器的类型。VISA 仪器会经过 Box API 使用的同一个 /invoke 代理,这包括电源、示波器、电池模拟器、电子负载和太阳能模拟器。其他仪器都在子进程内部构造和驱动:
LabJack、USB-202、FT232H、Aardvark、Joulescope 和 PPK2。
这些直连 USB 的驱动程序会独占地占用它们的设备。因此,执行服务会先请硬件服务释放它自己的占用。它故意让共享的 VISA 会话保持打开。拆掉那些会话,正是下一条电源命令出现
[Errno 16] Resource busy 的原因。
lager.* 硬件库。Box 会实时把它的输出回传给您。

