mirror of
https://github.com/r3-team/r3.git
synced 2025-12-18 00:54:41 -06:00
659 lines
20 KiB
Go
659 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"embed"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"r3/bruteforce"
|
|
"r3/cache"
|
|
"r3/cluster"
|
|
"r3/config"
|
|
"r3/data/data_image"
|
|
"r3/db"
|
|
"r3/db/embedded"
|
|
"r3/db/initialize"
|
|
"r3/db/upgrade"
|
|
"r3/handler"
|
|
"r3/handler/api"
|
|
"r3/handler/api_auth"
|
|
"r3/handler/cache_download"
|
|
"r3/handler/client_download"
|
|
"r3/handler/csv_download"
|
|
"r3/handler/csv_upload"
|
|
"r3/handler/data_access"
|
|
"r3/handler/data_auth"
|
|
"r3/handler/data_download"
|
|
"r3/handler/data_download_thumb"
|
|
"r3/handler/data_upload"
|
|
"r3/handler/icon_upload"
|
|
"r3/handler/ics_download"
|
|
"r3/handler/license_upload"
|
|
"r3/handler/manifest_download"
|
|
"r3/handler/transfer_export"
|
|
"r3/handler/transfer_import"
|
|
"r3/handler/websocket"
|
|
"r3/ldap"
|
|
"r3/log"
|
|
"r3/login"
|
|
"r3/login/login_session"
|
|
"r3/scheduler"
|
|
"r3/tools"
|
|
"strings"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
_ "time/tzdata" // to embed timezone DB
|
|
|
|
"github.com/kardianos/service"
|
|
)
|
|
|
|
var (
|
|
// overwritten by build parameters
|
|
appName string = "REI3"
|
|
appNameShort string = "R3"
|
|
appVersion string = "0.1.2.3"
|
|
appVersionClient string = "0.1.2.3"
|
|
|
|
// start parameters
|
|
cli struct {
|
|
adminCreate string
|
|
configFile string
|
|
debug bool
|
|
dynamicPort bool
|
|
imageMagick string
|
|
http bool
|
|
open bool
|
|
run bool
|
|
serviceName string
|
|
serviceStart bool
|
|
serviceStop bool
|
|
serviceInstall bool
|
|
serviceUninstall bool
|
|
setData string
|
|
wwwPath string
|
|
}
|
|
|
|
// embed static web files
|
|
//go:embed www/*
|
|
fsStatic embed.FS
|
|
|
|
//go:embed www/images/noPic.png
|
|
fsStaticNoPic []byte
|
|
)
|
|
|
|
type program struct {
|
|
embeddedDbOwned atomic.Bool // whether this instance has started the embedded database
|
|
logger service.Logger // logs to the operating system if called as service, otherwise to stdOut
|
|
stopping atomic.Bool
|
|
webServer *http.Server
|
|
}
|
|
|
|
func main() {
|
|
|
|
// set configuration parameters
|
|
if err := config.SetAppVersion(appVersion, "service"); err != nil {
|
|
fmt.Printf("failed to set app version, %v\n", err)
|
|
return
|
|
}
|
|
if err := config.SetAppVersion(appVersionClient, "fatClient"); err != nil {
|
|
fmt.Printf("failed to set app client version, %v\n", err)
|
|
return
|
|
}
|
|
config.SetAppName(appName, appNameShort)
|
|
|
|
// process configuration overwrites from command line
|
|
flag.StringVar(&cli.adminCreate, "newadmin", "", "Create new admin user (username:password), password must not contain spaces or colons")
|
|
flag.StringVar(&cli.configFile, "config", "config.json", "Location of configuration file (combined with -run)")
|
|
flag.BoolVar(&cli.dynamicPort, "dynamicport", false, "Start with a port provided by the operating system (combined with -run)")
|
|
flag.StringVar(&cli.imageMagick, "imagemagick", "", "Alternative location for the ImageMagick convert utility")
|
|
flag.BoolVar(&cli.http, "http", false, "Start with HTTP (not encrypted, for testing/development only, combined with -run)")
|
|
flag.BoolVar(&cli.open, "open", false, fmt.Sprintf("Open URL of %s in default browser (combined with -run)", appName))
|
|
flag.BoolVar(&cli.run, "run", false, fmt.Sprintf("Run %s from within this console (see 'config.json' for configuration)", appName))
|
|
flag.BoolVar(&cli.debug, "debug", false, "Logs all events regardless of configured log level (combined with -run)")
|
|
flag.BoolVar(&cli.serviceInstall, "install", false, fmt.Sprintf("Install %s service", appName))
|
|
flag.StringVar(&cli.serviceName, "servicename", appName, "Specify name of service to manage (to (un)install, start or stop service)")
|
|
flag.BoolVar(&cli.serviceStart, "start", false, fmt.Sprintf("Start %s service", appName))
|
|
flag.BoolVar(&cli.serviceStop, "stop", false, fmt.Sprintf("Stop %s service", appName))
|
|
flag.BoolVar(&cli.serviceUninstall, "uninstall", false, fmt.Sprintf("Uninstall %s service", appName))
|
|
flag.StringVar(&cli.setData, "setdata", "", "Write to config file: Data directory (platform files and database if stand-alone)")
|
|
flag.StringVar(&cli.wwwPath, "wwwpath", "", "(Development) Use web files from given path instead of embedded ones")
|
|
flag.Parse()
|
|
|
|
// enable debug mode
|
|
if cli.debug {
|
|
log.SetDebug(true)
|
|
}
|
|
|
|
// define service and service logger
|
|
svcDisplay := fmt.Sprintf("%s platform", appName)
|
|
if cli.serviceName != appName {
|
|
svcDisplay = fmt.Sprintf("%s platform (%s)", appName, cli.serviceName)
|
|
}
|
|
svcConfig := &service.Config{
|
|
Name: strings.ToLower(cli.serviceName),
|
|
DisplayName: svcDisplay,
|
|
Description: fmt.Sprintf("Provides the %s platform components", appName),
|
|
}
|
|
|
|
// initialize service
|
|
prg := &program{}
|
|
|
|
svc, err := service.New(prg, svcConfig)
|
|
if err != nil {
|
|
fmt.Printf("service could not be created, error: %v\n", err)
|
|
return
|
|
}
|
|
prg.logger, err = svc.Logger(nil)
|
|
if err != nil {
|
|
fmt.Printf("service logger could not be created, error: %v\n", err)
|
|
return
|
|
}
|
|
|
|
// listen to global shutdown channel
|
|
go func() {
|
|
<-scheduler.OsExit
|
|
prg.executeAborted(svc, nil)
|
|
}()
|
|
|
|
// add shut down in case of SIGTERM (terminal closed)
|
|
if service.Interactive() {
|
|
signal.Notify(scheduler.OsExit, syscall.SIGTERM)
|
|
}
|
|
|
|
// get path for executable & change working dir to it
|
|
app, err := os.Executable()
|
|
if err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
if err := os.Chdir(filepath.Dir(app)); err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
|
|
// load configuration from file
|
|
config.SetConfigFilePath(cli.configFile)
|
|
|
|
if err := config.LoadFile(); err != nil {
|
|
prg.logger.Errorf("failed to read configuration file, %v", err)
|
|
return
|
|
}
|
|
|
|
// apply portable mode settings if enabled
|
|
if config.File.Portable {
|
|
// compatability fix: Older portable configs (<3.10) had 443 as default port
|
|
if config.File.Web.Port == 443 {
|
|
cli.dynamicPort = true
|
|
}
|
|
cli.http = true
|
|
cli.run = true
|
|
cli.open = true
|
|
}
|
|
|
|
// print usage info if interactive and no arguments were added
|
|
if !config.File.Portable && service.Interactive() && len(os.Args) == 1 {
|
|
fmt.Printf("Available parameters:\n")
|
|
flag.PrintDefaults()
|
|
|
|
fmt.Printf("\n################################################################################\n")
|
|
fmt.Printf("This is the executable of %s, the open low-code platform, v%s\n", appName, appVersion)
|
|
fmt.Printf("Copyright (c) 2019-2025 Gabriel Victor Herbert\n\n")
|
|
fmt.Printf("%s can be installed as service (-install) or run from the console (-run).\n\n", appName)
|
|
fmt.Printf("When %s is running, use any modern browser to access it (port 443 by default).\n\n", appName)
|
|
fmt.Printf("For installation instructions, please refer to the included README file or visit\n")
|
|
fmt.Printf("https://rei3.de/en/docs/admin/ for the full admin documentation.\n")
|
|
fmt.Printf("################################################################################\n\n")
|
|
|
|
// wait for user input to keep console open
|
|
fmt.Printf("See above for available parameters. Press enter to return.\n")
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
reader.ReadString('\n')
|
|
return
|
|
}
|
|
|
|
// other cli arguments
|
|
if cli.serviceInstall {
|
|
if err := svc.Install(); err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
prg.logger.Info("service was successfully installed")
|
|
return
|
|
}
|
|
if cli.serviceUninstall {
|
|
if err := svc.Uninstall(); err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
prg.logger.Info("service was successfully uninstalled")
|
|
return
|
|
}
|
|
if cli.serviceStart {
|
|
if err := svc.Start(); err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
prg.logger.Info("service was successfully started")
|
|
return
|
|
}
|
|
if cli.serviceStop {
|
|
if err := svc.Stop(); err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
prg.logger.Info("service was successfully stopped")
|
|
return
|
|
}
|
|
if cli.dynamicPort {
|
|
config.File.Web.Port = 0
|
|
}
|
|
if cli.setData != "" {
|
|
config.File.Paths.Certificates = filepath.Join(cli.setData, "certificates")
|
|
config.File.Paths.EmbeddedDbData = filepath.Join(cli.setData, "database")
|
|
config.File.Paths.Files = filepath.Join(cli.setData, "files")
|
|
config.File.Paths.Temp = filepath.Join(cli.setData, "temp")
|
|
config.File.Paths.Transfer = filepath.Join(cli.setData, "transfer")
|
|
|
|
if err := config.WriteFile(); err != nil {
|
|
prg.logger.Errorf("failed to write configuration file, %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// main executable can be used to open the app in default browser even if its not started (-open without -run)
|
|
// used for shortcuts in start menu when installed on Windows systems with desktop experience
|
|
// if dynamic port (0) is used, we cannot open app without starting it (port is not known)
|
|
if cli.open && config.File.Web.Port != 0 && !config.File.Portable {
|
|
protocol := "https"
|
|
if cli.http {
|
|
protocol = "http"
|
|
}
|
|
tools.OpenRessource(fmt.Sprintf("%s://localhost:%d", protocol, config.File.Web.Port))
|
|
}
|
|
|
|
// interactive, app only starts if to be run from console or when creating an admin user
|
|
if service.Interactive() && !cli.run && cli.adminCreate == "" {
|
|
return
|
|
}
|
|
|
|
// Run() blocks until Stop() is called
|
|
if err := svc.Run(); err != nil {
|
|
prg.logger.Error(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Start() is called when service is being started
|
|
func (prg *program) Start(svc service.Service) error {
|
|
|
|
if !service.Interactive() {
|
|
prg.logger.Info("Starting service...")
|
|
} else {
|
|
log.SetOutputCli(true)
|
|
}
|
|
go prg.execute(svc)
|
|
return nil
|
|
}
|
|
|
|
// execute the application logic
|
|
func (prg *program) execute(svc service.Service) {
|
|
|
|
// start embedded database
|
|
if config.File.Db.Embedded {
|
|
prg.logger.Infof("start embedded database at '%s'", config.File.Paths.EmbeddedDbData)
|
|
embedded.SetPaths()
|
|
|
|
if err := embedded.Start(); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to start embedded database, %v", err))
|
|
return
|
|
}
|
|
|
|
// we own the embedded DB if we can successfully start it
|
|
// otherwise another instance might be running it
|
|
prg.embeddedDbOwned.Store(true)
|
|
}
|
|
|
|
// connect to database
|
|
// wait X seconds at first start for database service to become ready
|
|
if err := db.OpenWait(15, config.File.Db); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to open database connection, %v", err))
|
|
return
|
|
}
|
|
|
|
// check for first database start
|
|
if err := initialize.PrepareDbIfNew(); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to initialize database on first start, %v", err))
|
|
return
|
|
}
|
|
|
|
// apply configuration from database
|
|
if err := config.LoadFromDb(); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to apply configuration from database, %v", err))
|
|
return
|
|
}
|
|
bruteforce.SetConfig()
|
|
config.ActivateLicense()
|
|
config.SetLogLevels()
|
|
|
|
// run automatic database upgrade if required
|
|
if err := upgrade.RunIfRequired(); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed automatic upgrade of database, %v", err))
|
|
return
|
|
}
|
|
|
|
// process cli commands
|
|
if cli.adminCreate != "" {
|
|
adminInputs := strings.Split(cli.adminCreate, ":")
|
|
|
|
if len(adminInputs) != 2 {
|
|
prg.executeAborted(svc, fmt.Errorf("invalid syntax for admin creation, required is username:password"))
|
|
} else {
|
|
if err := login.CreateAdmin(adminInputs[0], adminInputs[1]); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to create admin user, %v", err))
|
|
} else {
|
|
prg.logger.Info("successfully created new admin user")
|
|
prg.executeAborted(svc, nil)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// store host details in cache (before cluster node startup)
|
|
if err := config.SetHostnameFromOs(); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to load host details, %v", err))
|
|
return
|
|
}
|
|
|
|
// prepare system & initalize caches once DB is ready
|
|
ctx, ctxCanc := context.WithTimeout(context.Background(), db.CtxDefTimeoutSysStart)
|
|
defer ctxCanc()
|
|
|
|
if err := initSystem(ctx); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to initalize system during startup, %v", err))
|
|
return
|
|
}
|
|
if err := initCaches(ctx); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to initalize required caches during startup, %v", err))
|
|
return
|
|
}
|
|
if err := initCachesOptional(ctx); err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to initalize optional caches during startup, %v", err))
|
|
return
|
|
}
|
|
|
|
// prepare image processing
|
|
data_image.PrepareProcessing(cli.imageMagick)
|
|
|
|
log.Info(log.ContextServer, fmt.Sprintf("is ready to start application (%s)", appVersion))
|
|
|
|
// start scheduler (must start after module cache)
|
|
go scheduler.Start()
|
|
|
|
// start web server
|
|
go websocket.StartBackgroundTasks()
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
if cli.wwwPath == "" {
|
|
fsStaticWww, err := fs.Sub(fs.FS(fsStatic), "www")
|
|
if err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to access embedded web file directory, %v", err))
|
|
return
|
|
}
|
|
mux.Handle("/", http.FileServer(http.FS(fsStaticWww)))
|
|
} else {
|
|
mux.Handle("/", http.FileServer(http.Dir(cli.wwwPath)))
|
|
}
|
|
handler.SetNoImage(fsStaticNoPic)
|
|
|
|
mux.HandleFunc("/api/", api.Handler)
|
|
mux.HandleFunc("/api/auth", api_auth.Handler)
|
|
mux.HandleFunc("/cache/download/", cache_download.Handler)
|
|
mux.HandleFunc("/csv/download/", csv_download.Handler)
|
|
mux.HandleFunc("/csv/upload", csv_upload.Handler)
|
|
mux.HandleFunc("/client/download/", client_download.Handler)
|
|
mux.HandleFunc("/client/download/config/", client_download.HandlerConfig)
|
|
mux.HandleFunc("/data/download/", data_download.Handler)
|
|
mux.HandleFunc("/data/download/thumb/", data_download_thumb.Handler)
|
|
mux.HandleFunc("/data/upload", data_upload.Handler)
|
|
mux.HandleFunc("/icon/upload", icon_upload.Handler)
|
|
mux.HandleFunc("/ics/download/", ics_download.Handler)
|
|
mux.HandleFunc("/license/upload", license_upload.Handler)
|
|
mux.HandleFunc("/manifests/", manifest_download.Handler)
|
|
mux.HandleFunc("/websocket", websocket.Handler)
|
|
mux.HandleFunc("/export/", transfer_export.Handler)
|
|
mux.HandleFunc("/import", transfer_import.Handler)
|
|
|
|
// legacy
|
|
mux.HandleFunc("/data/access", data_access.Handler)
|
|
mux.HandleFunc("/data/auth", data_auth.Handler)
|
|
|
|
webServerString := fmt.Sprintf("%s:%d", config.File.Web.Listen, config.File.Web.Port)
|
|
webListener, err := net.Listen("tcp", webServerString)
|
|
if err != nil {
|
|
prg.executeAborted(svc, fmt.Errorf("failed to register listener for HTTP server, %v", err))
|
|
return
|
|
}
|
|
config.File.Web.Port = webListener.Addr().(*net.TCPAddr).Port
|
|
|
|
prg.webServer = &http.Server{
|
|
Addr: webServerString,
|
|
Handler: mux,
|
|
IdleTimeout: 120 * time.Second,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
log.Info(log.ContextServer, fmt.Sprintf("starting web handlers for '%s'", webServerString))
|
|
|
|
// if dynamic port (0) is used we can only now open the app in default browser (port is now known)
|
|
if cli.open && config.File.Web.Port != 0 {
|
|
protocol := "https"
|
|
if cli.http {
|
|
protocol = "http"
|
|
}
|
|
tools.OpenRessource(fmt.Sprintf("%s://localhost:%d", protocol, config.File.Web.Port))
|
|
}
|
|
|
|
// show interactive user that application is ready for connection
|
|
if service.Interactive() {
|
|
fmt.Printf("Starting web server for '%s'...\n", webServerString)
|
|
}
|
|
|
|
// start web server and block routine
|
|
if cli.http {
|
|
if err := prg.webServer.Serve(webListener); err != nil && err != http.ErrServerClosed {
|
|
prg.executeAborted(svc, err)
|
|
}
|
|
} else {
|
|
cache.SetCertPaths(
|
|
filepath.Join(config.File.Paths.Certificates, config.File.Web.Cert),
|
|
filepath.Join(config.File.Paths.Certificates, config.File.Web.Key))
|
|
|
|
if err := cache.CheckRenewCert(); err != nil {
|
|
prg.executeAborted(svc, err)
|
|
return
|
|
}
|
|
|
|
// PreferServerCipherSuites & CipherSuites are deprecated
|
|
// https://github.com/golang/go/issues/45430
|
|
prg.webServer.TLSConfig = &tls.Config{
|
|
GetCertificate: cache.GetCert,
|
|
}
|
|
switch config.File.Web.TlsMinVersion {
|
|
case "": // prior to 3.8.4, defaults to not apply min. TLS version
|
|
case "1.1":
|
|
prg.webServer.TLSConfig.MinVersion = tls.VersionTLS11
|
|
case "1.2":
|
|
prg.webServer.TLSConfig.MinVersion = tls.VersionTLS12
|
|
case "1.3":
|
|
prg.webServer.TLSConfig.MinVersion = tls.VersionTLS13
|
|
default:
|
|
log.Warning(log.ContextServer, "failed to apply min. TLS version",
|
|
fmt.Errorf("version '%s' is not supported (valid: 1.1, 1.2 or 1.3)", config.File.Web.TlsMinVersion))
|
|
}
|
|
if err := prg.webServer.ServeTLS(webListener, "", ""); err != nil && err != http.ErrServerClosed {
|
|
prg.executeAborted(svc, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// init system with connected database
|
|
func initSystem(ctx context.Context) error {
|
|
tx, err := db.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
// set unique instance ID if empty
|
|
if err := config.SetInstanceIdIfEmpty_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to set instance ID, %v", err)
|
|
}
|
|
|
|
// process token secret for future client authentication from database
|
|
if err := config.ProcessTokenSecret_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to process token secret, %v", err)
|
|
}
|
|
|
|
// setup cluster node with shared database
|
|
if err := cluster.StartNode_tx(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// remove login sessions logs for this cluster node (in case they were not removed on shutdown)
|
|
if err := login_session.LogsRemoveForNode_tx(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
// load required caches from database
|
|
func initCaches(ctx context.Context) error {
|
|
tx, err := db.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
// module meta data must be loaded before module schema (informs about what modules to load)
|
|
if err := cache.LoadModuleIdMapMeta_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize module meta cache, %v", err)
|
|
}
|
|
if err := cache.LoadCaptionMapCustom_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize custom caption map cache, %v", err)
|
|
}
|
|
if err := cache.LoadSchema_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize schema cache, %v", err)
|
|
}
|
|
if err := cache.LoadMailAccountMap_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize mail account cache, %v", err)
|
|
}
|
|
if err := cache.LoadOauthClientMap_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize oauth client cache, %v", err)
|
|
}
|
|
if err := cache.LoadPwaDomainMap_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize PWA domain cache, %v", err)
|
|
}
|
|
if err := ldap.UpdateCache_tx(ctx, tx); err != nil {
|
|
return fmt.Errorf("failed to initialize LDAP cache, %v", err)
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
// load optional cache from database, might fail due to missing permissions
|
|
func initCachesOptional(ctx context.Context) error {
|
|
tx, err := db.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
if err := cache.LoadSearchDictionaries_tx(ctx, tx); err != nil {
|
|
log.Error(log.ContextServer, "failed to read/update text search dictionaries", err)
|
|
return tx.Rollback(ctx)
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
// properly shuts down application, if execution is aborted prematurely
|
|
func (prg *program) executeAborted(svc service.Service, err error) {
|
|
if err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
if service.Interactive() {
|
|
if err := prg.Stop(svc); err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
// in cases like cluster node shutdown, there is no exit signal
|
|
os.Exit(0)
|
|
} else {
|
|
if err := svc.Stop(); err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop() is also called when service is being shut down
|
|
func (prg *program) Stop(svc service.Service) error {
|
|
|
|
if !service.Interactive() {
|
|
prg.logger.Info("Stopping service...")
|
|
} else {
|
|
// keep shut down message visible
|
|
fmt.Println("Shutting down...")
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
if prg.stopping.Load() {
|
|
return nil
|
|
}
|
|
prg.stopping.Store(true)
|
|
|
|
ctx, ctxCanc := context.WithTimeout(context.Background(), db.CtxDefTimeoutShutdown)
|
|
defer ctxCanc()
|
|
|
|
// remove login session logs for this cluster node
|
|
if err := login_session.LogsRemoveForNode(ctx); err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
|
|
// stop scheduler
|
|
scheduler.Stop()
|
|
|
|
// stop web server if running
|
|
if prg.webServer != nil {
|
|
if err := prg.webServer.Shutdown(ctx); err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
log.Info(log.ContextServer, "stopped web handlers")
|
|
}
|
|
|
|
// close database connection and deregister cluster node if DB is open
|
|
if db.Pool != nil {
|
|
if err := cluster.StopNode(ctx); err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
db.Close()
|
|
log.Info(log.ContextServer, "stopped database handler")
|
|
}
|
|
|
|
// stop embedded database if owned
|
|
if prg.embeddedDbOwned.Load() {
|
|
if err := embedded.Stop(); err != nil {
|
|
prg.logger.Error(err)
|
|
}
|
|
log.Info(log.ContextServer, "stopped embedded database")
|
|
}
|
|
return nil
|
|
}
|