package main import ( "context" "errors" "log" "net/http" "os/signal" "syscall" "time" "booklib/cmd/webui/api" "booklib/internal/config" "booklib/internal/db" "booklib/internal/redispkg" "booklib/internal/scanner" "booklib/internal/seed" "booklib/internal/store" ) func main() { cfg, err := config.Load() if err != nil { log.Fatalf("config: %v", err) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() p, err := db.Connect(ctx, cfg.DatabaseURL) if err != nil { log.Fatalf("db connect: %v", err) } defer p.Close() if err := db.Migrate(ctx, p); err != nil { log.Fatalf("migrate: %v", err) } st := store.New(p) if err := seed.Admin(ctx, st, cfg.AdminUser, cfg.AdminPassword); err != nil { log.Fatalf("seed: %v", err) } rdb := redispkg.New(cfg.RedisURL) sc := scanner.New(st, cfg, rdb) go sc.Run(ctx) serveErr := make(chan error, 1) srv := &http.Server{Addr: cfg.Addr, Handler: api.NewRouter(cfg, st, rdb, sc), ReadHeaderTimeout: 10 * time.Second} go func() { log.Printf("listening on %s", cfg.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { serveErr <- err } close(serveErr) }() // Wait for signal OR serve error. select { case <-ctx.Done(): case err := <-serveErr: if err != nil { log.Printf("serve: %v", err) } } // stop() 先取消 ctx → scanner 循环退出;再等 HTTP 收尾。 // 在途 ScanLibraryByID(WithoutCancel)不受 ctx 控制,靠 redis 锁 TTL 兜底(已文档化的上限)。 stop() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("shutdown: %v", err) } }