You're running npm run dev on port 3000. You need that port for 30 seconds to test something else. Your only option today: kill the dev server. Restart it later. Lose your in-memory caches, build watcher state, websocket clients, and warm-up time.
I kept doing this. So I built park — a CLI that freezes a running process AND releases its TCP port, then brings it back later. Same PID. Same memory. Same open files.
See it in action
park 3000 # server freezes, port frees
python3 -m http.server 3000 & # grab :3000 for something else
kill %1 # done with it
park resume 3000 # original server is back
curl localhost:3000 # still serving, same warm state
Install in one line:
curl -fsSL https://raw.githubusercontent.com/mr-vaibh/park/main/install.sh | sh
Why not just Ctrl+Z?
Ctrl+Z sends SIGSTOP. The process freezes — but the kernel still considers the port bound. Try binding something else to it:
OSError: [Errno 48] Address already in use
The port is locked until the process actually closes the socket or dies. SIGSTOP does neither.
The trick: inject close(fd) from the outside
The only thing that frees a TCP port is closing the file descriptor that owns the listening socket. park does this without modifying, recompiling, or restarting the target process — it reaches inside and runs close(fd) in the target's own context.
This works on any process: npm run dev, python manage.py runserver, uvicorn, cargo run, anything.
How it actually works
Linux — ptrace + syscall injection
- Find the PID owning the port by walking
/proc/net/tcpand/proc/*/fd/* -
PTRACE_ATTACHto the target process - Save the register state
- Find executable scratch space (the
[vdso]page), write a tinysyscall; int3stub - Set
RAX=3(close),RDI=fd,RIP=scratch,PTRACE_CONT - The target executes one
close()syscall, traps onint3, we read the return value - Restore everything,
PTRACE_DETACH,SIGSTOP
On resume: inject socket() + setsockopt() + bind() + listen() + dup2() to recreate the listener at the exact same fd number. The process picks up exactly where it was.
macOS (Apple Silicon) — Mach kernel injection
macOS doesn't have Linux's ptrace model. The approach is fundamentally different:
-
task_for_pid()to get a Mach port to the target -
task_suspend()to halt every thread -
thread_suspend()each original thread individually — so they stay frozen even when we resume the task -
mach_vm_allocate()three 16KB pages inside the target: code (RX), data (RW), stack (RW) - Write an arm64
LDR+svc+brkstub onto the code page — the stub loads its arguments from the data page via PC-relative LDR instructions rather than trusting registers -
thread_create_running()— spawn a brand-new thread inside the target. We never touch any of the target's existing threads. This was the key insight that made macOS resume reliable. - The injection thread runs the stub, hits
brk #0, we catch it via a Mach exception port - On resume, also inject
kevent()to re-register the new listener with kqueue andfcntl(F_SETFL, O_NONBLOCK)so async frameworks like uvicorn survive
Three bugs that took the longest to find
1. Closing the wrong fd on macOS. The very first syscall after attaching had its x0 register clobbered by the kernel completing the target's in-progress syscall (usually kqueue). So close(3) was actually running as close(0). Fix: load args from memory via LDR instructions instead of trusting register state.
2. Target dying after park exits. Park's Mach exception port registration outlived the process — when park exited, the port became a dead name, and the next breakpoint in the target hit a dead-name send and died. Fix: save and restore the original exception port registration on detach.
3. uvicorn not responding after resume. The recreated listener had no kqueue registration (the kernel removed it when we closed the old fd). Asyncio's event loop never woke up to call accept(). TCP connections piled up in the kernel's backlog. Fix: inject kevent(EVFILT_READ|EV_ADD) to re-register with every kqueue the target held. Also, the new socket was blocking by default — asyncio needs O_NONBLOCK or it hangs in accept() after the first connection.
What it works with
| Target | macOS (Apple Silicon) | Linux x86_64 |
|---|---|---|
| uvicorn / FastAPI | park + resume | park + resume |
| python http.server | park + resume | park + resume |
| Flask / Django dev | park + resume | park + resume |
| Go net/http | park only | park + resume |
| npm run dev (Node) | park only | park + resume |
Try it
curl -fsSL https://raw.githubusercontent.com/mr-vaibh/park/main/install.sh | sh
park --help
Single binary. No runtime dependencies. MIT licensed.
GitHub: github.com/mr-vaibh/park
Website: byvaibhav.com/park
park is written in Go with one dependency (golang.org/x/sys). CGO is used only on macOS for the Mach APIs; the Linux path is pure Go. The full source is ~2000 lines across 20 files.
Top comments (0)