package main import ( "flag" "fmt" "io" "log" "os" "os/exec" "os/signal" "path/filepath" "strconv" "syscall" "time" "github.com/gin-gonic/gin" "github.com/monitor/backend/config" "github.com/monitor/backend/internal/db" "github.com/monitor/backend/internal/device" "github.com/monitor/backend/internal/handler" "github.com/monitor/backend/internal/storage" ) // 检查配置文件是否存在 func configFileExists() bool { configFile := os.Getenv("SERVER_CONFIG_FILE") if configFile == "" { configFile = "./config.json" } _, err := os.Stat(configFile) return err == nil } // 检查数据库配置是否为默认值 func isDefaultDBConfig(cfg *config.Config) bool { return cfg.DB.Host == "localhost" && cfg.DB.Port == 3306 && cfg.DB.Username == "root" && cfg.DB.Password == "" && cfg.DB.Database == "monitor" } // daemonize 实现守护进程功能 func daemonize() error { // 检查是否已经是守护进程模式 if os.Getenv("DAEMONIZED") == "1" { // 设置工作目录 if err := os.Chdir("/"); err != nil { return fmt.Errorf("failed to chdir to /: %v", err) } // 重设文件权限掩码 syscall.Umask(0) return nil } // 获取可执行文件的绝对路径 execPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %v", err) } // 创建环境变量,标记为守护进程模式 env := append(os.Environ(), "DAEMONIZED=1") // 启动新进程 cmd := exec.Command(execPath, os.Args[1:]...) cmd.Env = env cmd.Stdin = nil cmd.Stdout = nil cmd.Stderr = nil cmd.Dir = "/" cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, } // 启动进程 if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start daemon: %v", err) } // 父进程退出 os.Exit(0) return nil } // savePID 保存进程ID到文件 func savePID() error { if pidFile == "" { return nil } // 获取当前进程ID pid := strconv.Itoa(os.Getpid()) // 写入PID文件 if err := os.WriteFile(pidFile, []byte(pid), 0644); err != nil { return fmt.Errorf("failed to write PID file: %v", err) } return nil } // removePID 删除PID文件 func removePID() { if pidFile != "" { os.Remove(pidFile) } } // 命令行参数 var ( // 是否以守护进程模式运行 daemonMode bool // 日志文件路径 logFilePath string // 进程ID文件路径 pidFile string ) // main 函数启动服务器 func main() { // 解析命令行参数 flag.BoolVar(&daemonMode, "daemon", false, "Run as daemon (background process)") flag.BoolVar(&daemonMode, "d", false, "Run as daemon (background process) - shorthand") flag.StringVar(&logFilePath, "log-file", "", "Path to log file") flag.StringVar(&logFilePath, "l", "", "Path to log file - shorthand") flag.StringVar(&pidFile, "pid-file", "/tmp/monitor-backend.pid", "Path to PID file") flag.StringVar(&pidFile, "p", "/tmp/monitor-backend.pid", "Path to PID file - shorthand") flag.Parse() // 配置日志:同时输出到文件和标准输出 logFileName := fmt.Sprintf("monitor-backend-%s.log", time.Now().Format("2006-01-02")) // 处理日志文件路径 if logFilePath != "" { // 如果是相对路径,则使用可执行文件所在的目录作为基准目录 if !filepath.IsAbs(logFilePath) { // 获取可执行文件的目录 execPath, err := os.Executable() if err != nil { log.Printf("Warning: Failed to get executable path, using current directory for log file") logFileName = logFilePath } else { execDir := filepath.Dir(execPath) logFileName = filepath.Join(execDir, logFilePath) } } else { logFileName = logFilePath } } // 打开日志文件 logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { log.Printf("Warning: Failed to open log file %s, logging only to stdout: %v", logFileName, err) } else { defer logFile.Close() // 创建一个多输出写入器,同时写入文件和标准输出 log.SetOutput(io.MultiWriter(os.Stdout, logFile)) } log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) // 加载配置 cfg, err := config.LoadConfig() if err != nil { log.Fatalf("Failed to load config: %v", err) } // 检查配置文件是否存在 if !configFileExists() { // 生成配置文件 if err := config.SaveConfig(cfg); err != nil { log.Fatalf("Failed to generate config file: %v", err) } // 退出并提示用户配置 fmt.Println("配置文件已生成, 请进行配置然后启动服务.") os.Exit(0) } // 检查数据库配置是否为默认值 if isDefaultDBConfig(cfg) { // 退出并提示用户配置 fmt.Println("检测到数据库为默认配置, 请进行配置然后启动服务.") os.Exit(0) } // 如果指定了守护进程模式,则启动守护进程 if daemonMode { if err := daemonize(); err != nil { log.Fatalf("Failed to daemonize: %v", err) } } // 保存PID文件 if err := savePID(); err != nil { log.Printf("Warning: Failed to save PID file: %v", err) } // 注册信号处理,确保进程退出时删除PID文件 go func() { sigCh := make(chan os.Signal, 1) // 只处理可以捕获的信号 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) <-sigCh removePID() os.Exit(0) }() // 创建存储实例 store := storage.NewStorage(cfg) defer store.Close() // 初始化数据库连接(内部使用,不对外暴露) dbConfig := db.Config{ Type: cfg.DB.Type, Host: cfg.DB.Host, Port: cfg.DB.Port, Username: cfg.DB.Username, Password: cfg.DB.Password, Database: cfg.DB.Database, SSLMode: cfg.DB.SSLMode, Charset: cfg.DB.Charset, } // 尝试连接数据库,如果连接失败,只记录警告,不影响服务器启动 database, err := db.NewDB(dbConfig) if err != nil { log.Printf("Warning: Failed to connect to database: %v", err) log.Printf("Server will continue without database connection") } else { defer database.Close() log.Printf("Database connection initialized successfully") // 这里可以将database传递给需要使用的组件 // 例如:handler.SetDB(database) } // 创建Gin引擎,禁用调试模式 gin.SetMode(gin.ReleaseMode) r := gin.New() // 添加必要的中间件 r.Use(gin.Recovery()) // 设置Gin的日志输出到文件和标准输出 ginLogger := log.New(io.MultiWriter(os.Stdout, logFile), "[GIN] ", log.Ldate|log.Ltime) r.Use(gin.LoggerWithWriter(ginLogger.Writer())) // 设置CORS r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) // 设置全局存储实例 handler.SetStorage(store) // 初始化设备存储实例 var deviceStore device.Storage if database != nil { // 使用MySQL存储 mysqlStore, err := device.NewMySQLStorage(database.GetDB()) if err != nil { log.Printf("Warning: Failed to create MySQL device storage: %v", err) // 回退到内存存储 deviceStore = device.NewMemoryStorage() } else { log.Printf("MySQL device storage initialized successfully") deviceStore = mysqlStore } } else { // 使用内存存储 deviceStore = device.NewMemoryStorage() log.Printf("Using in-memory device storage") } handler.SetDeviceStorage(deviceStore) // 配置静态文件服务 // 从backend/static目录提供静态文件 // 先注册API路由,再处理静态文件,避免路由冲突 // 注册API路由 handler.RegisterRoutes(r) // 处理所有未匹配的路由,返回静态文件或index.html // 静态文件服务的路径处理:如果是相对路径,使用可执行文件所在目录作为基准目录 staticDir := "./static" if !filepath.IsAbs(staticDir) { // 获取可执行文件的目录 execPath, err := os.Executable() if err != nil { log.Printf("Warning: Failed to get executable path, using current directory for static files") } else { execDir := filepath.Dir(execPath) staticDir = filepath.Join(execDir, staticDir) } } r.NoRoute(func(c *gin.Context) { // 尝试提供静态文件 file := c.Request.URL.Path if file == "/" { file = "/index.html" } // 从static目录提供文件 c.File(staticDir + file) }) // 启动服务器 addr := fmt.Sprintf(":%d", cfg.Server.Port) log.Printf("Server starting on %s", addr) log.Printf("Static files served from ./static") if err := r.Run(addr); err != nil { log.Fatalf("Failed to start server: %v", err) } }