Files
gb28181/internal/app/app.go
T
2026-01-21 13:11:21 +08:00

205 lines
5.7 KiB
Go

package app
import (
"context"
"fmt"
"log/slog"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"github.com/gowvp/owl/internal/conf"
"github.com/ixugo/goddd/domain/version/versionapi"
"github.com/ixugo/goddd/pkg/logger"
"github.com/ixugo/goddd/pkg/server"
"github.com/ixugo/goddd/pkg/system"
)
func Run(bc *conf.Bootstrap) {
if bc.Server.Recording.DiskUsageThreshold <= 0 {
bc.Server.Recording.DiskUsageThreshold = 95.0
}
if bc.Server.Recording.SegmentSeconds <= 0 {
bc.Server.Recording.SegmentSeconds = 300
}
if bc.Server.Recording.RetainDays <= 0 {
bc.Server.Recording.RetainDays = 3
}
if bc.Server.Recording.StorageDir == "" {
bc.Server.Recording.StorageDir = "./configs/recordings"
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 以可执行文件所在目录为工作目录,防止以服务方式运行时,工作目录切换到其它位置
bin, _ := os.Executable()
if err := os.Chdir(filepath.Dir(bin)); err != nil {
slog.Error("change work dir fail", "err", err)
}
log, clean := SetupLog(bc)
defer clean()
go setupZLM(ctx, bc.ConfigDir)
if !bc.Server.AI.Disabled {
go setupAIClient(ctx, "http://127.0.0.1:15123/ai", bc.Debug)
}
// 如果需要执行表迁移,递增此版本号和表更新说明
versionapi.DBVersion = "0.0.23"
versionapi.DBRemark = "onvif device support"
handler, cleanUp, err := wireApp(bc, log)
if err != nil {
slog.Error("程序构建失败", "err", err)
panic(err)
}
defer cleanUp()
svc := server.New(handler,
server.Port(strconv.Itoa(bc.Server.HTTP.Port)),
server.ReadTimeout(bc.Server.HTTP.Timeout.Duration()),
server.WriteTimeout(bc.Server.HTTP.Timeout.Duration()),
)
go svc.Start()
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("服务启动成功 port:", bc.Server.HTTP.Port)
select {
case s := <-interrupt:
slog.Info(`<-interrupt`, "signal", s.String())
case err := <-svc.Notify():
system.ErrPrintf("err: %s\n", err.Error())
slog.Error(`<-server.Notify()`, "err", err)
}
cancel()
if err := svc.Shutdown(); err != nil {
slog.Error(`server.Shutdown()`, "err", err)
}
}
// SetupLog 初始化日志
func SetupLog(bc *conf.Bootstrap) (*slog.Logger, func()) {
logDir := filepath.Join(bc.ConfigDir, bc.Log.Dir)
_ = os.MkdirAll(logDir, 0o755)
return logger.SetupSlog(logger.Config{
Dir: logDir, // 日志地址
Debug: bc.Debug, // 服务级别Debug/Release
MaxAge: bc.Log.MaxAge.Duration(), // 日志存储时间
RotationTime: bc.Log.RotationTime.Duration(), // 循环时间
RotationSize: bc.Log.RotationSize * 1024 * 1024, // 循环大小
Level: bc.Log.Level, // 日志级别
})
}
func setupZLM(ctx context.Context, dir string) {
// 检查是否在 Docker 环境中
_, err := os.Stat("/.dockerenv")
if !(err == nil || os.Getenv("NVR_STREAM") == "ZLM") {
slog.Info("未在 Docker 环境中运行,跳过启动 zlm")
return
}
// 检查 MediaServer 文件是否存在
mediaServerPath := filepath.Join(system.Getwd(), "MediaServer")
if _, err := os.Stat(mediaServerPath); os.IsNotExist(err) {
slog.Info("MediaServer 文件不存在", "path", mediaServerPath)
return
}
workDir := system.Getwd()
configPath := filepath.Join(dir, "zlm.ini")
for {
select {
case <-ctx.Done():
slog.Info("收到退出信号,停止重启 zlm")
return
default:
slog.Info("MediaServer 启动中...")
cmd := exec.CommandContext(ctx, "./MediaServer", "-s", "default.pem", "-c", configPath)
cmd.Dir = workDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
// 启动命令 - 正常情况下会阻塞在这里
if err := cmd.Run(); err != nil {
slog.Error("zlm 运行失败", "err", err)
} else {
slog.Info("MediaServer 退出,将重新启动")
}
// 等待后重启(不管是正常退出还是异常退出)
time.Sleep(2 * time.Second)
}
}
}
func findPythonPath() string {
candidates := []string{
"/opt/homebrew/Caskroom/miniconda/base/bin/python", // macOS Homebrew Miniconda
"/opt/homebrew/anaconda3/bin/python", // macOS Homebrew Anaconda
"/usr/local/anaconda3/bin/python", // Linux Anaconda
"/usr/local/miniconda3/bin/python", // Linux Miniconda
"/root/miniconda3/bin/python", // Linux root Miniconda
}
for _, p := range candidates {
if _, err := os.Stat(p); err == nil {
return p
}
}
return "python3"
}
func setupAIClient(ctx context.Context, callback string, debug bool) {
workDir := filepath.Join(system.Getwd(), "analysis")
if _, err := os.Stat(filepath.Join(workDir, "main.py")); err != nil && os.IsNotExist(err) {
slog.Info("main.py 文件不存在,跳过启动 ai", "path", filepath.Join(workDir, "main.py"))
return
}
pythonPath := findPythonPath()
slog.Info("使用 Python 路径", "path", pythonPath)
args := []string{"main.py"}
if callback != "" {
args = append(args, "--callback-url", callback)
}
if debug {
args = append(args, "--log-level", "DEBUG")
}
for range 100 {
select {
case <-ctx.Done():
slog.Info("收到退出信号,停止重启 ai")
return
default:
slog.Info("ai 启动中...")
cmd := exec.CommandContext(ctx, pythonPath, args...)
cmd.Dir = workDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
// 启动命令 - 正常情况下会阻塞在这里
if err := cmd.Run(); err != nil {
slog.Error("ai 运行失败", "err", err)
} else {
slog.Info("ai 退出,将重新启动")
}
// 等待后重启(不管是正常退出还是异常退出)
time.Sleep(2 * time.Second)
}
}
}