Skip to content

feat(daemon): cross-platform Windows support via named-pipe IPC#15

Merged
sunerpy merged 14 commits into
mainfrom
feat/windows-daemon
Jun 22, 2026
Merged

feat(daemon): cross-platform Windows support via named-pipe IPC#15
sunerpy merged 14 commits into
mainfrom
feat/windows-daemon

Conversation

@sunerpy

@sunerpy sunerpy commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Make codegraph build and run on Windows by giving the per-project daemon a cross-platform IPC transport (Unix domain socket on Unix, named pipe on Windows) backed by the interprocess crate — matching upstream colby's Windows support. Zero change to extraction / golden output.

Ships as one PR (6 logical commits):

  1. refactor(daemon) — abstract IPC transport over interprocess (Unix byte-identical: nonblocking-accept + poll, Stream::split(), write-then-publish lock, rustix process introspection). MSRV declaration 1.70 → 1.75 (interprocess 2.4 requirement; toolchain is already stable).
  2. test(daemon) — parallel-safe regression tests: lock contention one-winner, shutdown-flag stops accept loop, stale-lock recovery, empty in-flight pidfile never deleted (audit BLOCKER fix(installer): never overwrite JSONC agent configs on parse failure #6).
  3. feat(daemon) — Windows named-pipe (GenericNamespaced) + windows-sys process liveness; supervision_lost_reason skips the ppid branch on Windows (no startup self-kill).
  4. ci — windows-latest job (clippy + test) + daemon windows cross-check, wired into CI Success as a merge blocker.
  5. build(release)x86_64-pc-windows-msvc target, .zip packaging, dist/*.zip added to the upload-assets glob (was tar.gz-only).
  6. docs — Windows install matrix + caveats (EN + zh-CN), MSRV 1.75, design doc, upstream ledger.

Verification

  • Linux gate green: fmt, clippy -D warnings, guardrail, golden equivalence byte-identical (4/4), golden_mcp 21/21, daemon regressions 10/10 parallel.
  • Windows compile/runtime is gated on the new windows-latest CI job (the Linux dev box can't cross-build the C deps tree-sitter/sqlite/ring without MSVC — by design).
  • Public daemon API unchanged; CLI compiles unchanged.

Reviewer note — pre-existing flaky tests (NOT introduced here)

Two tests flake intermittently only under peak full-workspace parallel load; both exist on main and this PR makes zero changes to their crates:

  • codegraph-mcp::golden_mcp::tools_list_matches_upstream_names_and_schemas
  • codegraph-watch::sync::tests::unchanged_file_hash_is_not_reindexed

They pass cleanly serial / per-crate. If CI reds on either, re-run — it is unrelated to this change. (Tracked for a separate follow-up.)

sunerpy added 14 commits June 22, 2026 12:45
…or change)

Replace the Unix-only UnixListener/UnixStream with a sync interprocess
local_socket abstraction (transport.rs): nonblocking-accept + poll loop instead
of the self-connect poke, Stream::split() for the reader/writer halves, rustix
for getppid + signal-0 liveness, and a write-then-publish lock that never
deletes an empty in-flight pidfile. Public API unchanged; Unix behavior
byte-identical (golden equivalence + daemon lifecycle green). Bumps MSRV
declaration 1.70 -> 1.75 (interprocess 2.x requirement; toolchain is stable).
…load

Add x86_64-pc-windows-msvc (native build on windows-latest) to the release
matrix, package as .zip via Compress-Archive, and extend the upload-assets glob
to dist/*.tar.gz + dist/*.zip so the Windows asset is not silently dropped.
Add Windows to the prebuilt install matrix, rewrite the stale
Windows-unsupported / daemon-does-not-work paragraphs (daemon now works via
named pipes on Windows), note the self-update .zip caveat, bump MSRV 1.70 ->
1.75 in both READMEs, and add docs/design/windows-daemon.md.
Fill the Windows arms of the cross-platform daemon: bare `codegraph-<hash>`
namespaced pipe name (interprocess GenericNamespaced), current_ppid()->0 +
OpenProcess/GetExitCodeProcess liveness via windows-sys, and a
cfg(windows)-skipped ppid branch in supervision_lost_reason so a Some(parent_pid)
never self-kills the daemon. Unix stays byte-identical (golden equivalence +
daemon lifecycle green); Windows runtime is gated on the windows-latest CI job.
…cking)

Add a windows-latest job (clippy + test --workspace) and a linux
`cargo check -p codegraph-daemon --target x86_64-pc-windows-msvc` cross-check;
wire windows into the ci-success gate (needs + failure check) so a red Windows
run blocks the PR via the required CI Success status check.
Add parallel-safe regression tests pinning the C1 behavior changes: lock
contention yields exactly one winner, stop() terminates the accept loop and
clears the pidfile, dead-pid locks recover, and an empty in-flight pidfile is
never deleted by a reader (audit BLOCKER #6). temp_project uses a process-global
AtomicU64 counter so parallel tests never collide on a coarse-clock nanos value.
codegraph-bench is a publish=false dev/benchmark harness using Unix-only APIs
(posix_fadvise, process_group, AsRawFd). The windows-latest clippy/test
--workspace run tried to compile it and failed. Exclude it on Windows only; the
Linux test job keeps --workspace so bench + golden equivalence stay covered. The
release windows build is unaffected (cargo build -p codegraph-rs skips dev-deps).
The `file` param is only read inside the #[cfg(unix)] set_mode block; on
Windows that block compiles out, so -D warnings tripped unused-variables. Add
#[cfg_attr(not(unix), allow(unused_variables))] (chmod is a no-op on Windows).
Surfaced by the new windows-latest CI job.
codegraph-bench is a dev-dependency of codegraph-cli, so --all-targets pulls it
into the Windows build graph (--exclude can't prevent that). cfg-gate the
Unix-only bits: process_group/kill_process_group (metrics) and
posix_fadvise/AsRawFd page-cache eviction (runner); on Windows these are
portable no-ops (child.kill() for cleanup, eviction skipped). Move libc to
[target.'cfg(unix)'.dependencies]. Unix behavior + golden oracle byte-identical.
The node-id/content-hash invariant SHA-256s raw fixture bytes (include_str!).
On Windows, git autocrlf checks out the LF fixtures as CRLF, changing the bytes
-> hash mismatch -> golden byte-stability breaks (caught by the windows-latest
CI job: content hash mismatch for src/app.ts). Force eol=lf for
crates/codegraph-bench/fixtures, reference/golden, and text source types; mark
the binary .db golden as binary. Renormalize is a no-op on Linux (already LF),
so Unix hashes stay byte-identical while Windows now checks out LF too.
…n Windows sharing violation

The codegraph-mcp golden test callees_matches_golden panicked on
windows-latest CI with Os{code:32} ERROR_SHARING_VIOLATION at the
fs::copy of the golden DB into a temp .codegraph/codegraph.db. Root cause
is a test-harness race, not a logic or golden-data bug:

- setup_mini_project() and index_fixture() keyed their temp dir on
  process::id() + SystemTime nanos. Windows' coarse SystemTime lets
  parallel test threads collide on identical nanos, so two tests share
  one temp .codegraph/codegraph.db and one test's open SQLite handle
  races the other's copy/remove (mandatory file locking on Windows).

Fix (test-harness only; no extraction/resolution/store/node-id/golden
change):
- Add a process-wide AtomicU64 TEMP_SEQ and append it to both temp dir
  names, the same collision-proof pattern codegraph-daemon's
  regressions.rs already uses. A colliding path is now impossible
  regardless of clock granularity.
- Wrap the golden-db and fixture copies in copy_with_retry(), which
  retries only on raw OS error 32 with a short sleep and otherwise
  panics with the original per-call-site context. Error 32 never occurs
  on Unix, so the first fs::copy always succeeds there and Unix behavior
  is byte-identical.

Also harden .gitattributes: the binary *.db / *.db-shm / *.db-wal /
*.sqlite / *.sqlite3 lines now carry '-text !eol' so the catch-all
'* text=auto eol=lf' can never leave a dangling eol=lf on a binary
SQLite DB. The golden .db bytes are unchanged.
tildify() stripped the home prefix with a hardcoded forward slash
(strip_prefix(&format!("{home}/"))) on the lossy path string. On
Windows path components use backslash separators, so the "{home}/"
literal never matched and the full absolute path was returned instead
of "~/foo.json".

Use the separator-agnostic Path::strip_prefix(&ctx.home), which matches
on path components, then normalize the displayed tail to POSIX-style
forward slashes so the rendered "~/..." is byte-identical on every
platform (upstream index.ts:437 emits POSIX-style ~/). On Unix there
are no backslashes to replace, so output is unchanged.
…sqlite3

The bench oracle and pipeline shelled out to the external `sqlite3` CLI
(`Command::new("sqlite3").arg(db).arg(".schema")`) to capture the schema
for canonicalization. The windows-latest CI runner has no `sqlite3` binary
on PATH (Linux/macOS runners ship it), so the spawn failed with io::Error
"program not found", panicking 6/7 tests in sync_incremental.rs on Windows.

Replace both `sqlite_schema` functions with an in-process rusqlite query
over sqlite_master (`SELECT name, type, sql ... ORDER BY rowid`), feeding
the result through the existing `normalize_schema`. The SQLite shell appends
a `/* name(cols) */` comment after each CREATE VIRTUAL TABLE; that comment
is reproduced from pragma_table_info so the normalized schema string is
byte-identical to the old CLI output and the committed golden equivalence
is preserved. rusqlite (bundled SQLite) is already a bench dependency, so no
new crate is added and the external runtime dependency is removed entirely.
…helling out to sqlite3

The schema_parity test shelled out to the external sqlite3 CLI via
Command::new("sqlite3").arg(db).arg(".schema"). The windows-latest CI
runner has no sqlite3 on PATH, so the spawn failed with NotFound
"program not found" and the test panicked on unwrap.

Replace the shell-out with an in-process rusqlite dump of sqlite_master
(SELECT name, type, sql ... ORDER BY rowid). To stay byte-identical to
the committed golden schema, reproduce the /* name(cols) */ column-list
comment that sqlite3 .schema appends after each CREATE VIRTUAL TABLE,
sourcing the columns from pragma_table_info. rusqlite is already a
dev-dependency; no new dependency is added and normalize_schema is
unchanged.
@sunerpy sunerpy merged commit f742a43 into main Jun 22, 2026
4 checks passed
@sunerpy sunerpy deleted the feat/windows-daemon branch June 22, 2026 07:01
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