-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #80 from nkryuchkov/feature/restart-visor
Implement visor restart from hypervisor
- Loading branch information
Showing
8 changed files
with
330 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package restart | ||
|
||
import ( | ||
"errors" | ||
"log" | ||
"os" | ||
"os/exec" | ||
"sync/atomic" | ||
"time" | ||
|
||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
var ( | ||
// ErrAlreadyStarting is returned on starting attempt when starting is in progress. | ||
ErrAlreadyStarting = errors.New("already starting") | ||
) | ||
|
||
const ( | ||
// DefaultCheckDelay is a default delay for checking if a new instance is started successfully. | ||
DefaultCheckDelay = 1 * time.Second | ||
extraWaitingTime = 1 * time.Second | ||
delayArgName = "--delay" | ||
) | ||
|
||
// Context describes data required for restarting visor. | ||
type Context struct { | ||
log logrus.FieldLogger | ||
cmd *exec.Cmd | ||
checkDelay time.Duration | ||
isStarting int32 | ||
appendDelay bool // disabled in tests | ||
} | ||
|
||
// CaptureContext captures data required for restarting visor. | ||
// Data used by CaptureContext must not be modified before, | ||
// therefore calling CaptureContext immediately after starting executable is recommended. | ||
func CaptureContext() *Context { | ||
cmd := exec.Command(os.Args[0], os.Args[1:]...) // nolint:gosec | ||
|
||
cmd.Stdout = os.Stdout | ||
cmd.Stdin = os.Stdin | ||
cmd.Stderr = os.Stderr | ||
cmd.Env = os.Environ() | ||
|
||
return &Context{ | ||
cmd: cmd, | ||
checkDelay: DefaultCheckDelay, | ||
appendDelay: true, | ||
} | ||
} | ||
|
||
// RegisterLogger registers a logger instead of standard one. | ||
func (c *Context) RegisterLogger(logger logrus.FieldLogger) { | ||
if c != nil { | ||
c.log = logger | ||
} | ||
} | ||
|
||
// SetCheckDelay sets a check delay instead of standard one. | ||
func (c *Context) SetCheckDelay(delay time.Duration) { | ||
if c != nil { | ||
c.checkDelay = delay | ||
} | ||
} | ||
|
||
// Start starts a new executable using Context. | ||
func (c *Context) Start() error { | ||
if !atomic.CompareAndSwapInt32(&c.isStarting, 0, 1) { | ||
return ErrAlreadyStarting | ||
} | ||
|
||
defer atomic.StoreInt32(&c.isStarting, 0) | ||
|
||
errCh := c.startExec() | ||
|
||
ticker := time.NewTicker(c.checkDelay) | ||
defer ticker.Stop() | ||
|
||
select { | ||
case err := <-errCh: | ||
c.errorLogger()("Failed to start new instance: %v", err) | ||
return err | ||
case <-ticker.C: | ||
c.infoLogger()("New instance started successfully, exiting from the old one") | ||
return nil | ||
} | ||
} | ||
|
||
func (c *Context) startExec() chan error { | ||
errCh := make(chan error, 1) | ||
|
||
go func() { | ||
defer close(errCh) | ||
|
||
c.adjustArgs() | ||
|
||
c.infoLogger()("Starting new instance of executable (args: %q)", c.cmd.Args) | ||
|
||
if err := c.cmd.Start(); err != nil { | ||
errCh <- err | ||
return | ||
} | ||
|
||
if err := c.cmd.Wait(); err != nil { | ||
errCh <- err | ||
return | ||
} | ||
}() | ||
|
||
return errCh | ||
} | ||
|
||
func (c *Context) adjustArgs() { | ||
args := c.cmd.Args | ||
|
||
i := 0 | ||
l := len(args) | ||
|
||
for i < l { | ||
if args[i] == delayArgName && i < len(args)-1 { | ||
args = append(args[:i], args[i+2:]...) | ||
l -= 2 | ||
} else { | ||
i++ | ||
} | ||
} | ||
|
||
if c.appendDelay { | ||
delay := c.checkDelay + extraWaitingTime | ||
args = append(args, delayArgName, delay.String()) | ||
} | ||
|
||
c.cmd.Args = args | ||
} | ||
|
||
func (c *Context) infoLogger() func(string, ...interface{}) { | ||
if c.log != nil { | ||
return c.log.Infof | ||
} | ||
|
||
logger := log.New(os.Stdout, "[INFO] ", log.LstdFlags) | ||
|
||
return logger.Printf | ||
} | ||
|
||
func (c *Context) errorLogger() func(string, ...interface{}) { | ||
if c.log != nil { | ||
return c.log.Errorf | ||
} | ||
|
||
logger := log.New(os.Stdout, "[ERROR] ", log.LstdFlags) | ||
|
||
return logger.Printf | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package restart | ||
|
||
import ( | ||
"os" | ||
"os/exec" | ||
"testing" | ||
"time" | ||
|
||
"github.com/SkycoinProject/skycoin/src/util/logging" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestCaptureContext(t *testing.T) { | ||
cc := CaptureContext() | ||
|
||
require.Equal(t, DefaultCheckDelay, cc.checkDelay) | ||
require.Equal(t, os.Args, cc.cmd.Args) | ||
require.Equal(t, os.Stdout, cc.cmd.Stdout) | ||
require.Equal(t, os.Stdin, cc.cmd.Stdin) | ||
require.Equal(t, os.Stderr, cc.cmd.Stderr) | ||
require.Equal(t, os.Environ(), cc.cmd.Env) | ||
require.Nil(t, cc.log) | ||
} | ||
|
||
func TestContext_RegisterLogger(t *testing.T) { | ||
cc := CaptureContext() | ||
require.Nil(t, cc.log) | ||
|
||
logger := logging.MustGetLogger("test") | ||
cc.RegisterLogger(logger) | ||
require.Equal(t, logger, cc.log) | ||
} | ||
|
||
func TestContext_Start(t *testing.T) { | ||
cc := CaptureContext() | ||
assert.NotZero(t, len(cc.cmd.Args)) | ||
|
||
t.Run("executable started", func(t *testing.T) { | ||
cmd := "touch" | ||
path := "/tmp/test_restart" | ||
cc.cmd = exec.Command(cmd, path) // nolint:gosec | ||
cc.appendDelay = false | ||
|
||
assert.NoError(t, cc.Start()) | ||
assert.NoError(t, os.Remove(path)) | ||
}) | ||
|
||
t.Run("bad args", func(t *testing.T) { | ||
cmd := "bad_command" | ||
cc.cmd = exec.Command(cmd) // nolint:gosec | ||
|
||
// TODO(nkryuchkov): Check if it works on Linux and Windows, if not then change the error text. | ||
assert.EqualError(t, cc.Start(), `exec: "bad_command": executable file not found in $PATH`) | ||
}) | ||
|
||
t.Run("already restarting", func(t *testing.T) { | ||
cmd := "touch" | ||
path := "/tmp/test_restart" | ||
cc.cmd = exec.Command(cmd, path) // nolint:gosec | ||
cc.appendDelay = false | ||
|
||
ch := make(chan error, 1) | ||
go func() { | ||
ch <- cc.Start() | ||
}() | ||
|
||
assert.NoError(t, cc.Start()) | ||
assert.Equal(t, ErrAlreadyStarting, <-ch) | ||
|
||
assert.NoError(t, os.Remove(path)) | ||
}) | ||
} | ||
|
||
func TestContext_SetCheckDelay(t *testing.T) { | ||
cc := CaptureContext() | ||
require.Equal(t, DefaultCheckDelay, cc.checkDelay) | ||
|
||
const oneSecond = 1 * time.Second | ||
|
||
cc.SetCheckDelay(oneSecond) | ||
require.Equal(t, oneSecond, cc.checkDelay) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.