Skip to content

node stop refuses to stop its own daemon on macOS when the system language is not English #519

Description

@sunchengzhu

English | 中文

Summary

On macOS with a non-English system language (LANG=zh_CN.UTF-8 in this report), offckb node stop exits 1 and refuses to signal the daemon it started itself: verifyDaemonIdentity() reads the process start time with ps -o lstart= and parses it with an English-only pattern, so a localized line is treated as "not our daemon". The daemon keeps running, so clean is blocked as well. @offckb/cli@0.4.13 stops the same daemon normally on this machine, so this is a regression introduced with the Fiber work (#483), the change that added this start-time check. Workaround: run the stop command with LC_ALL=C.

What I ran into

On a Mac whose system language is Chinese, OffCKB 0.5.0-canary-ee0ad6b starts the devnet daemon normally but cannot stop it. node stop exits with code 1 and refuses to signal the daemon it started itself:

{"ok":false,"code":"COMMAND_FAILED","message":"Process 93563 does not appear to be the offckb daemon. Refusing to send signals to avoid killing an unrelated process. If you are sure this is the daemon, stop it manually and remove .../devnet/data/logs/daemon.pid."}

Paths in the quoted output are abbreviated with ....

The same command with LC_ALL=C succeeds immediately and reports {"ok":true,"command":"node.stop","stopped":true,"pid":93563}. The only difference between the two runs is the locale, so the daemon identity check is reading localized output and failing closed.

Reproduction

Run the published canary (0.5.0-canary-ee0ad6b) from npm in a fresh, isolated user directory. The second and third stop commands target the same daemon process; only the locale of the stop command changes.

case_root=$(mktemp -d)
mkdir -p "$case_root/home"
export HOME="$case_root/home"
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_CACHE_HOME="$HOME/.cache"
export XDG_STATE_HOME="$HOME/.local/state"
export LANG=zh_CN.UTF-8
unset LC_ALL LC_TIME

offckb --json node --daemon --binary-path /absolute/path/to/ckb-0.208.0/ckb
offckb --json node stop
LC_ALL=C offckb --json node stop

Observed: the daemon starts (exit 0), the first node stop exits 1 with empty stdout, and the second node stop under LC_ALL=C exits 0 for that same PID.

$ node --version
v22.23.2
$ offckb --version
0.5.0-canary-ee0ad6b
$ locale
LANG="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_CTYPE="zh_CN.UTF-8"

$ ps -p <pid> -o lstart=          # what OffCKB reads with the inherited locale
五  9月/25 20:28:43 2026

$ LC_ALL=C ps -p <pid> -o lstart=
Fri Sep 25 20:28:43 2026

Cause

verifyDaemonIdentity() (src/util/daemon.ts L425-L455) compares the daemon's command line and its process start time. On macOS the start time comes from ps:

The product then refuses to signal the process.

Why this could affect normal use

  • Reachable on a normal upgrade. The daemon starts, so the failure appears only later, when the user tries to stop or clean up, and the environment stays running until the command is run under a different locale. The error message's suggestion — stop the process manually, then remove the PID file — asks the user to identify and kill OffCKB's own daemon by hand.
  • It blocks the standard stop path. node stop is the stop command that fails here; the error message instead asks the user to stop the process manually. fiber stop performs the same identity check with Fiber-specific wording (src/fiber/daemon.ts L309-L312), and fiber status uses it to decide the offckbManaged field — both are code-path expectations that I did not reproduce.
  • Cleanup is blocked as a consequence, not by the same check. clean refuses while the CKB daemon is running (src/cmd/clean.ts L18-L20); inside a running Fiber environment, fiber clean likewise requires the Fiber nodes to be stopped first (src/fiber/clean.ts). That refusal is correct behaviour — the problem is the stop they depend on. Only the plain-CKB path was exercised here.
  • Platform scope. macOS is affected through the ps path. On Linux the code reads /proc first, which avoids ps entirely; if that read fails it falls back to ps, so Linux is not categorically safe. I could not test Linux, so this is an expectation rather than a reproduced result.
  • Other locales with the same mechanism. Any locale that makes ps print a non-English lstart should hit the same path; I only reproduced it with the Chinese locale.

This is a regression from 0.4.13

I ran the same start/stop sequence with @offckb/cli@0.4.13 on this machine, under the same Chinese locale, and then compared the two builds:

  • @offckb/cli@0.4.13: node --daemon exits 0, and node stop exits 0 with {"ok":true,"command":"node.stop","stopped":true,"pid":98610}. Its verifyDaemonIdentity() only inspects the process command line through ps -o args=; it reads no start time at all, and lstart does not appear anywhere in that build.
  • 0.5.0-canary-ee0ad6b: the start-time comparison and the parsePsLstart regex are new, and this is what fails on a localized ps.

The difference between the two versions is the new check, so this is a regression introduced with the Fiber work rather than long-standing behaviour.

Each version ran in its own fresh temporary HOME/XDG pair, so the two PIDs above come from separate environments. Both runs used the same machine, the same CKB 0.208.0 binary and the same inherited locale:

# @offckb/cli@0.4.13
$ node .../offckb-0.4.13/build/index.js --version
0.4.13
$ ps -p $$ -o lstart=        # output format with the inherited locale
五  9月/25 20:40:55 2026
$ grep -c lstart build/index.js
0
$ node .../build/index.js --json node --daemon --binary-path <ckb>   -> exit 0
$ node .../build/index.js --json node stop                           -> exit 0
  {"ok":true,"command":"node.stop","stopped":true,"pid":98610}

# 0.5.0-canary-ee0ad6b, same machine and locale
$ node .../build/index.js --json node --daemon --binary-path <ckb>   -> exit 0
$ node .../build/index.js --json node stop                           -> exit 1, stdout empty
  {"ok":false,"code":"COMMAND_FAILED","message":"Process 93563 does not appear to be the offckb daemon. ..."}
$ LC_ALL=C node .../build/index.js --json node stop                  -> exit 0
  {"ok":true,"command":"node.stop","stopped":true,"pid":93563}

What I would expect

  • Stopping or cleaning a devnet that OffCKB itself started should work regardless of the operating system language.
  • Keep the start-time identity check: it is what prevents signaling an unrelated process that reused the daemon's PID, and skipping it when the time cannot be read would weaken that protection. The fix belongs in how the time is read: run the ps child with a fixed locale, for example env: { ...process.env, LC_ALL: 'C' }, so lstart can be parsed consistently. If the probe still fails, report that the process identity could not be verified and signal nothing — "cannot verify" may shape the error message, but it should not become a condition that allows stopping.
  • While preparing this report I checked whether ps -o etimes= (elapsed seconds) could replace lstart with a locale-independent value. On macOS that keyword does not exist (ps: etimes: keyword not found; the manual lists etime), so I removed that suggestion.

Environment

  • @offckb/cli@0.5.0-canary-ee0ad6b, installed from npm and checked with offckb --version.
  • macOS arm64; Node.js 22.23.2; the stop command inherited LANG=zh_CN.UTF-8 (with LC_ALL and LC_TIME unset), which is what the reproduction uses.
  • CKB 0.208.0; separate temporary HOME/XDG directories for these checks, no developer data involved.

I can share the raw command output, ps samples or daemon logs for either version if that helps.

Source references

Supporting source code in the tested canary

The comparison against 0.4.13 was made on the published npm tarballs (cli-0.4.13.tgz and the canary tarball), not on a local build.


中文版

系统语言不是英文时,macOS 上的 node stop 会拒绝停止它自己启动的守护进程

摘要

在系统语言不是英文的 macOS 上(本报告为 LANG=zh_CN.UTF-8),offckb node stop 退出码为 1,并拒绝给它自己启动的守护进程发信号:verifyDaemonIdentity() 用 ps -o lstart= 读取进程启动时间,再用只认英文的正则解析,于是本地化的输出被当成"这不是我们的 daemon"。daemon 因此一直运行,clean 也因它而按设计拒绝执行。同一台机器上,@offckb/cli@0.4.13 可以正常停止同一个 daemon,所以这是随 Fiber 功能(#483,即引入该启动时间校验的改动)引入的回归。临时绕过办法:停止命令前加 LC_ALL=C。

我遇到的情况

在系统语言为中文的 Mac 上,OffCKB 0.5.0-canary-ee0ad6b 能正常启动 devnet daemon,却无法停止它。node stop 退出码为 1,并且拒绝给它自己启动的 daemon 发信号:

{"ok":false,"code":"COMMAND_FAILED","message":"Process 93563 does not appear to be the offckb daemon. Refusing to send signals to avoid killing an unrelated process. If you are sure this is the daemon, stop it manually and remove .../devnet/data/logs/daemon.pid."}

引用的输出中,路径已用 ... 缩写。

同一条命令加上 LC_ALL=C 立刻成功,返回 {"ok":true,"command":"node.stop","stopped":true,"pid":93563}。两次运行唯一的差别就是 locale,说明守护进程身份校验读到了本地化输出并因此判定失败。

复现步骤

使用 npm 上发布的 canary(0.5.0-canary-ee0ad6b),在全新的隔离用户目录中执行。第二条和第三条停止命令针对同一个 daemon 进程,只有停止命令的 locale 不同。

case_root=$(mktemp -d)
mkdir -p "$case_root/home"
export HOME="$case_root/home"
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_CACHE_HOME="$HOME/.cache"
export XDG_STATE_HOME="$HOME/.local/state"
export LANG=zh_CN.UTF-8
unset LC_ALL LC_TIME

offckb --json node --daemon --binary-path /absolute/path/to/ckb-0.208.0/ckb
offckb --json node stop
LC_ALL=C offckb --json node stop

实测结果:daemon 启动成功(退出码 0),第一条 node stop 退出码 1 且 stdout 为空,第二条加 LC_ALL=C 的 node stop 对同一个 PID 退出码 0。

$ node --version
v22.23.2
$ offckb --version
0.5.0-canary-ee0ad6b
$ locale
LANG="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_CTYPE="zh_CN.UTF-8"

$ ps -p <pid> -o lstart=          # OffCKB 继承 locale 后读到的内容
五  9月/25 20:28:43 2026

$ LC_ALL=C ps -p <pid> -o lstart=
Fri Sep 25 20:28:43 2026

原因

verifyDaemonIdentity()(src/util/daemon.ts L425-L455)既比对守护进程命令行,也比对它读取到的进程启动时间。在 macOS 上启动时间来自 ps:

产品因此拒绝发送信号。

为什么这可能影响日常使用

  • 它挡住了标准的停止入口。 本次失败的命令就是 node stop;错误提示转而要求用户手动停止进程。fiber stop 使用同一身份校验、只是提示文本不同(src/fiber/daemon.ts L309-L312),fiber status 的 offckbManaged 字段也由它决定——这两条属于代码推断,我没有实测。
  • 清理是被间接阻塞的,不是同一个校验。 clean 在 CKB daemon 仍在运行时按设计拒绝(src/cmd/clean.ts L18-L20);在运行中的 Fiber 环境里,fiber clean 同样要求先停止 Fiber 节点(src/fiber/clean.ts)。拒绝本身是正确的,问题在于它们依赖的停止命令不可用。本次只验证了普通 CKB devnet 这条路径。
  • 平台范围。 macOS 走 ps 这条路。Linux 上代码优先读 /proc,直接绕开 ps;只有当 /proc 读取失败时才会回退到 ps,所以 Linux 并非绝对安全。我没有 Linux 机器实测,这是推断而非复现结果。
  • 其他会使 ps 输出本地化 lstart 的 locale 应当命中同一路径;我只用中文 locale 复现过。

这是相对 0.4.13 的回归

我在同一台机器、同一中文 locale 下用 @offckb/cli@0.4.13 跑了相同的启动/停止流程,再对比两个构建:

  • @offckb/cli@0.4.13:node --daemon 退出码 0,node stop 退出码 0,输出 {"ok":true,"command":"node.stop","stopped":true,"pid":98610}。它的 verifyDaemonIdentity() 只通过 ps -o args= 检查命令行,完全不读启动时间,该构建里也不出现 lstart。
  • 0.5.0-canary-ee0ad6b:新增了启动时间比对与 parsePsLstart 正则,本地化的 ps 输出正是失败点。

两个版本的差异就是这个新校验,因此这是随 Fiber 功能一起引入的回归,而不是长期存在的行为。

两个版本各自使用全新的临时 HOME/XDG,因此上面的两个 PID 来自彼此独立的环境。两次运行使用同一台机器、同一个 CKB 0.208.0 二进制和同一套继承的 locale:

# @offckb/cli@0.4.13
$ node .../offckb-0.4.13/build/index.js --version
0.4.13
$ ps -p $$ -o lstart=        # 继承 locale 后的输出格式
五  9月/25 20:40:55 2026
$ grep -c lstart build/index.js
0
$ node .../build/index.js --json node --daemon --binary-path <ckb>   -> 退出码 0
$ node .../build/index.js --json node stop                           -> 退出码 0
  {"ok":true,"command":"node.stop","stopped":true,"pid":98610}

# 0.5.0-canary-ee0ad6b,同一台机器、同一 locale
$ node .../build/index.js --json node --daemon --binary-path <ckb>   -> 退出码 0
$ node .../build/index.js --json node stop                           -> 退出码 1,stdout 为空
  {"ok":false,"code":"COMMAND_FAILED","message":"Process 93563 does not appear to be the offckb daemon. ..."}
$ LC_ALL=C node .../build/index.js --json node stop                  -> 退出码 0
  {"ok":true,"command":"node.stop","stopped":true,"pid":93563}

我希望的行为

  • OffCKB 自己启动的 devnet,无论操作系统语言是什么,都应该能正常停止和清理。
  • 保留启动时间校验:它正是防止 PID 被复用后误杀无关进程的保护;在读不到时间时跳过校验会削弱这层保护。要修的是"怎么读时间":给 ps 子进程固定 locale,例如 env: { ...process.env, LC_ALL: 'C' },让 lstart 能被稳定解析。若探测仍然失败,应报告"无法确认进程身份"并且不发任何信号——"无法判定"可以影响错误提示,但不应变成允许停止的降级条件。
  • 准备这份报告时我核对过用 ps -o etimes=(已运行秒数)替代 lstart 的可行性:macOS 没有这个关键字(ps: etimes: keyword not found,手册只列 etime),因此删去了该建议。

环境

  • @offckb/cli@0.5.0-canary-ee0ad6b,从 npm 安装,用 offckb --version 核对。
  • macOS arm64;Node.js 22.23.2;停止命令继承的是 LANG=zh_CN.UTF-8(LC_ALL、LC_TIME 未设置),复现步骤即以此为准。
  • CKB 0.208.0;上述检查都在独立的临时 HOME/XDG 目录中完成,未使用开发者真实数据。

如需复核,我可以补上两个版本的原始命令输出、ps 采样或 daemon 日志。

问题溯源

被测 canary 中的相关源码

与 0.4.13 的对比基于两个已发布的 npm 包(cli-0.4.13.tgz 与 canary tarball),不是本地构建。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions