224 lines
6.1 KiB
Bash
Executable File
224 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
BACKEND_DIR="$ROOT_DIR/backend"
|
||
WORKER_DIR="$BACKEND_DIR/worker"
|
||
WEB_DIR="$BACKEND_DIR/web"
|
||
ENV_FILE="$ROOT_DIR/deploy/.env.dev"
|
||
RUN_DIR="$ROOT_DIR/.run"
|
||
PID_FILE="$RUN_DIR/dev-all.pids"
|
||
|
||
mkdir -p "$RUN_DIR"
|
||
: > "$PID_FILE"
|
||
|
||
PIDS=()
|
||
|
||
cleanup() {
|
||
local pid
|
||
for pid in "${PIDS[@]:-}"; do
|
||
if kill -0 "$pid" 2>/dev/null; then
|
||
# Each job was started via `setsid`, so its PID is also its process
|
||
# group id; killing the negative pid takes down the whole subtree
|
||
# (e.g. npm -> tsx -> node) instead of leaving orphaned children.
|
||
kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
|
||
fi
|
||
done
|
||
wait 2>/dev/null || true
|
||
rm -f "$PID_FILE"
|
||
}
|
||
trap cleanup EXIT INT TERM
|
||
|
||
load_env() {
|
||
if [[ ! -f "$ENV_FILE" ]]; then
|
||
echo "[dev-all] 找不到 $ENV_FILE,請先:cp deploy/.env.dev.example deploy/.env.dev 並填值" >&2
|
||
exit 1
|
||
fi
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
source "$ENV_FILE"
|
||
set +a
|
||
|
||
: "${HAIXUN_MONGO_URI:?請在 $ENV_FILE 設定 HAIXUN_MONGO_URI}"
|
||
: "${HAIXUN_REDIS_ADDR:?請在 $ENV_FILE 設定 HAIXUN_REDIS_ADDR}"
|
||
: "${HAIXUN_REDIS_PASSWORD:?請在 $ENV_FILE 設定 HAIXUN_REDIS_PASSWORD}"
|
||
: "${HAIXUN_JWT_ACCESS_SECRET:?請在 $ENV_FILE 設定 HAIXUN_JWT_ACCESS_SECRET}"
|
||
: "${HAIXUN_JWT_REFRESH_SECRET:?請在 $ENV_FILE 設定 HAIXUN_JWT_REFRESH_SECRET}"
|
||
: "${HAIXUN_WORKER_SECRET:?請在 $ENV_FILE 設定 HAIXUN_WORKER_SECRET}"
|
||
: "${HAIXUN_MONGO_DB:=haixun}"
|
||
: "${HAIXUN_SECRETS_KEY:=}"
|
||
: "${HAIXUN_BACKEND_URL:=http://127.0.0.1:8890}"
|
||
: "${HAIXUN_WORKER_POLL_MS:=3000}"
|
||
: "${HAIXUN_8D_TARGET_SAMPLES:=4}"
|
||
: "${PLAYWRIGHT_HEADLESS:=true}"
|
||
|
||
export HAIXUN_MONGO_URI HAIXUN_MONGO_DB HAIXUN_REDIS_ADDR HAIXUN_REDIS_PASSWORD
|
||
export HAIXUN_JWT_ACCESS_SECRET HAIXUN_JWT_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||
export HAIXUN_SECRETS_KEY HAIXUN_BACKEND_URL
|
||
export HAIXUN_WORKER_POLL_MS HAIXUN_8D_TARGET_SAMPLES
|
||
export THREADS_APP_ID THREADS_APP_SECRET THREADS_REDIRECT_URI THREADS_OAUTH_SUCCESS_REDIRECT
|
||
export PLAYWRIGHT_HEADLESS
|
||
}
|
||
|
||
start() {
|
||
local name="$1"
|
||
shift
|
||
echo "[dev-all] starting $name"
|
||
# setsid makes the launched process its own session/process-group leader,
|
||
# so dev-all-stop.sh can reliably kill it *and* every child it spawns
|
||
# (npm -> tsx -> node, etc.) with a single `kill -- -PID`, even if this
|
||
# script itself gets killed abruptly and never runs its own cleanup trap.
|
||
setsid "$@" &
|
||
local pid="$!"
|
||
PIDS+=("$pid")
|
||
echo "$pid $name" >> "$PID_FILE"
|
||
}
|
||
|
||
port_in_use() {
|
||
local port="$1"
|
||
ss -tlnp 2>/dev/null | grep -qE ":${port} "
|
||
}
|
||
|
||
free_port() {
|
||
local port="$1"
|
||
if ! port_in_use "$port"; then
|
||
return 0
|
||
fi
|
||
echo "[dev-all] freeing port :$port"
|
||
if command -v fuser >/dev/null 2>&1; then
|
||
fuser -k "${port}/tcp" 2>/dev/null || true
|
||
else
|
||
local line pid
|
||
while IFS= read -r line; do
|
||
for pid in $(grep -oE 'pid=[0-9]+' <<<"$line" | cut -d= -f2); do
|
||
kill "$pid" 2>/dev/null || true
|
||
done
|
||
done < <(ss -tlnp 2>/dev/null | grep -E ":${port} " || true)
|
||
fi
|
||
sleep 0.3
|
||
}
|
||
|
||
ensure_http_service() {
|
||
local name="$1"
|
||
local port="$2"
|
||
local health_url="$3"
|
||
shift 3
|
||
|
||
if curl -fsS "$health_url" >/dev/null 2>&1; then
|
||
echo "[dev-all] reusing $name (already healthy on :$port)"
|
||
return 0
|
||
fi
|
||
if port_in_use "$port"; then
|
||
echo "[dev-all] port :$port occupied but $name unhealthy; restarting"
|
||
free_port "$port"
|
||
fi
|
||
start "$name" "$@"
|
||
wait_for_http "$name" "$health_url"
|
||
}
|
||
|
||
wait_for_http() {
|
||
local name="$1"
|
||
local url="$2"
|
||
local i
|
||
for i in {1..40}; do
|
||
if curl -fsS "$url" >/dev/null 2>&1; then
|
||
echo "[dev-all] ready $name"
|
||
return 0
|
||
fi
|
||
sleep 0.5
|
||
done
|
||
echo "[dev-all] $name did not become ready: $url" >&2
|
||
return 1
|
||
}
|
||
|
||
pick_web_port() {
|
||
python3 - <<'PY'
|
||
import socket
|
||
|
||
for port in range(5173, 5200):
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||
sock.settimeout(0.1)
|
||
if sock.connect_ex(('127.0.0.1', port)) != 0:
|
||
print(port)
|
||
raise SystemExit(0)
|
||
raise SystemExit('no free Vite port found in 5173..5199')
|
||
PY
|
||
}
|
||
|
||
resolve_web_port() {
|
||
local requested="${WEB_PORT:-}"
|
||
if [[ -n "$requested" && "$requested" -ge 1024 ]]; then
|
||
echo "$requested"
|
||
return
|
||
fi
|
||
if [[ -n "$requested" ]]; then
|
||
echo "[dev-all] WEB_PORT=$requested is for prod/nginx; using a free dev port instead" >&2
|
||
fi
|
||
pick_web_port
|
||
}
|
||
|
||
load_env
|
||
|
||
cd "$BACKEND_DIR"
|
||
mkdir -p bin
|
||
go build -o bin/gateway gateway.go
|
||
go build -o bin/worker ./cmd/worker
|
||
|
||
cd "$WORKER_DIR"
|
||
npm install
|
||
|
||
cd "$WEB_DIR"
|
||
npm install
|
||
|
||
cd "$BACKEND_DIR"
|
||
ensure_http_service "gateway" 8890 "http://127.0.0.1:8890/api/v1/health" \
|
||
./bin/gateway -f etc/gateway.yaml
|
||
|
||
GO_WORKER_TYPES=(
|
||
go-demo
|
||
go-expand-graph
|
||
go-placement-scan
|
||
go-scan-viral
|
||
go-analyze-copy-mission
|
||
go-expand-copy-mission-graph
|
||
go-generate-copy-matrix
|
||
go-generate-copy-draft
|
||
go-generate-outreach-draft
|
||
go-generate-topic-matrix
|
||
go-generate-formula-draft
|
||
go-generate-rewrite-draft
|
||
go-refresh-threads-token
|
||
go-publish-analytics
|
||
go-refill-publish-inventory
|
||
)
|
||
|
||
for worker_type in "${GO_WORKER_TYPES[@]}"; do
|
||
start "worker:$worker_type" env \
|
||
HAIXUN_JOB_WORKER_TYPE="$worker_type" \
|
||
HAIXUN_JOB_WORKER_ID="local-$worker_type" \
|
||
./bin/worker -f etc/gateway.worker.yaml
|
||
done
|
||
|
||
cd "$WORKER_DIR"
|
||
ensure_http_service "node-tool:keyword-search" 9101 "http://127.0.0.1:9101/health" \
|
||
env PORT=9101 npm run keyword-search-server
|
||
ensure_http_service "node-tool:resolve-media" 9102 "http://127.0.0.1:9102/health" \
|
||
env PORT=9102 npm run resolve-media-server
|
||
start "node-worker:style-8d" env \
|
||
HAIXUN_NODE_WORKER_TYPE=node-style-8d \
|
||
HAIXUN_NODE_WORKER_ID=local-node-style-8d \
|
||
npm run style-8d
|
||
|
||
cd "$WEB_DIR"
|
||
WEB_PORT="$(resolve_web_port)"
|
||
start "web" npm run dev -- --host 0.0.0.0 --port "$WEB_PORT" --strictPort
|
||
wait_for_http "web" "http://127.0.0.1:$WEB_PORT/"
|
||
|
||
echo
|
||
echo "[dev-all] all services are running"
|
||
echo "[dev-all] API: http://127.0.0.1:8890/api/v1/health"
|
||
echo "[dev-all] Web: http://127.0.0.1:$WEB_PORT/"
|
||
echo "[dev-all] Press Ctrl+C to stop"
|
||
|
||
wait -n
|