Skip to content

Fall back to CIM when wmic is unavailable - #5305

Open
lukiod wants to merge 4 commits into
tinyhumansai:mainfrom
lukiod:fix/windows-process-enum-without-wmic
Open

Fall back to CIM when wmic is unavailable#5305
lukiod wants to merge 4 commits into
tinyhumansai:mainfrom
lukiod:fix/windows-process-enum-without-wmic

Conversation

@lukiod

@lukiod lukiod commented Jul 31, 2026

Copy link
Copy Markdown

enumerate_all_processes shells out to wmic, which Microsoft removed in Windows 11 24H2. On those builds the spawn fails and process recovery sees no processes at all, so it can't find or reattach to anything.

Tries wmic first so older builds are unaffected, then falls back to Get-CimInstance Win32_Process via PowerShell. The fallback emits the same Key=Value blocks separated by blank lines that wmic /format:list produced, so parse_wmic_list_output handles both without changes.

Checked the fallback against real output on Windows 11: 280 records parsed, 279 with a pid (pid 0 is System Idle Process), 280 with a command, 278 with a parent — the two without are pid 0 and pid 4, which legitimately have none.

I couldn't build the whole crate to confirm — cef-dll-sys needs CMake and I don't have it installed — so this hasn't been compiled, only parsed and format-checked with rustfmt. Worth a CI run before merging.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Windows process recovery reliability by automatically using an alternative discovery method when the primary method is unavailable, fails, or returns no results.
    • Process recovery now handles command failures and output encoding more consistently, helping ensure available processes are detected and recovered correctly.

enumerate_all_processes shells out to wmic, which Microsoft removed in Windows
11 24H2. On those builds the spawn fails, enumerate_all_processes returns Err,
and startup recovery sees no processes at all.

Tries wmic first so older builds keep the existing path, then falls back to
Get-CimInstance. The fallback emits the same Key=Value blocks, so
parse_wmic_list_output handles both sources unchanged and no parsing code is
added.

Confirmed on Windows 11 that wmic is absent and that the CIM command returns
Caption, CommandLine, ExecutablePath, ParentProcessId and ProcessId in the
expected format.
@lukiod
lukiod requested a review from a team July 31, 2026 17:00
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d0f5214-aee8-4da0-b05e-ef8cf3182771

📥 Commits

Reviewing files that changed from the base of the PR and between 19a3810 and 573eb47.

📒 Files selected for processing (1)
  • app/src-tauri/src/process_recovery.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src-tauri/src/process_recovery.rs

📝 Walkthrough

Walkthrough

Windows process enumeration tries WMIC first. If WMIC fails or returns no processes, it uses PowerShell CIM. CIM produces UTF-8, WMIC-compatible records for the existing parser. Tests cover fallback selection, failures, encoding, fields, and parsing edge cases.

Changes

Process enumeration

Layer / File(s) Summary
WMIC and CIM enumeration fallback
app/src-tauri/src/process_recovery.rs
enumerate_all_processes uses WMIC when it returns processes. It falls back to enumerate_via_cim when WMIC fails or returns no processes. The CIM path runs Get-CimInstance Win32_Process, emits UTF-8 key-value records, validates command success, and uses the existing parser. Tests cover selection, failures, field coverage, encoding, and parsing edge cases.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant enumerate_all_processes
  participant WMIC
  participant PowerShell_CIM
  participant WMIC_list_parser
  enumerate_all_processes->>WMIC: enumerate processes
  WMIC-->>enumerate_all_processes: records, empty output, or error
  enumerate_all_processes->>PowerShell_CIM: enumerate when WMIC fails or is empty
  PowerShell_CIM-->>WMIC_list_parser: WMIC-compatible UTF-8 records
  WMIC_list_parser-->>enumerate_all_processes: parsed ProcessInfo values
Loading

Poem

A rabbit checked WMIC at dawn,
Then hopped to CIM when it was gone.
Processes returned in tidy rows,
Through the parser’s familiar flows.
“Fallbacks work!” the rabbit sings.

