From 430c91c535257e75bb00b24c6ceb081b586933de Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 8 Oct 2022 18:53:11 +0200 Subject: [PATCH] Refactor application shutdown (#4757) --- cmd/charger.go | 8 +-- cmd/charger_ramp.go | 8 +-- cmd/configure.go | 12 +--- cmd/helper.go | 93 +++++++++++++++++++++++++++++++ cmd/meter.go | 3 + cmd/root.go | 115 ++++++--------------------------------- cmd/setup.go | 5 ++ cmd/shutdown/shutdown.go | 13 +---- cmd/vehicle.go | 11 ++-- server/uds.go | 8 ++- 10 files changed, 137 insertions(+), 139 deletions(-) create mode 100644 cmd/helper.go diff --git a/cmd/charger.go b/cmd/charger.go index 4f47cc538..6d4a66104 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/util" "github.com/spf13/cobra" @@ -57,9 +56,6 @@ func runCharger(cmd *cobra.Command, args []string) { log.FATAL.Fatal(err) } - stopC := make(chan struct{}) - go shutdown.Run(stopC) - chargers := cp.chargers if len(args) == 1 { name := args[0] @@ -146,6 +142,6 @@ func runCharger(cmd *cobra.Command, args []string) { } } - close(stopC) - <-shutdown.Done() + // wait for shutdown + <-shutdownDoneC() } diff --git a/cmd/charger_ramp.go b/cmd/charger_ramp.go index 2aebdad9b..6a51ab04d 100644 --- a/cmd/charger_ramp.go +++ b/cmd/charger_ramp.go @@ -7,7 +7,6 @@ import ( "time" "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/util" "github.com/spf13/cobra" @@ -91,9 +90,6 @@ func runChargerRamp(cmd *cobra.Command, args []string) { log.FATAL.Fatal(err) } - stopC := make(chan struct{}) - go shutdown.Run(stopC) - chargers := cp.chargers if len(args) == 1 { name := args[0] @@ -121,6 +117,6 @@ func runChargerRamp(cmd *cobra.Command, args []string) { ramp(c, digits, delay) } - close(stopC) - <-shutdown.Done() + // wait for shutdown + <-shutdownDoneC() } diff --git a/cmd/configure.go b/cmd/configure.go index 5772cf392..640b24300 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -7,7 +7,6 @@ import ( "syscall" "github.com/evcc-io/evcc/cmd/configure" - "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/util" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -53,24 +52,15 @@ func runConfigure(cmd *cobra.Command, args []string) { util.LogLevel(viper.GetString("log"), nil) - stopC := make(chan struct{}) - go shutdown.Run(stopC) - // catch signals go func() { signalC := make(chan os.Signal, 1) signal.Notify(signalC, os.Interrupt, syscall.SIGTERM) - <-signalC // wait for signal - close(stopC) // signal loop to end - - <-shutdown.Done() + <-signalC // wait for signal os.Exit(1) }() impl.Run(log, lang, advanced, expand, category) - - close(stopC) - <-shutdown.Done() } diff --git a/cmd/helper.go b/cmd/helper.go new file mode 100644 index 000000000..282ad6632 --- /dev/null +++ b/cmd/helper.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/evcc-io/evcc/cmd/shutdown" +) + +// unwrap converts a wrapped error into slice of strings +func unwrap(err error) (res []string) { + for err != nil { + inner := errors.Unwrap(err) + if inner == nil { + res = append(res, err.Error()) + } else { + cur := strings.TrimSuffix(err.Error(), ": "+inner.Error()) + cur = strings.TrimSuffix(cur, inner.Error()) + res = append(res, strings.TrimSpace(cur)) + } + err = inner + } + return +} + +// redact redacts a configuration string +func redact(src string) string { + secrets := []string{ + "url", "uri", "host", "broker", "mac", // infrastructure + "sponsortoken", "plant", // global settings + "user", "password", "pin", // users + "token", "access", "refresh", // tokens + "ain", "id", "secret", "serial", "deviceid", "machineid", // devices + "vin"} // vehicles + return regexp. + MustCompile(fmt.Sprintf(`\b(%s)\b.*?:.*`, strings.Join(secrets, "|"))). + ReplaceAllString(src, "$1: *****") +} + +func publishErrorInfo(cfgFile string, err error) { + if cfgFile != "" { + file, pathErr := filepath.Abs(cfgFile) + if pathErr != nil { + file = cfgFile + } + publish("file", file) + + if src, fileErr := os.ReadFile(cfgFile); fileErr != nil { + log.ERROR.Println("could not open config file:", fileErr) + } else { + publish("config", redact(string(src))) + + // find line number + if match := regexp.MustCompile(`yaml: line (\d+):`).FindStringSubmatch(err.Error()); len(match) == 2 { + if line, err := strconv.Atoi(match[1]); err == nil { + publish("line", line) + } + } + } + } + + publish("fatal", unwrap(err)) +} + +// fatal logs a fatal error and runs shutdown functions before terminating +func fatal(err error) { + log.FATAL.Println(err) + <-shutdownDoneC() + os.Exit(1) +} + +// shutdownDoneC returns a channel that closes when shutdown has completed +func shutdownDoneC() <-chan struct{} { + doneC := make(chan struct{}) + go shutdown.Cleanup(doneC) + return doneC +} + +// exitWhenDone waits for shutdown to complete with timeout +func exitWhenDone(timeout time.Duration) { + select { + case <-shutdownDoneC(): // wait for shutdown + case <-time.After(timeout): + } + + os.Exit(1) +} diff --git a/cmd/meter.go b/cmd/meter.go index 4981b9e23..0953b2935 100644 --- a/cmd/meter.go +++ b/cmd/meter.go @@ -59,4 +59,7 @@ func runMeter(cmd *cobra.Command, args []string) { for name, v := range meters { d.DumpWithHeader(name, v) } + + // wait for shutdown + <-shutdownDoneC() } diff --git a/cmd/root.go b/cmd/root.go index a303291cd..70b916695 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,15 +7,11 @@ import ( _ "net/http/pprof" // pprof handler "os" "os/signal" - "path/filepath" - "regexp" - "strconv" "strings" "sync" "syscall" "time" - "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/server" @@ -101,59 +97,6 @@ func publish(key string, val any) { valueChan <- util.Param{Key: key, Val: val} } -func unwrap(err error) (res []string) { - for err != nil { - inner := errors.Unwrap(err) - if inner == nil { - res = append(res, err.Error()) - } else { - cur := strings.TrimSuffix(err.Error(), ": "+inner.Error()) - cur = strings.TrimSuffix(cur, inner.Error()) - res = append(res, strings.TrimSpace(cur)) - } - err = inner - } - return -} - -func redact(src string) string { - secrets := []string{ - "url", "uri", "host", "broker", "mac", // infrastructure - "sponsortoken", "plant", // global settings - "user", "password", "pin", // users - "token", "access", "refresh", // tokens - "ain", "id", "secret", "serial", "deviceid", "machineid", // devices - "vin"} // vehicles - return regexp. - MustCompile(fmt.Sprintf(`\b(%s)\b.*?:.*`, strings.Join(secrets, "|"))). - ReplaceAllString(src, "$1: *****") -} - -func publishErrorInfo(cfgFile string, err error) { - if cfgFile != "" { - file, pathErr := filepath.Abs(cfgFile) - if pathErr != nil { - file = cfgFile - } - publish("file", file) - - if src, fileErr := os.ReadFile(cfgFile); fileErr != nil { - log.ERROR.Println("could not open config file:", fileErr) - } else { - publish("config", redact(string(src))) - - // find line number - if match := regexp.MustCompile(`yaml: line (\d+):`).FindStringSubmatch(err.Error()); len(match) == 2 { - if line, err := strconv.Atoi(match[1]); err == nil { - publish("line", line) - } - } - } - } - - publish("fatal", unwrap(err)) -} - func runRoot(cmd *cobra.Command, args []string) { util.LogLevel(viper.GetString("log"), viper.GetStringMapString("levels")) log.INFO.Printf("evcc %s", server.FormattedVersion()) @@ -209,11 +152,6 @@ func runRoot(cmd *cobra.Command, args []string) { err = configureEnvironment(cmd, conf) } - // setup session log - if err == nil && conf.Database.Dsn != "" { - err = configureDatabase(conf.Database) - } - // setup site and loadpoints var site *core.Site if err == nil { @@ -248,10 +186,18 @@ func runRoot(cmd *cobra.Command, args []string) { pushChan, err = configureMessengers(conf.Messaging, cache) } + // run shutdown functions on stop + var once sync.Once stopC := make(chan struct{}) - go shutdown.Run(stopC) - siteC := make(chan struct{}) + // catch signals + go func() { + signalC := make(chan os.Signal, 1) + signal.Notify(signalC, os.Interrupt, syscall.SIGTERM) + + <-signalC // wait for signal + once.Do(func() { close(stopC) }) // signal loop to end + }() // show main ui if err == nil { @@ -277,14 +223,12 @@ func runRoot(cmd *cobra.Command, args []string) { go func() { site.Run(stopC, conf.Interval) - close(siteC) }() } else { - var once sync.Once httpd.RegisterShutdownHandler(func() { once.Do(func() { log.FATAL.Println("evcc was stopped. OS should restart the service. Or restart manually.") - close(siteC) + close(stopC) // signal loop to end }) }) @@ -296,42 +240,15 @@ func runRoot(cmd *cobra.Command, args []string) { publishErrorInfo(cfgFile, err) - go func() { - select { - case <-time.After(rebootDelay): - case <-siteC: - } - os.Exit(1) - }() + // wait for shutdown + go exitWhenDone(rebootDelay) } // uds health check listener - go server.HealthListener(site, siteC) + go server.HealthListener(site) - // catch signals - go func() { - signalC := make(chan os.Signal, 1) - signal.Notify(signalC, os.Interrupt, syscall.SIGTERM) - - <-signalC // wait for signal - close(stopC) // signal loop to end - - exitC := make(chan struct{}) - wg := new(sync.WaitGroup) - wg.Add(2) - - // wait for main loop and shutdown functions to finish - go func() { <-shutdown.Done(); wg.Done() }() - go func() { <-siteC; wg.Done() }() - go func() { wg.Wait(); close(exitC) }() - - select { - case <-exitC: // wait for loop to end - case <-time.NewTimer(conf.Interval).C: // wait max 1 period - } - - os.Exit(1) - }() + // wait for shutdown + go exitWhenDone(conf.Interval) log.FATAL.Println(httpd.ListenAndServe()) } diff --git a/cmd/setup.go b/cmd/setup.go index 56bdda0bd..274501a5f 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -73,6 +73,11 @@ func configureEnvironment(cmd *cobra.Command, conf config) (err error) { err = sponsor.ConfigureSponsorship(conf.SponsorToken) } + // setup persistence + if err == nil && conf.Database.Dsn != "" { + err = configureDatabase(conf.Database) + } + // setup telemetry if err == nil && conf.Telemetry { err = telemetry.Create(sponsor.Token, conf.Plant) diff --git a/cmd/shutdown/shutdown.go b/cmd/shutdown/shutdown.go index 392b702f0..44974a8c8 100644 --- a/cmd/shutdown/shutdown.go +++ b/cmd/shutdown/shutdown.go @@ -7,7 +7,6 @@ import ( var ( mu sync.Mutex handlers = make([]func(), 0) - exitC = make(chan struct{}) ) // Register registers a function for executing on application shutdown @@ -17,9 +16,8 @@ func Register(cb func()) { mu.Unlock() } -// Run executes the registered shutdown functions when the stop channel closes -func Run(stopC <-chan struct{}) { - <-stopC +// Cleanup executes the registered shutdown functions when the stop channel closes +func Cleanup(doneC chan struct{}) { wg := new(sync.WaitGroup) mu.Lock() @@ -34,10 +32,5 @@ func Run(stopC <-chan struct{}) { mu.Unlock() wg.Wait() - close(exitC) -} - -// Done returns a readable channel that closes when all registered functions have completed -func Done() <-chan struct{} { - return exitC + close(doneC) } diff --git a/cmd/vehicle.go b/cmd/vehicle.go index 5af8f9504..52cac5624 100644 --- a/cmd/vehicle.go +++ b/cmd/vehicle.go @@ -31,21 +31,21 @@ func runVehicle(cmd *cobra.Command, args []string) { // load config if err := loadConfigFile(&conf); err != nil { - log.FATAL.Fatal(err) + fatal(err) } // setup environment if err := configureEnvironment(cmd, conf); err != nil { - log.FATAL.Fatal(err) + fatal(err) } // select single vehicle if err := selectByName(cmd, &conf.Vehicles); err != nil { - log.FATAL.Fatal(err) + fatal(err) } if err := cp.configureVehicles(conf); err != nil { - log.FATAL.Fatal(err) + fatal(err) } vehicles := cp.vehicles @@ -104,4 +104,7 @@ func runVehicle(cmd *cobra.Command, args []string) { d.DumpWithHeader(name, v) } } + + // wait for shutdown + <-shutdownDoneC() } diff --git a/server/uds.go b/server/uds.go index dc8190382..df75a6d33 100644 --- a/server/uds.go +++ b/server/uds.go @@ -8,6 +8,7 @@ import ( "net/http" "os" + "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/core/site" ) @@ -27,7 +28,7 @@ func removeIfExists(file string) { } // HealthListener attaches listener to unix domain socket and runs listener -func HealthListener(site site.API, exitC <-chan struct{}) { +func HealthListener(site site.API) { removeIfExists(SocketPath) l, err := net.Listen("unix", SocketPath) @@ -42,6 +43,7 @@ func HealthListener(site site.API, exitC <-chan struct{}) { go func() { _ = httpd.Serve(l) }() - <-exitC - removeIfExists(SocketPath) // cleanup + shutdown.Register(func() { + removeIfExists(SocketPath) // cleanup + }) }