mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Have Task choose free ports for dev servers (#6145)
This commit is contained in:
@@ -4,10 +4,15 @@ tasks:
|
|||||||
dev:
|
dev:
|
||||||
desc: "Start backend dev server"
|
desc: "Start backend dev server"
|
||||||
ignore_error: true
|
ignore_error: true
|
||||||
|
vars:
|
||||||
|
PORT: '{{.PORT | default "8080"}}'
|
||||||
|
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||||
|
env:
|
||||||
|
SERVER_PORT: '{{.PORT}}'
|
||||||
cmds:
|
cmds:
|
||||||
- cmd: cmd /c gradlew.bat :stirling-pdf:bootRun
|
- cmd: '{{if .AIENGINE_URL}}set "AIENGINE_URL={{.AIENGINE_URL}}" && set "AIENGINE_ENABLED=true" && {{end}}cmd /c gradlew.bat :stirling-pdf:bootRun'
|
||||||
platforms: [windows]
|
platforms: [windows]
|
||||||
- cmd: ./gradlew :stirling-pdf:bootRun
|
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun'
|
||||||
platforms: [linux, darwin]
|
platforms: [linux, darwin]
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ vars:
|
|||||||
tasks:
|
tasks:
|
||||||
prepare:
|
prepare:
|
||||||
desc: "Prepare desktop build dependencies"
|
desc: "Prepare desktop build dependencies"
|
||||||
deps: [jlink, ":frontend:prepare:desktop", provisioner]
|
deps:
|
||||||
|
- jlink
|
||||||
|
- task: ":frontend:prepare"
|
||||||
|
vars: { MODE: desktop }
|
||||||
|
- provisioner
|
||||||
|
|
||||||
provisioner:
|
provisioner:
|
||||||
desc: "Build installer provisioner"
|
desc: "Build installer provisioner"
|
||||||
|
|||||||
@@ -28,20 +28,24 @@ tasks:
|
|||||||
deps: [prepare]
|
deps: [prepare]
|
||||||
ignore_error: true
|
ignore_error: true
|
||||||
dir: src
|
dir: src
|
||||||
|
vars:
|
||||||
|
PORT: '{{.PORT | default "5001"}}'
|
||||||
env:
|
env:
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
cmds:
|
cmds:
|
||||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001
|
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}}
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
desc: "Start engine dev server with hot reload"
|
desc: "Start engine dev server with hot reload"
|
||||||
deps: [prepare]
|
deps: [prepare]
|
||||||
ignore_error: true
|
ignore_error: true
|
||||||
dir: src
|
dir: src
|
||||||
|
vars:
|
||||||
|
PORT: '{{.PORT | default "5001"}}'
|
||||||
env:
|
env:
|
||||||
PYTHONUNBUFFERED: "1"
|
PYTHONUNBUFFERED: "1"
|
||||||
cmds:
|
cmds:
|
||||||
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port 5001 --reload
|
- uv run uvicorn stirling.api.app:app --host 0.0.0.0 --port {{.PORT}} --reload
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
desc: "Run linting"
|
desc: "Run linting"
|
||||||
|
|||||||
+57
-59
@@ -15,42 +15,21 @@ tasks:
|
|||||||
CI: '{{ .CI | default "false" }}'
|
CI: '{{ .CI | default "false" }}'
|
||||||
|
|
||||||
prepare:env:
|
prepare:env:
|
||||||
desc: "Generate .env from example if missing"
|
internal: true
|
||||||
run: once
|
run: when_changed
|
||||||
deps: [install]
|
deps: [install]
|
||||||
|
vars:
|
||||||
|
MODE: '{{.MODE | default ""}}'
|
||||||
cmds:
|
cmds:
|
||||||
- npx tsx scripts/setup-env.ts
|
- npx tsx scripts/setup-env.ts{{if .MODE}} --{{.MODE}}{{end}}
|
||||||
sources:
|
sources:
|
||||||
- scripts/setup-env.ts
|
- scripts/setup-env.ts
|
||||||
generates:
|
generates:
|
||||||
- .env.local
|
- .env.local
|
||||||
|
- .env{{if .MODE}}.{{.MODE}}{{end}}.local
|
||||||
prepare:env:saas:
|
|
||||||
desc: "Generate .env and .env.saas from examples if missing"
|
|
||||||
run: once
|
|
||||||
deps: [install]
|
|
||||||
cmds:
|
|
||||||
- npx tsx scripts/setup-env.ts --saas
|
|
||||||
sources:
|
|
||||||
- scripts/setup-env.ts
|
|
||||||
generates:
|
|
||||||
- .env.local
|
|
||||||
- .env.saas.local
|
|
||||||
|
|
||||||
prepare:env:desktop:
|
|
||||||
desc: "Generate .env and .env.desktop from examples if missing"
|
|
||||||
run: once
|
|
||||||
deps: [install]
|
|
||||||
cmds:
|
|
||||||
- npx tsx scripts/setup-env.ts --desktop
|
|
||||||
sources:
|
|
||||||
- scripts/setup-env.ts
|
|
||||||
generates:
|
|
||||||
- .env.local
|
|
||||||
- .env.desktop.local
|
|
||||||
|
|
||||||
prepare:icons:
|
prepare:icons:
|
||||||
desc: "Generate icon bundle from source references"
|
internal: true
|
||||||
run: once
|
run: once
|
||||||
deps: [install]
|
deps: [install]
|
||||||
cmds:
|
cmds:
|
||||||
@@ -58,64 +37,75 @@ tasks:
|
|||||||
|
|
||||||
prepare:
|
prepare:
|
||||||
desc: "Set up dev environment"
|
desc: "Set up dev environment"
|
||||||
run: once
|
run: when_changed
|
||||||
deps: [prepare:env, prepare:icons]
|
vars:
|
||||||
|
MODE: '{{.MODE | default ""}}'
|
||||||
prepare:saas:
|
deps:
|
||||||
desc: "Prepare for SaaS mode"
|
- task: prepare:env
|
||||||
run: once
|
vars: { MODE: '{{.MODE}}' }
|
||||||
deps: [prepare:env:saas, prepare:icons]
|
- prepare:icons
|
||||||
|
|
||||||
prepare:desktop:
|
|
||||||
desc: "Prepare for desktop mode"
|
|
||||||
run: once
|
|
||||||
deps: [prepare:env:desktop, prepare:icons]
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Development
|
# Development
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
dev:_run:
|
||||||
|
internal: true
|
||||||
|
ignore_error: true
|
||||||
|
vars:
|
||||||
|
MODE: '{{.MODE}}'
|
||||||
|
PORT: '{{.PORT | default "5173"}}'
|
||||||
|
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
|
||||||
|
OPEN: '{{.OPEN | default ""}}'
|
||||||
|
env:
|
||||||
|
BACKEND_URL: '{{.BACKEND_URL}}'
|
||||||
|
cmds:
|
||||||
|
- npx vite --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
desc: "Start frontend dev server"
|
desc: "Start frontend dev server"
|
||||||
deps: [prepare]
|
|
||||||
ignore_error: true
|
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite
|
- task: dev:proprietary
|
||||||
|
vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||||
|
|
||||||
dev:core:
|
dev:core:
|
||||||
desc: "Start frontend dev server in core mode"
|
desc: "Start frontend dev server in core mode"
|
||||||
deps: [prepare]
|
deps: [prepare]
|
||||||
ignore_error: true
|
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite --mode core
|
- task: dev:_run
|
||||||
|
vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||||
|
|
||||||
dev:proprietary:
|
dev:proprietary:
|
||||||
desc: "Start frontend dev server in proprietary mode"
|
desc: "Start frontend dev server in proprietary mode"
|
||||||
deps: [prepare]
|
deps: [prepare]
|
||||||
ignore_error: true
|
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite --mode proprietary
|
- task: dev:_run
|
||||||
|
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||||
|
|
||||||
dev:saas:
|
dev:saas:
|
||||||
desc: "Start frontend dev server in SaaS mode"
|
desc: "Start frontend dev server in SaaS mode"
|
||||||
deps: [prepare:saas]
|
deps:
|
||||||
ignore_error: true
|
- task: prepare
|
||||||
|
vars: { MODE: saas }
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite --mode saas
|
- task: dev:_run
|
||||||
|
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||||
|
|
||||||
dev:desktop:
|
dev:desktop:
|
||||||
desc: "Start frontend dev server in desktop mode"
|
desc: "Start frontend dev server in desktop mode"
|
||||||
deps: [prepare:desktop]
|
deps:
|
||||||
ignore_error: true
|
- task: prepare
|
||||||
|
vars: { MODE: desktop }
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite --mode desktop
|
- task: dev:_run
|
||||||
|
vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||||
|
|
||||||
dev:prototypes:
|
dev:prototypes:
|
||||||
desc: "Start frontend dev server in prototypes mode"
|
desc: "Start frontend dev server in prototypes mode"
|
||||||
deps: [prepare]
|
deps: [prepare]
|
||||||
ignore_error: true
|
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite --mode prototypes
|
- task: dev:_run
|
||||||
|
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Build
|
# Build
|
||||||
@@ -141,13 +131,17 @@ tasks:
|
|||||||
|
|
||||||
build:saas:
|
build:saas:
|
||||||
desc: "Build for SaaS mode"
|
desc: "Build for SaaS mode"
|
||||||
deps: [prepare:saas]
|
deps:
|
||||||
|
- task: prepare
|
||||||
|
vars: { MODE: saas }
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite build --mode saas
|
- npx vite build --mode saas
|
||||||
|
|
||||||
build:desktop:
|
build:desktop:
|
||||||
desc: "Build for desktop mode"
|
desc: "Build for desktop mode"
|
||||||
deps: [prepare:desktop]
|
deps:
|
||||||
|
- task: prepare
|
||||||
|
vars: { MODE: desktop }
|
||||||
cmds:
|
cmds:
|
||||||
- npx vite build --mode desktop
|
- npx vite build --mode desktop
|
||||||
|
|
||||||
@@ -211,13 +205,17 @@ tasks:
|
|||||||
|
|
||||||
typecheck:saas:
|
typecheck:saas:
|
||||||
desc: "Typecheck SaaS build variant"
|
desc: "Typecheck SaaS build variant"
|
||||||
deps: [prepare:saas]
|
deps:
|
||||||
|
- task: prepare
|
||||||
|
vars: { MODE: saas }
|
||||||
cmds:
|
cmds:
|
||||||
- npx tsc --noEmit --project src/saas/tsconfig.json
|
- npx tsc --noEmit --project src/saas/tsconfig.json
|
||||||
|
|
||||||
typecheck:desktop:
|
typecheck:desktop:
|
||||||
desc: "Typecheck desktop build variant"
|
desc: "Typecheck desktop build variant"
|
||||||
deps: [prepare:desktop]
|
deps:
|
||||||
|
- task: prepare
|
||||||
|
vars: { MODE: desktop }
|
||||||
cmds:
|
cmds:
|
||||||
- npx tsc --noEmit --project src/desktop/tsconfig.json
|
- npx tsc --noEmit --project src/desktop/tsconfig.json
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ This file provides guidance to AI Agents when working with code in this reposito
|
|||||||
|
|
||||||
This project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.
|
This project uses [Task](https://taskfile.dev/) as a unified command runner. All build, dev, test, lint, and docker commands can be run from the repo root via `task <command>`. Run `task --list` to see all available commands.
|
||||||
|
|
||||||
|
Task `desc:` fields should describe **what** the task does, not **how** it does it. Keep them generic and stable: don't reference implementation details like aliases, internal helpers, mode flags, or which other task delegates to which. The description is for users picking a command from `task --list`, not a changelog of refactors.
|
||||||
|
|
||||||
### Quick Reference
|
### Quick Reference
|
||||||
- `task install` — install all dependencies
|
- `task install` — install all dependencies
|
||||||
- `task dev` — start backend + frontend concurrently
|
- `task dev` — start backend + frontend concurrently
|
||||||
@@ -143,7 +145,7 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
|||||||
- These files are committed to Git and must not contain private keys
|
- These files are committed to Git and must not contain private keys
|
||||||
- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top
|
- Local overrides (API keys, machine-specific settings) go in uncommitted sibling `.env.local` / `.env.saas.local` / `.env.desktop.local` files — Vite automatically layers them on top
|
||||||
- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files
|
- Never use `|| 'hardcoded-fallback'` inline — put defaults in the committed env files
|
||||||
- `task frontend:prepare` / `prepare:saas` / `prepare:desktop` create empty `.local` override files on first run
|
- `task frontend:prepare` creates empty `.local` override files on first run; pass `MODE=saas` or `MODE=desktop` to also create the mode-specific `.local` file
|
||||||
- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks
|
- Prepare runs automatically as a dependency of all `dev*`, `build*`, and `desktop*` tasks
|
||||||
- See `frontend/README.md#environment-variables` for full documentation
|
- See `frontend/README.md#environment-variables` for full documentation
|
||||||
|
|
||||||
|
|||||||
+37
-7
@@ -2,6 +2,10 @@ version: '3'
|
|||||||
|
|
||||||
output: prefixed
|
output: prefixed
|
||||||
|
|
||||||
|
vars:
|
||||||
|
FIND_FREE_PORT_PS: powershell -NoProfile -File scripts\find-free-port.ps1 -Preferred
|
||||||
|
FIND_FREE_PORT_SH: bash scripts/find-free-port.sh
|
||||||
|
|
||||||
includes:
|
includes:
|
||||||
backend:
|
backend:
|
||||||
taskfile: .taskfiles/backend.yml
|
taskfile: .taskfiles/backend.yml
|
||||||
@@ -35,17 +39,43 @@ tasks:
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
desc: "Start backend + frontend concurrently"
|
desc: "Start backend + frontend concurrently on free ports"
|
||||||
|
vars:
|
||||||
|
PORTS:
|
||||||
|
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080,5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||||
|
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||||
|
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||||
deps:
|
deps:
|
||||||
- backend:dev
|
- task: backend:dev
|
||||||
- frontend:dev
|
vars:
|
||||||
|
PORT: '{{.BACKEND_PORT}}'
|
||||||
|
- task: frontend:dev
|
||||||
|
vars:
|
||||||
|
PORT: '{{.FRONTEND_PORT}}'
|
||||||
|
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||||
|
OPEN: "true"
|
||||||
|
|
||||||
dev:all:
|
dev:all:
|
||||||
desc: "Start backend + frontend + engine concurrently"
|
desc: "Start backend + frontend + engine concurrently on free ports"
|
||||||
|
vars:
|
||||||
|
PORTS:
|
||||||
|
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080,5173,5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
|
||||||
|
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||||
|
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||||
|
ENGINE_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||||
deps:
|
deps:
|
||||||
- backend:dev
|
- task: engine:dev
|
||||||
- frontend:dev:prototypes
|
vars:
|
||||||
- engine:dev
|
PORT: '{{.ENGINE_PORT}}'
|
||||||
|
- task: backend:dev
|
||||||
|
vars:
|
||||||
|
PORT: '{{.BACKEND_PORT}}'
|
||||||
|
AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}'
|
||||||
|
- task: frontend:dev:prototypes
|
||||||
|
vars:
|
||||||
|
PORT: '{{.FRONTEND_PORT}}'
|
||||||
|
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||||
|
OPEN: "true"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Build
|
# Build
|
||||||
|
|||||||
+17
-42
@@ -39,6 +39,16 @@ export default defineConfig(({ mode }) => {
|
|||||||
|
|
||||||
const tsconfigProject = TSCONFIG_MAP[effectiveMode];
|
const tsconfigProject = TSCONFIG_MAP[effectiveMode];
|
||||||
|
|
||||||
|
// Backend proxy target: default localhost:8080. Override via BACKEND_URL env var
|
||||||
|
// so the top-level dev launcher can wire a dynamically-assigned backend port.
|
||||||
|
const backendUrl = process.env.BACKEND_URL || "http://localhost:8080";
|
||||||
|
const backendProxy = {
|
||||||
|
target: backendUrl,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
xfwd: true,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
@@ -88,48 +98,13 @@ export default defineConfig(({ mode }) => {
|
|||||||
effectiveMode === "desktop"
|
effectiveMode === "desktop"
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
"/api": {
|
"/api": backendProxy,
|
||||||
target: "http://localhost:8080",
|
"/oauth2": backendProxy,
|
||||||
changeOrigin: true,
|
"/saml2": backendProxy,
|
||||||
secure: false,
|
"/login/oauth2": backendProxy,
|
||||||
xfwd: true,
|
"/login/saml2": backendProxy,
|
||||||
},
|
"/swagger-ui": backendProxy,
|
||||||
"/oauth2": {
|
"/v1/api-docs": backendProxy,
|
||||||
target: "http://localhost:8080",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
xfwd: true,
|
|
||||||
},
|
|
||||||
"/saml2": {
|
|
||||||
target: "http://localhost:8080",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
xfwd: true,
|
|
||||||
},
|
|
||||||
"/login/oauth2": {
|
|
||||||
target: "http://localhost:8080",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
xfwd: true,
|
|
||||||
},
|
|
||||||
"/login/saml2": {
|
|
||||||
target: "http://localhost:8080",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
xfwd: true,
|
|
||||||
},
|
|
||||||
"/swagger-ui": {
|
|
||||||
target: "http://localhost:8080",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
xfwd: true,
|
|
||||||
},
|
|
||||||
"/v1/api-docs": {
|
|
||||||
target: "http://localhost:8080",
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
xfwd: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
base: env.RUN_SUBPATH ? `/${env.RUN_SUBPATH}` : "./",
|
base: env.RUN_SUBPATH ? `/${env.RUN_SUBPATH}` : "./",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Prints one free TCP port per preferred port given as an argument.
|
||||||
|
#
|
||||||
|
# For each element of -Preferred, emits that port if it's free; otherwise
|
||||||
|
# emits a random free port in 20000-49999. Probes by attempting to bind a
|
||||||
|
# TcpListener on loopback. Tracks picks within this run so outputs are
|
||||||
|
# guaranteed distinct from each other.
|
||||||
|
param([int[]]$Preferred)
|
||||||
|
|
||||||
|
$script:picked = @()
|
||||||
|
|
||||||
|
function Test-PortFree {
|
||||||
|
param([int]$Port)
|
||||||
|
if ($script:picked -contains $Port) { return $false }
|
||||||
|
try {
|
||||||
|
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
|
||||||
|
$listener.Start()
|
||||||
|
$listener.Stop()
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RandomFreePort {
|
||||||
|
while ($true) {
|
||||||
|
$port = Get-Random -Minimum 20000 -Maximum 50000
|
||||||
|
if (Test-PortFree $port) { return $port }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($p in $Preferred) {
|
||||||
|
if (Test-PortFree $p) {
|
||||||
|
$script:picked += $p
|
||||||
|
} else {
|
||||||
|
$script:picked += Get-RandomFreePort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($p in $script:picked) {
|
||||||
|
[Console]::Out.Write("$p`n")
|
||||||
|
}
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prints one free TCP port per preferred port given as an argument.
|
||||||
|
#
|
||||||
|
# For each argument, emits that port if it's free; otherwise emits a random
|
||||||
|
# free port in 20000-49999. Probes via bash's /dev/tcp pseudo-device (connect
|
||||||
|
# failure = nobody listening). Tracks picks within this run so outputs are
|
||||||
|
# guaranteed distinct from each other.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
declare -a picked=()
|
||||||
|
|
||||||
|
is_free() {
|
||||||
|
local port=$1
|
||||||
|
for p in ${picked[@]+"${picked[@]}"}; do
|
||||||
|
if [ "$p" = "$port" ]; then return 1; fi
|
||||||
|
done
|
||||||
|
if (exec 3<>"/dev/tcp/127.0.0.1/$port") >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
random_free_port() {
|
||||||
|
while true; do
|
||||||
|
local port=$((RANDOM % 30000 + 20000))
|
||||||
|
if is_free "$port"; then
|
||||||
|
echo "$port"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
for preferred in "$@"; do
|
||||||
|
if is_free "$preferred"; then
|
||||||
|
picked+=("$preferred")
|
||||||
|
else
|
||||||
|
picked+=("$(random_free_port)")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for p in "${picked[@]}"; do
|
||||||
|
echo "$p"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user