mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 17:17:37 +00:00
Caddy 式进程内模块系统(详见 docs/plugin-architecture/MODULE-AUTHOR-GUIDE.md): - internal/plugin:命名空间注册表(init 注册/重复 panic)、Provision→Validate→Start→Stop 生命周期(Start 半途逆序回滚、Stop 逆序+ctx deadline)、Host 四项 ports 能力面、 modules: 配置子树(enabled 三态;viper 含点 key 手工提取)、Runtime 状态机 - internal/plugin/plugintest:模块测试夹具(NewHost Options + RunLifecycle) - internal/modules:standard/imports.go 唯一插装清单 + job.hello 示例模块(默认 disabled) - Wire/main 接入:Build+Start 失败 fail-fast(显式 Cleanup 防 leader 锁泄漏)、 runtime.Stop 在 cleanup 并行组首位(先于 Redis/Ent) - admin 可观测:GET /api/v1/admin/modules(只读,错误文本脱敏) - 默认配置零行为变更(Phase-0 安全网 + bench allocs 零漂移验证) 含两轮对抗式审计修复(1×P1 启动失败泄漏 leader 锁 + 11×P2)与回归测试。
198 lines
5.5 KiB
Go
198 lines
5.5 KiB
Go
package main
|
||
|
||
//go:generate go run github.com/google/wire/cmd/wire
|
||
|
||
import (
|
||
"context"
|
||
_ "embed"
|
||
"errors"
|
||
"flag"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
_ "github.com/Wei-Shaw/sub2api/ent/runtime"
|
||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||
_ "github.com/Wei-Shaw/sub2api/internal/modules/standard" // 插件模块插装清单(唯一插装点)
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||
"github.com/Wei-Shaw/sub2api/internal/setup"
|
||
"github.com/Wei-Shaw/sub2api/internal/web"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
//go:embed VERSION
|
||
var embeddedVersion string
|
||
|
||
// Build-time variables (can be set by ldflags)
|
||
var (
|
||
Version = ""
|
||
Commit = "unknown"
|
||
Date = "unknown"
|
||
BuildType = "source" // "source" for manual builds, "release" for CI builds (set by ldflags)
|
||
)
|
||
|
||
func init() {
|
||
// 如果 Version 已通过 ldflags 注入(例如 -X main.Version=...),则不要覆盖。
|
||
if strings.TrimSpace(Version) != "" {
|
||
return
|
||
}
|
||
|
||
// 默认从 embedded VERSION 文件读取版本号(编译期打包进二进制)。
|
||
Version = strings.TrimSpace(embeddedVersion)
|
||
if Version == "" {
|
||
Version = "0.0.0-dev"
|
||
}
|
||
}
|
||
|
||
// initLogger configures the default slog handler based on gin.Mode().
|
||
// In non-release mode, Debug level logs are enabled.
|
||
func main() {
|
||
logger.InitBootstrap()
|
||
defer logger.Sync()
|
||
|
||
// Parse command line flags
|
||
setupMode := flag.Bool("setup", false, "Run setup wizard in CLI mode")
|
||
showVersion := flag.Bool("version", false, "Show version information")
|
||
flag.Parse()
|
||
|
||
if *showVersion {
|
||
log.Printf("Sub2API %s (commit: %s, built: %s)\n", Version, Commit, Date)
|
||
return
|
||
}
|
||
|
||
// CLI setup mode
|
||
if *setupMode {
|
||
if err := setup.RunCLI(); err != nil {
|
||
log.Fatalf("Setup failed: %v", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Check if setup is needed
|
||
if setup.NeedsSetup() {
|
||
// Check if auto-setup is enabled (for Docker deployment)
|
||
if setup.AutoSetupEnabled() {
|
||
log.Println("Auto setup mode enabled...")
|
||
if err := setup.AutoSetupFromEnv(); err != nil {
|
||
log.Fatalf("Auto setup failed: %v", err)
|
||
}
|
||
// Continue to main server after auto-setup
|
||
} else {
|
||
log.Println("First run detected, starting setup wizard...")
|
||
runSetupServer()
|
||
return
|
||
}
|
||
}
|
||
|
||
// Normal server mode
|
||
runMainServer()
|
||
}
|
||
|
||
func runSetupServer() {
|
||
r := gin.New()
|
||
r.Use(middleware.Recovery())
|
||
r.Use(middleware.CORS(config.CORSConfig{}))
|
||
r.Use(middleware.SecurityHeaders(config.CSPConfig{Enabled: true, Policy: config.DefaultCSPPolicy}, nil))
|
||
|
||
// Register setup routes
|
||
setup.RegisterRoutes(r)
|
||
|
||
// Serve embedded frontend if available
|
||
if web.HasEmbeddedFrontend() {
|
||
r.Use(web.ServeEmbeddedFrontend())
|
||
}
|
||
|
||
// Get server address from config.yaml or environment variables (SERVER_HOST, SERVER_PORT)
|
||
// This allows users to run setup on a different address if needed
|
||
addr := config.GetServerAddress()
|
||
log.Printf("Setup wizard available at http://%s", addr)
|
||
log.Println("Complete the setup wizard to configure Sub2API")
|
||
|
||
protocols := new(http.Protocols)
|
||
protocols.SetHTTP1(true)
|
||
protocols.SetUnencryptedHTTP2(true)
|
||
|
||
server := &http.Server{
|
||
Addr: addr,
|
||
Handler: r,
|
||
ReadHeaderTimeout: 30 * time.Second,
|
||
IdleTimeout: 120 * time.Second,
|
||
Protocols: protocols,
|
||
}
|
||
|
||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||
log.Fatalf("Failed to start setup server: %v", err)
|
||
}
|
||
}
|
||
|
||
func runMainServer() {
|
||
cfg, err := config.LoadForBootstrap()
|
||
if err != nil {
|
||
log.Fatalf("Failed to load config: %v", err)
|
||
}
|
||
if err := logger.Init(logger.OptionsFromConfig(cfg.Log)); err != nil {
|
||
log.Fatalf("Failed to initialize logger: %v", err)
|
||
}
|
||
if cfg.RunMode == config.RunModeSimple {
|
||
log.Println("⚠️ WARNING: Running in SIMPLE mode - billing and quota checks are DISABLED")
|
||
}
|
||
|
||
buildInfo := handler.BuildInfo{
|
||
Version: Version,
|
||
BuildType: BuildType,
|
||
}
|
||
|
||
app, err := initializeApplication(buildInfo)
|
||
if err != nil {
|
||
log.Fatalf("Failed to initialize application: %v", err)
|
||
}
|
||
defer app.Cleanup()
|
||
|
||
// 启动插件模块:Build(实例化 → Provision → Validate)+ Start(按稳定序)。
|
||
// 任一失败立即中止启动(fail-fast)。注意:log.Fatalf 经 os.Exit 退出,
|
||
// defer 的 app.Cleanup() 不会执行;而部分后台服务在 Wire 构造期已自启动并
|
||
// 持有 Redis leader 锁,必须先显式 Cleanup 再退出,避免锁滞留到 TTL 过期。
|
||
// ⚠ 不要把下面的 log.Fatalf 改成 return:那会让上面 defer 的 Cleanup 与
|
||
// 此处显式 Cleanup 双触发,而 Cleanup 各步骤并未统一保证幂等(审计 B-2)。
|
||
if err := app.Runtime.Build(); err != nil {
|
||
app.Cleanup()
|
||
log.Fatalf("Failed to build plugin modules: %v", err)
|
||
}
|
||
if err := app.Runtime.Start(context.Background()); err != nil {
|
||
app.Cleanup()
|
||
log.Fatalf("Failed to start plugin modules: %v", err)
|
||
}
|
||
|
||
// 启动服务器
|
||
go func() {
|
||
if err := app.Server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||
log.Fatalf("Failed to start server: %v", err)
|
||
}
|
||
}()
|
||
|
||
log.Printf("Server started on %s", app.Server.Addr)
|
||
|
||
// 等待中断信号
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
|
||
log.Println("Shutting down server...")
|
||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
if err := app.Server.Shutdown(ctx); err != nil {
|
||
log.Fatalf("Server forced to shutdown: %v", err)
|
||
}
|
||
|
||
log.Println("Server exited")
|
||
}
|