{"record":{"id":"28396868dc4de8af","repo":"can1357/oh-my-pi","slug":"port-port-is-held-by-the-current-process-hol","errorCode":null,"errorMessage":"Port ${port} is held by the current process (${holder.image}, PID ${holder.pid}).","messagePattern":"Port (.+?) is held by the current process \\((.+?), PID (.+?)\\)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stats/src/port-conflict.ts","lineNumber":227,"sourceCode":"\t\t}\n\t}\n\n\ttry {\n\t\tprocess.kill(holder.pid, \"SIGKILL\");\n\t} catch (error) {\n\t\tif (error instanceof Error && \"code\" in error && error.code === \"ESRCH\") return;\n\t\tthrow new Error(`Failed to kill ${holder.image} (PID ${holder.pid})`, { cause: error });\n\t}\n\tawait Bun.sleep(PROCESS_EXIT_POLL_MS);\n}\n\nasync function reclaimStatsPort(port: number): Promise<\"retry\"> {\n\tconst holder = await findPortHolder(port);\n\tif (!holder) {\n\t\tthrow new Error(`Port ${port} is in use, but the listening process could not be identified.`);\n\t}\n\tif (holder.pid === process.pid) {\n\t\tthrow new Error(`Port ${port} is held by the current process (${holder.image}, PID ${holder.pid}).`);\n\t}\n\n\tconst normalizedImage = holder.image\n\t\t.toLowerCase()\n\t\t.replace(/\\.exe$/, \"\")\n\t\t.replace(/ \\(deleted\\)$/, \"\");\n\tconst normalizedCommand = holder.commandLine.toLowerCase().replaceAll(\"\\\\\", \"/\");\n\tconst hasStatsIdentity =\n\t\tnormalizedImage === \"omp-stats\" ||\n\t\t/(?:^|[/\"'\\s])omp-stats(?:\\.exe)?(?:[\"'\\s]|$)/.test(normalizedCommand) ||\n\t\t/\\/packages\\/stats\\/src\\/index\\.ts(?:[\"'\\s]|$)/.test(normalizedCommand) ||\n\t\t(normalizedImage === \"omp\" && /(?:^|\\s)stats(?:\\s|$)/.test(normalizedCommand)) ||\n\t\t/(?:^|\\/)omp(?:\\.exe)?[\"'\\s]+stats(?:[\"'\\s]|$)/.test(normalizedCommand);\n\tif (!STATS_RUNTIME_IMAGES[normalizedImage] || !hasStatsIdentity) {\n\t\tthrow new Error(\n\t\t\t`Port ${port} is in use by ${holder.image} (PID ${holder.pid}), which is not identifiable as an omp stats dashboard; refusing to stop it.`,\n\t\t);\n\t}","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/stats/src/port-conflict.ts#L209-L245","documentation":"reclaimStatsPort() refuses to proceed when the PID occupying the stats port is the current process itself. Killing it would terminate the running omp process, so the code throws with the holder image and PID instead of attempting reclamation. This guards against a self-kill during port recovery.","triggerScenarios":"prepareStatsPort or recoverStatsPort finds port occupied and findPortHolder() reports holder.pid === process.pid — i.e. the omp stats server is trying to reclaim a port it is itself already bound to (double startup within one process, or recovery logic triggered by a health-check loop in the same process).","commonSituations":"Two stats-server instances accidentally started in the same process (e.g. HMR reload re-running initialization while the old listener is still open); recovery path invoked from within the already-running server; tests binding the same port in-process.","solutions":["Close the existing in-process listener before attempting reclamation (call the server's stop/close handler)","Deduplicate startup: keep a singleton guard so the stats server initializes once per process","Use a different port for the second instance","If recovery is intentional, skip reclamation and treat 'already bound by self' as success"],"exampleFix":"// before\nawait recoverStatsPort(port); // throws when pid === process.pid\n// after\nif (server) {\n  await server.stop(); // release our own listener first\n}\nawait recoverStatsPort(port);","handlingStrategy":"validation","validationCode":"const holder = await findPortHolder(port);\nif (holder?.pid === process.pid) {\n  console.warn(`Stats port ${port} is already held by this process — reuse the existing server instead of recovering`);\n  return; // or close the existing listener first\n}","typeGuard":"function isSelfHeld(holder: PortHolder | null): boolean {\n  return holder?.pid === process.pid;\n}","tryCatchPattern":"try {\n  await recoverStatsPort(port);\n} catch (err) {\n  if (err.message.includes('held by the current process')) {\n    // the running server already owns the port — treat as healthy, skip recovery\n  } else throw err;\n}","preventionTips":["Guard stats-server startup with a singleton so it initializes once per process","Close the existing in-process listener before attempting port reclamation","Treat 'port held by self' as a healthy state in recovery logic, not a failure","Avoid HMR/reload paths that re-run server initialization without stopping the old instance"],"tags":["port","self-kill","process","startup"],"backgroundTag":"port-held-by-current-process","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}