diff --git a/.air.toml b/.air.toml index d6e480a..8acaf66 100644 --- a/.air.toml +++ b/.air.toml @@ -16,7 +16,7 @@ tmp_dir = "tmp" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html", "go.tmpl"] include_file = [] - kill_delay = "0s" + kill_delay = "1s" log = "build-errors.log" poll = false poll_interval = 0 @@ -24,7 +24,7 @@ tmp_dir = "tmp" pre_cmd = [] rerun = false rerun_delay = 500 - send_interrupt = false + send_interrupt = true stop_on_error = false [color] diff --git a/README.md b/README.md index 0bb32a3..aafce98 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ The `ratchetd` cmd binary uses Oauth so you will need to create a new Oauth App. ## Additional Resources +- [Graceful Shutdown](https://dev.to/mokiat/proper-http-shutdown-in-go-3fji) - [Content Range Requests](https://web.archive.org/web/20230918195519/https://benramsey.com/blog/2008/05/206-partial-content-and-range-requests/) - [HTTP 204 and 205 Status Codes](https://web.archive.org/web/20230918193536/https://benramsey.com/blog/2008/05/http-status-204-no-content-and-205-reset-content/) - [How to Disable FileServer Directory Listings](https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings) diff --git a/cmd/ratchetd/main.go b/cmd/ratchetd/main.go index f36483c..e13483f 100644 --- a/cmd/ratchetd/main.go +++ b/cmd/ratchetd/main.go @@ -1,12 +1,17 @@ package main import ( + "context" "crypto/tls" + "errors" "flag" "fmt" + "io" "log/slog" "net/http" "os" + "os/signal" + "syscall" "time" rdb "git.runcible.io/learning/ratchet/internal/database" @@ -22,17 +27,20 @@ import ( // commit string // ) -func main() { +func run(ctx context.Context, w io.Writer, args []string) error { // CONFIGURATION // Parse command line options - addr := flag.String("addr", "0.0.0.0", "HTTP network address") - port := flag.String("port", "5001", "HTTP port") - logLevel := flag.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].") - dbPath := flag.String("database", "./ratchet.db", "A path to a sqlite3 database") - certPath := flag.String("cert", "./tls/cert.pem", "A public cert in .pem format") - keyPath := flag.String("key", "./tls/key.pem", "A private key in .pem format") + flags := flag.NewFlagSet(args[0], flag.ExitOnError) + addr := flags.String("addr", "0.0.0.0", "HTTP network address") + port := flags.String("port", "5001", "HTTP port") + logLevel := flags.String("logging", "INFO", "Logging Level. Valid values [INFO, DEBUG, WARN, ERROR].") + dbPath := flags.String("database", "./ratchet.db", "A path to a sqlite3 database") + certPath := flags.String("cert", "./tls/cert.pem", "A public cert in .pem format") + keyPath := flags.String("key", "./tls/key.pem", "A private key in .pem format") // must call parse or all values will be the defaults - flag.Parse() + if err := flags.Parse(args[1:]); err != nil { + return err + } // DEPENDENCY INJECTION FOR HANDLERS // Setup Logging @@ -41,17 +49,22 @@ func main() { db, err := rdb.OpenSqlite3DB(*dbPath) if err != nil { - slog.Error(err.Error()) - os.Exit(1) + return err } // Close db connection before exiting main. - defer db.Close() + defer func() { + slog.Info("Cleaning up database") + _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)") + if err != nil { + slog.Error(fmt.Sprintf("Error checkpointing database: %v", err)) + } + db.Close() + }() //tc, err := server.InitTemplateCache() tc, err := server.InitFSTemplateCache() if err != nil { - slog.Error(err.Error()) - os.Exit(1) + return err } // SessionManager @@ -126,8 +139,37 @@ func main() { slog.Info(fmt.Sprintf("Listening on https://%s", srv.Addr)) - err = srv.ListenAndServeTLS(*certPath, *keyPath) - slog.Error(err.Error()) - os.Exit(1) + go func() { + if err = srv.ListenAndServeTLS(*certPath, *keyPath); !errors.Is(err, http.ErrServerClosed) { + slog.Error(err.Error()) + os.Exit(1) + } + slog.Info("Stopped serving connections") + }() + + // Handle graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + // Block until signal is recieved + <-sigChan + shutdownCtx, shutdownRelease := context.WithTimeout(ctx, 10*time.Second) + defer shutdownRelease() + + if err := srv.Shutdown(shutdownCtx); err != nil { + slog.Error("Failed to close within context timeout. Forcing server close.") + srv.Close() + return err + } + + return nil + +} + +func main() { + ctx := context.Background() + if err := run(ctx, os.Stdout, os.Args); err != nil { + slog.Error(err.Error()) + os.Exit(1) + } }