Files
monitor/backend/main.go
2025-12-07 10:30:10 +08:00

328 lines
8.7 KiB
Go
Raw 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.
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)
}
}