package main import ( "context" "io/fs" "log" "mime" "net/http" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "arma3-web-server/embed" "arma3-web-server/internal/api" "arma3-web-server/internal/services" "github.com/gin-gonic/gin" ) func main() { devDeploy := getEnv("DEV_DEPLOY_DIR", filepath.Join("..", "dev-deploy")) dataDir := mustAbs(getEnv("DATA_DIR", filepath.Join(devDeploy, "data"))) serverfileDir := mustAbs(getEnv("SERVERFILE_DIR", filepath.Join(devDeploy, "serverfiles"))) modsDir := mustAbs(getEnv("MODS_DIR", filepath.Join(devDeploy, "serverfiles", "mods"))) cfgDir := mustAbs(getEnv("CFG_DIR", filepath.Join(devDeploy, "serverfiles", "cfg"))) profilesDir := mustAbs(getEnv("PROFILES_DIR", filepath.Join(devDeploy, "serverfiles", "profiles"))) listen := getEnv("LISTEN", ":8080") log.Printf("data dir: %s", dataDir) log.Printf("serverfile dir: %s", serverfileDir) log.Printf("mods dir: %s", modsDir) log.Printf("cfg dir: %s", cfgDir) log.Printf("profiles dir: %s", profilesDir) log.Printf("listening on %s", listen) dirs := []string{ dataDir, filepath.Join(dataDir, "modlists"), serverfileDir, modsDir, cfgDir, profilesDir, filepath.Join(serverfileDir, "steamapps", "workshop", "content", "107410"), } for _, dir := range dirs { if err := os.MkdirAll(dir, 0755); err != nil { log.Fatalf("mkdir %s: %v", dir, err) } } settings := services.NewSettingsManager(dataDir) streamer := services.NewLogStreamer() configMgr := services.NewConfigManager(cfgDir) modlistMgr := services.NewModlistManager(dataDir) process := services.NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, settings, modlistMgr, configMgr, streamer) steamcmd := services.NewSteamCmdManager(serverfileDir, streamer) scheduler := services.NewScheduler(settings, modlistMgr, steamcmd) r := gin.Default() subFS, subErr := fs.Sub(embed.Frontend, "dist") if subErr == nil { if _, err := subFS.Open("index.html"); err == nil { r.NoRoute(func(c *gin.Context) { path := strings.TrimPrefix(c.Request.URL.Path, "/") if path == "" { path = "index.html" } data, err := fs.ReadFile(subFS, path) if err == nil { ct := mime.TypeByExtension(filepath.Ext(path)) if ct == "" { ct = "text/html; charset=utf-8" } c.Data(http.StatusOK, ct, data) return } data, _ = fs.ReadFile(subFS, "index.html") c.Data(http.StatusOK, "text/html; charset=utf-8", data) }) log.Print("serving embedded frontend") } } handler := api.New(settings, configMgr, modlistMgr, process, steamcmd, scheduler, streamer, dataDir, serverfileDir, modsDir, cfgDir, profilesDir) handler.SetupRoutes(r) // Startup auto-tasks s, err := settings.Load() if err != nil { log.Printf("startup: load settings: %v", err) } else { if s.AutoUpdateOnStartup && s.SteamUser != "" { go func() { log.Print("startup: auto-updating gameserver") if err := steamcmd.UpdateGame(s.SteamBranch, s.SteamUser); err != nil { log.Printf("startup: auto-update game failed: %v", err) } }() } if s.AutoUpdateModsOnStartup && s.ActiveModlist != "" { go func() { log.Print("startup: auto-updating mods") ml, err := modlistMgr.Get(s.ActiveModlist) if err != nil { log.Printf("startup: load modlist for auto-update: %v", err) return } var modIDs []string for _, m := range ml.Mods { if m.Enabled { modIDs = append(modIDs, m.ID) } } if len(modIDs) > 0 { if err := steamcmd.DownloadMods(modIDs); err != nil { log.Printf("startup: auto-update mods failed: %v", err) } } }() } if s.AutoStartOnStartup && s.WasRunning { go func() { log.Print("startup: auto-restarting server (was running before)") if err := process.Start(); err != nil { log.Printf("startup: auto-start failed: %v", err) } }() } } // Start the scheduled updater scheduler.Start() log.Printf("starting server on %s", listen) srv := &http.Server{Addr: listen, Handler: r} go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Print("shutting down server...") scheduler.Stop() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("server forced shutdown: %v", err) } log.Print("server exited") } func mustAbs(p string) string { a, err := filepath.Abs(p) if err != nil { log.Fatalf("abs %s: %v", p, err) } return a } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }