You start an app, something goes wrong, you hit Ctrl+C — and now the app won't start again because the port is already in use. Sound familiar? This is the orphaned process problem, and it's more common than it should be.
What Happened
I was running JiuwenClaw, a Python-based AI agent platform. It launches several sub-processes on startup: an agent server and a gateway, each binding to their own port. After terminating the parent with Ctrl+C, the app refused to restart:
Port 19001 already in use
Port 18092 already in use
Why Ctrl+C Doesn't Always Clean Up
When you press Ctrl+C in a terminal, the shell sends SIGINT to the foreground process group. If the parent process spawned children via Python's subprocess module without explicitly adding them to the same process group — or without a signal handler that forwards the signal — those children keep running after the parent dies.
They become orphans: no parent, no controlling terminal, but still holding their network ports open.
Finding the Culprits
Two commands are all you need.
Check which ports are in use:
ss -tlnp | grep -E '19001|18092'
Output:
LISTEN 127.0.0.1:19001 users:(("python3",pid=80615,...))
LISTEN 127.0.0.1:18092 users:(("python3",pid=80613,...))
Identify the processes by PID:
lsof -i :19001 -i :18092
Output:
python3 80613 user ... TCP localhost:18092 (LISTEN)
python3 80615 user ... TCP localhost:19001 (LISTEN)
Confirm what they are:
ps -p 80613 -o pid,ppid,cmd --no-headers
ps -p 80615 -o pid,ppid,cmd --no-headers
Output:
80613 2602 python3 -m jiuwenclaw.app_agentserver
80615 2602 python3 -m jiuwenclaw.app_gateway
There they are — orphaned sub-processes from the previous run, still alive and holding the ports.
Killing Them
Once you have the PIDs:
kill 80613 80615
Verify the ports are clear:
ss -tlnp | grep -E '19001|18092'
No output means the ports are free. You can now restart the app normally.
If a process ignores SIGTERM, escalate with SIGKILL:
kill -9 80613 80615
One-liner: kill by port
If you just want to nuke whatever is on a port without finding the PID first:
fuser -k 19001/tcp 18092/tcp
Or using ss and kill together:
ss -tlnp | grep 19001 | grep -oP 'pid=\K[0-9]+' | xargs kill
Mitigations
1. Fix the application's signal handling (ideal)
The root cause is that the app doesn't forward signals to its children. A well-behaved Python launcher should use a process group and kill the whole group on exit:
import os
import signal
import subprocess
proc = subprocess.Popen(["python3", "-m", "myapp.server"], start_new_session=True)
def cleanup(sig, frame):
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)
Using start_new_session=True puts the child in its own process group, and os.killpg kills the whole group cleanly.
2. Use a stop script or PID file
Many services write a PID file on startup and provide a stop command. If the app supports it, prefer jiuwenclaw-stop over Ctrl+C — a proper stop command can clean up child processes before exiting.
3. Wrap the launcher in a shell trap
If you're starting the app via a shell script, add a trap:
#!/bin/bash
jiuwenclaw-start &
PARENT_PID=$!
cleanup() {
kill -- -$(ps -o pgid= $PARENT_PID | tr -d ' ')
}
trap cleanup EXIT INT TERM
wait $PARENT_PID
The kill -- -<pgid> sends the signal to the entire process group.
4. Run inside a process supervisor
Tools like systemd, supervisord, or s6 track all child processes and kill them as a unit when the service stops. If you're running something in production or semi-permanent, this is the right answer — it also handles restarts, logging, and health checks.
5. Check before you start
Add a pre-flight check to your workflow. Before starting any service, quickly verify the ports it needs are free:
ss -tlnp | grep -E '19001|18092' && echo "Port conflict!" || echo "Ports clear"
Summary
| Step | Command |
|---|---|
| Find listening ports | ss -tlnp \| grep <port> |
| Find PID by port | lsof -i :<port> |
| Confirm process identity | ps -p <pid> -o pid,cmd |
| Kill by PID | kill <pid> |
| Kill by port directly | fuser -k <port>/tcp |
| Verify ports are free | ss -tlnp \| grep <port> |
The quick fix is always kill. The real fix is making sure the app cleans up after itself — or wrapping it in something that does.
No comments:
Post a Comment