Skip to content

feat(connector): add Server Status editor window#30

Open
rkdrnf wants to merge 3 commits into
youngwoocho02:mainfrom
rkdrnf:feat/connector-status-window
Open

feat(connector): add Server Status editor window#30
rkdrnf wants to merge 3 commits into
youngwoocho02:mainfrom
rkdrnf:feat/connector-status-window

Conversation

@rkdrnf
Copy link
Copy Markdown
Contributor

@rkdrnf rkdrnf commented May 2, 2026

Summary

Adds a Tools > Unity CLI > Server Status editor window that shows
the connector's runtime state and exposes recovery levers for the
failure scenarios fixed in #29.

⚠️ Depends on #29. This branch is stacked on
feat/connector-failure-recovery. The diff currently includes
both PRs' commits — once #29 merges, this PR's diff will reduce
to the window-specific changes.

What it adds

Editor window

  • Status dot, bound port, current Heartbeat state
  • Queued command count and pending-test list (reads existing
    test-pending-{port}.json files written by TestRunnerState)
  • Start / Stop / Restart buttons that call new
    HttpServer.ManualStart / ManualStop APIs. ManualStop sets
    s_ManuallyStopped so the failure-recovery watchdog respects the
    user's intent (otherwise it would instantly revive the listener
    and the Stop button would be useless)
  • Purge button — drains the queue, faults every in-flight
    TaskCompletionSource with "Purged by user", and calls
    CommandRouter.ResetLock() (the recovery hook exposed in fix(connector): auto-recover when HTTP listener dies #29).
    Gated by EditorUtility.DisplayDialog since it's destructive for
    any waiting CLI client

New connector APIs

  • HttpServer.PendingCount, HttpServer.PurgePending(),
    HttpServer.ManualStart / ManualStop
  • Heartbeat.CurrentState — returns \"stopped\" when the listener
    is down so the window stays truthful after ManualStop

Implementation notes

  • s_Pending (ConcurrentDictionary) tracks in-flight TCS; populated
    in HandleRequest enqueue, drained in ProcessItem finally and
    PurgePending
  • ProcessItem switches to TrySetResult so a racing PurgePending
    doesn't trigger a double-set exception

Test plan

  • Open Tools > Unity CLI > Server Status. With Unity idle:
    Status = Running, Port shown, State = ready, Queued = 0.
  • Click Stop → dot turns gray, State = stopped. Watchdog
    does not revive (verify by waiting >5s and checking logs).
  • Click Start → listener rebinds within ~1 frame.
  • While a long-running command is in flight (e.g. running tests),
    verify Queued count > 0 and the pending test row shows port +
    filter.
  • Click Purge, confirm dialog, verify CLI client receives
    \"Purged by user\" and Queued returns to 0.
  • Stop, then trigger an assembly reload — verify the listener
    restarts (Stop intent should not survive a domain reload).
  • go test ./... passes (no Go-side changes).

rkdrnf added 3 commits May 2, 2026 12:36
The listener could enter several dead states that required an Editor
restart to recover from:

1. Zombie listener: Start() early-returned when s_Listener != null, so
   if the listener crashed without nulling the reference, every later
   call became a no-op.
2. ListenLoop exited via exception without clearing s_Listener, leaving
   the connector permanently dead until the next assembly reload.
3. Initial port-in-use was fatal — once all 10 ports failed at startup,
   nothing ever retried.
4. A hung command handler held CommandRouter's static semaphore
   forever; restarting the HTTP server didn't help because the lock
   lives in CommandRouter.

Changes:
- HttpServer.IsRunning checks the listener actually accepts traffic,
  and Start() tears down a stale reference before rebinding.
- ListenLoop has a finally block that nulls the listener and marks the
  heartbeat stopped if it exits unexpectedly.
- ProcessQueue acts as a watchdog: if the listener is down it calls
  Start() (rate-limited via AUTO_RESTART_INTERVAL), so transient port
  conflicts and silent crashes recover automatically. Failure logging
  is rate-limited to avoid console spam.
- CommandRouter.Dispatch captures the semaphore locally so a
  ResetLock() swap can't make an in-flight call double-release the new
  semaphore. ResetLock() exposes a recovery hook for future UI/CLI
  surfaces that need to clear a hung handler without restarting Unity.
- Heartbeat.MarkStopped() lets failure paths flag the heartbeat file
  as dead so the CLI sees an honest "not responding" state.
MarkStopped() set s_ForcedState = "stopped" and wrote, but the next
Tick (~500ms later) cleared s_ForcedState and wrote a live snapshot
because Tick only guarded on Port == 0 — a port set during the
last successful bind survives a listener crash. Result: the
"stopped" state existed for one heartbeat interval, then got
overwritten with state="ready" while the listener was actually dead.

Switch the guard to !HttpServer.IsRunning so the heartbeat goes
silent when the listener is down, letting MarkStopped's write
persist until the watchdog restarts it.
Adds a Tools > Unity CLI > Server Status window that shows the
connector's runtime state and exposes recovery levers for the
failure scenarios fixed in the prior commit:

- Status dot, port, current Heartbeat state.
- Queued command count and pending-test list (read from existing
  test-pending-{port}.json files).
- Start / Stop / Restart buttons. ManualStop sets s_ManuallyStopped
  so the watchdog respects the user's intent (otherwise the watchdog
  would instantly revive the listener).
- Purge button — drains the queue, faults every in-flight
  TaskCompletionSource with "Purged by user", calls
  CommandRouter.ResetLock(), and deletes test-pending-*.json files.
  Confirmation dialog gates the action since it's destructive for
  any waiting CLI client.

HttpServer now tracks pending TCS in a ConcurrentDictionary so
PurgePending can fault them. ProcessItem switches to TrySetResult
so the racing PurgePending call doesn't double-set.

Heartbeat exposes CurrentState for the window's display row;
returns "stopped" when the listener is down so the dot and label
stay consistent with reality after ManualStop.
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