跳到正文
Deployboardv0.0.3

macOS · 一条命令 · 127.0.0.1

Launchd 没有 dashboard。这是在浏览器管理自己的 launchd services 的 console,同时输出 Prometheus metrics。

一条命令装好。浏览器里看状态、日志、告警。

$brew install --cask francistse/tap/deployboard
安装一条命令浏览器状态 · 日志 · 告警输出Prometheus /metrics
127.0.0.1:9410 · Deployboard
Deployboard
在浏览器管理自己的 launchd services
SystemLightDark⚙ 设置
16 开 · 2 关我的 16 · 其他 1 · 噪音 528写入模式http://127.0.0.1:9410
搜索标签…
全部 (545)运行中 (163)已排程 (2)已完成 (0)已停止 (239)错误 (139)离线 (1)已禁用 (1)
标签PID退出重启操作
示例应用3105 项
com.example.app.api我的已列入
730872h 14m028
🔔重启日志
com.example.app.web我的自动(路径)↻ 频繁重启
730916d 3h01207↻
🔔重启日志
com.example.app.collector我的自动(路径)
7310418m021
🔔重启日志
com.example.worker.nightly我的已列入
—13
🔔启动日志
com.example.backup.weekly我的自动(路径)下次 · Sun 03:00
—00
🔔启动日志
运维0012 项
com.example.worker.old我的已列入⏹ 已退役 12 天
——0
启用日志
com.example.legacy.agent我的自动(路径)
—00
启动日志
GET /api/eventstext/event-stream每 5 秒推送

点「我的」、「其他」或「噪音」。然后看 /metrics 和 Telegram 告警。

为什么

launchctl 列出一切,于是你什么也看不见。

原有的工具回答「加载了什么」,从不回答「我部署了什么,它是否健康」。按用户域来过滤也没用——一台正在使用的 Mac 上,看起来像「你的」任务,大多是 Dropbox、Google 的更新程序、Spotify、Tailscale,再加上一两个浏览器辅助程序。那个重启了 1,207 次的任务,就埋在正中间。

Deployboard 从一份明确的清单出发:你自己控制的允许列表,再加上路径推导——plist 的日志路径、工作目录、program 或参数只要落在你的项目树里,那就是你的任务,自动算进去。其余是系统自己的事,永远只隔一次点击,但不会挡在前面。

没有 Deployboard
  • launchctl list — 545 行,分不清哪些是你的
  • 悄悄重启了 1,207 次的开发服务器,看起来仍很健康
  • 你有意停用的 plist,一直被当成失败
  • 没有历史:「这算正常吗?」没有答案
  • 要等你偶然发现,才知道有东西已经死了
有了 Deployboard
  • 我的 / 其他 / 噪音 — 默认先显示你的部署
  • 来自 launchctl print 的 runs(重启次数),超过阈值就标出
  • disabled 是一种状态,不是错误 — 退役就是退役
  • Prometheus 的 /metrics,让 Grafana 能显示「从何时开始」
  • 状态变化时才发 Telegram 告警,按应用,带冷却时间

状态

七种状态,其中一种的意思是「我把它退役了」。

大多数工具把状态收成 running / stopped / error,于是你几个月前自己关掉的 plist,会永远看起来像失败。Deployboard 保留 launchd 真正区分的那些状态,并读取 launchctl print-disabled,让意图也成为答案的一部分。

运行中

PID > 0 — launchd print 也确认它在运行

已排程

PID = 0、正常退出、已定义计划、没有近期日志

已完成

PID = 0、正常退出、日志的 mtime 在 --recent-window 之内

已停止

PID = 0、正常退出、没有计划、没有近期日志

错误

上次退出状态非零

已禁用

launchctl print-disabled 显示 disabled,没有存活的 pid — 退役,不是故障

离线

存在 plist 文件,但 launchctl list 不返回该 label

这个 fork 加上的

上游回答「加载了什么」。这里回答「我部署了什么」。

四项新增,全部建在 launchctl 自己的输出上——不去猜一个任务属于哪个厂商。

01

用 launchctl print,而不是 launchctl list。

