diff --git a/cmd/flamenco-manager/main.go b/cmd/flamenco-manager/main.go index a2f56035..6b4b5ca4 100644 --- a/cmd/flamenco-manager/main.go +++ b/cmd/flamenco-manager/main.go @@ -77,11 +77,30 @@ func main() { return } - // The main context determines the lifetime of the application. All - // long-running goroutines need to keep an eye on this, and stop their work - // once it closes. - mainCtx, mainCtxCancel := context.WithCancel(context.Background()) + startFlamenco := true + for startFlamenco { + startFlamenco = runFlamencoManager() + // After the first run, the first-time wizard should not be forced any more. + // If the configuration is still incomplete it can still auto-trigger. + cliArgs.firstTimeWizard = false + + if startFlamenco { + log.Info(). + Str("version", appinfo.ApplicationVersion). + Str("os", runtime.GOOS). + Str("arch", runtime.GOARCH). + Msgf("restarting %v", appinfo.ApplicationName) + } + } + + log.Info().Msg("stopping the Flamenco Manager process") +} + +// runFlamencoManager starts the entire Flamenco Manager, and only returns after +// it has been completely shut down. +// Returns true if it should be restarted again. +func runFlamencoManager() bool { // Load configuration. configService := config.NewService() err := configService.Load() @@ -106,7 +125,7 @@ func main() { log.Error().Err(err).Msg("could not write configuration file") os.Exit(1) } - return + return false } // TODO: enable TLS via Let's Encrypt. @@ -149,6 +168,11 @@ func main() { configService.Get().WorkerTimeout, timeService, persist, taskStateMachine, logStorage, webUpdater) + // The main context determines the lifetime of the application. All + // long-running goroutines need to keep an eye on this, and stop their work + // once it closes. + mainCtx, mainCtxCancel := context.WithCancel(context.Background()) + installSignalHandler(mainCtxCancel) // Before doing anything new, clean up in case we made a mess in an earlier run. @@ -201,8 +225,16 @@ func main() { go openWebbrowser(mainCtx, urls[0]) } + // Allow the Flamenco API itself trigger a shutdown as well. + log.Debug().Msg("waiting for a shutdown request from Flamenco") + doRestart := flamenco.WaitForShutdown(mainCtx) + log.Info().Bool("willRestart", doRestart).Msg("going to shut down the service") + mainCtxCancel() + wg.Wait() - log.Info().Msg("shutdown complete") + log.Info().Bool("willRestart", doRestart).Msg("Flamenco Manager service shut down") + + return doRestart } func buildFlamencoAPI( @@ -215,7 +247,7 @@ func buildFlamencoAPI( webUpdater *webupdates.BiDirComms, lastRender *last_rendered.LastRenderedProcessor, localStorage local_storage.StorageInfo, -) api.ServerInterface { +) *api_impl.Flamenco { compiler, err := job_compilers.Load(timeService) if err != nil { log.Fatal().Err(err).Msg("error loading job compilers") diff --git a/internal/manager/api_impl/api_impl.go b/internal/manager/api_impl/api_impl.go index 12e0a8f9..52ef35e2 100644 --- a/internal/manager/api_impl/api_impl.go +++ b/internal/manager/api_impl/api_impl.go @@ -4,6 +4,7 @@ package api_impl // SPDX-License-Identifier: GPL-3.0-or-later import ( + "context" "fmt" "net/http" "strconv" @@ -30,6 +31,10 @@ type Flamenco struct { // the same task. It is also used for certain other queries, like // `MayWorkerRun` to prevent similar race conditions. taskSchedulerMutex sync.Mutex + + // done is closed by Flamenco when it wants the application to shut down and + // restart itself from scratch. + done chan struct{} } var _ api.ServerInterface = (*Flamenco)(nil) @@ -58,9 +63,29 @@ func NewFlamenco( clock: ts, lastRender: lr, localStorage: localStorage, + + done: make(chan struct{}), } } +// WaitForShutdown waits until Flamenco wants to shut down the application. +// Returns `true` when the application should restart. +// Returns `false` when the context closes. +func (f *Flamenco) WaitForShutdown(ctx context.Context) bool { + select { + case <-ctx.Done(): + return false + case <-f.done: + return true + } +} + +// requestShutdown closes the 'done' channel, signalling to callers of +// WaitForShutdown() that a shutdown is requested. +func (f *Flamenco) requestShutdown() { + close(f.done) +} + // sendAPIError wraps sending of an error in the Error format, and // handling the failure to marshal that. func sendAPIError(e echo.Context, code int, message string, args ...interface{}) error { diff --git a/internal/manager/api_impl/meta.go b/internal/manager/api_impl/meta.go index 950eaef3..42801040 100644 --- a/internal/manager/api_impl/meta.go +++ b/internal/manager/api_impl/meta.go @@ -292,6 +292,10 @@ func (f *Flamenco) SaveWizardConfig(e echo.Context) error { } logger.Info().Msg("first-time wizard: updating configuration") + + // Request the shutdown in a goroutine, so that this one can continue sending the response. + go f.requestShutdown() + return e.NoContent(http.StatusNoContent) }