Documentation
¶
Overview ¶
launchd.go —— macOS 侧的服务托管实现。
边界:
- plist 里**不写 AbandonProcessGroup**。P1 探针(spec §7.1)实测:以 setsid 拉起的执行者能活过 launchctl kickstart -k 与 bootout,本就不需要它。 写上它等于给一条已被实测证伪的假设留下痕迹,下一个人会以为它是必需的
- plist 的 ProgramArguments 里**不带 --executor**(spec D5)
本文件是 noconsole_windows.go 的非 Windows 对应物:这些平台上不存在 「子进程弹控制台窗口」这回事,因此是空实现。
Package service 把 agentd 交给本平台的进程管理器托管。
职责:
- 生成服务单元(macOS 的 launchd plist / Linux 的 systemd unit)
- 安装、卸载、查询状态;安装后**复核服务真的起来了**
边界:
- 不下载、不判断版本、不读 handoff 的配置文件:要什么路径由调用方在 Spec 里给全
- 不负责重启策略之外的进程管理:拉起、崩溃重启都是管理器的事
- 三个平台各一个实现:launchd(macOS)/ systemd(Linux)/ schtasks(Windows)。 Windows 走计划任务而非 SCM 服务,理由见 windows.go 的文件头
systemd.go -- Linux 侧的服务托管实现。
边界:
- 写 /etc/systemd/system 需要 root。**无权限时必须明确提示「需要 sudo」** 而不是把 permission denied 扁平抛出(B45 的教训:真因只落在日志里等于没有)
- unit 里 KillMode=process 与 Restart=always 是硬要求,理由见各自注释
未真机验证:本仓库暂无 Linux 机器(spec §10)。本文件的正确性目前完全由 systemd_test.go 的内容断言守着,改动时务必同步维护那些断言。
windows.go —— Windows 侧的服务托管实现(Task Scheduler 计划任务)。
为什么是计划任务而不是 SCM 服务:executor 的凭据全挂在用户 profile 下, SCM 服务默认跑在 Session 0 / SYSTEM,%USERPROFILE% 会变,用户态认证链路也 会随之失效。计划任务保留用户身份,并与其它平台的单元托管模型一致。
边界:
- 不加 //go:build windows:靠 New() 的 runtime.GOOS switch 分发,确保 XML 内容能在 macOS/Linux 上单测。
- 单元走 XML 而不是命令行参数:IgnoreNew 是承重配置,只能用 XML 表达。
- 不做日志重定向:Task Scheduler 没有 StandardOutPath 式的能力,agentd 自己负责日志落盘。
Index ¶
Constants ¶
const LaunchdLabel = "dev.gosuper.handoff.agentd"
LaunchdLabel 是 macOS 上的 job 标签,同时也是 plist 的文件名主干。
const SystemdUnit = "handoff-agentd.service"
SystemdUnit 是 Linux 上的 unit 文件名。
const WindowsTaskName = "handoff-agentd"
WindowsTaskName 是计划任务的名字,同时也是 XML 文件名的主干。
Variables ¶
var ErrNotInstalled = errors.New("服务单元未安装")
ErrNotInstalled 是「单元没装」的哨兵错误。
Start / Stop / Restart 都不代为安装,一律用它包装返回。上层(CLI、桌面壳) 靠 errors.Is 区分「没装」与「装了但操作失败」:前者的处置是 handoff service install,后者是去查日志。
Functions ¶
func UnitReferences ¶ added in v0.3.0
UnitReferences 报告已注册的计划任务是否指向 exePath 这个二进制。
参数:
- log: 日志入口
- exePath: 调用方自己的可执行文件绝对路径(须先 EvalSymlinks)
返回:
- true 表示「本进程退出后,计划任务会把同一个二进制重新拉起」
- 第二个返回值是判否的理由原文,供调用方打日志;判是时为空
为什么问的是「任务指不指向我」而不是「谁把我拉起来的」:换版闸二真正 要的保证是「我 exit(0) 之后还有人把我拉回来」。schtasks 不像 systemd / launchd 那样给被拉起的进程注入任何环境变量,「谁拉起我」在 Windows 上 根本问不出来;而「任务在不在、指的是不是我」既问得出,又恰好是那个保证。
顺带挡住一个真实的坑:从别的目录跑一个 agentd(如临时工作树里的构建), 此时任务指向的是另一个二进制——换版会换掉没人运行的那个文件,upgrade 报成功而机器上跑的还是旧版。路径对不上就判否,正是闸二该拦的情形。
Types ¶
type Manager ¶
type Manager interface {
// Install 生成单元、写盘、加载、启动,并复核真的起来了。失败时回滚。
Install(spec Spec) error
// Start 启动一个**已安装**的单元,不改动单元定义本身。
//
// 与 Install 的分工是承重的:Install 负责「让单元存在并跑起来」,为此会
// 重写单元定义(Windows 上是删掉任务再重建);Start 只负责「让已存在的
// 单元跑起来」。把两者混为一谈的代价在 Windows 上最明显——每次换版都会
// 把计划任务删了重建,用户对任务定义的任何修改和任务历史一并消失。
//
// 单元没装时返回错误,**不代为安装**:调用方据此决定是否回落到 Install,
// 而不是让 Start 悄悄替 Install 干活——那样调用方就再也分不清这两种情形。
Start() error
// Stop 停止一个**已安装**的单元,并关掉自动拉起,直到显式 Start。
//
// 「关掉自动拉起」是承重的:三个平台都配了「退出就拉起」(launchd
// KeepAlive=true / systemd Restart=always / Windows 每分钟重复触发),
// 只杀进程在任何一个平台上都停不住。且这个「关掉」必须跨重启生效,
// 否则用户重启机器后会发现自己停掉的东西又回来了。
//
// 单元没装时返回包装了 ErrNotInstalled 的错误,**不代为安装**。
Stop() error
// Restart 重启一个**已安装**的单元,不改动单元定义本身。
//
// 语义与 systemctl restart 对齐:单元当前没在跑(含被 Stop 停住)时,
// Restart 等价于 Start——用户在 agentd 崩着的时候敲 restart,要的是
// 它起来,而不是一句「它没在跑」。
//
// 单元没装时返回包装了 ErrNotInstalled 的错误,**不代为安装**。
Restart() error
// Uninstall 停止并移除单元。单元本来就不在时返回 nil(幂等)。
Uninstall() error
// Status 查询状态。「没装」是正常答案,不是错误。
Status() (Status, error)
// Kind 返回管理器种类:"launchd" / "systemd" / "schtasks"。
Kind() string
// UnitPath 返回单元文件的落点路径。
UnitPath() (string, error)
}
Manager 是平台无关的服务托管接口。
type Spec ¶
Spec 描述「要托管的是哪个 agentd」。
字段说明:
- BinPath: handoff 可执行文件的**绝对路径**,调用方须先做 EvalSymlinks, 否则服务会指向一个 symlink,升级换掉链接目标后单元还指着旧的
- ConfigPath: 传给 agentd 的 --config
- LogPath: 管理器把 stdout/stderr 重定向到哪
type Status ¶
type Status struct {
Installed bool
Running bool
// Disabled 表示单元被显式停用(handoff service stop),自动拉起已关掉。
//
// 与「装了没跑」是两种状态,不能合并:前者的处置是 handoff service start,
// 后者的处置是查日志找崩溃原因。合成一个布尔,status 就会给出错误的
// 处置建议——把用户支去重装一个本来好好的单元。
Disabled bool
// Detail 是管理器原文的摘要,供排障。查不到时为空
Detail string
}
Status 是服务的当前状态。
Installed 与 Running 是两件事:单元装了但没跑(崩溃循环、被手动 stop) 是一个真实且常见的状态,合并成一个布尔会让用户看不出区别。