diff --git a/cmd/explored/main.go b/cmd/explored/main.go index b1fa2d0d..1b271e04 100644 --- a/cmd/explored/main.go +++ b/cmd/explored/main.go @@ -69,9 +69,12 @@ var cfg = config.Config{ }, } -// stdoutFatalError prints an error message to stdout and exits with a 1 exit code. -func stdoutFatalError(msg string) { - fmt.Println(msg) +// checkFatalError prints an error message to stderr and exits with a 1 exit code. If err is nil, this is a no-op. +func checkFatalError(context string, err error) { + if err == nil { + return + } + os.Stderr.WriteString(fmt.Sprintf("%s: %s\n", context, err)) os.Exit(1) } @@ -89,19 +92,13 @@ func tryLoadConfig() { } f, err := os.Open(configPath) - if err != nil { - stdoutFatalError("failed to open config file: " + err.Error()) - return - } + checkFatalError("failed to open config file", err) defer f.Close() dec := yaml.NewDecoder(f) dec.KnownFields(true) - if err := dec.Decode(&cfg); err != nil { - fmt.Println("failed to decode config file:", err) - os.Exit(1) - } + checkFatalError("failed to decode config file", dec.Decode(&cfg)) } // jsonEncoder returns a zapcore.Encoder that encodes logs as JSON intended for @@ -178,94 +175,7 @@ func forwardUPNP(ctx context.Context, addr string, log *zap.Logger) string { return net.JoinHostPort(ip, portStr) } -func main() { - tryLoadConfig() - - flag.StringVar(&cfg.Directory, "dir", cfg.Directory, "directory to store node state in") - flag.StringVar(&cfg.HTTP.Address, "http", cfg.HTTP.Address, "address to serve API on") - flag.StringVar(&cfg.Consensus.Network, "network", cfg.Consensus.Network, "network to connect to") - flag.StringVar(&cfg.Syncer.Address, "addr", cfg.Syncer.Address, "p2p address to listen on") - flag.BoolVar(&cfg.Syncer.EnableUPNP, "upnp", cfg.Syncer.EnableUPNP, "attempt to forward ports and discover IP with UPnP") - flag.Parse() - - if flag.Arg(0) == "version" { - fmt.Println("explored", build.Version()) - fmt.Println("Commit:", build.Commit()) - fmt.Println("Build Date:", build.Time()) - return - } - - if err := os.MkdirAll(cfg.Directory, 0700); err != nil { - stdoutFatalError("failed to open log file: " + err.Error()) - return - } - - var logCores []zapcore.Core - if cfg.Log.StdOut.Enabled { - // if no log level is set for stdout, use the global log level - if cfg.Log.StdOut.Level == "" { - cfg.Log.StdOut.Level = cfg.Log.Level - } - - var encoder zapcore.Encoder - switch cfg.Log.StdOut.Format { - case "json": - encoder = jsonEncoder() - default: // stdout defaults to human - encoder = humanEncoder(cfg.Log.StdOut.EnableANSI) - } - - // create the stdout logger - level := parseLogLevel(cfg.Log.StdOut.Level) - logCores = append(logCores, zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), level)) - } - - if cfg.Log.File.Enabled { - // if no log level is set for file, use the global log level - if cfg.Log.File.Level == "" { - cfg.Log.File.Level = cfg.Log.Level - } - - // normalize log path - if cfg.Log.File.Path == "" { - cfg.Log.File.Path = filepath.Join(cfg.Directory, "explored.log") - } - - // configure file logging - var encoder zapcore.Encoder - switch cfg.Log.File.Format { - case "human": - encoder = humanEncoder(false) // disable colors in file log - default: // log file defaults to JSON - encoder = jsonEncoder() - } - - fileWriter, closeFn, err := zap.Open(cfg.Log.File.Path) - if err != nil { - stdoutFatalError("failed to open log file: " + err.Error()) - return - } - defer closeFn() - - // create the file logger - level := parseLogLevel(cfg.Log.File.Level) - logCores = append(logCores, zapcore.NewCore(encoder, zapcore.Lock(fileWriter), level)) - } - - var log *zap.Logger - if len(logCores) == 1 { - log = zap.New(logCores[0], zap.AddCaller()) - } else { - log = zap.New(zapcore.NewTee(logCores...), zap.AddCaller()) - } - defer log.Sync() - - // redirect stdlib log to zap - zap.RedirectStdLog(log.Named("stdlib")) - - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) - defer cancel() - +func runRootCmd(ctx context.Context, log *zap.Logger) error { var network *consensus.Network var genesisBlock types.Block @@ -285,36 +195,31 @@ func main() { bdb, err := coreutils.OpenBoltChainDB(filepath.Join(cfg.Directory, "consensus.db")) if err != nil { - log.Error("failed to open bolt database", zap.Error(err)) - return + return fmt.Errorf("failed to open bolt database: %w", err) } defer bdb.Close() dbstore, tipState, err := chain.NewDBStore(bdb, network, genesisBlock) if err != nil { - log.Error("failed to create chain store", zap.Error(err)) - return + return fmt.Errorf("failed to create chain store: %w", err) } cm := chain.NewManager(dbstore, tipState) store, err := sqlite.OpenDatabase(filepath.Join(cfg.Directory, "explored.sqlite3"), log.Named("sqlite3")) if err != nil { - log.Error("failed to open sqlite database", zap.Error(err)) - return + return fmt.Errorf("failed to open sqlite database: %w", err) } defer store.Close() syncerListener, err := net.Listen("tcp", cfg.Syncer.Address) if err != nil { - log.Error("failed to create listener", zap.Error(err)) - return + return fmt.Errorf("failed to create listener: %w", err) } defer syncerListener.Close() httpListener, err := net.Listen("tcp", cfg.HTTP.Address) if err != nil { - log.Error("failed to create listener", zap.Error(err)) - return + return fmt.Errorf("failed to create listener: %w", err) } defer httpListener.Close() @@ -334,8 +239,7 @@ func main() { ps, err := syncerutil.NewJSONPeerStore(filepath.Join(cfg.Directory, "peers.json")) if err != nil { - log.Error("failed to open peer store", zap.Error(err)) - return + return fmt.Errorf("failed to open peer store: %w", err) } for _, peer := range cfg.Syncer.Peers { ps.AddPeer(peer) @@ -352,8 +256,7 @@ func main() { e, err := explorer.NewExplorer(cm, store, cfg.Index.BatchSize, cfg.Scanner, log.Named("explorer")) if err != nil { - log.Error("failed to create explorer", zap.Error(err)) - return + return fmt.Errorf("failed to create explorer: %w", err) } timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 60*time.Second) defer timeoutCancel() @@ -383,8 +286,94 @@ func main() { <-ctx.Done() log.Info("shutting down") - go func() { - <-time.After(time.Minute) - log.Fatal("shutdown timed out") - }() + time.AfterFunc(3*time.Minute, func() { + log.Fatal("failed to shut down within 3 minutes") + }) + + return nil +} + +func main() { + tryLoadConfig() + + flag.StringVar(&cfg.Directory, "dir", cfg.Directory, "directory to store node state in") + flag.StringVar(&cfg.HTTP.Address, "http", cfg.HTTP.Address, "address to serve API on") + flag.StringVar(&cfg.Consensus.Network, "network", cfg.Consensus.Network, "network to connect to") + flag.StringVar(&cfg.Syncer.Address, "addr", cfg.Syncer.Address, "p2p address to listen on") + flag.BoolVar(&cfg.Syncer.EnableUPNP, "upnp", cfg.Syncer.EnableUPNP, "attempt to forward ports and discover IP with UPnP") + flag.Parse() + + if flag.Arg(0) == "version" { + fmt.Println("explored", build.Version()) + fmt.Println("Commit:", build.Commit()) + fmt.Println("Build Date:", build.Time()) + return + } + + checkFatalError("failed to open log file", os.MkdirAll(cfg.Directory, 0700)) + + var logCores []zapcore.Core + if cfg.Log.StdOut.Enabled { + // if no log level is set for stdout, use the global log level + if cfg.Log.StdOut.Level == "" { + cfg.Log.StdOut.Level = cfg.Log.Level + } + + var encoder zapcore.Encoder + switch cfg.Log.StdOut.Format { + case "json": + encoder = jsonEncoder() + default: // stdout defaults to human + encoder = humanEncoder(cfg.Log.StdOut.EnableANSI) + } + + // create the stdout logger + level := parseLogLevel(cfg.Log.StdOut.Level) + logCores = append(logCores, zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), level)) + } + + if cfg.Log.File.Enabled { + // if no log level is set for file, use the global log level + if cfg.Log.File.Level == "" { + cfg.Log.File.Level = cfg.Log.Level + } + + // normalize log path + if cfg.Log.File.Path == "" { + cfg.Log.File.Path = filepath.Join(cfg.Directory, "explored.log") + } + + // configure file logging + var encoder zapcore.Encoder + switch cfg.Log.File.Format { + case "human": + encoder = humanEncoder(false) // disable colors in file log + default: // log file defaults to JSON + encoder = jsonEncoder() + } + + fileWriter, closeFn, err := zap.Open(cfg.Log.File.Path) + checkFatalError("failed to open log file", err) + defer closeFn() + + // create the file logger + level := parseLogLevel(cfg.Log.File.Level) + logCores = append(logCores, zapcore.NewCore(encoder, zapcore.Lock(fileWriter), level)) + } + + var log *zap.Logger + if len(logCores) == 1 { + log = zap.New(logCores[0], zap.AddCaller()) + } else { + log = zap.New(zapcore.NewTee(logCores...), zap.AddCaller()) + } + defer log.Sync() + + // redirect stdlib log to zap + zap.RedirectStdLog(log.Named("stdlib")) + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + checkFatalError("daemon startup failed", runRootCmd(ctx, log)) }