Skip to content

fix(daemon): identify our daemon on Windows via CIM, not ps - #38

Merged
leeguooooo merged 1 commit into
leeguooooo:mainfrom
CelChe:fix/win-daemon-identity
Jul 26, 2026
Merged

fix(daemon): identify our daemon on Windows via CIM, not ps#38
leeguooooo merged 1 commit into
leeguooooo:mainfrom
CelChe:fix/win-daemon-identity

Conversation

@CelChe

@CelChe CelChe commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Problem

_process_is_our_daemon() reads /proc/<pid>/cmdline on Linux and falls back
to ps -o command= -p <pid> everywhere else. 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 check then misbehaves:

  • cs daemon stoppid 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:

> Get-CimInstance Win32_Process -Filter 'ProcessId=25520' | % CommandLine
"C:\...\python.exe" -m claude_statusbar.cli daemon _run --render-interval 1.0

> cs daemon status
daemon: pid 25520 alive but is not our daemon (PID was reused).

Fix

Query CIM on win32 before the ps path. wmic would be cheaper but is
deprecated and already absent from current Windows 11 builds.

  • ~0.5s per call, paid only by stop / install / drift-restart — never by a render.
  • Falls back powershellpwsh.
  • Returns False when neither exists, so an unrelated process is still never signalled.

Tests

Three cases in tests/test_daemon.py, all platform-independent (sys.platform
monkeypatched): our daemon matches and ps is asserted never consulted; a
foreign 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 semantics
on Windows) — unchanged by this diff.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved daemon process detection on Windows using reliable process identity checks.
    • Prevented incorrect daemon restarts or termination when the process identity cannot be confirmed.
    • Added handling for environments where PowerShell is unavailable.

`_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>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Windows daemon identity verification

Layer / File(s) Summary
Windows command-line lookup and validation
src/claude_statusbar/daemon.py, tests/test_daemon.py
Windows daemon checks query process command lines through PowerShell/CIM with a timeout and interpreter fallback. Tests cover matching, non-matching, and unavailable-shell results without consulting ps.

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
Loading

Suggested reviewers: leeguooooo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: Windows daemon identification now uses CIM instead of ps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b33bb9e and 077c90b.

📒 Files selected for processing (2)
  • src/claude_statusbar/daemon.py
  • tests/test_daemon.py

Comment on lines 381 to +395
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 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 -C3

Repository: 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 3

Repository: 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.py

Repository: 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.py

Repository: 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 4

Repository: 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.py

Repository: 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 || true

Repository: 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.py

Repository: 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 3

Repository: 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.

Comment thread tests/test_daemon.py
Comment on lines +1063 to +1068
return _sp.CompletedProcess(
cmd, 0,
stdout=('"C:\Python\python.exe" -m claude_statusbar.cli '
'daemon _run --render-interval 1.0\n'),
stderr="",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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))
PY

Repository: 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))
PY

Repository: 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))
PY

Repository: 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.

@leeguooooo
leeguooooo merged commit add900d into leeguooooo:main Jul 26, 2026
6 checks passed
leeguooooo added a commit that referenced this pull request Jul 26, 2026
#38 带进来的两条断言里 `"C:\Python\python.exe"` / `"C:\Windows\explorer.exe"`
是非法转义序列(`\P` `\W`),Python 3.12+ 每次收集测试都报 SyntaxWarning,
未来版本会直接变成 SyntaxError。断言内容不变。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants