#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' 用法: npm run dev:rust:sh ./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110 ./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish ./scripts/dev-rust-stack.sh --clear-database 说明: 1. 默认同时启动 SpacetimeDB standalone、Rust api-server 与 Vite 前端。 2. 默认会 publish server-rs/crates/spacetime-module,但不会清空数据库。 3. 只有显式传入 --clear-database 时,才会追加 spacetime publish --clear-database。 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 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] SpacetimeDB 进程在就绪前退出。" >&2 exit 1 fi if spacetime server ping "${server}" >/dev/null 2>&1; then return fi sleep 0.5 done echo "[dev:rust] 等待 SpacetimeDB 就绪超时: ${server}" >&2 exit 1 } 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" API_HOST="127.0.0.1" API_PORT="8082" WEB_HOST="0.0.0.0" WEB_PORT="3000" SPACETIME_HOST="127.0.0.1" SPACETIME_PORT="3101" SPACETIME_ROOT_DIR="${SERVER_RS_DIR}/.spacetimedb/local" DATABASE="" API_LOG="info,tower_http=info" SPACETIME_TIMEOUT_SECONDS="60" SKIP_SPACETIME=0 SKIP_PUBLISH=0 CLEAR_DATABASE=0 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 ;; --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 的值}" 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 ;; --skip-spacetime) SKIP_SPACETIME=1 shift ;; --skip-publish) SKIP_PUBLISH=1 shift ;; --clear-database) CLEAR_DATABASE=1 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 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}" 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] rust api: ${RUST_SERVER_TARGET}" echo "[dev:rust] spacetime: ${SPACETIME_SERVER}" echo "[dev:rust] database: ${DATABASE}" if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then mkdir -p "${SPACETIME_ROOT_DIR}" echo "[dev:rust] 启动 spacetimedb" ( cd "${SERVER_RS_DIR}" exec spacetime \ start \ --edition standalone \ --listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}" ) & PIDS+=("$!") NAMES+=("spacetimedb") fi if [[ "${SKIP_PUBLISH}" -ne 1 ]]; then echo "[dev:rust] 等待 SpacetimeDB 就绪" wait_for_spacetime "${SPACETIME_SERVER}" "${SPACETIME_TIMEOUT_SECONDS}" "${PIDS[0]:-}" PUBLISH_ARGS=( publish "${DATABASE}" --server "${SPACETIME_SERVER}" --module-path "${MODULE_PATH}" ) if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then PUBLISH_ARGS+=(--clear-database) fi PUBLISH_ARGS+=(--yes) echo "[dev:rust] 发布 SpacetimeDB 模块: ${DATABASE}" 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}" ) & PIDS+=("$!") NAMES+=("api-server") echo "[dev:rust] 启动 vite" ( cd "${REPO_ROOT}" GENARRATIVE_BACKEND_STACK="rust" \ RUST_SERVER_TARGET="${RUST_SERVER_TARGET}" \ GENARRATIVE_RUNTIME_SERVER_TARGET="${RUST_SERVER_TARGET}" \ VITE_DEV_HOST="${WEB_HOST}" \ exec node "${VITE_CLI_PATH}" "--port=${WEB_PORT}" "--host=${WEB_HOST}" ) & PIDS+=("$!") NAMES+=("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}"