Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gracefully handle startup errors #4383

Merged
merged 30 commits into from
Sep 22, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Gracefully handle startup errors
andig committed Sep 21, 2022
commit 46ee58eba682d82fe3bc9aeecb44475b15a613cf
147 changes: 91 additions & 56 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"errors"
"fmt"
"net/http"
_ "net/http/pprof" // pprof handler
@@ -12,6 +13,9 @@ import (
"time"

"github.com/evcc-io/evcc/cmd/shutdown"
"github.com/evcc-io/evcc/core"
"github.com/evcc-io/evcc/hems"
"github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/server"
"github.com/evcc-io/evcc/server/updater"
"github.com/evcc-io/evcc/util"
@@ -138,6 +142,17 @@ func Execute() {
}
}

var valueChan chan util.Param

func publish(key string, val any) {
valueChan <- util.Param{Key: key, Val: val}
}

func fatal(err error) {
log.FATAL.Println(err)
publish("fatal", err)
}

func run(cmd *cobra.Command, args []string) {
util.LogLevel(viper.GetString("log"), viper.GetStringMapString("levels"))
log.INFO.Printf("evcc %s", server.FormattedVersion())
@@ -150,6 +165,11 @@ func run(cmd *cobra.Command, args []string) {

util.LogLevel(viper.GetString("log"), viper.GetStringMapString("levels"))

// full http request log
if cmd.PersistentFlags().Lookup(flagHeaders).Changed {
request.LogHeaders = true
}

// network config
if viper.GetString("uri") != "" {
log.WARN.Println("`uri` is deprecated and will be ignored. Use `network` instead.")
@@ -161,33 +181,56 @@ func run(cmd *cobra.Command, args []string) {

log.INFO.Printf("listening at :%d", conf.Network.Port)

// setup environment
if err := configureEnvironment(conf); err != nil {
log.FATAL.Fatal(err)
// start broadcasting values
tee := new(util.Tee)

// value cache
cache := util.NewCache()
go cache.Run(pipe.NewDropper(ignoreErrors...).Pipe(tee.Attach()))

// create webserver
socketHub := server.NewSocketHub()
httpd := server.NewHTTPd(fmt.Sprintf(":%d", conf.Network.Port), socketHub)

// metrics
if viper.GetBool("metrics") {
httpd.Router().Handle("/metrics", promhttp.Handler())
}

// full http request log
if cmd.PersistentFlags().Lookup(flagHeaders).Changed {
request.LogHeaders = true
// pprof
if viper.GetBool("profile") {
httpd.Router().PathPrefix("/debug/").Handler(http.DefaultServeMux)
}

// setup loadpoints
cp.TrackVisitors() // track duplicate usage
// publish to UI
go socketHub.Run(tee.Attach(), cache)

// setup values channel
valueChan = make(chan util.Param)
go tee.Run(valueChan)

site, err := configureSiteAndLoadpoints(conf)
// setup environment
// if err := configureEnvironment(conf); err != nil {
err := configureEnvironment(conf)
if err == nil {
err = fmt.Errorf("bad things: %w", errors.New("foo"))
}
if err != nil {
log.FATAL.Fatal(err)
fatal(err)
}

// start broadcasting values
tee := &util.Tee{}
// setup loadpoints
cp.TrackVisitors() // track duplicate usage

// value cache
cache := util.NewCache()
go cache.Run(pipe.NewDropper(ignoreErrors...).Pipe(tee.Attach()))
var site *core.Site
if err == nil {
if site, err = configureSiteAndLoadpoints(conf); err != nil {
fatal(err)
}
}

// setup database
if conf.Influx.URL != "" {
if err == nil && conf.Influx.URL != "" {
configureDatabase(conf.Influx, site.LoadPoints(), tee.Attach())
}

@@ -197,11 +240,7 @@ func run(cmd *cobra.Command, args []string) {
go publisher.Run(site, pipe.NewDropper(ignoreMqtt...).Pipe(tee.Attach()))
}

// create webserver
socketHub := server.NewSocketHub()
httpd := server.NewHTTPd(fmt.Sprintf(":%d", conf.Network.Port), site, socketHub, cache)

// announce webserver on mDNS
// announce web server on mDNS
if strings.HasSuffix(conf.Network.Host, ".local") {
host := strings.TrimSuffix(conf.Network.Host, ".local")
if zc, err := zeroconf.RegisterProxy("EV Charge Controller", "_http._tcp", "local.", conf.Network.Port, host, nil, []string{}, nil); err == nil {
@@ -211,56 +250,52 @@ func run(cmd *cobra.Command, args []string) {
}
}

// metrics
if viper.GetBool("metrics") {
httpd.Router().Handle("/metrics", promhttp.Handler())
}

// pprof
if viper.GetBool("profile") {
httpd.Router().PathPrefix("/debug/").Handler(http.DefaultServeMux)
}

// start HEMS server
if conf.HEMS.Type != "" {
hems := configureHEMS(conf.HEMS, site, httpd)
go hems.Run()
if err == nil && conf.HEMS.Type != "" {
var hems hems.HEMS
if hems, err = configureHEMS(conf.HEMS, site, httpd); err == nil {
go hems.Run()
}
}

// publish to UI
go socketHub.Run(tee.Attach(), cache)

// setup values channel
valueChan := make(chan util.Param)
go tee.Run(valueChan)

// expose sponsor to UI
if sponsor.Subject != "" {
valueChan <- util.Param{Key: "sponsor", Val: sponsor.Subject}
// setup messaging
var pushChan chan push.Event
if err == nil {
pushChan, err = configureMessengers(conf.Messaging, cache)
}

// allow web access for vehicles
cp.webControl(conf.Network, httpd.Router(), valueChan)
// show main ui
if err == nil {
httpd.Site(site, cache)

// version check
go updater.Run(log, httpd, tee, valueChan)
// set channels
site.DumpConfig()
site.Prepare(valueChan, pushChan)

// capture log messages for UI
util.CaptureLogs(valueChan)
// version check
go updater.Run(log, httpd, tee, valueChan)

// setup messaging
pushChan := configureMessengers(conf.Messaging, cache)
// capture log messages for UI
util.CaptureLogs(valueChan)

// set channels
site.DumpConfig()
site.Prepare(valueChan, pushChan)
// expose sponsor to UI
if sponsor.Subject != "" {
publish("sponsor", sponsor.Subject)
}

// allow web access for vehicles
cp.webControl(conf.Network, httpd.Router(), valueChan)
}

stopC := make(chan struct{})
go shutdown.Run(stopC)

siteC := make(chan struct{})
go func() {
site.Run(stopC, conf.Interval)
// site may be nil if setup never completed
if site != nil {
site.Run(stopC, conf.Interval)
}
close(siteC)
}()

24 changes: 12 additions & 12 deletions cmd/setup.go
Original file line number Diff line number Diff line change
@@ -123,12 +123,12 @@ func configureJavascript(conf map[string]interface{}) error {
}

// setup HEMS
func configureHEMS(conf typedConfig, site *core.Site, httpd *server.HTTPd) hems.HEMS {
func configureHEMS(conf typedConfig, site *core.Site, httpd *server.HTTPd) (hems.HEMS, error) {
hems, err := hems.NewFromConfig(conf.Type, conf.Other, site, httpd)
if err != nil {
log.FATAL.Fatalf("failed configuring hems: %v", err)
err = fmt.Errorf("failed configuring hems: %w", err)
}
return hems
return hems, err
}

// setup EEBus
@@ -145,25 +145,25 @@ func configureEEBus(conf map[string]interface{}) error {
}

// setup messaging
func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Event {
notificationChan := make(chan push.Event, 1)
notificationHub, err := push.NewHub(conf.Events, cache)
func configureMessengers(conf messagingConfig, cache *util.Cache) (chan push.Event, error) {
messageChan := make(chan push.Event, 1)

messageHub, err := push.NewHub(conf.Events, cache)
if err != nil {
log.FATAL.Fatalf("failed configuring push services: %v", err)
return messageChan, fmt.Errorf("failed configuring push services: %w", err)
}

for _, service := range conf.Services {
impl, err := push.NewMessengerFromConfig(service.Type, service.Other)
if err != nil {
log.FATAL.Fatal(err)
log.FATAL.Fatalf("failed configuring messenger %s: %v", service.Type, err)
return messageChan, fmt.Errorf("failed configuring push service %s: %w", service.Type, err)
}
notificationHub.Add(impl)
messageHub.Add(impl)
}

go notificationHub.Run(notificationChan)
go messageHub.Run(messageChan)

return notificationChan
return messageChan, nil
}

func configureTariffs(conf tariffConfig) (tariff.Tariffs, error) {
67 changes: 37 additions & 30 deletions server/http.go
Original file line number Diff line number Diff line change
@@ -43,15 +43,7 @@ type HTTPd struct {
}

// NewHTTPd creates HTTP server with configured routes for loadpoint
func NewHTTPd(addr string, site site.API, hub *SocketHub, cache *util.Cache) *HTTPd {
routes := map[string]route{
"health": {[]string{"GET"}, "/health", healthHandler(site)},
"state": {[]string{"GET"}, "/state", stateHandler(cache)},
"buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoC, site.GetBufferSoC)},
"prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoC, site.GetPrioritySoC)},
"residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
}

func NewHTTPd(addr string, hub *SocketHub) *HTTPd {
router := mux.NewRouter().StrictSlash(true)

// websocket
@@ -61,11 +53,36 @@ func NewHTTPd(addr string, site site.API, hub *SocketHub, cache *util.Cache) *HT
static := router.PathPrefix("/").Subrouter()
static.Use(handlers.CompressHandler)

static.HandleFunc("/", indexHandler(site))
// static.HandleFunc("/", indexHandler(site))
static.HandleFunc("/", indexHandler())
for _, dir := range []string{"assets", "meta"} {
static.PathPrefix("/" + dir).Handler(http.FileServer(http.FS(Assets)))
}

srv := &HTTPd{
Server: &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorLog: log.ERROR,
},
}
srv.SetKeepAlivesEnabled(true)

return srv
}

// Router returns the main router
func (s *HTTPd) Router() *mux.Router {
return s.Handler.(*mux.Router)
}

// Main configures the site
func (s *HTTPd) Site(site site.API, cache *util.Cache) {
router := s.Server.Handler.(*mux.Router)

// api
api := router.PathPrefix("/api").Subrouter()
api.Use(jsonHandler)
@@ -75,13 +92,21 @@ func NewHTTPd(addr string, site site.API, hub *SocketHub, cache *util.Cache) *HT
))

// site api
routes := map[string]route{
"health": {[]string{"GET"}, "/health", healthHandler(site)},
"state": {[]string{"GET"}, "/state", stateHandler(cache)},
"buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoC, site.GetBufferSoC)},
"prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoC, site.GetPrioritySoC)},
"residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
}

for _, r := range routes {
api.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
}

// loadpoint api
for id, lp := range site.LoadPoints() {
lpAPI := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id)).Subrouter()
loadpoint := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id)).Subrouter()

routes := map[string]route{
"mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)},
@@ -99,26 +124,8 @@ func NewHTTPd(addr string, site site.API, hub *SocketHub, cache *util.Cache) *HT
}

for _, r := range routes {
lpAPI.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
loadpoint.Methods(r.Methods...).Path(r.Pattern).Handler(r.HandlerFunc)
}
}

srv := &HTTPd{
Server: &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorLog: log.ERROR,
},
}
srv.SetKeepAlivesEnabled(true)

return srv
}

// Router returns the main router
func (s *HTTPd) Router() *mux.Router {
return s.Handler.(*mux.Router)
}
11 changes: 6 additions & 5 deletions server/http_handler.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,8 @@ import (
"github.com/gorilla/mux"
)

func indexHandler(site site.API) http.HandlerFunc {
// func indexHandler(site site.API) http.HandlerFunc {
func indexHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=UTF-8")

@@ -34,9 +35,9 @@ func indexHandler(site site.API) http.HandlerFunc {
}

if err := t.Execute(w, map[string]interface{}{
"Version": Version,
"Commit": Commit,
"Configured": len(site.LoadPoints()),
"Version": Version,
"Commit": Commit,
// "Configured": len(site.LoadPoints()),
}); err != nil {
log.ERROR.Println("httpd: failed to render main page:", err.Error())
}
@@ -70,7 +71,7 @@ func jsonError(w http.ResponseWriter, status int, err error) {
// healthHandler returns current charge mode
func healthHandler(site site.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !site.Healthy() {
if site == nil || !site.Healthy() {
w.WriteHeader(http.StatusInternalServerError)
return
}