4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Is it possible to modify your server program to run on "any" port (i.e. pass 0 as port number) and output the one it was given? That would avoid both polling and the race condition. \$\endgroup\$ – Toby Speight Jan 8 at 17:59
  • 1
    \$\begingroup\$ It's a dedicated server, so polling isn't really a problem. This runs on average in 2ms, returning the first port it tries, as almost all will be available. Even the race condition is unlikely to ever be an issue because nothing else will likely be grabbing ports. Spring boot already has a mechanism to dynamically assign by using port zero, but there is apparently some admin app code that gets confused by that, and the devs said it's sometimes buggy. This will let me bypass all their issues - but to answer your question, I have no control over their code. \$\endgroup\$ – Paul Hodges Jan 8 at 19:42
  • \$\begingroup\$ Please don't change your code after receiving an answer. You now have two different answers, addressing different versions of the question. That's unhelpful and confusing. See What should I do when someone answers my question?. \$\endgroup\$ – Toby Speight Jan 10 at 11:25
5
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ That's a good point. Have a suggested solution? \$\endgroup\$ – Paul Hodges Jan 8 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 Jan 8 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 Jan 8 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 Jan 8 at 21:39
2
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Why not simply seq "$lo" "$hi" instead of writing a custom function ports? \$\endgroup\$ – Toby Speight Jan 10 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 Jan 10 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 Jan 10 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 running shuf, 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 Jan 10 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 Jan 11 at 13:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.