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:
|
||||
desc: "Start backend dev server"
|
||||
ignore_error: true
|
||||
vars:
|
||||
PORT: '{{.PORT | default "8080"}}'
|
||||
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
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]
|
||||
- cmd: ./gradlew :stirling-pdf:bootRun
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
|
||||
build:
|
||||
|
||||
@@ -6,7 +6,11 @@ vars:
|
||||
tasks:
|
||||
prepare:
|
||||
desc: "Prepare desktop build dependencies"
|
||||
deps: [jlink, ":frontend:prepare:desktop", provisioner]
|
||||
deps:
|
||||
- jlink
|
||||
- task: ":frontend:prepare"
|
||||
vars: { MODE: desktop }
|
||||
- provisioner
|
||||
|
||||
provisioner:
|
||||
desc: "Build installer provisioner"
|
||||
|
||||
@@ -28,20 +28,24 @@ tasks:
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
dir: src
|
||||
vars:
|
||||
PORT: '{{.PORT | default "5001"}}'
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
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:
|
||||
desc: "Start engine dev server with hot reload"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
dir: src
|
||||
vars:
|
||||
PORT: '{{.PORT | default "5001"}}'
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
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:
|
||||
desc: "Run linting"
|
||||
|
||||
+57
-59
@@ -15,42 +15,21 @@ tasks:
|
||||
CI: '{{ .CI | default "false" }}'
|
||||
|
||||
prepare:env:
|
||||
desc: "Generate .env from example if missing"
|
||||
run: once
|
||||
internal: true
|
||||
run: when_changed
|
||||
deps: [install]
|
||||
vars:
|
||||
MODE: '{{.MODE | default ""}}'
|
||||
cmds:
|
||||
- npx tsx scripts/setup-env.ts
|
||||
- npx tsx scripts/setup-env.ts{{if .MODE}} --{{.MODE}}{{end}}
|
||||
sources:
|
||||
- scripts/setup-env.ts
|
||||
generates:
|
||||
- .env.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
|
||||
- .env{{if .MODE}}.{{.MODE}}{{end}}.local
|
||||
|
||||
prepare:icons:
|
||||
desc: "Generate icon bundle from source references"
|
||||
internal: true
|
||||
run: once
|
||||
deps: [install]
|
||||
cmds:
|
||||
@@ -58,64 +37,75 @@ tasks:
|
||||
|
||||
prepare:
|
||||
desc: "Set up dev environment"
|
||||
run: once
|
||||
deps: [prepare:env, prepare:icons]
|
||||
|
||||
prepare:saas:
|
||||
desc: "Prepare for SaaS mode"
|
||||
run: once
|
||||
deps: [prepare:env:saas, prepare:icons]
|
||||
|
||||
prepare:desktop:
|
||||
desc: "Prepare for desktop mode"
|
||||
run: once
|
||||
deps: [prepare:env:desktop, prepare:icons]
|
||||
run: when_changed
|
||||
vars:
|
||||
MODE: '{{.MODE | default ""}}'
|
||||
deps:
|
||||
- task: prepare:env
|
||||
vars: { MODE: '{{.MODE}}' }
|
||||
- prepare:icons
|
||||
|
||||
# ============================================================
|
||||
# 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:
|
||||
desc: "Start frontend dev server"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- npx vite
|
||||
- task: dev:proprietary
|
||||
vars: { PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:core:
|
||||
desc: "Start frontend dev server in core mode"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- npx vite --mode core
|
||||
- task: dev:_run
|
||||
vars: { MODE: core, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:proprietary:
|
||||
desc: "Start frontend dev server in proprietary mode"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- npx vite --mode proprietary
|
||||
- task: dev:_run
|
||||
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:saas:
|
||||
desc: "Start frontend dev server in SaaS mode"
|
||||
deps: [prepare:saas]
|
||||
ignore_error: true
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- npx vite --mode saas
|
||||
- task: dev:_run
|
||||
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:desktop:
|
||||
desc: "Start frontend dev server in desktop mode"
|
||||
deps: [prepare:desktop]
|
||||
ignore_error: true
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- npx vite --mode desktop
|
||||
- task: dev:_run
|
||||
vars: { MODE: desktop, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
dev:prototypes:
|
||||
desc: "Start frontend dev server in prototypes mode"
|
||||
deps: [prepare]
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- npx vite --mode prototypes
|
||||
- task: dev:_run
|
||||
vars: { MODE: prototypes, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
|
||||
|
||||
# ============================================================
|
||||
# Build
|
||||
@@ -141,13 +131,17 @@ tasks:
|
||||
|
||||
build:saas:
|
||||
desc: "Build for SaaS mode"
|
||||
deps: [prepare:saas]
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- npx vite build --mode saas
|
||||
|
||||
build:desktop:
|
||||
desc: "Build for desktop mode"
|
||||
deps: [prepare:desktop]
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- npx vite build --mode desktop
|
||||
|
||||
@@ -211,13 +205,17 @@ tasks:
|
||||
|
||||
typecheck:saas:
|
||||
desc: "Typecheck SaaS build variant"
|
||||
deps: [prepare:saas]
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: saas }
|
||||
cmds:
|
||||
- npx tsc --noEmit --project src/saas/tsconfig.json
|
||||
|
||||
typecheck:desktop:
|
||||
desc: "Typecheck desktop build variant"
|
||||
deps: [prepare:desktop]
|
||||
deps:
|
||||
- task: prepare
|
||||
vars: { MODE: desktop }
|
||||
cmds:
|
||||
- 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.
|
||||
|
||||
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
|
||||
- `task install` — install all dependencies
|
||||
- `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
|
||||
- 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
|
||||
- `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
|
||||
- See `frontend/README.md#environment-variables` for full documentation
|
||||
|
||||
|
||||
+37
-7
@@ -2,6 +2,10 @@ version: '3'
|
||||
|
||||
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:
|
||||
backend:
|
||||
taskfile: .taskfiles/backend.yml
|
||||
@@ -35,17 +39,43 @@ tasks:
|
||||
# ============================================================
|
||||
|
||||
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:
|
||||
- backend:dev
|
||||
- frontend:dev
|
||||
- task: backend:dev
|
||||
vars:
|
||||
PORT: '{{.BACKEND_PORT}}'
|
||||
- task: frontend:dev
|
||||
vars:
|
||||
PORT: '{{.FRONTEND_PORT}}'
|
||||
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
|
||||
OPEN: "true"
|
||||
|
||||
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:
|
||||
- backend:dev
|
||||
- frontend:dev:prototypes
|
||||
- engine:dev
|
||||
- task: engine:dev
|
||||
vars:
|
||||
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
|
||||
|
||||
+17
-42
@@ -39,6 +39,16 @@ export default defineConfig(({ mode }) => {
|
||||
|
||||
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 {
|
||||
plugins: [
|
||||
react(),
|
||||
@@ -88,48 +98,13 @@ export default defineConfig(({ mode }) => {
|
||||
effectiveMode === "desktop"
|
||||
? undefined
|
||||
: {
|
||||
"/api": {
|
||||
target: "http://localhost:8080",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
xfwd: true,
|
||||
},
|
||||
"/oauth2": {
|
||||
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,
|
||||
},
|
||||
"/api": backendProxy,
|
||||
"/oauth2": backendProxy,
|
||||
"/saml2": backendProxy,
|
||||
"/login/oauth2": backendProxy,
|
||||
"/login/saml2": backendProxy,
|
||||
"/swagger-ui": backendProxy,
|
||||
"/v1/api-docs": backendProxy,
|
||||
},
|
||||
},
|
||||
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