feat: Windows TCP fallback for embedded gRPC server#39
Conversation
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
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
There was a problem hiding this comment.
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--socketelsewhere. - 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.
| 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) |
There was a problem hiding this comment.
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.
capiscio_sdk/_rpc/process.py
Outdated
| raise RuntimeError( | ||
| f"capiscio server exited unexpectedly:\n" | ||
| f"stdout: {stdout.decode()}\n" | ||
| f"stderr: {stderr.decode()}" |
There was a problem hiding this comment.
_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.
| 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}" |
- 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
|
✅ Documentation validation passed!
|
|
✅ All checks passed! Ready for review. |
|
✅ SDK server contract tests passed (test_server_integration.py). Cross-product scenarios are validated in capiscio-e2e-tests. |
Summary
On Windows, Unix domain sockets are not available. The embedded capiscio-core gRPC server previously only used
--socketmode, 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 viasocket.bind(('', 0))_wait_grpc_ready(): Extracted shared gRPC readiness probe (used by both TCP and socket paths)ensure_running(): Routes to TCP onwin32, Unix socket otherwiseclient.pylocalhost:50051on Windows instead ofunix:///tmp/capiscio.sockimport sysTests
Testing
find_binaryfailures excluded — unrelated to this change)