aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/mirumd/main.go')
-rw-r--r--cmd/mirumd/main.go255+255 −0
1 files changed, 255 insertions, 0 deletions
diff --git a/cmd/mirumd/main.go b/cmd/mirumd/main.go
new file mode 100644
--- /dev/null
+++ b/cmd/mirumd/main.go
@@ -0,0 +1,255 @@
+// Copyright (c) 2026 Nikolay Govorov
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net"
+ "net/http"
+ "os"
+
+ "dimidiumlabs/mirum/internal/config"
+ "dimidiumlabs/mirum/internal/database"
+ "dimidiumlabs/mirum/internal/forges"
+ "dimidiumlabs/mirum/internal/protocol/pb"
+ "dimidiumlabs/mirum/internal/protocol/pb/pbconnect"
+ "dimidiumlabs/mirum/internal/supervisor"
+
+ "github.com/coreos/go-systemd/v22/activation"
+ "github.com/spf13/cobra"
+)
+
+func hardenServer(s *http.Server) *http.Server {
+ s.IdleTimeout = config.HTTPIdleTimeout
+ s.MaxHeaderBytes = config.HTTPMaxHeaderBytes
+ s.ReadHeaderTimeout = config.HTTPReadHeaderTimeout
+ return s
+}
+
+func main() {
+ var socketPath string
+
+ root := &cobra.Command{Use: "mirumd", Short: "Mirum CI server"}
+ root.PersistentFlags().StringVar(&socketPath, "socket", "", "admin socket path (default from config or /run/mirumd/admin.sock)")
+ root.AddGroup(&cobra.Group{ID: "main", Title: "Commands:"})
+
+ daemonCmd := &cobra.Command{
+ Use: "daemon",
+ Short: "Start the server",
+ GroupID: "main",
+ SilenceUsage: true,
+ SilenceErrors: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ configFile, _ := cmd.Flags().GetString("config")
+ return daemon(configFile, socketPath)
+ },
+ }
+ daemonCmd.Flags().String("config", "", "path to config file")
+ _ = daemonCmd.MarkFlagRequired("config")
+ root.AddCommand(daemonCmd)
+
+ // Admin subcommands are generated from admin.proto via reflection.
+ buildAdminCLI(root, func() pbconnect.AdminClient { return adminClient(socketPath) })
+ for _, c := range root.Commands() {
+ if c.GroupID == "" {
+ c.GroupID = "main"
+ }
+ }
+
+ if err := root.Execute(); err != nil {
+ os.Exit(1)
+ }
+}
+
+func daemon(configFile, socketFlag string) error {
+ cfg, err := getConfig(configFile)
+ if err != nil {
+ slog.Error("config parsing failed", "err", err)
+ return err
+ }
+
+ if socketFlag != "" {
+ cfg.AdminSocket = socketFlag
+ }
+
+ slog.Info("config loaded", "configfile", configFile)
+
+ sup := supervisor.Detect()
+ ctx, cancel := context.WithCancel(sup.WaitForStop(context.Background()))
+ defer cancel()
+
+ db, err := database.Open(ctx, cfg.DatabaseUri)
+ if err != nil {
+ slog.Error("couldn't open database", "err", err)
+ return err
+ }
+
+ srv := &server{
+ db: db,
+ cfg: cfg,
+ forge: &forges.GitHub{Secret: cfg.WebhookSecret, Token: cfg.GitHubToken},
+ queue: make(chan *pb.Task, config.TaskQueueCapacity),
+ }
+ defer srv.Close()
+
+ if err := db.Migrate(ctx); err != nil {
+ slog.Error("migration failed", "err", err)
+ return err
+ }
+
+ slog.Info("database ready")
+
+ go srv.PurgeSessions(ctx)
+
+ adminPath, adminHandler := NewAdminHandler(srv)
+
+ webSrv := hardenServer(NewWebServer(ctx, srv, adminPath, adminHandler))
+ grpcSrv := hardenServer(NewGrpcServer(ctx, srv))
+
+ adminMux := http.NewServeMux()
+ adminMux.Handle(adminPath, adminHandler)
+ adminSrv := hardenServer(&http.Server{
+ Handler: adminMux,
+ ConnContext: func(ctx context.Context, _ net.Conn) context.Context {
+ return context.WithValue(ctx, actorKey{}, database.OperatorActor())
+ },
+ BaseContext: func(_ net.Listener) context.Context {
+ return ctx
+ },
+ })
+
+ grpcLn, webLn, adminLn, err := listeners(cfg)
+ if err != nil {
+ slog.Error("listeners failed", "err", err)
+ return err
+ }
+
+ slog.Info("listening", "grpc", grpcLn.Addr(), "web", webLn.Addr(), "admin", cfg.AdminSocket)
+
+ errs := make(chan error, 3)
+ serve := func(name string, fn func() error) {
+ go func() {
+ err := fn()
+ if errors.Is(err, http.ErrServerClosed) {
+ err = nil
+ }
+ if err != nil {
+ err = fmt.Errorf("%s server: %w", name, err)
+ }
+ errs <- err
+ }()
+ }
+ serve("web", func() error {
+ if webSrv.TLSConfig != nil {
+ return webSrv.ServeTLS(webLn, "", "")
+ }
+ return webSrv.Serve(webLn)
+ })
+ serve("grpc", func() error { return grpcSrv.ServeTLS(grpcLn, "", "") })
+ serve("admin", func() error { return adminSrv.Serve(adminLn) })
+
+ sup.Ready()
+ go sup.StartWatchdog(ctx)
+
+ var runErr error
+ select {
+ case <-ctx.Done():
+ slog.Info("shutting down")
+ case err := <-errs:
+ runErr = err
+ // Propagate the crash to all handler contexts so Poll and
+ // other long-lived RPCs exit via ctx.Done(); Shutdown below
+ // then completes without waiting on them.
+ cancel()
+ if err != nil {
+ slog.Error("server exited, shutting down peers", "err", err)
+ } else {
+ slog.Warn("server exited unexpectedly, shutting down peers")
+ }
+ }
+
+ sup.Stopping()
+
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), config.HTTPShutdownTimeout)
+ defer shutdownCancel()
+
+ shutdown := func(name string, s *http.Server) {
+ if err := s.Shutdown(shutdownCtx); err != nil {
+ slog.Error("server shutdown", "name", name, "err", err)
+ }
+ }
+
+ shutdown("web", webSrv)
+ shutdown("grpc", grpcSrv)
+ shutdown("admin", adminSrv)
+
+ return runErr
+}
+
+// listeners returns gRPC, web, and admin listeners.
+// With systemd socket activation it expects two named fds: "grpc" and "web".
+// Without socket activation it falls back to configured addresses.
+func listeners(cfg *appConfig) (grpcLn, webLn, adminLn net.Listener, err error) {
+ named, err := activation.ListenersWithNames()
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("socket activation: %w", err)
+ }
+
+ if lns := named["grpc"]; len(lns) > 0 {
+ grpcLn = lns[0]
+ } else if grpcLn, err = net.Listen("tcp", cfg.GrpcAddr); err != nil {
+ return nil, nil, nil, err
+ }
+ defer func() {
+ if err != nil && grpcLn != nil {
+ _ = grpcLn.Close()
+ }
+ }()
+
+ if lns := named["web"]; len(lns) > 0 {
+ webLn = lns[0]
+ } else if webLn, err = net.Listen("tcp", cfg.WebAddr); err != nil {
+ return nil, nil, nil, err
+ }
+ defer func() {
+ if err != nil && webLn != nil {
+ _ = webLn.Close()
+ }
+ }()
+
+ _ = os.Remove(cfg.AdminSocket)
+ if adminLn, err = net.Listen("unix", cfg.AdminSocket); err != nil {
+ return nil, nil, nil, err
+ }
+ defer func() {
+ if err != nil && adminLn != nil {
+ _ = adminLn.Close()
+ }
+ }()
+
+ if err = os.Chmod(cfg.AdminSocket, 0o660); err != nil {
+ return nil, nil, nil, fmt.Errorf("chmod admin socket: %w", err)
+ }
+
+ return grpcLn, webLn, adminLn, nil
+}
+
+func adminClient(socketPath string) pbconnect.AdminClient {
+ if socketPath == "" {
+ socketPath = "/run/mirumd/admin.sock"
+ }
+ return pbconnect.NewAdminClient(
+ &http.Client{
+ Transport: &http.Transport{
+ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ return net.Dial("unix", socketPath)
+ },
+ },
+ },
+ "http://localhost.unix",
+ )
+}