diff --git a/cmd/selenoid.go b/cmd/selenoid.go index 5abd52e..31b2e7c 100644 --- a/cmd/selenoid.go +++ b/cmd/selenoid.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" "os" "runtime" + "time" ) var ( @@ -24,6 +25,8 @@ var ( skipDownload bool vnc bool force bool + graceful bool + gracefulTimeout time.Duration args string env string browserEnv string @@ -155,6 +158,13 @@ func initFlags() { } { c.Flags().BoolVarP(&force, "force", "f", false, "force action") } + for _, c := range []*cobra.Command{ + selenoidStopCmd, + selenoidStopUICmd, + } { + c.Flags().BoolVarP(&graceful, "graceful", "", false, "do action gracefully (e.g. gracefully stop Selenoid)") + c.Flags().DurationVarP(&gracefulTimeout, "graceful-timeout", "", 30*time.Second, "graceful timeout value (how much time to wait for graceful action execution)") + } for _, c := range []*cobra.Command{ selenoidStartCmd, selenoidUpdateCmd, @@ -170,17 +180,19 @@ func initFlags() { func createLifecycle(configDir string, port uint16) (*selenoid.Lifecycle, error) { config := selenoid.LifecycleConfig{ - Quiet: quiet, - Force: force, - ConfigDir: configDir, - UseDrivers: useDrivers, - Browsers: browsers, - BrowserEnv: browserEnv, - Download: !skipDownload, - Args: args, - Env: env, - Port: int(port), - DisableLogs: disableLogs, + Quiet: quiet, + Force: force, + Graceful: graceful, + GracefulTimeout: gracefulTimeout, + ConfigDir: configDir, + UseDrivers: useDrivers, + Browsers: browsers, + BrowserEnv: browserEnv, + Download: !skipDownload, + Args: args, + Env: env, + Port: int(port), + DisableLogs: disableLogs, LastVersions: lastVersions, RegistryUrl: registry, diff --git a/docs/selenoid-commands.adoc b/docs/selenoid-commands.adoc index 488c66e..86bdb39 100644 --- a/docs/selenoid-commands.adoc +++ b/docs/selenoid-commands.adoc @@ -1,16 +1,17 @@ == Quick Start Guide -This guide will show how to start Selenoid in a fastest way with help of Configuration Manager. -First, you need to download latest release binary from https://github.com/aerokube/cm/releases/latest[GitHub releases] for your platform (linux/darwin/windows). +To start Selenoid: -Having a binary launch one command: +. Download the latest release binary from https://github.com/aerokube/cm/releases/latest[GitHub releases] for your platform (linux/darwin/windows). +. Having the binary launch one command: ++ .On Linux and Mac OS [source,bash] ---- $ ./cm selenoid start --vnc ---- - ++ .On Windows [source,powershell] ---- diff --git a/selenoid/base.go b/selenoid/base.go index e7377b6..96af935 100644 --- a/selenoid/base.go +++ b/selenoid/base.go @@ -6,6 +6,7 @@ import ( "os" "os/user" "path/filepath" + "time" "github.com/fatih/color" "github.com/mattn/go-colorable" @@ -90,6 +91,11 @@ type Forceable struct { Force bool } +type GracefulAware struct { + Graceful bool + GracefulTimeout time.Duration +} + type VersionAware struct { Version string } diff --git a/selenoid/docker.go b/selenoid/docker.go index 7cf91db..d8f8ecd 100644 --- a/selenoid/docker.go +++ b/selenoid/docker.go @@ -76,6 +76,7 @@ type DockerConfigurator struct { PortAware UserNSAware LogsAware + GracefulAware LastVersions int Pull bool RegistryUrl string @@ -102,6 +103,7 @@ func NewDockerConfigurator(config *LifecycleConfig) (*DockerConfigurator, error) PortAware: PortAware{Port: config.Port}, UserNSAware: UserNSAware{UserNS: config.UserNS}, LogsAware: LogsAware{DisableLogs: config.DisableLogs}, + GracefulAware: GracefulAware{Graceful: config.Graceful, GracefulTimeout: config.GracefulTimeout}, RegistryUrl: config.RegistryUrl, BrowsersJson: config.BrowsersJson, LastVersions: config.LastVersions, @@ -981,7 +983,15 @@ func (c *DockerConfigurator) createNetworkIfNeeded(networkName string) error { } func (c *DockerConfigurator) removeContainer(id string) error { - return c.docker.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) + ctx := context.Background() + if c.Graceful { + err := c.docker.ContainerStop(ctx, id, &c.GracefulTimeout) + if err == nil { + return c.docker.ContainerRemove(ctx, id, types.ContainerRemoveOptions{RemoveVolumes: true}) + } + return err + } + return c.docker.ContainerRemove(ctx, id, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) } func (c *DockerConfigurator) Stop() error { diff --git a/selenoid/drivers.go b/selenoid/drivers.go index c9111c3..e8663a9 100644 --- a/selenoid/drivers.go +++ b/selenoid/drivers.go @@ -25,6 +25,8 @@ import ( "regexp" "runtime" "strings" + "syscall" + "time" ) const ( @@ -67,6 +69,7 @@ type DriversConfigurator struct { PortAware RequestedBrowsersAware LogsAware + GracefulAware DriversInfoUrl string GithubBaseUrl string @@ -86,6 +89,7 @@ func NewDriversConfigurator(config *LifecycleConfig) *DriversConfigurator { DownloadAware: DownloadAware{DownloadNeeded: config.Download}, RequestedBrowsersAware: RequestedBrowsersAware{Browsers: config.Browsers}, LogsAware: LogsAware{DisableLogs: config.DisableLogs}, + GracefulAware: GracefulAware{Graceful: config.Graceful, GracefulTimeout: config.GracefulTimeout}, DriversInfoUrl: config.DriversInfoUrl, GithubBaseUrl: config.GithubBaseUrl, OS: config.OS, @@ -615,21 +619,41 @@ func (d *DriversConfigurator) StartUI() error { return runCommand(d.getSelenoidUIBinaryPath(), args, env) } -var killFunc = func(p os.Process) error { - return p.Kill() +var killFunc = func(p *os.Process, graceful bool, gracefulTimeout time.Duration) error { + if isWindows() || !graceful { + return p.Kill() + } + err := p.Signal(syscall.SIGTERM) + if err != nil { + return fmt.Errorf("failed to send signal: %v", err) + } + exitCode := make(chan int) + go func() { + ps, _ := p.Wait() + exitCode <- ps.ExitCode() + }() + select { + case <-time.After(gracefulTimeout): + return p.Kill() + case code := <-exitCode: + if code != 0 { + return fmt.Errorf("process exited with code %d", code) + } + return nil + } } func (d *DriversConfigurator) Stop() error { - return killAllProcesses(findSelenoidProcesses()) + return d.killAllProcesses(findSelenoidProcesses()) } func (d *DriversConfigurator) StopUI() error { - return killAllProcesses(findSelenoidUIProcesses()) + return d.killAllProcesses(findSelenoidUIProcesses()) } -func killAllProcesses(processes []os.Process) error { +func (d *DriversConfigurator) killAllProcesses(processes []*os.Process) error { for _, p := range processes { - err := killFunc(p) + err := killFunc(p, d.Graceful, d.GracefulTimeout) if err != nil { return err } @@ -642,23 +666,23 @@ func (d *DriversConfigurator) Close() error { return nil } -func findSelenoidProcesses() []os.Process { +func findSelenoidProcesses() []*os.Process { return findProcesses("selenoid") } -func findSelenoidUIProcesses() []os.Process { +func findSelenoidUIProcesses() []*os.Process { return findProcesses("selenoid-ui") } -func findProcesses(regex string) []os.Process { - ret := []os.Process{} +func findProcesses(regex string) []*os.Process { + var ret []*os.Process processes, _ := ps.Processes() for _, process := range processes { matched, _ := regexp.MatchString(regex, process.Executable()) if matched { p, err := os.FindProcess(process.Pid()) if err == nil { - ret = append(ret, *p) + ret = append(ret, p) } } } diff --git a/selenoid/drivers_test.go b/selenoid/drivers_test.go index 79a8711..e58515d 100644 --- a/selenoid/drivers_test.go +++ b/selenoid/drivers_test.go @@ -16,6 +16,7 @@ import ( "reflect" "runtime" "testing" + "time" ) const ( @@ -32,7 +33,7 @@ var ( func init() { mockDriverServer = httptest.NewServer(driversMux()) - killFunc = func(_ os.Process) error { return nil } + killFunc = func(_ *os.Process, _ bool, _ time.Duration) error { return nil } } func driversMux() http.Handler { diff --git a/selenoid/lifecycle.go b/selenoid/lifecycle.go index 96f0fb3..cd8aa4a 100644 --- a/selenoid/lifecycle.go +++ b/selenoid/lifecycle.go @@ -7,20 +7,23 @@ import ( "github.com/docker/docker/client" "github.com/fatih/color" "io" + "time" ) type LifecycleConfig struct { - Quiet bool - Force bool - ConfigDir string - Browsers string - BrowserEnv string - Download bool - Args string - Env string - Version string - Port int - DisableLogs bool + Quiet bool + Force bool + Graceful bool + GracefulTimeout time.Duration + ConfigDir string + Browsers string + BrowserEnv string + Download bool + Args string + Env string + Version string + Port int + DisableLogs bool // Docker specific LastVersions int