2018-04-05 03:09:02 -07:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2020-07-05 09:56:17 -07:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-07-29 07:53:14 -07:00
|
|
|
"io"
|
2018-04-23 12:15:50 -07:00
|
|
|
"net"
|
2018-04-05 03:09:02 -07:00
|
|
|
"os"
|
2018-11-02 06:44:40 -07:00
|
|
|
"os/signal"
|
2020-10-16 06:56:10 -07:00
|
|
|
"path"
|
2018-06-25 09:33:07 -07:00
|
|
|
"path/filepath"
|
2020-10-14 08:13:48 -07:00
|
|
|
"strconv"
|
2020-10-16 06:56:10 -07:00
|
|
|
"strings"
|
2018-11-02 06:44:40 -07:00
|
|
|
"syscall"
|
2018-11-09 16:08:35 -08:00
|
|
|
"time"
|
2018-04-05 03:09:02 -07:00
|
|
|
|
2020-12-03 15:17:21 -08:00
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/rs/zerolog/log"
|
2018-04-05 03:09:02 -07:00
|
|
|
"github.com/spf13/cobra"
|
2021-01-28 17:58:44 -08:00
|
|
|
"github.com/spf13/pflag"
|
2018-04-05 03:09:02 -07:00
|
|
|
"github.com/spf13/viper"
|
2020-07-05 09:56:17 -07:00
|
|
|
tmcfg "github.com/tendermint/tendermint/config"
|
2020-12-03 15:17:21 -08:00
|
|
|
tmlog "github.com/tendermint/tendermint/libs/log"
|
2020-07-29 07:53:14 -07:00
|
|
|
dbm "github.com/tendermint/tm-db"
|
2018-12-10 06:27:25 -08:00
|
|
|
|
2019-05-28 01:44:04 -07:00
|
|
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
2018-12-10 06:27:25 -08:00
|
|
|
"github.com/cosmos/cosmos-sdk/server/config"
|
2020-07-27 10:57:15 -07:00
|
|
|
"github.com/cosmos/cosmos-sdk/server/types"
|
2020-07-07 08:40:46 -07:00
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
2018-12-10 06:27:25 -08:00
|
|
|
"github.com/cosmos/cosmos-sdk/version"
|
2018-04-05 03:09:02 -07:00
|
|
|
)
|
|
|
|
|
2020-07-05 09:56:17 -07:00
|
|
|
// DONTCOVER
|
|
|
|
|
2020-07-07 08:40:46 -07:00
|
|
|
// ServerContextKey defines the context key used to retrieve a server.Context from
|
|
|
|
// a command's Context.
|
|
|
|
const ServerContextKey = sdk.ContextKey("server.context")
|
|
|
|
|
2018-04-18 21:49:24 -07:00
|
|
|
// server context
|
2018-04-05 03:31:33 -07:00
|
|
|
type Context struct {
|
2020-07-05 09:56:17 -07:00
|
|
|
Viper *viper.Viper
|
|
|
|
Config *tmcfg.Config
|
2020-12-03 15:17:21 -08:00
|
|
|
Logger tmlog.Logger
|
2018-04-05 03:31:33 -07:00
|
|
|
}
|
|
|
|
|
2020-10-14 08:13:48 -07:00
|
|
|
// ErrorCode contains the exit code for server exit.
|
|
|
|
type ErrorCode struct {
|
|
|
|
Code int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e ErrorCode) Error() string {
|
|
|
|
return strconv.Itoa(e.Code)
|
|
|
|
}
|
|
|
|
|
2018-04-05 04:16:20 -07:00
|
|
|
func NewDefaultContext() *Context {
|
2020-12-03 15:17:21 -08:00
|
|
|
return NewContext(
|
|
|
|
viper.New(),
|
|
|
|
tmcfg.DefaultConfig(),
|
|
|
|
ZeroLogWrapper{log.Logger},
|
|
|
|
)
|
2018-04-05 04:16:20 -07:00
|
|
|
}
|
|
|
|
|
2020-12-03 15:17:21 -08:00
|
|
|
func NewContext(v *viper.Viper, config *tmcfg.Config, logger tmlog.Logger) *Context {
|
2020-07-05 09:56:17 -07:00
|
|
|
return &Context{v, config, logger}
|
2018-04-05 03:31:33 -07:00
|
|
|
}
|
|
|
|
|
2021-01-28 17:58:44 -08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-07-07 08:40:46 -07:00
|
|
|
// 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. 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) error {
|
|
|
|
serverCtx := NewDefaultContext()
|
2020-10-16 06:56:10 -07:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2020-11-03 09:14:56 -08:00
|
|
|
|
2020-10-16 06:56:10 -07:00
|
|
|
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()
|
|
|
|
|
2020-12-03 15:17:21 -08:00
|
|
|
// intercept configuration files, using both Viper instances separately
|
2020-10-16 06:56:10 -07:00
|
|
|
config, err := interceptConfigs(serverCtx.Viper)
|
2020-07-07 08:40:46 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-12-03 15:17:21 -08:00
|
|
|
|
|
|
|
// return value is a tendermint configuration object
|
2020-10-16 06:56:10 -07:00
|
|
|
serverCtx.Config = config
|
2021-01-28 17:58:44 -08:00
|
|
|
if err = bindFlags(basename, cmd, serverCtx.Viper); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2020-12-03 15:17:21 -08:00
|
|
|
var logWriter io.Writer
|
|
|
|
if strings.ToLower(serverCtx.Viper.GetString(flags.FlagLogFormat)) == tmcfg.LogFormatPlain {
|
|
|
|
logWriter = zerolog.ConsoleWriter{Out: os.Stderr}
|
|
|
|
} else {
|
|
|
|
logWriter = os.Stderr
|
2020-07-07 08:40:46 -07:00
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2020-12-03 15:17:21 -08:00
|
|
|
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)
|
2020-07-07 08:40:46 -07:00
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2020-12-03 15:17:21 -08:00
|
|
|
serverCtx.Logger = ZeroLogWrapper{zerolog.New(logWriter).Level(logLvl).With().Timestamp().Logger()}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2020-07-07 08:40:46 -07:00
|
|
|
return SetCmdServerContext(cmd, serverCtx)
|
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2020-07-07 08:40:46 -07:00
|
|
|
// 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
|
2018-04-05 03:09:02 -07:00
|
|
|
}
|
2020-07-07 08:40:46 -07:00
|
|
|
|
|
|
|
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
|
2018-04-05 03:09:02 -07:00
|
|
|
}
|
2018-06-25 09:33:07 -07:00
|
|
|
|
2020-07-05 09:56:17 -07:00
|
|
|
// 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.
|
2020-10-16 06:56:10 -07:00
|
|
|
func interceptConfigs(rootViper *viper.Viper) (*tmcfg.Config, error) {
|
2020-07-05 09:56:17 -07:00
|
|
|
rootDir := rootViper.GetString(flags.FlagHome)
|
|
|
|
configPath := filepath.Join(rootDir, "config")
|
2021-02-22 03:51:35 -08:00
|
|
|
tmCfgFile := filepath.Join(configPath, "config.toml")
|
2020-07-05 09:56:17 -07:00
|
|
|
|
|
|
|
conf := tmcfg.DefaultConfig()
|
|
|
|
|
2021-02-22 03:51:35 -08:00
|
|
|
switch _, err := os.Stat(tmCfgFile); {
|
2020-10-20 05:41:44 -07:00
|
|
|
case os.IsNotExist(err):
|
2020-07-05 09:56:17 -07:00
|
|
|
tmcfg.EnsureRoot(rootDir)
|
|
|
|
|
|
|
|
if err = conf.ValidateBasic(); err != nil {
|
|
|
|
return nil, fmt.Errorf("error in config file: %v", err)
|
|
|
|
}
|
2018-07-16 09:43:10 -07:00
|
|
|
|
2020-09-08 09:33:53 -07:00
|
|
|
conf.RPC.PprofListenAddress = "localhost:6060"
|
2018-07-16 09:43:10 -07:00
|
|
|
conf.P2P.RecvRate = 5120000
|
|
|
|
conf.P2P.SendRate = 5120000
|
2018-11-09 16:08:35 -08:00
|
|
|
conf.Consensus.TimeoutCommit = 5 * time.Second
|
2021-02-22 03:51:35 -08:00
|
|
|
tmcfg.WriteConfigFile(tmCfgFile, conf)
|
2020-10-20 05:41:44 -07:00
|
|
|
|
|
|
|
case err != nil:
|
|
|
|
return nil, err
|
|
|
|
|
|
|
|
default:
|
2020-07-05 09:56:17 -07:00
|
|
|
rootViper.SetConfigType("toml")
|
|
|
|
rootViper.SetConfigName("config")
|
|
|
|
rootViper.AddConfigPath(configPath)
|
2021-02-22 03:51:35 -08:00
|
|
|
|
2020-07-05 09:56:17 -07:00
|
|
|
if err := rootViper.ReadInConfig(); err != nil {
|
2021-02-22 03:51:35 -08:00
|
|
|
return nil, fmt.Errorf("failed to read in %s: %w", tmCfgFile, err)
|
2020-07-05 09:56:17 -07:00
|
|
|
}
|
2018-07-16 09:43:10 -07:00
|
|
|
}
|
2018-09-19 08:25:52 -07:00
|
|
|
|
2021-02-22 03:51:35 -08:00
|
|
|
// 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.
|
2020-10-16 06:56:10 -07:00
|
|
|
if err := rootViper.Unmarshal(conf); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-02-22 03:51:35 -08:00
|
|
|
|
2020-07-20 09:42:46 -07:00
|
|
|
conf.SetRoot(rootDir)
|
|
|
|
|
2021-02-22 03:51:35 -08:00
|
|
|
appCfgFilePath := filepath.Join(configPath, "app.toml")
|
|
|
|
if _, err := os.Stat(appCfgFilePath); os.IsNotExist(err) {
|
2020-10-16 06:56:10 -07:00
|
|
|
appConf, err := config.ParseConfig(rootViper)
|
2020-07-05 09:56:17 -07:00
|
|
|
if err != nil {
|
2021-02-22 03:51:35 -08:00
|
|
|
return nil, fmt.Errorf("failed to parse %s: %w", appCfgFilePath, err)
|
2020-07-05 09:56:17 -07:00
|
|
|
}
|
|
|
|
|
2021-02-22 03:51:35 -08:00
|
|
|
config.WriteConfigFile(appCfgFilePath, appConf)
|
2018-09-19 08:25:52 -07:00
|
|
|
}
|
|
|
|
|
2020-10-16 06:56:10 -07:00
|
|
|
rootViper.SetConfigType("toml")
|
|
|
|
rootViper.SetConfigName("app")
|
|
|
|
rootViper.AddConfigPath(configPath)
|
2021-02-22 03:51:35 -08:00
|
|
|
|
|
|
|
if err := rootViper.MergeInConfig(); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to merge configuration: %w", err)
|
2020-07-05 09:56:17 -07:00
|
|
|
}
|
2018-09-19 08:25:52 -07:00
|
|
|
|
2020-07-05 09:56:17 -07:00
|
|
|
return conf, nil
|
2018-06-25 09:33:07 -07:00
|
|
|
}
|
2018-04-05 03:24:53 -07:00
|
|
|
|
2018-04-18 21:49:24 -07:00
|
|
|
// add server commands
|
2020-11-02 11:10:14 -08:00
|
|
|
func AddCommands(rootCmd *cobra.Command, defaultNodeHome string, appCreator types.AppCreator, appExport types.AppExporter, addStartFlags types.ModuleInitFlags) {
|
2018-05-31 18:38:15 -07:00
|
|
|
tendermintCmd := &cobra.Command{
|
2018-05-28 15:00:37 -07:00
|
|
|
Use: "tendermint",
|
|
|
|
Short: "Tendermint subcommands",
|
|
|
|
}
|
|
|
|
|
2018-05-31 18:38:15 -07:00
|
|
|
tendermintCmd.AddCommand(
|
2020-07-07 08:40:46 -07:00
|
|
|
ShowNodeIDCmd(),
|
|
|
|
ShowValidatorCmd(),
|
|
|
|
ShowAddressCmd(),
|
|
|
|
VersionCmd(),
|
2018-05-28 15:00:37 -07:00
|
|
|
)
|
2020-11-02 11:10:14 -08:00
|
|
|
startCmd := StartCmd(appCreator, defaultNodeHome)
|
|
|
|
addStartFlags(startCmd)
|
2018-05-28 15:00:37 -07:00
|
|
|
|
|
|
|
rootCmd.AddCommand(
|
2020-11-02 11:10:14 -08:00
|
|
|
startCmd,
|
2020-07-07 08:40:46 -07:00
|
|
|
UnsafeResetAllCmd(),
|
2019-05-28 01:44:04 -07:00
|
|
|
flags.LineBreak,
|
2018-05-31 18:38:15 -07:00
|
|
|
tendermintCmd,
|
2020-08-06 07:58:41 -07:00
|
|
|
ExportCmd(appExport, defaultNodeHome),
|
2019-05-28 01:44:04 -07:00
|
|
|
flags.LineBreak,
|
2020-07-07 10:20:09 -07:00
|
|
|
version.NewVersionCommand(),
|
2018-04-05 03:24:53 -07:00
|
|
|
)
|
|
|
|
}
|
2018-04-21 19:26:46 -07:00
|
|
|
|
2018-04-23 12:15:50 -07:00
|
|
|
// 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
|
2018-10-10 15:45:41 -07:00
|
|
|
func ExternalIP() (string, error) {
|
2018-04-23 12:15:50 -07:00
|
|
|
ifaces, err := net.Interfaces()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-04-23 12:15:50 -07:00
|
|
|
for _, iface := range ifaces {
|
2018-07-07 19:34:46 -07:00
|
|
|
if skipInterface(iface) {
|
|
|
|
continue
|
2018-04-23 12:15:50 -07:00
|
|
|
}
|
|
|
|
addrs, err := iface.Addrs()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-04-23 12:15:50 -07:00
|
|
|
for _, addr := range addrs {
|
2018-07-07 19:34:46 -07:00
|
|
|
ip := addrToIP(addr)
|
2018-04-23 12:15:50 -07:00
|
|
|
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?")
|
|
|
|
}
|
2018-07-07 19:34:46 -07:00
|
|
|
|
2018-11-02 06:44:40 -07:00
|
|
|
// 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)
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-11-02 06:44:40 -07:00
|
|
|
go func() {
|
|
|
|
sig := <-sigs
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2019-05-10 19:27:11 -07:00
|
|
|
if cleanupFunc != nil {
|
|
|
|
cleanupFunc()
|
|
|
|
}
|
|
|
|
exitCode := 128
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-11-02 06:44:40 -07:00
|
|
|
switch sig {
|
|
|
|
case syscall.SIGINT:
|
2019-05-10 19:27:11 -07:00
|
|
|
exitCode += int(syscall.SIGINT)
|
|
|
|
case syscall.SIGTERM:
|
|
|
|
exitCode += int(syscall.SIGTERM)
|
2018-11-02 06:44:40 -07:00
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2019-05-10 19:27:11 -07:00
|
|
|
os.Exit(exitCode)
|
2018-11-02 06:44:40 -07:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2020-10-14 08:13:48 -07:00
|
|
|
// 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}
|
|
|
|
}
|
|
|
|
|
2018-07-07 19:34:46 -07:00
|
|
|
func skipInterface(iface net.Interface) bool {
|
|
|
|
if iface.Flags&net.FlagUp == 0 {
|
|
|
|
return true // interface down
|
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-07-07 19:34:46 -07:00
|
|
|
if iface.Flags&net.FlagLoopback != 0 {
|
|
|
|
return true // loopback interface
|
|
|
|
}
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-07-07 19:34:46 -07:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func addrToIP(addr net.Addr) net.IP {
|
|
|
|
var ip net.IP
|
2020-05-02 12:26:59 -07:00
|
|
|
|
2018-07-07 19:34:46 -07:00
|
|
|
switch v := addr.(type) {
|
|
|
|
case *net.IPNet:
|
|
|
|
ip = v.IP
|
|
|
|
case *net.IPAddr:
|
|
|
|
ip = v.IP
|
|
|
|
}
|
|
|
|
return ip
|
|
|
|
}
|
2020-07-29 07:53:14 -07:00
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
}
|