feat: Add cosmovisor run command (#10316)

* [10285]: Move the Run function into a new cmd/run.go and have main call the renamed/updated RunCosmovisorCommand function.

* [10285]: Shorten the run warning to a single line and log it at the start and end of a run.

* [10285]: Make the version.go funcs public. Allow for --version. Limit help request to just the first argument.

* [10285]: Create a LogConfigOrError that logs the config or config errors in a standard way.

* [10285]: Output config info/errors with the help command.

* [10285]: Update the Run command to use the new config/error logging.

* [10285]: Only provide the first arg to the cmd tester funcs, since that's all that really matters.

* [10285]: Tweak a function comment.

* [10285]: For the version command, run the binary with version too.

* [10285]: Trim whitespace from the first argument before checking if it's any commands.

* [10285]: Update and add some unit tests.

* [10285]: In GetConfigFromEnv, make sure the cfg isn't null before trying to use it.

* [10285]: Add some unit tests for LogConfigOrError.

* [10285]: Allow for the poll interval to be defined as a duration.

* [10285]: Add changelog line.

* [10285]: Update the README to reflect the addition of the run action.

* [10285]: slight tweak to changlog.

* [10285]: Add a couple lines to the help text about getting help from the configured binary.

* [10285]: Remove a println from a unit test.

* [10285]: Put the help command first in the README.

Co-authored-by: Robert Zaremba <robert@zaremba.ch>
This commit is contained in:
Daniel Wedul 2021-10-18 09:32:24 -06:00 committed by GitHub
parent 99e2e0f060
commit 573d6b236e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 442 additions and 179 deletions

View File

@ -36,6 +36,14 @@ Ref: https://keepachangelog.com/en/1.0.0/
## [Unreleased]
### Features
+ [\#10285](https://github.com/cosmos/cosmos-sdk/pull/10316) Added `run` action.
### Deprecated
+ [\#10285](https://github.com/cosmos/cosmos-sdk/pull/10316) Running `cosmovisor` without the `run` argument.
## v1.0.0 2021-09-30
### Features

View File

@ -4,9 +4,9 @@
#### Design
Cosmovisor is designed to be used as a wrapper for an `Cosmos SDK` app:
* it will pass all arguments to the associated app (configured by `DAEMON_NAME` env variable).
Running `cosmovisor arg1 arg2 ....` will run `app arg1 arg2 ...`;
Cosmovisor is designed to be used as a wrapper for a `Cosmos SDK` app:
* it will pass arguments to the associated app (configured by `DAEMON_NAME` env variable).
Running `cosmovisor run arg1 arg2 ....` will run `app arg1 arg2 ...`;
* it will manage an app by restarting and upgrading if needed;
* it is configured using environment variables, not positional arguments.
@ -34,7 +34,14 @@ go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@latest
### Command Line Arguments And Environment Variables
All arguments passed to `cosmovisor` will be passed to the application binary (as a subprocess). `cosmovisor` will return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor` cannot accept any command-line arguments other than those available to the application binary, nor will it print anything to output other than what is printed by the application binary.
The first argument passed to `cosmovisor` is the action for `cosmovisor` to take. Options are:
* `help`, `--help`, or `-h` - Output `cosmovisor` help information and check your `cosmovisor` configuration.
* `run` - Run the configured binary using the rest of the provided arguments.
* `version`, or `--version` - Output the `cosmovisor` version and also run the binary with the `version` argument.
All arguments passed to `cosmovisor run` will be passed to the application binary (as a subprocess). `cosmovisor` will return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor run` cannot accept any command-line arguments other than those available to the application binary.
*Note: Use of `cosmovisor` without one of the action arguments is deprecated. For backwards compatability, if the first argument is not an action argument, `run` is assumed. However, this fallback might be removed in future versions, so it is recommended that you always provide `run`.
`cosmovisor` reads its configuration from environment variables:
@ -42,11 +49,10 @@ All arguments passed to `cosmovisor` will be passed to the application binary (a
* `DAEMON_NAME` is the name of the binary itself (e.g. `gaiad`, `regend`, `simd`, etc.).
* `DAEMON_ALLOW_DOWNLOAD_BINARIES` (*optional*), if set to `true`, will enable auto-downloading of new binaries (for security reasons, this is intended for full nodes rather than validators). By default, `cosmovisor` will not auto-download new binaries.
* `DAEMON_RESTART_AFTER_UPGRADE` (*optional*, default = `true`), if `true`, restarts the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade. Otherwise (`false`), `cosmovisor` stops running after an upgrade and requires the system administrator to manually restart it. Note restart is only after the upgrade and does not auto-restart the subprocess after an error occurs.
* `DAEMON_POLL_INTERVAL` is the interval length in milliseconds for polling the upgrade plan file. Default: 300.
* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `false`, backs up the data before trying the upgrade. Otherwise (`true`), upgrades directly without performing a backup. The default value of false is useful and recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option `UNSAFE_SKIP_BACKUP=false`.
* `DAEMON_POLL_INTERVAL` is the interval length for polling the upgrade plan file. The value can either be a number (in milliseconds) or a duration (e.g. `1s`). Default: 300 milliseconds.
* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `true`, upgrades directly without performing a backup. Otherwise (`false`, default) backs up the data before trying the upgrade. The default value of false is useful and recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option `UNSAFE_SKIP_BACKUP=false`.
* `DAEMON_PREUPGRADE_MAX_RETRIES` (defaults to `0`). The maximum number of times to call `pre-upgrade` in the application after exit status of `31`. After the maximum number of retries, cosmovisor fails the upgrade.
### Folder Layout
`$DAEMON_HOME/cosmovisor` is expected to belong completely to `cosmovisor` and the subprocesses that are controlled by it. The folder content is organized as follows:
@ -91,22 +97,6 @@ In order to support downloadable binaries, a tarball for each upgrade binary wil
The `DAEMON` specific code and operations (e.g. tendermint config, the application db, syncing blocks, etc.) all work as expected. The application binaries' directives such as command-line flags and environment variables also work as expected.
### Commands
Because Cosmovisor is meant to be used as a wrapper for a Cosmos SDK application, it does not require many commands.
To determine the version of Cosmovisor, run the following command:
```
cosmovisor version
```
The output of the `cosmovisor version` command shows the version of the Cosmos SDK application and the version of Cosmovisor:
```
Cosmovisor Version: v0.1.0-85-g65baacac0
0.43.0-beta1-319-ge3aec1840
```
### Detecting Upgrades
`cosmovisor` is polling the `$DAEMON_HOME/data/upgrade-info.json` file for new upgrade instructions. The file is created by the x/upgrade module in `BeginBlocker` when an upgrade is detected and the blockchain reaches the upgrade height.
@ -278,7 +268,7 @@ cp ./build/simd $DAEMON_HOME/cosmovisor/upgrades/test1/bin
Start `cosmosvisor`:
```
cosmovisor start
cosmovisor run start
```
Open a new terminal window and submit an upgrade proposal along with a deposit and a vote (these commands must be run within 20 seconds of each other):

View File

@ -11,6 +11,8 @@ import (
"strings"
"time"
"github.com/rs/zerolog"
cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors"
)
@ -143,13 +145,18 @@ func GetConfigFromEnv() (*Config, error) {
interval := os.Getenv(EnvInterval)
if interval != "" {
switch i, e := strconv.ParseUint(interval, 10, 32); {
case e != nil:
errs = append(errs, fmt.Errorf("invalid %s: %w", EnvInterval, err))
case i == 0:
errs = append(errs, fmt.Errorf("invalid %s: cannot be 0", EnvInterval))
default:
cfg.PollInterval = time.Millisecond * time.Duration(i)
var intervalUInt uint64
intervalUInt, err = strconv.ParseUint(interval, 10, 32)
if err == nil {
cfg.PollInterval = time.Millisecond * time.Duration(intervalUInt)
} else {
cfg.PollInterval, err = time.ParseDuration(interval)
}
switch {
case err != nil:
errs = append(errs, fmt.Errorf("invalid %s: could not parse \"%s\" into either a duration or uint (milliseconds)", EnvInterval, interval))
case cfg.PollInterval <= 0:
errs = append(errs, fmt.Errorf("invalid %s: must be greater than 0", EnvInterval))
}
} else {
cfg.PollInterval = 300 * time.Millisecond
@ -168,6 +175,24 @@ func GetConfigFromEnv() (*Config, error) {
return cfg, nil
}
// LogConfigOrError logs either the config details or the error.
func LogConfigOrError(logger zerolog.Logger, cfg *Config, cerr error) {
switch {
case cerr != nil:
switch err := cerr.(type) {
case *cverrors.MultiError:
logger.Error().Msg("multiple configuration errors found:")
for i, e := range err.GetErrors() {
logger.Error().Err(e).Msg(fmt.Sprintf(" %d:", i+1))
}
default:
logger.Error().Err(cerr).Msg("configuration error:")
}
case cfg != nil:
logger.Info().Msg("Configuration is valid:\n" + cfg.DetailString())
}
}
// validate returns an error if this config is invalid.
// it enforces Home/cosmovisor is a valid directory and exists,
// and that Name is set

View File

@ -1,12 +1,15 @@
package cosmovisor
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -451,6 +454,18 @@ func (s *argsTestSuite) TestGetConfigFromEnv() {
expectedCfg: newConfig(absPath, "testname", false, false, false, 987, 1),
expectedErrCount: 0,
},
{
name: "poll interval 1s",
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "1s", "1"},
expectedCfg: newConfig(absPath, "testname", false, false, false, 1000, 1),
expectedErrCount: 0,
},
{
name: "poll interval -3m",
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "-3m", "1"},
expectedCfg: nil,
expectedErrCount: 1,
},
// EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries
{
name: "prepupgrade max retries bad",
@ -497,3 +512,86 @@ func (s *argsTestSuite) TestGetConfigFromEnv() {
})
}
}
func (s *argsTestSuite) TestLogConfigOrError() {
cfg := &Config{
Home: "/no/place/like/it",
Name: "cosmotestvisor",
AllowDownloadBinaries: true,
RestartAfterUpgrade: true,
PollInterval: 999,
UnsafeSkipBackup: false,
PreupgradeMaxRetries: 20,
}
errNormal := fmt.Errorf("this is a single error")
errs := []error{
fmt.Errorf("multi-error error 1"),
fmt.Errorf("multi-error error 2"),
fmt.Errorf("multi-error error 3"),
}
errMulti := errors.FlattenErrors(errs...)
makeTestLogger := func(testName string, out io.Writer) zerolog.Logger {
output := zerolog.ConsoleWriter{Out: out, TimeFormat: time.Kitchen, NoColor: true}
return zerolog.New(output).With().Str("test", testName).Timestamp().Logger()
}
tests := []struct {
name string
cfg *Config
err error
contains []string
notcontains []string
}{
{
name: "normal error",
cfg: nil,
err: errNormal,
contains: []string{"configuration error", errNormal.Error()}, // TODO: Fix this.
notcontains: nil,
},
{
name: "multi error",
cfg: nil,
err: errMulti,
contains: []string{"multiple configuration errors found", errs[0].Error(), errs[1].Error(), errs[2].Error()},
notcontains: nil,
},
{
name: "config",
cfg: cfg,
err: nil,
contains: []string{"Configuration is valid", cfg.DetailString()},
notcontains: nil,
},
{
name: "error and config - no config details",
cfg: cfg,
err: errNormal,
contains: []string{"error"},
notcontains: []string{"Configuration is valid", EnvName, cfg.Home}, // Just some spot checks.
},
{
name: "nil nil - no output",
cfg: nil,
err: nil,
contains: nil,
notcontains: []string{" "},
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
var b bytes.Buffer
logger := makeTestLogger(tc.name, &b)
LogConfigOrError(logger, tc.cfg, tc.err)
output := b.String()
for _, expected := range tc.contains {
assert.Contains(t, output, expected)
}
for _, unexpected := range tc.notcontains {
assert.NotContains(t, output, unexpected)
}
})
}
}

View File

@ -3,33 +3,34 @@ package cmd
import (
"fmt"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/cosmos/cosmos-sdk/cosmovisor"
)
// HelpArgs are the strings that indicate a cosmovisor help command.
var HelpArgs = []string{"help", "--help", "-h"}
// ShouldGiveHelp checks the env and provided args to see if help is needed or being requested.
// Help is needed if either cosmovisor.EnvName and/or cosmovisor.EnvHome env vars aren't set.
// Help is requested if any args are "help", "--help", or "-h".
func ShouldGiveHelp(args []string) bool {
if len(os.Getenv(cosmovisor.EnvName)) == 0 || len(os.Getenv(cosmovisor.EnvHome)) == 0 {
return true
}
if len(args) == 0 {
return false
}
for _, arg := range args {
if strings.EqualFold(arg, "help") || strings.EqualFold(arg, "--help") || strings.EqualFold(arg, "-h") {
return true
}
}
return false
// Help is requested if the first arg is "help", "--help", or "-h".
func ShouldGiveHelp(arg string) bool {
return isOneOf(arg, HelpArgs) || len(os.Getenv(cosmovisor.EnvName)) == 0 || len(os.Getenv(cosmovisor.EnvHome)) == 0
}
// DoHelp outputs help text
func DoHelp() {
// Not using the logger for this output because the header and footer look weird for help text.
fmt.Println(GetHelpText())
// Check the config and output details or any errors.
// Not using the cosmovisor.Logger in order to ignore any level it might have set,
// and also to not have any of the extra parameters in the output.
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.Kitchen}
logger := zerolog.New(output).With().Timestamp().Logger()
cfg, err := cosmovisor.GetConfigFromEnv()
cosmovisor.LogConfigOrError(logger, cfg, err)
}
// GetHelpText creates the help text multi-line string.
@ -45,5 +46,8 @@ and restart the App.
Configuration of Cosmovisor is done through environment variables, which are
documented in: https://github.com/cosmos/cosmos-sdk/tree/master/cosmovisor/README.md
To get help for the configured binary:
cosmovisor run help
`, cosmovisor.EnvName, cosmovisor.EnvHome)
}

View File

@ -170,13 +170,13 @@ func (s *HelpTestSuite) TestShouldGiveHelpEnvVars() {
s.T().Run(tc.name, func(t *testing.T) {
prepEnv(t, cosmovisor.EnvHome, tc.envHome)
prepEnv(t, cosmovisor.EnvName, tc.envName)
actual := ShouldGiveHelp(nil)
actual := ShouldGiveHelp("not-a-help-arg")
assert.Equal(t, tc.expected, actual)
})
}
}
func (s HelpTestSuite) TestShouldGiveHelpArgs() {
func (s HelpTestSuite) TestShouldGiveHelpArg() {
initialEnv := s.clearEnv()
defer s.setEnv(nil, initialEnv)
@ -184,79 +184,59 @@ func (s HelpTestSuite) TestShouldGiveHelpArgs() {
tests := []struct {
name string
args []string
arg string
expected bool
}{
{
name: "nil args",
args: nil,
name: "empty string",
arg: "",
expected: false,
},
{
name: "empty args",
args: []string{},
name: "random",
arg: "random",
expected: false,
},
{
name: "one arg random",
args: []string{"random"},
name: "help",
arg: "help",
expected: true,
},
{
name: "-h",
arg: "-h",
expected: true,
},
{
name: "--help",
arg: "--help",
expected: true,
},
{
name: "help weird casing",
arg: "hELP",
expected: true,
},
{
name: "version",
arg: "version",
expected: false,
},
{
name: "five args random",
args: []string{"random1", "--random2", "-r", "random4", "-random5"},
name: "--version",
arg: "--version",
expected: false,
},
{
name: "one arg help",
args: []string{"help"},
expected: true,
},
{
name: " two args help first",
args: []string{"help", "arg2"},
expected: true,
},
{
name: "two args help second",
args: []string{"arg1", "help"},
expected: true,
},
{
name: "one arg -h",
args: []string{"-h"},
expected: true,
},
{
name: "two args -h first",
args: []string{"-h", "arg2"},
expected: true,
},
{
name: "two args -h second",
args: []string{"arg1", "-h"},
expected: true,
},
{
name: "one arg --help",
args: []string{"--help"},
expected: true,
},
{
name: "two args --help first",
args: []string{"--help", "arg2"},
expected: true,
},
{
name: "two args --help second",
args: []string{"arg1", "--help"},
expected: true,
name: "run",
arg: "run",
expected: false,
},
}
for _, tc := range tests {
s.T().Run(tc.name, func(t *testing.T) {
actual := ShouldGiveHelp(tc.args)
s.T().Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) {
actual := ShouldGiveHelp(tc.arg)
assert.Equal(t, tc.expected, actual)
})
}

View File

@ -1,12 +1,41 @@
package cmd
// RunCosmovisorCommands executes cosmosvisor commands e.g `cosmovisor version`
// Returned boolean is whether or not execution should continue.
func RunCosmovisorCommands(args []string) {
switch {
case ShouldGiveHelp(args):
DoHelp()
case isVersionCommand(args):
printVersion()
import (
"strings"
"github.com/cosmos/cosmos-sdk/cosmovisor"
)
// RunCosmovisorCommand executes the desired cosmovisor command.
func RunCosmovisorCommand(args []string) error {
arg0 := ""
if len(args) > 0 {
arg0 = strings.TrimSpace(args[0])
}
switch {
case ShouldGiveHelp(arg0):
DoHelp()
return nil
case IsVersionCommand(arg0):
PrintVersion()
return Run([]string{"version"})
case IsRunCommand(arg0):
return Run(args[1:])
}
warnRun := func() {
cosmovisor.Logger.Warn().Msg("Use of cosmovisor without the 'run' command is deprecated. Use: cosmovisor run [args]")
}
warnRun()
defer warnRun()
return Run(args)
}
// isOneOf returns true if the given arg equals one of the provided options (ignoring case).
func isOneOf(arg string, options []string) bool {
for _, opt := range options {
if strings.EqualFold(arg, opt) {
return true
}
}
return false
}

View File

@ -0,0 +1,40 @@
package cmd
import (
"os"
"github.com/cosmos/cosmos-sdk/cosmovisor"
)
// RunArgs are the strings that indicate a cosmovisor run command.
var RunArgs = []string{"run"}
// IsRunCommand checks if the given args indicate that a run is desired.
func IsRunCommand(arg string) bool {
return isOneOf(arg, RunArgs)
}
// Run runs the configured program with the given args and monitors it for upgrades.
func Run(args []string) error {
cfg, cerr := cosmovisor.GetConfigFromEnv()
cosmovisor.LogConfigOrError(cosmovisor.Logger, cfg, cerr)
if cerr != nil {
return cerr
}
launcher, err := cosmovisor.NewLauncher(cfg)
if err != nil {
return err
}
doUpgrade, err := launcher.Run(args, os.Stdout, os.Stderr)
// if RestartAfterUpgrade, we launch after a successful upgrade (only condition LaunchProcess returns nil)
for cfg.RestartAfterUpgrade && err == nil && doUpgrade {
cosmovisor.Logger.Info().Str("app", cfg.Name).Msg("upgrade detected, relaunching")
doUpgrade, err = launcher.Run(args, os.Stdout, os.Stderr)
}
if doUpgrade && err == nil {
cosmovisor.Logger.Info().Msg("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.")
}
return err
}

View File

@ -0,0 +1,76 @@
package cmd
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestIsRunCommand(t *testing.T) {
cases := []struct {
name string
arg string
expected bool
}{
{
name: "empty string",
arg: "",
expected: false,
},
{
name: "random",
arg: "random",
expected: false,
},
{
name: "run",
arg: "run",
expected: true,
},
{
name: "run weird casing",
arg: "RUn",
expected: true,
},
{
name: "--run",
arg: "--run",
expected: false,
},
{
name: "help",
arg: "help",
expected: false,
},
{
name: "-h",
arg: "-h",
expected: false,
},
{
name: "--help",
arg: "--help",
expected: false,
},
{
name: "version",
arg: "version",
expected: false,
},
{
name: "--version",
arg: "--version",
expected: false,
},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) {
actual := IsRunCommand(tc.arg)
require.Equal(t, tc.expected, actual)
})
}
}
// TODO: Write tests for func Run(args []string) error

View File

@ -2,14 +2,20 @@ package cmd
import (
"fmt"
"strings"
)
// Version represents Cosmovisor version value. Set during build
var Version string
func isVersionCommand(args []string) bool {
return len(args) == 1 && strings.EqualFold(args[0], "version")
// VersionArgs is the strings that indicate a cosmovisor version command.
var VersionArgs = []string{"version", "--version"}
// IsVersionCommand checks if the given args indicate that the version is being requested.
func IsVersionCommand(arg string) bool {
return isOneOf(arg, VersionArgs)
}
func printVersion() { fmt.Println("Cosmovisor Version: ", Version) }
// PrintVersion prints the cosmovisor version.
func PrintVersion() {
fmt.Println("Cosmovisor Version: ", Version)
}

View File

@ -1,6 +1,7 @@
package cmd
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
@ -8,33 +9,77 @@ import (
func TestIsVersionCommand(t *testing.T) {
cases := []struct {
name string
args []string
expectRes bool
}{{
name: "valid args - lowercase",
args: []string{"version"},
expectRes: true,
}, {
name: "typo",
args: []string{"vrsion"},
expectRes: false,
}, {
name: "non version command",
args: []string{"start"},
expectRes: false,
}, {
name: "incorrect format",
args: []string{"start", "version"},
expectRes: false,
}}
name string
arg string
expected bool
}{
{
name: "empty string",
arg: "",
expected: false,
},
{
name: "random",
arg: "random",
expected: false,
},
{
name: "version",
arg: "version",
expected: true,
},
{
name: "--version",
arg: "--version",
expected: true,
},
{
name: "version weird casing",
arg: "veRSiOn",
expected: true,
},
{
// -v should be reserved for verbose, and should not be used for --version.
name: "-v",
arg: "-v",
expected: false,
},
{
name: "typo",
arg: "vrsion",
expected: false,
},
{
name: "non version command",
arg: "start",
expected: false,
},
{
name: "help",
arg: "help",
expected: false,
},
{
name: "-h",
arg: "-h",
expected: false,
},
{
name: "--help",
arg: "--help",
expected: false,
},
{
name: "run",
arg: "run",
expected: false,
},
}
for i := range cases {
tc := cases[i]
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
res := isVersionCommand(tc.args)
require.Equal(tc.expectRes, res)
for _, tc := range cases {
t.Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) {
actual := IsVersionCommand(tc.arg)
require.Equal(t, tc.expected, actual)
})
}
}

View File

@ -1,54 +1,16 @@
package main
import (
"fmt"
"os"
"github.com/cosmos/cosmos-sdk/cosmovisor"
"github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor/cmd"
"github.com/cosmos/cosmos-sdk/cosmovisor/errors"
)
func main() {
cosmovisor.SetupLogging()
if err := Run(os.Args[1:]); err != nil {
if err := cmd.RunCosmovisorCommand(os.Args[1:]); err != nil {
cosmovisor.Logger.Error().Err(err).Msg("")
os.Exit(1)
}
}
// Run is the main loop, but returns an error
func Run(args []string) error {
cmd.RunCosmovisorCommands(args)
cfg, cerr := cosmovisor.GetConfigFromEnv()
if cerr != nil {
switch err := cerr.(type) {
case *errors.MultiError:
cosmovisor.Logger.Error().Msg("multiple configuration errors found:")
for i, e := range err.GetErrors() {
cosmovisor.Logger.Error().Err(e).Msg(fmt.Sprintf(" %d:", i+1))
}
default:
cosmovisor.Logger.Error().Err(err).Msg("configuration error:")
}
return cerr
}
cosmovisor.Logger.Info().Msg("Configuration is valid:\n" + cfg.DetailString())
launcher, err := cosmovisor.NewLauncher(cfg)
if err != nil {
return err
}
doUpgrade, err := launcher.Run(args, os.Stdout, os.Stderr)
// if RestartAfterUpgrade, we launch after a successful upgrade (only condition LaunchProcess returns nil)
for cfg.RestartAfterUpgrade && err == nil && doUpgrade {
cosmovisor.Logger.Info().Str("app", cfg.Name).Msg("upgrade detected, relaunching")
doUpgrade, err = launcher.Run(args, os.Stdout, os.Stderr)
}
if doUpgrade && err == nil {
cosmovisor.Logger.Info().Msg("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.")
}
return err
}