This commit is contained in:
2026-05-08 22:07:05 +08:00
61 changed files with 4364 additions and 202 deletions

View File

@@ -7,6 +7,7 @@ usage() {
用法:
npm run dev:rust
./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110
./scripts/dev-rust-stack.sh --spacetime-data-dir server-rs/.spacetimedb/local/data
./scripts/dev-rust-stack.sh --admin-web-port 3102
./scripts/dev-rust-stack.sh --api-timeout-seconds 600
./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish
@@ -18,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 作为本地数据与日志目录。
4. SpacetimeDB 默认使用 server-rs/.spacetimedb/local/data 作为本地数据目录;端口被占用时自动接受 SpacetimeDB 建议的最近可用端口
5. 默认在发布模块前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
EOF
}
@@ -126,18 +127,18 @@ cleanup() {
wait_for_spacetime() {
local server="$1"
local timeout_seconds="$2"
local root_dir="$3"
local data_dir="$3"
local process_pid="${4:-}"
local deadline=$((SECONDS + timeout_seconds))
while ((SECONDS < deadline)); do
if [[ -n "${process_pid}" ]] && ! kill -0 "${process_pid}" 2>/dev/null; then
echo "[dev:rust] SpacetimeDB 进程在就绪前退出。" >&2
print_spacetime_start_failure_diagnostics "${root_dir}"
print_spacetime_start_failure_diagnostics "${data_dir}"
exit 1
fi
if is_spacetime_ready "${server}" "${root_dir}"; then
if is_spacetime_ready "${server}"; then
return
fi
@@ -145,16 +146,56 @@ wait_for_spacetime() {
done
echo "[dev:rust] 等待 SpacetimeDB 就绪超时: ${server}" >&2
print_spacetime_start_failure_diagnostics "${root_dir}"
print_spacetime_start_failure_diagnostics "${data_dir}"
exit 1
}
wait_for_spacetime_listen_addr() {
local log_file="$1"
local timeout_seconds="$2"
local process_pid="${3:-}"
local deadline=$((SECONDS + timeout_seconds))
local listen_addr=""
while ((SECONDS < deadline)); do
if [[ -f "${log_file}" ]]; then
listen_addr="$(sed -n 's/^.*Starting SpacetimeDB listening on \([^[:space:]]\+\).*$/\1/p' "${log_file}" | tail -n 1)"
if [[ -n "${listen_addr}" ]]; then
echo "${listen_addr}"
return
fi
fi
if [[ -n "${process_pid}" ]] && ! kill -0 "${process_pid}" 2>/dev/null; then
echo "[dev:rust] SpacetimeDB 进程在输出监听地址前退出。" >&2
if [[ -f "${log_file}" ]]; then
echo "[dev:rust] 最近 SpacetimeDB 启动日志: ${log_file}" >&2
tail -n 80 "${log_file}" >&2 || true
fi
exit 1
fi
sleep 0.2
done
echo "[dev:rust] 等待 SpacetimeDB 输出监听地址超时。" >&2
if [[ -f "${log_file}" ]]; then
echo "[dev:rust] 最近 SpacetimeDB 启动日志: ${log_file}" >&2
tail -n 80 "${log_file}" >&2 || true
fi
exit 1
}
port_from_listen_addr() {
local listen_addr="$1"
echo "${listen_addr##*:}"
}
is_spacetime_ready() {
local server="$1"
local root_dir="$2"
local output
if output="$(spacetime --root-dir="${root_dir}" server ping "${server}" 2>&1)" &&
if output="$(spacetime server ping "${server}" 2>&1)" &&
[[ "${output}" == *"Server is online:"* ]]; then
return 0
fi
@@ -174,10 +215,10 @@ request.on("error", () => process.exit(1));
}
print_spacetime_start_failure_diagnostics() {
local root_dir="$1"
local log_file="${root_dir}/data/logs/spacetime-standalone.log"
local data_dir="$1"
local log_file="${data_dir}/logs/spacetime-standalone.log"
echo "[dev:rust] SpacetimeDB root-dir: ${root_dir}" >&2
echo "[dev:rust] SpacetimeDB data-dir: ${data_dir}" >&2
if [[ ! -f "${log_file}" ]]; then
echo "[dev:rust] 未找到 SpacetimeDB standalone 日志: ${log_file}" >&2
@@ -189,26 +230,28 @@ print_spacetime_start_failure_diagnostics() {
if grep -q "mismatched database identity" "${log_file}" 2>/dev/null; then
echo "[dev:rust] 检测到本地 replica 与当前数据库 identity 不一致。" >&2
echo "[dev:rust] 常见原因是同一个 root-dir 保留了旧库 data/replicas/1但 control-db 已指向新库。" >&2
echo "[dev:rust] 若这是可丢弃的本地开发库,请先停止 SpacetimeDB再备份或移走 ${root_dir}/data 后重新启动。" >&2
echo "[dev:rust] 若需要保留数据,不要清理目录;请改回创建旧库的 database/root-dir或先走迁移导出。" >&2
echo "[dev:rust] 常见原因是同一个 data-dir 保留了旧库 replicas/1但 control-db 已指向新库。" >&2
echo "[dev:rust] 若这是可丢弃的本地开发库,请先停止 SpacetimeDB再备份或移走 ${data_dir} 后重新启动。" >&2
echo "[dev:rust] 若需要保留数据,不要清理目录;请改回创建旧库的 database/data-dir或先走迁移导出。" >&2
fi
}
describe_spacetime_root_owner() {
local root_dir="$1"
local windows_root_dir="${root_dir}"
local data_dir="$1"
local windows_data_dir="${data_dir}"
if [[ "${windows_root_dir}" =~ ^/([a-zA-Z])/(.*)$ ]]; then
windows_root_dir="${BASH_REMATCH[1]}:/${BASH_REMATCH[2]}"
if [[ "${windows_data_dir}" =~ ^/([a-zA-Z])/(.*)$ ]]; then
windows_data_dir="${BASH_REMATCH[1]}:/${BASH_REMATCH[2]}"
fi
# Windows 本地开发最常见的失败是同一个 root-dir 下已有 standalone 持有 spacetime.pid
# Windows 本地开发最常见的失败是同一个 data-dir 下已有 standalone 持有 spacetime.pid
# 启动前先打印占用进程,避免用户只看到底层 os error 33 而不知道该停哪个实例。
if command -v powershell.exe >/dev/null 2>&1; then
ROOT_DIR_FOR_POWERSHELL="${windows_root_dir}" powershell.exe -NoProfile -Command '
$rootDir = $env:ROOT_DIR_FOR_POWERSHELL
$normalized = $rootDir.Replace("/", "\")
# 只有 Windows/Git Bash 风格路径才交给 PowerShell 查 Windows 进程;
# WSL/Linux 的 /tmp、/home 路径不能直接拿去匹配 Windows CommandLine容易误命中无关 spacetime 进程。
if command -v powershell.exe >/dev/null 2>&1 && [[ "${data_dir}" =~ ^/([a-zA-Z])/ ]]; then
DATA_DIR_FOR_POWERSHELL="${windows_data_dir}" powershell.exe -NoProfile -Command '
$dataDir = $env:DATA_DIR_FOR_POWERSHELL
$normalized = $dataDir.Replace("/", "\")
Get-CimInstance Win32_Process |
Where-Object { $_.Name -match "spacetime" -and $_.CommandLine -and $_.CommandLine.Replace("/", "\") -like "*$normalized*" } |
ForEach-Object { "pid=$($_.ProcessId) name=$($_.Name) command=$($_.CommandLine)" }
@@ -217,7 +260,7 @@ Get-CimInstance Win32_Process |
fi
if command -v ps >/dev/null 2>&1; then
ps -eo user=,pid=,ppid=,stat=,comm=,args= 2>/dev/null | awk -v root_dir="${root_dir}" '
ps -eo user=,pid=,ppid=,stat=,comm=,args= 2>/dev/null | awk -v data_dir="${data_dir}" '
{
user = $1
pid = $2
@@ -230,7 +273,7 @@ Get-CimInstance Win32_Process |
sub(/^.*\//, "", name)
# 只认真实的 SpacetimeDB 启动进程,避免 .spacetimedb 路径让 grep/awk 自身误命中。
if ((name == "spacetime" || name == "spacetimedb-cli") && index(args, root_dir) > 0) {
if ((name == "spacetime" || name == "spacetimedb-cli") && index(args, data_dir) > 0) {
print user " " pid " " ppid " " stat " " name " " args
}
}
@@ -271,50 +314,6 @@ request.on("error", () => process.exit(1));
exit 1
}
sync_local_spacetime_install() {
local root_dir="$1"
# SpacetimeDB standalone 会在 --root-dir 下回调 bin/current/spacetimedb-cli.exe
# Windows 本地开发使用工程内 root-dir 时,需要把用户级安装目录同步进来。
if [[ "${OSTYPE:-}" != msys* && "${OSTYPE:-}" != cygwin* ]]; then
return
fi
local target_cli="${root_dir}/bin/current/spacetimedb-cli.exe"
if [[ -f "${target_cli}" ]]; then
return
fi
local spacetime_command
spacetime_command="$(command -v spacetime || true)"
if [[ -z "${spacetime_command}" ]]; then
return
fi
local install_dir
install_dir="$(cd -- "$(dirname -- "${spacetime_command}")" && pwd)"
if [[ ! -d "${install_dir}/bin" ]]; then
return
fi
echo "[dev:rust] 同步本机 SpacetimeDB 安装到 ${root_dir}"
mkdir -p "${root_dir}"
cp -a "${install_dir}/bin" "${root_dir}/"
if [[ -f "${install_dir}/spacetime.exe" ]]; then
cp -f "${install_dir}/spacetime.exe" "${root_dir}/spacetime.exe"
fi
# Git Bash 复制 Windows junction 时可能不会生成可执行的 current 目录;
# 若 current 缺失,则用最新版本目录复制出一个真实目录,满足 standalone 回调路径。
if [[ ! -f "${target_cli}" ]]; then
local version_dir
version_dir="$(find "${root_dir}/bin" -mindepth 1 -maxdepth 1 -type d ! -name current | sort -V | tail -n 1)"
if [[ -n "${version_dir}" && -f "${version_dir}/spacetimedb-cli.exe" ]]; then
rm -rf "${root_dir}/bin/current"
cp -a "${version_dir}" "${root_dir}/bin/current"
fi
fi
}
generate_migration_bootstrap_secret() {
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
@@ -363,10 +362,11 @@ ADMIN_WEB_PORT="3102"
SPACETIME_HOST="127.0.0.1"
SPACETIME_PORT="3101"
SPACETIME_ROOT_DIR="${SERVER_RS_DIR}/.spacetimedb/local"
SPACETIME_DATA_DIR="${SPACETIME_ROOT_DIR}/data"
DATABASE=""
API_LOG="info,tower_http=info"
SPACETIME_TIMEOUT_SECONDS="60"
API_SERVER_TIMEOUT_SECONDS="300"
API_SERVER_TIMEOUT_SECONDS="600"
SKIP_SPACETIME=0
SKIP_PUBLISH=0
PRESERVE_DATABASE=0
@@ -436,6 +436,11 @@ while [[ $# -gt 0 ]]; do
;;
--spacetime-root-dir)
SPACETIME_ROOT_DIR="${2:?缺少 --spacetime-root-dir 的值}"
SPACETIME_DATA_DIR="${SPACETIME_ROOT_DIR}/data"
shift 2
;;
--spacetime-data-dir)
SPACETIME_DATA_DIR="${2:?缺少 --spacetime-data-dir 的值}"
shift 2
;;
--database)
@@ -537,39 +542,44 @@ echo "[dev:rust] rust api: ${RUST_SERVER_TARGET}"
echo "[dev:rust] spacetime: ${SPACETIME_SERVER}"
echo "[dev:rust] database: ${DATABASE}"
echo "[dev:rust] spacetime root: ${SPACETIME_ROOT_DIR}"
echo "[dev:rust] spacetime data: ${SPACETIME_DATA_DIR}"
echo "[dev:rust] api timeout: ${API_SERVER_TIMEOUT_SECONDS}s"
if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then
mkdir -p "${SPACETIME_ROOT_DIR}"
sync_local_spacetime_install "${SPACETIME_ROOT_DIR}"
if is_spacetime_ready "${SPACETIME_SERVER}" "${SPACETIME_ROOT_DIR}"; then
echo "[dev:rust] 复用已运行的 SpacetimeDB: ${SPACETIME_SERVER}"
else
SPACETIME_ROOT_OWNER="$(describe_spacetime_root_owner "${SPACETIME_ROOT_DIR}")"
if [[ -n "${SPACETIME_ROOT_OWNER}" ]]; then
echo "[dev:rust] 当前 root-dir 已被其他 SpacetimeDB 实例占用,无法再次启动。" >&2
echo "[dev:rust] 目标地址未就绪: ${SPACETIME_SERVER}" >&2
echo "[dev:rust] 如需复用,请传入占用实例实际端口,例如 --spacetime-port 3199如需重启请先停止下列进程。" >&2
echo "${SPACETIME_ROOT_OWNER}" >&2
exit 1
fi
echo "[dev:rust] 启动 spacetimedb"
(
cd "${SERVER_RS_DIR}"
exec spacetime \
--root-dir="${SPACETIME_ROOT_DIR}" \
start \
--edition standalone \
--listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}"
) &
PIDS+=("$!")
NAMES+=("spacetimedb")
mkdir -p "${SPACETIME_ROOT_DIR}" "${SPACETIME_DATA_DIR}"
SPACETIME_ROOT_OWNER="$(describe_spacetime_root_owner "${SPACETIME_DATA_DIR}")"
if [[ -n "${SPACETIME_ROOT_OWNER}" ]]; then
echo "[dev:rust] 当前 data-dir 已被其他 SpacetimeDB 实例占用,无法再次启动。" >&2
echo "[dev:rust] 如需复用,请传入占用实例实际端口并追加 --skip-spacetime如需重启请先停止下列进程。" >&2
echo "${SPACETIME_ROOT_OWNER}" >&2
exit 1
fi
SPACETIME_START_LOG="${SPACETIME_DATA_DIR}/logs/dev-rust-spacetime-start.log"
mkdir -p "$(dirname -- "${SPACETIME_START_LOG}")"
: >"${SPACETIME_START_LOG}"
echo "[dev:rust] 启动 spacetimedb"
(
cd "${SERVER_RS_DIR}"
# 当目标端口被占用时SpacetimeDB 会询问是否使用最近的可用端口;
# 这里直接发送回车接受默认建议,再从启动日志解析实际监听端口。
printf '\n' | spacetime \
start \
--data-dir "${SPACETIME_DATA_DIR}" \
--listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}"
) 2>&1 | tee "${SPACETIME_START_LOG}" &
PIDS+=("$!")
NAMES+=("spacetimedb")
SPACETIME_LISTEN_ADDR="$(wait_for_spacetime_listen_addr "${SPACETIME_START_LOG}" "${SPACETIME_TIMEOUT_SECONDS}" "${PIDS[0]:-}")"
SPACETIME_PORT="$(port_from_listen_addr "${SPACETIME_LISTEN_ADDR}")"
SPACETIME_SERVER="http://${SPACETIME_HOST}:${SPACETIME_PORT}"
echo "[dev:rust] spacetime actual: ${SPACETIME_SERVER}"
fi
if [[ "${SKIP_PUBLISH}" -ne 1 ]]; then
echo "[dev:rust] 等待 SpacetimeDB 就绪"
wait_for_spacetime "${SPACETIME_SERVER}" "${SPACETIME_TIMEOUT_SECONDS}" "${SPACETIME_ROOT_DIR}" "${PIDS[0]:-}"
wait_for_spacetime "${SPACETIME_SERVER}" "${SPACETIME_TIMEOUT_SECONDS}" "${SPACETIME_DATA_DIR}" "${PIDS[0]:-}"
prepare_migration_bootstrap_secret
PUBLISH_ARGS=(
@@ -586,7 +596,12 @@ if [[ "${SKIP_PUBLISH}" -ne 1 ]]; then
PUBLISH_ARGS+=(--yes)
echo "[dev:rust] 发布 SpacetimeDB 模块: ${DATABASE}"
spacetime --root-dir="${SPACETIME_ROOT_DIR}" "${PUBLISH_ARGS[@]}"
(
cd "${SERVER_RS_DIR}"
# spacetime publish 会在内部调用 Cargo从 server-rs 目录执行,确保读取
# server-rs/.cargo/config.toml 中的 sccache/linker 配置,并复用同一套 target 缓存。
spacetime "${PUBLISH_ARGS[@]}"
)
fi
echo "[dev:rust] 启动 api-server"