* Spawn the process and resolve when it answers HTTP, or reject if it * exits before that or the timeout fires. Callers are expected to wrap * port-collision recovery at a higher level (catch failure → pick new port * → new ServerManager).
()
| 76 | * → new ServerManager). |
| 77 | */ |
| 78 | async start(): Promise<void> { |
| 79 | if (this._status !== "stopped" && this._status !== "failed" && this._status !== "exited") { |
| 80 | throw new Error(`cannot start: server is ${this._status}`); |
| 81 | } |
| 82 | this._status = "starting"; |
| 83 | this.opts.output.appendLine(`[server] spawning: ${this.opts.command} ${this.opts.args.join(" ")}`); |
| 84 | |
| 85 | let proc: SpawnedLike; |
| 86 | try { |
| 87 | proc = this.spawnFn(this.opts.command, this.opts.args); |
| 88 | } catch (err) { |
| 89 | this._status = "failed"; |
| 90 | this.opts.output.appendLine(`[server] spawn failed: ${(err as Error).message}`); |
| 91 | throw err; |
| 92 | } |
| 93 | this.proc = proc; |
| 94 | |
| 95 | proc.stdout?.on("data", (chunk) => this.opts.output.appendLine(`[server] ${chunk.toString().trimEnd()}`)); |
| 96 | proc.stderr?.on("data", (chunk) => this.opts.output.appendLine(`[server:err] ${chunk.toString().trimEnd()}`)); |
| 97 | |
| 98 | let exitedEarly = false; |
| 99 | let earlyExitCode: number | null = null; |
| 100 | proc.on("exit", (code) => { |
| 101 | if (this._status === "starting" || this._status === "ready") { |
| 102 | exitedEarly = this._status === "starting"; |
| 103 | earlyExitCode = code; |
| 104 | this._status = this._status === "starting" ? "failed" : "exited"; |
| 105 | this.opts.output.appendLine(`[server] process exited with code ${code}`); |
| 106 | } |
| 107 | }); |
| 108 | proc.on("error", (err) => { |
| 109 | this.opts.output.appendLine(`[server] error: ${err.message}`); |
| 110 | if (this._status === "starting") this._status = "failed"; |
| 111 | }); |
| 112 | |
| 113 | const deadline = Date.now() + this.readinessTimeoutMs; |
| 114 | while (Date.now() < deadline) { |
| 115 | if (exitedEarly) { |
| 116 | throw new Error(`server exited before becoming ready (code ${earlyExitCode})`); |
| 117 | } |
| 118 | const healthy = await this.probeFn(this.opts.url); |
| 119 | if (healthy) { |
| 120 | // Process may have died after the probe but before we checked status. |
| 121 | if (this._status === "starting") { |
| 122 | this._status = "ready"; |
| 123 | this.opts.output.appendLine(`[server] ready at ${this.opts.url}`); |
| 124 | return; |
| 125 | } |
| 126 | } |
| 127 | await delay(this.readinessPollMs); |
| 128 | } |
| 129 | this._status = "failed"; |
| 130 | this.dispose(); |
| 131 | throw new Error(`server did not become ready within ${this.readinessTimeoutMs}ms at ${this.opts.url}`); |
| 132 | } |
| 133 | |
| 134 | dispose(): void { |
| 135 | if (this.proc) { |