Rodrigo Mesquita pushed to branch wip/romes/async-rpc at Glasgow Haskell Compiler / GHC
Commits:
-
ee89cbd5
by Rodrigo Mesquita at 2026-10-14T11:43:41+01:00
-
4771d003
by Rodrigo Mesquita at 2026-10-14T11:43:41+01:00
10 changed files:
- compiler/GHC/Runtime/Interpreter.hs
- compiler/GHC/Runtime/Interpreter/JS.hs
- compiler/GHC/Runtime/Interpreter/Process.hs
- compiler/GHC/Runtime/Interpreter/Types.hs
- compiler/GHC/Runtime/Interpreter/Wasm.hs
- compiler/GHC/Tc/Gen/Splice.hs
- libraries/ghci/GHCi/Message.hs
- libraries/ghci/GHCi/Server.hs
- libraries/ghci/ghci.cabal.in
- testsuite/tests/ghci/custom-external-interpreter-commands/Main.hs
Changes:
| ... | ... | @@ -626,11 +626,9 @@ spawnIServ conf = do |
| 626 | 626 | []
|
| 627 | 627 | (iservConfOpts conf)
|
| 628 | 628 | interpPipe <- mkPipeFromHandles rh wh
|
| 629 | - lock <- newMVar ()
|
|
| 630 | 629 | let process = InterpProcess
|
| 631 | 630 | { interpHandle = ph
|
| 632 | 631 | , interpPipe
|
| 633 | - , interpLock = lock
|
|
| 634 | 632 | }
|
| 635 | 633 | |
| 636 | 634 | pending_frees <- newMVar []
|
| ... | ... | @@ -127,11 +127,9 @@ startTHRunnerProcess interp_js settings = do |
| 127 | 127 | std_in <- readIORef interp_in
|
| 128 | 128 | |
| 129 | 129 | interpPipe <- mkPipeFromHandles rh wh
|
| 130 | - lock <- newMVar ()
|
|
| 131 | 130 | let proc = InterpProcess
|
| 132 | 131 | { interpHandle = hdl
|
| 133 | 132 | , interpPipe
|
| 134 | - , interpLock = lock
|
|
| 135 | 133 | }
|
| 136 | 134 | pure (std_in, proc)
|
| 137 | 135 | |
| ... | ... | @@ -382,7 +380,7 @@ jsLinkPlan logger tmpfs tmp_dir ar_cache link_cfg cfg inst link_plan = do |
| 382 | 380 | -- Haskell server.
|
| 383 | 381 | server_started <- jsServerStarted <$> readMVar (instJSState (instExtra inst))
|
| 384 | 382 | if server_started
|
| 385 | - then sendMessageNoResponse inst $ LoadObj all_js
|
|
| 383 | + then sendMessage inst $ LoadObj all_js
|
|
| 386 | 384 | else jsLoadFile inst all_js
|
| 387 | 385 | |
| 388 | 386 | ----------------------------------------------------------------
|
| ... | ... | @@ -2,16 +2,8 @@ module GHC.Runtime.Interpreter.Process |
| 2 | 2 | (
|
| 3 | 3 | -- * Message API
|
| 4 | 4 | Message(..)
|
| 5 | - , DelayedResponse (..)
|
|
| 6 | - -- * Top-level message API (these acquire/release a lock)
|
|
| 7 | 5 | , sendMessage
|
| 8 | - , sendMessageNoResponse
|
|
| 9 | - , sendMessageDelayedResponse
|
|
| 10 | - , receiveDelayedResponse
|
|
| 11 | - -- * Nested message API (these require the interpreter to already be locked)
|
|
| 12 | - , sendAnyValue
|
|
| 13 | - , receiveAnyValue
|
|
| 14 | - , receiveTHMessage
|
|
| 6 | + , sendTHComputation
|
|
| 15 | 7 | )
|
| 16 | 8 | where
|
| 17 | 9 | |
| ... | ... | @@ -27,97 +19,62 @@ import GHC.Utils.Exception as Ex |
| 27 | 19 | import Data.Binary
|
| 28 | 20 | import System.Exit
|
| 29 | 21 | import System.Process
|
| 30 | -import Control.Concurrent.MVar (MVar, withMVar, takeMVar, putMVar, isEmptyMVar)
|
|
| 31 | - |
|
| 32 | -data DelayedResponse a = DelayedResponse
|
|
| 22 | +import Control.Concurrent
|
|
| 23 | +import Data.IORef
|
|
| 24 | +import Control.Concurrent.STM
|
|
| 33 | 25 | |
| 34 | 26 | -- -----------------------------------------------------------------------------
|
| 35 | 27 | -- Top-level Message API
|
| 36 | 28 | |
| 37 | --- | Send a message to the interpreter process that doesn't expect a response
|
|
| 38 | --- (locks the interpreter while sending)
|
|
| 39 | -sendMessageNoResponse :: ExtInterpInstance d -> Message () -> IO ()
|
|
| 40 | -sendMessageNoResponse i m =
|
|
| 41 | - withLock i $ writeInterpProcess (instProcess i) (putMessage m)
|
|
| 42 | - |
|
| 43 | --- | Send a message to the interpreter that expects a response
|
|
| 44 | --- (locks the interpreter while until the response is received)
|
|
| 29 | +-- | Send a message to the interpreter and expect a response, blocking until it arrives.
|
|
| 30 | +--
|
|
| 31 | +-- This issues an asynchronous RPC on the external interpreter which does not
|
|
| 32 | +-- block the external interpreter thread. We block here on the host because we
|
|
| 33 | +-- wait for the asynchronous response to arrive.
|
|
| 45 | 34 | sendMessage :: Binary a => ExtInterpInstance d -> Message a -> IO a
|
| 46 | -sendMessage i m = withLock i $ callInterpProcess (instProcess i) m
|
|
| 47 | - |
|
| 48 | --- | Send a message to the interpreter process whose response is expected later
|
|
| 35 | +sendMessage i msg =
|
|
| 36 | + remoteCall (interpPipe proc) msg
|
|
| 37 | + `catchException` \(e :: SomeException) -> handleInterpProcessFailure proc e
|
|
| 38 | + where
|
|
| 39 | + proc = instProcess i
|
|
| 40 | + |
|
| 41 | +-- | Send a message with a TH computation to the ext. interpreter (like
|
|
| 42 | +-- 'RunTH', or 'RunModFinalizers') and handle 'THMessage' requests from the
|
|
| 43 | +-- ext. interpreter to us in the meantime.
|
|
| 49 | 44 | --
|
| 50 | --- This is useful to avoid forgetting to receive the value and to ensure that
|
|
| 51 | --- the type of the response isn't lost. Use receiveDelayedResponse to read it.
|
|
| 52 | --- (locks the interpreter until the response is received using
|
|
| 53 | --- `receiveDelayedResponse`)
|
|
| 54 | -sendMessageDelayedResponse :: ExtInterpInstance d -> Message a -> IO (DelayedResponse a)
|
|
| 55 | -sendMessageDelayedResponse i m = do
|
|
| 56 | - lock i
|
|
| 57 | - writeInterpProcess (instProcess i) (putMessage m)
|
|
| 58 | - pure DelayedResponse
|
|
| 59 | - |
|
| 60 | --- | Expect a delayed result to be received now
|
|
| 61 | -receiveDelayedResponse :: Binary a => ExtInterpInstance d -> DelayedResponse a -> IO a
|
|
| 62 | -receiveDelayedResponse i DelayedResponse = do
|
|
| 63 | - ensureLocked i
|
|
| 64 | - r <- readInterpProcess (instProcess i) get
|
|
| 65 | - unlock i
|
|
| 66 | - pure r
|
|
| 67 | - |
|
| 68 | --- -----------------------------------------------------------------------------
|
|
| 69 | --- Nested Message API
|
|
| 70 | - |
|
| 71 | --- | Send any value (requires locked interpreter)
|
|
| 72 | -sendAnyValue :: Binary a => ExtInterpInstance d -> a -> IO ()
|
|
| 73 | -sendAnyValue i m = ensureLocked i >> writeInterpProcess (instProcess i) (put m)
|
|
| 74 | - |
|
| 75 | --- | Expect a value to be received (requires locked interpreter)
|
|
| 76 | -receiveAnyValue :: ExtInterpInstance d -> Get a -> IO a
|
|
| 77 | -receiveAnyValue i get = ensureLocked i >> readInterpProcess (instProcess i) get
|
|
| 78 | - |
|
| 79 | --- | Wait for a Template Haskell message (requires locked interpreter)
|
|
| 80 | -receiveTHMessage :: ExtInterpInstance d -> IO THMsg
|
|
| 81 | -receiveTHMessage i = ensureLocked i >> receiveAnyValue i getTHMessage
|
|
| 82 | - |
|
| 83 | --- -----------------------------------------------------------------------------
|
|
| 84 | - |
|
| 85 | -getLock :: ExtInterpInstance d -> MVar ()
|
|
| 86 | -getLock = interpLock . instProcess
|
|
| 87 | - |
|
| 88 | -withLock :: ExtInterpInstance d -> IO a -> IO a
|
|
| 89 | -withLock i f = withMVar (getLock i) (const f)
|
|
| 90 | - |
|
| 91 | -lock :: ExtInterpInstance d -> IO ()
|
|
| 92 | -lock i = takeMVar (getLock i)
|
|
| 93 | - |
|
| 94 | -unlock :: ExtInterpInstance d -> IO ()
|
|
| 95 | -unlock i = putMVar (getLock i) ()
|
|
| 96 | - |
|
| 97 | -ensureLocked :: ExtInterpInstance d -> IO ()
|
|
| 98 | -ensureLocked i =
|
|
| 99 | - isEmptyMVar (getLock i) >>= \case
|
|
| 100 | - False -> panic "ensureLocked: external interpreter not locked"
|
|
| 101 | - _ -> pure ()
|
|
| 102 | - |
|
| 103 | - |
|
| 104 | --- | Send a 'Message' and receive the response from the interpreter process
|
|
| 105 | -callInterpProcess :: Binary a => InterpProcess -> Message a -> IO a
|
|
| 106 | -callInterpProcess i msg =
|
|
| 107 | - remoteCall (interpPipe i) msg
|
|
| 108 | - `catchException` \(e :: SomeException) -> handleInterpProcessFailure i e
|
|
| 109 | - |
|
| 110 | --- | Read a value from the interpreter process
|
|
| 111 | -readInterpProcess :: InterpProcess -> Get a -> IO a
|
|
| 112 | -readInterpProcess i get =
|
|
| 113 | - readPipe (interpPipe i) get
|
|
| 114 | - `catchException` \(e :: SomeException) -> handleInterpProcessFailure i e
|
|
| 115 | - |
|
| 116 | --- | Send a value to the interpreter process
|
|
| 117 | -writeInterpProcess :: InterpProcess -> Put -> IO ()
|
|
| 118 | -writeInterpProcess i put =
|
|
| 119 | - writePipe (interpPipe i) put
|
|
| 120 | - `catchException` \(e :: SomeException) -> handleInterpProcessFailure i e
|
|
| 45 | +-- This function blocks until the TH computation returns a value, but spawns in
|
|
| 46 | +-- the background a thread to handle the 'THMessage's until the TH computation
|
|
| 47 | +-- is fully done.
|
|
| 48 | +sendTHComputation
|
|
| 49 | + :: Binary a
|
|
| 50 | + => ExtInterpInstance d
|
|
| 51 | + -> Message a
|
|
| 52 | + -> (forall b. Binary b => THMessage b -> IO (Bool, b))
|
|
| 53 | + -- ^ Handle one message. Return True to end the loop.
|
|
| 54 | + -- This handler must never uninterrutibly mask an expression which may get
|
|
| 55 | + -- stuck. If it does, then 'sendTHComputation' will also get uninterruptibly stuck.
|
|
| 56 | + -> IO a
|
|
| 57 | +sendTHComputation i msg reqHandler = mask $ \restore -> do
|
|
| 58 | + -- inline "withAsync/wait"
|
|
| 59 | + var <- newEmptyTMVarIO
|
|
| 60 | + ht <- forkFinally (restore handlerLoop) (atomically . putTMVar var)
|
|
| 61 | + restore (sendMessage i msg <* atomically (readTMVar var))
|
|
| 62 | + `onException` uninterruptibleMask_ (throwTo ht CancelTHHandler)
|
|
| 63 | + where
|
|
| 64 | + proc = instProcess i
|
|
| 65 | + handleOne doneRef = do
|
|
| 66 | + handleRemoteTHCall (interpPipe proc) $ \req -> do
|
|
| 67 | + (stop, res) <- reqHandler req
|
|
| 68 | + writeIORef doneRef stop
|
|
| 69 | + return res
|
|
| 70 | + handlerLoop = do
|
|
| 71 | + doneRef <- newIORef False
|
|
| 72 | + let loop = do
|
|
| 73 | + handleOne doneRef
|
|
| 74 | + isDone <- readIORef doneRef
|
|
| 75 | + if isDone then return ()
|
|
| 76 | + else loop
|
|
| 77 | + in loop
|
|
| 121 | 78 | |
| 122 | 79 | handleInterpProcessFailure :: InterpProcess -> SomeException -> IO a
|
| 123 | 80 | handleInterpProcessFailure i e = do
|
| ... | ... | @@ -130,3 +87,8 @@ handleInterpProcessFailure i e = do |
| 130 | 87 | terminateProcess hdl
|
| 131 | 88 | _ <- waitForProcess hdl
|
| 132 | 89 | throw e
|
| 90 | + |
|
| 91 | +data CancelTHHandler = CancelTHHandler deriving Show
|
|
| 92 | +instance Exception CancelTHHandler where
|
|
| 93 | + fromException = asyncExceptionFromException
|
|
| 94 | + toException = asyncExceptionToException |
| ... | ... | @@ -111,7 +111,6 @@ type WasmInterp = ExtInterpState WasmInterpConfig () |
| 111 | 111 | data InterpProcess = InterpProcess
|
| 112 | 112 | { interpPipe :: !Pipe -- ^ Pipe to communicate with the server
|
| 113 | 113 | , interpHandle :: !ProcessHandle -- ^ Process handle of the server
|
| 114 | - , interpLock :: !(MVar ()) -- ^ Lock to prevent concurrent access to the stream
|
|
| 115 | 114 | }
|
| 116 | 115 | |
| 117 | 116 | -- | Status of an external interpreter
|
| ... | ... | @@ -73,14 +73,12 @@ spawnWasmInterp WasmInterpConfig {..} = do |
| 73 | 73 | hSetBuffering rh NoBuffering
|
| 74 | 74 | interpPipe <- mkPipeFromHandles rh wh
|
| 75 | 75 | pending_frees <- newMVar []
|
| 76 | - lock <- newMVar ()
|
|
| 77 | 76 | pure
|
| 78 | 77 | $ ExtInterpInstance
|
| 79 | 78 | { instProcess =
|
| 80 | 79 | InterpProcess
|
| 81 | 80 | { interpHandle = ph,
|
| 82 | - interpPipe,
|
|
| 83 | - interpLock = lock
|
|
| 81 | + interpPipe
|
|
| 84 | 82 | },
|
| 85 | 83 | instPendingFrees = pending_frees,
|
| 86 | 84 | instExtra = ()
|
| ... | ... | @@ -170,6 +170,7 @@ import Data.IORef |
| 170 | 170 | import GHC.Parser.HaddockLex (lexHsDoc)
|
| 171 | 171 | import GHC.Parser (parseIdentifier)
|
| 172 | 172 | import GHC.Rename.Doc (rnHsDoc)
|
| 173 | +import Data.Function
|
|
| 173 | 174 | |
| 174 | 175 | {-
|
| 175 | 176 | Note [Template Haskell state diagram]
|
| ... | ... | @@ -1165,11 +1166,12 @@ runRemoteModFinalizers (ThModFinalizers finRefs) = do |
| 1165 | 1166 | case th_state of
|
| 1166 | 1167 | Nothing -> return () -- TH was not started, nothing to do
|
| 1167 | 1168 | Just fhv -> do
|
| 1168 | - r <- liftIO $ withForeignRef fhv $ \st ->
|
|
| 1169 | - withForeignRefs finRefs $ \qrefs ->
|
|
| 1170 | - sendMessageDelayedResponse inst (RunModFinalizers st qrefs)
|
|
| 1171 | - () <- runRemoteTH inst []
|
|
| 1172 | - qr <- liftIO $ receiveDelayedResponse inst r
|
|
| 1169 | + qr <- withRunInIO $ \runInIO -> do
|
|
| 1170 | + recovers <- newIORef []
|
|
| 1171 | + withForeignRef fhv $ \st ->
|
|
| 1172 | + withForeignRefs finRefs $ \qrefs ->
|
|
| 1173 | + sendTHComputation inst (RunModFinalizers st qrefs) $ \msg ->
|
|
| 1174 | + runInIO (runRemoteTH recovers msg)
|
|
| 1173 | 1175 | checkQResult qr
|
| 1174 | 1176 | |
| 1175 | 1177 | runQResult
|
| ... | ... | @@ -1824,48 +1826,47 @@ runTH ty fhv = do |
| 1824 | 1826 | rstate <- getTHState inst
|
| 1825 | 1827 | loc <- location
|
| 1826 | 1828 | -- run a remote TH request
|
| 1827 | - r <- liftIO $
|
|
| 1829 | + qr <- withRunInIO $ \runInIO -> do
|
|
| 1830 | + recovers <- newIORef []
|
|
| 1828 | 1831 | withForeignRef rstate $ \state_hv ->
|
| 1829 | - withForeignRef fhv $ \q_hv ->
|
|
| 1830 | - sendMessageDelayedResponse inst (RunTH state_hv q_hv ty (Just loc))
|
|
| 1831 | - -- respond to requests from the interpreter
|
|
| 1832 | - runRemoteTH inst []
|
|
| 1833 | - -- get the final result
|
|
| 1834 | - qr <- liftIO $ receiveDelayedResponse inst r
|
|
| 1832 | + withForeignRef fhv $ \q_hv ->
|
|
| 1833 | + sendTHComputation inst (RunTH state_hv q_hv ty (Just loc)) $ \msg ->
|
|
| 1834 | + runInIO (runRemoteTH recovers msg)
|
|
| 1835 | 1835 | bs <- checkQResult qr
|
| 1836 | 1836 | return $! runGet get (LB.fromStrict bs)
|
| 1837 | 1837 | |
| 1838 | 1838 | |
| 1839 | --- | communicate with a remotely-running TH computation until it finishes.
|
|
| 1839 | +-- | Communicate with a remotely-running TH computation until it finishes.
|
|
| 1840 | 1840 | -- See Note [Remote Template Haskell] in libraries/ghci/GHCi/TH.hs.
|
| 1841 | 1841 | runRemoteTH
|
| 1842 | - :: ExtInterpInstance d
|
|
| 1843 | - -> [Messages TcRnMessage] -- saved from nested calls to qRecover
|
|
| 1844 | - -> TcM ()
|
|
| 1845 | -runRemoteTH inst recovers = do
|
|
| 1846 | - THMsg msg <- liftIO $ receiveTHMessage inst
|
|
| 1842 | + :: Binary a
|
|
| 1843 | + => IORef [Messages TcRnMessage] -- ^ saved from nested calls to qRecover
|
|
| 1844 | + -> THMessage a
|
|
| 1845 | + -> TcM (Bool, a)
|
|
| 1846 | +runRemoteTH recovers msg = do
|
|
| 1847 | 1847 | case msg of
|
| 1848 | - RunTHDone -> return ()
|
|
| 1848 | + RunTHDone -> return (True, ())
|
|
| 1849 | 1849 | StartRecover -> do -- Note [TH recover with -fexternal-interpreter]
|
| 1850 | 1850 | v <- getErrsVar
|
| 1851 | 1851 | msgs <- readTcRef v
|
| 1852 | 1852 | writeTcRef v emptyMessages
|
| 1853 | - runRemoteTH inst (msgs : recovers)
|
|
| 1853 | + modifyIORef recovers (msgs:) & liftIO
|
|
| 1854 | + return (False, ())
|
|
| 1854 | 1855 | EndRecover caught_error -> do
|
| 1855 | - let (prev_msgs, rest) = case recovers of
|
|
| 1856 | - [] -> panic "EndRecover"
|
|
| 1857 | - a : b -> (a,b)
|
|
| 1856 | + (prev_msgs, rest) <- liftIO $
|
|
| 1857 | + readIORef recovers >>= \case
|
|
| 1858 | + [] -> panic "EndRecover"
|
|
| 1859 | + a : b -> pure (a,b)
|
|
| 1858 | 1860 | v <- getErrsVar
|
| 1859 | 1861 | warn_msgs <- getWarningMessages <$> readTcRef v
|
| 1860 | 1862 | -- keep the warnings only if there were no errors
|
| 1861 | 1863 | writeTcRef v $ if caught_error
|
| 1862 | 1864 | then prev_msgs
|
| 1863 | 1865 | else mkMessages warn_msgs `unionMessages` prev_msgs
|
| 1864 | - runRemoteTH inst rest
|
|
| 1866 | + writeIORef recovers rest & liftIO
|
|
| 1867 | + return (False, ())
|
|
| 1865 | 1868 | _other -> do
|
| 1866 | - r <- handleTHMessage msg
|
|
| 1867 | - liftIO $ sendAnyValue inst r
|
|
| 1868 | - runRemoteTH inst recovers
|
|
| 1869 | + (False,) <$> handleTHMessage msg
|
|
| 1869 | 1870 | |
| 1870 | 1871 | -- | Check a QResult
|
| 1871 | 1872 | checkQResult :: QResult a -> TcM a
|
| 1 | -{-# LANGUAGE GADTs, DeriveGeneric, StandaloneDeriving, ScopedTypeVariables,
|
|
| 2 | - GeneralizedNewtypeDeriving, ExistentialQuantification, RecordWildCards,
|
|
| 3 | - CPP, NamedFieldPuns #-}
|
|
| 1 | +{-# LANGUAGE RecursiveDo, GADTs, DeriveGeneric, StandaloneDeriving, ScopedTypeVariables,
|
|
| 2 | + GeneralizedNewtypeDeriving, ExistentialQuantification, RecordWildCards, CPP,
|
|
| 3 | + NamedFieldPuns, LambdaCase, TypeAbstractions, TypeApplications,
|
|
| 4 | + LinearTypes, RankNTypes, UnboxedTuples, BlockArguments, TupleSections, MagicHash #-}
|
|
| 4 | 5 | {-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-orphans #-}
|
| 5 | 6 | |
| 6 | 7 | -- |
|
| ... | ... | @@ -22,10 +23,25 @@ module GHCi.Message |
| 22 | 23 | , ResumeContext(..)
|
| 23 | 24 | , QState(..)
|
| 24 | 25 | , getMessage, putMessage, getTHMessage, putTHMessage
|
| 25 | - , Pipe, mkPipeFromHandles, mkPipeFromContinuations, remoteCall, remoteTHCall, readPipe, writePipe
|
|
| 26 | + |
|
| 27 | + , Pipe(..), mkPipeFromHandles, mkPipeFromContinuations, closePipe
|
|
| 28 | + , remoteCall, remoteTHCall
|
|
| 29 | + , handleRemoteCall, handleRemoteTHCall
|
|
| 30 | + , RemoteProcessException(..)
|
|
| 31 | + |
|
| 26 | 32 | , BreakModule
|
| 27 | 33 | , BreakUnitId
|
| 28 | 34 | , LoadedDLL
|
| 35 | + |
|
| 36 | + -- * Low-level interface for async bidirectional RPC
|
|
| 37 | + , remoteAsyncReq
|
|
| 38 | + , handleAsyncReq
|
|
| 39 | + -- ** Consume `reply` exactly once
|
|
| 40 | + , WriteReply -- abstract
|
|
| 41 | + , writeReply
|
|
| 42 | + -- ** Async matching internals
|
|
| 43 | + , AsyncManager(..), CorrelationId
|
|
| 44 | + , IddMsg(..), newAsyncManager
|
|
| 29 | 45 | ) where
|
| 30 | 46 | |
| 31 | 47 | import Prelude -- See note [Why do we import Prelude here?]
|
| ... | ... | @@ -45,6 +61,7 @@ import qualified GHC.Exts.Heap as Heap |
| 45 | 61 | import GHC.ForeignSrcLang
|
| 46 | 62 | import GHC.Fingerprint
|
| 47 | 63 | import GHC.Conc (pseq, par)
|
| 64 | +import GHC.IO (IO(..))
|
|
| 48 | 65 | import Control.Concurrent
|
| 49 | 66 | import Control.DeepSeq
|
| 50 | 67 | import Control.Exception
|
| ... | ... | @@ -63,6 +80,8 @@ import Data.Dynamic |
| 63 | 80 | import Data.Typeable (TypeRep)
|
| 64 | 81 | import Data.IORef
|
| 65 | 82 | import Data.Map (Map)
|
| 83 | +import Data.IntMap (IntMap)
|
|
| 84 | +import qualified Data.IntMap as IntMap
|
|
| 66 | 85 | import Foreign
|
| 67 | 86 | import GHC.Generics
|
| 68 | 87 | import GHC.Stack.CCS
|
| ... | ... | @@ -71,6 +90,11 @@ import qualified GHC.Boot.TH.Monad as TH |
| 71 | 90 | import System.Exit
|
| 72 | 91 | import System.IO
|
| 73 | 92 | import System.IO.Error
|
| 93 | +import Control.Monad
|
|
| 94 | +import Control.Concurrent.STM
|
|
| 95 | +import Data.Bifunctor
|
|
| 96 | +import GHC.Base (State#)
|
|
| 97 | +import GHC.Exts (RealWorld)
|
|
| 74 | 98 | |
| 75 | 99 | -- -----------------------------------------------------------------------------
|
| 76 | 100 | -- The RPC protocol between GHC and the interactive server
|
| ... | ... | @@ -693,50 +717,298 @@ serializeBCOs rbcos = parMap doChunk (chunkList 100 rbcos) |
| 693 | 717 | |
| 694 | 718 | -- | An opaque pipe for bidirectional binary data transmission.
|
| 695 | 719 | data Pipe = Pipe
|
| 696 | - { getSome :: !(IO ByteString)
|
|
| 697 | - , putAll :: !(B.Builder -> IO ())
|
|
| 698 | - , pipeLeftovers :: !(IORef (Maybe ByteString))
|
|
| 720 | + { getSome :: !(IO ByteString)
|
|
| 721 | + , putAll :: !(B.Builder -> IO ())
|
|
| 722 | + , pipeLeftovers :: !(IORef (Maybe ByteString))
|
|
| 723 | + , pipeLock :: !(MVar ()) -- ^ Lock to prevent concurrent writes to the stream
|
|
| 724 | + , pipeAsyncMngr :: !AsyncManager
|
|
| 725 | + , pipeCleanupRW :: !(IO ())
|
|
| 726 | + -- ^ Clean up the read/write resources, e.g. closes the handles if 'mkPipeFromHandles'
|
|
| 727 | + , pipeIsClosed :: !(TVar Bool)
|
|
| 699 | 728 | }
|
| 700 | 729 | |
| 701 | 730 | -- | Make a 'Pipe' from a 'Handle' to read and a 'Handle' to write.
|
| 731 | +-- Must call 'closePipe' at the end to clean up and close the handles.
|
|
| 702 | 732 | mkPipeFromHandles :: Handle -> Handle -> IO Pipe
|
| 703 | 733 | mkPipeFromHandles pipeRead pipeWrite = do
|
| 704 | 734 | let getSome = B.hGetSome pipeRead (32*1024)
|
| 705 | 735 | putAll b = do
|
| 706 | 736 | B.hPutBuilder pipeWrite b
|
| 707 | 737 | hFlush pipeWrite
|
| 708 | - pipeLeftovers <- newIORef Nothing
|
|
| 709 | - pure $ Pipe { getSome, putAll, pipeLeftovers }
|
|
| 738 | + mkPipeFromContinuations getSome putAll (hClose pipeRead >> hClose pipeWrite)
|
|
| 739 | + |
|
| 740 | +-- | Make a 'Pipe' from a reader function and a writer function, plus an action
|
|
| 741 | +-- to clean up the reader and writer resources. The pipe should be closed with
|
|
| 742 | +-- 'closePipe', which will call this clean up action too.
|
|
| 743 | +mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ()) -> IO () -> IO Pipe
|
|
| 744 | +mkPipeFromContinuations getSome putAll pipeCleanupRW = mdo
|
|
| 745 | + pipeLeftovers <- newIORef Nothing
|
|
| 746 | + pipeLock <- newMVar ()
|
|
| 747 | + pipeIsClosed <- newTVarIO False
|
|
| 748 | + mdo
|
|
| 749 | + pipeAsyncMngr <- newAsyncManager p
|
|
| 750 | + let p = Pipe { .. }
|
|
| 751 | + pure p
|
|
| 752 | + |
|
| 753 | +-- | Cleanly close our end of the Pipe, terminating the async manager's
|
|
| 754 | +-- drain-read-end thread. (This will close the handles if created from
|
|
| 755 | +-- 'mkPipeFromHandles').
|
|
| 756 | +--
|
|
| 757 | +-- The remote process does not necessarily detect that this end of the pipe was
|
|
| 758 | +-- closed, but it must handle cleanly e.g. a handle being closed (perhaps by
|
|
| 759 | +-- closing the pipe on its end too)
|
|
| 760 | +--
|
|
| 761 | +-- The requests we've sent that are pending are all canceled with
|
|
| 762 | +-- 'PipeClosedException' by ourselves.
|
|
| 763 | +closePipe :: Pipe -> IO ()
|
|
| 764 | +closePipe Pipe{..} = do
|
|
| 765 | + |
|
| 766 | + -- releases handle lock taken by reading, and cleans up by replying to every
|
|
| 767 | + -- pending request with 'PipeClosedException'.
|
|
| 768 | + throwTo (asyncDrainTid pipeAsyncMngr) PipeClosedException
|
|
| 710 | 769 | |
| 711 | --- | Make a 'Pipe' from a reader function and a writer function.
|
|
| 712 | -mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ()) -> IO Pipe
|
|
| 713 | -mkPipeFromContinuations getSome putAll = do
|
|
| 714 | - pipeLeftovers <- newIORef Nothing
|
|
| 715 | - pure $ Pipe { getSome, putAll, pipeLeftovers }
|
|
| 770 | + pipeCleanupRW -- hClose won't deadlock now that the drainPipe thread is killed
|
|
| 716 | 771 | |
| 772 | +-- | Send a 'Message' request to and wait for a reply from the interpreter
|
|
| 773 | +-- (blocks on the calling side, doesn't necessarily block the interpreter)
|
|
| 717 | 774 | remoteCall :: Binary a => Pipe -> Message a -> IO a
|
| 718 | -remoteCall pipe msg = do
|
|
| 719 | - writePipe pipe (putMessage msg)
|
|
| 720 | - readPipe pipe get
|
|
| 775 | +remoteCall pipe msg = either throwIO (pure . runGet get)
|
|
| 776 | + =<< remoteAsyncReq pipe (putMessage msg)
|
|
| 777 | + |
|
| 778 | +-- | Handle a single 'Message' request on the interpreter and reply to the host
|
|
| 779 | +handleRemoteCall :: Pipe
|
|
| 780 | + -> (Msg -> IO Msg) {-^ Msg hook -}
|
|
| 781 | + -> (forall a. (Show a, Binary a) => Message a -> IO a)
|
|
| 782 | + -> IO ()
|
|
| 783 | +handleRemoteCall pipe hook k =
|
|
| 784 | + handleAsyncReq pipe getMessage \writer msg0 -> IO \s0 ->
|
|
| 785 | + case hook msg0 |#> s0 of -- unIO for linearity checking
|
|
| 786 | + (# s1, Msg msg #) -> case k msg |#> s1 of
|
|
| 787 | + (# s2, a #) -> writeReply writer (put a) |#> s2
|
|
| 788 | + |
|
| 789 | +-- | Send a 'THMessage' request to the host and wait for a reply from the host
|
|
| 790 | +-- to the interpreter
|
|
| 791 | +remoteTHCall :: Binary a => Pipe -> THMessage a -> IO a
|
|
| 792 | +remoteTHCall pipe msg = either throwIO (pure . runGet get)
|
|
| 793 | + =<< remoteAsyncReq pipe (putTHMessage msg)
|
|
| 794 | + |
|
| 795 | +-- | Handle a 'THMessage' request on the host and reply to the interpreter.
|
|
| 796 | +handleRemoteTHCall
|
|
| 797 | + :: Pipe -> (forall a. (Show a, Binary a) => THMessage a -> IO a) -> IO ()
|
|
| 798 | +handleRemoteTHCall pipe k =
|
|
| 799 | + handleAsyncReq pipe getTHMessage \writer (THMsg msg) -> IO \s0 ->
|
|
| 800 | + case k msg |#> s0 of -- unIO for linearity checking
|
|
| 801 | + (# s1, a #) -> writeReply writer (put a) |#> s1
|
|
| 802 | + |
|
| 803 | +--------------------------------------------------------------------------------
|
|
| 804 | +-- * Matching requests with async replies
|
|
| 805 | +--------------------------------------------------------------------------------
|
|
| 806 | + |
|
| 807 | +-- | Send an asynchronous request and block waiting for a matching reply
|
|
| 808 | +remoteAsyncReq :: Pipe -> Put -> IO (Either SomeException LB.ByteString)
|
|
| 809 | +remoteAsyncReq pipe msg = do
|
|
| 810 | + |
|
| 811 | + let pendingReplies = pendingReply (pipeAsyncMngr pipe)
|
|
| 812 | + |
|
| 813 | + isClosed <- readTVarIO (pipeIsClosed pipe)
|
|
| 814 | + when isClosed $
|
|
| 815 | + throwIO PipeClosedException
|
|
| 816 | + |
|
| 817 | + -- Mark pending request
|
|
| 818 | + uq <- freshReqId (pipeAsyncMngr pipe)
|
|
| 819 | + wv <- newEmptyMVar
|
|
| 820 | + atomicModifyIORef' pendingReplies $
|
|
| 821 | + \m -> (IntMap.insert uq wv m, ())
|
|
| 822 | + |
|
| 823 | + -- Write request
|
|
| 824 | + writePipe pipe (Request uq (runPut msg))
|
|
| 825 | + `onException`
|
|
| 826 | + -- the req wasn't done, or no one will be reading the reply:
|
|
| 827 | + -- in either case, drop it from the pending map.
|
|
| 828 | + atomicModifyIORef' pendingReplies
|
|
| 829 | + (\m -> (IntMap.delete uq m, ()))
|
|
| 830 | + |
|
| 831 | + -- Block waiting for reply
|
|
| 832 | + takeMVar wv
|
|
| 833 | + |
|
| 834 | +-- | Receive a single asynchronous request and send a matching reply
|
|
| 835 | +-- asynchronously (the handler may fork a computation that eventually replies
|
|
| 836 | +-- and can return before actually replying, as long as it eventually does).
|
|
| 837 | +--
|
|
| 838 | +-- The handler receives as an argument the function to use to reply to the request.
|
|
| 839 | +-- It must be used exactly once (all requests expect a reply, and you can't
|
|
| 840 | +-- reply more than once). Linear types enforce the handler replies in every
|
|
| 841 | +-- branch, and if the handler otherwise throws, it is handled by 'withAsyncHandler'.
|
|
| 842 | +handleAsyncReq :: Pipe -> Get a -> (WriteReply Put %1 -> a -> IO r) -> IO r
|
|
| 843 | +handleAsyncReq pipe getit k = do
|
|
| 844 | + ack <- newEmptyMVar
|
|
| 845 | + |
|
| 846 | + isOver <- atomically $ do
|
|
| 847 | + isClosed <- readTVar (pipeIsClosed pipe)
|
|
| 848 | + isEmpty <- isEmptyTQueue (requestsQueue (pipeAsyncMngr pipe))
|
|
| 849 | + return (isClosed && isEmpty)
|
|
| 850 | + |
|
| 851 | + when isOver $ do
|
|
| 852 | + throwIO PipeClosedException
|
|
| 853 | + |
|
| 854 | + -- Block waiting for request
|
|
| 855 | + (uq, bs) <- atomically $ readTQueue (requestsQueue (pipeAsyncMngr pipe))
|
|
| 856 | + |
|
| 857 | + let reply p = do
|
|
| 858 | + writePipe pipe (Reply uq (bimap toSerializableException runPut p))
|
|
| 859 | + putMVar ack ()
|
|
| 860 | + |
|
| 861 | + -- Run the continuation which must reply exactly once with 'writeReply'
|
|
| 862 | + case runGetOrFail getit bs of
|
|
| 863 | + Left (_, _, err) -> do
|
|
| 864 | + let exc = DecodeAsyncReqException err
|
|
| 865 | + reply (Left (toException exc))
|
|
| 866 | + throwIO exc
|
|
| 867 | + Right (_, _, it) ->
|
|
| 868 | + k (WriteReply reply) it
|
|
| 869 | + |
|
| 870 | + -- If there's an exception before the reply is sent, reply with an error instead.
|
|
| 871 | + `catch` \(e :: SomeException) -> do
|
|
| 872 | + |
|
| 873 | + tryTakeMVar ack >>= \case
|
|
| 874 | + Just () ->
|
|
| 875 | + -- Already replied
|
|
| 876 | + throwIO e
|
|
| 877 | + Nothing -> do
|
|
| 878 | + -- Reply with an error
|
|
| 879 | + reply (Left e)
|
|
| 880 | + throwIO e
|
|
| 881 | + |
|
| 882 | +-- | An exception that can be thrown on the host process representing an
|
|
| 883 | +-- expection that happened on the remote one.
|
|
| 884 | +newtype RemoteProcessException = RemoteProcessException SerializableException deriving Show
|
|
| 885 | +instance Exception RemoteProcessException
|
|
| 886 | + |
|
| 887 | +-- | 'handleSyncReq' failed to decode an incoming request payload using the given @'Get' a@
|
|
| 888 | +newtype DecodeAsyncReqException = DecodeAsyncReqException String deriving Show
|
|
| 889 | +instance Exception DecodeAsyncReqException
|
|
| 890 | + |
|
| 891 | +-- | We've closed the pipe on our end and we terminate all pending requests with this.
|
|
| 892 | +data PipeClosedException = PipeClosedException deriving Show
|
|
| 893 | +instance Exception PipeClosedException
|
|
| 894 | + |
|
| 895 | +-- ** Async matching "internals" -----------------------------------------------
|
|
| 896 | + |
|
| 897 | +data AsyncManager = AsyncManager
|
|
| 898 | + { pendingReply :: !(IORef (IntMap (MVar (Either SomeException LB.ByteString))))
|
|
| 899 | + -- ^ When a request is sent, keep track that we're expecting a reply. The
|
|
| 900 | + -- MVar is signaled when the reply arrives. The exception can originate
|
|
| 901 | + -- from the host process, or from the remote one (with 'RemoteProcessException').
|
|
| 902 | + , requestsQueue :: !(TQueue (CorrelationId, LB.ByteString))
|
|
| 903 | + -- ^ A queue of pending received requests. When a request is received,
|
|
| 904 | + -- queue it here. A request handler will pop the next request from it.
|
|
| 905 | + -- (Nothing except the 'drainPipe' thread should read from the pipe).
|
|
| 906 | + , asyncDrainTid :: !ThreadId
|
|
| 907 | + , asyncNextReq :: !(IORef Int)
|
|
| 908 | + }
|
|
| 721 | 909 | |
| 722 | -writePipe :: Pipe -> Put -> IO ()
|
|
| 723 | -writePipe Pipe{..} put = putAll $ execPut put
|
|
| 910 | +type CorrelationId = Int
|
|
| 724 | 911 | |
| 725 | -remoteTHCall :: Binary a => Pipe -> THMessage a -> IO a
|
|
| 726 | -remoteTHCall pipe msg = do
|
|
| 727 | - writePipe pipe (putTHMessage msg)
|
|
| 728 | - readPipe pipe get
|
|
| 912 | +-- | We have a single type for requests and replies because both the host and
|
|
| 913 | +-- interpreter may send requests *and* replies on the write end *and* on recv
|
|
| 914 | +-- both on the read end.
|
|
| 915 | +--
|
|
| 916 | +-- For instance:
|
|
| 917 | +-- 1. The host sends a RunTH request on its Host Write End (HWE)
|
|
| 918 | +-- 2. The interpreter sends a NewName TH request on its Interpreter Write End (IWE)
|
|
| 919 | +-- 3. The host sends a reply to the TH request on its HWE
|
|
| 920 | +-- 4. The interpreter sends the reply to the RunTH request on its IWE
|
|
| 921 | +data IddMsg = Request !CorrelationId LB.ByteString
|
|
| 922 | + | Reply !CorrelationId (Either SerializableException LB.ByteString)
|
|
| 923 | + |
|
| 924 | +instance Binary IddMsg where
|
|
| 925 | + put (Request i bs) = putWord8 0 >> put i >> put bs
|
|
| 926 | + put (Reply i bs) = putWord8 1 >> put i >> put bs
|
|
| 927 | + get = do
|
|
| 928 | + b <- getWord8
|
|
| 929 | + case b of
|
|
| 930 | + 0 -> Request <$> get <*> get
|
|
| 931 | + 1 -> Reply <$> get <*> get
|
|
| 932 | + _ -> error $ "impossible: Binary IddMsg - " ++ show b
|
|
| 933 | + |
|
| 934 | +newAsyncManager :: Pipe -> IO AsyncManager
|
|
| 935 | +newAsyncManager pipe = do
|
|
| 936 | + asyncNextReq <- newIORef 0
|
|
| 937 | + pendingReply <- newIORef IntMap.empty
|
|
| 938 | + requestsQueue <- newTQueueIO
|
|
| 939 | + |
|
| 940 | + -- Fork thread to drain read end.
|
|
| 941 | + -- (cleaned up on 'closePipe')
|
|
| 942 | + asyncDrainTid <- forkIO (drainPipe pipe)
|
|
| 729 | 943 | |
| 730 | -readPipe :: Pipe -> Get a -> IO a
|
|
| 731 | -readPipe Pipe{..} get = do
|
|
| 944 | + let am = AsyncManager{..}
|
|
| 945 | + pure am
|
|
| 946 | + |
|
| 947 | +-- | Drains the read end of the Pipe and match 'Reply'es against pending 'Request's.
|
|
| 948 | +--
|
|
| 949 | +-- In pratice, we'll have both a thread draining the read end of the host's
|
|
| 950 | +-- 'Pipe' and a thread draining the read end of the interpreter's 'Pipe', since
|
|
| 951 | +-- both processes construct a 'Pipe' and send and receive messages
|
|
| 952 | +drainPipe :: Pipe -> IO ()
|
|
| 953 | +drainPipe p = catch loop cancelPending where
|
|
| 954 | + loop = do
|
|
| 955 | + readPipe p >>= \case
|
|
| 956 | + -- Receive a reply to a pending notification
|
|
| 957 | + Just (Reply uq bs) -> do
|
|
| 958 | + mwv <- atomicModifyIORef' (pendingReply am) $ \m -> do
|
|
| 959 | + case IntMap.lookup uq m of
|
|
| 960 | + Nothing -> (m, Nothing)
|
|
| 961 | + Just wv -> (IntMap.delete uq m, Just wv)
|
|
| 962 | + case mwv of
|
|
| 963 | + Just wv ->
|
|
| 964 | + putMVar wv (bimap (toException . RemoteProcessException) id bs) -- signal matching request
|
|
| 965 | + Nothing ->
|
|
| 966 | + -- the req was dropped from pending bc there was an exception, and no
|
|
| 967 | + -- one's listening any more. the reply is ignored
|
|
| 968 | + pure ()
|
|
| 969 | + loop
|
|
| 970 | + -- Receive a request and queue it
|
|
| 971 | + Just (Request uq bs) -> do
|
|
| 972 | + atomically $ writeTQueue (requestsQueue am) (uq,bs)
|
|
| 973 | + loop
|
|
| 974 | + -- EOF on read handle
|
|
| 975 | + Nothing -> do
|
|
| 976 | + atomically $ writeTVar (pipeIsClosed p) True
|
|
| 977 | + pure () -- stops thread
|
|
| 978 | + |
|
| 979 | + cancelPending (e::SomeException) = do
|
|
| 980 | + pendings <- readIORef (pendingReply am)
|
|
| 981 | + mapM_ (\p -> putMVar p (Left e {-host exception-})) pendings
|
|
| 982 | + |
|
| 983 | + am = pipeAsyncMngr p
|
|
| 984 | + |
|
| 985 | +freshReqId :: AsyncManager -> IO CorrelationId
|
|
| 986 | +freshReqId AsyncManager{asyncNextReq} =
|
|
| 987 | + atomicModifyIORef' asyncNextReq $ \i -> (i+1, i)
|
|
| 988 | + |
|
| 989 | +--------------------------------------------------------------------------------
|
|
| 990 | +-- * Pipe thread-safe read/write
|
|
| 991 | +--------------------------------------------------------------------------------
|
|
| 992 | +-- All messages on the wire are 'IddMsg', which contain a correlation header
|
|
| 993 | +-- and identify whether they are requests or replies.
|
|
| 994 | + |
|
| 995 | +writePipe :: Pipe -> IddMsg -> IO ()
|
|
| 996 | +writePipe p@Pipe{..} = withLock p . putAll . execPut . put
|
|
| 997 | + |
|
| 998 | +-- | Only the 'drainPipe' thread should read from the pipe directly. Other
|
|
| 999 | +-- functions should wait for the matching reply to be delivered asynchronously.
|
|
| 1000 | +-- (see 'withAsyncReq'). It doesn't lock to read bc it assumes single reader is 'drainPipe'.
|
|
| 1001 | +--
|
|
| 1002 | +-- Returns @Nothing@ on EOF.
|
|
| 1003 | +readPipe :: Pipe -> IO (Maybe IddMsg)
|
|
| 1004 | +readPipe Pipe{..} = do
|
|
| 732 | 1005 | leftovers <- readIORef pipeLeftovers
|
| 733 | 1006 | m <- getBin getSome get leftovers
|
| 734 | 1007 | case m of
|
| 735 | - Nothing -> throw $
|
|
| 736 | - mkIOError eofErrorType "GHCi.Message.readPipe" Nothing Nothing
|
|
| 1008 | + Nothing -> return Nothing
|
|
| 737 | 1009 | Just (result, new_leftovers) -> do
|
| 738 | 1010 | writeIORef pipeLeftovers new_leftovers
|
| 739 | - return result
|
|
| 1011 | + return (Just result)
|
|
| 740 | 1012 | |
| 741 | 1013 | getBin
|
| 742 | 1014 | :: (IO ByteString) -> Get a -> Maybe ByteString
|
| ... | ... | @@ -758,3 +1030,22 @@ getBin getsome get leftover = go leftover (runGetIncremental get) |
| 758 | 1030 | else go Nothing (fun (Just b))
|
| 759 | 1031 | go _lft (Fail _rest _off str) =
|
| 760 | 1032 | throwIO (ErrorCall ("getBin: " ++ str))
|
| 1033 | + |
|
| 1034 | +withLock :: Pipe -> IO c -> IO c
|
|
| 1035 | +withLock Pipe{..} = bracket (takeMVar pipeLock) (putMVar pipeLock) . const
|
|
| 1036 | + |
|
| 1037 | +--------------------------------------------------------------------------------
|
|
| 1038 | +-- * Consume `reply` exactly once
|
|
| 1039 | +--------------------------------------------------------------------------------
|
|
| 1040 | + |
|
| 1041 | +-- | Every request should get exactly one reply.
|
|
| 1042 | +data WriteReply a where
|
|
| 1043 | + WriteReply :: (Either SomeException a -> IO ()) %1 -> WriteReply a
|
|
| 1044 | + |
|
| 1045 | +-- | Write a reply exactly once (consuming the ability to write more replies)
|
|
| 1046 | +writeReply :: WriteReply a %1 -> a -> IO ()
|
|
| 1047 | +writeReply (WriteReply r) = \x -> r (Right x)
|
|
| 1048 | + |
|
| 1049 | +-- | Infix, linear, 'unIO'
|
|
| 1050 | +(|#>) :: IO a %1 -> State# RealWorld -> (# State# RealWorld, a #)
|
|
| 1051 | +(|#>) (IO fs) s = fs s |
| 1 | -{-# LANGUAGE CPP, RankNTypes, RecordWildCards, GADTs, ScopedTypeVariables #-}
|
|
| 1 | +{-# LANGUAGE LambdaCase, CPP, RankNTypes, RecordWildCards, GADTs, ScopedTypeVariables #-}
|
|
| 2 | 2 | module GHCi.Server
|
| 3 | 3 | ( MessageHook
|
| 4 | 4 | , CustomMessageHandler
|
| ... | ... | @@ -15,6 +15,7 @@ import GHCi.Signals |
| 15 | 15 | import GHCi.TH
|
| 16 | 16 | import GHCi.Message
|
| 17 | 17 | import Data.ByteString (ByteString)
|
| 18 | +import Data.IORef
|
|
| 18 | 19 | #if defined(wasm32_HOST_ARCH)
|
| 19 | 20 | import qualified Data.ByteString.Builder as B
|
| 20 | 21 | import qualified Data.ByteString.Internal as B
|
| ... | ... | @@ -60,27 +61,26 @@ servWithCustom |
| 60 | 61 | -> (forall a .IO a -> IO a)
|
| 61 | 62 | -> CustomMessageHandler
|
| 62 | 63 | -> IO ()
|
| 63 | -servWithCustom verbose hook pipe restore customHandler = loop
|
|
| 64 | +servWithCustom verbose hook pipe restore customHandler = newIORef False >>= loop
|
|
| 64 | 65 | where
|
| 65 | - loop = do
|
|
| 66 | + loop doneRef = do
|
|
| 66 | 67 | when verbose $ trace "reading pipe..."
|
| 67 | - Msg msg <- readPipe pipe getMessage >>= hook
|
|
| 68 | + handleRemoteCall pipe hook $ \(msg :: Message a) -> do
|
|
| 68 | 69 | |
| 69 | - discardCtrlC
|
|
| 70 | + discardCtrlC
|
|
| 70 | 71 | |
| 71 | - when verbose $ trace ("msg: " ++ (show msg))
|
|
| 72 | - case msg of
|
|
| 73 | - CustomMessage tag payload -> handleCustom tag payload
|
|
| 74 | - Shutdown -> return ()
|
|
| 75 | - RunTH st q ty loc -> wrapRunTH $ runTH pipe st q ty loc
|
|
| 76 | - RunModFinalizers st qrefs -> wrapRunTH $ runModFinalizerRefs pipe st qrefs
|
|
| 77 | - _other -> run msg >>= reply
|
|
| 72 | + when verbose $ trace ("msg: " ++ (show msg))
|
|
| 73 | + case msg of
|
|
| 74 | + CustomMessage tag payload -> handleCustom tag payload
|
|
| 75 | + Shutdown -> writeIORef doneRef True >> reply ()
|
|
| 76 | + -- TODO: These two should probably be done off the main thread
|
|
| 77 | + RunTH st q ty loc -> wrapRunTH $ runTH pipe st q ty loc
|
|
| 78 | + RunModFinalizers st qrefs -> wrapRunTH $ runModFinalizerRefs pipe st qrefs
|
|
| 79 | + _other -> run msg >>= reply
|
|
| 78 | 80 | |
| 79 | - reply :: forall a. (Binary a, Show a) => a -> IO ()
|
|
| 80 | - reply r = do
|
|
| 81 | - when verbose $ trace ("writing pipe: " ++ show r)
|
|
| 82 | - writePipe pipe (put r)
|
|
| 83 | - loop
|
|
| 81 | + isDone <- readIORef doneRef
|
|
| 82 | + if isDone then closePipe
|
|
| 83 | + else loop doneRef
|
|
| 84 | 84 | |
| 85 | 85 | handleCustom tag payload = do
|
| 86 | 86 | mresp <- customHandler tag payload
|
| ... | ... | @@ -89,17 +89,22 @@ servWithCustom verbose hook pipe restore customHandler = loop |
| 89 | 89 | Nothing ->
|
| 90 | 90 | error $ "GHCi.Server: unhandled CustomMessage with tag " ++ show tag
|
| 91 | 91 | |
| 92 | + reply :: forall a. Show a => a -> IO a
|
|
| 93 | + reply r = do
|
|
| 94 | + when verbose $ trace ("writing pipe: " ++ show r)
|
|
| 95 | + return r -- the result of the handler gets sent
|
|
| 96 | + |
|
| 92 | 97 | -- Run some TH code, which may interact with GHC by sending
|
| 93 | 98 | -- THMessage requests, and then finally send RunTHDone followed by a
|
| 94 | 99 | -- QResult. For an overview of how TH works with Remote GHCi, see
|
| 95 | 100 | -- Note [Remote Template Haskell] in libraries/ghci/GHCi/TH.hs.
|
| 96 | - wrapRunTH :: forall a. (Binary a, Show a) => IO a -> IO ()
|
|
| 101 | + wrapRunTH :: forall a. (Binary a, Show a) => IO a -> IO (QResult a)
|
|
| 97 | 102 | wrapRunTH io = do
|
| 98 | 103 | when verbose $ trace "wrapRunTH..."
|
| 99 | 104 | r <- try io
|
| 100 | 105 | when verbose $ trace "wrapRunTH done."
|
| 101 | 106 | when verbose $ trace "writing RunTHDone."
|
| 102 | - writePipe pipe (putTHMessage RunTHDone)
|
|
| 107 | + remoteTHCall pipe RunTHDone
|
|
| 103 | 108 | case r of
|
| 104 | 109 | Left e
|
| 105 | 110 | | Just (GHCiQException _ err) <- fromException e -> do
|
| ... | ... | @@ -179,7 +184,7 @@ defaultServerWithCustom customHandler = do |
| 179 | 184 | #if defined(wasm32_HOST_ARCH)
|
| 180 | 185 | -- See Note [wasm ghci signal handlers] for details
|
| 181 | 186 | installSignalHandlers $ js_register_signal_handler cb_sig
|
| 182 | - pipe <- mkPipeFromContinuations (recv_buf cb_recv) (send_buf cb_send)
|
|
| 187 | + pipe <- mkPipeFromContinuations (recv_buf cb_recv) (send_buf cb_send) (pure ())
|
|
| 183 | 188 | #else
|
| 184 | 189 | when verbose $
|
| 185 | 190 | printf "GHC iserv starting (in: %s; out: %s)\n" (show inh) (show outh)
|
| ... | ... | @@ -95,7 +95,8 @@ library |
| 95 | 95 | filepath >= 1.4 && < 1.6,
|
| 96 | 96 | ghc-boot == @ProjectVersionMunged@,
|
| 97 | 97 | ghc-heap >= 9.10.1 && <=@ProjectVersionMunged@,
|
| 98 | - transformers >= 0.5 && < 0.7
|
|
| 98 | + transformers >= 0.5 && < 0.7,
|
|
| 99 | + stm >= 2.4 && < 2.6
|
|
| 99 | 100 | |
| 100 | 101 | if flag(bootstrap)
|
| 101 | 102 | build-depends:
|
| ... | ... | @@ -12,6 +12,7 @@ import GHCi.Message |
| 12 | 12 | , mkPipeFromHandles
|
| 13 | 13 | , remoteCall
|
| 14 | 14 | , Pipe
|
| 15 | + , closePipe
|
|
| 15 | 16 | )
|
| 16 | 17 | import GHCi.Server
|
| 17 | 18 | ( CustomMessageHandler
|
| ... | ... | @@ -153,14 +154,13 @@ customMessage pipe c = do |
| 153 | 154 | runClient :: Int -> IO ()
|
| 154 | 155 | runClient input = do
|
| 155 | 156 | serverExe <- getExecutablePath
|
| 156 | - withServer serverExe $ \hFromServer hToServer -> do
|
|
| 157 | - pipe <- mkPipeFromHandles hFromServer hToServer
|
|
| 157 | + withServer serverExe $ \pipe -> do
|
|
| 158 | 158 | res <- customMessage pipe (SquareCommand input)
|
| 159 | 159 | putStrLn $ "Square returned: " ++ show res
|
| 160 | 160 | res2 <- customMessage pipe (MulCommand 2 res)
|
| 161 | 161 | putStrLn $ "Mul returned: " ++ show res2
|
| 162 | 162 | |
| 163 | -withServer :: FilePath -> (Handle -> Handle -> IO a) -> IO a
|
|
| 163 | +withServer :: FilePath -> (Pipe -> IO a) -> IO a
|
|
| 164 | 164 | withServer serverExe action = do
|
| 165 | 165 | (ghcRead, serverWrite) <- createPipe
|
| 166 | 166 | (serverRead, ghcWrite) <- createPipe
|
| ... | ... | @@ -171,13 +171,12 @@ withServer serverExe action = do |
| 171 | 171 | , std_out = Inherit
|
| 172 | 172 | , std_err = Inherit
|
| 173 | 173 | }
|
| 174 | - bracket (mkHandles ghcRead ghcWrite)
|
|
| 175 | - (\(hFromServer, hToServer) -> do
|
|
| 176 | - hClose hFromServer
|
|
| 177 | - hClose hToServer
|
|
| 174 | + bracket (mkHandles ghcRead ghcWrite >>= uncurry mkPipeFromHandles)
|
|
| 175 | + (\pipe -> do
|
|
| 176 | + closePipe pipe
|
|
| 178 | 177 | terminateProcess ph
|
| 179 | 178 | void (waitForProcess ph))
|
| 180 | - (\(hFromServer, hToServer) -> action hFromServer hToServer)
|
|
| 179 | + (\pipe -> action pipe)
|
|
| 181 | 180 | where
|
| 182 | 181 | mkHandles r w = do
|
| 183 | 182 | hR <- fdToHandle r
|