602 lines
19 KiB
Bash
602 lines
19 KiB
Bash
#!/usr/bin/env bash
|
||
|
||
set -euo pipefail
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:
|
||
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
|
||
./scripts/dev-rust-stack.sh --preserve-database
|
||
./scripts/dev-rust-stack.sh --no-migration-bootstrap-secret
|
||
npm run dev:rust:logs -- --follow
|
||
|
||
说明:
|
||
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 建议的最近可用端口。
|
||
5. 默认在发布模块前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
|
||
EOF
|
||
}
|
||
|
||
require_command() {
|
||
local command_name="$1"
|
||
|
||
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||
echo "[dev:rust] 缺少命令: ${command_name}" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
resolve_client_host() {
|
||
local host_name="$1"
|
||
|
||
if [[ "${host_name}" == "0.0.0.0" || "${host_name}" == "::" ]]; then
|
||
echo "127.0.0.1"
|
||
return
|
||
fi
|
||
|
||
echo "${host_name}"
|
||
}
|
||
|
||
cleanup() {
|
||
local index
|
||
|
||
for ((index = ${#PIDS[@]} - 1; index >= 0; index--)); do
|
||
local pid="${PIDS[index]}"
|
||
local name="${NAMES[index]}"
|
||
|
||
if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
|
||
echo "[dev:rust] 停止 ${name} (pid=${pid})"
|
||
if command -v pgrep >/dev/null 2>&1; then
|
||
while read -r child_pid; do
|
||
if [[ -n "${child_pid}" ]]; then
|
||
kill "${child_pid}" 2>/dev/null || true
|
||
fi
|
||
done < <(pgrep -P "${pid}" 2>/dev/null || true)
|
||
fi
|
||
kill "${pid}" 2>/dev/null || true
|
||
sleep 0.2
|
||
if kill -0 "${pid}" 2>/dev/null; then
|
||
kill -9 "${pid}" 2>/dev/null || true
|
||
fi
|
||
fi
|
||
done
|
||
}
|
||
|
||
wait_for_spacetime() {
|
||
local server="$1"
|
||
local timeout_seconds="$2"
|
||
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 "${data_dir}"
|
||
exit 1
|
||
fi
|
||
|
||
if is_spacetime_ready "${server}"; then
|
||
return
|
||
fi
|
||
|
||
sleep 0.5
|
||
done
|
||
|
||
echo "[dev:rust] 等待 SpacetimeDB 就绪超时: ${server}" >&2
|
||
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 output
|
||
|
||
if output="$(spacetime server ping "${server}" 2>&1)" &&
|
||
[[ "${output}" == *"Server is online:"* ]]; then
|
||
return 0
|
||
fi
|
||
|
||
# SpacetimeDB CLI 2.1.0 在 Windows 下可能对已监听的 standalone 返回 502;
|
||
# 直接探测 HTTP 健康端点,避免 npm run dev:rust 卡在“等待 SpacetimeDB 就绪”。
|
||
node -e '
|
||
const target = new URL("/v1/ping", process.argv[1]);
|
||
const client = target.protocol === "https:" ? require("https") : require("http");
|
||
const request = client.get(target, { timeout: 1000 }, (response) => {
|
||
response.resume();
|
||
process.exit(response.statusCode >= 200 && response.statusCode < 300 ? 0 : 1);
|
||
});
|
||
request.on("timeout", () => request.destroy(new Error("timeout")));
|
||
request.on("error", () => process.exit(1));
|
||
' "${server}" >/dev/null 2>&1
|
||
}
|
||
|
||
print_spacetime_start_failure_diagnostics() {
|
||
local data_dir="$1"
|
||
local log_file="${data_dir}/logs/spacetime-standalone.log"
|
||
|
||
echo "[dev:rust] SpacetimeDB data-dir: ${data_dir}" >&2
|
||
|
||
if [[ ! -f "${log_file}" ]]; then
|
||
echo "[dev:rust] 未找到 SpacetimeDB standalone 日志: ${log_file}" >&2
|
||
return
|
||
fi
|
||
|
||
echo "[dev:rust] 最近 SpacetimeDB standalone 日志: ${log_file}" >&2
|
||
tail -n 80 "${log_file}" >&2 || true
|
||
|
||
if grep -q "mismatched database identity" "${log_file}" 2>/dev/null; then
|
||
echo "[dev:rust] 检测到本地 replica 与当前数据库 identity 不一致。" >&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 data_dir="$1"
|
||
local windows_data_dir="${data_dir}"
|
||
|
||
if [[ "${windows_data_dir}" =~ ^/([a-zA-Z])/(.*)$ ]]; then
|
||
windows_data_dir="${BASH_REMATCH[1]}:/${BASH_REMATCH[2]}"
|
||
fi
|
||
|
||
# Windows 本地开发最常见的失败是同一个 data-dir 下已有 standalone 持有 spacetime.pid;
|
||
# 启动前先打印占用进程,避免用户只看到底层 os error 33 而不知道该停哪个实例。
|
||
# 只有 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)" }
|
||
' 2>/dev/null || true
|
||
return
|
||
fi
|
||
|
||
if command -v ps >/dev/null 2>&1; then
|
||
ps -eo user=,pid=,ppid=,stat=,comm=,args= 2>/dev/null | awk -v data_dir="${data_dir}" '
|
||
{
|
||
user = $1
|
||
pid = $2
|
||
ppid = $3
|
||
stat = $4
|
||
command = $5
|
||
args = $0
|
||
sub(/^[[:space:]]*[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]*/, "", args)
|
||
name = command
|
||
sub(/^.*\//, "", name)
|
||
|
||
# 只认真实的 SpacetimeDB 启动进程,避免 .spacetimedb 路径让 grep/awk 自身误命中。
|
||
if ((name == "spacetime" || name == "spacetimedb-cli") && index(args, data_dir) > 0) {
|
||
print user " " pid " " ppid " " stat " " name " " args
|
||
}
|
||
}
|
||
' || true
|
||
fi
|
||
}
|
||
|
||
wait_for_api_server() {
|
||
local health_url="$1"
|
||
local timeout_seconds="$2"
|
||
local process_pid="${3:-}"
|
||
local deadline=$((SECONDS + timeout_seconds))
|
||
|
||
while ((SECONDS < deadline)); do
|
||
if [[ -n "${process_pid}" ]] && ! kill -0 "${process_pid}" 2>/dev/null; then
|
||
echo "[dev:rust] api-server 进程在就绪前退出。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 使用 Node 发起健康检查,避免要求 Windows 本地额外安装 curl/wget。
|
||
if node -e '
|
||
const target = process.argv[1];
|
||
const client = target.startsWith("https:") ? require("https") : require("http");
|
||
const request = client.get(target, { timeout: 1000 }, (response) => {
|
||
response.resume();
|
||
process.exit(response.statusCode >= 200 && response.statusCode < 500 ? 0 : 1);
|
||
});
|
||
request.on("timeout", () => request.destroy(new Error("timeout")));
|
||
request.on("error", () => process.exit(1));
|
||
' "${health_url}" >/dev/null 2>&1; then
|
||
return
|
||
fi
|
||
|
||
sleep 0.5
|
||
done
|
||
|
||
echo "[dev:rust] 等待 api-server 就绪超时: ${health_url}" >&2
|
||
exit 1
|
||
}
|
||
|
||
|
||
generate_migration_bootstrap_secret() {
|
||
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
|
||
}
|
||
|
||
prepare_migration_bootstrap_secret() {
|
||
case "${MIGRATION_BOOTSTRAP_SECRET_MODE}" in
|
||
auto)
|
||
MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)"
|
||
;;
|
||
manual)
|
||
if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then
|
||
echo "[dev:rust] 迁移引导密钥至少需要 16 个字符。" >&2
|
||
exit 1
|
||
fi
|
||
;;
|
||
disabled)
|
||
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
|
||
echo "[dev:rust] 未启用迁移引导密钥。"
|
||
return
|
||
;;
|
||
*)
|
||
echo "[dev:rust] 未知迁移引导密钥模式: ${MIGRATION_BOOTSTRAP_SECRET_MODE}" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}"
|
||
echo "[dev:rust] 迁移引导密钥: ${MIGRATION_BOOTSTRAP_SECRET}"
|
||
}
|
||
|
||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||
SERVER_RS_DIR="${REPO_ROOT}/server-rs"
|
||
MANIFEST_PATH="${SERVER_RS_DIR}/Cargo.toml"
|
||
MODULE_PATH="${SERVER_RS_DIR}/crates/spacetime-module"
|
||
VITE_CLI_PATH="${REPO_ROOT}/scripts/vite-cli.mjs"
|
||
ADMIN_WEB_DIR="${REPO_ROOT}/apps/admin-web"
|
||
|
||
API_HOST="127.0.0.1"
|
||
API_PORT="8082"
|
||
WEB_HOST="0.0.0.0"
|
||
WEB_PORT="3000"
|
||
ADMIN_WEB_HOST="127.0.0.1"
|
||
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="600"
|
||
SKIP_SPACETIME=0
|
||
SKIP_PUBLISH=0
|
||
PRESERVE_DATABASE=0
|
||
MIGRATION_BOOTSTRAP_SECRET=""
|
||
MIGRATION_BOOTSTRAP_SECRET_MODE="auto"
|
||
PIDS=()
|
||
NAMES=()
|
||
|
||
read_local_spacetime_database() {
|
||
local config_path="${REPO_ROOT}/spacetime.local.json"
|
||
|
||
if [[ ! -f "${config_path}" ]]; then
|
||
return
|
||
fi
|
||
|
||
node -e '
|
||
const fs = require("fs");
|
||
const path = process.argv[1];
|
||
try {
|
||
const value = JSON.parse(fs.readFileSync(path, "utf8")).database;
|
||
if (typeof value === "string" && value.trim()) {
|
||
process.stdout.write(value.trim());
|
||
}
|
||
} catch (error) {
|
||
process.stderr.write(`[dev:rust] ignore invalid spacetime.local.json: ${error.message}\n`);
|
||
}
|
||
' "${config_path}"
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-h|--help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
--api-host)
|
||
API_HOST="${2:?缺少 --api-host 的值}"
|
||
shift 2
|
||
;;
|
||
--api-port)
|
||
API_PORT="${2:?缺少 --api-port 的值}"
|
||
shift 2
|
||
;;
|
||
--web-host)
|
||
WEB_HOST="${2:?缺少 --web-host 的值}"
|
||
shift 2
|
||
;;
|
||
--web-port)
|
||
WEB_PORT="${2:?缺少 --web-port 的值}"
|
||
shift 2
|
||
;;
|
||
--admin-web-host)
|
||
ADMIN_WEB_HOST="${2:?缺少 --admin-web-host 的值}"
|
||
shift 2
|
||
;;
|
||
--admin-web-port)
|
||
ADMIN_WEB_PORT="${2:?缺少 --admin-web-port 的值}"
|
||
shift 2
|
||
;;
|
||
--spacetime-host)
|
||
SPACETIME_HOST="${2:?缺少 --spacetime-host 的值}"
|
||
shift 2
|
||
;;
|
||
--spacetime-port)
|
||
SPACETIME_PORT="${2:?缺少 --spacetime-port 的值}"
|
||
shift 2
|
||
;;
|
||
--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)
|
||
DATABASE="${2:?缺少 --database 的值}"
|
||
shift 2
|
||
;;
|
||
--log)
|
||
API_LOG="${2:?缺少 --log 的值}"
|
||
shift 2
|
||
;;
|
||
--spacetime-timeout-seconds)
|
||
SPACETIME_TIMEOUT_SECONDS="${2:?缺少 --spacetime-timeout-seconds 的值}"
|
||
shift 2
|
||
;;
|
||
--api-timeout-seconds)
|
||
API_SERVER_TIMEOUT_SECONDS="${2:?缺少 --api-timeout-seconds 的值}"
|
||
shift 2
|
||
;;
|
||
--skip-spacetime)
|
||
SKIP_SPACETIME=1
|
||
shift
|
||
;;
|
||
--skip-publish)
|
||
SKIP_PUBLISH=1
|
||
shift
|
||
;;
|
||
--clear-database)
|
||
PRESERVE_DATABASE=0
|
||
shift
|
||
;;
|
||
--preserve-database)
|
||
PRESERVE_DATABASE=1
|
||
shift
|
||
;;
|
||
--migration-bootstrap-secret)
|
||
MIGRATION_BOOTSTRAP_SECRET="${2:?缺少 --migration-bootstrap-secret 的值}"
|
||
MIGRATION_BOOTSTRAP_SECRET_MODE="manual"
|
||
shift 2
|
||
;;
|
||
--no-migration-bootstrap-secret)
|
||
MIGRATION_BOOTSTRAP_SECRET=""
|
||
MIGRATION_BOOTSTRAP_SECRET_MODE="disabled"
|
||
shift
|
||
;;
|
||
*)
|
||
echo "[dev:rust] 未知参数: $1" >&2
|
||
usage >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if [[ -z "${DATABASE//[[:space:]]/}" ]]; then
|
||
DATABASE="$(read_local_spacetime_database)"
|
||
fi
|
||
|
||
if [[ -z "${DATABASE//[[:space:]]/}" ]]; then
|
||
DATABASE="genarrative-dev"
|
||
fi
|
||
|
||
if [[ ! -f "${MANIFEST_PATH}" ]]; then
|
||
echo "[dev:rust] 未找到 ${MANIFEST_PATH},无法启动 Rust 本地栈。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -f "${MODULE_PATH}/Cargo.toml" ]]; then
|
||
echo "[dev:rust] 未找到 ${MODULE_PATH}/Cargo.toml,无法发布 SpacetimeDB 模块。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -f "${VITE_CLI_PATH}" ]]; then
|
||
echo "[dev:rust] 未找到 ${VITE_CLI_PATH},无法启动 Web 前端。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [[ ! -f "${ADMIN_WEB_DIR}/package.json" ]]; then
|
||
echo "[dev:rust] 未找到 ${ADMIN_WEB_DIR}/package.json,无法启动后台前端。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
require_command cargo
|
||
require_command node
|
||
|
||
if [[ "${SKIP_SPACETIME}" -ne 1 || "${SKIP_PUBLISH}" -ne 1 ]]; then
|
||
require_command spacetime
|
||
fi
|
||
|
||
SPACETIME_SERVER="http://${SPACETIME_HOST}:${SPACETIME_PORT}"
|
||
API_TARGET_HOST="$(resolve_client_host "${API_HOST}")"
|
||
RUST_SERVER_TARGET="http://${API_TARGET_HOST}:${API_PORT}"
|
||
ADMIN_WEB_TARGET_HOST="$(resolve_client_host "${ADMIN_WEB_HOST}")"
|
||
|
||
trap cleanup EXIT INT TERM
|
||
|
||
echo "[dev:rust] repo: ${REPO_ROOT}"
|
||
echo "[dev:rust] web: http://127.0.0.1:${WEB_PORT}"
|
||
echo "[dev:rust] admin web: http://${ADMIN_WEB_TARGET_HOST}:${ADMIN_WEB_PORT}"
|
||
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}" "${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_DATA_DIR}" "${PIDS[0]:-}"
|
||
prepare_migration_bootstrap_secret
|
||
|
||
PUBLISH_ARGS=(
|
||
publish
|
||
"${DATABASE}"
|
||
--server "${SPACETIME_SERVER}"
|
||
--module-path "${MODULE_PATH}"
|
||
)
|
||
|
||
if [[ "${PRESERVE_DATABASE}" -ne 1 ]]; then
|
||
PUBLISH_ARGS+=(-c=on-conflict)
|
||
fi
|
||
|
||
PUBLISH_ARGS+=(--yes)
|
||
|
||
echo "[dev:rust] 发布 SpacetimeDB 模块: ${DATABASE}"
|
||
(
|
||
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"
|
||
(
|
||
cd "${REPO_ROOT}"
|
||
GENARRATIVE_API_HOST="${API_HOST}" \
|
||
GENARRATIVE_API_PORT="${API_PORT}" \
|
||
GENARRATIVE_API_LOG="${API_LOG}" \
|
||
GENARRATIVE_SPACETIME_SERVER_URL="${SPACETIME_SERVER}" \
|
||
GENARRATIVE_SPACETIME_DATABASE="${DATABASE}" \
|
||
exec cargo run -p api-server --manifest-path "${MANIFEST_PATH}"
|
||
) &
|
||
API_PID="$!"
|
||
PIDS+=("${API_PID}")
|
||
NAMES+=("api-server")
|
||
|
||
echo "[dev:rust] 等待 api-server 就绪"
|
||
wait_for_api_server "${RUST_SERVER_TARGET}/healthz" "${API_SERVER_TIMEOUT_SECONDS}" "${API_PID}"
|
||
|
||
echo "[dev:rust] 启动 vite"
|
||
(
|
||
cd "${REPO_ROOT}"
|
||
RUST_SERVER_TARGET="${RUST_SERVER_TARGET}" \
|
||
GENARRATIVE_RUNTIME_SERVER_TARGET="${RUST_SERVER_TARGET}" \
|
||
ADMIN_WEB_TARGET="http://${ADMIN_WEB_TARGET_HOST}:${ADMIN_WEB_PORT}" \
|
||
ADMIN_WEB_PORT="${ADMIN_WEB_PORT}" \
|
||
VITE_DEV_HOST="${WEB_HOST}" \
|
||
exec node "${VITE_CLI_PATH}" "--port=${WEB_PORT}" "--host=${WEB_HOST}"
|
||
) &
|
||
PIDS+=("$!")
|
||
NAMES+=("vite")
|
||
|
||
echo "[dev:rust] 启动 admin vite"
|
||
(
|
||
cd "${ADMIN_WEB_DIR}"
|
||
ADMIN_API_TARGET="${RUST_SERVER_TARGET}" \
|
||
GENARRATIVE_API_TARGET="${RUST_SERVER_TARGET}" \
|
||
GENARRATIVE_API_PORT="${API_PORT}" \
|
||
exec node "${VITE_CLI_PATH}" "--host=${ADMIN_WEB_HOST}" "--port=${ADMIN_WEB_PORT}"
|
||
) &
|
||
PIDS+=("$!")
|
||
NAMES+=("admin-vite")
|
||
|
||
echo "[dev:rust] 本地 Rust 栈已启动。按 Ctrl+C 停止全部子进程。"
|
||
|
||
set +e
|
||
wait -n "${PIDS[@]}"
|
||
EXIT_CODE="$?"
|
||
set -e
|
||
|
||
echo "[dev:rust] 子进程已退出,开始回收本地 Rust 栈,退出码: ${EXIT_CODE}"
|
||
exit "${EXIT_CODE}"
|