Skip to content

feat: Windows TCP fallback for embedded gRPC server#39

Merged
beonde merged 2 commits intomainfrom
fix/windows-tcp-fallback
Mar 11, 2026
Merged

feat: Windows TCP fallback for embedded gRPC server#39
beonde merged 2 commits intomainfrom
fix/windows-tcp-fallback

Conversation

@beonde
Copy link
Member

@beonde beonde commented Mar 11, 2026

Summary

On Windows, Unix domain sockets are not available. The embedded capiscio-core gRPC server previously only used --socket mode, making the SDK non-functional on Windows.

Changes

process.py

  • _start_tcp(): New method that spawns capiscio-core with --address localhost:<port> using an ephemeral TCP port (same approach as capiscio-mcp-python)
  • _start_unix_socket(): Extracted existing Unix socket logic (macOS/Linux behavior unchanged)
  • _find_free_port(): Allocates ephemeral port via socket.bind(('', 0))
  • _wait_grpc_ready(): Extracted shared gRPC readiness probe (used by both TCP and socket paths)
  • ensure_running(): Routes to TCP on win32, Unix socket otherwise

client.py

  • Fixed fallback address: uses localhost:50051 on Windows instead of unix:///tmp/capiscio.sock
  • Added missing import sys

Tests

  • 7 new unit tests covering: free port allocation, platform delegation, TCP spawn command, process isolation flags, address property behavior

Testing

  • 21/21 process tests pass (3 pre-existing find_binary failures excluded — unrelated to this change)
  • 338/348 full unit suite passes (same 10 pre-existing failures, 0 regressions)

On Windows, Unix domain sockets are not available. This change:
- Adds _start_tcp() that spawns capiscio-core with --address flag
  using an ephemeral TCP port (same approach as capiscio-mcp-python)
- Adds _start_unix_socket() to keep existing Unix socket behavior on
  macOS/Linux unchanged
- Extracts _wait_grpc_ready() to share readiness probe logic
- Adds _find_free_port() using socket.bind(('', 0))
- Fixes client.py fallback address to use localhost:50051 on Windows
- Adds 7 unit tests covering the new code paths
Copilot AI review requested due to automatic review settings March 11, 2026 20:42
@github-actions
Copy link

✅ Documentation validation passed!

Unified docs will be deployed from capiscio-docs repo.

@github-actions
Copy link

✅ All checks passed! Ready for review.

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions
Copy link

✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a Windows-compatible startup path for the embedded capiscio-core gRPC server by falling back from Unix domain sockets to a TCP listener, keeping macOS/Linux behavior unchanged.

Changes:

  • Route ProcessManager.ensure_running() to start capiscio-core via --address localhost:<ephemeral> on Windows, and via --socket elsewhere.
  • Add a shared gRPC readiness probe used by both startup modes.
  • Update client fallback defaults for Windows and expand unit test coverage around process startup/address selection.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
capiscio_sdk/_rpc/process.py Adds Windows TCP startup, free-port selection, and shared gRPC readiness checking.
capiscio_sdk/_rpc/client.py Adjusts default fallback address on Windows and adds missing sys import.
tests/unit/test_process.py Adds unit tests for free-port allocation, platform delegation, TCP spawn args, isolation flags, and address behavior.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +261 to +270
try:
popen_kwargs = {
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
}
if sys.platform == "win32":
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
else:
popen_kwargs["start_new_session"] = True
self._process = subprocess.Popen(cmd, **popen_kwargs)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

In _start_tcp, the child process is long-lived but stdout/stderr are piped without any reader. If capiscio-core logs enough data, the OS pipe buffer can fill and block the server process. Consider redirecting these streams to DEVNULL/a log file, or starting a background drain thread if you need to capture output for errors.

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +357
raise RuntimeError(
f"capiscio server exited unexpectedly:\n"
f"stdout: {stdout.decode()}\n"
f"stderr: {stderr.decode()}"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

_wait_grpc_ready raises immediately when the process exits unexpectedly, but it doesn’t reset internal state (e.g., _process, _started) or attempt cleanup; calling self.stop() before raising would keep the singleton manager consistent. Also, stdout.decode() / stderr.decode() can raise UnicodeDecodeError and mask the real failure—use a safe decode (e.g., errors='replace') or decode defensively.

Suggested change
raise RuntimeError(
f"capiscio server exited unexpectedly:\n"
f"stdout: {stdout.decode()}\n"
f"stderr: {stderr.decode()}"
# Decode defensively so we don't mask the real failure with UnicodeDecodeError
try:
stdout_text = stdout.decode(errors="replace") if stdout is not None else ""
except Exception:
stdout_text = "<failed to decode stdout>"
try:
stderr_text = stderr.decode(errors="replace") if stderr is not None else ""
except Exception:
stderr_text = "<failed to decode stderr>"
# Ensure internal state and resources are cleaned up before raising
self.stop()
raise RuntimeError(
"capiscio server exited unexpectedly:\n"
f"stdout: {stdout_text}\n"
f"stderr: {stderr_text}"

Copilot uses AI. Check for mistakes.
- Add _drain_pipes() to close stdout/stderr after successful startup,
  preventing OS pipe buffer fill on long-lived server processes
- Use errors='replace' in .decode() calls to avoid masking real failures
  with UnicodeDecodeError
- Call self.stop() before raising in _wait_grpc_ready and socket wait
  loop to keep singleton state consistent
@github-actions
Copy link

✅ Documentation validation passed!

Unified docs will be deployed from capiscio-docs repo.

@github-actions
Copy link

✅ All checks passed! Ready for review.

@github-actions
Copy link

✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests.

@beonde beonde merged commit f20dee9 into main Mar 11, 2026
13 checks passed
@beonde beonde deleted the fix/windows-tcp-fallback branch March 11, 2026 21:21
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