Add eager timeouts to the polling functions used in daml start and assistant integration tests. (#12361)

* Add timeouts to polling funcs in daml start.

This PR adds timeouts to some polling functions used in daml start and
the assistant integration tests, and also early exits based on a process
exit status. E.g. waitForHttpServer will make sure some process is
still running, instead of waiting to timeout.

The effect of this is that now whenever there is some error in a
subprocess, daml start and the integration tests will finish early
instead of running forever (or timing out in bazel).

changelog_begin
changelog_end

* missing a readPortFile instance
This commit is contained in:
Sofia Faro 2022-01-12 10:01:56 +00:00 committed by GitHub
parent 8bc5804520
commit 09013eb501
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 109 additions and 64 deletions

View File

@ -28,7 +28,7 @@ main = do
let portFile = tempDir </> "portfile" let portFile = tempDir </> "portfile"
let interpolatedServerArgs = map (replace "%PORT_FILE%" portFile) splitServerArgs let interpolatedServerArgs = map (replace "%PORT_FILE%" portFile) splitServerArgs
let serverProc = proc serverExe interpolatedServerArgs let serverProc = proc serverExe interpolatedServerArgs
withCreateProcess serverProc $ \_stdin _stdout _stderr _ph -> do withCreateProcess serverProc $ \_stdin _stdout _stderr ph -> do
port <- readPortFile maxRetries portFile port <- readPortFile ph maxRetries portFile
let interpolatedClientArgs = map (replace "%PORT%" (show port)) splitClientArgs let interpolatedClientArgs = map (replace "%PORT%" (show port)) splitClientArgs
callProcess clientExe interpolatedClientArgs callProcess clientExe interpolatedClientArgs

View File

@ -113,7 +113,7 @@ withReplClient opts@Options{..} f = withTempFile $ \portFile -> do
, concat [ ["--max-inbound-message-size", show (getMaxInboundMessageSize size)] | Just size <- [optMaxInboundMessageSize] ] , concat [ ["--max-inbound-message-size", show (getMaxInboundMessageSize size)] | Just size <- [optMaxInboundMessageSize] ]
] ]
withCreateProcess replServer { std_out = optStdout } $ \_ stdout _ ph -> do withCreateProcess replServer { std_out = optStdout } $ \_ stdout _ ph -> do
port <- readPortFile maxRetries portFile port <- readPortFile ph maxRetries portFile
let grpcConfig = ClientConfig (Host "127.0.0.1") (Port port) [] Nothing Nothing let grpcConfig = ClientConfig (Host "127.0.0.1") (Port port) [] Nothing Nothing
threadDelay 1000000 threadDelay 1000000
withGRPCClient grpcConfig $ \client -> do withGRPCClient grpcConfig $ \client -> do

View File

