Refactor application shutdown (#4757)
This commit is contained in:
parent
4881aaa095
commit
430c91c535
10 changed files with 137 additions and 139 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
93
cmd/helper.go
Normal file
93
cmd/helper.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -59,4 +59,7 @@ func runMeter(cmd *cobra.Command, args []string) {
|
|||
for name, v := range meters {
|
||||
d.DumpWithHeader(name, v)
|
||||
}
|
||||
|
||||
// wait for shutdown
|
||||
<-shutdownDoneC()
|
||||
}
|
||||
|
|
|
|||
115
cmd/root.go
115
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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue