package server import ( "errors" "fmt" "io" "net" "os" "os/signal" "path" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" tmcfg "github.com/tendermint/tendermint/config" tmlog "github.com/tendermint/tendermint/libs/log" dbm "github.com/tendermint/tm-db" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/server/config" "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/version" ) // DONTCOVER // ServerContextKey defines the context key used to retrieve a server.Context from // a command's Context. const ServerContextKey = sdk.ContextKey("server.context") // server context type Context struct { Viper *viper.Viper Config *tmcfg.Config Logger tmlog.Logger } // ErrorCode contains the exit code for server exit. type ErrorCode struct { Code int } func (e ErrorCode) Error() string { return strconv.Itoa(e.Code) } func NewDefaultContext() *Context { return NewContext( viper.New(), tmcfg.DefaultConfig(), ZeroLogWrapper{log.Logger}, ) } func NewContext(v *viper.Viper, config *tmcfg.Config, logger tmlog.Logger) *Context { return &Context{v, config, logger} } func bindFlags(basename string, cmd *cobra.Command, v *viper.Viper) (err error) { defer func() { recover() }() cmd.Flags().VisitAll(func(f *pflag.Flag) { // Environment variables can't have dashes in them, so bind them to their equivalent // keys with underscores, e.g. --favorite-color to STING_FAVORITE_COLOR err = v.BindEnv(f.Name, fmt.Sprintf("%s_%s", basename, strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_")))) if err != nil { panic(err) } err = v.BindPFlag(f.Name, f) if err != nil { panic(err) } // Apply the viper config value to the flag when the flag is not set and viper has a value if !f.Changed && v.IsSet(f.Name) { val := v.Get(f.Name) err = cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)) if err != nil { panic(err) } } }) return } // InterceptConfigsPreRunHandler performs a pre-run function for the root daemon // application command. It will create a Viper literal and a default server // Context. The server Tendermint configuration will either be read and parsed // or created and saved to disk, where the server Context is updated to reflect // the Tendermint configuration. It takes custom app config template and config // settings to create a custom Tendermint configuration. If the custom template // is empty, it uses default-template provided by the server. The Viper literal // is used to read and parse the application configuration. Command handlers can // fetch the server Context to get the Tendermint configuration or to get access // to Viper. func InterceptConfigsPreRunHandler(cmd *cobra.Command, customAppConfigTemplate string, customAppConfig interface{}) error { serverCtx := NewDefaultContext() // Get the executable name and configure the viper instance so that environmental // variables are checked based off that name. The underscore character is used // as a separator executableName, err := os.Executable() if err != nil { return err } basename := path.Base(executableName) // Configure the viper instance serverCtx.Viper.BindPFlags(cmd.Flags()) serverCtx.Viper.BindPFlags(cmd.PersistentFlags()) serverCtx.Viper.SetEnvPrefix(basename) serverCtx.Viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) serverCtx.Viper.AutomaticEnv() // intercept configuration files, using both Viper instances separately config, err := interceptConfigs(serverCtx.Viper, customAppConfigTemplate, customAppConfig) if err != nil { return err } // return value is a tendermint configuration object serverCtx.Config = config if err = bindFlags(basename, cmd, serverCtx.Viper); err != nil { return err } var logWriter io.Writer if strings.ToLower(serverCtx.Viper.GetString(flags.FlagLogFormat)) == tmlog.LogFormatPlain { logWriter = zerolog.ConsoleWriter{Out: os.Stderr} } else { logWriter = os.Stderr } logLvlStr := serverCtx.Viper.GetString(flags.FlagLogLevel) logLvl, err := zerolog.ParseLevel(logLvlStr) if err != nil { return fmt.Errorf("failed to parse log level (%s): %w", logLvlStr, err) } serverCtx.Logger = ZeroLogWrapper{zerolog.New(logWriter).Level(logLvl).With().Timestamp().Logger()} return SetCmdServerContext(cmd, serverCtx) } // GetServerContextFromCmd returns a Context from a command or an empty Context // if it has not been set. func GetServerContextFromCmd(cmd *cobra.Command) *Context { if v := cmd.Context().Value(ServerContextKey); v != nil { serverCtxPtr := v.(*Context) return serverCtxPtr } return NewDefaultContext() } // SetCmdServerContext sets a command's Context value to the provided argument. func SetCmdServerContext(cmd *cobra.Command, serverCtx *Context) error { v := cmd.Context().Value(ServerContextKey) if v == nil { return errors.New("server context not set") } serverCtxPtr := v.(*Context) *serverCtxPtr = *serverCtx return nil } // interceptConfigs parses and updates a Tendermint configuration file or // creates a new one and saves it. It also parses and saves the application // configuration file. The Tendermint configuration file is parsed given a root // Viper object, whereas the application is parsed with the private package-aware // viperCfg object. func interceptConfigs(rootViper *viper.Viper, customAppTemplate string, customConfig interface{}) (*tmcfg.Config, error) { rootDir := rootViper.GetString(flags.FlagHome) configPath := filepath.Join(rootDir, "config") tmCfgFile := filepath.Join(configPath, "config.toml") conf := tmcfg.DefaultConfig() switch _, err := os.Stat(tmCfgFile); { case os.IsNotExist(err): tmcfg.EnsureRoot(rootDir) if err = conf.ValidateBasic(); err != nil { return nil, fmt.Errorf("error in config file: %v", err) } conf.RPC.PprofListenAddress = "localhost:6060" conf.P2P.RecvRate = 5120000 conf.P2P.SendRate = 5120000 conf.Consensus.TimeoutCommit = 5 * time.Second tmcfg.WriteConfigFile(rootDir, conf) case err != nil: return nil, err default: rootViper.SetConfigType("toml") rootViper.SetConfigName("config") rootViper.AddConfigPath(configPath) if err := rootViper.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read in %s: %w", tmCfgFile, err) } } // Read into the configuration whatever data the viper instance has for it. // This may come from the configuration file above but also any of the other // sources viper uses. if err := rootViper.Unmarshal(conf); err != nil { return nil, err } conf.SetRoot(rootDir) appCfgFilePath := filepath.Join(configPath, "app.toml") if _, err := os.Stat(appCfgFilePath); os.IsNotExist(err) { if customAppTemplate != "" { config.SetConfigTemplate(customAppTemplate) if err = rootViper.Unmarshal(&customConfig); err != nil { return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err) } config.WriteConfigFile(appCfgFilePath, customConfig) } else { appConf, err := config.ParseConfig(rootViper) if err != nil { return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err) } config.WriteConfigFile(appCfgFilePath, appConf) } } rootViper.SetConfigType("toml") rootViper.SetConfigName("app") rootViper.AddConfigPath(configPath) if err := rootViper.MergeInConfig(); err != nil { return nil, fmt.Errorf("failed to merge configuration: %w", err) } return conf, nil } // add server commands func AddCommands(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, appExport types.AppExporter, addStartFlags types.ModuleInitFlags) { tendermintCmd := &cobra.Command{ Use: "tendermint", Short: "Tendermint subcommands", } tendermintCmd.AddCommand( ShowNodeIDCmd(), ShowValidatorCmd(), ShowAddressCmd(), VersionCmd(), ) startCmd := StartCmd(appCreator, defaultNodeHome) addStartFlags(startCmd) rootCmd.AddCommand( startCmd, UnsafeResetAllCmd(), tendermintCmd, ExportCmd(appExport, defaultNodeHome), version.NewVersionCommand(), ) } // https://stackoverflow.com/questions/23558425/how-do-i-get-the-local-ip-address-in-go // TODO there must be a better way to get external IP func ExternalIP() (string, error) { ifaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range ifaces { if skipInterface(iface) { continue } addrs, err := iface.Addrs() if err != nil { return "", err } for _, addr := range addrs { ip := addrToIP(addr) if ip == nil || ip.IsLoopback() { continue } ip = ip.To4() if ip == nil { continue // not an ipv4 address } return ip.String(), nil } } return "", errors.New("are you connected to the network?") } // TrapSignal traps SIGINT and SIGTERM and terminates the server correctly. func TrapSignal(cleanupFunc func()) { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigs if cleanupFunc != nil { cleanupFunc() } exitCode := 128 switch sig { case syscall.SIGINT: exitCode += int(syscall.SIGINT) case syscall.SIGTERM: exitCode += int(syscall.SIGTERM) } os.Exit(exitCode) }() } // WaitForQuitSignals waits for SIGINT and SIGTERM and returns. func WaitForQuitSignals() ErrorCode { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) sig := <-sigs return ErrorCode{Code: int(sig.(syscall.Signal)) + 128} } func skipInterface(iface net.Interface) bool { if iface.Flags&net.FlagUp == 0 { return true // interface down } if iface.Flags&net.FlagLoopback != 0 { return true // loopback interface } return false } func addrToIP(addr net.Addr) net.IP { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } return ip } func openDB(rootDir string) (dbm.DB, error) { dataDir := filepath.Join(rootDir, "data") return sdk.NewLevelDB("application", dataDir) } func openTraceWriter(traceWriterFile string) (w io.Writer, err error) { if traceWriterFile == "" { return } return os.OpenFile( traceWriterFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666, ) }