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:
@@ -111,8 +111,8 @@
|
||||
|
||||
- 现象:本地 `npm run dev` 因 `3101` 已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查 `spacetime-module` 语法而被完整联调链路拖慢。
|
||||
- 原因:`npm run dev` 默认同时启动 SpacetimeDB standalone、发布 `server-rs/crates/spacetime-module`、启动 Rust `api-server`、主站 Vite 与后台 Vite;并非每个阶段都需要完整重启和重新发布。
|
||||
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。`3101` 已被可复用 standalone 占用时也可显式使用 `npm run dev -- --skip-spacetime`;未修改 `spacetime-module` 时使用 `npm run dev -- --skip-publish`;只查模块语法时执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。
|
||||
- 验证:`--skip-spacetime` 后脚本复用现有 `http://127.0.0.1:3101`;`--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。
|
||||
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。确认需要新启动 SpacetimeDB 时,脚本先检测 `3101`,被占用则输出占用进程并选择最近可用端口,保证 publish 与 `api-server` 都连接同一个实际 SpacetimeDB URL。`api-server` 启动前也会检测 `8082` 并选择最近可用端口。Windows / Git Bash 下不要用 `tr/head/xargs` 管道读取 `spacetime.pid` 或 URL 记录,脚本应使用 Node 读取并短重试,避免 `tr: read error: Device or resource busy`;未修改 `spacetime-module` 时使用 `npm run dev -- --skip-publish`;只查模块语法时执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。
|
||||
- 验证:`--skip-spacetime` 后脚本复用现有 `http://127.0.0.1:3101`;`3101` 或 `8082` 被其他进程占用时,脚本输出占用进程并使用最近可用端口;`--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。
|
||||
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md`、`scripts/dev-rust-stack.sh`。
|
||||
|
||||
## 本地 SpacetimeDB publish 401 可清本地库重发
|
||||
|
||||
@@ -36,10 +36,10 @@ npm run dev:rust
|
||||
1. 检查 `cargo`、`node` 与 `spacetime` CLI。
|
||||
2. Windows Git Bash 下如 `server-rs/.spacetimedb/local/bin/current/spacetimedb-cli.exe` 不存在,先把本机 `spacetime` 所在安装目录的 `bin/` 与 `spacetime.exe` 同步到 `server-rs/.spacetimedb/local/`。
|
||||
3. 启动 SpacetimeDB 前先检查 `server-rs/.spacetimedb/local/data/spacetime.pid`:如果 pid 对应进程仍存在,且同目录 `dev-rust-spacetime-url` 中记录的 URL 可被 `spacetime server ping` 判定在线,则直接复用该宿主;如果 URL 记录缺失,会依次尝试从 `logs/dev-rust-spacetime-start.log` 和 `logs/spacetime-standalone.log` 中解析最近一次监听地址兜底。否则按正常流程重新启动。
|
||||
4. 正常启动 `spacetime start --data-dir server-rs/.spacetimedb/local/data --listen-addr 127.0.0.1:3101`,确保本地数据库与 SpacetimeDB 内部日志落在项目数据目录中;启动成功后把实际 URL 写入 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。
|
||||
4. 如果确认需要新启动 SpacetimeDB,脚本会先检测 `127.0.0.1:3101` 是否可监听;若已占用,输出占用进程并选择从 `3101` 起向后的最近可用端口,再执行 `spacetime start --data-dir server-rs/.spacetimedb/local/data --listen-addr <实际地址>`。启动成功后把实际 URL 写入 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`,后续 publish 与 `api-server` 都使用同一个实际 URL。
|
||||
5. 等待 SpacetimeDB 就绪:优先接受 `spacetime server ping http://127.0.0.1:<spacetime-port>` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:<spacetime-port>/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`。
|
||||
6. 执行 `spacetime publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
|
||||
7. 注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*` 后启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
|
||||
6. 执行 `spacetime publish <本地数据库名> --server <实际 SpacetimeDB URL> --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
|
||||
7. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*`,启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
|
||||
8. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`。
|
||||
9. 注入 `RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
|
||||
10. 任一子进程退出时,脚本回收其余子进程。
|
||||
@@ -61,7 +61,7 @@ Vite 代理覆盖范围:
|
||||
|
||||
本地联调跳过策略:
|
||||
|
||||
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,可使用 `npm run dev -- --skip-spacetime` 跳过 SpacetimeDB 宿主启动,只复用现有监听实例并继续后续发布、`api-server` 与前端启动。若占用方不是本仓库本地 SpacetimeDB,先停止占用进程或改用 `--spacetime-port`。
|
||||
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,脚本会优先按 `spacetime.pid` 与 `dev-rust-spacetime-url` 复用该宿主;如果确认不是可复用宿主,则会先输出占用进程并选择最近可用端口。也可显式使用 `npm run dev -- --skip-spacetime` 跳过 SpacetimeDB 宿主启动,或用 `--spacetime-port` 指定起始探测端口。
|
||||
2. 如果当前没有修改 `server-rs/crates/spacetime-module`,可使用 `npm run dev -- --skip-publish` 跳过数据库发布,降低本地启动时的 SpacetimeDB wasm 编译耗时。
|
||||
3. 如果当前阶段只需要检查 `spacetime-module` 语法,不需要重新发布本地数据库,可执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。该命令只做 Rust 编译检查,不生成新数据库,也不刷新 bindings。
|
||||
|
||||
@@ -118,6 +118,7 @@ npm run dev:rust:logs -- --follow
|
||||
4. 如果 Vite 输出 `/api/auth/refresh`、`/api/auth/login-options` 或 `/api/runtime/custom-world-gallery` 的 `ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。
|
||||
5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0` 也不能直接视为已就绪;本地脚本会继续探测 `/v1/ping`。若 `/v1/ping` 返回 `200`,说明 standalone 已经可用,可以继续发布模块;若 `/v1/ping` 也失败,脚本会继续等待新启动实例,或在 root-dir 已被其他实例占用时输出占用进程。
|
||||
6. 如果本地 `spacetime publish` 显示 `401` 无权限,且确认本地开发数据可以丢弃,可执行 `spacetime --root-dir=server-rs/.spacetimedb/local server clear` 清除本地 SpacetimeDB 数据库后重新发布。重新发布时日志应表现为创建新的数据库,而不是更新旧数据库;如果仍显示更新旧库或继续无权限,说明 root-dir、库名或 CLI 身份仍未对齐。
|
||||
7. Windows / Git Bash 下读取 `spacetime.pid` 或 `dev-rust-spacetime-url` 时,如果文件正被 SpacetimeDB 更新,不能用 `tr/head/xargs` 管道直接读;脚本使用 Node 读取并短重试,避免出现 `tr: read error: Device or resource busy` 后直接中断。
|
||||
|
||||
编译警告治理:
|
||||
|
||||
|
||||
@@ -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