尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

no-mistakes Daemon 与 Worktree:后台守护进程的架构、生命周期管理与崩溃恢复实战指南

no-mistakes Daemon 与 Worktree:后台守护进程的架构、生命周期管理与崩溃恢复实战指南 no-mistakes Daemon 与 Worktree后台守护进程的架构、生命周期管理与崩溃恢复实战指南【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes本文基于 daemon.md 展开结合仓库中 internal/daemon 与 internal/cli/daemon_cmd.go 的源码实现系统讲解 no-mistakes 守护进程daemon为什么存在、如何被托管、如何管理 pipeline 运行与 worktree、如何处理并发推送、如何从崩溃中恢复以及完整的日志体系。读完你可以掌握 daemon 的全部生命周期操作启动/停止/重启/状态、理解单例锁与就绪语义、掌握崩溃恢复与日志排障的完整思路并知道在多大程度上可以放心地把运行长时间流水线这件事交给它。Daemon 为什么存在no-mistakes 的核心理念是让git push no-mistakes保持快速并且让 gate本地守门仓库在 shell 命令返回之后仍然能继续工作。这决定了守护进程必须存在Git 将推送交给本地 gate 仓库的 post-receive hookhook 通知 daemon 后立即退出push 命令本身不被长时间阻塞daemon 独占所有长尾工作创建 detached worktree、执行 pipeline、向 TUI 推送事件、持久化状态、清理残留以及崩溃恢复。在源码层面这个模型体现在 internal/cli/daemon_cmd.go 的notify-push隐藏命令上hook 通过no-mistakes daemon notify-push内部走ipc.MethodPushReceivedRPC把 gate 路径、ref、old/new SHA 以及--push-option传给 daemon随即返回。daemon 则通过 internal/daemon/daemon.go 中的RunManager接管后续一切NewRunManager(d, p, stepFactory)之后pipeline 执行、worktree 创建与清理、TUI 事件流都由这一个长驻进程统一拥有。托管服务与平台差异安装器installer倾向于把 daemon 安装为受管后台服务macOSper-userlaunchdagent产物形如~/Library/LaunchAgents/com.kunchenguid.no-mistakes.daemon.suffix.plistLinuxper-usersystemduser service形如~/.config/systemd/user/no-mistakes-daemon-suffix.serviceWindowsTask Scheduler 任务形如no-mistakes-daemon-suffix。命名隔离多安装不冲突这些服务标识不是固定值而是由NM_HOME派生一个短而稳定的后缀。从源码看internal/daemon/service.go 定义了三个基名常量const ( launchdServiceLabelBase com.kunchenguid.no-mistakes.daemon systemdServiceNameBase no-mistakes-daemon windowsTaskNameBase no-mistakes-daemon )实际标识由launchdServiceLabel/systemdServiceName/windowsTaskName拼接NM_HOME派生的后缀serviceInstanceSuffix得到。这样同一台机器上多个 no-mistakes 安装只要使用不同的NM_HOME根目录服务名、socket、锁、PID 文件就不会互相碰撞。最小环境下的网络可达性受管服务以最小环境启动只有HOME和一个精选的PATH。为了让 daemon 及其 spawn 的 agent例如claude --print能通过公司代理或本地 HTTP(S) 代理访问网络安装/刷新服务时会把代理变量烘焙进服务定义转发的变量包括HTTP_PROXY、HTTPS_PROXY、NO_PROXY、ALL_PROXY以及它们的全小写拼写http_proxy等——因为工具对大小写读取不一致curl 的普通 HTTP 请求只认小写http_proxy一旦烘焙进服务定义之后例行daemon restart或二进制升级都不会剥离它们只有当你想修改/移除代理时才需要重新导出变量由于代理 URL 可能内嵌凭据如http://user:passhost只要向服务文件转发了代理值生成的服务文件就限定为属主可读的0600权限未设置代理变量时保持常规0644。Windows 的 Task Scheduler 直接继承登录环境无需转发。相关实现见 internal/daemon/service.go 的proxyEnvKeys与 internal/daemon/service_render.go 的服务定义渲染/漂移检测逻辑。登录 shell 环境解析daemon 启动时macOS/Linux会从你的登录 shell 解析环境保留 shell 的PATH顺序并追加缺失的常见目录~/.local/bin、~/go/bin、~/.cargo/bin、~/bin、/opt/homebrew/bin、/usr/local/bin、/usr/bin、/bin。若登录 shell 解析失败则退化为增强的进程环境回退可能丢失 nvm/fnm/volta 等版本管理器目录并在日志中告警。Windows 直接复用当前进程环境。因此如果你的环境变量没有写进登录 shell 的 rc 文件.zprofile、.zshrc、.profile、.bash_profile、.bashrc或 PowerShell profiledaemon 是看不到的。把它们放到登录 shell 会加载的位置然后重启 daemon 使其生效。NM_HOME下的环境变量布局可参考 environment.md全局配置、gate 仓库、worktrees、日志、数据库、socket/PID/锁、servers PID 记录、eval 目录全部随NM_HOME迁移。回退detached 进程如果受管服务的安装或启动不可用、失败no-mistakes会回退为启动一个分离的detacheddaemon 进程。这条回退路径贯穿daemon start、update以及所有确保 daemon 运行的命令。启动与停止绝大多数用户不需要直接管理 daemon——no-mistakes、init、attach、rerun、update等常用命令在需要时都会确保服务已安装并运行。显式管理的命令如下# 显式管理 no-mistakes daemon start no-mistakes daemon stop no-mistakes daemon restart no-mistakes daemon status # 确保 daemon 运行尽量走受管服务 no-mistakes no-mistakes init no-mistakes attach no-mistakes rerun no-mistakes axi run no-mistakes axi respond # 替换二进制后重置 daemon no-mistakes update其中daemon status在 internal/cli/daemon_cmd.go 中实现为通过daemon.IsRunning探测运行中会输出● daemon running (pid NNNN)未运行则输出○ daemon not running。update 的重启守护no-mistakes update在 daemon 正在运行或存在陈旧 daemon 产物时会先停止再启动 daemon确保新可执行文件被使用。它优先走受管服务路径服务启动不可用/失败时回退到 detached daemon。但存在一个关键保护如果有 pending 或 running 状态的 pipeline 运行update默认拒绝重启 daemon并打印每个活跃运行的 ID、状态、分支和短 head SHA。相关守卫函数是 internal/cli/daemon_cmd.go 的guardDestructiveDaemonLifecyclefunc guardDestructiveDaemonLifecycle(p *paths.Paths, stderr io.Writer, action string, force bool) error { runs, err : lifecycle.ActiveRuns(p) if err ! nil { return fmt.Errorf(check active pipeline runs: %w, err) } if len(runs) 0 { return nil } if force { // 打印警告与活跃运行列表继续执行 return nil } return fmt.Errorf(refusing %s because %d active pipeline runs are in progress; pass --force to stop/restart the daemon anyway\n%s, action, len(runs), lifecycle.RunList(runs)) }要点想无视活跃运行强推重启必须显式传--forcedaemon stop、daemon restart各有一个自己的--force并自行接受这些运行可能失败-y/--yes不会绕过这个守卫若 daemon 已从不同的可执行路径运行update仍会先提示再替换-y/--yes只用于非交互地回答该提示若无法确定 daemon 可执行路径update在替换任何东西之前直接中止。递归遏制验证链中的 agent 无权动 daemon--force覆盖仅对普通顶层调用者开放。一个由活跃验证步骤 agent 派生出来的进程不能 start/stop/restart/update daemon——递归遏制recursive containment会在任何生命周期变更之前拒绝命令且没有--force或--yes可以绕过。这是为了防止 pipeline 内部 agent 自我管理守护进程造成的死锁或误操作。可审计谁动了 daemon每一次daemon stop、daemon restart或update调用无论是否--force都会把调用者的 PID、父 PID 和父进程命令行追加记录到~/.no-mistakes/logs/cli.log事后发生事故时能定位是哪个 agent 或进程触发的。进程启动与就绪是两回事daemon 在~/.no-mistakes下留下三类运行期工件PID 文件~/.no-mistakes/daemon.pid写入身份记录PID 启动时间供daemon status展示IPC 端点Unix 域 socket~/.no-mistakes/socketWindows 上为 localhost TCP 监听 同路径的受保护端点文件单例锁~/.no-mistakes/daemon.lock详见下一节。连接超时别让卡死的进程挂住调用者CLI 客户端等待 socket 接受连接的时间由全局配置daemon_connect_timeout约束默认3s可被NM_DAEMON_CONNECT_TIMEOUT环境变量覆盖。即使 daemon 进程活着但卡住连接也会在超时后失败而不是无限挂起。NM_DAEMON_CONNECT_TIMEOUT接受任何正的 Go duration 字符串优先级高于config.yaml为空、不可解析或非正值时忽略并回退到配置值。排障可参考 troubleshooting.md 中关于陈旧产物stale artifacts的检查。死 socketfail fast 而非静默换新确保 daemon 运行的命令no-mistakes、init、attach、rerun、axi run、axi respond在 socket 文件存在但无人应答时例如非正常退出留下的死 socket会快速失败而不是静默启动一个替代 daemon。唯一的自愈入口是no-mistakes daemon start。完全停止交接complete-stop handoff收到关闭请求后daemon stop会等待 daemon 进程本身退出才返回成功。仅有 IPC 健康丢失是不够的——listener 在关闭开始时就已关闭而单例锁等进程级资源要到进程退出时才释放。daemon restart采用同样的完全停止交接再启动替代进程从而保证新旧进程不会争抢同一个根目录root。就绪语义与启动窗口从源码 internal/daemon/daemon.go 的runWithOptionsLocked可见完整的启动顺序prepareDaemonEnvironment登录 shell 环境→ 加载全局配置 → 打开数据库 → 获取单例锁 → 发布 PID 文件 → 执行排他的崩溃恢复recoverOnStartup→ 绑定 IPC socket → 通过confirmLocalIPCHealth确认健康后才记录daemon ready。关键点取得单例锁后、排他恢复开始前daemon 就发布 PID——但启动成功要以 IPC server 返回真实健康响应为准daemon start给冷环境准备与恢复留出最多45 秒子进程在就绪前退出会被及时报告PID 文件存在或 socket 已绑定都不算就绪证明detached 启动超时命令会杀死并 reap 该子进程后再返回受管启动失败先清理受管尝试再试 detached 回退两条路径都失败时保留两个错误。单例锁一个 NM_HOME 只能有一个活 daemon同一时刻一个NM_HOME只能有一个活 daemon 持有。实现见 internal/daemon/lock.go启动时崩溃恢复运行前、socket 绑定前对~/.no-mistakes/daemon.lock获取排他的 OS 文件锁持有到进程结束第二个 daemon 试图针对同一 root 启动时会以a no-mistakes daemon is already running for this NM_HOME失败附持有者的 PID 与启动时间而不会偷走第一个 daemon 的 socket、也不会对它的活运行执行崩溃恢复该锁由 OS 内核在进程退出/崩溃时自动释放即使 SIGKILL因此永远不会陈旧——这是它优于 PID 文件的地方作为独立的安全层daemon 还拒绝在仍有进程应答的 socket 上重复绑定只有可证明陈旧的 socket 文件无监听者才会被移除并重新绑定。锁文件里还会写入诊断记录lockHolderRecordPID StartedAt供被拒的第二个 daemon 报告持有者。Daemon 的核心职责push 经 post-receive hook 到达后daemon 按以下流程执行在~/.no-mistakes/worktrees/repoID/runID/创建 detached worktree若全局配置worktree_roots为该仓库指定了目录则创建在root/runID。放置位置在运行创建时一次性解析并记录在运行上之后编辑该设置不会重定向已存在的运行在该 worktree 内启动 pipeline executor向所有已连接的 TUI 客户端流式推送事件并向 AXI 客户端提供请求/响应状态运行结束成功或失败时清理 worktree遵守下述保留规则。worktree_roots 的语义与限制默认情况下运行 worktree 放在NM_HOME之外、所有 checkout 之外因此 mise、direnv 等按路径祖先解析的目录级工具链配置不会渗透进来。把 checkout 指向自有目录后其运行会创建在value/runID继承该目录的配置。细节要点见 global-config.md 的worktree_roots一节相对值在加载时被拒绝daemon 的工作目录与配置无关目录归你所有no-mistakes 从不枚举它只操作运行记录点名的确切目录清理、孤儿进程 reap、no-mistakes eject都以此为准每个 checkout 需要自己的 root两个条目指向同一目录、同一 checkout 的两种拼写、root 等于 checkout 自身都会在加载时被拒绝两个 daemon 启动时也无法工作的值会被拒绝位于NM_HOME内与 no-mistakes 自身状态冲突如 worktree 与日志目录混叠以及位于任何 checkout 内运行期间 checkout 变脏分支同步会拒绝移动它修改条目只影响新运行每个运行记录其创建目录重启后续跑、读 diff、清理、reap、eject 都沿用该运行实际所在的目录no-mistakes init --worktree-root dir会打印应添加到全局配置的确切条目全局配置需手工维护init 不会替你改写。protected_paths 拒绝时的保留当仓库配置protected_paths存在未解决的拒绝refusal时daemon 会跨 shutdown、被更新 push 取消以及崩溃恢复保留 index 与工作文件——包括 trusted-config 加载失败导致运行无法恢复的情形。但该保留不削弱恢复校验也不会让终止的运行保持活跃孤儿进程清理与测试证据过期仍然适用。被拒绝步骤成功完成后保留解除操作员显式 skip 与 abort 维持既有清理行为。有界事件交付慢客户端拖不垮运行事件投递是有界的慢或卡死的客户端永远不会拖住运行。压力下 daemon 可能丢弃普通日志输出但绝不会静默丢失状态变更——它会把这些合并成一个 gap 信号TUI 与axi收到后重新读取权威运行状态。因此实时视图落后时会跳过日志行但会收敛到运行的真实状态。连接断开后TUI 以有界延迟重试并在重连时对账若 daemon 持续不可用则表面化连接错误而不是无限重试。提示引导与子进程清理pipeline agent 被提示prompted把有意的写入保持在 detached worktree 内避免改动系统级状态Homebrew 包、/Applications下的应用、全局工具配置。这能减少意外的机器级副作用与 macOS App Management 弹窗但它是提示引导prompt steering不是真正的沙箱。执行步骤期间daemon 拥有子进程清理职责配置的命令与一次性 agent 子进程在完成/失败/取消时作为进程树整体终止避免泄漏的测试 worker、build watcher 或 dev server 跨运行累积每个进程先被请求退出仅在数秒后仍在运行时才强制 kill进程仍可通过把自己脱离到独立 session 逃逸该树因此运行结束时 daemon 还会终止该运行 worktree 内仍在存活的任何进程再移除目录该清扫按工作目录界定范围绝不触碰运行仍活跃的 worktree也绝不可能触达在~/.no-mistakes/worktrees/之外、或不在运行记录点名的 worktree root 中工作的进程。并发 push 处理同一分支上已有活跃运行时的再次 pushdaemon 的行为是取消进行中的运行原因cancelled: superseded by new push等待其结束用最新 push 启动新运行。不同分支的 push 则并发运行。这正是 daemon 存在的另一原因分支级协调在一个长驻进程内比在相互独立的 hook 调用之间更容易推理正确。崩溃恢复daemon 启动时会检查被留在pending或running状态的运行意味着 daemon 崩溃时它们正在活跃恢复流程如下源码入口 internal/daemon/daemon.go 的recoverOnStartup按顺序调用reapOrphanedServers→migrateGateConfigs→ReconcileTerminalPRRuns→recoverableParkedRuns→RecoverStaleRunsExcept等终止型 PR 运行先完成持久化 PR 状态已为merged或closed的遗留活跃行含其 CI 步骤再做活跃运行恢复与 parked 运行规划parked 审批门只恢复可完整验证的已记录 approval gateworktree 与步骤历史可验证不完整或歧义的活跃运行 fail closedforge 配置重新校验重建被恢复运行前重新解析并校验已配置的仓库 forge profile使恢复后的 provider 检查与 agent 使用与仓库绑定的身份模型而不是持久化的凭据或环境里的活跃账号parked CI 门复查恢复前通过配置的 provider 重新检查持久化 PR URL——当前已 merged/closed 的 PR 完成这个陈旧 gateopen、未知或不可达的 PR 保持 parked。protected_paths拒绝异常 阻止自动对账ci_monitor_interrupted正在监控已创建 PR 的 CI 的运行被保留为ci_monitor_interrupted而非判失败——PR 仍然 open重启中断不算 pipeline 失败。该运行是终止态永不恢复head 固定与恢复 ref在失败任何其他陈旧活跃运行之前校验其受管 worktree head并把未发布的 descendant 钉在运行专属的恢复 ref 下避免后续 rerun 或受管 custody 恢复落到陈旧 gate 分支其余陈旧运行标记为failed消息 daemon crashed during execution孤儿 managed agent serverreap 崩溃 daemon 或 setup wizard 遗留的受管 agent serverworktree 内残留进程终止崩溃 daemon 留在已无运行拥有的 worktree 中的进程使用与运行清理相同的工作目录界定外加十分钟年龄下限——确保与启动并发开始的运行不会被误判为泄漏见orphanProcessMinAge其默认值即procreap.DefaultMinAge孤儿 worktree 目录通过git worktree remove --force移除——但绝不移除运行仍为pending/running的目录也不移除存在未解决 refusal 需要保留的目录。~/.no-mistakes/worktrees/下清理终态运行的遗留物与无运行记录的目录配置的 worktree root 中只清扫/移除运行记录点名的目录。ci_monitor_interrupted的 worktree 在 checkout 的 commit 与运行最后推送的 commit 不同时也会保留可能含有未推送的 CI auto-fix commitgate 迁移迁移权威仓库记录点名的 gate加上严格的repoID.git形状的遗留目录。变更未打戳候选前先验证目录是裸仓库不依赖当前目录或祖先 Git 发现无关与畸形目录被拒绝不做 hook 或 Git 变更。对验证过的遗留 gate安装/刷新 no-mistakes 管理的 pre-receive 准入与 post-receive 通知 hook自定义 pre-receive hook 保留在准入包装之后启用 push-option 支持并重放 per-worktree hook-path 隔离配置戳stamp只在整体迁移成功后记录内容版本化的 gate 配置戳正常重启时直接从文件系统检查已打戳的 gate不再重跑会变更 Git 的命令清除 parked-awaiting-agent 标记使恢复后的失败运行不再显示为仍在等待axi respond。日志体系日志文件内容轮转策略~/.no-mistakes/logs/daemon.logdaemon 生命周期日志当前 32 MiB 3 个备份~/.no-mistakes/logs/managed-server.log受管 Rovo Dev 与 OpenCode server 的 stdout/stderr当前 16 MiB 2 个备份~/.no-mistakes/logs/daemon-bootstrap.loglifecycle logger 就绪前的输出与直接崩溃输出当前 1 MiB 2 个备份~/.no-mistakes/logs/wizard-agent.logsetup wizard 捕获的受管 agent-server 输出—~/.no-mistakes/logs/runID/step.log每个 pipeline step 的输出—~/.no-mistakes/logs/cli.logdaemon stop/restart/update调用者审计PID、父 PID、父命令行—备份使用.1作为最新保留文件。细节说明daemon.log 的启动日志记录简洁的阶段耗时、gate 迁移计数并且只有在 IPC 健康确认成功后才会出现最终的daemon ready消息只读 IPC 请求health、运行状态读取只在debug级别出现变更操作、流启动、生命周期迁移与失败请求保持在info/warn可见每个 pipeline step 写~/.no-mistakes/logs/runID/step.log致命步骤错误也会追加进去——即使细节来自命令 stderrstep 日志也包含失败原因日志级别由全局配置控制log_level: debug # debug | info | warn | error默认info取值为debug、info、warn、error见 global-config.md 的log_level一节。internal/daemon/daemon.go 的initLogger会先用info初始化 lifecycle handler再在加载全局配置后用配置级别重设确保启动诊断始终保留。关闭流程no-mistakes daemon stop停止当前 daemon 进程但不卸载受管服务。下一次no-mistakes daemon start、no-mistakes、init、attach、rerun或update会通过同一个服务管理器再次启动它若服务不可用则启动 detached daemon。关闭步骤取消所有活跃运行等待 goroutine 结束上限 30 秒移除 PID 文件与 socket。关闭前受活跃运行守卫保护有 pending/running 运行时代默认拒绝仅顶层调用者可传--force强行通过而验证步骤 agent 派生的进程被递归遏制规则直接拒绝internal/cli/daemon_cmd.go 的guardDestructiveDaemonLifecycle是这一守卫的实现。同时每次 stop/restart 的调用者身份都会写入cli.log保证整个关闭链路可审计、可回溯。小结no-mistakes 的 daemon 设计围绕push 要快、运行要稳、崩溃要能恢复三个目标展开hook 与 daemon 通过 IPC 解耦daemon 用 OS 单例锁与 socket 绑定双重防护保证单一活实例用 PID 发布与 IPC 健康检查分离进程启动与就绪两个状态用受管服务 detached 回退双路径保障跨 CLI 调用的可用性并用一套分文件、分策略轮转的日志体系让每次生命周期变更与每次崩溃恢复都可观测、可审计。理解这些机制后无论是日常daemon start/stop/status管理、update时的活跃运行守卫还是排查daemon crashed during execution类型的恢复痕迹你都能从文档与源码两个层面找到确切依据。【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表