launchctl list 没有重启次数,也没有 disabled 标志。Deployboard 用 launchctl print 读取 runs 和 pid,用 launchctl print-disabled 读取意图——于是抖动看得见,有意退役的任务也不再看起来像坏了。

02

清单由你决定。

我的 / 其他 / 噪音,每一行都有来源(已列入、自动(路径)、未分类)。分类操作会写回 config.json,大约 2 秒内热重载——教它一个新应用,不必重启。

03

给 launchd 的 Prometheus 指标。

deployboard_job_up、_runs、_last_exit_code、_disabled,再加上 IPv4/IPv6 端口探测。仪表盘只能告诉你「现在停了」;时间序列能告诉你「03:10 开始抖动」。

04

可按应用开关的 Telegram 告警。

只在状态变化时通知——error、offline、端口无法连接、重启风暴,以及恢复——并有按应用的开关、冷却时间、免打扰时段。机器人令牌放在 macOS 钥匙串里,永远不写进文件。

上游仍然保留的部分
日志跟踪

每个任务的 stdout 与 stderr,最多 10,000 行,可实时筛选。

六项诊断

退出码、program 是否存在、program 是否可执行、plist 所有者、plist 权限、日志路径。

SSE,不是轮询

GET /api/events 每 5 秒推送完整的任务列表。

一键控制

经 launchctl 做 start / stop / reload,并有确认对话框。

JSON API

与界面用的是同一面,有文档,curl 也好用。

零依赖

一个 Go 二进制文件,前端以 go:embed 嵌入,没有守护进程,没有遥测。

指标

这个领域里,唯一的 launchd exporter。

把 Prometheus 指到界面用的同一个端口。标签基数有界,省略是诚实的(从未退出的任务没有 last-exit 序列),只有暴露了端口的任务才有 probe 序列——探测也会试 IPv6 的 ::1,因为有些开发服务器只绑在那里。

GET /metrics
# HELP deployboard_job_up 1 when the launchd job is running.
# TYPE deployboard_job_up gauge
deployboard_job_up{label="com.example.app.api",category="ours",group="Example App"} 1
deployboard_job_runs{label="com.example.app.web",category="ours",group="Example App"} 1207
deployboard_job_disabled{label="com.example.worker.old",category="ours"} 1
deployboard_job_probe_reachable{label="com.example.app.api",port="8001"} 1
deployboard_job_probe_latency_seconds{label="com.example.app.api",port="8001"} 0.003
deployboard_jobs_total{category="ours"} 16
deployboard_jobs_total{category="noise"} 528
promql
# restart churn, per job, over 6h
deriv(deployboard_job_runs{category="ours"}[6h]) * 3600
# anything of yours that is not running
deployboard_job_up{category="ours"} == 0

从提供界面的同一个进程抓取即可,不必再部署 exporter,也不需要 sidecar。没有的序列就省略,不会编造:从未退出的任务不会有 last_exit 行,没有端口的任务也不会有 probe 行。

告警

它在 Telegram 上告诉你,而且只说一次。

按状态变化来发,所以一个来回抖动的任务不能刷屏:坏掉时一条,若持续重启则一条受速率限制的,恢复时再一条。恢复消息永远不会被免打扰时段压掉。机器人令牌放在 macOS 钥匙串里,由仪表盘自己写入——永远不进文件,永远不进日志行,也永远不回到页面上。

✈telegram · deployboard
🔴 RUN_STORM
Example App · com.example.app.web
状态: 运行中
重启: 1207
2026-10-03 10:41:07 +0800
触发类型
状态变为 errorerror
有 plist,但未加载offline
端口不再响应unreachable
两轮扫描之间 runs +25run_storm
恢复为 runningrecovery
按应用的开关、冷却时间、免打扰时段,以及免打扰时段永远吞不掉的恢复消息。

API

界面用到的一切,你的脚本也能用。

Web 界面是 /api/jobs 和 /api/events 的一个消费者。curl 也是。绑定服务器,指向端点,解析 JSON。

