I have a situation where we need to be able to run multiple instances of a Spring app with dynamically assigned ports defined at runtime. I need to be able to get an available port at runtime, create some associated file structure for dedicated logs &c, and plug the port number into the command-line arguments passed to Java.
Below is an adaptation of some code I pulled off the Unix exchange and tweaked. It currently seems to do what I need - runs in single-digit milliseconds, accepts upper an lower bounds or provides reasonable defaults, won't spin off into an infinite loop if someone does something stupid like specifying the same upper and lower bound as a port that's already in use, doesn't actually send a newline into a port that's already in use, etc.
Anyone see a flaw, or a way to make it cleaner?
I know there's a possible race condition, and suggestions for that are welcome. I don't need non-bash portability.
getPort() {
local -i lo=${1:-50000} hi=${2:-65000}
local -i range=$(( hi - lo + 1 )) candidate
for try in {1..999}
do candidate=$(( lo + ( $RANDOM$RANDOM % range ) ))
{ printf "" >/dev/tcp/127.0.0.1/$candidate ||
printf "" >/dev/tcp/-thisIP,redacted-/$candidate
} >/dev/null 2>&1 || { echo $candidate; return 0; }
done
return 1;
}
ADDENDUM
Since I am only interested in ports on the local machine, I should be ok if I test both localhost and every IP for this machine, which in my case will be only one. (IP redacted above.)
Yes?
Also, since someone could pass in 0 and 65000 as args, just using a 16bit $RANDOM
alone doesn't cover all cases. I took the cheesy-easy route and used $RNADOM$RANDOM
, which has some weirdness since about 2/3 of the time the 5th digit from the right will only be one of [123]
, and the distribution curve will slope up from the low end, but these aren't problematic in my case. Still, I'd love to see examples of how to smooth it out.
2 Answers 2
There's an obvious flaw if the candidate port is closed on 127.0.0.1
but open on a different interface that you want your service to bind to. That's particularly relevant if you want it to listen on all interfaces (0.0.0.0
).
If your service binds to the localhost address, then that's obviously not a concern.
-
\$\begingroup\$ That's a good point. Have a suggested solution? \$\endgroup\$Paul Hodges– Paul Hodges2020年01月08日 19:37:07 +00:00Commented Jan 8, 2020 at 19:37
-
\$\begingroup\$ Sorry, no suggested solution in a shell script (all I can think of is writing a C program to bind to the address/port combination). An alternative approach may be to use the output of
netstat -A inet -l
, or to discover where it gets the list of listening ports; I didn't pursue that far enough. \$\endgroup\$Toby Speight– Toby Speight2020年01月08日 20:45:08 +00:00Commented Jan 8, 2020 at 20:45 -
\$\begingroup\$ Assuming no multi-IP NIC cards, shouldn't I be ok if I test both localhost and the machine's IP? I only care that the port be available on this machine... right? \$\endgroup\$Paul Hodges– Paul Hodges2020年01月08日 21:37:01 +00:00Commented Jan 8, 2020 at 21:37
-
1\$\begingroup\$ If you're in a position to make such assumptions about your target environments, then go ahead (but put a big fat comment in the code so that you know its limitations!). \$\endgroup\$Toby Speight– Toby Speight2020年01月08日 21:39:16 +00:00Commented Jan 8, 2020 at 21:39
Use more functions
The expression that checks if a port is available is a bit complex. It would be good to encapsulate it in a separate function.
Avoid hardcoded values
It seems you are using a hard-coded IP address buried in the middle of the code. It would be better to extract such values and define them at the top of the script. Then you wouldn't even need to redact anything for code review purposes.
Validate input
The function accepts ranges that wouldn't make sense. It would be good to add input validation to fail fast with a clear message.
Exploring a list of things in random order
In this example "things" is port numbers within a range. A simple technique to explore all of them in random order is to enumerate and shuffle.
ports "$lo" "$hi" | shuf | while read candidate; do
if is_available "$candidate"; then
echo "$candidate"
return
fi
done
return 1
Where ports
is a custom function that produces the candidate ports within the specified range, and shuf
is the GNU shuffle tool or a custom function that implements Fisher-Yates shuffle using a Bash array.
If in the typical use case you expect to find an available port easily in at most a few random trials, then I agree that this shuffling technique would be overkill, and your current method is just fine.
-
1\$\begingroup\$ Why not simply
seq "$lo" "$hi"
instead of writing a custom functionports
? \$\endgroup\$Toby Speight– Toby Speight2020年01月10日 11:23:15 +00:00Commented Jan 10, 2020 at 11:23 -
\$\begingroup\$ @TobySpeight
seq
is not recommended, and it's not a standard tool, so not always available. If it was, then sure, that would be fine. \$\endgroup\$janos– janos2020年01月10日 11:48:27 +00:00Commented Jan 10, 2020 at 11:48 -
\$\begingroup\$ Shame we can't use
printf '%d\n {$lo..$hi}
(brace expansions are performed before parameter expansions, unfortunately). \$\endgroup\$Toby Speight– Toby Speight2020年01月10日 12:07:18 +00:00Commented Jan 10, 2020 at 12:07 -
\$\begingroup\$ The "hardcode" is localhost. That IP never changes. I calc the local machine's IP dynamically for the second one. More functions is good in a large, shared codebase, but there's a vanishing point where the benefits are outweighed. That way lies Java and other madness. Validation is a good point, thanks. Passing a list through
shuf
requires generating a whole list and runningshuf
, when on this system the odds are any random port will be ok on the first pass. On a busier system (like my laptop) it would be worth the extra overhead, but this saves notable deploy time over 40 svcs. \$\endgroup\$Paul Hodges– Paul Hodges2020年01月10日 14:45:08 +00:00Commented Jan 10, 2020 at 14:45 -
\$\begingroup\$ @PaulHodges The hardcoded value I referred to is "-thisIP,redacted-", not localhost. "More functions" is a relative term. I recommend more than what you used (= the concrete example I gave), and certainly less than the vanishing point you described. \$\endgroup\$janos– janos2020年01月11日 13:58:18 +00:00Commented Jan 11, 2020 at 13:58
0
as port number) and output the one it was given? That would avoid both polling and the race condition. \$\endgroup\$