Skip to content

fix: Unity connection stays alive: the server restarts itself after failures and the CLI detects frozen Editors instead of hanging - #1312

Merged
hatayama merged 14 commits into
v3-betafrom
fix/ipc-connection-stability
Jun 12, 2026
Merged

fix: Unity connection stays alive: the server restarts itself after failures and the CLI detects frozen Editors instead of hanging#1312
hatayama merged 14 commits into
v3-betafrom
fix/ipc-connection-stability

Conversation

@hatayama

@hatayama hatayama commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • The Unity-side IPC server now recovers on its own after a failed restart, instead of staying unreachable until the next domain reload.
  • The CLI now tells a frozen Editor apart from a slow command: it reports main-thread stalls live and fails with a clear recovery hint within minutes, instead of hanging silently for up to 30 minutes.
  • Commands issued while Unity is reloading a large project now wait for the reload to finish instead of failing after 10 seconds.
  • Windows is covered for the first time: the named pipe listener got the same stop-race hardening as the Unix listener, and CI now runs the Go test suite on Windows including real named pipe tests.

User Impact

  • Before: one failed server recovery (typically a readiness timeout during a heavy import right after a domain reload) left every uloop command failing with 'server is not responding' until the next reload or a manual restart. Now recovery retries on a 5s/15s/30s backoff and the server comes back on its own.
  • Before: when the Editor froze mid-command, run-tests and similar long commands sat silent for up to 30 minutes. Now the server sends a heartbeat every 10 seconds carrying how long the editor main thread has been unresponsive; the CLI shows the stall as progress after 30 seconds and fails after 5 minutes with a message pointing at 'uloop launch -r'. If frames stop entirely, the CLI fails after 60 seconds of silence instead of waiting out the deadline.
  • Before: a domain reload longer than 10 seconds made commands fail spuriously. Dial retries now extend to 60 seconds while a running Unity process for the project is confirmed; behavior with no Unity running is unchanged (immediate failure).
  • Before: compile recovery after a domain reload depended on matching English error strings, which silently breaks on Windows named pipes and non-English locales. Classification is now based on typed error causes, including go-winio's pipe deadline error which is not a standard deadline type.
  • Before: on Windows, a stop racing with the next accept could leave the server's accept loop blocked on a pipe nothing can wake. The named pipe listener now hands the pipe to exactly one stopper and refuses to accept once stopped, so the server loop exits and recovers.
  • Several shutdown races that could degrade the server over time are fixed: the 'already disposed CancellationTokenSource' errors seen in Editor.log, a double-close race in the Unix socket listener that could make the accept loop spin, exceptions escaping async void server start/stop, and connections that kept reading after their framing state became unusable.

Changes

  • UnityCliLoopServerController: recovery retries with backoff (abandoned if the user manually stops the server); async void start/stop replaced with observed fire-and-forget tasks.
  • UnityCliLoopBridgeServer: per-request heartbeat sender with a per-connection write lock; CTS disposal skipped when shutdown tasks outlive the 5s timeout; broken reassembler state now closes the connection.
  • BridgeTransportListener: both the Unix socket and Windows named pipe listeners make Stop idempotent via Interlocked and surface accept-after-stop as ObjectDisposedException so the server loop exits and recovers.
  • JsonRpcProcessor: heartbeat negotiation via an acceptsHeartbeat request flag answered with heartbeatIntervalSeconds in the dispatch ack. Old CLI / new server and new CLI / old server pairs keep the legacy behavior; MINIMUM_REQUIRED_CLI_VERSION intentionally unchanged.
  • Go CLI: typed transport-error classification (errors.Is / os.IsTimeout / Timeout()-probe through the unwrap chain, with the string match kept as fallback); heartbeat-aware sliding silence deadline that never extends compile's explicit short response timeout; unity-alive extension of the dial-retry window.
  • CI: the windows-latest job now runs the transport package tests on Windows, including Windows-only tests against a real winio named pipe (heartbeat deadline sliding, silence diagnosis, typed EOF on pipe close, fast failure on a missing pipe).

