sandwich-webdriver-0.3.0.0: src/Test/Sandwich/WebDriver/Internal/StartWebDriver.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Test.Sandwich.WebDriver.Internal.StartWebDriver where
import Control.Monad
import Control.Monad.Catch (MonadMask)
import Control.Monad.IO.Class
import Control.Monad.IO.Unlift
import Control.Monad.Logger
import Control.Monad.Reader
import Control.Retry
import Data.Default
import Data.Function (fix)
import Data.String.Interpolate
import qualified Data.Text as T
import GHC.Stack
import System.Directory
import System.FilePath
import System.IO (hClose, hGetLine)
import Test.Sandwich
import Test.Sandwich.Contexts.Files
import Test.Sandwich.Contexts.Util.Ports (findFreePortOrException)
import Test.Sandwich.Util.Process
import Test.Sandwich.WebDriver.Internal.Capabilities.Extra
import Test.Sandwich.WebDriver.Internal.Dependencies
import Test.Sandwich.WebDriver.Internal.Types
import Test.Sandwich.WebDriver.Internal.Util
import qualified Test.WebDriver as W
import UnliftIO.Async
import UnliftIO.Concurrent
import UnliftIO.Exception
import UnliftIO.Process
import UnliftIO.Timeout
#ifndef mingw32_HOST_OS
import Test.Sandwich.WebDriver.Internal.StartWebDriver.Xvfb
#endif
type Constraints m = (
HasCallStack, MonadLoggerIO m, MonadUnliftIO m, MonadMask m, MonadFail m
)
-- | Spin up a Selenium WebDriver and create a WebDriver
startWebDriver :: (
Constraints m, MonadReader context m, HasBaseContext context
, HasFile context "java", HasFile context "selenium.jar", HasBrowserDependencies context
) => WdOptions -> OnDemandOptions -> FilePath -> m WebDriver
startWebDriver wdOptions@(WdOptions {capabilities=capabilities'', ..}) (OnDemandOptions {..}) runRoot = do
-- Create a unique name for this webdriver so the folder for its log output doesn't conflict with any others
webdriverName <- ("webdriver_" <>) <$> liftIO makeUUID
-- Directory to log everything for this webdriver
let webdriverRoot = runRoot </> (T.unpack webdriverName)
liftIO $ createDirectoryIfMissing True webdriverRoot
-- Directory to hold any downloads
let downloadDir = webdriverRoot </> "Downloads"
liftIO $ createDirectoryIfMissing True downloadDir
-- Get selenium, driver args, and capabilities with browser paths applied
java <- askFile @"java"
seleniumPath <- askFile @"selenium.jar"
(driverArgs, capabilities') <- fillInCapabilitiesAndGetDriverArgs webdriverRoot capabilities''
-- Set up xvfb if configured
xvfbOnDemand <- newMVar OnDemandNotStarted
(maybeXvfbSession, javaEnv) <- case runMode of
#ifndef mingw32_HOST_OS
RunInXvfb (XvfbConfig {..}) -> do
(s, e) <- makeXvfbSession xvfbResolution xvfbStartFluxbox webdriverRoot xvfbToUse xvfbOnDemand
return (Just s, Just e)
#endif
_ -> return (Nothing, Nothing)
-- Create a distinct process name
webdriverProcessName <- ("webdriver_process_" <>) <$> (liftIO makeUUID)
let webdriverProcessRoot = webdriverRoot </> T.unpack webdriverProcessName
liftIO $ createDirectoryIfMissing True webdriverProcessRoot
-- Retry up to 10 times
-- This is necessary because sometimes we get a race for the port we get from findFreePortOrException.
-- There doesn't seem to be any way to make Selenium choose its own port.
let policy = constantDelay 0 <> limitRetries 10
(port, hRead, p) <- recoverAll policy $ \retryStatus -> flip withException (\(e :: SomeException) -> warn [i|Exception in startWebDriver retry: #{e}|]) $ do
when (rsIterNumber retryStatus > 0) $
warn [i|Trying again to start selenium server (attempt #{rsIterNumber retryStatus})|]
(hRead, hWrite) <- createPipe
port <- findFreePortOrException
let allArgs = driverArgs <> ["-jar", seleniumPath
, "-port", show port]
let cp = (proc java allArgs) {
env = javaEnv
, std_in = Inherit
, std_out = UseHandle hWrite
, std_err = UseHandle hWrite
, create_group = True
}
-- Start the process and wait for it to be ready
debug [i|#{java} #{T.unwords $ fmap T.pack allArgs}|]
(_, _, _, p) <- liftIO $ createProcess cp
let teardown = do
gracefullyStopProcess p 30_000_000
liftIO $ hClose hRead
-- On exception, make sure the process is gone and the pipe handle is closed
flip withException (\(_ :: SomeException) -> teardown) $ do
-- Read from the (combined) output stream until we see the up and running message,
-- or the process ends and we get an exception from hGetLine
startupResult <- timeout 10_000_000 $ fix $ \loop -> do
line <- fmap T.pack $ liftIO $ hGetLine hRead
debug line
if | "Selenium Server is up and running" `T.isInfixOf` line -> return ()
| otherwise -> loop
case startupResult of
Nothing -> do
let msg = [i|Didn't see "up and running" line in Selenium output after 10s.|]
warn msg
expectationFailure (T.unpack msg)
Just () -> return (port, hRead, p)
-- TODO: save this in the WebDriver to tear it down later?
_logAsync <- async $ forever (liftIO (hGetLine hRead) >>= (debug . T.pack))
-- Final extra capabilities configuration
capabilities <-
configureHeadlessCapabilities wdOptions runMode capabilities'
>>= configureDownloadCapabilities downloadDir
-- Make the WebDriver
WebDriver <$> pure (T.unpack webdriverName)
<*> pure (p, maybeXvfbSession)
<*> pure (wdOptions {
capabilities = capabilities
})
<*> liftIO (newMVar mempty)
<*> pure (def { W.wdPort = fromIntegral port
, W.wdCapabilities = capabilities
, W.wdHTTPManager = httpManager
, W.wdHTTPRetryCount = httpRetryCount
})
<*> pure downloadDir
<*> pure ffmpegToUse
<*> newMVar OnDemandNotStarted
<*> pure xvfbToUse
<*> pure xvfbOnDemand
stopWebDriver :: Constraints m => WebDriver -> m ()
stopWebDriver (WebDriver {wdWebDriver=(h, maybeXvfbSession)}) = do
-- | TODO: expose this as an option
let gracePeriod :: Int
gracePeriod = 30000000
gracefullyStopProcess h gracePeriod
whenJust maybeXvfbSession $ \(XvfbSession {..}) -> do
whenJust xvfbFluxboxProcess $ \p -> do
gracefullyStopProcess p gracePeriod
gracefullyStopProcess xvfbProcess gracePeriod