mihomo-party-org/clash-party · critical

Core PID ${proc.pid ?? 'unknown'} is still running after SIG

Error message

Core PID ${proc.pid ?? 'unknown'} is still running after SIGKILL

What it means

This error is thrown in the core stop path (src/main/core/manager.ts:875) after SIGINT failed to stop the core process and a follow-up SIGKILL also failed: waitForExit() still reports the child running. The library throws because it cannot guarantee the previous core process is dead, which would make a subsequent start unreliable (port conflicts, stale state).

Source

Thrown at src/main/core/manager.ts:875

setStopCoreBeforeAdminRestart(stopCore)

async function ensureCoreProcessExited(proc: ChildProcess | null): Promise<void> {
  if (!proc) return

  const waitForExit = async (): Promise<boolean> => {
    const deadline = Date.now() + coreShutdownTimeout
    while (proc.exitCode === null && proc.signalCode === null && Date.now() < deadline) {
      await delay(50)
    }
    return proc.exitCode !== null || proc.signalCode !== null
  }

  if (await waitForExit()) return
  managerLogger.warn(`Core PID ${proc.pid ?? 'unknown'} did not exit after SIGINT; sending SIGKILL`)
  proc.kill('SIGKILL')
  if (!(await waitForExit())) {
    throw new Error(`Core PID ${proc.pid ?? 'unknown'} is still running after SIGKILL`)
  }
}

async function restartCoreOnce(forceStop: boolean): Promise<void> {
  const startAttempt = await runCoreOperation(async () => {
    const previousChild = child
    await stopCoreInternal(forceStop)
    if (process.platform === 'darwin') await ensureCoreProcessExited(previousChild)
    return startCoreInternal(false, true)
  })
  await startAttempt.readiness
}

function trackCoreRestart(operation: () => Promise<void>): Promise<void> {
  if (pendingRestart) return pendingRestart

  isRestarting = true
  const restart = operation().finally(() => {

View on GitHub (pinned to 911e090537)

Solutions

  1. Investigate why the core ignores SIGKILL: check process state (ps/proc), permissions, containers, or I/O hangs
  2. Increase the waitForExit timeout if exits are merely slow
  3. Escalate externally (kill -9 on the host, container restart) and verify the port is free before restarting the core
  4. Report/log the stuck PID for diagnosis rather than silently starting a new core over the old one

Example fix

// before
if (!(await waitForExit())) {
  throw new Error(`Core PID ${proc.pid ?? 'unknown'} is still running after SIGKILL`)
}
// after
if (!(await waitForExit())) {
  managerLogger.error(`Core PID ${proc.pid ?? 'unknown'} unkillable; state=${proc.signalCode}`)
  throw new CoreProcessUnkillableError(proc.pid)
}
Defensive patterns

Strategy: retry

Validate before calling

const stale = await isPidAlive(proc.pid)
if (stale) {
  await killProcessTree(proc.pid, { force: true })
  await waitForPortFree(corePort)
}

Try / catch

try {
  await stopCore()
} catch (err) {
  if (err.message.includes('still running after SIGKILL')) {
    await escalateExternalKill(proc.pid)
    await waitForPortFree(corePort)
    await startCore()
  } else throw err
}

Prevention

When it happens

Trigger: stopCoreInternal sends SIGINT, the core ignores it, SIGKILL is sent, and waitForExit() still finds the PID alive after the timeout — typically a stuck or zombie child, PID reuse, or a process in uninterruptible kernel state (D state).

Common situations: Core binary hung on I/O or a dead lock making it un-killable; container/permission issues where SIGKILL cannot reach the process; PID reused by another process; extremely slow exit exceeding the wait timeout.

Related errors


AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30). Data as JSON: /api/errors/628b59ca55b4c64b. Report an issue: GitHub.