Verification

  • scripts/check-go-cli.sh passes (fmt, vet, lint, tests, dist rebuild); GOOS=windows go vet passes for the Windows-tagged code. cli/contract.json and MINIMUM_REQUIRED_CLI_VERSION are bumped to 3.0.0-beta.30 per the lockstep convention enforced by the CI minimum-version gate.
  • cli/dist/darwin-arm64/uloop compile: no errors, no warnings.
  • Full EditMode suite via the rebuilt CLI over a heartbeat-negotiated connection: 1215/1217 passed. The 2 failures (InputVisualizationCanvasPrefabTests, OnionAssemblyDependencyTests) are pre-existing on the base and untouched by this change.
  • New tests: recovery backoff (3), shutdown CTS disposal (2), Unix listener stop (2), Windows listener stopped-state (2, run on all platforms), heartbeat negotiation and frames (6 C#, 5 Go), transport error classification (5), retry window extension (2), Windows named pipe E2E (4, run by the new Windows CI job).
  • Remaining manual step: an end-to-end pass with a real Unity Editor on Windows (named pipe listener + heartbeats in the actual Editor).

hatayama added 8 commits June 12, 2026 13:21
…er down

A single recovery failure (typically the 30s readiness timeout firing while
Unity is still busy with a heavy import right after a domain reload) used to
log an error and give up, leaving the IPC server unreachable until the next
domain reload or a manual restart.

ExecuteTrackedRecoveryAsync now retries the recovery action on a 5s/15s/30s
backoff schedule before surfacing the failure. The total backoff stays inside
the CLI's 180s readiness polling window, so a late success is still picked up
by a waiting uloop command. An explicit Stop Server issued during the backoff
abandons the retry so manual intent always wins.
StartServer/StopServer were async void, so an exception such as the
InvalidOperationException thrown by MarkServerReadyAsync on readiness probe
failure escaped to the Unity synchronization context as an unhandled
exception. Routing through Task + Forget() keeps the fire-and-forget call
shape while observing and logging failures.
isTransportDisconnectError and isFinalResponseTimeoutError matched substrings
like "EOF" and "i/o timeout". Domain reload recovery (compile status polling,
execute-dynamic-code reconnect, play-mode wait) depends on this classification,
and error strings differ across platforms and locales — Windows named pipe
errors in particular do not contain the POSIX phrases, so a reload during
compile could surface as a hard failure instead of recovering.

Check errors.Is against io.EOF/net.ErrClosed/syscall causes and
os.ErrDeadlineExceeded/os.IsTimeout first, keeping the string match only as a
fallback for errors without a typed cause. The classifiers move to
transport_errors.go because they are shared by several wait paths and
compile_wait.go exceeded the 500-line architecture limit.
…r progress

ValidateState() returning false (disposed reassembler) was silently ignored in
the read loop, so the connection kept reading bytes it could never frame and
the CLI hung until its own timeout. Breaking out of the loop closes the
connection, which the CLI observes immediately as a disconnect.
… timeout

DisposeCancellationSourceAfterServerTaskAsync disposed the source
unconditionally after the 5s shutdown wait, even when a client task was still
running (e.g. blocked on a main-thread switch during domain reload). Such a
task then hit ObjectDisposedException the next time it touched its token —
the "already disposed CancellationTokenSource" errors seen in Editor.log.

When the wait times out the source is now intentionally leaked instead of
disposed: one undisposed CTS is harmless, a disposed-in-use one is not.
…allers

Stop() is reachable from three threads: the accept-loop cancellation
registration, StopServer on the main thread, and unexpected-exit cleanup on
the thread pool. The unguarded close-then-null sequence allowed a double
Close on the same socket and left AcceptClient to dereference a nulled
_listener, which surfaced as NullReferenceException and made the accept loop
spin instead of recovering.

Interlocked.Exchange hands the socket to exactly one Stop caller, and
AcceptClient on a stopped listener now throws ObjectDisposedException, which
ServerLoopAsync already treats as an exit-and-recover signal.
…d running

Domain reload on large projects can keep the IPC endpoint down well past the
10s retry window, so commands issued during an externally triggered reload
(e.g. the user saving a script) failed with "server not responding" even
though Unity was fine.

Undispatched dial failures — which are always safe to retry — now keep
retrying for base * 6 (60s) as long as a running Unity process is found.
Behavior elsewhere is unchanged: no Unity process still fails immediately,
busy responses stay bounded by the 10s base window, and each attempt keeps
the base accept bound so a dispatched-but-never-acked request still fails at
10s instead of hanging for the extended window.
…command

Long-running commands (run-tests and other non-compile tools) waited on a
30-minute absolute read deadline. When the Editor froze with the process
alive, the CLI sat silent for the full window with no diagnosis.

The server now sends a heartbeat frame every 10 seconds while an accepted
request executes, written by a background timer and carrying how long the
editor main thread has gone without an EditorApplication.update tick. The
CLI distinguishes three cases:

- No frame for six intervals: the connection or server stalled; fail with a
  heartbeat-silence diagnosis instead of waiting out the 30-minute deadline.
- Main thread stalled over 5 minutes: the Editor is likely frozen; fail with
  a diagnosis pointing at 'uloop launch -r'. Stalls over 30 seconds surface
  as spinner progress before that.
- Heartbeats flowing: keep waiting up to the unchanged 30-minute cap.

Negotiation is per-request via an acceptsHeartbeat flag answered by
heartbeatIntervalSeconds in the dispatch ack, so old CLI/new server and new
CLI/old server pairs keep the legacy absolute-deadline behavior and
MINIMUM_REQUIRED_CLI_VERSION intentionally stays unchanged. Compile's
explicit short response timeout also stays absolute: heartbeat frames are
skipped but never extend it, preserving the fallback to status polling.
Frame writes now share a per-connection lock because heartbeats and
responses are written from different threads.
@coderabbitai

coderabbitai Bot commented Jun 12, 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d960c866-48e6-4f7f-ac12-6f235f34e09e

📥 Commits

Reviewing files that changed from the base of the PR and between 527c7e1 and 72063fa.

📒 Files selected for processing (5)
  • cli/internal/cli/control_play_mode_wait.go
  • cli/internal/cli/run.go
  • cli/internal/cli/spinner.go
  • cli/internal/cli/spinner_test.go
  • cli/internal/unityipc/client.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/internal/unityipc/client.go

📝 Walkthrough

Walkthrough

Adds negotiated JSON-RPC heartbeats (processor + server + client), editor main-thread liveness tracking, safer transport stop/accept semantics, recovery retry/backoff and shutdown-timeout behavior, transport error helpers, many new tests, and a CLI version bump with Windows Go test steps.

Changes

Heartbeat, retry, and transport hardening

Layer / File(s) Summary
Heartbeat contracts and editor liveness
Packages/src/Editor/Infrastructure/Threading/EditorMainThreadLivenessTracker.cs, Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs, Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs, Assets/Tests/Editor/JsonRpcHeartbeatTests.cs, Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs
Adds EditorMainThreadLivenessTracker, parses uloop.acceptsHeartbeat, extends early-response writer signature to include a heartbeat JSON factory, and updates tests for the changed delegate shape and heartbeat metadata.
Server heartbeat emission flow
Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs
Serializes framed writes with a per-connection write lock, coordinates heartbeat sending/stopping with final response writes, and adds helpers for heartbeat lifecycle management.
CLI heartbeat client and tests
cli/internal/unityipc/client.go, cli/internal/unityipc/client_heartbeat_test.go, cli/internal/unityipc/client_windows_test.go, .github/workflows/*
Client advertises acceptsHeartbeat, slides a heartbeat-silence deadline, detects main-thread stall, and is exercised by TCP and Windows named-pipe tests; CI workflow adds a Windows Go test step.
Transport listener stop/disposal semantics
Packages/src/Editor/Infrastructure/BridgeTransportListener.cs, Assets/Tests/Editor/BridgeTransportListenerTests.cs
Makes Unix socket and Windows named-pipe listeners surface stopped state as ObjectDisposedException, atomically swap and dispose active handles, and adds idempotent-stop and post-stop accept tests.
Shutdown timeout and cancellation cleanup
Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs, Assets/Tests/Editor/UnityCliLoopBridgeServerShutdownTests.cs
Bounds token-source disposal with a timeout during shutdown; skips disposal if shutdown tasks outlive the timeout and tests both timely disposal and timeout skipping.
Recovery backoff and controller retries
Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs, Packages/src/Editor/ToolContracts/UnityCliLoopServerConfig.cs, Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs
Adds configurable recovery retry delays, injects wait strategy into controller, changes Start/Stop to synchronous wrappers that .Forget() async tasks, and implements retry loop with backoff and tests covering retry/abandon scenarios.
Connection retry window and transport error helpers
cli/internal/cli/connection_retry.go, cli/internal/cli/transport_errors.go, cli/internal/cli/transport_errors_test.go, cli/internal/cli/connection_retry_test.go
Introduces extended unity-alive retry window while keeping busy-RPC bounded, centralizes transport error classification predicates, and adds tests for typed/unwrapped error matching and retry timing.
UI spinner adapter and tests
cli/internal/cli/spinner.go, cli/internal/cli/spinner_test.go, cli/internal/cli/run.go, cli/internal/cli/control_play_mode_wait.go
Adds newSpinnerProgressFunc, maps unityipc progress events to contextual spinner messages, replaces inline spinner update callbacks across commands, and adds tests validating event mapping and payload passthrough.
Misc tests, delegate shape updates, and CLI version bump
Assets/Tests/Editor/*, cli/contract.json, Packages/src/Editor/Domain/CliConstants.cs, .github/workflows/build-and-test.yml
Adds/updates multiple editor tests (heartbeats, bridge listeners, shutdown, startup-lock), updates existing test delegate usages to the new early-response signature, bumps CLI minimum from beta.29 to beta.30, and adds Windows Go test step for CLI transport tests.

Sequence Diagram(s):

sequenceDiagram
  participant Client
  participant BridgeServer
  participant JsonRpcProcessor
  participant EditorMainThreadLivenessTracker
  Client->>BridgeServer: send request with uloop.acceptsHeartbeat=true
  BridgeServer->>JsonRpcProcessor: ProcessRequestWithEarlyResponseAsync(...)
  JsonRpcProcessor->>EditorMainThreadLivenessTracker: SecondsSinceLastMainThreadTick()
  EditorMainThreadLivenessTracker-->>JsonRpcProcessor: mainThreadStallSeconds
  JsonRpcProcessor-->>BridgeServer: dispatch-accepted (+heartbeat factory)
  BridgeServer-->>Client: accepted response and then heartbeat frames
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: server restarts after failures and CLI detects frozen Editors instead of hanging, which aligns with the substantial recovery retry, heartbeat, and detection logic changes across both server and CLI.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering recovery retries, heartbeat negotiation, timeout handling, transport error classification, listener hardening, and CI improvements—all of which are present in the file summaries.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ipc-connection-stability

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 and usage tips.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/internal/unityipc/client.go (1)

199-205: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate ctx cancellation before waiting for the dispatch ack.

watchConnectionCancellation starts only after the "accepted" frame arrives. If Unity accepts the socket and then never sends that first response, the read on Lines 199-205 sits on the fixed accept deadline, so caller cancellation can hang until the accept timeout expires.

Suggested fix
  if deadline, ok := acceptCtx.Deadline(); ok {
  	_ = conn.SetDeadline(deadline)
  }
+
+	stopCancelWatcher := watchConnectionCancellation(ctx, conn)
+	defer stopCancelWatcher()

  writeStartedAt := time.Now()
  if err := Write(conn, payload); err != nil {
  	timing.Write = time.Since(writeStartedAt)
  	timing.Total = time.Since(startedAt)
@@
 		if err := applyPostAcceptDeadline(conn, heartbeatSilence, absoluteDeadline); err != nil {
 			timing.Total = time.Since(startedAt)
 			outcome.Timing = timing
 			return outcome, err
 		}
-		stopCancelWatcher := watchConnectionCancellation(ctx, conn)
-		defer stopCancelWatcher()

 		for {

Also applies to: 221-223

🤖 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 `@cli/internal/unityipc/client.go` around lines 199 - 205, Start watching ctx
cancellation before blocking on readRPCResponse by invoking the same
cancellation watcher used by watchConnectionCancellation (or creating a small
goroutine) immediately after creating reader and before calling readRPCResponse;
on ctx.Done close the conn (or set a short read deadline) so the blocked read
returns promptly. Update both the read at the readRPCResponse call (reader :=
bufio.NewReader(conn); response, err := readRPCResponse(...)) and the analogous
block around lines 221-223 to ensure the cancellation watcher runs prior to
waiting for the "accepted"/dispatch ack, and preserve existing timing/outcome
assignment on error return.
🤖 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 `@cli/internal/unityipc/client_heartbeat_test.go`:
- Around line 205-217: The server writer goroutine is still running when
assertions run, causing spurious broken-pipe failures because defer close(done)
executes after assertions; change the test to stop the writer immediately after
the client times out by replacing defer close(done) with an explicit close(done)
right after SendWithProgressOutcome returns (or alternatively make writeFrame
failures non-fatal within the server goroutine). Update the test around
startHeartbeatTestServer/done/writeFrame/heartbeatAck/heartbeat and ensure
close(done) happens before asserting the timeout result.

---

Outside diff comments:
In `@cli/internal/unityipc/client.go`:
- Around line 199-205: Start watching ctx cancellation before blocking on
readRPCResponse by invoking the same cancellation watcher used by
watchConnectionCancellation (or creating a small goroutine) immediately after
creating reader and before calling readRPCResponse; on ctx.Done close the conn
(or set a short read deadline) so the blocked read returns promptly. Update both
the read at the readRPCResponse call (reader := bufio.NewReader(conn); response,
err := readRPCResponse(...)) and the analogous block around lines 221-223 to
ensure the cancellation watcher runs prior to waiting for the
"accepted"/dispatch ack, and preserve existing timing/outcome assignment on
error return.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 12677c06-b93e-4c24-825a-74b2db90a32a

📥 Commits

Reviewing files that changed from the base of the PR and between 325aba4 and 7f5992a.

⛔ Files ignored due to path filters (4)
  • Assets/Tests/Editor/BridgeTransportListenerTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/JsonRpcHeartbeatTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/UnityCliLoopBridgeServerShutdownTests.cs.meta is excluded by none and included by none
  • Packages/src/Editor/Infrastructure/Threading/EditorMainThreadLivenessTracker.cs.meta is excluded by none and included by none
📒 Files selected for processing (20)
  • Assets/Tests/Editor/BridgeTransportListenerTests.cs
  • Assets/Tests/Editor/JsonRpcHeartbeatTests.cs
  • Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs
  • Assets/Tests/Editor/UnityCliLoopBridgeServerShutdownTests.cs
  • Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs
  • Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs
  • Packages/src/Editor/Infrastructure/Api/JsonRpcRequest.cs
  • Packages/src/Editor/Infrastructure/BridgeTransportListener.cs
  • Packages/src/Editor/Infrastructure/InfrastructureEditorStartup.cs
  • Packages/src/Editor/Infrastructure/Server/UnityCliLoopServerController.cs
  • Packages/src/Editor/Infrastructure/Threading/EditorMainThreadLivenessTracker.cs
  • Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopServerConfig.cs
  • cli/internal/cli/compile_wait.go
  • cli/internal/cli/connection_retry.go
  • cli/internal/cli/connection_retry_test.go
  • cli/internal/cli/transport_errors.go
  • cli/internal/cli/transport_errors_test.go
  • cli/internal/unityipc/client.go
  • cli/internal/unityipc/client_heartbeat_test.go
💤 Files with no reviewable changes (1)
  • cli/internal/cli/compile_wait.go

Comment thread cli/internal/unityipc/client_heartbeat_test.go
hatayama added 2 commits June 12, 2026 13:35
The Windows listener had the same class of race fixed for the Unix socket
listener: Stop() disposed and nulled _activePipe non-atomically while the
accept loop, the cancellation registration, and cleanup paths all touch the
field. A Stop racing with the next accept could null the reference of a
freshly created pipe without disposing it, leaving the accept loop blocked
on a pipe nothing can wake.

Stop now hands the pipe to exactly one caller via Interlocked.Exchange and
marks the listener stopped; accepting on a stopped listener surfaces as
ObjectDisposedException so ServerLoopAsync exits and triggers recovery, and
a post-publish re-check closes the window between the stopped check and the
pipe assignment. The stopped-state paths run before any pipe is created, so
their tests execute on all platforms.
… errors

Go tests never ran on Windows in CI, so the Windows-only transport code
(named pipe dialing, winio deadlines) was effectively unverified. The
windows-latest job now runs the Go test suite, and new Windows-only tests
exercise a real winio named pipe end to end: heartbeats sliding the silence
deadline, silence failing with a timeout-classified diagnosis, pipe close
surfacing as typed io.EOF, and a missing pipe failing fast as a retryable
connection attempt.

Writing those tests surfaced a classification gap: go-winio's deadline error
is not os.ErrDeadlineExceeded and os.IsTimeout does not unwrap wrapped
errors, so the heartbeat-silence diagnosis and timeout classification missed
it. Both classifiers now probe the unwrap chain for a Timeout() cause.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found and verified against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs Outdated
Comment thread cli/internal/unityipc/client_heartbeat_test.go
hatayama added 2 commits June 12, 2026 13:41
Running the full Go suite on windows-latest surfaced many pre-existing
Windows-incompatible tests outside the transport layer (path separator
assumptions, helper binaries built without .exe, TCP-only fixtures in
release automation tests). Fixing those is unrelated to this change; the
job's purpose is to execute the named pipe transport code, so the run is
scoped to internal/unityipc where the Windows-only tests live.
The repository convention moves cli/contract.json and
MINIMUM_REQUIRED_CLI_VERSION in lockstep on Go CLI changes, and the CI
minimum-version gate enforces it. Strictly speaking the package does not
depend on new CLI behavior (heartbeats are negotiated per request and both
old/new pairings keep working), but the lockstep bump keeps the shipped CLI
and package aligned and lets the gate confirm the decision was deliberate.

@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: 1

🤖 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 `@Packages/src/Editor/Infrastructure/BridgeTransportListener.cs`:
- Around line 183-185: _stop flag is set to true in Stop() and never reset,
causing AcceptClient()/ThrowIfStopped() to always fail after first Stop(); make
the Windows listener restartable by clearing _stopped at the start of Start()
(and/or before reinitializing the pipe) using the same synchronization used
around Stop()/Accept—i.e., set _stopped = false in Start() (and update any other
places where a new accept loop or pipe is created such as the blocks referenced
around AcceptClient()/ThrowIfStopped() and the regions noted at 194-196 and
249-271) so subsequent Start/AcceptClient cycles succeed.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d39d8cd6-cce3-4f90-be47-65effb933e40

📥 Commits

Reviewing files that changed from the base of the PR and between 7f5992a and 625dab4.

📒 Files selected for processing (7)
  • .github/workflows/build-and-test.yml
  • Assets/Tests/Editor/BridgeTransportListenerTests.cs
  • Packages/src/Editor/Infrastructure/BridgeTransportListener.cs
  • cli/internal/cli/transport_errors.go
  • cli/internal/cli/transport_errors_test.go
  • cli/internal/unityipc/client.go
  • cli/internal/unityipc/client_windows_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • cli/internal/cli/transport_errors_test.go
  • cli/internal/cli/transport_errors.go
  • cli/internal/unityipc/client.go

Comment thread Packages/src/Editor/Infrastructure/BridgeTransportListener.cs
…test

The heartbeat frame write used the server token, so StopHeartbeatsAsync could
block on an in-flight write to a slow client and stall the final response;
the write now uses the heartbeat token so stopping heartbeats cancels it.

In the explicit-response-timeout test the server goroutine kept writing
through writeFrame (t.Errorf) after the client timed out and closed the
connection, failing the test on an expected broken pipe. Expected post-close
write failures now end the loop silently.
The IPC client built the "unity editor main thread busy for Ns..."
progress message from heartbeat frames, but every progress callback
discarded its message argument and rewrote the spinner with a static
executing label, so the stall diagnosis never reached the user.

Name the connection-stage progress events and map only those tokens to
the contextual executing message; any other progress payload is
display-ready text and now passes through to the spinner verbatim.

Verified on Windows against a real Editor: a delayCall-scheduled
85-second main-thread sleep made get-hierarchy render "unity editor
main thread busy for 30s..." through "80s..." and complete without
disconnecting.
@hatayama
hatayama merged commit 0392ca9 into v3-beta Jun 12, 2026
8 checks passed
@hatayama
hatayama deleted the fix/ipc-connection-stability branch June 12, 2026 12:24
@github-actions github-actions Bot mentioned this pull request Jun 12, 2026
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