🚥 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 clearly and concisely describes the main change: falling back to CIM when WMIC is unavailable.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes process recovery on Windows 11 24H2+ where Microsoft removed wmic.exe by introducing a select_enumeration dispatch layer that tries wmic first and falls back to Get-CimInstance Win32_Process via PowerShell when wmic is absent or returns nothing. The PowerShell script emits the same Key=Value block format that wmic /format:list produced, so parse_wmic_list_output handles both paths without modification.

  • select_enumeration covers three distinct failure modes — spawn error, non-zero exit, and a cleanly-exiting shim with empty output — with log::debug! on every branch, satisfying the repo's logging requirement.
  • UTF-8 encoding is pinned with [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 before any record is emitted, ensuring non-Latin paths are not silently garbled.
  • Test coverage drives select_enumeration directly via injected closures (avoiding the need for a live Windows host) and includes a round-trip parse test with non-ASCII paths (Müller/用户) to guard the encoding contract.

Confidence Score: 5/5

Safe to merge; the change is additive, isolated to the Windows process enumeration path, and all three fallback branches are covered by injected-closure unit tests.

The dispatch logic is straightforward and well-exercised: the only real production path that was previously broken (wmic absent on 24H2) now falls through to a working CIM implementation. The parser is unchanged, the encoding fix is correct, and every branch of select_enumeration has a dedicated test.

Files Needing Attention: No files require special attention. process_recovery.rs is the only changed file and the logic is self-contained.

Important Files Changed

Filename Overview
app/src-tauri/src/process_recovery.rs Adds select_enumeration dispatch layer with enumerate_via_wmic / enumerate_via_cim fallback; includes logging on every branch, UTF-8 pin in the PowerShell script, and full unit-test coverage for the new selection logic and the parser against non-ASCII CIM output. No logic errors found.

Reviews (4): Last reviewed commit: "test(windows): use a genuinely non-ASCII..." | Re-trigger Greptile

Comment thread app/src-tauri/src/process_recovery.rs
Comment thread app/src-tauri/src/process_recovery.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@app/src-tauri/src/process_recovery.rs`:
- Around line 760-769: Extract the WMIC/CIM fallback selection and command
construction from enumerate_via_wmic into pure or injectable helpers, then add
Rust tests covering non-empty WMIC short-circuiting, WMIC failure or empty
output falling back to CIM, CIM failure propagation, empty CIM output, and
serialization/parsing of comma-containing command lines and paths, null fields,
and non-ASCII text without spawning system commands.
- Around line 760-769: Update enumerate_via_cim to pass -ErrorAction Stop to
Get-CimInstance and return an error instead of Ok when parsing produces an empty
process list; preserve successful non-empty snapshots and the existing fallback
behavior in the caller.
- Around line 807-811: Update the Windows enumeration helpers
enumerate_via_wmic() and enumerate_via_cim() to use a shared timeout-capable
process runner instead of calling Command::output() directly. Ensure the runner
terminates the child when the deadline is exceeded and propagates a suitable
error, while preserving each helper’s existing command and output parsing
behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 03d2532e-b778-43f6-889b-106d206afc1a

📥 Commits

Reviewing files that changed from the base of the PR and between 068ae45 and d95f1c9.

📒 Files selected for processing (1)
  • app/src-tauri/src/process_recovery.rs

Comment thread app/src-tauri/src/process_recovery.rs Outdated
Comment on lines +807 to +811
let output = std::process::Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", script])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("spawn powershell: {e}"))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching process_recovery/core_process/lib:\n'
git ls-files | rg 'app/src-tauri/src/(process_recovery|core_process)\.rs|app/src-tauri/src/lib\.rs' || true

printf '\nOutline process_recovery.rs relevant symbols:\n'
ast-grep outline app/src-tauri/src/process_recovery.rs --view compact || true

printf '\nLines 760-840 process_recovery.rs:\n'
sed -n '760,840p' app/src-tauri/src/process_recovery.rs | nl -ba -v760

printf '\nLines 150-195 lib.rs:\n'
sed -n '150,195,196p' app/src-tauri/src/lib.rs | nl -ba -v150 | sed -n '1,80p'

printf '\nLines 660-760 core_process.rs (if exists):\n'
sed -n '660,760p' app/src-tauri/src/core_process.rs | nl -ba -v660

printf '\nSearch for timeouts/deadlines around recovery/enumerate functions:\n'
rg -n "timeout|deadline|Duration|instant|enumerate_via_wmic|enumerate_via_cim|recover|recovery|Command::output|powershell|WMIC|CIM" app/src-tauri/src/process_recovery.rs app/src-tauri/src/core_process.rs app/src-tauri/src/lib.rs || true

Repository: tinyhumansai/openhuman

Length of output: 582


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching process_recovery/core_process/lib:\n'
git ls-files | rg 'app/src-tauri/src/(process_recovery|core_process)\.rs|app/src-tauri/src/lib\.rs' || true

printf '\nOutline process_recovery.rs relevant symbols:\n'
ast-grep outline app/src-tauri/src/process_recovery.rs --view expanded | sed -n '1,220p'

printf '\nLines 760-840 process_recovery.rs:\n'
sed -n '760,840p' app/src-tauri/src/process_recovery.rs | awk '{printf "%6d\t%s\n", NR+759, $0}'

printf '\nLines 150-195 lib.rs:\n'
sed -n '150,195p' app/src-tauri/src/lib.rs | awk '{printf "%6d\t%s\n", NR+149, $0}'

printf '\nLines 660-760 core_process.rs:\n'
sed -n '660,760p' app/src-tauri/src/core_process.rs | awk '{printf "%6d\t%s\n", NR+659, $0}'

printf '\nSearch for timeouts/deadlines and related calls:\n'
rg -n "timeout|deadline|Duration|Instant|enumerate_via_wmic|enumerate_via_cim|recover|recovery|Command::output|powershell|WMIC|CIM|core_process" app/src-tauri/src/process_recovery.rs app/src-tauri/src/core_process.rs app/src-tauri/src/lib.rs || true

Repository: tinyhumansai/openhuman

Length of output: 32297


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Relevant command registration:\n'
sed -n '820,845p' app/src-tauri/src/lib.rs | awk '{printf "%6d\t%s\n", NR+819, $0}'

printf '\nCommand call sites for process_diagnostics_list_owned:\n'
rg -n "process_diagnostics_list_owned|invoke|list_.*diagnostics|diagnostics_list" . || true

printf '\nCommand call sites for recover_port_conflict:\n'
rg -n "recover_port_conflict" . || true

printf '\nBehavioral model: Command without timeout waiting on hanging child.\n'
python3 - <<'PY'
from pathlib import Path
p = Path('app/src-tauri/src/process_recovery.rs')
text = p.read_text()
checks = {
    "enumerate_via_cim_spawns_powershell": 'let output = std::process::Command::new("powershell")' in text,
    "enumerate_via_cim_uses_output": '.output()' in text and 'fn enumerate_via_cim()' in text[text.find('fn enumerate_via_cim'): text.find('fn enumerate_via_cim')+500],
    "enumerate_all_calls": 'match enumerate_via_wmic()' in text and 'enumerate_via_cim()' in text,
    "reap_calls_enumerate": 'enumerate_openhuman_processes()' in text,
    "recover_block": 'tokio::task::spawn_blocking(crate::process_recovery::reap_stale_openhuman_processes)' in text,
    "diagnostics_block": 'process_recovery::enumerate_openhuman_processes()' in text,
}
for k, v in checks.items():
    print(f"{k}: {v}")
PY

Repository: tinyhumansai/openhuman

Length of output: 50379


Add a timeout for Windows process enumeration.

enumerate_via_cim() uses Command::output() directly for the PowerShell child, so PowerShell or a hung CIM provider can block indefinitely. process_diagnostics_list_owned() calls this synchronously in app/src-tauri/src/lib.rs, and reap_stale_openhuman_processes() also calls it on the Windows recovery path. Use one timeout-capable runner for enumerate_via_wmic() and enumerate_via_cim() that terminates the child after the deadline.

🤖 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 `@app/src-tauri/src/process_recovery.rs` around lines 807 - 811, Update the
Windows enumeration helpers enumerate_via_wmic() and enumerate_via_cim() to use
a shared timeout-capable process runner instead of calling Command::output()
directly. Ensure the runner terminates the child when the deadline is exceeded
and propagates a suitable error, while preserving each helper’s existing command
and output parsing behavior.

PowerShell writes stdout in the console OEM code page, so a process path outside
that page is replaced with `?` before Rust sees it and no decode can recover it.
Pinning OutputEncoding makes the CIM path round-trip.

Adds debug logging on both enumeration branches, per the logging rules in
AGENTS.md.
@lukiod

lukiod commented Aug 1, 2026

Copy link
Copy Markdown
Author

Both fair. Fixed in f053879.

The encoding one is worse than mangling — PowerShell replaces the character before it reaches Rust, so it's lost, not mis-decoded. Measured here:

without pin: b'Caption=\x82??'                      -> "Caption=\ufffd??"
with pin:    b'Caption=\xc3\xa9\xe4\xb8\xad\xe6\x96\x87' -> "Caption=é中文"

é came back as the cp437 byte and the CJK as literal ?. Now pins [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 at the start of the script.

Added log::debug! on both enumeration branches and a log::warn! when powershell exits non-zero, using the existing [startup-recovery] prefix.

Couldn't run cargo check here — the CEF build script needs cmake, which isn't installed. Ran the real script instead and put its output through a verbatim copy of parse_wmic_list_output: 272 processes, 114 with executable paths, one pid 0 record (System Idle Process).

The fallback only fires where wmic is absent, so the branch that matters is the
one CI never reaches and no test exercised it.

select_enumeration now takes both enumerators as arguments, so the choice can be
driven directly without spawning anything, and the CIM script is a named constant
rather than an inline literal so its contents can be asserted.

Six tests: wmic results used with CIM untouched, CIM on wmic error, CIM on empty
wmic output, CIM failure propagating rather than returning an empty list, the
script keeping its UTF-8 pin and every parsed field, and the parser reading CIM
blocks with a non-ASCII path and empty fields.

Cannot run cargo here: the CEF build script needs cmake. Compiled and ran the
extracted logic with the same tests standalone under rustc, all twelve checks
pass, and rustfmt parses the file clean.
@lukiod

lukiod commented Aug 1, 2026

Copy link
Copy Markdown
Author

Tests added in 19a3810.

select_enumeration now takes both enumerators as arguments so the choice can be driven without spawning anything, and the CIM script is a named constant so its contents can be asserted:

  • wmic results used, CIM not consulted
  • CIM on wmic error
  • CIM on empty wmic output (a 24H2 shim can exit 0 with nothing)
  • CIM failure propagates rather than returning an empty list — an empty list reads as "nothing stale to reap"
  • script keeps the UTF-8 pin and every field the parser reads
  • parser handles CIM blocks with a non-ASCII path and empty fields

Still can't run cargo here — the CEF build script needs cmake. I compiled the extracted logic with the same tests standalone under rustc (12 checks pass) and rustfmt --check parses the file clean, but the suite itself hasn't run in a real build.

The fixture was named for non-ASCII and asserted "non-ASCII path was mangled",
but the path was C:\Users\Ordner\OpenHuman.exe, which is entirely ASCII. The
assertion could not fail whatever the encoding did.

Uses Müller (representable in cp1252) and 用户 (not), so a regression that drops
the OutputEncoding pin fails on one or the other whichever code page the runner
uses, and compares the whole string rather than a substring.
@lukiod

lukiod commented Aug 1, 2026

Copy link
Copy Markdown
Author

Good catch on the fixture. C:\Users\Ordner\OpenHuman.exe is a German word but entirely ASCII, so "non-ASCII path was mangled" could not fail whatever the encoding did.

573eb47 uses Müller (representable in cp1252) and 用户 (not), so a regression that drops the OutputEncoding pin fails on one or the other whichever code page the runner uses. Also compares the whole string instead of a substring.

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.

1 participant