* Wait for workspace initialization to complete. * Used by tools (bash, file_*) to ensure files are ready before executing. * * Behavior: * - No init state: Returns immediately (init not needed or backwards compat) * - Init succeeded/failed: Returns immediately (tools proceed regardle
(workspaceId: string, abortSignal?: AbortSignal)
| 420 | * @param abortSignal Optional signal to abort the wait early |
| 421 | */ |
| 422 | async waitForInit(workspaceId: string, abortSignal?: AbortSignal): Promise<void> { |
| 423 | const state = this.getInitState(workspaceId); |
| 424 | |
| 425 | // No init state - proceed immediately (backwards compat or init not needed) |
| 426 | if (!state) { |
| 427 | return; |
| 428 | } |
| 429 | |
| 430 | // Init already completed (success or failure) - proceed immediately |
| 431 | // Tools should work regardless of init outcome |
| 432 | if (state.status !== "running") { |
| 433 | return; |
| 434 | } |
| 435 | |
| 436 | // Early exit if already aborted |
| 437 | if (abortSignal?.aborted) { |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | // Init is running - wait for completion promise with timeout |
| 442 | const promiseEntry = this.initPromises.get(workspaceId); |
| 443 | |
| 444 | if (!promiseEntry) { |
| 445 | // State says running but no promise exists (shouldn't happen, but handle gracefully) |
| 446 | log.error(`Init state is running for ${workspaceId} but no promise found, proceeding`); |
| 447 | return; |
| 448 | } |
| 449 | |
| 450 | const INIT_HOOK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes |
| 451 | |
| 452 | // Track cleanup handlers |
| 453 | let timeoutId: NodeJS.Timeout | undefined; |
| 454 | let abortHandler: (() => void) | undefined; |
| 455 | |
| 456 | try { |
| 457 | const abortPromise = new Promise<void>((resolve) => { |
| 458 | if (!abortSignal) return; // Never resolves if no signal |
| 459 | if (abortSignal.aborted) { |
| 460 | resolve(); |
| 461 | return; |
| 462 | } |
| 463 | abortHandler = () => resolve(); |
| 464 | abortSignal.addEventListener("abort", abortHandler, { once: true }); |
| 465 | }); |
| 466 | |
| 467 | // Intentional: provisioning (Coder/devcontainer/etc.) can be long-running, so we |
| 468 | // avoid timeouts until .mux/init begins. The wait is still interruptible via |
| 469 | // abortSignal or workspace deletion (clearInMemoryState). |
| 470 | const phase = state.phase ?? "runtime_setup"; |
| 471 | if (phase === "runtime_setup") { |
| 472 | const first = await Promise.race([ |
| 473 | promiseEntry.promise.then(() => "complete"), |
| 474 | promiseEntry.hookPhasePromise.then(() => "hook"), |
| 475 | abortPromise.then(() => "abort"), |
| 476 | ]); |
| 477 | if (first !== "hook") { |
| 478 | return; |
| 479 | } |
nothing calls this directly
no test coverage detected