@ -127,7 +127,7 @@ withSandbox StartOptions{..} darPath scenarioArgs sandboxArgs kont =
cantonOptions <- determineCantonOptions sandboxPortM cantonPortSpec portFile cantonOptions <- determineCantonOptions sandboxPortM cantonPortSpec portFile
withCantonSandbox cantonOptions sandboxArgs $ \ph -> do withCantonSandbox cantonOptions sandboxArgs $ \ph -> do
putStrLn "Waiting for canton sandbox to start." putStrLn "Waiting for canton sandbox to start."
sandboxPort <- readPortFileWith decodeCantonSandboxPort maxRetries portFile sandboxPort <- readPortFileWith decodeCantonSandboxPort (unsafeProcessHandle ph) maxRetries portFile
runLedgerUploadDar ((defaultLedgerFlags Grpc) {fPortM = Just sandboxPort}) (Just darPath) runLedgerUploadDar ((defaultLedgerFlags Grpc) {fPortM = Just sandboxPort}) (Just darPath)
kont ph (SandboxPort sandboxPort) kont ph (SandboxPort sandboxPort)
@ -143,7 +143,7 @@ withSandbox StartOptions{..} darPath scenarioArgs sandboxArgs kont =
] ]
withPlatformJar args "sandbox-logback.xml" $ \ph -> do withPlatformJar args "sandbox-logback.xml" $ \ph -> do
putStrLn "Waiting for sandbox to start: " putStrLn "Waiting for sandbox to start: "
port <- readPortFile maxRetries portFile port <- readPortFile (unsafeProcessHandle ph) maxRetries portFile
kont ph (SandboxPort port) kont ph (SandboxPort port)
withNavigator :: SandboxPort -> NavigatorPort -> [String] -> (Process () () () -> IO a) -> IO a withNavigator :: SandboxPort -> NavigatorPort -> [String] -> (Process () () () -> IO a) -> IO a
@ -156,8 +156,8 @@ withNavigator (SandboxPort sandboxPort) navigatorPort args a = do
logbackArg <- getLogbackArg (damlSdkJarFolder </> "navigator-logback.xml") logbackArg <- getLogbackArg (damlSdkJarFolder </> "navigator-logback.xml")
withJar damlSdkJar [logbackArg] ("navigator":navigatorArgs) $ \ph -> do withJar damlSdkJar [logbackArg] ("navigator":navigatorArgs) $ \ph -> do
putStrLn "Waiting for navigator to start: " putStrLn "Waiting for navigator to start: "
-- TODO We need to figure out a sane timeout for this step. waitForHttpServer 240 (unsafeProcessHandle ph) (putStr "." *> threadDelay 500000)
waitForHttpServer (putStr "." *> threadDelay 500000) (navigatorURL navigatorPort) [] (navigatorURL navigatorPort) []
a ph a ph
withJsonApi :: SandboxPort -> JsonApiPort -> [String] -> (Process () () () -> IO a) -> IO a withJsonApi :: SandboxPort -> JsonApiPort -> [String] -> (Process () () () -> IO a) -> IO a
@ -185,7 +185,8 @@ withJsonApi (SandboxPort sandboxPort) (JsonApiPort jsonApiPort) extraArgs a = do
let headers = let headers =
[ ("Authorization", "Bearer " <> T.encodeUtf8 token) [ ("Authorization", "Bearer " <> T.encodeUtf8 token)
] :: HTTP.RequestHeaders ] :: HTTP.RequestHeaders
waitForHttpServer (putStr "." *> threadDelay 500000) ("http://localhost:" <> show jsonApiPort <> "/v1/query") headers waitForHttpServer 240 (unsafeProcessHandle ph) (putStr "." *> threadDelay 500000)
("http://localhost:" <> show jsonApiPort <> "/v1/query") headers
a ph a ph
data JsonApiConfig = JsonApiConfig data JsonApiConfig = JsonApiConfig

View File

