← back to Dw Vendor Microsites

lib/port-alloc.sh

69 lines

#!/usr/bin/env bash
# port-alloc.sh <vendor_code>
#
# Allocates the lowest free port >= 10000, checked against (a) ports already in
# manifest.json, (b) live remote `ss -ltn`, (c) live remote `pm2 jlist`. flock-atomic
# so concurrent iTerm sessions don't collide. If the vendor already has a port in the
# manifest, returns that (idempotent). Prints just the chosen port to stdout.
set -euo pipefail

VENDOR="${1:?usage: port-alloc.sh <vendor_code>}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MANIFEST="$REPO/manifest.json"
LOCKFILE="$REPO/.port-alloc.lock"
LOW=10000; HIGH=10999

# idempotent: if this vendor already has a port recorded, reuse it
if [ -f "$MANIFEST" ]; then
  existing="$(python3 -c "
import json,sys
try:
  m=json.load(open('$MANIFEST'))
except: sys.exit()
v=(m.get('vendors') or {}).get('$VENDOR') or {}
p=v.get('port')
print(p if p else '')
" 2>/dev/null || true)"
  if [ -n "$existing" ]; then echo "$existing"; exit 0; fi
fi

# macOS has no flock(1); use a portable mkdir-based mutex (atomic create).
LOCKDIR="${LOCKFILE}.d"
for _i in $(seq 1 100); do
  if mkdir "$LOCKDIR" 2>/dev/null; then break; fi
  sleep 0.1
done
trap 'rmdir "$LOCKDIR" 2>/dev/null || true' EXIT

# ports already claimed in the manifest
USED_MANIFEST="$(python3 -c "
import json
try:
  m=json.load(open('$MANIFEST'))
  print(' '.join(str(v.get('port')) for v in (m.get('vendors') or {}).values() if v.get('port')))
except: print('')
" 2>/dev/null || true)"

# live remote ports (ss + pm2). Soft-fail to empty on ssh hiccup.
USED_REMOTE="$(ssh -o ConnectTimeout=10 my-server '
  (ss -ltn 2>/dev/null | awk "{print \$4}" | sed -E "s/.*:([0-9]+)\$/\1/" | grep -E "^[0-9]+\$");
  (pm2 jlist 2>/dev/null | python3 -c "import sys,json;
try:
  d=json.load(sys.stdin)
  print(\"\n\".join(str((p.get(\"pm2_env\",{}).get(\"PORT\") or \"\")) for p in d))
except: pass" 2>/dev/null)
' 2>/dev/null | grep -E '^[0-9]+$' | sort -u || true)"

ALL_USED=" $USED_MANIFEST $USED_REMOTE "

port=""
for ((p=LOW; p<=HIGH; p++)); do
  case "$ALL_USED" in
    *" $p "*) continue ;;
    *) port=$p; break ;;
  esac
done

[ -z "$port" ] && { echo "port-alloc: no free port in $LOW-$HIGH" >&2; exit 1; }
echo "$port"