#!/usr/bin/env bash
# OpsxDB launcher — scene profiles, not random process soup
#
# ./run.sh # = web:仅 Gin 官网 + docs(:8788)
# ./run.sh web # 同上(浏览器不当客户端)
# ./run.sh gui # 桌面唯一操作入口:临时 .app(Dock 名 OpsxDB)+ 默认 Gin
# ./run.sh all # 同 gui(兼容旧命令;不再起「浏览器工作台产品」)
# ./run.sh server|desktop # 单组件
# ./run.sh package # Tauri 安装包 + sidecar
# ./run.sh stop|restart|status|help
#
# 产品形态:浏览器 = 官网/docs;操作 = 桌面 Tauri。
# src 的 Vite 仅作桌面打包/devUrl 源,不要用浏览器当工作台。
set -euo pipefail
PROJECT_NAME="OpsxDB"
SERVER_PORT="${OPSXDB_SERVER_PORT:-8788}"
WEB_PORT="${OPSXDB_WEB_PORT:-5174}"
SERVER_DIR="server"
SERVER_BIN="opsxdb-server"
SERVER_LABEL="com.opsx.db.server"
WEB_LABEL="com.opsx.db.web"
DESKTOP_LABEL="com.opsx.db.desktop"
# gui 默认是否顺带起 Gin(可用 OPSXDB_GUI_WITH_SERVER=0 关掉)
GUI_WITH_SERVER="${OPSXDB_GUI_WITH_SERVER:-1}"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warning() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERR]${NC} $*"; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
LOG_DIR="$SCRIPT_DIR/logs"
mkdir -p "$LOG_DIR" "$SCRIPT_DIR/$SERVER_DIR/bin"
SERVER_LOG="$LOG_DIR/server.log"
WEB_LOG="$LOG_DIR/web.log"
DESKTOP_LOG="$LOG_DIR/desktop.log"
# shellcheck source=scripts/gui-dev-app.sh
source "$SCRIPT_DIR/scripts/gui-dev-app.sh"
# node/npm 不在此强制注入,由 detect_node 按需探测(start_web/start_desktop 前调用)。
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.cargo/bin:$PATH"
# detect_node 探测 node/npm:PATH 已有直接用;否则依次探测
# $NVM_DIR/versions/node、~/.nvm/versions/node(取最高版本)、homebrew、/usr/local。
# 找不到给出清晰报错并退出。结果写入 NODE_BIN_DIR 供 launchctl/nohup 子 shell 使用。
NODE_BIN_DIR=""
detect_node() {
if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
NODE_BIN_DIR="$(cd "$(dirname "$(command -v node)")" && pwd)"
return 0
fi
local roots=() d v best=""
[ -n "${NVM_DIR:-}" ] && roots+=("$NVM_DIR/versions/node")
roots+=("${HOME}/.nvm/versions/node")
for d in "${roots[@]}"; do
[ -d "$d" ] || continue
v="$(ls -1 "$d" 2>/dev/null | sort -V | tail -n 1)"
if [ -n "$v" ] && [ -x "$d/$v/bin/node" ]; then
best="$d/$v/bin"
break
fi
done
if [ -z "$best" ]; then
for d in /opt/homebrew/bin /usr/local/bin; do
if [ -x "$d/node" ]; then
best="$d"
break
fi
done
fi
if [ -z "$best" ]; then
error "未找到 node/npm。请先安装 Node.js(https://nodejs.org)或 nvm,或设置 NVM_DIR 后重试。"
exit 1
fi
NODE_BIN_DIR="$best"
export PATH="$NODE_BIN_DIR:$PATH"
info "使用 Node.js: $NODE_BIN_DIR"
}
check_port() { lsof -ti:"$1" 2>/dev/null | head -1 || true; }
opsxdb_health() {
curl -fsS --max-time 2 "http://127.0.0.1:$SERVER_PORT/health" 2>/dev/null \
| rg -q '"service"[[:space:]]*:[[:space:]]*"opsxdb"'
}
opsxdb_vite() {
local html
html="$(curl -fsS --max-time 2 "http://127.0.0.1:$WEB_PORT/" 2>/dev/null)" || return 1
[[ "$html" == *'
OpsxDB'* && "$html" == *'/src/main.tsx'* ]]
}
kill_port() {
local pids
pids="$(lsof -ti:"$1" 2>/dev/null || true)"
if [ -n "$pids" ]; then
info "释放端口 $1"
# shellcheck disable=SC2086
kill -9 $pids 2>/dev/null || true
sleep 0.3
fi
}
start_detached() {
local label="$1" log_file="$2" command="$3"
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then
launchctl remove "$label" 2>/dev/null || true
launchctl submit -l "$label" -o "$log_file" -e "$log_file" -- /bin/bash -lc "$command"
else
nohup /bin/bash -lc "$command" >>"$log_file" 2>&1 /dev/null 2>&1; then
launchctl remove "$label" 2>/dev/null || true
fi
}
ensure_npm() {
detect_node
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
warning "缺少 node_modules,正在 npm install..."
npm install
fi
}
build_server() {
info "编译 Gin 服务..."
# -trimpath 去本地路径;-ldflags "-s -w" 去符号表/调试信息;CGO_ENABLED=0 静态化减体积
(cd "$SCRIPT_DIR/$SERVER_DIR" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o "$SCRIPT_DIR/$SERVER_DIR/bin/$SERVER_BIN" ./cmd/opsxdb)
}
wait_http() {
local url="$1" name="$2" retries="${3:-60}"
local i=0
while ! curl -fsS "$url" >/dev/null 2>&1; do
i=$((i + 1))
if [ "$i" -ge "$retries" ]; then
error "$name 启动超时:$url"
return 1
fi
sleep 0.5
done
}
start_server() {
if opsxdb_health; then
success "后端已在运行 :$SERVER_PORT"
return 0
fi
if [ -n "$(check_port "$SERVER_PORT")" ]; then
error ":$SERVER_PORT 已被其他进程占用,不能启动 OpsxDB Gin;不会结束该进程"
return 1
fi
info "启动后端 :$SERVER_PORT ..."
: >"$SERVER_LOG"
build_server
# 默认仅本机;对外需 OPSXDB_BIND_PUBLIC=1
local bind_env="OPSXDB_ADDR=127.0.0.1:$SERVER_PORT"
if [ -n "${OPSXDB_ADDR:-}" ]; then
bind_env="OPSXDB_ADDR=$OPSXDB_ADDR"
elif [ "${OPSXDB_BIND_PUBLIC:-0}" = "1" ] || [ "${OPSXDB_BIND_PUBLIC:-}" = "true" ]; then
bind_env="OPSXDB_BIND_PUBLIC=1"
fi
start_detached "$SERVER_LABEL" "$SERVER_LOG" \
"export PATH=\"/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR/$SERVER_DIR\" && exec env $bind_env GIN_MODE=debug \"./bin/$SERVER_BIN\""
wait_http "http://127.0.0.1:$SERVER_PORT/health" "后端"
success "后端 UP http://127.0.0.1:$SERVER_PORT/health"
}
stop_server() {
stop_detached "$SERVER_LABEL"
if opsxdb_health; then
kill_port "$SERVER_PORT"
elif [ -n "$(check_port "$SERVER_PORT")" ]; then
warning ":$SERVER_PORT 由其他服务占用,未停止该进程"
fi
success "后端已停止"
}
# 仅供桌面 Tauri devUrl / 打包源;不是浏览器产品入口
start_vite_for_desktop() {
if opsxdb_vite; then
success "桌面 UI(Vite) 已在运行 :$WEB_PORT"
return 0
fi
if [ -n "$(check_port "$WEB_PORT")" ]; then
error ":$WEB_PORT 已被其他服务占用,无法启动 OpsxDB 桌面 UI;不会结束该进程"
return 1
fi
info "启动桌面 UI 源 :${WEB_PORT}(仅本机给 Tauri,勿当浏览器客户端)..."
: >"$WEB_LOG"
ensure_npm
# Vite 用 nohup:launchctl submit 在本机对 npm -w 经常交不出进程(web.log 空、40s 超时)
nohup /bin/bash -lc "export PATH=\"$NODE_BIN_DIR:/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR\" && exec npm run dev:ui" >>"$WEB_LOG" 2>&1 "$DESKTOP_LOG"
ensure_npm
kill_opsxdb_gui
build_server
prepare_sidecar
npm run tauri -- build --debug --no-bundle
ensure_gui_dev_app || return 1
if [ "${GUI_DEV_NEEDS_VITE:-0}" = "1" ]; then
start_vite_for_desktop
fi
open "$GUI_APP" 2>>"$DESKTOP_LOG" || { error "open 失败,见 $DESKTOP_LOG"; return 1; }
local i=0
while true; do
if gui_is_running; then
success "桌面 OpsxDB 已启动(临时 .app,Dock 用 icon.icns / 名称 OpsxDB)"
info "路径: $GUI_APP"
return 0
fi
i=$((i + 1))
if [ "$i" -ge 30 ]; then
error "桌面启动超时,见 $DESKTOP_LOG"
tail -n 40 "$DESKTOP_LOG" || true
return 1
fi
sleep 1
done
}
stop_desktop() {
kill_opsxdb_gui
success "桌面已停止"
}
status() {
echo "$PROJECT_NAME 状态"
if opsxdb_health; then
success "server :$SERVER_PORT UP $(curl -fsS "http://127.0.0.1:$SERVER_PORT/health")"
else
warning "server :$SERVER_PORT DOWN"
if [ -n "$(check_port "$SERVER_PORT")" ]; then
warning ":$SERVER_PORT 被其他进程占用;桌面可使用独立动态端口"
fi
fi
if opsxdb_vite; then
success "vite :${WEB_PORT} UP(桌面源,非浏览器产品)"
else
warning "vite :$WEB_PORT DOWN"
if [ -n "$(check_port "$WEB_PORT")" ]; then
warning ":$WEB_PORT 被其他服务占用"
fi
fi
if curl -fsS "http://127.0.0.1:$SERVER_PORT/home" >/dev/null 2>&1 \
|| curl -fsS "http://127.0.0.1:$SERVER_PORT/docs" >/dev/null 2>&1; then
success "site :$SERVER_PORT 官网/docs 可达"
fi
if gui_is_running; then
success "gui OpsxDB UP"
else
warning "gui OpsxDB DOWN"
fi
}
prepare_sidecar() {
local triple
triple="$(rustc -vV 2>/dev/null | awk '/host:/ {print $2}')"
if [ -z "$triple" ]; then
error "无法检测 rustc host triple"
return 1
fi
local bin_dir="$SCRIPT_DIR/src-tauri/binaries"
mkdir -p "$bin_dir"
cp "$SCRIPT_DIR/$SERVER_DIR/bin/$SERVER_BIN" "$bin_dir/opsxdb-server-$triple"
chmod +x "$bin_dir/opsxdb-server-$triple"
success "sidecar → binaries/opsxdb-server-$triple"
}
package_desktop() {
info "构建桌面可安装包(含本机 sidecar)..."
ensure_npm
build_server
prepare_sidecar
(
cd "$SCRIPT_DIR"
export VITE_OPSXDB_API="http://127.0.0.1:$SERVER_PORT"
npm run tauri -- build
)
success "桌面包构建完成(见 src-tauri/target/release/bundle)"
}
usage() {
cat <