(settings: DaemonClientSettings)
| 227 | } |
| 228 | |
| 229 | async function startLocalDaemon(settings: DaemonClientSettings): Promise<EnsuredDaemon> { |
| 230 | let lockRecoveryCount = 0; |
| 231 | const cleanupResults: DaemonStartupCleanupResult[] = []; |
| 232 | let startError: string | undefined; |
| 233 | let daemonProcess: ExecDetachedExit | { pid: number } | undefined; |
| 234 | for (let attempt = 1; attempt <= DAEMON_STARTUP_ATTEMPTS; attempt += 1) { |
| 235 | let launch: DaemonStartupLaunch; |
| 236 | try { |
| 237 | launch = startDaemon(settings); |
| 238 | daemonProcess = { pid: launch.pid }; |
| 239 | } catch (error) { |
| 240 | startError = error instanceof Error ? error.message : String(error); |
| 241 | cleanupResults.push(await cleanupFailedDaemonStartupMetadata(settings.paths, 'start_error')); |
| 242 | if (attempt < DAEMON_STARTUP_ATTEMPTS) { |
| 243 | await sleep(150); |
| 244 | continue; |
| 245 | } |
| 246 | break; |
| 247 | } |
| 248 | |
| 249 | const startup = await waitForDaemonStartup(DAEMON_STARTUP_TIMEOUT_MS, settings, launch); |
| 250 | if (startup.kind === 'ready') return { info: startup.info, startedByClient: true }; |
| 251 | if (startup.kind === 'early_exit') { |
| 252 | daemonProcess = startup.exit; |
| 253 | startError = describeDaemonEarlyExit(startup.exit); |
| 254 | cleanupResults.push(await cleanupFailedDaemonStartupMetadata(settings.paths, 'start_error')); |
| 255 | if (attempt < DAEMON_STARTUP_ATTEMPTS) { |
| 256 | await sleep(150); |
| 257 | continue; |
| 258 | } |
| 259 | break; |
| 260 | } |
| 261 | |
| 262 | if (await recoverDaemonLockHolder(settings.paths)) { |
| 263 | lockRecoveryCount += 1; |
| 264 | continue; |
| 265 | } |
| 266 | |
| 267 | const metadataState = getDaemonMetadataState(settings.paths); |
| 268 | const hasAnotherAttempt = attempt < DAEMON_STARTUP_ATTEMPTS; |
| 269 | const cleanup = await cleanupFailedDaemonStartupMetadata(settings.paths, 'startup_timeout', { |
| 270 | stopLiveProcesses: false, |
| 271 | }); |
| 272 | cleanupResults.push(cleanup); |
| 273 | if (cleanup.retainedInfoProcess || cleanup.retainedLockProcess) { |
| 274 | const extended = await waitForDaemonStartup(DAEMON_STARTUP_TIMEOUT_MS, settings, launch); |
| 275 | if (extended.kind === 'ready') return { info: extended.info, startedByClient: true }; |
| 276 | if (extended.kind === 'early_exit') { |
| 277 | daemonProcess = extended.exit; |
| 278 | startError = describeDaemonEarlyExit(extended.exit); |
| 279 | } |
| 280 | break; |
| 281 | } |
| 282 | if (!hasAnotherAttempt) break; |
| 283 | |
| 284 | // Detached daemon startup can race on busy CI hosts; retry when no metadata exists yet. |
| 285 | if (!metadataState.hasInfo && !metadataState.hasLock) await sleep(150); |
| 286 | } |
no test coverage detected