package main import ( "context" "io" "log/slog" "os" "os/signal" "strings" "syscall" "time" _ "github.com/joho/godotenv/autoload" _ "net/http/pprof" "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/util/cliutil" "github.com/earthboundkid/versioninfo/v2" "github.com/urfave/cli/v3" ) func main() { if err := run(os.Args); err != nil { slog.Error("exiting process", "err", err.Error()) os.Exit(-1) } } func run(args []string) error { app := cli.Command{ Name: "scrumble", Usage: "scrumble.social server instance", Version: versioninfo.Short(), } app.Flags = []cli.Flag{ &cli.StringSliceFlag{ Name: "admin-password", Usage: "secret password/token for accessing admin endpoints (multiple values allowed)", Sources: cli.EnvVars("SCRUMBLE_ADMIN_PASSWORD", "SCRUMBLE_ADMIN_KEY"), }, &cli.StringFlag{ Name: "log-level", Usage: "log verbosity level (eg: warn, info, debug)", Sources: cli.EnvVars("SCRUMBLE_LOG_LEVEL", "GO_LOG_LEVEL", "LOG_LEVEL"), }, } app.Commands = []*cli.Command{ &cli.Command{ Name: "serve", Usage: "run the scrumble daemon", Action: runServer, Flags: []cli.Flag{ &cli.StringFlag{ Name: "db-url", Usage: "database connection string for relay database", Value: "sqlite://data/scrumble.sqlite", Sources: cli.EnvVars("DATABASE_URL"), }, &cli.IntFlag{ Name: "max-db-conn", Usage: "limit on size of database connection pool", Sources: cli.EnvVars("MAX_DB_CONNECTIONS", "MAX_METADB_CONNECTIONS"), Value: 40, }, &cli.StringFlag{ Name: "plc-host", Usage: "method, hostname, and port of PLC registry", Value: "https://plc.directory", Sources: cli.EnvVars("SCRUMBLE_PLC_HOST", "ATP_PLC_HOST"), }, &cli.StringFlag{ Name: "bind", Usage: "IP or address, and port, to listen on for HTTP APIs (including firehose)", Value: ":3000", Sources: cli.EnvVars("SCRUMBLE_BIND"), }, &cli.IntFlag{ Name: "ident-cache-size", Value: 10_000, Usage: "size of in-process identity cache (eg, DID docs)", Sources: cli.EnvVars("SCRUMBLE_IDENT_CACHE_SIZE"), }, &cli.StringFlag{ Name: "metrics-listen", Usage: "IP or address, and port, to listen on for prometheus metrics", Value: ":2471", Sources: cli.EnvVars("SCRUMBLE_METRICS_LISTEN"), }, }, }, } return app.Run(context.Background(), args) } func configLogger(cmd *cli.Command, writer io.Writer) *slog.Logger { var level slog.Level switch strings.ToLower(cmd.String("log-level")) { case "error": level = slog.LevelError case "warn": level = slog.LevelWarn case "info": level = slog.LevelInfo case "debug": level = slog.LevelDebug default: level = slog.LevelInfo } logger := slog.New(slog.NewJSONHandler(writer, &slog.HandlerOptions{ Level: level, })) slog.SetDefault(logger) return logger } func runServer(ctx context.Context, cmd *cli.Command) error { logger := configLogger(cmd, os.Stdout) // Trap SIGINT to trigger a shutdown. signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) dburl := cmd.String("db-url") maxConn := cmd.Int("max-db-conn") logger.Info("configuring database", "url", dburl, "maxConn", maxConn) db, err := cliutil.SetupDatabase(dburl, maxConn) if err != nil { return err } baseDir := identity.BaseDirectory{ SkipHandleVerification: true, SkipDNSDomainSuffixes: []string{".bsky.social"}, TryAuthoritativeDNS: true, PLCURL: cmd.String("plc-host"), } dir := identity.NewCacheDirectory(&baseDir, cmd.Int("ident-cache-size"), time.Hour*24, time.Minute*2, time.Minute*5) srv, err := NewServer(db) if err != nil { return err } srv.dir = &dir // start metrics endpoint go func() { if err := srv.StartMetrics(cmd.String("metrics-listen")); err != nil { logger.Error("failed to start metrics endpoint", "err", err) os.Exit(1) } }() srvErr := make(chan error, 1) go func() { err := srv.StartHTTP(cmd.String("bind")) srvErr <- err }() go func() { err := srv.StartIndexer() srvErr <- err }() logger.Info("startup complete") select { case <-signals: logger.Info("received shutdown signal") errs := srv.Shutdown() for err := range errs { logger.Error("error during shutdown", "err", err) } case err := <-srvErr: if err != nil { logger.Error("error during startup", "err", err) } logger.Info("shutting down") errs := srv.Shutdown() for err := range errs { logger.Error("error during shutdown", "err", err) } } logger.Info("shutdown complete") return nil }