Rodrigo Mesquita pushed to branch wip/romes/async-rpc at Glasgow Haskell Compiler / GHC
Commits:
-
1210c80c
by Rodrigo Mesquita at 2026-10-15T12:02:13+01:00
-
01640eec
by Rodrigo Mesquita at 2026-10-15T12:02:13+01:00
-
ea5bd12e
by Rodrigo Mesquita at 2026-10-15T12:02:14+01:00
-
48377e28
by Rodrigo Mesquita at 2026-10-15T12:02:14+01:00
-
cd423df8
by Andrea Vezzosi at 2026-10-15T12:03:25+01:00
12 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/Message/Async.hs
- libraries/ghci/GHCi/Server.hs
- libraries/ghci/GHCi/TH.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 []
|
| ... | ... | @@ -656,6 +654,8 @@ stopInterp interp = case interpInstance interp of |
| 656 | 654 | if isJust ex
|
| 657 | 655 | then pure ()
|
| 658 | 656 | else sendMessage i Shutdown
|
| 657 | + -- We asked the server to shutdown, so we will also shutdown any communication.
|
|
| 658 | + closePipe (interpPipe (instProcess i))
|
|
| 659 | 659 | pure InterpPending
|
| 660 | 660 | |
| 661 | 661 | -- -----------------------------------------------------------------------------
|
| ... | ... | @@ -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,63 @@ 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 | + -> (InboxId -> 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 | + mbox <- newMailbox (pipeAsyncMngr (interpPipe proc))
|
|
| 61 | + ht <- forkFinally (restore (handlerLoop mbox)) (atomically . putTMVar var)
|
|
| 62 | + restore (sendMessage i (msg mbox) <* atomically (readTMVar var))
|
|
| 63 | + `onException` uninterruptibleMask_ (throwTo ht CancelTHHandler)
|
|
| 64 | + where
|
|
| 65 | + proc = instProcess i
|
|
| 66 | + handleOne mbox doneRef = do
|
|
| 67 | + handleRemoteTHCall (interpPipe proc) mbox $ \req -> do
|
|
| 68 | + (stop, res) <- reqHandler req
|
|
| 69 | + writeIORef doneRef stop
|
|
| 70 | + return res
|
|
| 71 | + handlerLoop mbox = do
|
|
| 72 | + doneRef <- newIORef False
|
|
| 73 | + let loop = do
|
|
| 74 | + handleOne mbox doneRef
|
|
| 75 | + isDone <- readIORef doneRef
|
|
| 76 | + if isDone then return ()
|
|
| 77 | + else loop
|
|
| 78 | + in loop
|
|
| 121 | 79 | |
| 122 | 80 | handleInterpProcessFailure :: InterpProcess -> SomeException -> IO a
|
| 123 | 81 | handleInterpProcessFailure i e = do
|
| ... | ... | @@ -130,3 +88,8 @@ handleInterpProcessFailure i e = do |
| 130 | 88 | terminateProcess hdl
|
| 131 | 89 | _ <- waitForProcess hdl
|
| 132 | 90 | throw e
|
| 91 | + |
|
| 92 | +data CancelTHHandler = CancelTHHandler deriving Show
|
|
| 93 | +instance Exception CancelTHHandler where
|
|
| 94 | + fromException = asyncExceptionFromException
|
|
| 95 | + 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 (\ii -> RunModFinalizers ii 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 (\ii -> RunTH ii 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,7 +23,16 @@ 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(..), PipeClosedException(..)
|
|
| 31 | + |
|
| 32 | + , InboxId(..)
|
|
| 33 | + , newMailbox
|
|
| 34 | + , closeMailbox
|
|
| 35 | + |
|
| 26 | 36 | , BreakModule
|
| 27 | 37 | , BreakUnitId
|
| 28 | 38 | , LoadedDLL
|
| ... | ... | @@ -45,18 +55,14 @@ import qualified GHC.Exts.Heap as Heap |
| 45 | 55 | import GHC.ForeignSrcLang
|
| 46 | 56 | import GHC.Fingerprint
|
| 47 | 57 | import GHC.Conc (pseq, par)
|
| 58 | +import GHC.IO (IO(..))
|
|
| 48 | 59 | import Control.Concurrent
|
| 49 | 60 | import Control.DeepSeq
|
| 50 | 61 | import Control.Exception
|
| 51 | -#if MIN_VERSION_base(4,20,0)
|
|
| 52 | -import Control.Exception.Context
|
|
| 53 | -#endif
|
|
| 54 | 62 | import Data.Binary
|
| 55 | 63 | import Data.Binary.Get
|
| 56 | 64 | import Data.Binary.Put
|
| 57 | 65 | import Data.ByteString (ByteString)
|
| 58 | -import qualified Data.ByteString as B
|
|
| 59 | -import qualified Data.ByteString.Builder as B
|
|
| 60 | 66 | import qualified Data.ByteString.Lazy as LB
|
| 61 | 67 | import qualified Data.ByteString.Short as BS
|
| 62 | 68 | import Data.Dynamic
|
| ... | ... | @@ -68,9 +74,7 @@ import GHC.Generics |
| 68 | 74 | import GHC.Stack.CCS
|
| 69 | 75 | import qualified GHC.Boot.TH.Syntax as TH
|
| 70 | 76 | import qualified GHC.Boot.TH.Monad as TH
|
| 71 | -import System.Exit
|
|
| 72 | -import System.IO
|
|
| 73 | -import System.IO.Error
|
|
| 77 | +import GHCi.Message.Async
|
|
| 74 | 78 | |
| 75 | 79 | -- -----------------------------------------------------------------------------
|
| 76 | 80 | -- The RPC protocol between GHC and the interactive server
|
| ... | ... | @@ -214,14 +218,16 @@ data Message a where |
| 214 | 218 | -- they did, we have to serialize the value anyway, so we might
|
| 215 | 219 | -- as well serialize it to force it.
|
| 216 | 220 | RunTH
|
| 217 | - :: RemoteRef (IORef QState)
|
|
| 221 | + :: InboxId -- ^ announce mailbox for related 'remoteTHCall's
|
|
| 222 | + -> RemoteRef (IORef QState)
|
|
| 218 | 223 | -> HValueRef {- e.g. TH.Q TH.Exp -}
|
| 219 | 224 | -> THResultType
|
| 220 | 225 | -> Maybe TH.Loc
|
| 221 | 226 | -> Message (QResult ByteString)
|
| 222 | 227 | |
| 223 | 228 | -- | Run the given mod finalizers.
|
| 224 | - RunModFinalizers :: RemoteRef (IORef QState)
|
|
| 229 | + RunModFinalizers :: InboxId -- ^ announce mailbox for related 'remoteTHCall's
|
|
| 230 | + -> RemoteRef (IORef QState)
|
|
| 225 | 231 | -> [RemoteRef (TH.Q ())]
|
| 226 | 232 | -> Message (QResult ())
|
| 227 | 233 | |
| ... | ... | @@ -458,45 +464,6 @@ data BreakUnitId |
| 458 | 464 | -- | A dummy type that tags pointers returned by 'LoadDLLs'.
|
| 459 | 465 | data LoadedDLL
|
| 460 | 466 | |
| 461 | --- SomeException can't be serialized because it contains dynamic
|
|
| 462 | --- types. However, we do very limited things with the exceptions that
|
|
| 463 | --- are thrown by interpreted computations:
|
|
| 464 | ---
|
|
| 465 | --- * We print them, e.g. "*** Exception: <something>"
|
|
| 466 | --- * UserInterrupt has a special meaning
|
|
| 467 | --- * In ghc -e, exitWith should exit with the appropriate exit code
|
|
| 468 | ---
|
|
| 469 | --- So all we need to do is distinguish UserInterrupt and ExitCode, and
|
|
| 470 | --- all other exceptions can be represented by their 'show' string.
|
|
| 471 | ---
|
|
| 472 | -data SerializableException
|
|
| 473 | - = EUserInterrupt
|
|
| 474 | - | EExitCode ExitCode
|
|
| 475 | - | EOtherException String
|
|
| 476 | - deriving (Generic, Show)
|
|
| 477 | - |
|
| 478 | -toSerializableException :: SomeException -> SerializableException
|
|
| 479 | -toSerializableException ex
|
|
| 480 | - | Just UserInterrupt <- fromException ex = EUserInterrupt
|
|
| 481 | - | Just (ec::ExitCode) <- fromException ex = (EExitCode ec)
|
|
| 482 | - | otherwise = EOtherException $
|
|
| 483 | -#if MIN_VERSION_base(4,20,0)
|
|
| 484 | - -- Exception plus backtrace as seen in `displayExceptionWithInfo`
|
|
| 485 | - case displayExceptionContext (someExceptionContext ex) of
|
|
| 486 | - "" -> displayException (ex :: SomeException)
|
|
| 487 | - cx -> displayException (ex :: SomeException) ++ "\n\n" ++ cx
|
|
| 488 | -#else
|
|
| 489 | - show (ex :: SomeException)
|
|
| 490 | -#endif
|
|
| 491 | - |
|
| 492 | -fromSerializableException :: SerializableException -> SomeException
|
|
| 493 | -fromSerializableException EUserInterrupt = toException UserInterrupt
|
|
| 494 | -fromSerializableException (EExitCode c) = toException c
|
|
| 495 | -fromSerializableException (EOtherException str) = toException (ErrorCall str)
|
|
| 496 | - |
|
| 497 | -instance Binary ExitCode
|
|
| 498 | -instance Binary SerializableException
|
|
| 499 | - |
|
| 500 | 467 | data THResult a
|
| 501 | 468 | = THException String
|
| 502 | 469 | | THComplete a
|
| ... | ... | @@ -513,11 +480,13 @@ instance Binary THResultType |
| 513 | 480 | -- StartTH message. A new one is created per module that GHC
|
| 514 | 481 | -- typechecks.
|
| 515 | 482 | data QState = QState
|
| 516 | - { qsMap :: Map TypeRep Dynamic
|
|
| 483 | + { qsMap :: Map TypeRep Dynamic
|
|
| 517 | 484 | -- ^ persistent data between splices in a module
|
| 518 | - , qsLocation :: Maybe TH.Loc
|
|
| 485 | + , qsLocation :: Maybe TH.Loc
|
|
| 519 | 486 | -- ^ location for current splice, if any
|
| 520 | - , qsPipe :: Pipe
|
|
| 487 | + , qsInboxId :: InboxId
|
|
| 488 | + -- ^ the mailbox expecting requests for each RunTH-like run
|
|
| 489 | + , qsPipe :: Pipe
|
|
| 521 | 490 | -- ^ pipe to communicate with GHC
|
| 522 | 491 | }
|
| 523 | 492 | instance Show QState where show _ = "<QState>"
|
| ... | ... | @@ -600,9 +569,9 @@ getMessage = do |
| 600 | 569 | 29 -> Msg <$> (BreakpointStatus <$> get <*> get)
|
| 601 | 570 | 30 -> Msg <$> (GetBreakpointVar <$> get <*> get)
|
| 602 | 571 | 31 -> Msg <$> return StartTH
|
| 603 | - 32 -> Msg <$> (RunModFinalizers <$> get <*> get)
|
|
| 572 | + 32 -> Msg <$> (RunModFinalizers <$> get <*> get <*> get)
|
|
| 604 | 573 | 33 -> Msg <$> (AddSptEntry <$> get <*> get)
|
| 605 | - 34 -> Msg <$> (RunTH <$> get <*> get <*> get <*> get)
|
|
| 574 | + 34 -> Msg <$> (RunTH <$> get <*> get <*> get <*> get <*> get)
|
|
| 606 | 575 | 35 -> Msg <$> (GetClosure <$> get)
|
| 607 | 576 | 36 -> Msg <$> (Seq <$> get)
|
| 608 | 577 | 37 -> Msg <$> return RtsRevertCAFs
|
| ... | ... | @@ -648,9 +617,9 @@ putMessage m = case m of |
| 648 | 617 | BreakpointStatus arr ix -> putWord8 29 >> put arr >> put ix
|
| 649 | 618 | GetBreakpointVar a b -> putWord8 30 >> put a >> put b
|
| 650 | 619 | StartTH -> putWord8 31
|
| 651 | - RunModFinalizers a b -> putWord8 32 >> put a >> put b
|
|
| 620 | + RunModFinalizers a b c -> putWord8 32 >> put a >> put b >> put c
|
|
| 652 | 621 | AddSptEntry a b -> putWord8 33 >> put a >> put b
|
| 653 | - RunTH st q loc ty -> putWord8 34 >> put st >> put q >> put loc >> put ty
|
|
| 622 | + RunTH ii st q loc ty -> putWord8 34 >> put ii >> put st >> put q >> put loc >> put ty
|
|
| 654 | 623 | GetClosure a -> putWord8 35 >> put a
|
| 655 | 624 | Seq a -> putWord8 36 >> put a
|
| 656 | 625 | RtsRevertCAFs -> putWord8 37
|
| ... | ... | @@ -688,73 +657,40 @@ serializeBCOs rbcos = parMap doChunk (chunkList 100 rbcos) |
| 688 | 657 | chunkList _ [] = []
|
| 689 | 658 | chunkList n xs = as : chunkList n bs where (as,bs) = splitAt n xs
|
| 690 | 659 | |
| 691 | --- -----------------------------------------------------------------------------
|
|
| 692 | --- Reading/writing messages
|
|
| 693 | - |
|
| 694 | --- | An opaque pipe for bidirectional binary data transmission.
|
|
| 695 | -data Pipe = Pipe
|
|
| 696 | - { getSome :: !(IO ByteString)
|
|
| 697 | - , putAll :: !(B.Builder -> IO ())
|
|
| 698 | - , pipeLeftovers :: !(IORef (Maybe ByteString))
|
|
| 699 | - }
|
|
| 700 | - |
|
| 701 | --- | Make a 'Pipe' from a 'Handle' to read and a 'Handle' to write.
|
|
| 702 | -mkPipeFromHandles :: Handle -> Handle -> IO Pipe
|
|
| 703 | -mkPipeFromHandles pipeRead pipeWrite = do
|
|
| 704 | - let getSome = B.hGetSome pipeRead (32*1024)
|
|
| 705 | - putAll b = do
|
|
| 706 | - B.hPutBuilder pipeWrite b
|
|
| 707 | - hFlush pipeWrite
|
|
| 708 | - pipeLeftovers <- newIORef Nothing
|
|
| 709 | - pure $ Pipe { getSome, putAll, pipeLeftovers }
|
|
| 710 | - |
|
| 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 }
|
|
| 716 | - |
|
| 660 | +-- | Send a 'Message' request to and wait for a reply from the interpreter
|
|
| 661 | +-- (blocks on the calling side, doesn't necessarily block the interpreter)
|
|
| 717 | 662 | remoteCall :: Binary a => Pipe -> Message a -> IO a
|
| 718 | -remoteCall pipe msg = do
|
|
| 719 | - writePipe pipe (putMessage msg)
|
|
| 720 | - readPipe pipe get
|
|
| 721 | - |
|
| 722 | -writePipe :: Pipe -> Put -> IO ()
|
|
| 723 | -writePipe Pipe{..} put = putAll $ execPut put
|
|
| 724 | - |
|
| 725 | -remoteTHCall :: Binary a => Pipe -> THMessage a -> IO a
|
|
| 726 | -remoteTHCall pipe msg = do
|
|
| 727 | - writePipe pipe (putTHMessage msg)
|
|
| 728 | - readPipe pipe get
|
|
| 729 | - |
|
| 730 | -readPipe :: Pipe -> Get a -> IO a
|
|
| 731 | -readPipe Pipe{..} get = do
|
|
| 732 | - leftovers <- readIORef pipeLeftovers
|
|
| 733 | - m <- getBin getSome get leftovers
|
|
| 734 | - case m of
|
|
| 735 | - Nothing -> throw $
|
|
| 736 | - mkIOError eofErrorType "GHCi.Message.readPipe" Nothing Nothing
|
|
| 737 | - Just (result, new_leftovers) -> do
|
|
| 738 | - writeIORef pipeLeftovers new_leftovers
|
|
| 739 | - return result
|
|
| 740 | - |
|
| 741 | -getBin
|
|
| 742 | - :: (IO ByteString) -> Get a -> Maybe ByteString
|
|
| 743 | - -> IO (Maybe (a, Maybe ByteString))
|
|
| 744 | - |
|
| 745 | -getBin getsome get leftover = go leftover (runGetIncremental get)
|
|
| 746 | - where
|
|
| 747 | - go Nothing (Done leftover _ msg) =
|
|
| 748 | - return (Just (msg, if B.null leftover then Nothing else Just leftover))
|
|
| 749 | - go _ Done{} = throwIO (ErrorCall "getBin: Done with leftovers")
|
|
| 750 | - go (Just leftover) (Partial fun) = do
|
|
| 751 | - go Nothing (fun (Just leftover))
|
|
| 752 | - go Nothing (Partial fun) = do
|
|
| 753 | - -- putStrLn "before hGetSome"
|
|
| 754 | - b <- getsome
|
|
| 755 | - -- putStrLn $ "hGetSome: " ++ show (B.length b)
|
|
| 756 | - if B.null b
|
|
| 757 | - then return Nothing
|
|
| 758 | - else go Nothing (fun (Just b))
|
|
| 759 | - go _lft (Fail _rest _off str) =
|
|
| 760 | - throwIO (ErrorCall ("getBin: " ++ str)) |
|
| 663 | +remoteCall pipe msg = either throwIO (pure . runGet get)
|
|
| 664 | + =<< remoteAsyncReq pipe (putMessage msg) Nothing
|
|
| 665 | + |
|
| 666 | +-- | Handle a single 'Message' request on the interpreter and reply to the host
|
|
| 667 | +handleRemoteCall :: Pipe
|
|
| 668 | + -> (Msg -> IO Msg) {-^ Msg hook -}
|
|
| 669 | + -> (forall a. (Show a, Binary a) => Message a -> IO a)
|
|
| 670 | + -> IO ()
|
|
| 671 | +handleRemoteCall pipe hook k =
|
|
| 672 | + handleAsyncReq pipe getMessage Nothing \writer msg0 -> IO \s0 ->
|
|
| 673 | + case hook msg0 |#> s0 of -- unIO for linearity checking
|
|
| 674 | + (# s1, Msg msg #) -> case k msg |#> s1 of
|
|
| 675 | + (# s2, a #) -> writeReply writer (put a) |#> s2
|
|
| 676 | + |
|
| 677 | +-- | Send a 'THMessage' request to the host and wait for a reply from the host
|
|
| 678 | +-- to the interpreter.
|
|
| 679 | +--
|
|
| 680 | +-- A 'THMessage' request from the interpreter is always associated with a
|
|
| 681 | +-- 'RunTH'-like run. Since the host may have many in-flight TH runs, we always
|
|
| 682 | +-- want a specific host 'InboxId' (which is announced in the 'RunTH' request)
|
|
| 683 | +remoteTHCall :: Binary a => Pipe -> InboxId -> THMessage a -> IO a
|
|
| 684 | +remoteTHCall pipe ii msg = either throwIO (pure . runGet get)
|
|
| 685 | + =<< remoteAsyncReq pipe (putTHMessage msg) (Just ii)
|
|
| 686 | + |
|
| 687 | +-- | Handle a 'THMessage' request on the host and reply to the interpreter.
|
|
| 688 | +-- Symmterically to 'remoteTHCall', we always want to specify the 'InboxId' for
|
|
| 689 | +-- RunTH "reverse requests"
|
|
| 690 | +handleRemoteTHCall
|
|
| 691 | + :: Pipe -> InboxId -> (forall a. (Show a, Binary a) => THMessage a -> IO a) -> IO ()
|
|
| 692 | +handleRemoteTHCall pipe ii k =
|
|
| 693 | + handleAsyncReq pipe getTHMessage (Just ii) \writer (THMsg msg) -> IO \s0 ->
|
|
| 694 | + case k msg |#> s0 of -- unIO for linearity checking
|
|
| 695 | + (# s1, a #) -> writeReply writer (put a) |#> s1
|
|
| 696 | + |
| 1 | +{-# LANGUAGE RecursiveDo, GADTs, DeriveGeneric, StandaloneDeriving, ScopedTypeVariables,
|
|
| 2 | + GeneralizedNewtypeDeriving, ExistentialQuantification, RecordWildCards, CPP,
|
|
| 3 | + NamedFieldPuns, LambdaCase, TypeAbstractions, TypeApplications,
|
|
| 4 | + LinearTypes, RankNTypes, UnboxedTuples, BlockArguments, TupleSections, MagicHash #-}
|
|
| 5 | +{-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-orphans #-}
|
|
| 6 | +module GHCi.Message.Async
|
|
| 7 | + (
|
|
| 8 | + -- * Pipes
|
|
| 9 | + Pipe(..), mkPipeFromHandles, mkPipeFromContinuations
|
|
| 10 | + , closePipe, PipeReadClosedException(..)
|
|
| 11 | + |
|
| 12 | + -- * Interface for async bidirectional RPC
|
|
| 13 | + , remoteAsyncReq, handleAsyncReq
|
|
| 14 | + , RemoteProcessException(..), DecodeAsyncReqException(..)
|
|
| 15 | + |
|
| 16 | + -- ** Mailboxes
|
|
| 17 | + , InboxId(..)
|
|
| 18 | + , newMailbox
|
|
| 19 | + , closeMailbox
|
|
| 20 | + |
|
| 21 | + -- ** Async matching internals
|
|
| 22 | + , AsyncManager(..), CorrelationId
|
|
| 23 | + , IddMsg(..), newAsyncManager
|
|
| 24 | + |
|
| 25 | + -- ** Consume `reply` exactly once
|
|
| 26 | + , WriteReply(..)
|
|
| 27 | + , writeReply, (|#>)
|
|
| 28 | + |
|
| 29 | + -- * Serializable exceptions
|
|
| 30 | + , SerializableException(..)
|
|
| 31 | + , toSerializableException, fromSerializableException
|
|
| 32 | + ) where
|
|
| 33 | + |
|
| 34 | +import Prelude -- See note [Why do we import Prelude here?]
|
|
| 35 | +import GHCi.TH.Binary () -- For Binary instances
|
|
| 36 | + |
|
| 37 | +import GHC.IO (IO(..))
|
|
| 38 | +import Control.Concurrent
|
|
| 39 | +import Control.Exception
|
|
| 40 | +#if MIN_VERSION_base(4,20,0)
|
|
| 41 | +import Control.Exception.Context
|
|
| 42 | +#endif
|
|
| 43 | +import Data.Binary
|
|
| 44 | +import Data.Binary.Get
|
|
| 45 | +import Data.Binary.Put
|
|
| 46 | +import Data.ByteString (ByteString)
|
|
| 47 | +import qualified Data.ByteString as B
|
|
| 48 | +import qualified Data.ByteString.Builder as B
|
|
| 49 | +import qualified Data.ByteString.Lazy as LB
|
|
| 50 | +import Data.IORef
|
|
| 51 | +import Data.IntMap (IntMap)
|
|
| 52 | +import qualified Data.IntMap as IntMap
|
|
| 53 | +import GHC.Generics
|
|
| 54 | +import System.Exit
|
|
| 55 | +import System.IO
|
|
| 56 | +import Control.Monad
|
|
| 57 | +import Control.Concurrent.STM
|
|
| 58 | +import Data.Bifunctor
|
|
| 59 | +import GHC.Base (State#)
|
|
| 60 | +import GHC.Exts (RealWorld)
|
|
| 61 | + |
|
| 62 | +--------------------------------------------------------------------------------
|
|
| 63 | +-- * Reading/writing and Pipes
|
|
| 64 | +--------------------------------------------------------------------------------
|
|
| 65 | + |
|
| 66 | +-- | An opaque pipe for bidirectional binary data transmission.
|
|
| 67 | +data Pipe = Pipe
|
|
| 68 | + { getSome :: !(IO ByteString)
|
|
| 69 | + , putAll :: !(B.Builder -> IO ())
|
|
| 70 | + , pipeLeftovers :: !(IORef (Maybe ByteString))
|
|
| 71 | + , pipeLock :: !(MVar ()) -- ^ Lock to prevent concurrent writes to the stream
|
|
| 72 | + , pipeAsyncMngr :: !AsyncManager
|
|
| 73 | + , pipeCloseRead :: !(IO ())
|
|
| 74 | + -- ^ closes read end of the pipe
|
|
| 75 | + , pipeCloseWrite :: !(IO ())
|
|
| 76 | + -- ^ closes write end of the pipe
|
|
| 77 | + , pipeReadClosed :: !(TVar Bool)
|
|
| 78 | + -- ^ After the pipe is closed, messages can no longer be received from the
|
|
| 79 | + -- other end, but the 'AsyncManager' queues might not be empty yet.
|
|
| 80 | + }
|
|
| 81 | + |
|
| 82 | +-- | Make a 'Pipe' from a 'Handle' to read and a 'Handle' to write.
|
|
| 83 | +-- Must call 'closePipe' at the end to clean up.
|
|
| 84 | +mkPipeFromHandles :: Handle -> Handle -> IO Pipe
|
|
| 85 | +mkPipeFromHandles pipeRead pipeWrite = do
|
|
| 86 | + let getSome = B.hGetSome pipeRead (32*1024)
|
|
| 87 | + putAll b = do
|
|
| 88 | + B.hPutBuilder pipeWrite b
|
|
| 89 | + hFlush pipeWrite
|
|
| 90 | + mkPipeFromContinuations getSome putAll (hClose pipeRead) (hClose pipeWrite)
|
|
| 91 | + |
|
| 92 | +-- | Make a 'Pipe' from a reader function and a writer function.
|
|
| 93 | +-- The pipe should be closed with 'closePipe', which will call 'pipeCloseRead' and 'pipeCloseWrite'.
|
|
| 94 | +mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ())
|
|
| 95 | + -> IO ()
|
|
| 96 | + -- ^ pipeCloseRead
|
|
| 97 | + -> IO ()
|
|
| 98 | + -- ^ pipeCloseWrite
|
|
| 99 | + -> IO Pipe
|
|
| 100 | +mkPipeFromContinuations getSome putAll pipeCloseRead pipeCloseWrite = mdo
|
|
| 101 | + pipeLeftovers <- newIORef Nothing
|
|
| 102 | + pipeLock <- newMVar ()
|
|
| 103 | + pipeReadClosed <- newTVarIO False
|
|
| 104 | + mdo
|
|
| 105 | + pipeAsyncMngr <- newAsyncManager p
|
|
| 106 | + let p = Pipe { .. }
|
|
| 107 | + pure p
|
|
| 108 | + |
|
| 109 | +-- | Cleanly close our ends of the Pipe, terminating the async manager's
|
|
| 110 | +-- drain-read-end thread. Also calls 'pipeCloseRead' and 'pipeCloseWrite', in that order.
|
|
| 111 | +--
|
|
| 112 | +-- If the pipe was created using 'mkPipeFromHandles', the handles are also closed.
|
|
| 113 | +--
|
|
| 114 | +-- The requests we've sent that are pending are all canceled with
|
|
| 115 | +-- 'PipeReadClosedException' by ourselves.
|
|
| 116 | +--
|
|
| 117 | +-- No special notification is sent to the remote process, a message to that
|
|
| 118 | +-- effect should be sent ahead of time if one is needed (see 'Shutdown').
|
|
| 119 | +closePipe :: Pipe -> IO ()
|
|
| 120 | +closePipe Pipe{..} = do
|
|
| 121 | + |
|
| 122 | + readAlreadyClosed <- atomically $ swapTVar pipeReadClosed True
|
|
| 123 | + |
|
| 124 | + -- this triggers pipeCloseRead
|
|
| 125 | + unless readAlreadyClosed $ do
|
|
| 126 | + |
|
| 127 | + -- releases handle lock taken by reading, and cleans up by replying to every
|
|
| 128 | + -- pending request with 'PipeReadClosedException'.
|
|
| 129 | + throwTo (asyncDrainTid pipeAsyncMngr) PipeReadClosedException
|
|
| 130 | + |
|
| 131 | + pipeCloseWrite
|
|
| 132 | + |
|
| 133 | +-- | We've closed the pipe read end on this process
|
|
| 134 | +data PipeReadClosedException = PipeReadClosedException deriving Show
|
|
| 135 | +instance Exception PipeReadClosedException
|
|
| 136 | + |
|
| 137 | +--------------------------------------------------------------------------------
|
|
| 138 | +-- ** Pipe thread-safe read/write
|
|
| 139 | +--------------------------------------------------------------------------------
|
|
| 140 | +-- All messages on the wire are 'IddMsg', which contain a correlation header
|
|
| 141 | +-- and identify whether they are requests or replies.
|
|
| 142 | + |
|
| 143 | +writePipe :: Pipe -> IddMsg -> IO ()
|
|
| 144 | +writePipe p@Pipe{..} = withLock p . putAll . execPut . put
|
|
| 145 | + |
|
| 146 | +-- | Only the 'drainPipe' thread should read from the pipe directly. Other
|
|
| 147 | +-- functions should wait for the matching reply to be delivered asynchronously.
|
|
| 148 | +-- (see 'withAsyncReq'). It doesn't lock to read bc it assumes single reader is 'drainPipe'.
|
|
| 149 | +--
|
|
| 150 | +-- Returns @Nothing@ on EOF.
|
|
| 151 | +readPipe :: Pipe -> IO (Maybe IddMsg)
|
|
| 152 | +readPipe Pipe{..} = do
|
|
| 153 | + leftovers <- readIORef pipeLeftovers
|
|
| 154 | + m <- getBin getSome get leftovers
|
|
| 155 | + case m of
|
|
| 156 | + Nothing -> return Nothing
|
|
| 157 | + Just (result, new_leftovers) -> do
|
|
| 158 | + writeIORef pipeLeftovers new_leftovers
|
|
| 159 | + return (Just result)
|
|
| 160 | + |
|
| 161 | +getBin
|
|
| 162 | + :: (IO ByteString) -> Get a -> Maybe ByteString
|
|
| 163 | + -> IO (Maybe (a, Maybe ByteString))
|
|
| 164 | + |
|
| 165 | +getBin getsome get leftover = go leftover (runGetIncremental get)
|
|
| 166 | + where
|
|
| 167 | + go Nothing (Done leftover _ msg) =
|
|
| 168 | + return (Just (msg, if B.null leftover then Nothing else Just leftover))
|
|
| 169 | + go _ Done{} = throwIO (ErrorCall "getBin: Done with leftovers")
|
|
| 170 | + go (Just leftover) (Partial fun) = do
|
|
| 171 | + go Nothing (fun (Just leftover))
|
|
| 172 | + go Nothing (Partial fun) = do
|
|
| 173 | + -- putStrLn "before hGetSome"
|
|
| 174 | + b <- getsome
|
|
| 175 | + -- putStrLn $ "hGetSome: " ++ show (B.length b)
|
|
| 176 | + if B.null b
|
|
| 177 | + then return Nothing
|
|
| 178 | + else go Nothing (fun (Just b))
|
|
| 179 | + go _lft (Fail _rest _off str) =
|
|
| 180 | + throwIO (ErrorCall ("getBin: " ++ str))
|
|
| 181 | + |
|
| 182 | +withLock :: Pipe -> IO c -> IO c
|
|
| 183 | +withLock Pipe{..} = bracket (takeMVar pipeLock) (putMVar pipeLock) . const
|
|
| 184 | + |
|
| 185 | +--------------------------------------------------------------------------------
|
|
| 186 | +-- * Matching requests with async replies
|
|
| 187 | +--------------------------------------------------------------------------------
|
|
| 188 | + |
|
| 189 | +-- | Send an asynchronous request and block waiting for a matching reply
|
|
| 190 | +remoteAsyncReq :: Pipe -> Put -> (Maybe InboxId) -> IO (Either SomeException LB.ByteString)
|
|
| 191 | +remoteAsyncReq pipe msg ii = do
|
|
| 192 | + |
|
| 193 | + let pendingReplies = pendingReply (pipeAsyncMngr pipe)
|
|
| 194 | + |
|
| 195 | + -- Mark pending request
|
|
| 196 | + uq <- freshId (pipeAsyncMngr pipe)
|
|
| 197 | + wv <- newEmptyTMVarIO
|
|
| 198 | + |
|
| 199 | + isClosed <- atomically $ do
|
|
| 200 | + isClosed <- readTVar (pipeReadClosed pipe)
|
|
| 201 | + -- don't send the req if we won't get a reply.
|
|
| 202 | + if isClosed
|
|
| 203 | + then pure True
|
|
| 204 | + else do
|
|
| 205 | + modifyTVar pendingReplies (IntMap.insert uq wv)
|
|
| 206 | + pure False
|
|
| 207 | + |
|
| 208 | + when isClosed $ do
|
|
| 209 | + throwIO PipeReadClosedException
|
|
| 210 | + |
|
| 211 | + -- Write request
|
|
| 212 | + writePipe pipe (Request uq ii (runPut msg))
|
|
| 213 | + `onException`
|
|
| 214 | + -- the req wasn't done, or no one will be reading the reply:
|
|
| 215 | + -- in either case, drop it from the pending map.
|
|
| 216 | + atomically (modifyTVar pendingReplies (IntMap.delete uq))
|
|
| 217 | + |
|
| 218 | + -- Block waiting for reply
|
|
| 219 | + atomically (takeTMVar wv)
|
|
| 220 | + `onException`
|
|
| 221 | + atomically (modifyTVar pendingReplies (IntMap.delete uq))
|
|
| 222 | + |
|
| 223 | +-- | Receive a single asynchronous request and send a matching reply
|
|
| 224 | +-- asynchronously (the handler may fork a computation that eventually replies
|
|
| 225 | +-- and can return before actually replying, as long as it eventually does).
|
|
| 226 | +--
|
|
| 227 | +-- The handler receives as an argument the function to use to reply to the request.
|
|
| 228 | +-- It must be used exactly once (all requests expect a reply, and you can't
|
|
| 229 | +-- reply more than once). Linear types enforce the handler replies in every
|
|
| 230 | +-- branch, and if the handler otherwise throws, it is handled by 'handleAsyncReq'.
|
|
| 231 | +handleAsyncReq :: Pipe -> Get a -> (Maybe InboxId) -> (WriteReply Put %1 -> a -> IO r) -> IO r
|
|
| 232 | +handleAsyncReq pipe getit mii k = mask $ \restore -> do
|
|
| 233 | + let am = pipeAsyncMngr pipe
|
|
| 234 | + ack <- newEmptyMVar
|
|
| 235 | + |
|
| 236 | + -- Block waiting for request
|
|
| 237 | + mbreq <- atomically $ do
|
|
| 238 | + isClosed <- readTVar (pipeReadClosed pipe)
|
|
| 239 | + taken <- case mii of
|
|
| 240 | + Nothing -> tryReadTQueue $ requestsQueue am
|
|
| 241 | + Just ii -> tryReadTQueue =<< getMailbox am ii
|
|
| 242 | + case taken of
|
|
| 243 | + Just req -> pure (Just req)
|
|
| 244 | + Nothing | isClosed -> pure Nothing -- empty and closed
|
|
| 245 | + | otherwise -> retry -- wait for msg
|
|
| 246 | + |
|
| 247 | + case mbreq of
|
|
| 248 | + Nothing -> throwIO PipeReadClosedException -- can't expect reqs
|
|
| 249 | + Just (uq, bs) -> do
|
|
| 250 | + let reply p = do
|
|
| 251 | + writePipe pipe (Reply uq (bimap toSerializableException runPut p))
|
|
| 252 | + putMVar ack ()
|
|
| 253 | + |
|
| 254 | + -- Run the continuation which must reply exactly once with 'writeReply'
|
|
| 255 | + case runGetOrFail getit bs of
|
|
| 256 | + Left (_, _, err) -> do
|
|
| 257 | + let exc = DecodeAsyncReqException err
|
|
| 258 | + reply (Left (toException exc))
|
|
| 259 | + throwIO exc
|
|
| 260 | + Right (_, _, it) ->
|
|
| 261 | + restore (k (WriteReply reply) it)
|
|
| 262 | + |
|
| 263 | + -- If there's an exception before the reply is sent, reply with an error instead.
|
|
| 264 | + `catch` \(e :: SomeException) -> do
|
|
| 265 | + |
|
| 266 | + tryTakeMVar ack >>= \case
|
|
| 267 | + Just () ->
|
|
| 268 | + -- Already replied
|
|
| 269 | + throwIO e
|
|
| 270 | + Nothing -> do
|
|
| 271 | + -- Reply with an error
|
|
| 272 | + reply (Left e)
|
|
| 273 | + throwIO e
|
|
| 274 | + |
|
| 275 | +-- | 'handleSyncReq' failed to decode an incoming request payload using the given @'Get' a@
|
|
| 276 | +newtype DecodeAsyncReqException = DecodeAsyncReqException String deriving Show
|
|
| 277 | +instance Exception DecodeAsyncReqException
|
|
| 278 | + |
|
| 279 | + |
|
| 280 | +-- ** Mailboxes ----------------------------------------------------------------
|
|
| 281 | + |
|
| 282 | +-- | A 'Request' may optionally declare an 'InboxId'. If it does, it can only
|
|
| 283 | +-- be read by a 'handleAsyncReq' for that specific 'InboxId'. That is,
|
|
| 284 | +-- @'handleAsyncReq' p get Nothing@ will not receive messages meant for a
|
|
| 285 | +-- specific inbox.
|
|
| 286 | +--
|
|
| 287 | +-- This is useful to prevent mixing up reverse requests, where we want to send
|
|
| 288 | +-- many reverse requests but have the caller handle them without interleaving
|
|
| 289 | +-- them.
|
|
| 290 | +newtype InboxId = InboxId Int deriving (Show, Binary)
|
|
| 291 | + |
|
| 292 | +-- | Create a new mailbox from where we can read all requests addressed to this 'InboxId'.
|
|
| 293 | +newMailbox :: AsyncManager -> IO InboxId
|
|
| 294 | +newMailbox am = InboxId <$> freshId am
|
|
| 295 | + -- `mailboxQueues` is written on demand, no need to do it here.
|
|
| 296 | + |
|
| 297 | +-- | Clean up a created mailbox by 'InboxId'.
|
|
| 298 | +closeMailbox :: AsyncManager -> InboxId -> IO ()
|
|
| 299 | +closeMailbox AsyncManager{..} (InboxId ii) =
|
|
| 300 | + atomically (modifyTVar mailboxQueues (IntMap.delete ii))
|
|
| 301 | + |
|
| 302 | +-- | Get or create a new mailbox for this 'InboxId'
|
|
| 303 | +getMailbox :: AsyncManager -> InboxId -> STM (TQueue (CorrelationId, LB.ByteString))
|
|
| 304 | +getMailbox am (InboxId ii) = do
|
|
| 305 | + mboxes <- readTVar (mailboxQueues am)
|
|
| 306 | + case IntMap.lookup ii mboxes of
|
|
| 307 | + Nothing -> do
|
|
| 308 | + q <- newTQueue
|
|
| 309 | + writeTVar (mailboxQueues am) (IntMap.insert ii q mboxes)
|
|
| 310 | + pure q
|
|
| 311 | + Just q ->
|
|
| 312 | + pure q
|
|
| 313 | + |
|
| 314 | +-- ** Async matching "internals" -----------------------------------------------
|
|
| 315 | + |
|
| 316 | +data AsyncManager = AsyncManager
|
|
| 317 | + { pendingReply :: !(TVar (IntMap (TMVar (Either SomeException LB.ByteString))))
|
|
| 318 | + -- ^ When a request is sent, keep track that we're expecting a reply. The
|
|
| 319 | + -- TMVar is signaled when the reply arrives. The exception can originate
|
|
| 320 | + -- from the host process, or from the remote one (with 'RemoteProcessException').
|
|
| 321 | + , requestsQueue :: !(TQueue (CorrelationId, LB.ByteString))
|
|
| 322 | + -- ^ A queue of pending received requests. When a request is received
|
|
| 323 | + -- /without/ a specific 'InboxId', queue it here. A request handler will
|
|
| 324 | + -- pop the next request from it. (Nothing except the 'drainPipe' thread
|
|
| 325 | + -- should read from the pipe). Read with @'handleAsyncReq' p get Nothing@.
|
|
| 326 | + , mailboxQueues :: !(TVar (IntMap (TQueue (CorrelationId, LB.ByteString))))
|
|
| 327 | + -- ^ Requests for a specific 'InboxId' that can be read using
|
|
| 328 | + -- @'handleAsyncReq' p get (Just <mailboxid>)@.
|
|
| 329 | + , asyncDrainTid :: !ThreadId
|
|
| 330 | + , asyncFreshId :: !(IORef Int)
|
|
| 331 | + }
|
|
| 332 | + |
|
| 333 | +-- | Each 'Request' expects a 'Reply' with a matching 'CorrelationId'.
|
|
| 334 | +type CorrelationId = Int
|
|
| 335 | + |
|
| 336 | +-- | We have a single type for requests and replies because both the host and
|
|
| 337 | +-- interpreter may send requests *and* replies on the write end *and* on recv
|
|
| 338 | +-- both on the read end.
|
|
| 339 | +--
|
|
| 340 | +-- For instance:
|
|
| 341 | +-- 1. The host sends a RunTH request on its Host Write End (HWE)
|
|
| 342 | +-- 2. The interpreter sends a NewName TH request on its Interpreter Write End (IWE)
|
|
| 343 | +-- 3. The host sends a reply to the TH request on its HWE
|
|
| 344 | +-- 4. The interpreter sends the reply to the RunTH request on its IWE
|
|
| 345 | +--
|
|
| 346 | +-- Additionally, we want to keep concurrent TH requests for independent RunTH
|
|
| 347 | +-- calls separated. Thus the interpreter requests additionally specify a
|
|
| 348 | +-- mailbox announced by RunTH:
|
|
| 349 | +-- 1. To interpreter: @Request _ Nothing (RunTH <mailid>)@
|
|
| 350 | +-- 2. From interpreter: @Request _ (Just <mailid>) NewName@
|
|
| 351 | +-- And the host will relate the <mailid> reverse requests to that RunTH run
|
|
| 352 | +-- exclusively.
|
|
| 353 | +data IddMsg = Request !CorrelationId !(Maybe InboxId) LB.ByteString
|
|
| 354 | + | Reply !CorrelationId (Either SerializableException LB.ByteString)
|
|
| 355 | + |
|
| 356 | +instance Binary IddMsg where
|
|
| 357 | + put (Request i ii bs) = putWord8 0 >> put i >> put ii >> put bs
|
|
| 358 | + put (Reply i bs) = putWord8 1 >> put i >> put bs
|
|
| 359 | + get = do
|
|
| 360 | + b <- getWord8
|
|
| 361 | + case b of
|
|
| 362 | + 0 -> Request <$> get <*> get <*> get
|
|
| 363 | + 1 -> Reply <$> get <*> get
|
|
| 364 | + _ -> fail $ "impossible: Binary IddMsg - " ++ show b
|
|
| 365 | + |
|
| 366 | +newAsyncManager :: Pipe -> IO AsyncManager
|
|
| 367 | +newAsyncManager pipe = do
|
|
| 368 | + asyncFreshId <- newIORef 0
|
|
| 369 | + pendingReply <- newTVarIO IntMap.empty
|
|
| 370 | + requestsQueue <- newTQueueIO
|
|
| 371 | + mailboxQueues <- newTVarIO IntMap.empty
|
|
| 372 | + |
|
| 373 | + -- Fork thread to drain read end.
|
|
| 374 | + -- (cleaned up on 'closePipe')
|
|
| 375 | + asyncDrainTid <- forkIOWithUnmask (drainPipe pipe)
|
|
| 376 | + |
|
| 377 | + let am = AsyncManager{..}
|
|
| 378 | + pure am
|
|
| 379 | + |
|
| 380 | +-- | Drains the read end of the Pipe and match 'Reply'es against pending 'Request's.
|
|
| 381 | +--
|
|
| 382 | +-- In pratice, we'll have both a thread draining the read end of the host's
|
|
| 383 | +-- 'Pipe' and a thread draining the read end of the interpreter's 'Pipe', since
|
|
| 384 | +-- both processes construct a 'Pipe' and send and receive messages
|
|
| 385 | +--
|
|
| 386 | +-- Closes the Pipe read end when done reading
|
|
| 387 | +drainPipe :: Pipe -> (forall x. IO x -> IO x) -> IO ()
|
|
| 388 | +drainPipe p unmask = catch (unmask $ loop `finally` pipeCloseRead p) replyToPendings where
|
|
| 389 | + loop = do
|
|
| 390 | + readPipe p >>= \case
|
|
| 391 | + -- Receive a reply to a pending notification
|
|
| 392 | + Just (Reply uq bs) -> do
|
|
| 393 | + atomically $ do
|
|
| 394 | + m <- readTVar (pendingReply am)
|
|
| 395 | + case IntMap.lookup uq m of
|
|
| 396 | + Nothing ->
|
|
| 397 | + -- the req was dropped from pending bc there was an exception, and no
|
|
| 398 | + -- one's listening any more. the reply is ignored
|
|
| 399 | + pure ()
|
|
| 400 | + Just wv -> do
|
|
| 401 | + writeTVar (pendingReply am) (IntMap.delete uq m)
|
|
| 402 | + -- signal matching request
|
|
| 403 | + putTMVar wv (bimap (toException . RemoteProcessException) id bs)
|
|
| 404 | + loop
|
|
| 405 | + -- Receive a request and queue it
|
|
| 406 | + Just (Request uq mbi bs) -> do
|
|
| 407 | + atomically $ case mbi of
|
|
| 408 | + Nothing ->
|
|
| 409 | + -- "Global" request
|
|
| 410 | + writeTQueue (requestsQueue am) (uq,bs)
|
|
| 411 | + Just ii -> do
|
|
| 412 | + -- "Specific mailbox" request
|
|
| 413 | + q <- getMailbox am ii
|
|
| 414 | + writeTQueue q (uq,bs)
|
|
| 415 | + loop
|
|
| 416 | + -- EOF on read handle
|
|
| 417 | + Nothing -> do
|
|
| 418 | + -- clean up pipe then exit loop
|
|
| 419 | + replyToPendings (toException PipeReadClosedException)
|
|
| 420 | + pure ()
|
|
| 421 | + |
|
| 422 | + replyToPendings e = atomically $ do
|
|
| 423 | + -- reqs are only added if pipe not closed;
|
|
| 424 | + -- thus we close and take all pending atomically.
|
|
| 425 | + writeTVar (pipeReadClosed p) True
|
|
| 426 | + pendings <- swapTVar (pendingReply am) IntMap.empty -- take pending
|
|
| 427 | + mapM_ (\p -> putTMVar p (Left e {-host exception-})) pendings
|
|
| 428 | + |
|
| 429 | + am = pipeAsyncMngr p
|
|
| 430 | + |
|
| 431 | +-- | An exception that can be thrown on the host process representing an
|
|
| 432 | +-- expection that happened on the remote one.
|
|
| 433 | +newtype RemoteProcessException = RemoteProcessException SerializableException deriving Show
|
|
| 434 | +instance Exception RemoteProcessException
|
|
| 435 | + |
|
| 436 | +-- | Come up with a fresh id for 'CorrelationId' or 'InboxId'
|
|
| 437 | +freshId :: AsyncManager -> IO Int
|
|
| 438 | +freshId AsyncManager{asyncFreshId} =
|
|
| 439 | + atomicModifyIORef' asyncFreshId $ \i -> (i+1, i)
|
|
| 440 | + |
|
| 441 | +--------------------------------------------------------------------------------
|
|
| 442 | +-- * Consume `reply` exactly once
|
|
| 443 | +--------------------------------------------------------------------------------
|
|
| 444 | + |
|
| 445 | +-- | Every request should get exactly one reply.
|
|
| 446 | +data WriteReply a where
|
|
| 447 | + WriteReply :: (Either SomeException a -> IO ()) %1 -> WriteReply a
|
|
| 448 | + |
|
| 449 | +-- | Write a reply exactly once (consuming the ability to write more replies)
|
|
| 450 | +writeReply :: WriteReply a %1 -> a -> IO ()
|
|
| 451 | +writeReply (WriteReply r) = \x -> r (Right x)
|
|
| 452 | + |
|
| 453 | +-- | Infix, linear, 'unIO'
|
|
| 454 | +(|#>) :: IO a %1 -> State# RealWorld -> (# State# RealWorld, a #)
|
|
| 455 | +(|#>) (IO fs) s = fs s
|
|
| 456 | + |
|
| 457 | +--------------------------------------------------------------------------------
|
|
| 458 | +-- * Serializable Exceptions
|
|
| 459 | +--------------------------------------------------------------------------------
|
|
| 460 | + |
|
| 461 | +-- SomeException can't be serialized because it contains dynamic
|
|
| 462 | +-- types. However, we do very limited things with the exceptions that
|
|
| 463 | +-- are thrown by interpreted computations:
|
|
| 464 | +--
|
|
| 465 | +-- * We print them, e.g. "*** Exception: <something>"
|
|
| 466 | +-- * UserInterrupt has a special meaning
|
|
| 467 | +-- * In ghc -e, exitWith should exit with the appropriate exit code
|
|
| 468 | +--
|
|
| 469 | +-- So all we need to do is distinguish UserInterrupt and ExitCode, and
|
|
| 470 | +-- all other exceptions can be represented by their 'show' string.
|
|
| 471 | +--
|
|
| 472 | +data SerializableException
|
|
| 473 | + = EUserInterrupt
|
|
| 474 | + | EExitCode ExitCode
|
|
| 475 | + | EOtherException String
|
|
| 476 | + deriving (Generic, Show)
|
|
| 477 | + |
|
| 478 | +toSerializableException :: SomeException -> SerializableException
|
|
| 479 | +toSerializableException ex
|
|
| 480 | + | Just UserInterrupt <- fromException ex = EUserInterrupt
|
|
| 481 | + | Just (ec::ExitCode) <- fromException ex = (EExitCode ec)
|
|
| 482 | + | otherwise = EOtherException $
|
|
| 483 | +#if MIN_VERSION_base(4,20,0)
|
|
| 484 | + -- Exception plus backtrace as seen in `displayExceptionWithInfo`
|
|
| 485 | + case displayExceptionContext (someExceptionContext ex) of
|
|
| 486 | + "" -> displayException (ex :: SomeException)
|
|
| 487 | + cx -> displayException (ex :: SomeException) ++ "\n\n" ++ cx
|
|
| 488 | +#else
|
|
| 489 | + show (ex :: SomeException)
|
|
| 490 | +#endif
|
|
| 491 | + |
|
| 492 | +fromSerializableException :: SerializableException -> SomeException
|
|
| 493 | +fromSerializableException EUserInterrupt = toException UserInterrupt
|
|
| 494 | +fromSerializableException (EExitCode c) = toException c
|
|
| 495 | +fromSerializableException (EOtherException str) = toException (ErrorCall str)
|
|
| 496 | + |
|
| 497 | +instance Binary ExitCode
|
|
| 498 | +instance Binary SerializableException |
| 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,29 @@ 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 | - |
|
| 69 | - discardCtrlC
|
|
| 70 | - |
|
| 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
|
|
| 78 | - |
|
| 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
|
|
| 68 | + handleRemoteCall pipe hook $ \(msg :: Message a) -> do
|
|
| 69 | + |
|
| 70 | + discardCtrlC
|
|
| 71 | + |
|
| 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 ii st q ty loc -> wrapRunTH ii $ runTH pipe ii st q ty loc
|
|
| 78 | + RunModFinalizers ii st qrefs -> wrapRunTH ii $ runModFinalizerRefs pipe ii st qrefs
|
|
| 79 | + _other -> run msg >>= reply
|
|
| 80 | + |
|
| 81 | + -- closePipe here will wait until every active pipeWrite pipe exits, but
|
|
| 82 | + -- they should never block for long, as the host process is either reading
|
|
| 83 | + -- or has called pipeCloseRead.
|
|
| 84 | + isDone <- readIORef doneRef
|
|
| 85 | + if isDone then closePipe pipe
|
|
| 86 | + else loop doneRef
|
|
| 84 | 87 | |
| 85 | 88 | handleCustom tag payload = do
|
| 86 | 89 | mresp <- customHandler tag payload
|
| ... | ... | @@ -89,17 +92,22 @@ servWithCustom verbose hook pipe restore customHandler = loop |
| 89 | 92 | Nothing ->
|
| 90 | 93 | error $ "GHCi.Server: unhandled CustomMessage with tag " ++ show tag
|
| 91 | 94 | |
| 95 | + reply :: forall a. Show a => a -> IO a
|
|
| 96 | + reply r = do
|
|
| 97 | + when verbose $ trace ("writing pipe: " ++ show r)
|
|
| 98 | + return r -- the result of the handler gets sent
|
|
| 99 | + |
|
| 92 | 100 | -- Run some TH code, which may interact with GHC by sending
|
| 93 | 101 | -- THMessage requests, and then finally send RunTHDone followed by a
|
| 94 | 102 | -- QResult. For an overview of how TH works with Remote GHCi, see
|
| 95 | 103 | -- Note [Remote Template Haskell] in libraries/ghci/GHCi/TH.hs.
|
| 96 | - wrapRunTH :: forall a. (Binary a, Show a) => IO a -> IO ()
|
|
| 97 | - wrapRunTH io = do
|
|
| 104 | + wrapRunTH :: forall a. (Binary a, Show a) => InboxId -> IO a -> IO (QResult a)
|
|
| 105 | + wrapRunTH ii io = do
|
|
| 98 | 106 | when verbose $ trace "wrapRunTH..."
|
| 99 | 107 | r <- try io
|
| 100 | 108 | when verbose $ trace "wrapRunTH done."
|
| 101 | 109 | when verbose $ trace "writing RunTHDone."
|
| 102 | - writePipe pipe (putTHMessage RunTHDone)
|
|
| 110 | + remoteTHCall pipe ii RunTHDone
|
|
| 103 | 111 | case r of
|
| 104 | 112 | Left e
|
| 105 | 113 | | Just (GHCiQException _ err) <- fromException e -> do
|
| ... | ... | @@ -179,7 +187,7 @@ defaultServerWithCustom customHandler = do |
| 179 | 187 | #if defined(wasm32_HOST_ARCH)
|
| 180 | 188 | -- See Note [wasm ghci signal handlers] for details
|
| 181 | 189 | installSignalHandlers $ js_register_signal_handler cb_sig
|
| 182 | - pipe <- mkPipeFromContinuations (recv_buf cb_recv) (send_buf cb_send)
|
|
| 190 | + pipe <- mkPipeFromContinuations (recv_buf cb_recv) (send_buf cb_send) (pure ()) (pure ())
|
|
| 183 | 191 | #else
|
| 184 | 192 | when verbose $
|
| 185 | 193 | printf "GHC iserv starting (in: %s; out: %s)\n" (show inh) (show outh)
|
| ... | ... | @@ -115,8 +115,8 @@ import qualified GHC.Boot.TH.Monad as TH |
| 115 | 115 | import Unsafe.Coerce
|
| 116 | 116 | |
| 117 | 117 | -- | Create a new instance of 'QState'
|
| 118 | -initQState :: Pipe -> QState
|
|
| 119 | -initQState p = QState M.empty Nothing p
|
|
| 118 | +initQState :: Pipe -> InboxId -> QState
|
|
| 119 | +initQState p ii = QState M.empty Nothing ii p
|
|
| 120 | 120 | |
| 121 | 121 | -- | The monad in which we run TH computations on the server
|
| 122 | 122 | newtype GHCiQ a = GHCiQ { runGHCiQ :: IORef QState -> IO a }
|
| ... | ... | @@ -156,7 +156,7 @@ noLoc = TH.Loc "<no file>" "<no package>" "<no module>" (0,0) (0,0) |
| 156 | 156 | ghcCmd :: Binary a => THMessage (THResult a) -> GHCiQ a
|
| 157 | 157 | ghcCmd m = GHCiQ $ \sRef -> do
|
| 158 | 158 | s <- readIORef sRef
|
| 159 | - r <- remoteTHCall (qsPipe s) m
|
|
| 159 | + r <- remoteTHCall (qsPipe s) (qsInboxId s) m
|
|
| 160 | 160 | case r of
|
| 161 | 161 | THException str -> throwIO (GHCiQException s str)
|
| 162 | 162 | THComplete res -> return res
|
| ... | ... | @@ -189,9 +189,9 @@ metaHandlersGHCiQ runInIO = TH.MetaHandlers { |
| 189 | 189 | -- See Note [TH recover with -fexternal-interpreter] in GHC.Tc.Gen.Splice
|
| 190 | 190 | , mRecover = \h a -> runInIO $ GHCiQ $ \sRef -> mask $ \unmask -> do
|
| 191 | 191 | s <- readIORef sRef
|
| 192 | - remoteTHCall (qsPipe s) StartRecover
|
|
| 192 | + remoteTHCall (qsPipe s) (qsInboxId s) StartRecover
|
|
| 193 | 193 | e <- try $ unmask $ runGHCiQ (runQinGHCiQ a <* ghcCmd FailIfErrs) sRef
|
| 194 | - remoteTHCall (qsPipe s) (EndRecover (isLeft e))
|
|
| 194 | + remoteTHCall (qsPipe s) (qsInboxId s) (EndRecover (isLeft e))
|
|
| 195 | 195 | case e of
|
| 196 | 196 | Left GHCiQException{} ->
|
| 197 | 197 | runGHCiQ (runQinGHCiQ h) sRef
|
| ... | ... | @@ -233,26 +233,28 @@ metaHandlersGHCiQ runInIO = TH.MetaHandlers { |
| 233 | 233 | -- a new IORef QState, and return a RemoteRef to it.
|
| 234 | 234 | startTH :: IO (RemoteRef (IORef QState))
|
| 235 | 235 | startTH = do
|
| 236 | - r <- newIORef (initQState (error "startTH: no pipe"))
|
|
| 236 | + r <- newIORef (initQState (error "startTH: no pipe") (error "startTH: no inbox id"))
|
|
| 237 | 237 | mkRemoteRef r
|
| 238 | 238 | |
| 239 | 239 | -- | Runs the mod finalizers.
|
| 240 | 240 | --
|
| 241 | 241 | -- The references must be created on the caller process.
|
| 242 | -runModFinalizerRefs :: Pipe -> RemoteRef (IORef QState)
|
|
| 242 | +runModFinalizerRefs :: Pipe -> InboxId
|
|
| 243 | + -> RemoteRef (IORef QState)
|
|
| 243 | 244 | -> [RemoteRef (TH.Q ())]
|
| 244 | 245 | -> IO ()
|
| 245 | -runModFinalizerRefs pipe rstate qrefs = do
|
|
| 246 | +runModFinalizerRefs pipe ii rstate qrefs = do
|
|
| 246 | 247 | qs <- mapM localRef qrefs
|
| 247 | 248 | qstateref <- localRef rstate
|
| 248 | 249 | qstate <- readIORef qstateref
|
| 249 | - qstate' <- newIORef $ qstate { qsPipe = pipe }
|
|
| 250 | + qstate' <- newIORef $ qstate { qsInboxId = ii, qsPipe = pipe }
|
|
| 250 | 251 | _ <- runGHCiQ (runQinGHCiQ $ sequence_ qs) qstate'
|
| 251 | 252 | return ()
|
| 252 | 253 | |
| 253 | 254 | -- | The implementation of the 'RunTH' message
|
| 254 | 255 | runTH
|
| 255 | 256 | :: Pipe
|
| 257 | + -> InboxId
|
|
| 256 | 258 | -> RemoteRef (IORef QState)
|
| 257 | 259 | -- ^ The TH state, created by 'startTH'
|
| 258 | 260 | -> HValueRef
|
| ... | ... | @@ -264,13 +266,13 @@ runTH |
| 264 | 266 | -> IO ByteString
|
| 265 | 267 | -- ^ Returns an (encoded) result that depends on the THResultType
|
| 266 | 268 | |
| 267 | -runTH pipe rstate rhv ty mb_loc = do
|
|
| 269 | +runTH pipe ii rstate rhv ty mb_loc = do
|
|
| 268 | 270 | hv <- localRef rhv
|
| 269 | 271 | case ty of
|
| 270 | - THExp -> runTHQ pipe rstate mb_loc (unsafeCoerce hv :: TH.Q TH.Exp)
|
|
| 271 | - THPat -> runTHQ pipe rstate mb_loc (unsafeCoerce hv :: TH.Q TH.Pat)
|
|
| 272 | - THType -> runTHQ pipe rstate mb_loc (unsafeCoerce hv :: TH.Q TH.Type)
|
|
| 273 | - THDec -> runTHQ pipe rstate mb_loc (unsafeCoerce hv :: TH.Q [TH.Dec])
|
|
| 272 | + THExp -> runTHQ pipe ii rstate mb_loc (unsafeCoerce hv :: TH.Q TH.Exp)
|
|
| 273 | + THPat -> runTHQ pipe ii rstate mb_loc (unsafeCoerce hv :: TH.Q TH.Pat)
|
|
| 274 | + THType -> runTHQ pipe ii rstate mb_loc (unsafeCoerce hv :: TH.Q TH.Type)
|
|
| 275 | + THDec -> runTHQ pipe ii rstate mb_loc (unsafeCoerce hv :: TH.Q [TH.Dec])
|
|
| 274 | 276 | THAnnWrapper -> do
|
| 275 | 277 | hv <- unsafeCoerce <$> localRef rhv
|
| 276 | 278 | case hv :: AnnotationWrapper of
|
| ... | ... | @@ -279,10 +281,10 @@ runTH pipe rstate rhv ty mb_loc = do |
| 279 | 281 | |
| 280 | 282 | -- | Run a Q computation.
|
| 281 | 283 | runTHQ
|
| 282 | - :: Binary a => Pipe -> RemoteRef (IORef QState) -> Maybe TH.Loc -> TH.Q a
|
|
| 284 | + :: Binary a => Pipe -> InboxId -> RemoteRef (IORef QState) -> Maybe TH.Loc -> TH.Q a
|
|
| 283 | 285 | -> IO ByteString
|
| 284 | -runTHQ pipe rstate mb_loc ghciq = do
|
|
| 286 | +runTHQ pipe ii rstate mb_loc ghciq = do
|
|
| 285 | 287 | qstateref <- localRef rstate
|
| 286 | - modifyIORef' qstateref (\qstate -> qstate { qsLocation = mb_loc, qsPipe = pipe })
|
|
| 288 | + modifyIORef' qstateref (\qstate -> qstate { qsLocation = mb_loc, qsInboxId = ii, qsPipe = pipe })
|
|
| 287 | 289 | r <- runGHCiQ (runQinGHCiQ ghciq) qstateref
|
| 288 | 290 | return $! LB.toStrict (runPut (put r)) |
| ... | ... | @@ -76,6 +76,7 @@ library |
| 76 | 76 | exposed-modules:
|
| 77 | 77 | GHCi.BreakArray
|
| 78 | 78 | GHCi.Message
|
| 79 | + GHCi.Message.Async
|
|
| 79 | 80 | GHCi.ResolvedBCO
|
| 80 | 81 | GHCi.RemoteTypes
|
| 81 | 82 | GHCi.FFI
|
| ... | ... | @@ -95,7 +96,8 @@ library |
| 95 | 96 | filepath >= 1.4 && < 1.6,
|
| 96 | 97 | ghc-boot == @ProjectVersionMunged@,
|
| 97 | 98 | ghc-heap >= 9.10.1 && <=@ProjectVersionMunged@,
|
| 98 | - transformers >= 0.5 && < 0.7
|
|
| 99 | + transformers >= 0.5 && < 0.7,
|
|
| 100 | + stm >= 2.4 && < 2.6
|
|
| 99 | 101 | |
| 100 | 102 | if flag(bootstrap)
|
| 101 | 103 | 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,14 @@ 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
|
|
| 174 | + bracket (mkHandles ghcRead ghcWrite >>= \hds -> (hds,) <$> uncurry mkPipeFromHandles hds)
|
|
| 175 | + (\((hFromServer, hToServer), pipe) -> do
|
|
| 176 | + closePipe pipe
|
|
| 176 | 177 | hClose hFromServer
|
| 177 | 178 | hClose hToServer
|
| 178 | 179 | terminateProcess ph
|
| 179 | 180 | void (waitForProcess ph))
|
| 180 | - (\(hFromServer, hToServer) -> action hFromServer hToServer)
|
|
| 181 | + (\(_, pipe) -> action pipe)
|
|
| 181 | 182 | where
|
| 182 | 183 | mkHandles r w = do
|
| 183 | 184 | hR <- fdToHandle r
|