can1357/oh-my-pi · error · Error
Failed to kill ${holder.image} (PID ${holder.pid})
Error message
Failed to kill ${holder.image} (PID ${holder.pid}) What it means
If the holder survives the SIGTERM poll window, terminatePortHolder() escalates to SIGKILL. A non-ESRCH failure of that kill is wrapped as 'Failed to kill <image> (PID <pid>)'. As with the other stages this almost always means the OS refused the signal — normally EPERM against a process the current user does not own.
Source
Thrown at packages/stats/src/port-conflict.ts:216
if (error instanceof Error && "code" in error && error.code === "ESRCH") return;
throw new Error(`Failed to stop ${holder.image} (PID ${holder.pid})`, { cause: error });
}
for (let attempt = 0; attempt < PROCESS_EXIT_POLLS; attempt++) {
await Bun.sleep(PROCESS_EXIT_POLL_MS);
try {
process.kill(holder.pid, 0);
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ESRCH") return;
throw new Error(`Failed to inspect ${holder.image} (PID ${holder.pid})`, { cause: error });
}
}
try {
process.kill(holder.pid, "SIGKILL");
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ESRCH") return;
throw new Error(`Failed to kill ${holder.image} (PID ${holder.pid})`, { cause: error });
}
await Bun.sleep(PROCESS_EXIT_POLL_MS);
}
async function reclaimStatsPort(port: number): Promise<"retry"> {
const holder = await findPortHolder(port);
if (!holder) {
throw new Error(`Port ${port} is in use, but the listening process could not be identified.`);
}
if (holder.pid === process.pid) {
throw new Error(`Port ${port} is held by the current process (${holder.image}, PID ${holder.pid}).`);
}
const normalizedImage = holder.image
.toLowerCase()
.replace(/\.exe$/, "")
.replace(/ \(deleted\)$/, "");
const normalizedCommand = holder.commandLine.toLowerCase().replaceAll("\\", "/");View on GitHub (pinned to 9690622007)
Solutions
- Kill the process manually with sudo: sudo kill -9 <pid>
- Check ps -p <pid> -o user,stat to confirm ownership/state; if uninterruptible (D state), fix the kernel/IO hang first
- Run omp stats as the same user as the port holder, or reconfigure so the port belongs to the current user
- Choose a different port for the stats dashboard to avoid the conflict entirely
Example fix
// before
await prepareStatsPort(port); // throws 'Failed to kill omp (PID 1234)'
// after
try {
await prepareStatsPort(port);
} catch (err) {
if (err.message.includes('Failed to kill')) {
console.error(`Manual intervention needed: sudo kill -9 <pid>`);
} else throw err;
} Defensive patterns
Strategy: try-catch
Validate before calling
const out = Bun.spawnSync(['ps', '-o', 'user=', '-p', String(pid)]);
if (out.stdout.toString().trim() !== process.env.USER) {
console.error(`PID ${pid} owned by another user; SIGKILL will fail without sudo`);
} Type guard
function isNodeErrno(err: unknown): err is NodeJS.ErrnoException {
return err instanceof Error && typeof (err as NodeJS.ErrnoException).code === 'string';
} Try / catch
try {
await prepareStatsPort(port);
} catch (err) {
if (err.message.startsWith('Failed to kill')) {
console.error(`${err.message} — escalate: sudo kill -9 <pid>, or switch ports`);
} else throw err;
} Prevention
- Do not let root or service managers own the stats port
- Verify holder ownership/state (ps -o user,stat) before reclamation attempts
- Configure a dedicated stats port unlikely to collide with other services
- If a process ignores SIGTERM and SIGKILL fails, investigate D-state/hangs rather than retrying
When it happens
Trigger: process.kill(holder.pid, 'SIGKILL') throwing EPERM/EINVAL after the process ignored SIGTERM for the full poll period, during reclaimStatsPort (called from prepareStatsPort/recoverStatsPort).
Common situations: A root-owned or another-user process squatting on the stats port and ignoring SIGTERM; zombies in uninterruptible sleep where even SIGKILL delivery fails; PID handed to a new process between polls with restrictive permissions.
Related errors
- Failed to stop ${holder.image} (PID ${holder.pid})
- Failed to inspect ${holder.image} (PID ${holder.pid})
- Daemon ${operation.name} process is unavailable
- Port ${port} is held by the current process (${holder.image}
- Port ${port} is in use by ${holder.image} (PID ${holder.pid}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/2370a92fdc4b70be.
Report an issue: GitHub.