fix(dev): precheck dev ports and avoid pid file locks
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -19,7 +19,7 @@ usage() {
|
||||
1. 默认同时启动 SpacetimeDB standalone、Rust api-server、主站 Vite 与后台 Vite。
|
||||
2. 当前开发阶段默认 publish server-rs/crates/spacetime-module 时追加 -c=on-conflict 在结构冲突时清理旧模块数据。
|
||||
3. 只有显式传入 --preserve-database 时,才会跳过 -c=on-conflict。
|
||||
4. SpacetimeDB 默认使用 server-rs/.spacetimedb/local/data 作为本地数据目录;端口被占用时自动接受 SpacetimeDB 建议的最近可用端口。
|
||||
4. SpacetimeDB 默认使用 server-rs/.spacetimedb/local/data 作为本地数据目录;需要新启动时会先检测端口并选择最近可用端口。
|
||||
5. 默认在发布模块前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
|
||||
EOF
|
||||
}
|
||||
@@ -191,6 +191,144 @@ port_from_listen_addr() {
|
||||
echo "${listen_addr##*:}"
|
||||
}
|
||||
|
||||
read_digits_from_file() {
|
||||
local file_path="$1"
|
||||
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const filePath = process.argv[1];
|
||||
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
const digits = fs.readFileSync(filePath, "utf8").replace(/\D/gu, "").slice(0, 20);
|
||||
if (digits) {
|
||||
process.stdout.write(digits);
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(1);
|
||||
} catch {
|
||||
sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
' "${file_path}" 2>/dev/null
|
||||
}
|
||||
|
||||
read_trimmed_first_line_from_file() {
|
||||
local file_path="$1"
|
||||
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const filePath = process.argv[1];
|
||||
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
const firstLine = fs.readFileSync(filePath, "utf8").split(/\r?\n/u)[0]?.trim() ?? "";
|
||||
if (firstLine) {
|
||||
process.stdout.write(firstLine);
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(1);
|
||||
} catch {
|
||||
sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
' "${file_path}" 2>/dev/null
|
||||
}
|
||||
|
||||
validate_tcp_port() {
|
||||
local port="$1"
|
||||
local label="$2"
|
||||
|
||||
if ! [[ "${port}" =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then
|
||||
echo "[dev:rust] ${label} 端口无效: ${port}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_tcp_port_available() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
|
||||
node -e '
|
||||
const net = require("net");
|
||||
const host = process.argv[1];
|
||||
const port = Number(process.argv[2]);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
process.exit(2);
|
||||
}
|
||||
const server = net.createServer();
|
||||
server.once("error", () => process.exit(1));
|
||||
server.once("listening", () => server.close(() => process.exit(0)));
|
||||
server.listen({ host, port, exclusive: true });
|
||||
' "${host}" "${port}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
describe_tcp_port_owner() {
|
||||
local port="$1"
|
||||
|
||||
if command -v powershell.exe >/dev/null 2>&1; then
|
||||
PORT_FOR_POWERSHELL="${port}" powershell.exe -NoProfile -Command '
|
||||
$port = [int]$env:PORT_FOR_POWERSHELL
|
||||
Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 5 |
|
||||
ForEach-Object {
|
||||
$process = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
|
||||
$name = if ($process) { $process.ProcessName } else { "unknown" }
|
||||
"pid=$($_.OwningProcess) name=$name local=$($_.LocalAddress):$($_.LocalPort)"
|
||||
}
|
||||
' 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
ss -lntp 2>/dev/null | awk -v port=":${port}" 'NR == 1 || index($0, port) > 0 { print }' || true
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
netstat -lntp 2>/dev/null | awk -v port=":${port}" 'NR == 1 || index($0, port) > 0 { print }' || true
|
||||
fi
|
||||
}
|
||||
|
||||
find_nearest_available_port() {
|
||||
local host="$1"
|
||||
local requested_port="$2"
|
||||
local label="$3"
|
||||
local max_probe_count="${4:-100}"
|
||||
|
||||
validate_tcp_port "${requested_port}" "${label}"
|
||||
|
||||
local port="${requested_port}"
|
||||
local probe_count=0
|
||||
|
||||
while ((port <= 65535 && probe_count < max_probe_count)); do
|
||||
if is_tcp_port_available "${host}" "${port}"; then
|
||||
if [[ "${port}" != "${requested_port}" ]]; then
|
||||
echo "[dev:rust] ${label} 端口 ${host}:${requested_port} 已占用,改用最近可用端口 ${host}:${port}" >&2
|
||||
fi
|
||||
echo "${port}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ((probe_count == 0)); then
|
||||
echo "[dev:rust] ${label} 端口 ${host}:${requested_port} 已占用,开始查找最近可用端口。" >&2
|
||||
describe_tcp_port_owner "${requested_port}" >&2 || true
|
||||
fi
|
||||
|
||||
port=$((port + 1))
|
||||
probe_count=$((probe_count + 1))
|
||||
done
|
||||
|
||||
echo "[dev:rust] 无法为 ${label} 在 ${host}:${requested_port} 起向后 ${max_probe_count} 个端口内找到可用端口。" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
spacetime_url_record_path() {
|
||||
local data_dir="$1"
|
||||
echo "${data_dir}/dev-rust-spacetime-url"
|
||||
@@ -215,8 +353,7 @@ read_spacetime_pid() {
|
||||
fi
|
||||
|
||||
local pid
|
||||
pid="$(tr -cd '0-9' <"${pid_file}" | head -c 20)"
|
||||
if [[ -z "${pid}" ]]; then
|
||||
if ! pid="$(read_digits_from_file "${pid_file}")"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -230,16 +367,13 @@ try_reuse_existing_spacetime() {
|
||||
|
||||
local existing_pid
|
||||
local recorded_url=""
|
||||
if ! existing_pid="$(read_spacetime_pid "${data_dir}")"; then
|
||||
return 1
|
||||
if [[ -f "${url_file}" ]]; then
|
||||
if ! recorded_url="$(read_trimmed_first_line_from_file "${url_file}")"; then
|
||||
recorded_url=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! kill -0 "${existing_pid}" 2>/dev/null; then
|
||||
echo "[dev:rust] 发现过期 spacetime.pid: ${existing_pid},将重新启动 SpacetimeDB。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${url_file}" ]]; then
|
||||
if [[ -z "${recorded_url}" ]]; then
|
||||
local start_log
|
||||
start_log="$(spacetime_start_log_path "${data_dir}")"
|
||||
if [[ -f "${start_log}" ]]; then
|
||||
@@ -261,11 +395,9 @@ try_reuse_existing_spacetime() {
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${recorded_url}" ]]; then
|
||||
echo "[dev:rust] 发现运行中的 SpacetimeDB pid=${existing_pid},但未找到 URL 记录: ${url_file}。"
|
||||
echo "[dev:rust] 未找到可复用的 SpacetimeDB URL 记录: ${url_file}。"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
recorded_url="$(head -n 1 "${url_file}" | tr -d '\r' | xargs)"
|
||||
fi
|
||||
|
||||
if [[ -z "${recorded_url}" ]]; then
|
||||
@@ -277,10 +409,24 @@ try_reuse_existing_spacetime() {
|
||||
SPACETIME_SERVER="${recorded_url}"
|
||||
SPACETIME_PORT="$(port_from_listen_addr "${recorded_url}")"
|
||||
SPACETIME_REUSED_EXISTING=1
|
||||
echo "[dev:rust] 复用已启动 SpacetimeDB: ${SPACETIME_SERVER} (pid=${existing_pid})"
|
||||
if existing_pid="$(read_spacetime_pid "${data_dir}")"; then
|
||||
echo "[dev:rust] 复用已启动 SpacetimeDB: ${SPACETIME_SERVER} (pid=${existing_pid})"
|
||||
else
|
||||
echo "[dev:rust] 复用已启动 SpacetimeDB: ${SPACETIME_SERVER} (pid 文件暂不可读)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! existing_pid="$(read_spacetime_pid "${data_dir}")"; then
|
||||
echo "[dev:rust] URL 不在线且 spacetime.pid 暂不可读: ${recorded_url},将检查 data-dir 占用。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! kill -0 "${existing_pid}" 2>/dev/null; then
|
||||
echo "[dev:rust] 发现过期 spacetime.pid: ${existing_pid},将重新启动 SpacetimeDB。"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "[dev:rust] pid=${existing_pid} 存在,但 URL 不在线: ${recorded_url},将重新启动 SpacetimeDB。"
|
||||
return 1
|
||||
}
|
||||
@@ -760,15 +906,17 @@ if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SPACETIME_PORT="$(find_nearest_available_port "${SPACETIME_HOST}" "${SPACETIME_PORT}" "SpacetimeDB")"
|
||||
SPACETIME_SERVER="http://${SPACETIME_HOST}:${SPACETIME_PORT}"
|
||||
|
||||
SPACETIME_START_LOG="$(spacetime_start_log_path "${SPACETIME_DATA_DIR}")"
|
||||
mkdir -p "$(dirname -- "${SPACETIME_START_LOG}")"
|
||||
: >"${SPACETIME_START_LOG}"
|
||||
echo "[dev:rust] 启动 spacetimedb"
|
||||
(
|
||||
cd "${SERVER_RS_DIR}"
|
||||
# 当目标端口被占用时,SpacetimeDB 会询问是否使用最近的可用端口;
|
||||
# 这里直接发送回车接受默认建议,再从启动日志解析实际监听端口。
|
||||
printf '\n' | spacetime \
|
||||
# 启动前已经由脚本选定端口,避免 api-server 和 SpacetimeDB 对数据库地址认知不一致。
|
||||
spacetime \
|
||||
start \
|
||||
--data-dir "${SPACETIME_DATA_DIR}" \
|
||||
--listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}" \
|
||||
@@ -815,8 +963,11 @@ fi
|
||||
|
||||
echo "[dev:rust] 启动 api-server"
|
||||
load_api_server_env_files
|
||||
API_PORT="$(find_nearest_available_port "${API_HOST}" "${API_PORT}" "api-server")"
|
||||
API_TARGET_HOST="$(resolve_client_host "${API_HOST}")"
|
||||
# `.env.local` 可以给单独 `dev:web` 配置代理目标,但完整栈的前端必须跟随本次 `--api-port`。
|
||||
RUST_SERVER_TARGET="http://${API_TARGET_HOST}:${API_PORT}"
|
||||
echo "[dev:rust] api actual: ${RUST_SERVER_TARGET}"
|
||||
(
|
||||
cd "${REPO_ROOT}"
|
||||
GENARRATIVE_API_HOST="${API_HOST}" \
|
||||
|
||||
Reference in New Issue
Block a user