diff --git a/docs/cli-flags.adoc b/docs/cli-flags.adoc index 9e347f7e..bc46b9ec 100644 --- a/docs/cli-flags.adoc +++ b/docs/cli-flags.adoc @@ -25,6 +25,8 @@ The following flags are supported by `selenoid` command: Network address to accept connections (default ":4444") -log-conf string Container logging configuration file (default "config/container-logs.json") +-max-timeout duration + Maximum valid session idle timeout in time.Duration format (default 1h0m0s) -mem value Containers memory limit e.g. 128m or 1g -retry-count int diff --git a/docs/special-capabilities.adoc b/docs/special-capabilities.adoc index 507e3917..39c6e7dc 100644 --- a/docs/special-capabilities.adoc +++ b/docs/special-capabilities.adoc @@ -87,6 +87,17 @@ name: "myCoolTestName" The main application of this capability - is debugging tests in the UI which is showing specified name for every running session. +=== Custom Session Timeout: sessionTimeout + +Sometimes you may want to change idle timeout for selected browser session. To achieve this - pass the following capability: + +.Type: int +---- +sessionTimeout: 30 +---- + +Timeout is always specified in seconds, should be positive and can be no more than the value set by `-max-timeout` flag. + === Per-session Time Zone: timeZone Some tests require particular time zone to be set in operating system. diff --git a/main.go b/main.go index 3cbcdc7e..c55fb983 100644 --- a/main.go +++ b/main.go @@ -67,6 +67,7 @@ var ( enableFileUpload bool listen string timeout time.Duration + maxTimeout time.Duration newSessionAttemptTimeout time.Duration sessionDeleteTimeout time.Duration serviceStartupTimeout time.Duration @@ -104,6 +105,7 @@ func init() { flag.IntVar(&limit, "limit", 5, "Simultaneous container runs") flag.IntVar(&retryCount, "retry-count", 1, "New session attempts retry count") flag.DurationVar(&timeout, "timeout", 60*time.Second, "Session idle timeout in time.Duration format") + flag.DurationVar(&maxTimeout, "max-timeout", 1*time.Hour, "Maximum valid session idle timeout in time.Duration format") flag.DurationVar(&newSessionAttemptTimeout, "session-attempt-timeout", 30*time.Second, "New session attempt timeout in time.Duration format") flag.DurationVar(&sessionDeleteTimeout, "session-delete-timeout", 30*time.Second, "Session delete timeout in time.Duration format") flag.DurationVar(&serviceStartupTimeout, "service-startup-timeout", 30*time.Second, "Service startup timeout in time.Duration format") diff --git a/selenoid.go b/selenoid.go index 724b4a82..cd7c462e 100644 --- a/selenoid.go +++ b/selenoid.go @@ -132,6 +132,13 @@ func create(w http.ResponseWriter, r *http.Request) { browser.Caps = browser.W3CCaps.Caps } browser.Caps.ProcessExtensionCapabilities() + sessionTimeout, err := getSessionTimeout(browser.Caps.SessionTimeout, maxTimeout, timeout) + if err != nil { + log.Printf("[%d] [BAD_SESSION_TIMEOUT] [%ds]", requestId, browser.Caps.SessionTimeout) + util.JsonError(w, err.Error(), http.StatusBadRequest) + queue.Drop() + return + } resolution, err := getScreenResolution(browser.Caps.ScreenResolution) if err != nil { log.Printf("[%d] [BAD_SCREEN_RESOLUTION] [%s]", requestId, browser.Caps.ScreenResolution) @@ -273,7 +280,8 @@ func create(w http.ResponseWriter, r *http.Request) { Fileserver: startedService.FileserverHostPort, VNC: startedService.VNCHostPort, Cancel: cancelAndRenameVideo, - Timeout: onTimeout(timeout, func() { + Timeout: sessionTimeout, + TimeoutCh: onTimeout(sessionTimeout, func() { request{r}.session(s.ID).Delete(requestId) })}) queue.Create() @@ -320,6 +328,18 @@ func getVideoScreenSize(videoScreenSize string, screenResolution string) (string return shortenScreenResolution(screenResolution), nil } +func getSessionTimeout(sessionTimeout uint32, maxTimeout time.Duration, defaultTimeout time.Duration) (time.Duration, error) { + if sessionTimeout > 0 { + std := time.Duration(sessionTimeout) * time.Second + if std <= maxTimeout { + return std, nil + } else { + return 0, fmt.Errorf("Invalid sessionTimeout capability: should be <= %s", maxTimeout) + } + } + return defaultTimeout, nil +} + func getVideoFileName(videoOutputDir string) string { filename := "" for { @@ -355,9 +375,9 @@ func proxy(w http.ResponseWriter, r *http.Request) { sess.Lock.Lock() defer sess.Lock.Unlock() select { - case <-sess.Timeout: + case <-sess.TimeoutCh: default: - close(sess.Timeout) + close(sess.TimeoutCh) } if r.Method == http.MethodDelete && len(fragments) == 3 { if enableFileUpload { @@ -368,7 +388,7 @@ func proxy(w http.ResponseWriter, r *http.Request) { queue.Release() log.Printf("[%d] [SESSION_DELETED] [%s]", requestId, id) } else { - sess.Timeout = onTimeout(timeout, func() { + sess.TimeoutCh = onTimeout(sess.Timeout, func() { request{r}.session(id).Delete(requestId) }) if len(fragments) == 4 && fragments[len(fragments)-1] == "file" && enableFileUpload { diff --git a/selenoid_test.go b/selenoid_test.go index 3702fcf9..3387861c 100644 --- a/selenoid_test.go +++ b/selenoid_test.go @@ -89,6 +89,24 @@ func TestGetShortScreenResolution(t *testing.T) { AssertThat(t, res, EqualTo{"1024x768x24"}) } +func TestInvalidSessionTimeoutCapability(t *testing.T) { + testBadSessionTimeoutCapability(t, 3601) +} + +func TestNegativeSessionTimeoutCapability(t *testing.T) { + testBadSessionTimeoutCapability(t, -1) +} + +func testBadSessionTimeoutCapability(t *testing.T, timeoutValue int) { + manager = &BrowserNotFound{} + + rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(fmt.Sprintf(`{"desiredCapabilities":{"sessionTimeout":%d}}`, timeoutValue)))) + AssertThat(t, err, Is{nil}) + AssertThat(t, rsp, Code{http.StatusBadRequest}) + + AssertThat(t, queue.Used(), EqualTo{0}) +} + func TestMalformedScreenResolutionCapability(t *testing.T) { manager = &BrowserNotFound{} @@ -174,8 +192,9 @@ func TestNewSessionBadHostResponse(t *testing.T) { func TestSessionCreated(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} + timeout = 5 * time.Second - resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities": {"enableVideo": true, "enableVNC": true}}`))) + resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities": {"enableVideo": true, "enableVNC": true, "sessionTimeout": 3}}`))) AssertThat(t, err, Is{nil}) var sess map[string]string AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) diff --git a/session/session.go b/session/session.go index 672a9340..53210976 100644 --- a/session/session.go +++ b/session/session.go @@ -4,6 +4,7 @@ import ( "net/url" "reflect" "sync" + "time" ) // Caps - user capabilities @@ -24,6 +25,7 @@ type Caps struct { ApplicationContainers []string `json:"applicationContainers"` HostsEntries []string `json:"hostsEntries"` Labels map[string]string `json:"labels"` + SessionTimeout uint32 `json:"sessionTimeout"` ExtensionCapabilities map[string]interface{} `json:"selenoid:options"` } @@ -70,7 +72,8 @@ type Session struct { Fileserver string VNC string Cancel func() - Timeout chan struct{} + Timeout time.Duration + TimeoutCh chan struct{} Lock sync.Mutex }