2 {-# LANGUAGE DataKinds #-}
3 {-# LANGUAGE ScopedTypeVariables #-}
4 {-# LANGUAGE NondecreasingIndentation #-}
5 -- | A GHC run-server, which supports running multiple GHC scripts
6 -- without having to restart from scratch.
7 module Test
.Cabal
.Server
(
18 import Test
.Cabal
.Script
19 import Test
.Cabal
.TestCode
21 import Prelude
hiding (log)
22 import Control
.Concurrent
.MVar
23 import Control
.Concurrent
24 import Control
.Concurrent
.Async
28 import Data
.List
(intercalate
, isPrefixOf)
29 import Distribution
.Simple
.Program
.Db
30 import Distribution
.Simple
.Program
31 import Control
.Exception
32 import qualified Control
.Exception
as E
36 import Text
.Read (readMaybe
)
37 import Foreign
.C
.Error
(Errno
(..), ePIPE
)
39 import qualified GHC
.IO.Exception
as GHC
41 import Distribution
.Verbosity
43 import System
.Process
.Internals
44 ( ProcessHandle__
( OpenHandle
)
48 import qualified System
.Win32
.Process
as Win32
51 -- TODO: Compare this implementation with
52 -- https://github.com/ndmitchell/ghcid/blob/master/src/Language/Haskell/Ghcid.hs
53 -- which does something similar
55 -- ----------------------------------------------------------------- --
57 -- ----------------------------------------------------------------- --
59 -- | A GHCi server session, which we can ask to run scripts.
60 -- It operates in a *fixed* runner environment as specified
61 -- by 'serverScriptEnv'.
62 data Server
= Server
{
63 serverStdin
:: Handle,
64 serverStdout
:: Handle,
65 serverStderr
:: Handle,
66 serverProcessHandle
:: ProcessHandle
,
67 serverProcessId
:: ProcessId
,
68 serverScriptEnv
:: ScriptEnv
,
69 -- | Accumulators which we use to keep tracking
70 -- of stdout/stderr we've incrementally read out. In the event
71 -- of an error we'll use this to give diagnostic information.
72 serverStdoutAccum
:: MVar
[String],
73 serverStderrAccum
:: MVar
[String],
74 serverLogChan
:: Chan ServerLogMsg
77 -- | Portable representation of process ID; just a string rendered
79 type ProcessId
= String
81 data ServerLogMsg
= ServerLogMsg ServerLogMsgType
String
83 data ServerLogMsgType
= ServerOut ProcessId
86 | ServerMeta ProcessId
89 data ServerResult
= ServerResult
90 { serverResultTestCode
:: TestCode
91 , serverResultCommand
:: String
92 , serverResultStdout
:: String
93 , serverResultStderr
:: String
96 -- | With 'ScriptEnv', create a new GHCi 'Server' session.
97 -- When @f@ returns, the server is terminated and no longer
99 withNewServer
:: Chan ServerLogMsg
-> ScriptEnv
-> (Server
-> IO a
) -> IO a
100 withNewServer chan senv f
=
101 bracketWithInit
(startServer chan senv
) initServer stopServer f
103 -- | Like 'bracket', but with an initialization function on the resource
104 -- which will be called, unmasked, on the resource to transform it
105 -- in some way. If the initialization function throws an exception, the cleanup
106 -- handler will get invoked with the original resource; if it succeeds, the
107 -- cleanup handler will get invoked with the transformed resource.
108 -- The cleanup handler must be able to handle both cases.
110 -- This can help avoid race conditions in certain situations: with
111 -- normal use of 'bracket', the resource acquisition function
112 -- MUST return immediately after the resource is acquired. If it
113 -- performs any interruptible actions afterwards, it could be
114 -- interrupted and the exception handler not called.
115 bracketWithInit
:: IO a
-> (a
-> IO a
) -> (a
-> IO b
) -> (a
-> IO c
) -> IO c
116 bracketWithInit before initialize after thing
=
117 mask
$ \restore
-> do
119 a
<- restore
(initialize a0
) `onException` uninterruptibleMask_
(after a0
)
120 r
<- restore
(thing a
) `onException` uninterruptibleMask_
(after a
)
121 _
<- uninterruptibleMask_
(after a
)
124 -- | Run an hs script on the GHCi server, returning the 'ServerResult' of
125 -- executing the command.
127 -- * The script MUST have an @hs@ or @lhs@ filename; GHCi
128 -- will reject non-Haskell filenames.
130 -- * If the script is not well-typed, the returned output
131 -- will be of GHC's compile errors.
133 -- * Inside your script, do not rely on 'getProgName' having
136 -- * Current working directory and environment overrides
137 -- are currently not implemented.
139 runOnServer
:: Server
-> Maybe FilePath -> [(String, Maybe String)]
140 -> FilePath -> [String] -> IO ServerResult
141 runOnServer s mb_cwd env_overrides script_path args
= do
142 -- TODO: cwd not implemented
143 when (isJust mb_cwd
) $ error "runOnServer change directory not implemented"
144 -- TODO: env_overrides not implemented
145 unless (null env_overrides
) $ error "runOnServer set environment not implemented"
147 -- Set arguments returned by System.getArgs
148 write s
$ ":set args " ++ show args
149 -- Output start sigil (do it here so we pick up compilation
151 write s
$ "System.IO.hPutStrLn System.IO.stdout " ++ show start_sigil
152 write s
$ "System.IO.hPutStrLn System.IO.stderr " ++ show start_sigil
153 _
<- readUntilSigil s start_sigil IsOut
154 _
<- readUntilSigil s start_sigil IsErr
155 -- Drain the output produced by the script as we are running so that
156 -- we do not deadlock over a full pipe.
157 withAsync
(readUntilEnd s IsOut
) $ \a_exit_out
-> do
158 withAsync
(readUntilSigil s end_sigil IsErr
) $ \a_err
-> do
159 -- NB: No :set prog; don't rely on this value in test scripts,
160 -- we pass it in via the arguments
161 -- NB: load drops all bindings, which is GOOD. Avoid holding onto
163 write s
$ ":load " ++ script_path
164 -- Create a ref which will record the exit status of the command
165 -- NB: do this after :load so it doesn't get dropped
166 write s
$ "ref <- Data.IORef.newIORef Test.Cabal.TestCode.TestCodeFail"
167 -- TODO: What if an async exception gets raised here? At the
168 -- moment, there is no way to recover until we get to the top-level
169 -- bracket; then stopServer which correctly handles this case.
170 -- If you do want to be able to abort this computation but KEEP
171 -- USING THE SERVER SESSION, you will need to have a lot more
172 -- sophisticated logic.
173 write s
$ "Test.Cabal.Server.runMain ref Main.main"
175 -- NB: We're line-oriented, so we MUST add an extra newline
176 -- to ensure that we see the end sigil.
177 write s
$ "System.IO.hPutStrLn System.IO.stdout " ++ show ""
178 write s
$ "System.IO.hPutStrLn System.IO.stderr " ++ show ""
179 write s
$ "Data.IORef.readIORef ref >>= \\e -> " ++
180 " System.IO.hPutStrLn System.IO.stdout (" ++ show end_sigil
++ " ++ \" \" ++ show e)"
181 write s
$ "System.IO.hPutStrLn System.IO.stderr " ++ show end_sigil
182 (code
, out
) <- wait a_exit_out
185 -- Give the user some indication about how they could run the
187 (real_path
, real_args
) <- runnerCommand
(serverScriptEnv s
) mb_cwd env_overrides script_path args
190 serverResultTestCode
= code
,
191 serverResultCommand
= showCommandForUser real_path real_args
,
192 serverResultStdout
= out
,
193 serverResultStderr
= err
196 -- | Helper function which we use in the GHCi session to communicate
197 -- the exit code of the process.
198 runMain
:: IORef TestCode
-> IO () -> IO ()
200 E
.catch (m
>> writeIORef ref TestCodeOk
) serverHandler
202 serverHandler
:: SomeException
-> IO ()
204 -- TODO: Probably a few more cases you could handle;
205 -- e.g., StackOverflow should return ExitCode 2; also signals.
206 writeIORef ref
$ case fromException e
of
207 Just test_code
-> test_code
210 -- Only rethrow for non ExitFailure exceptions
211 case fromException e
:: Maybe TestCode
of
215 -- ----------------------------------------------------------------- --
216 -- Initialize/tear down
217 -- ----------------------------------------------------------------- --
219 -- | Start a new GHCi session.
220 startServer
:: Chan ServerLogMsg
-> ScriptEnv
-> IO Server
221 startServer chan senv
= do
222 (prog
, _
) <- requireProgram verbosity ghcProgram
(runnerProgramDb senv
)
223 let ghc_args
= runnerGhcArgs senv Nothing
++ ["--interactive", "-v0", "-ignore-dot-ghci"]
224 proc_spec
= (proc
(programPath prog
) ghc_args
) {
226 -- Closing fds is VERY important to avoid
227 -- deadlock; we won't see the end of a
228 -- stream until everyone gives up.
231 std_out
= CreatePipe
,
234 when (verbosity
>= verbose
) $
235 writeChan chan
(ServerLogMsg AllServers
(showCommandForUser
(programPath prog
) ghc_args
))
236 (Just hin
, Just hout
, Just herr
, proch
) <- createProcess proc_spec
237 out_acc
<- newMVar
[]
238 err_acc
<- newMVar
[]
244 serverProcessHandle
= proch
,
245 serverProcessId
= show tid
,
246 serverLogChan
= chan
,
247 serverStdoutAccum
= out_acc
,
248 serverStderrAccum
= err_acc
,
249 serverScriptEnv
= senv
252 verbosity
= runnerVerbosity senv
254 -- | Unmasked initialization for the server
255 initServer
:: Server
-> IO Server
257 -- NB: withProcessHandle reads an MVar and is interruptible
259 pid
<- withProcessHandle
(serverProcessHandle s0
) $ \ph
->
262 OpenHandle x
-> fmap show (Win32
.getProcessId x
)
264 OpenHandle x
-> return (show x
)
266 -- TODO: handle OpenExtHandle?
267 _
-> return (serverProcessId s0
)
269 let s
= s0
{ serverProcessId
= pid
}
270 -- We will read/write a line at a time, including for
271 -- output; our demarcation tokens are an entire line.
272 forM_
[serverStdin
, serverStdout
, serverStderr
] $ \f -> do
273 hSetBuffering (f s
) LineBuffering
274 hSetEncoding
(f s
) utf8
275 write s
":set prompt \"\""
276 write s
"System.IO.hSetBuffering System.IO.stdout System.IO.LineBuffering"
279 -- | Stop a GHCi session.
280 stopServer
:: Server
-> IO ()
282 -- This is quite a bit of funny business.
283 -- On Linux, terminateProcess will send a SIGINT, which
284 -- GHCi will swallow and actually only use to terminate
285 -- whatever computation is going on at that time. So we
286 -- have to follow up with an actual :quit command to
287 -- finish it up (if you delete it, the processes will
288 -- hang around). On Windows, this will just actually kill
289 -- the process so the rest should be unnecessary.
290 mb_exit
<- getProcessExitCode
(serverProcessHandle s
)
293 threadDelay
2000000 -- 2sec
294 log ServerMeta s
$ "Terminating..."
295 terminateProcess
(serverProcessHandle s
)
297 -- Ask to quit. If we're in the middle of a computation,
298 -- this will buffer up (unless the program is intercepting
299 -- stdin, but that should NOT happen.)
300 ignore
$ write s
":quit"
302 -- NB: it's important that we used create_group. We
303 -- run this AFTER write s ":quit" because if we C^C
304 -- sufficiently early in GHCi startup process, GHCi
305 -- will actually die, and then hClose will fail because
306 -- the ":quit" command was buffered up but never got
308 interruptProcessGroupOf
(serverProcessHandle s
)
310 log ServerMeta s
$ "Waiting..."
311 -- Close input BEFORE waiting, close output AFTER waiting.
312 -- If you get either order wrong, deadlock!
313 ignoreSigPipe
$ hClose (serverStdin s
)
314 -- waitForProcess has race condition
315 -- https://github.com/haskell/process/issues/46
316 waitForProcess
$ serverProcessHandle s
319 r
<- hGetContents (f s
)
320 _
<- evaluate
(length r
)
324 withAsync
(drain serverStdout
) $ \a_out
-> do
325 withAsync
(drain serverStderr
) $ \a_err
-> do
329 log ServerMeta s
$ "Terminating GHCi"
330 race hardKiller softKiller
332 log ServerMeta s
$ "GHCi died unexpectedly"
335 -- Drain the output buffers
336 rest_out
<- wait a_out
337 rest_err
<- wait a_err
338 if r
/= Right ExitSuccess
&&
339 r
/= Right
(ExitFailure
(-2)) -- SIGINT; happens frequently for some reason
340 then do withMVar
(serverStdoutAccum s
) $ \acc
->
341 mapM_ (info ServerOut s
) (reverse acc
)
342 info ServerOut s rest_out
343 withMVar
(serverStderrAccum s
) $ \acc
->
344 mapM_ (info ServerErr s
) (reverse acc
)
345 info ServerErr s rest_err
348 Left
() -> "GHCi was forcibly terminated"
349 Right exit
-> "GHCi exited with " ++ show exit
) ++
350 if verbosity
< verbose
351 then " (use -v for more information)"
353 else log ServerOut s rest_out
355 log ServerMeta s
$ "Done"
358 verbosity
= runnerVerbosity
(serverScriptEnv s
)
360 ignoreSigPipe
:: IO () -> IO ()
361 ignoreSigPipe
= E
.handle
$ \e
-> case e
of
362 GHC
.IOError { GHC
.ioe_type
= GHC
.ResourceVanished
, GHC
.ioe_errno
= Just ioe
}
363 | Errno ioe
== ePIPE
-> return ()
366 -- Using the procedure from
367 -- https://www.schoolofhaskell.com/user/snoyberg/general-haskell/exceptions/catching-all-exceptions
368 ignore
:: IO () -> IO ()
369 ignore m
= withAsync m
$ \a -> void
(waitCatch a
)
371 -- ----------------------------------------------------------------- --
373 -- ----------------------------------------------------------------- --
375 log :: (ProcessId
-> ServerLogMsgType
) -> Server
-> String -> IO ()
377 when (verbosity
>= verbose
) $ info ctor s msg
379 verbosity
= runnerVerbosity
(serverScriptEnv s
)
381 info
:: (ProcessId
-> ServerLogMsgType
) -> Server
-> String -> IO ()
383 writeChan chan
(ServerLogMsg
(ctor
(serverProcessId s
)) msg
)
385 chan
= serverLogChan s
387 -- | Write a string to the prompt of the GHCi server.
388 write
:: Server
-> String -> IO ()
391 hPutStrLn (serverStdin s
) msg
392 hFlush (serverStdin s
) -- line buffering should get it, but just for good luck
394 accumulate
:: MVar
[String] -> String -> IO ()
396 modifyMVar_ acc
(\msgs
-> return (msg
:msgs
))
398 flush
:: MVar
[String] -> IO [String]
399 flush acc
= modifyMVar acc
(\msgs
-> return ([], reverse msgs
))
401 data OutOrErr
= IsOut | IsErr
403 serverHandle
:: Server
-> OutOrErr
-> Handle
404 serverHandle s IsOut
= serverStdout s
405 serverHandle s IsErr
= serverStderr s
407 serverAccum
:: Server
-> OutOrErr
-> MVar
[String]
408 serverAccum s IsOut
= serverStdoutAccum s
409 serverAccum s IsErr
= serverStderrAccum s
411 outOrErrMsgType
:: OutOrErr
-> (ProcessId
-> ServerLogMsgType
)
412 outOrErrMsgType IsOut
= ServerOut
413 outOrErrMsgType IsErr
= ServerErr
415 -- | Consume output from the GHCi server until we hit a "start
416 -- sigil" (indicating that the subsequent output is for the
417 -- command we want.) Call this only immediately after you
418 -- send a command to GHCi to emit the start sigil.
419 readUntilSigil
:: Server
-> String -> OutOrErr
-> IO String
420 readUntilSigil s sigil outerr
= do
421 l
<- hGetLine (serverHandle s outerr
)
422 log (outOrErrMsgType outerr
) s l
423 if sigil `
isPrefixOf` l
-- NB: on Windows there might be extra goo at end
424 then intercalate
"\n" `
fmap` flush
(serverAccum s outerr
)
425 else do accumulate
(serverAccum s outerr
) l
426 readUntilSigil s sigil outerr
428 -- | Consume output from the GHCi server until we hit the
429 -- end sigil. Return the consumed output as well as the
430 -- exit code (which is at the end of the sigil).
431 readUntilEnd
:: Server
-> OutOrErr
-> IO (TestCode
, String)
432 readUntilEnd s outerr
= go
[]
435 l
<- hGetLine (serverHandle s outerr
)
436 log (outOrErrMsgType outerr
) s l
437 if end_sigil `
isPrefixOf` l
438 -- NB: NOT unlines, we don't want the trailing newline!
439 then do exit
<- evaluate
(parseExit l
)
440 _
<- flush
(serverAccum s outerr
) -- TODO: don't toss this out
441 return (exit
, intercalate
"\n" (reverse rs
))
442 else do accumulate
(serverAccum s outerr
) l
444 parseExit l
= case readMaybe
(drop (length end_sigil
) l
) of
445 Nothing
-> error $ "Cannot parse TestCode at the end of: " ++ l
448 -- | The start and end sigils. This should be chosen to be
449 -- reasonably unique, so that test scripts don't accidentally
450 -- generate them. If these get spuriously generated, we will
451 -- probably deadlock.
452 start_sigil
, end_sigil
:: String
453 start_sigil
= "BEGIN Test.Cabal.Server"
454 end_sigil
= "END Test.Cabal.Server"