Files

350 lines
10 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# OpsxShell launcher — scene profiles(对齐 opsxdb / opsxapi)
#
# ./run.sh # = web:仅 Gin 官网 + docs(:8789)
# ./run.sh web # 同上(浏览器不当操作客户端)
# ./run.sh gui # 桌面唯一操作入口:临时 .app(Dock 图标)+ 默认 Gin
# ./run.sh all # 同 gui(兼容旧命令)
# ./run.sh server|desktop
# ./run.sh clean-apps # 全量开发机卫生(~/devops/scripts/clean-opsx-apps.sh)
# ./run.sh stop|restart|status|help
#
# 产品形态:浏览器 = 官网/docs;操作 = 桌面 Tauri(src/ + src-tauri/)。
# Vite :5175 仅作桌面 WebView 源,不要用浏览器当工作台。
set -euo pipefail
PROJECT_NAME="OpsxShell"
SERVER_PORT="${OPSXSHELL_SERVER_PORT:-8789}"
WEB_PORT="${OPSXSHELL_WEB_PORT:-5175}"
SERVER_DIR="server"
SERVER_BIN="opsxshell-server"
SERVER_LABEL="com.opsx.shell.server"
WEB_LABEL="com.opsx.shell.web"
DESKTOP_LABEL="com.opsx.shell.desktop"
# gui 默认是否顺带起 Gin(可用 OPSXSHELL_GUI_WITH_SERVER=0 关掉)
GUI_WITH_SERVER="${OPSXSHELL_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=gui-dev-app.sh
source "$SCRIPT_DIR/scripts/gui-dev-app.sh"
OPSX_CLEAN_SCRIPT="${OPSX_CLEAN_SCRIPT:-$HOME/devops/scripts/clean-opsx-apps.sh}"
# gui 启动前轻量清:推残留 DMG + 注销非 /Applications、非 keep-path 的幽灵。
# 不删 target、不 ResetLaunchPad / kill Dock。逻辑在 clean-opsx-apps.sh --light。
opsx_light_clean_before_gui() {
local keep="$1"
if [ "$(uname -s)" != "Darwin" ]; then
return 0
fi
if [ ! -x "$OPSX_CLEAN_SCRIPT" ]; then
warning "找不到清理脚本: $OPSX_CLEAN_SCRIPT(跳过轻量清)"
return 0
fi
info "gui light-clean (keep-path=${keep})..."
"$OPSX_CLEAN_SCRIPT" --light --keep-path="$keep" --no-verify \
|| warning "轻量清未完全成功(继续启动)"
}
run_clean_apps() {
if [ ! -x "$OPSX_CLEAN_SCRIPT" ]; then
error "找不到: $OPSX_CLEAN_SCRIPT"
return 1
fi
# 转发剩余参数(如 --delete-target-apps);默认 full
"$OPSX_CLEAN_SCRIPT" "$@"
}
# Same path as src-tauri infrastructure/file_log.rs (dirs::data_local_dir).
if [ "$(uname -s)" = "Darwin" ]; then
APP_LOG="$HOME/Library/Application Support/opsxshell/logs/opsxshell.log"
else
APP_LOG="${XDG_DATA_HOME:-$HOME/.local/share}/opsxshell/logs/opsxshell.log"
fi
export PATH="/opt/homebrew/bin:/usr/local/bin:${HOME}/.cargo/bin:$PATH"
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 或 nvm。"
exit 1
fi
NODE_BIN_DIR="$best"
export PATH="$NODE_BIN_DIR:$PATH"
info "使用 Node.js: $NODE_BIN_DIR"
}
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 &
fi
}
stop_detached() {
local label="$1"
if [ "$(uname -s)" = "Darwin" ] && command -v launchctl >/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 官网/docs..."
(
cd "$SCRIPT_DIR/$SERVER_DIR"
export GOPROXY="${GOPROXY:-https://goproxy.io,direct}"
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" \
-o "$SCRIPT_DIR/$SERVER_DIR/bin/$SERVER_BIN" ./cmd/opsxshell-site
)
}
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 curl -fsS "http://127.0.0.1:$SERVER_PORT/health" >/dev/null 2>&1; then
success "官网服务已在运行 :$SERVER_PORT"
return 0
fi
info "启动官网/docs :$SERVER_PORT ..."
: >"$SERVER_LOG"
kill_port "$SERVER_PORT"
build_server
local bind_env="OPSXSHELL_ADDR=127.0.0.1:$SERVER_PORT"
if [ -n "${OPSXSHELL_ADDR:-}" ]; then
bind_env="OPSXSHELL_ADDR=$OPSXSHELL_ADDR"
elif [ "${OPSXSHELL_BIND_PUBLIC:-0}" = "1" ] || [ "${OPSXSHELL_BIND_PUBLIC:-}" = "true" ]; then
bind_env="OPSXSHELL_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/ · docs → http://127.0.0.1:$SERVER_PORT/docs"
}
stop_server() {
stop_detached "$SERVER_LABEL"
kill_port "$SERVER_PORT"
success "官网服务已停止"
}
# 仅供桌面 Tauri devUrl;不是浏览器产品入口
start_vite_for_desktop() {
if curl -fsS "http://127.0.0.1:$WEB_PORT/" >/dev/null 2>&1; then
success "桌面 UI(Vite) 已在运行 :$WEB_PORT"
return 0
fi
info "启动桌面 UI 源 :${WEB_PORT}(仅本机给 Tauri,勿当浏览器客户端)..."
: >"$WEB_LOG"
ensure_npm
kill_port "$WEB_PORT"
start_detached "$WEB_LABEL" "$WEB_LOG" \
"export PATH=\"$NODE_BIN_DIR:/opt/homebrew/bin:/usr/local/bin:\$PATH\"; cd \"$SCRIPT_DIR\" && exec npm run dev:ui"
wait_http "http://127.0.0.1:$WEB_PORT/" "Vite" 80
success "桌面 UI(Vite) UP http://127.0.0.1:${WEB_PORT}(请用 ./run.sh gui)"
}
stop_web() {
stop_detached "$WEB_LABEL"
kill_port "$WEB_PORT"
success "Vite 已停止"
}
start_desktop() {
opsx_light_clean_before_gui "$GUI_APP"
info "启动桌面 OpsxShell(临时 .app + 当前图标)..."
: >"$DESKTOP_LOG"
ensure_npm
kill_opsxshell_gui
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 "桌面 OpsxShell 已启动(临时 .app,Dock 用 icon.icns / 名称 OpsxShell)"
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_opsxshell_gui
success "桌面已停止"
}
status() {
echo "$PROJECT_NAME 状态"
if curl -fsS "http://127.0.0.1:$SERVER_PORT/health" >/dev/null 2>&1; then
success "server :$SERVER_PORT UP $(curl -fsS "http://127.0.0.1:$SERVER_PORT/health")"
else
warning "server :$SERVER_PORT DOWN"
fi
if curl -fsS "http://127.0.0.1:$WEB_PORT/" >/dev/null 2>&1; then
success "vite :${WEB_PORT} UP(桌面源,非浏览器产品)"
else
warning "vite :$WEB_PORT DOWN"
fi
if curl -fsS "http://127.0.0.1:$SERVER_PORT/" >/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 OpsxShell UP"
else
warning "gui OpsxShell DOWN"
fi
}
usage() {
cat <<USAGE
$PROJECT_NAME 启动脚本(产品形态:浏览器=官网/docs · 操作=桌面)
./run.sh # 同 web
./run.sh web # 仅 Gin:官网 + docs → http://127.0.0.1:$SERVER_PORT/
./run.sh gui # 桌面:临时 .app(Dock=OpsxShell / icon.icns;默认也起 Gin)
./run.sh all # 同 gui(兼容旧名)
./run.sh server # 只 Gin(官网/docs)
./run.sh desktop # 只 Desktop
./run.sh stop | restart | status | help
./run.sh clean-apps # 全量开发机卫生(默认只注销不删 target)
端口: Gin/官网 $SERVER_PORT · 桌面 Vite 源 ${WEB_PORT}(不对外当客户端)
日志: logs/{server,web,desktop}.log · 应用日志见 $APP_LOG
USAGE
}
start_web_scene() {
start_server
stop_web || true
echo
success "官网/文档就绪 → http://127.0.0.1:$SERVER_PORT/ · docs → http://127.0.0.1:$SERVER_PORT/docs"
info "操作请用 ./run.sh gui(桌面)。:$WEB_PORT 不是产品入口。"
}
start_gui_scene() {
if [ "$GUI_WITH_SERVER" = "1" ]; then
start_server
else
info "跳过 Gin(OPSXSHELL_GUI_WITH_SERVER=0)"
fi
start_desktop
echo
success "GUI 场景就绪 → 桌面窗口 OpsxShell"
}
start_all_scene() {
start_gui_scene
}
stop_all() {
stop_desktop || true
stop_web || true
stop_server || true
}
cmd="${1:-web}"
case "$cmd" in
web|start) start_web_scene ;;
gui|desktop-scene) start_gui_scene ;;
all) start_all_scene ;;
server) start_server ;;
web-only|vite)
warning "Vite 仅桌面源,不是浏览器工作台;请用: ./run.sh gui"
start_vite_for_desktop
;;
desktop|gui-only) start_desktop ;;
stop) stop_all ;;
restart)
stop_all || true
start_web_scene
;;
status) status ;;
clean-apps) run_clean_apps "${@:2}" ;;
help|-h|--help) usage ;;
*) error "未知命令: $cmd"; usage; exit 1 ;;
esac