@ -30,7 +30,6 @@ module DA.Daml.Helper.Util
import Control.Exception.Safe import Control.Exception.Safe
import Control.Monad.Extra import Control.Monad.Extra
import Control.Monad.Loops (untilJust)
import qualified Data.Aeson as Aeson import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSL8 import qualified Data.ByteString.Lazy.Char8 as BSL8
@ -45,7 +44,8 @@ import System.FilePath
import System.IO import System.IO
import System.IO.Extra (withTempFile) import System.IO.Extra (withTempFile)
import System.Info.Extra import System.Info.Extra
import System.Process (showCommandForUser, terminateProcess) import System.Exit (exitFailure)
import System.Process (ProcessHandle, getProcessExitCode, showCommandForUser, terminateProcess)
import System.Process.Typed import System.Process.Typed
import qualified Web.JWT as JWT import qualified Web.JWT as JWT
import qualified Data.Aeson as A import qualified Data.Aeson as A
@ -196,35 +196,60 @@ damlSdkJarFolder = "daml-sdk"
damlSdkJar :: FilePath damlSdkJar :: FilePath
damlSdkJar = damlSdkJarFolder </> "daml-sdk.jar" damlSdkJar = damlSdkJarFolder </> "daml-sdk.jar"
-- | `waitForConnectionOnPort sleep port` keeps trying to establish a TCP connection on the given port. -- | `waitForConnectionOnPort numTries processHandle sleep port` tries to establish a TCP connection
-- Between each connection request it calls `sleep`. -- on the given port, in a given number of tries. Between each connection request it checks that a
waitForConnectionOnPort :: IO () -> Int -> IO () -- certain process is still alive and calls `sleep`.
waitForConnectionOnPort sleep port = do waitForConnectionOnPort :: Int -> ProcessHandle -> IO () -> Int -> IO ()
waitForConnectionOnPort 0 _processHandle _sleep port = do
hPutStrLn stderr ("Failed to connect to port " <> show port <> " in time.")
exitFailure
waitForConnectionOnPort numTries processHandle sleep port = do
let hints = defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV], addrSocketType = Stream } let hints = defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV], addrSocketType = Stream }
addr : _ <- getAddrInfo (Just hints) (Just "127.0.0.1") (Just $ show port) addr : _ <- getAddrInfo (Just hints) (Just "127.0.0.1") (Just $ show port)
untilJust $ do r <- tryIO $ checkConnection addr
r <- tryIO $ checkConnection addr case r of
case r of Right _ -> pure ()
Left _ -> sleep *> pure Nothing Left _ -> do
Right _ -> pure $ Just () sleep
status <- getProcessExitCode processHandle
case status of
Nothing -> waitForConnectionOnPort (numTries-1) processHandle sleep port
Just exitCode -> do
hPutStrLn stderr ("Failed to connect to port " <> show port
<> " before process exited with " <> show exitCode)
exitFailure
where where
checkConnection addr = bracket checkConnection addr = bracket
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)) (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
close close
(\s -> connect s (addrAddress addr)) (\s -> connect s (addrAddress addr))
-- | `waitForHttpServer sleep url` keeps trying to establish an HTTP connection on the given URL. -- | `waitForHttpServer numTries processHandle sleep url headers` tries to establish an HTTP connection on
-- Between each connection request it calls `sleep`. -- the given URL with the given headers, in a given number of tries. Between each connection request
waitForHttpServer :: IO () -> String -> HTTP.RequestHeaders -> IO () -- it checks that a certain process is still alive and calls `sleep`.
waitForHttpServer sleep url headers = do waitForHttpServer :: Int -> ProcessHandle -> IO () -> String -> HTTP.RequestHeaders -> IO ()
waitForHttpServer 0 _processHandle _sleep url _headers = do
hPutStrLn stderr ("Failed to connect to HTTP server " <> url <> " in time.")
exitFailure
waitForHttpServer numTries processHandle sleep url headers = do
request <- HTTP.parseRequest $ "HEAD " <> url request <- HTTP.parseRequest $ "HEAD " <> url
request <- pure (HTTP.setRequestHeaders headers request) request <- pure (HTTP.setRequestHeaders headers request)
untilJust $ do r <- tryJust (\e -> guard (isIOException e || isHttpException e)) $ HTTP.httpNoBody request
r <- tryJust (\e -> guard (isIOException e || isHttpException e)) $ HTTP.httpNoBody request case r of
case r of Right resp | HTTP.statusCode (HTTP.getResponseStatus resp) == 200 -> pure ()
Right resp Right resp -> do
| HTTP.statusCode (HTTP.getResponseStatus resp) == 200 -> pure $ Just () hPutStrLn stderr ("HTTP server " <> url <> " replied with status code "
_ -> sleep *> pure Nothing <> show (HTTP.statusCode (HTTP.getResponseStatus resp)) <> ".")
exitFailure
Left _ -> do
sleep
status <- getProcessExitCode processHandle
case status of
Nothing -> waitForHttpServer (numTries-1) processHandle sleep url headers
Just exitCode -> do
hPutStrLn stderr ("Failed to connect to HTTP server " <> url
<> " before process exited with " <> show exitCode)
exitFailure
where isIOException e = isJust (fromException e :: Maybe IOException) where isIOException e = isJust (fromException e :: Maybe IOException)
isHttpException e = isJust (fromException e :: Maybe HTTP.HttpException) isHttpException e = isJust (fromException e :: Maybe HTTP.HttpException)

View File

@ -65,7 +65,7 @@ hardcodedToken alice = tokenFor [T.pack alice] "sandbox" "AssistantIntegrationTe
authorizationHeaders :: String -> RequestHeaders authorizationHeaders :: String -> RequestHeaders
authorizationHeaders alice = [("Authorization", "Bearer " <> T.encodeUtf8 (hardcodedToken alice))] authorizationHeaders alice = [("Authorization", "Bearer " <> T.encodeUtf8 (hardcodedToken alice))]
withDamlServiceIn :: FilePath -> String -> [String] -> IO a -> IO a withDamlServiceIn :: FilePath -> String -> [String] -> (ProcessHandle -> IO a) -> IO a
withDamlServiceIn path command args act = withDevNull $ \devNull -> do withDamlServiceIn path command args act = withDevNull $ \devNull -> do
let proc' = (shell $ unwords $ ["daml", command] <> args) let proc' = (shell $ unwords $ ["daml", command] <> args)
{ std_out = UseHandle devNull { std_out = UseHandle devNull
@ -73,7 +73,7 @@ withDamlServiceIn path command args act = withDevNull $ \devNull -> do
, cwd = Just path , cwd = Just path
} }
withCreateProcess proc' $ \_ _ _ ph -> do withCreateProcess proc' $ \_ _ _ ph -> do
r <- act r <- act ph
interruptProcessGroupOf ph interruptProcessGroupOf ph
pure r pure r
@ -159,8 +159,8 @@ damlStart tmpDir withCantonSandbox = do
outReader <- forkIO $ forever $ do outReader <- forkIO $ forever $ do
line <- hGetLine startStdout line <- hGetLine startStdout
atomically $ writeTChan outChan line atomically $ writeTChan outChan line
waitForHttpServer waitForHttpServer 120 startPh
(threadDelay 100000) (threadDelay 500000)
("http://localhost:" <> show jsonApiPort <> "/v1/query") ("http://localhost:" <> show jsonApiPort <> "/v1/query")
(authorizationHeaders "Alice") -- dummy party here, not important (authorizationHeaders "Alice") -- dummy party here, not important
scriptOutput <- readFileUTF8 (projDir </> scriptOutputFile) scriptOutput <- readFileUTF8 (projDir </> scriptOutputFile)
@ -207,7 +207,7 @@ quickSandbox projDir = do
]) ])
{std_out = UseHandle devNull, create_group = True, cwd = Just projDir} {std_out = UseHandle devNull, create_group = True, cwd = Just projDir}
(_, _, _, sandboxPh) <- createProcess sandboxProc (_, _, _, sandboxPh) <- createProcess sandboxProc
waitForConnectionOnPort (threadDelay 500000) $ fromIntegral sandboxPort waitForConnectionOnPort 240 sandboxPh (threadDelay 500000) $ fromIntegral sandboxPort
pure $ pure $
QuickSandboxResource QuickSandboxResource
{ quickProjDir = projDir { quickProjDir = projDir
@ -359,10 +359,10 @@ damlToolTests =
, "client-id" , "client-id"
, "--secret" , "--secret"
, "client-secret" , "client-secret"
] $ do ] $ \ ph -> do
let endpoint = let endpoint =
"http://localhost:" <> show middlewarePort <> "/livez" "http://localhost:" <> show middlewarePort <> "/livez"
waitForHttpServer (threadDelay 100000) endpoint [] waitForHttpServer 240 ph (threadDelay 500000) endpoint []
req <- parseRequest endpoint req <- parseRequest endpoint
manager <- newManager defaultManagerSettings manager <- newManager defaultManagerSettings
resp <- httpLbs req manager resp <- httpLbs req manager
@ -456,9 +456,9 @@ damlStartTests getDamlStart =
, "--http-port" , "--http-port"
, show triggerServicePort , show triggerServicePort
, "--wall-clock-time" , "--wall-clock-time"
] $ do ] $ \ ph -> do
let endpoint = "http://localhost:" <> show triggerServicePort <> "/livez" let endpoint = "http://localhost:" <> show triggerServicePort <> "/livez"
waitForHttpServer (threadDelay 100000) endpoint [] waitForHttpServer 240 ph (threadDelay 500000) endpoint []
req <- parseRequest endpoint req <- parseRequest endpoint
manager <- newManager defaultManagerSettings manager <- newManager defaultManagerSettings
resp <- httpLbs req manager resp <- httpLbs req manager
@ -475,9 +475,9 @@ damlStartTests getDamlStart =
, show sandboxPort , show sandboxPort
, "--port" , "--port"
, show navigatorPort , show navigatorPort
] $ do ] $ \ ph -> do
waitForHttpServer waitForHttpServer 240 ph
(threadDelay 100000) (threadDelay 500000)
("http://localhost:" <> show navigatorPort) ("http://localhost:" <> show navigatorPort)
[] []
subtest "Navigator startup via daml ledger outside project directory" $ do subtest "Navigator startup via daml ledger outside project directory" $ do
@ -491,11 +491,11 @@ damlStartTests getDamlStart =
, show sandboxPort , show sandboxPort
, "--port" , "--port"
, show navigatorPort , show navigatorPort
] $ do ] $ \ ph -> do
-- waitForHttpServer will only return once we get a 200 response so we -- waitForHttpServer will only return once we get a 200 response so we
-- dont need to do anything else. -- dont need to do anything else.
waitForHttpServer waitForHttpServer 240 ph
(threadDelay 100000) (threadDelay 500000)
("http://localhost:" <> show navigatorPort) ("http://localhost:" <> show navigatorPort)
[] []
@ -538,8 +538,8 @@ damlStartNotSharedTest = testCase "daml start --sandbox-port=0" $
, "--canton-admin-api-port=0" , "--canton-admin-api-port=0"
, "--canton-domain-public-port=0" , "--canton-domain-public-port=0"
, "--canton-domain-admin-port=0" , "--canton-domain-admin-port=0"
] $ do ] $ \ ph -> do
jsonApiPort <- readPortFile maxRetries (tmpDir </> "jsonapi.port") jsonApiPort <- readPortFile ph maxRetries (tmpDir </> "jsonapi.port")
initialRequest <- initialRequest <-
parseRequest $ parseRequest $
"http://localhost:" <> show jsonApiPort <> "/v1/parties/allocate" "http://localhost:" <> show jsonApiPort <> "/v1/parties/allocate"
@ -667,8 +667,8 @@ quickstartTests quickstartDir mvnDir getSandbox =
, "--port" , "--port"
, show p , show p
, ".daml/dist/quickstart-0.0.1.dar" , ".daml/dist/quickstart-0.0.1.dar"
] $ do ] $ \ ph -> do
waitForConnectionOnPort (threadDelay 100000) p waitForConnectionOnPort 240 ph (threadDelay 500000) p
addr:_ <- getAddrInfo (Just socketHints) (Just "127.0.0.1") (Just $ show p) addr:_ <- getAddrInfo (Just socketHints) (Just "127.0.0.1") (Just $ show p)
bracket bracket
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)) (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
@ -699,7 +699,7 @@ quickstartTests quickstartDir mvnDir getSandbox =
, cwd = Just quickProjDir } , cwd = Just quickProjDir }
withCreateProcess mavenProc $ \_ _ _ mavenPh -> do withCreateProcess mavenProc $ \_ _ _ mavenPh -> do
let url = "http://localhost:" <> show restPort <> "/iou" let url = "http://localhost:" <> show restPort <> "/iou"
waitForHttpServer (threadDelay 1000000) url [] waitForHttpServer 240 mavenPh (threadDelay 500000) url []
threadDelay 5000000 threadDelay 5000000
manager <- newManager defaultManagerSettings manager <- newManager defaultManagerSettings
req <- parseRequest url req <- parseRequest url
@ -836,9 +836,9 @@ cantonTests = testGroup "daml canton-sandbox"
, "--domain-public-port", show domainPublicApiPort , "--domain-public-port", show domainPublicApiPort
, "--domain-admin-port", show domainAdminApiPort , "--domain-admin-port", show domainAdminApiPort
, "--port-file", portFile , "--port-file", portFile
] $ do ] $ \ ph -> do
-- wait for port file to be written -- wait for port file to be written
_ <- readPortFileWith decodeCantonSandboxPort maxRetries portFile _ <- readPortFileWith decodeCantonSandboxPort ph maxRetries portFile
step "Uploading DAR" step "Uploading DAR"
callCommandSilentIn (dir </> "skeleton") $ unwords callCommandSilentIn (dir </> "skeleton") $ unwords
["daml ledger upload-dar --host=localhost --port=" <> show ledgerApiPort, ".daml/dist/skeleton-0.0.1.dar"] ["daml ledger upload-dar --host=localhost --port=" <> show ledgerApiPort, ".daml/dist/skeleton-0.0.1.dar"]

