From 1a9078249f85b2243c78ac102c769f9873f4f8e3 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 14 Sep 2023 20:16:49 -0500 Subject: [PATCH 1/5] gui: Consolidate create logic. This consolidates the logic that creates the GUI instance. It's generally good practice to first create everything that can fail and then create the final instance at once with the results versus doing it piecemeal. Piecemeal creation is typically more error prone and, while not a huge concern here, it also ends up needlessly creating objects that are just thrown away in the event of a later error. --- internal/gui/gui.go | 52 +++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/internal/gui/gui.go b/internal/gui/gui.go index 591f3e0c..b147c615 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -121,8 +121,6 @@ type headerData struct { // route configures the http router of the user interface. func (ui *GUI) route() { - ui.router = mux.NewRouter() - // Use a separate router without rate limiting (or other restrictions) for // static assets. assetsRouter := ui.router.PathPrefix("/assets").Subrouter() @@ -179,28 +177,8 @@ func (ui *GUI) renderTemplate(w http.ResponseWriter, name string, data interface } } -// NewGUI creates an instance of the user interface. -func NewGUI(cfg *Config) (*GUI, error) { - ui := &GUI{ - cfg: cfg, - limiter: pool.NewRateLimiter(), - } - - ui.cookieStore = sessions.NewCookieStore(cfg.CSRFSecret) - ui.websocketServer = NewWebsocketServer() - - err := ui.loadTemplates() - if err != nil { - return nil, err - } - - ui.route() - - return ui, nil -} - // loadTemplates initializes the html templates of the pool user interface. -func (ui *GUI) loadTemplates() error { +func loadTemplates(cfg *Config) (*template.Template, error) { var templates []string findTemplate := func(path string, f os.FileInfo, err error) error { // If path doesn't exist, or other error with path, return error so @@ -214,9 +192,9 @@ func (ui *GUI) loadTemplates() error { return nil } - err := filepath.Walk(ui.cfg.GUIDir, findTemplate) + err := filepath.Walk(cfg.GUIDir, findTemplate) if err != nil { - return err + return nil, err } httpTemplates := template.New("template").Funcs(template.FuncMap{ @@ -224,16 +202,26 @@ func (ui *GUI) loadTemplates() error { "floatToPercent": floatToPercent, }) - // Since template.Must panics with non-nil error, it is much more - // informative to pass the error to the caller to log it and exit - // gracefully. - httpTemplates, err = httpTemplates.ParseFiles(templates...) + return httpTemplates.ParseFiles(templates...) +} + +// NewGUI creates an instance of the user interface. +func NewGUI(cfg *Config) (*GUI, error) { + templates, err := loadTemplates(cfg) if err != nil { - return err + return nil, err } - ui.templates = template.Must(httpTemplates, nil) - return nil + ui := GUI{ + cfg: cfg, + limiter: pool.NewRateLimiter(), + templates: templates, + cookieStore: sessions.NewCookieStore(cfg.CSRFSecret), + router: mux.NewRouter(), + websocketServer: NewWebsocketServer(), + } + ui.route() + return &ui, nil } // Run starts the user interface. From d437489884f5b920cce15f30c905d2132bbb28a5 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 14 Sep 2023 20:23:52 -0500 Subject: [PATCH 2/5] gui: Init cache when GUI is created. This reworks the GUI cache initialization to happen when the GUI is created. It accomplishes this by updating the init func to accept the config instead of individual components and moving the logic that acquires the details via the config from the Run method into it. It also unexports the InitCache method since it is only ever used internal to the gui package. Not only does it further consolidate the creation logic in one place, it also exposes any errors back to the main thread when the pool is first initializing so any issues can be corrected immediately. --- internal/gui/cache.go | 40 ++++++++++++++++++++++++++++++------- internal/gui/gui.go | 46 ++++++------------------------------------- 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/internal/gui/cache.go b/internal/gui/cache.go index 19a83d31..955f21eb 100644 --- a/internal/gui/cache.go +++ b/internal/gui/cache.go @@ -89,19 +89,45 @@ type Cache struct { lastPaymentInfoMtx sync.RWMutex } -// InitCache initialises and returns a cache for use in the GUI. -func InitCache(work []*pool.AcceptedWork, quotas []*pool.Quota, - hashData map[string][]*pool.HashData, pendingPmts []*pool.Payment, - archivedPmts []*pool.Payment, blockExplorerURL string, - lastPmtHeight uint32, lastPmtPaidOn, lastPmtCreatedOn int64) *Cache { +// initCache initialises and returns a cache for use in the GUI. +func initCache(cfg *Config) (*Cache, error) { + work, err := cfg.FetchMinedWork() + if err != nil { + return nil, err + } + + quotas, err := cfg.FetchWorkQuotas() + if err != nil { + return nil, err + } + + hashData, err := cfg.FetchHashData() + if err != nil { + return nil, err + } + + pendingPmts, err := cfg.FetchPendingPayments() + if err != nil { + return nil, err + } + + archivedPmts, err := cfg.FetchArchivedPayments() + if err != nil { + return nil, err + } + + lastPmtHeight, lastPmtPaidOn, lastPmtCreatedOn, err := cfg.FetchLastPaymentInfo() + if err != nil { + return nil, err + } - cache := Cache{blockExplorerURL: blockExplorerURL} + cache := Cache{blockExplorerURL: cfg.BlockExplorerURL} cache.updateMinedWork(work) cache.updateRewardQuotas(quotas) cache.updateHashData(hashData) cache.updatePayments(pendingPmts, archivedPmts) cache.updateLastPaymentInfo(lastPmtHeight, lastPmtPaidOn, lastPmtCreatedOn) - return &cache + return &cache, nil } // min returns the smaller of the two provided integers. diff --git a/internal/gui/gui.go b/internal/gui/gui.go index b147c615..9a595d56 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -212,12 +212,18 @@ func NewGUI(cfg *Config) (*GUI, error) { return nil, err } + cache, err := initCache(cfg) + if err != nil { + return nil, err + } + ui := GUI{ cfg: cfg, limiter: pool.NewRateLimiter(), templates: templates, cookieStore: sessions.NewCookieStore(cfg.CSRFSecret), router: mux.NewRouter(), + cache: cache, websocketServer: NewWebsocketServer(), } ui.route() @@ -291,46 +297,6 @@ func (ui *GUI) Run(ctx context.Context) { } }() - // Initialise the cache. - work, err := ui.cfg.FetchMinedWork() - if err != nil { - log.Error(err) - return - } - - quotas, err := ui.cfg.FetchWorkQuotas() - if err != nil { - log.Error(err) - return - } - - hashData, err := ui.cfg.FetchHashData() - if err != nil { - log.Error(err) - return - } - - pendingPayments, err := ui.cfg.FetchPendingPayments() - if err != nil { - log.Error(err) - return - } - - archivedPayments, err := ui.cfg.FetchArchivedPayments() - if err != nil { - log.Error(err) - return - } - - lastPmtHeight, lastPmtPaidOn, lastPmtCreatedOn, err := ui.cfg.FetchLastPaymentInfo() - if err != nil { - log.Error(err) - return - } - - ui.cache = InitCache(work, quotas, hashData, pendingPayments, archivedPayments, - ui.cfg.BlockExplorerURL, lastPmtHeight, lastPmtPaidOn, lastPmtCreatedOn) - // Use a ticker to periodically update cached data and push updates through // any established websockets signalCh := ui.cfg.FetchCacheChannel() From 9052617fb85fbf39313718b85b9c82ed0925d956 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 14 Sep 2023 20:46:18 -0500 Subject: [PATCH 3/5] gui: Split out cache and websocket update run logic. This moves the code that periodically updates the cache and notifies websocket clients into a separate method that is invoked by run. It also runs the updates in the background and introduces a waitgroup for it. The intention is to have the webserver covered by the waitgroup in a future commit. --- internal/gui/gui.go | 151 ++++++++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 69 deletions(-) diff --git a/internal/gui/gui.go b/internal/gui/gui.go index 9a595d56..edc4dcfd 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "golang.org/x/crypto/acme/autocert" @@ -230,75 +231,11 @@ func NewGUI(cfg *Config) (*GUI, error) { return &ui, nil } -// Run starts the user interface. -func (ui *GUI) Run(ctx context.Context) { - go func() { - switch { - case ui.cfg.UseLEHTTPS: - certMgr := &autocert.Manager{ - Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache("certs"), - HostPolicy: autocert.HostWhitelist(ui.cfg.Domain), - } - - log.Info("Starting GUI server on port 443 (https)") - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ":https", - Handler: ui.router, - TLSConfig: &tls.Config{ - GetCertificate: certMgr.GetCertificate, - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - }, - }, - } - - if err := ui.server.ListenAndServeTLS("", ""); err != nil { - log.Error(err) - } - case ui.cfg.NoGUITLS: - log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ui.cfg.GUIListen, - Handler: ui.router, - } - - if err := ui.server.ListenAndServe(); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } - default: - log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ui.cfg.GUIListen, - Handler: ui.router, - } - - if err := ui.server.ListenAndServeTLS(ui.cfg.TLSCertFile, - ui.cfg.TLSKeyFile); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } - } - }() - - // Use a ticker to periodically update cached data and push updates through - // any established websockets +// updateCacheAndNotifyWebsocketClients periodically updates cached data and +// pushes updates to any established websocket clients. +// +// It must be run as a routine. +func (ui *GUI) updateCacheAndNotifyWebsocketClients(ctx context.Context) { signalCh := ui.cfg.FetchCacheChannel() ticker := time.NewTicker(15 * time.Second) defer ticker.Stop() @@ -375,3 +312,79 @@ func (ui *GUI) Run(ctx context.Context) { } } } + +// Run starts the user interface. +func (ui *GUI) Run(ctx context.Context) { + go func() { + switch { + case ui.cfg.UseLEHTTPS: + certMgr := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache("certs"), + HostPolicy: autocert.HostWhitelist(ui.cfg.Domain), + } + + log.Info("Starting GUI server on port 443 (https)") + ui.server = &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ":https", + Handler: ui.router, + TLSConfig: &tls.Config{ + GetCertificate: certMgr.GetCertificate, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + }, + } + + if err := ui.server.ListenAndServeTLS("", ""); err != nil { + log.Error(err) + } + case ui.cfg.NoGUITLS: + log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) + ui.server = &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + + if err := ui.server.ListenAndServe(); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + default: + log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) + ui.server = &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + + if err := ui.server.ListenAndServeTLS(ui.cfg.TLSCertFile, + ui.cfg.TLSKeyFile); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + } + }() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + ui.updateCacheAndNotifyWebsocketClients(ctx) + wg.Done() + }() + wg.Wait() +} From eed059117477f78e57799ea7847a206d054677b2 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 14 Sep 2023 20:34:04 -0500 Subject: [PATCH 4/5] gui: Split webserver run logic into separate method. This moves the code that starts the webserver listeners into a separate method that is invoked by run. It also removes the server field from the GUI type since it is only needed locally in the new method. --- internal/gui/gui.go | 135 +++++++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/internal/gui/gui.go b/internal/gui/gui.go index edc4dcfd..be2a12eb 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -94,7 +94,6 @@ type GUI struct { templates *template.Template cookieStore *sessions.CookieStore router *mux.Router - server *http.Server cache *Cache websocketServer *WebsocketServer } @@ -231,6 +230,75 @@ func NewGUI(cfg *Config) (*GUI, error) { return &ui, nil } +// runWebServer starts the web server according per the configuration options +// associated with the GUI instance. +// +// It must be run as a routine. +func (ui *GUI) runWebServer(ctx context.Context) { + switch { + case ui.cfg.UseLEHTTPS: + certMgr := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache("certs"), + HostPolicy: autocert.HostWhitelist(ui.cfg.Domain), + } + + log.Info("Starting GUI server on port 443 (https)") + server := &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ":https", + Handler: ui.router, + TLSConfig: &tls.Config{ + GetCertificate: certMgr.GetCertificate, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + }, + } + + if err := server.ListenAndServeTLS("", ""); err != nil { + log.Error(err) + } + case ui.cfg.NoGUITLS: + log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) + server := &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + + if err := server.ListenAndServe(); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + default: + log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) + server := &http.Server{ + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + + if err := server.ListenAndServeTLS(ui.cfg.TLSCertFile, + ui.cfg.TLSKeyFile); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + } +} + // updateCacheAndNotifyWebsocketClients periodically updates cached data and // pushes updates to any established websocket clients. // @@ -315,70 +383,7 @@ func (ui *GUI) updateCacheAndNotifyWebsocketClients(ctx context.Context) { // Run starts the user interface. func (ui *GUI) Run(ctx context.Context) { - go func() { - switch { - case ui.cfg.UseLEHTTPS: - certMgr := &autocert.Manager{ - Prompt: autocert.AcceptTOS, - Cache: autocert.DirCache("certs"), - HostPolicy: autocert.HostWhitelist(ui.cfg.Domain), - } - - log.Info("Starting GUI server on port 443 (https)") - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ":https", - Handler: ui.router, - TLSConfig: &tls.Config{ - GetCertificate: certMgr.GetCertificate, - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - }, - }, - } - - if err := ui.server.ListenAndServeTLS("", ""); err != nil { - log.Error(err) - } - case ui.cfg.NoGUITLS: - log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ui.cfg.GUIListen, - Handler: ui.router, - } - - if err := ui.server.ListenAndServe(); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } - default: - log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) - ui.server = &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ui.cfg.GUIListen, - Handler: ui.router, - } - - if err := ui.server.ListenAndServeTLS(ui.cfg.TLSCertFile, - ui.cfg.TLSKeyFile); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } - } - }() + go ui.runWebServer(ctx) var wg sync.WaitGroup wg.Add(1) From 9609c45d86e072f072d1e3a0d76d71e569b82443 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 14 Sep 2023 20:40:55 -0500 Subject: [PATCH 5/5] gui: Make webserver respect the context. This modifies the webserver creation logic to respect the context so it is shutdown in an orderly fashion when the context is canceled. It also updates the gui Run method to add the goroutine to the waitgroup now that it will be shutdown as expected. --- internal/gui/gui.go | 109 ++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/internal/gui/gui.go b/internal/gui/gui.go index be2a12eb..f422f36e 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -10,6 +10,7 @@ import ( "crypto/tls" "errors" "html/template" + "net" "net/http" "os" "path/filepath" @@ -235,6 +236,22 @@ func NewGUI(cfg *Config) (*GUI, error) { // // It must be run as a routine. func (ui *GUI) runWebServer(ctx context.Context) { + // Create base HTTP/S server configuration. + server := http.Server{ + // Use the provided context as the parent context for all requests to + // ensure handlers are able to react to both client disconnects as well + // as shutdown via the provided context. + BaseContext: func(l net.Listener) context.Context { + return ctx + }, + + WriteTimeout: time.Second * 30, + ReadTimeout: time.Second * 30, + IdleTimeout: time.Second * 30, + Addr: ui.cfg.GUIListen, + Handler: ui.router, + } + switch { case ui.cfg.UseLEHTTPS: certMgr := &autocert.Manager{ @@ -243,60 +260,50 @@ func (ui *GUI) runWebServer(ctx context.Context) { HostPolicy: autocert.HostWhitelist(ui.cfg.Domain), } - log.Info("Starting GUI server on port 443 (https)") - server := &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ":https", - Handler: ui.router, - TLSConfig: &tls.Config{ - GetCertificate: certMgr.GetCertificate, - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - }, + server.Addr = ":https" + server.TLSConfig = &tls.Config{ + GetCertificate: certMgr.GetCertificate, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, }, } - if err := server.ListenAndServeTLS("", ""); err != nil { - log.Error(err) - } + go func() { + log.Info("Starting GUI server on port 443 (https)") + if err := server.ListenAndServeTLS("", ""); err != nil { + log.Error(err) + } + }() + case ui.cfg.NoGUITLS: - log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) - server := &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ui.cfg.GUIListen, - Handler: ui.router, - } + go func() { + log.Infof("Starting GUI server on %s (http)", ui.cfg.GUIListen) + if err := server.ListenAndServe(); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + }() - if err := server.ListenAndServe(); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } default: - log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) - server := &http.Server{ - WriteTimeout: time.Second * 30, - ReadTimeout: time.Second * 30, - IdleTimeout: time.Second * 30, - Addr: ui.cfg.GUIListen, - Handler: ui.router, - } - - if err := server.ListenAndServeTLS(ui.cfg.TLSCertFile, - ui.cfg.TLSKeyFile); err != nil && - !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } + go func() { + log.Infof("Starting GUI server on %s (https)", ui.cfg.GUIListen) + if err := server.ListenAndServeTLS(ui.cfg.TLSCertFile, + ui.cfg.TLSKeyFile); err != nil && + !errors.Is(err, http.ErrServerClosed) { + log.Error(err) + } + }() } + + // Wait until the context is canceled and gracefully shutdown the server. + <-ctx.Done() + server.Shutdown(ctx) } // updateCacheAndNotifyWebsocketClients periodically updates cached data and @@ -383,10 +390,12 @@ func (ui *GUI) updateCacheAndNotifyWebsocketClients(ctx context.Context) { // Run starts the user interface. func (ui *GUI) Run(ctx context.Context) { - go ui.runWebServer(ctx) - var wg sync.WaitGroup - wg.Add(1) + wg.Add(2) + go func() { + ui.runWebServer(ctx) + wg.Done() + }() go func() { ui.updateCacheAndNotifyWebsocketClients(ctx) wg.Done()