/* Webapi is the executable for the main web server. It builds a web server around APIs from `service/api`. Webapi connects to external resources needed (database) and starts two web servers: the API web server, and the debug. Everything is served via the API web server, except debug variables (/debug/vars) and profiler infos (pprof). Usage: webapi [flags] Flags and configurations are handled automatically by the code in `load-configuration.go`. Return values (exit codes): 0 The program ended successfully (no errors, stopped by signal) > 0 The program ended due to an error Note that this program will update the schema of the database to the latest version available (embedded in the executable during the build). */ package main import ( "context" "database/sql" "errors" "fmt" "math/rand" "net/http" "os" "os/signal" "syscall" "github.com/ardanlabs/conf" _ "github.com/mattn/go-sqlite3" "github.com/notherealmarco/WASAPhoto/service/api" "github.com/notherealmarco/WASAPhoto/service/database" "github.com/notherealmarco/WASAPhoto/service/globaltime" "github.com/sirupsen/logrus" ) // main is the program entry point. The only purpose of this function is to call run() and set the exit code if there is // any error func main() { if err := run(); err != nil { _, _ = fmt.Fprintln(os.Stderr, "error: ", err) os.Exit(1) } } // run executes the program. The body of this function should perform the following steps: // * reads the configuration // * creates and configure the logger // * connects to any external resources (like databases, authenticators, etc.) // * creates an instance of the service/api package // * starts the principal web server (using the service/api.Router.Handler() for HTTP handlers) // * waits for any termination event: SIGTERM signal (UNIX), non-recoverable server error, etc. // * closes the principal web server func run() error { rand.Seed(globaltime.Now().UnixNano()) // Load Configuration and defaults cfg, err := loadConfiguration() if err != nil { if errors.Is(err, conf.ErrHelpWanted) { return nil } return err } // Init logging logger := logrus.New() logger.SetOutput(os.Stdout) if cfg.Debug { logger.SetLevel(logrus.DebugLevel) } else { logger.SetLevel(logrus.InfoLevel) } logger.Infof("application initializing") // Create the directories if they don't exist if err := os.MkdirAll(cfg.Data.Path, 0755); err != nil { logger.WithError(err).Error("error creating data directory") return fmt.Errorf("creating data directory: %w", err) } // Start Database logger.Println("initializing database support") dbconn, err := sql.Open("sqlite3", cfg.DB.Filename) if err != nil { logger.WithError(err).Error("error opening SQLite DB") return fmt.Errorf("opening SQLite: %w", err) } defer func() { logger.Debug("database stopping") _ = dbconn.Close() }() db, err := database.New(dbconn) if err != nil { logger.WithError(err).Error("error creating AppDatabase") return fmt.Errorf("creating AppDatabase: %w", err) } // Start (main) API server logger.Info("initializing API server") // Make a channel to listen for an interrupt or terminate signal from the OS. // Use a buffered channel because the signal package requires it. shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) // Make a channel to listen for errors coming from the listener. Use a // buffered channel so the goroutine can exit if we don't collect this error. serverErrors := make(chan error, 1) // Create the API router apirouter, err := api.New(api.Config{ Logger: logger, Database: db, DataPath: cfg.Data.Path, }) if err != nil { logger.WithError(err).Error("error creating the API server instance") return fmt.Errorf("creating the API server instance: %w", err) } router := apirouter.Handler() router, err = registerWebUI(router) if err != nil { logger.WithError(err).Error("error registering web UI handler") return fmt.Errorf("registering web UI handler: %w", err) } // Apply CORS policy router = applyCORSHandler(router) // Create the API server apiserver := http.Server{ Addr: cfg.Web.APIHost, Handler: router, ReadTimeout: cfg.Web.ReadTimeout, ReadHeaderTimeout: cfg.Web.ReadTimeout, WriteTimeout: cfg.Web.WriteTimeout, } // Start the service listening for requests in a separate goroutine go func() { logger.Infof("API listening on %s", apiserver.Addr) serverErrors <- apiserver.ListenAndServe() logger.Infof("stopping API server") }() // Waiting for shutdown signal or POSIX signals select { case err := <-serverErrors: // Non-recoverable server error return fmt.Errorf("server error: %w", err) case sig := <-shutdown: logger.Infof("signal %v received, start shutdown", sig) // Asking API server to shut down and load shed. err := apirouter.Close() if err != nil { logger.WithError(err).Warning("graceful shutdown of apirouter error") } // Give outstanding requests a deadline for completion. ctx, cancel := context.WithTimeout(context.Background(), cfg.Web.ShutdownTimeout) defer cancel() // Asking listener to shut down and load shed. err = apiserver.Shutdown(ctx) if err != nil { logger.WithError(err).Warning("error during graceful shutdown of HTTP server") err = apiserver.Close() } // Log the status of this shutdown. switch { case sig == syscall.SIGSTOP: return errors.New("integrity issue caused shutdown") case err != nil: return fmt.Errorf("could not stop server gracefully: %w", err) } } return nil }