Skip to content

Clear a port held by more than one process - #789

Open
opdavies wants to merge 1 commit into
thoughtbot:mainfrom
opdavies:clear-port-listen
Open

Clear a port held by more than one process#789
opdavies wants to merge 1 commit into
thoughtbot:mainfrom
opdavies:clear-port-listen

Conversation

@opdavies

@opdavies opdavies commented Aug 23, 2026

Copy link
Copy Markdown

clear-port killed nothing when more than one process held the port:

kill: `453236
785863': not a pid or valid job spec

lsof prints one PID per line, and quoting the result handed the whole list to kill as a single argument. Piping the PIDs into xargs gives each one an argument of its own.

They go out in a single kill rather than one at a time. A forked server shares its listening socket with every worker, so the parent and the workers all appear in the LISTEN list, and the parent tears its workers down as soon as it is signalled. Signalling one PID at a time would reach those workers after they had already gone and report No such process for a run that worked.

More than one process is the ordinary case rather than an edge case, because lsof -ti4TCP:PORT matches established connections as well as listeners, so a browser with the page open is listed alongside the server it is talking to. -sTCP:LISTEN narrows the match to the listening process, which is both what the script sets out to kill and what stops the browser being killed once kill starts working.

The pipe into xargs is preferred to an unquoted $(...), which batches just as well but trips SC2086 and runs kill with no arguments when the port is free. -r covers that last case on GNU xargs, and is a documented no-op on the BSD xargs that ships with macOS.

@purinkle

Copy link
Copy Markdown
Contributor

Thanks for tracking this down. The bug reproduces for me: with two PIDs, kill "$port_num" gets a single argument containing a newline and fails. Narrowing with -sTCP:LISTEN looks right too.

Two things I ran into while reading it.

The loop can report failure on a run that worked. For a forked server, such as Puma or Node in cluster mode, the parent and every worker inherit the listening socket, so they all appear in the LISTEN list. The loop signals the parent first. The parent then tears down its workers, and by the time the loop reaches the worker PIDs they are already gone. kill writes No such process for each one and returns 1, and that becomes the exit status of the script. Passing every PID to a single kill avoids this, because all the signals go out before anything can be reaped.

The pull request description does not match the code. The description says "Leave the expansion unquoted so that each PID becomes an argument of its own", which is the batched version. The diff uses the loop instead. The commit body is accurate, so it is only the description that has drifted.

The unquoted form does trip shellcheck (SC2086). Piping into xargs gives you both the batched signals and a clean lint:

lsof -ti4TCP:"$1" -sTCP:LISTEN | xargs -r kill

I checked that with shellcheck 0.11.0 and it is clean. With a stubbed lsof: no match exits 0, two dead PIDs give one error line each and exit 1, and a missing argument still prints the usage and exits 1.

On -r, the BSD xargs on macOS accepts the flag, and it does not run the command on empty input anyway. GNU xargs runs the command once on empty input unless you pass -r. I did not have GNU findutils to hand, so that half is from the documentation rather than something I ran.

One last thing worth folding in: the header comment still says "Kills the process running on the provided port", singular, which is the assumption this change exists to correct.

`clear-port` killed nothing whenever more than one process held the
port:

    kill: `453236
    785863': not a pid or valid job spec

lsof prints one PID per line, and quoting the result handed the whole
list to kill as a single argument. Pipe the PIDs into xargs so that each
becomes an argument of its own, which also retires the `$?` check that
only ever guarded the empty case.

Batching them into one kill matters as much as unquoting. A forked
server shares its listening socket with every worker, so the parent and
the workers all appear in the LISTEN list, and the parent tears its
workers down as soon as it is signalled. Signalling one PID at a time
would reach those workers after they had already gone and report `No
such process` for a run that did exactly what it set out to do. A single
kill sends every signal without waiting for the parent to reap them in
between.

More than one process is the ordinary case rather than an edge case,
because `lsof -ti4TCP:PORT` matches established connections as well as
listeners, so a browser with the page open is listed alongside the
server it is talking to. `-sTCP:LISTEN` narrows the match to the
listening process, which is both what the script sets out to kill and
what stops the browser being killed once kill starts working.

The pipe into xargs is preferred to an unquoted `$(...)`, which batches
just as well but trips SC2086 and runs kill with no arguments when the
port is free. `-r` covers that last case on GNU xargs, and is a
documented no-op on the BSD xargs that ships with macOS.
@opdavies

Copy link
Copy Markdown
Author

Thanks Rob, here are the changes:

  • Swapped the loop for lsof -ti4TCP:"$1" -sTCP:LISTEN | xargs -r kill. Signals now go out in one batch, so the forked-server case no longer reports failure on a run that worked.
  • Description rewritten to match the code, and the header comment is plural.
  • Confirmed the GNU half of -r: on findutils 4.11.0, empty input without it runs kill bare; with it, silent and exit 0. shellcheck 0.11.0 is clean.

Let me know if there's anything else you want me to address.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants