From e7a479ccf52324d8aee619c6f176fc4f2e41ba1a Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Fri, 26 Jun 2026 16:16:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(build):=20=E7=8B=AC=E7=AB=8B=E4=BA=8C?= =?UTF-8?q?=E8=BF=9B=E5=88=B6=E8=BE=93=E5=87=BA=E5=88=B0=E6=A0=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E5=B9=B6=E4=BF=AE=E5=A4=8D=20local=20=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=20SQLite=20=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SQLite/redka 数据目录相对可执行文件解析,自动创建 data/ 父目录 - ./run.sh build 产物改为 ./aiops(与 run.sh 同级),数据/日志在 ./data ./logs - 解决从项目根执行时 data/ 不存在导致的 SQLite CANTOPEN 报错 --- .gitignore | 2 + backend/main.go | 10 ++++- backend/pkg/appdir/appdir.go | 36 ++++++++++++++++++ backend/pkg/database/database.go | 6 ++- run.sh | 65 ++++++++++++++++++-------------- 5 files changed, 88 insertions(+), 31 deletions(-) create mode 100644 backend/pkg/appdir/appdir.go diff --git a/.gitignore b/.gitignore index 3ea5152..58324f8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,10 @@ backend/vendor/ *.out # Compiled binary +aiops backend/aiops-server backend/aiops +data/ # Config (keep backend/config.yaml as default) config.local.yaml diff --git a/backend/main.go b/backend/main.go index cf1cea5..e89077b 100644 --- a/backend/main.go +++ b/backend/main.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "os/signal" + "path/filepath" "strings" "syscall" "time" @@ -14,6 +15,7 @@ import ( "ezops/router" "ezops/config" "ezops/seed" + "ezops/pkg/appdir" "ezops/pkg/database" "ezops/pkg/logger" ezredis "ezops/pkg/redis" @@ -136,7 +138,7 @@ func main() { Port: cfg.Redis.Port, Password: cfg.Redis.Password, DB: cfg.Redis.DB, - DataDir: "data/redka", + DataDir: appdir.Resolve("data/redka"), }) if err != nil { if cfg.Mode == "local" { @@ -237,6 +239,7 @@ func loadConfig() *config.Config { viper.SetConfigType("yaml") viper.AddConfigPath("./configs") viper.AddConfigPath(".") + viper.AddConfigPath(appdir.Root()) // 如果指定了配置文件路径,使用它 // Logan.Li if *flagConfig != "" { @@ -295,6 +298,11 @@ func loadConfig() *config.Config { os.Exit(1) } + // SQLite / redka 相对路径统一解析到可执行文件目录 // Logan.Li + if cfg.Database.Driver == "sqlite" && cfg.Database.DBName != "" && !filepath.IsAbs(cfg.Database.DBName) { + cfg.Database.DBName = appdir.Resolve(cfg.Database.DBName) + } + return &cfg } diff --git a/backend/pkg/appdir/appdir.go b/backend/pkg/appdir/appdir.go new file mode 100644 index 0000000..55cfa4c --- /dev/null +++ b/backend/pkg/appdir/appdir.go @@ -0,0 +1,36 @@ +// appdir — 独立二进制运行目录(数据/配置相对可执行文件,不依赖 cwd)// Logan.Li +package appdir + +import ( + "os" + "path/filepath" +) + +// Root 可执行文件所在目录(解析软链)// Logan.Li +func Root() string { + exe, err := os.Executable() + if err != nil { + return "." + } + if resolved, err := filepath.EvalSymlinks(exe); err == nil { + exe = resolved + } + return filepath.Dir(exe) +} + +// Resolve 相对路径 → 基于可执行文件目录的绝对路径 // Logan.Li +func Resolve(path string) string { + if path == "" || filepath.IsAbs(path) { + return path + } + return filepath.Join(Root(), path) +} + +// EnsureParentDir 确保文件父目录存在 // Logan.Li +func EnsureParentDir(filePath string) error { + dir := filepath.Dir(filePath) + if dir == "" || dir == "." { + return nil + } + return os.MkdirAll(dir, 0o755) +} \ No newline at end of file diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index 3e3cf47..b8c7590 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -7,6 +7,7 @@ import ( "ezops/config" "ezops/model" + "ezops/pkg/appdir" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -29,7 +30,10 @@ func New(cfg config.DatabaseConfig) (*gorm.DB, error) { cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName) dialector = postgres.Open(dsn) case "sqlite": - dsn := cfg.DBName + dsn := appdir.Resolve(cfg.DBName) + if err := appdir.EnsureParentDir(dsn); err != nil { + return nil, fmt.Errorf("创建数据库目录失败: %w", err) + } if !strings.Contains(dsn, "?") { // WAL 提升并发读,busy_timeout 降低锁冲突 // Logan.Li dsn += "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)" diff --git a/run.sh b/run.sh index 01e1ab9..21bf2a5 100755 --- a/run.sh +++ b/run.sh @@ -61,8 +61,11 @@ cd "$SCRIPT_DIR" # PID 文件 BACKEND_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.backend.pid" FRONTEND_PID_FILE="$SCRIPT_DIR/$FRONTEND_DIR/.frontend.pid" -BINARY_PID_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.pid" -BINARY_ARGS_FILE="$SCRIPT_DIR/$BACKEND_DIR/.aiops.args" +STANDALONE_BIN_PATH="$SCRIPT_DIR/$STANDALONE_BIN" +BINARY_PID_FILE="$SCRIPT_DIR/.aiops.pid" +BINARY_ARGS_FILE="$SCRIPT_DIR/.aiops.args" +DATA_DIR="$SCRIPT_DIR/data" +LOGS_DIR="$SCRIPT_DIR/logs" # 检测操作系统 detect_os() { @@ -311,6 +314,7 @@ clean_embed_static() { clean_go_artifacts() { info "清理 Go 旧编译产物..." + rm -f "$STANDALONE_BIN_PATH" rm -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" rm -f "$SCRIPT_DIR/$BACKEND_DIR/$BACKEND_BIN" } @@ -352,6 +356,7 @@ stop_binary_process() { [ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true else fuser -k "${port}/tcp" 2>/dev/null || true + pkill -f "${STANDALONE_BIN_PATH}" 2>/dev/null || true pkill -f "${SCRIPT_DIR}/${BACKEND_DIR}/${STANDALONE_BIN}" 2>/dev/null || true pkill -f "./${STANDALONE_BIN} --port" 2>/dev/null || true fi @@ -364,8 +369,8 @@ wait_binary_ready() { while ! curl -sf "http://localhost:${port}/version" > /dev/null 2>&1; do retries=$((retries + 1)) if [ $retries -ge 30 ]; then - error "二进制启动超时,查看日志: $BACKEND_DIR/logs/aiops.log" - tail -20 "$SCRIPT_DIR/$BACKEND_DIR/logs/aiops.log" 2>/dev/null || true + error "二进制启动超时,查看日志: $LOGS_DIR/aiops.log" + tail -20 "$LOGS_DIR/aiops.log" 2>/dev/null || true return 1 fi sleep 1 @@ -375,7 +380,7 @@ wait_binary_ready() { # 确保二进制默认 config.yaml 存在 // Logan.Li ensure_binary_config() { - local cfg="$SCRIPT_DIR/$BACKEND_DIR/config.yaml" + local cfg="$SCRIPT_DIR/config.yaml" if [ -f "$cfg" ]; then return 0 fi @@ -434,14 +439,14 @@ build_standalone_binary() { cd "$SCRIPT_DIR/$BACKEND_DIR" info "编译中..." CGO_ENABLED=1 GOPROXY=https://goproxy.cn,direct GONOSUMCHECK='*' GOTOOLCHAIN=go1.24.0 \ - go build -ldflags="-w -s" -o "$STANDALONE_BIN" . - if [ ! -f "$STANDALONE_BIN" ]; then + go build -ldflags="-w -s" -o "$STANDALONE_BIN_PATH" . + if [ ! -f "$STANDALONE_BIN_PATH" ]; then error "编译失败" return 1 fi local bin_size - bin_size=$(du -h "$STANDALONE_BIN" | cut -f1) - success "编译完成: $BACKEND_DIR/$STANDALONE_BIN ($bin_size)" + bin_size=$(du -h "$STANDALONE_BIN_PATH" | cut -f1) + success "编译完成: ./$STANDALONE_BIN ($bin_size)" cd "$SCRIPT_DIR" return 0 } @@ -528,16 +533,17 @@ start_binary_daemon() { local args=("$@") local port port=$(binary_listen_port "${args[@]}") - cd "$SCRIPT_DIR/$BACKEND_DIR" - mkdir -p data logs uploads - rotate_log "logs/aiops.log" + cd "$SCRIPT_DIR" + mkdir -p "$DATA_DIR" "$LOGS_DIR" uploads + rotate_log "$LOGS_DIR/aiops.log" stop_binary_process "$port" - nohup ./"$STANDALONE_BIN" "${args[@]}" >> logs/aiops.log 2>&1 & + nohup ./"$STANDALONE_BIN" "${args[@]}" >> "$LOGS_DIR/aiops.log" 2>&1 & echo $! > "$BINARY_PID_FILE" save_binary_args "${args[@]}" if wait_binary_ready "$port"; then success "二进制已启动 (PID: $(cat "$BINARY_PID_FILE"), 端口: $port)" - info "日志: $BACKEND_DIR/logs/aiops.log" + info "日志: $LOGS_DIR/aiops.log" + info "数据: $DATA_DIR/" verify_binary_version "$port" || true return 0 fi @@ -691,9 +697,9 @@ cmd_build() { echo "" info "使用方法:" echo " ./run.sh binary upgrade # 停服重编译并后台启动" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 8080 # 前台启动" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --mode local # 本地模式 (SQLite + redka)" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml # 使用配置文件" + echo " ./$STANDALONE_BIN --port 8080 # 前台启动" + echo " ./$STANDALONE_BIN --mode local # 本地模式 (SQLite + redka)" + echo " ./$STANDALONE_BIN --config config.yaml # 使用配置文件" ;; agent) banner "========== 编译 Agent ==========" @@ -756,9 +762,9 @@ cmd_binary() { port=$(binary_listen_port "${args[@]}") info "启动参数: ${args[*]}" stop_binary_process "$port" - if [ -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then - local backup="$SCRIPT_DIR/$BACKEND_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)" - cp "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" "$backup" + if [ -f "$STANDALONE_BIN_PATH" ]; then + local backup="$SCRIPT_DIR/${STANDALONE_BIN}.bak.$(date +%Y%m%d%H%M%S)" + cp "$STANDALONE_BIN_PATH" "$backup" info "已备份旧二进制: $backup" fi build_standalone_binary || exit 1 @@ -771,7 +777,7 @@ cmd_binary() { else load_binary_args args fi - if [ ! -f "$SCRIPT_DIR/$BACKEND_DIR/$STANDALONE_BIN" ]; then + if [ ! -f "$STANDALONE_BIN_PATH" ]; then info "二进制不存在,先编译..." build_standalone_binary || exit 1 fi @@ -814,16 +820,16 @@ cmd_binary() { echo " ./run.sh binary [--mode local --port 8080 ...] 前台运行 (SQLite + redka)" ;; run|*) - cd "$SCRIPT_DIR/$BACKEND_DIR" - if [ ! -f "$STANDALONE_BIN" ]; then + if [ ! -f "$STANDALONE_BIN_PATH" ]; then info "二进制不存在,先编译..." build_standalone_binary || exit 1 fi banner "========== 前台启动独立二进制 ==========" - mkdir -p data logs uploads + mkdir -p "$DATA_DIR" "$LOGS_DIR" uploads if [ $# -gt 0 ]; then save_binary_args "$@" fi + cd "$SCRIPT_DIR" ./"$STANDALONE_BIN" "$@" ;; esac @@ -1200,7 +1206,7 @@ cmd_help() { echo " ./run.sh kubernetes build 仅构建 embed 镜像" echo "" echo -e " ${YELLOW}编译 & 独立二进制${NC}" - echo " ./run.sh build 编译独立二进制 ./$BACKEND_DIR/$STANDALONE_BIN" + echo " ./run.sh build 编译独立二进制 ./$STANDALONE_BIN" echo " ./run.sh binary upgrade 停服→清旧→重编译→后台启动(生产升级)" echo " ./run.sh binary start|stop|status 进程管理" echo " ./run.sh binary [args] 前台启动(调试)" @@ -1208,10 +1214,11 @@ cmd_help() { echo " ./run.sh build frontend 构建前端生产包" echo "" echo -e " ${BLUE}独立二进制用法${NC}" - echo " ./$BACKEND_DIR/$STANDALONE_BIN 默认启动 (SQLite, 端口 8080)" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --port 9090 自定义端口" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --config config.yaml 使用配置文件" - echo " ./$BACKEND_DIR/$STANDALONE_BIN --db-driver mysql \\" + echo " ./$STANDALONE_BIN 默认启动 (SQLite, 端口 8080)" + echo " ./$STANDALONE_BIN --port 9090 自定义端口" + echo " ./$STANDALONE_BIN --mode local 本地模式 (SQLite + redka)" + echo " ./$STANDALONE_BIN --config config.yaml 使用配置文件" + echo " ./$STANDALONE_BIN --db-driver mysql \\" echo " --db-host 127.0.0.1 --db-user root \\" echo " --db-password pass --db-name aiops MySQL 模式" echo ""