package main // main.go — lvmh daemon entrypoint: env wiring, flags, HTTP server. import ( "context" "flag" "fmt" "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" ) // Server and env wiring constants. const ( envDB string = "LVMH_DB" defaultDBPath string = "/data/lvmh.db" envGitLabBaseURL string = "GITLAB_BASE_URL" defaultGitLabBase string = "https://git.westphal.fr" defaultListenAddr string = ":8686" shutdownGrace time.Duration = 10 * time.Second webDistDefault string = "/app/web-dist" ) func main() { if err := run(os.Args[1:]); err != nil { log.Fatal(err) } } // run is the testable main: parses args, wires the server, blocks on SIGINT/ // SIGTERM, then shuts down. Split out of main for coverage of the wiring. func run(args []string) error { fs := flag.NewFlagSet("lvmh-daemon", flag.ContinueOnError) var ( addr = fs.String("addr", defaultListenAddr, "listen address") dbPath = fs.String("db", envOr(envDB, defaultDBPath), "sqlite database path") webdist = fs.String("webdist", webDistDefault, "serve web UI from this directory instead of the embedded build") ) if err := fs.Parse(args); err != nil { return err } // Fall back to the embedded UI when the override directory is absent. if st, err := os.Stat(*webdist); err != nil || !st.IsDir() { *webdist = "" } daemonToken = os.Getenv(envToken) if daemonToken == "" { return fmt.Errorf("LVMH_TOKEN must be set") } if dir := filepath.Dir(*dbPath); dir != "" && dir != "." { if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create db dir: %w", err) } } store, err := OpenStore(*dbPath) if err != nil { return fmt.Errorf("open store: %w", err) } defer store.Close() hub := NewHub(store) gitlab := NewGitLab(store, envOr(envGitLabBaseURL, defaultGitLabBase)) spawnerCtx, spawnerCtxCancel := context.WithCancel(context.Background()) defer spawnerCtxCancel() spawner, err := NewSpawner(spawnerCtx, store, hub, envOr(envGitLabBaseURL, defaultGitLabBase)) if err != nil { return fmt.Errorf("docker client: %w", err) } srv := NewServer(store, hub, spawner, gitlab) httpServer := &http.Server{ Addr: *addr, Handler: srv.Routes(*webdist), ReadHeaderTimeout: 10 * time.Second, } serveErr := make(chan error, 1) go func() { log.Printf("lvmh daemon listening on %s (db %s)", *addr, *dbPath) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { serveErr <- err return } serveErr <- nil }() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) defer signal.Stop(stop) select { case err := <-serveErr: spawnerCtxCancel() return fmt.Errorf("listen: %w", err) case <-stop: } log.Printf("shutting down") spawnerCtxCancel() ctx, cancel := context.WithTimeout(context.Background(), shutdownGrace) defer cancel() if err := httpServer.Shutdown(ctx); err != nil { log.Printf("shutdown: %v", err) } return nil }