View File

@ -35,6 +35,7 @@ da_haskell_library(
"optparse-applicative", "optparse-applicative",
"pretty-show", "pretty-show",
"pretty", "pretty",
"process",
"random", "random",
"safe", "safe",
"safe-exceptions", "safe-exceptions",

View File

@ -13,20 +13,38 @@ import Safe (readMay)
import System.Exit import System.Exit
import System.IO import System.IO
import System.IO.Error import System.IO.Error
import System.Process
readPortFileWith :: (String -> Maybe t) -> Int -> String -> IO t readOnce :: (String -> Maybe t) -> FilePath -> IO (Maybe t)
readPortFileWith _ 0 file = do readOnce parseFn file = catchJust
(guard . shouldCatch)
(parseFn <$> readFile file)
(const $ pure Nothing)
readPortFileWith :: (String -> Maybe t) -> ProcessHandle -> Int -> FilePath -> IO t
readPortFileWith _ _ 0 file = do
T.hPutStrLn stderr ("Port file was not written to '" <> pack file <> "' in time.") T.hPutStrLn stderr ("Port file was not written to '" <> pack file <> "' in time.")
exitFailure exitFailure
readPortFileWith parseFn n file = do readPortFileWith parseFn ph n file = do
fileContent <- catchJust (guard . shouldCatch) (readFile file) (const $ pure "") result <- readOnce parseFn file
case parseFn fileContent of case result of
Nothing -> do
threadDelay (1000 * retryDelayMillis)
readPortFileWith parseFn (n-1) file
Just p -> pure p Just p -> pure p
Nothing -> do
status <- getProcessExitCode ph
case status of
Nothing -> do -- Process still active. Try again.
threadDelay (1000 * retryDelayMillis)
readPortFileWith parseFn ph (n-1) file
Just exitCode -> do -- Process exited already. Try reading one last time, then give up.
threadDelay (1000 * retryDelayMillis)
result <- readOnce parseFn file
case result of
Just p -> pure p
Nothing -> do
T.hPutStrLn stderr ("Port file was not written to '" <> pack file <> "' before process exit with " <> pack (show exitCode))
exitFailure
readPortFile :: Int -> String -> IO Int readPortFile :: ProcessHandle -> Int -> FilePath -> IO Int
readPortFile = readPortFileWith readMay readPortFile = readPortFileWith readMay
-- On Windows we sometimes get permission errors. It looks like -- On Windows we sometimes get permission errors. It looks like

View File

@ -58,10 +58,10 @@ createHttpJson httpJsonOutput getLedgerPort HttpJsonConfig {actor, mbSharedSecre
writeFileUTF8 tokenFile $ T.unpack token writeFileUTF8 tokenFile $ T.unpack token
httpJsonProc <- getHttpJsonProc getLedgerPort portFile httpJsonProc <- getHttpJsonProc getLedgerPort portFile
mask $ \unmask -> do mask $ \unmask -> do
ph <- createProcess httpJsonProc {std_out = UseHandle httpJsonOutput} ph@(_,_,_,ph') <- createProcess httpJsonProc {std_out = UseHandle httpJsonOutput}
let cleanup = cleanupProcess ph >> rmTmpDir let cleanup = cleanupProcess ph >> rmTmpDir
let waitForStart = do let waitForStart = do
port <- readPortFile maxRetries portFile port <- readPortFile ph' maxRetries portFile
pure pure
(HttpJsonResource (HttpJsonResource
{ httpJsonProcess = ph { httpJsonProcess = ph

View File

@ -88,9 +88,9 @@ createSandbox :: FilePath -> Handle -> SandboxConfig -> IO SandboxResource
createSandbox portFile sandboxOutput conf = do createSandbox portFile sandboxOutput conf = do
sandboxProc <- getSandboxProc conf portFile sandboxProc <- getSandboxProc conf portFile
mask $ \unmask -> do mask $ \unmask -> do
ph <- createProcess sandboxProc { std_out = UseHandle sandboxOutput } ph@(_,_,_,ph') <- createProcess sandboxProc { std_out = UseHandle sandboxOutput }
let waitForStart = do let waitForStart = do
port <- readPortFile maxRetries portFile port <- readPortFile ph' maxRetries portFile
pure (SandboxResource ph port) pure (SandboxResource ph port)
unmask (waitForStart `onException` cleanupProcess ph) unmask (waitForStart `onException` cleanupProcess ph)