agalwood/Motrix · critical · AppError
EngineProcessTerminationFailed
EngineProcessTerminationFailed
Error message
aria2 process ${expectedPid} did not exit after force termination What it means
Thrown as an AppError (code EngineProcessTerminationFailed) by Aria2ProcessManager.forceTerminateVerified after it sends a force-kill (SIGKILL via kill() or inspector.forceTerminate) and then polls inspector.isAlive(expectedPid) in a loop for up to 5 seconds (100ms intervals). If the process is still alive after the 5s deadline, this error fires. This indicates the OS could not terminate the process — typically a permissions issue or an uninterruptible state.
Source
Thrown at src/core/engine/aria2/aria2-process-manager.ts:272
'The process changed or is not verified as Motrix-owned'
)
}
if (
processInfo.ownership === EngineProcessOwnership.CurrentApp &&
this.getPid() === expectedPid
) {
this.kill()
} else {
this.inspector.forceTerminate(expectedPid)
}
const deadline = Date.now() + 5_000
while (this.inspector.isAlive(expectedPid) && Date.now() < deadline) {
await new Promise<void>((resolve) => setTimeout(resolve, 100))
}
if (this.inspector.isAlive(expectedPid)) {
throw new AppError(
ErrorCode.EngineProcessTerminationFailed,
`aria2 process ${expectedPid} did not exit after force termination`
)
}
await this.clearOwnershipRecord(expectedPid)
}
private toProcessInfo(
inspected: InspectedProcess,
ownership: EngineProcessOwnership,
safeToTerminate: boolean
): EngineProcessInfo {
return {
pid: inspected.pid,
name: inspected.name,
executableName: inspected.executablePath
? path.basename(inspected.executablePath)
: null,View on GitHub (pinned to 1a708ee577)
Solutions
- Manually kill the process with elevated privileges: sudo kill -9 <expectedPid>
- Check the process state (ps -o pid,stat,cmd -p <expectedPid>) — if it's in D state, address the underlying I/O issue or reboot
- If the PID was reused, verify with inspectPort and update the expected PID
- Restart the application or the host to clear the stuck process
- In containers, ensure the init process (tini/dumb-init) forwards signals correctly
Defensive patterns
Strategy: try-catch
Try / catch
try {
await processManager.forceTerminateVerified(expectedPid, port, expected)
} catch (e) {
if (e instanceof AppError && e.code === ErrorCode.EngineProcessTerminationFailed) {
// Process won't die — escalate to OS-level kill or prompt user to restart
log.error({ pid: expectedPid }, 'force termination failed, manual intervention required')
await promptUserRestart()
} else throw e
} Prevention
- Check process state (ps -o pid,stat) before relying on force termination — D-state processes need a reboot
- Ensure the app has sufficient OS permissions to kill the aria2 process
- Use container init processes (tini/dumb-init) that properly forward signals in Docker/Podman
When it happens
Trigger: forceTerminateVerified kills the verified process, then enters a wait loop (deadline = now + 5000ms, polling every 100ms); after the deadline, inspector.isAlive(expectedPid) still returns true.
Common situations: The process is running as a different user/root and the current process lacks kill permission; the process is in an uninterruptible sleep state (D state, e.g. stuck on a disk I/O or NFS); zombie process that hasn't been reaped by its parent; containerized environment where kill signals are restricted; the PID was reused by a new process.
Related errors
- EngineProcessOwnershipUnverified
- EngineProtocolError
- EngineStartFailed
- EngineProcessOwnershipUnverified
- aria2 addTorrent gid must contain exactly 16 hexadecimal cha
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/df3a030e966829c6.
Report an issue: GitHub.