fix(daemon): identify our daemon on Windows via CIM, not ps - #38
Conversation
`_process_is_our_daemon` falls back to `ps -o command= -p <pid>` off Linux. Windows has no `ps` — and when Git Bash is on PATH it has a *worse* one: the MSYS `ps` only lists MSYS processes, so a natively spawned daemon reads as gone and the function returns False for the real, running daemon. Everything gated on that identity check then misbehaves: - `cs daemon stop` → "pid N is alive but is NOT our daemon (PID reused). Refusing to SIGTERM." The daemon cannot be stopped at all. - `render_thin._signal_outdated_daemon` → refuses to SIGTERM after an upgrade, so the daemon serves stale code indefinitely while every session falls back to the ~45ms inline render path, forever. - `cs daemon status` → reports a healthy daemon as a recycled PID. Observed on Windows 11 / v3.32.0 with a live daemon whose real cmdline is `python.exe -m claude_statusbar.cli daemon _run --render-interval 1.0`. Query CIM instead on win32. `wmic` would be cheaper but is deprecated and absent from current Windows 11 builds. ~0.5s per call, paid only by the stop / install / drift-restart paths — never by a render. Falls back from `powershell` to `pwsh`, and stays conservative (False) when neither exists, so an unrelated process is still never signalled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughChangesWindows daemon identity verification
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant process_check as _process_is_our_daemon
participant cmdline_lookup as _win_process_cmdline
participant powershell_cim as PowerShell/CIM
process_check->>cmdline_lookup: query target PID
cmdline_lookup->>powershell_cim: retrieve CommandLine with timeout
powershell_cim-->>cmdline_lookup: process command line or lookup failure
cmdline_lookup-->>process_check: command line
process_check->>process_check: apply _cmdline_is_our_daemon
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/claude_statusbar/daemon.py`:
- Around line 381-395: Prevent the Windows `_win_process_cmdline` CIM probe from
running when `_process_is_our_daemon` is invoked through
`render_thin._spawn_daemon_async` and `spawn_if_dead`. Keep the identity check
for explicit daemon-management commands, while allowing the render fallback to
avoid the potentially blocking Windows probe.
In `@tests/test_daemon.py`:
- Around line 1063-1068: Update the CompletedProcess mock in the daemon test to
escape backslashes in the Windows executable path literals, including the Python
and explorer paths, using doubled backslashes or raw strings while preserving
the expected command output.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 67c0c9e1-926e-4740-80a2-981299c2402c
📒 Files selected for processing (2)
src/claude_statusbar/daemon.pytests/test_daemon.py
| def _process_is_our_daemon(pid: int) -> bool: | ||
| """Verify the PID actually belongs to *our* daemon, not a recycled PID. | ||
|
|
||
| Linux: read /proc/<pid>/cmdline directly (cheap, no fork). | ||
| Windows: query CIM (see `_win_process_cmdline`). | ||
| macOS / fallback: shell out to `ps -o command= -p <pid>` (~10ms — only | ||
| runs on stop/install paths, never on the per-render hot path). | ||
|
|
||
| Returns False on any error (better to assume not-ours and skip than to | ||
| accidentally SIGTERM an unrelated user process). | ||
| """ | ||
| if sys.platform == "win32": | ||
| cmdline = _win_process_cmdline(pid) | ||
| return _cmdline_is_our_daemon(cmdline) if cmdline else False | ||
|
|
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm spawn_if_dead's call sites and whether it's on the interactive render path.
rg -n 'spawn_if_dead' --type=py -C3Repository: leeguooooo/claude-code-usage-bar
Length of output: 170
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map daemon.py, then inspect spawn_if_dead, render-related call sites, and the PID verification path.
ast-grep outline src/claude_statusbar/daemon.py --view expanded || true
printf '\n--- spawn_if_dead matches ---\n'
rg -n 'spawn_if_dead|_process_is_our_daemon|_win_process_cmdline|rendered\.meta\.json|cs render|render' src/claude_statusbar/daemon.py -C 3Repository: leeguooooo/claude-code-usage-bar
Length of output: 17063
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the relevant sections around the reported lines and the spawn_if_dead implementation.
sed -n '340,460p' src/claude_statusbar/daemon.py
printf '\n---\n'
sed -n '800,860p' src/claude_statusbar/daemon.pyRepository: leeguooooo/claude-code-usage-bar
Length of output: 6736
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Files:\n'
git ls-files | sed -n '1,200p'
printf '\n--- references ---\n'
rg -n '_process_is_our_daemon|spawn_if_dead|rendered\.meta\.json|cs render|render' src -C 2 || true
printf '\n--- daemon.py slices ---\n'
wc -l src/claude_statusbar/daemon.py
sed -n '360,450p' src/claude_statusbar/daemon.py
printf '\n---\n'
sed -n '800,860p' src/claude_statusbar/daemon.pyRepository: leeguooooo/claude-code-usage-bar
Length of output: 50391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Trace where render_thin calls spawn_if_dead and whether it's only on stale-meta fallback.
rg -n 'spawn_if_dead|rendered\.meta\.json|stale|_is_fresh|_signal_outdated_daemon' src/claude_statusbar/render_thin.py -C 4Repository: leeguooooo/claude-code-usage-bar
Length of output: 6056
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the thin client control flow around the stale-meta check.
sed -n '1,260p' src/claude_statusbar/render_thin.pyRepository: leeguooooo/claude-code-usage-bar
Length of output: 10203
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Narrowly check whether render_thin invokes spawn_if_dead.
rg -n 'spawn_if_dead|rendered\.meta\.json|_is_fresh|_signal_outdated_daemon' src/claude_statusbar/render_thin.py -C 3 || trueRepository: leeguooooo/claude-code-usage-bar
Length of output: 2643
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the spawn helper and the render fallback call site to see whether the CIM lookup is synchronous.
sed -n '320,430p' src/claude_statusbar/render_thin.pyRepository: leeguooooo/claude-code-usage-bar
Length of output: 4514
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show just the async-spawn helper and its usage points.
rg -n '_spawn_daemon_async|spawn_if_dead|threading\.Thread|start_new_session' src/claude_statusbar/render_thin.py -C 3Repository: leeguooooo/claude-code-usage-bar
Length of output: 1628
Keep the Windows CIM probe off the cs render fallback path. spawn_if_dead() is still reached from render_thin._spawn_daemon_async(), so a stale or missing daemon can make cs render block on _process_is_our_daemon() before falling back inline. On Windows that can add up to 16s of powershell/pwsh timeout latency to a status-line tick; reserve the identity check for explicit daemon-management commands, or skip it on this render-triggered path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/claude_statusbar/daemon.py` around lines 381 - 395, Prevent the Windows
`_win_process_cmdline` CIM probe from running when `_process_is_our_daemon` is
invoked through `render_thin._spawn_daemon_async` and `spawn_if_dead`. Keep the
identity check for explicit daemon-management commands, while allowing the
render fallback to avoid the potentially blocking Windows probe.
| return _sp.CompletedProcess( | ||
| cmd, 0, | ||
| stdout=('"C:\Python\python.exe" -m claude_statusbar.cli ' | ||
| 'daemon _run --render-interval 1.0\n'), | ||
| stderr="", | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -euo pipefail
# Show the relevant sections with line numbers
sed -n '1058,1072p;1078,1088p' tests/test_daemon.py
# Probe Python's handling of the exact string literals used in the review comment
python3 - <<'PY'
import warnings
snippets = [
'"C:\\Python\\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0\n',
"C:\\Windows\\explorer.exe\n",
'"C:\Python\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0\n',
"C:\Windows\explorer.exe\n",
]
for s in snippets:
print("SNIPPET:", repr(s))
try:
compile(f"x = {s!r}", "<snippet>", "exec")
print("compile: ok")
except Exception as e:
print("compile:", type(e).__name__, e)
# Specifically compile literal source containing invalid escapes to see warnings/errors.
sources = [
'x = "C:\\Python\\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0\n',
'x = "C:\\Windows\\explorer.exe\n"',
'x = "C:\\Python\\python.exe"\n',
'x = "C:\\Windows\\explorer.exe"\n',
]
for src in sources:
print("\nSOURCE:", src.encode("unicode_escape").decode())
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
try:
compile(src, "<src>", "exec")
print("compile: ok")
except Exception as e:
print("compile:", type(e).__name__, e)
for item in w:
print("warning:", item.category.__name__, str(item.message))
PYRepository: leeguooooo/claude-code-usage-bar
Length of output: 1877
🏁 Script executed:
set -euo pipefail
printf '\n-- targeted source --\n'
nl -ba tests/test_daemon.py | sed -n '1058,1072p;1078,1088p'
printf '\n-- python probe --\n'
python3 - <<'PY'
import warnings
# Build the exact source text without letting this script's parser interpret it.
good1 = '"C:\\\\Python\\\\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0\n'
good2 = 'C:\\\\Windows\\\\explorer.exe\n'
bad1 = '"C:\\Python\\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0\n'
bad2 = 'C:\\Windows\\explorer.exe\n'
for label, text in [('good1', good1), ('good2', good2), ('bad1', bad1), ('bad2', bad2)]:
print(label, repr(text))
src = 'x = ' + repr(text) + '\n'
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
try:
compile(src, '<src>', 'exec')
print(' compile: ok')
except Exception as e:
print(' compile:', type(e).__name__, e)
for item in w:
print(' warning:', item.category.__name__, str(item.message))
# Compile exact raw source code containing invalid escapes
raw_sources = [
'x = "C:\\Python\\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0\n',
'x = "C:\\Windows\\explorer.exe\\n"\n',
'x = "C:\\Python\\python.exe"\n',
'x = "C:\\Windows\\explorer.exe"\n',
]
for src in raw_sources:
print('\nRAW SRC:', src.encode('unicode_escape').decode())
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
try:
compile(src, '<raw>', 'exec')
print(' compile: ok')
except Exception as e:
print(' compile:', type(e).__name__, e)
for item in w:
print(' warning:', item.category.__name__, str(item.message))
PYRepository: leeguooooo/claude-code-usage-bar
Length of output: 233
🏁 Script executed:
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("tests/test_daemon.py")
lines = path.read_text().splitlines()
for n in [1063, 1064, 1065, 1066, 1067, 1068, 1082, 1083, 1084, 1085]:
print(f"{n}: {lines[n-1]!r}")
PY
printf '\n-- warning-mode probe --\n'
python3 -Wd - <<'PY'
s1 = "C:\Python\python.exe"
s2 = "C:\Windows\explorer.exe"
print(repr(s1))
print(repr(s2))
PYRepository: leeguooooo/claude-code-usage-bar
Length of output: 819
Escape the Windows path literals.
"C:\Python\python.exe" and "C:\Windows\explorer.exe" use invalid escapes; write them as C:\\Python\\python.exe and C:\\Windows\\explorer.exe (or raw strings) to avoid parser warnings.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_daemon.py` around lines 1063 - 1068, Update the CompletedProcess
mock in the daemon test to escape backslashes in the Windows executable path
literals, including the Python and explorer paths, using doubled backslashes or
raw strings while preserving the expected command output.
#38 带进来的两条断言里 `"C:\Python\python.exe"` / `"C:\Windows\explorer.exe"` 是非法转义序列(`\P` `\W`),Python 3.12+ 每次收集测试都报 SyntaxWarning, 未来版本会直接变成 SyntaxError。断言内容不变。
Problem
_process_is_our_daemon()reads/proc/<pid>/cmdlineon Linux and falls backto
ps -o command= -p <pid>everywhere else. Windows has nops— and whenGit Bash is on PATH it has a worse one: the MSYS
psonly lists MSYSprocesses, so a natively spawned daemon reads as gone and the function returns
False for the real, running daemon.
Everything gated on that check then misbehaves:
cs daemon stop→pid N is alive but is NOT our daemon (PID reused). Refusing to SIGTERM.The daemon can never be stopped.render_thin._signal_outdated_daemon()→ refuses to SIGTERM after an upgrade, so the daemon keeps serving stale code indefinitely while every session falls back to the ~45ms inline path. The code-drift defense added in 3.8.1 is a no-op on Windows.cs daemon status→ reports a healthy daemon as a recycled PID.Reproduced on Windows 11 / v3.32.0. Live daemon, real cmdline via CIM:
Fix
Query CIM on
win32before thepspath.wmicwould be cheaper but isdeprecated and already absent from current Windows 11 builds.
powershell→pwsh.Tests
Three cases in
tests/test_daemon.py, all platform-independent (sys.platformmonkeypatched): our daemon matches and
psis asserted never consulted; aforeign cmdline returns False; no shell available returns False.
tests/test_daemon.py: 58 passed, 1 skipped, 1 pre-existing unrelated failure(
test_release_pidfile_leaves_someone_elses_file_alone, flock/inode semanticson Windows) — unchanged by this diff.
🤖 Generated with Claude Code
Summary by CodeRabbit