方法路径说明
GET/api/inventory类别与分组计数,以及每个任务如何被分类
POST/api/inventory/classify将任务固定为我的 / 隐藏 / 回到自动
GET/metricsPrometheus 文本格式 — 任务、runs、退出码、探测
GET/api/jobs列出每个任务,包括离线的 plist
GET/api/jobs/{label}单个任务快照(status、pid、runs、disabled)
GET/api/alerts按应用的告警状态,以及已启用 / 已禁用计数
POST/api/alerts/{label}切换告警: {"enabled": false}
POST/api/alerts/{label}/test为该任务发送一条测试告警
GET/api/settings/telegram令牌已保存?有 chat id?——绝不返回令牌本身
GET/api/jobs/{label}/logs?lines=200跟踪 stdout 与 stderr(最多 10,000)
GET/api/eventsSSE 流 — 每 5 秒推送完整任务列表
POST/api/jobs/{label}/reloadlaunchctl bootout + bootstrap(只读模式返回 403)

安装

一条命令。一个配置文件。一个标签页。

三种安装方式、同一二进制:Homebrew cask(首屏命令)、不用 Go 也能装 LaunchAgent 的 curl install-release.sh、或从克隆运行 bash install.sh。不需要 sudo;脚本支持 --dry-run 和 --uninstall。

01 · 安装
$ brew install --cask francistse/tap/deployboard

用 Homebrew cask 装二进制,或用 install-release.sh / install.sh 一并写入带 RunAtLoad + KeepAlive 的 LaunchAgent。不需要 sudo;脚本支持 --dry-run 和 --uninstall。

02 · 指向你的应用
$ open ~/.config/deployboard/config.json

要么在 config.json 的 inventory.ours 下列出你的命名空间,要么把项目文件夹加进 derive_roots,让 plist 路径自己分类。未分类的会出现在「其他」——在界面点一下就固定,config.json 会热重载。

03 · 看着它,或抓取它

仪表盘在 127.0.0.1 打开。用 Prometheus 抓取 /metrics,用铃铛按应用开关告警,并在系统、浅色、深色之间切换主题。

7

状态值

上游六个 + disabled

2s

配置热重载

不必重启就能分类新应用

12

Prometheus 序列家族

任务、runs、退出码、探测

127.0.0.1

唯一绑定的地址

没有云、没有遥测、没有更新探测

FAQ

一个 fork 必须回答的问题。

包括它和上游是什么关系。

它和所 fork 的 launch-pilot 有什么不同?

上游做的事都还在——任务表、日志跟踪、六项诊断、start/stop/reload。Deployboard 加上清单视图(我的 / 其他 / 噪音,带路径推导)、launchctl print 的补充(runs 和 disabled)、Prometheus 端点、可按应用开关的 Telegram 告警、一条命令的 macOS 安装,以及只读模式。

它怎么知道哪些任务是我的?

两条规则,按顺序:先是你写下的允许列表(inventory.ours),然后是推导——如果 plist 的日志路径、工作目录、program 或参数落在你列于 derive_roots 的目录里,那就是你的任务。plist 指向的一切都是证据,所以没有猜测,也没有域名启发式。

如果它把一个任务判错了呢?

它会出现在「其他」,标成「未分类」,你在该行选择自动 / 我的 / 隐藏。这会写入 config.json,服务器大约两秒内热重载。没有东西会被悄悄藏起来:完整的 launchd 列表永远只隔一个标签。

这些东西会离开我的机器吗?

不会。HTTP 服务器只绑定 127.0.0.1,没有分析统计,也不会回传。整个项目唯一能打出去的,是你明确开启的 Telegram 消息——用的令牌存在你自己的钥匙串里。

可以只用指标、不要告警吗?

可以。告警默认对你自己的应用开启,对其余的关闭;可以按应用、按类别,或整个关掉。/metrics 和 JSON API 照样可用,而 --read-only 会拒绝每一次会改变状态的调用。

为什么它从不修改我的 plist?

有意为之。Deployboard 读取 launchd 状态,也可以请 launchctl 对一个 label 做 start、stop 或 reload,但不会改写磁盘上的 plist。如果你要的是 plist 编辑器,LaunchControl 是现成的付费选择;这个工具关心的是,你的部署正在做什么。