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