| ... |
... |
@@ -27,7 +27,7 @@ module GHCi.Message |
|
27
|
27
|
, Pipe(..), mkPipeFromHandles, mkPipeFromContinuations, closePipe
|
|
28
|
28
|
, remoteCall, remoteTHCall
|
|
29
|
29
|
, handleRemoteCall, handleRemoteTHCall
|
|
30
|
|
- , RemoteProcessException(..)
|
|
|
30
|
+ , RemoteProcessException(..), PipeClosedException(..)
|
|
31
|
31
|
|
|
32
|
32
|
, BreakModule
|
|
33
|
33
|
, BreakUnitId
|
| ... |
... |
@@ -89,7 +89,6 @@ import qualified GHC.Boot.TH.Syntax as TH |
|
89
|
89
|
import qualified GHC.Boot.TH.Monad as TH
|
|
90
|
90
|
import System.Exit
|
|
91
|
91
|
import System.IO
|
|
92
|
|
-import System.IO.Error
|
|
93
|
92
|
import Control.Monad
|
|
94
|
93
|
import Control.Concurrent.STM
|
|
95
|
94
|
import Data.Bifunctor
|
| ... |
... |
@@ -722,38 +721,54 @@ data Pipe = Pipe |
|
722
|
721
|
, pipeLeftovers :: !(IORef (Maybe ByteString))
|
|
723
|
722
|
, pipeLock :: !(MVar ()) -- ^ Lock to prevent concurrent writes to the stream
|
|
724
|
723
|
, pipeAsyncMngr :: !AsyncManager
|
|
725
|
|
- , pipeCleanupRW :: !(IO ())
|
|
726
|
|
- -- ^ Clean up the read/write resources, e.g. closes the handles if 'mkPipeFromHandles'
|
|
|
724
|
+ , pipeIsClosed :: !(TVar Bool)
|
|
|
725
|
+ -- ^ After the pipe is closed, requests can no longer be received nor sent
|
|
727
|
726
|
}
|
|
728
|
727
|
|
|
729
|
728
|
-- | Make a 'Pipe' from a 'Handle' to read and a 'Handle' to write.
|
|
730
|
|
--- Must call 'closePipe' at the end to clean up and close the handles.
|
|
|
729
|
+-- Must call 'closePipe' at the end to clean up.
|
|
731
|
730
|
mkPipeFromHandles :: Handle -> Handle -> IO Pipe
|
|
732
|
731
|
mkPipeFromHandles pipeRead pipeWrite = do
|
|
733
|
732
|
let getSome = B.hGetSome pipeRead (32*1024)
|
|
734
|
733
|
putAll b = do
|
|
735
|
734
|
B.hPutBuilder pipeWrite b
|
|
736
|
735
|
hFlush pipeWrite
|
|
737
|
|
- mkPipeFromContinuations getSome putAll (hClose pipeRead >> hClose pipeWrite)
|
|
|
736
|
+ mkPipeFromContinuations getSome putAll
|
|
738
|
737
|
|
|
739
|
|
--- | Make a 'Pipe' from a reader function and a writer function, plus an action
|
|
740
|
|
--- to clean up the reader and writer resources. The pipe should be closed with
|
|
741
|
|
--- 'closePipe', which will call this clean up action too.
|
|
742
|
|
-mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ()) -> IO () -> IO Pipe
|
|
743
|
|
-mkPipeFromContinuations getSome putAll pipeCleanupRW = mdo
|
|
|
738
|
+-- | Make a 'Pipe' from a reader function and a writer function.
|
|
|
739
|
+-- The pipe should be closed with 'closePipe'.
|
|
|
740
|
+mkPipeFromContinuations :: IO ByteString -> (B.Builder -> IO ()) -> IO Pipe
|
|
|
741
|
+mkPipeFromContinuations getSome putAll = mdo
|
|
744
|
742
|
pipeLeftovers <- newIORef Nothing
|
|
745
|
743
|
pipeLock <- newMVar ()
|
|
|
744
|
+ pipeIsClosed <- newTVarIO False
|
|
746
|
745
|
mdo
|
|
747
|
746
|
pipeAsyncMngr <- newAsyncManager p
|
|
748
|
747
|
let p = Pipe { .. }
|
|
749
|
748
|
pure p
|
|
750
|
749
|
|
|
751
|
|
--- | Cleanly close this pipe, terminating the async manager's drain-read-end thread.
|
|
752
|
|
--- This doesn't close the handles
|
|
|
750
|
+-- | Cleanly close our end of the Pipe, terminating the async manager's
|
|
|
751
|
+-- drain-read-end thread. Calling 'closePipe' on an already closed pipe is a no-op.
|
|
|
752
|
+--
|
|
|
753
|
+-- If the pipe was created using 'mkPipeFromHandles', the handles must only by
|
|
|
754
|
+-- closed after calling 'closePipe'.
|
|
|
755
|
+--
|
|
|
756
|
+-- The requests we've sent that are pending are all canceled with
|
|
|
757
|
+-- 'PipeClosedException' by ourselves.
|
|
|
758
|
+--
|
|
|
759
|
+-- The remote process does not necessarily detect that this end of the pipe was
|
|
|
760
|
+-- closed, but it must handle cleanly and clean up after exceptions caused from
|
|
|
761
|
+-- it such as a handle being closed.
|
|
753
|
762
|
closePipe :: Pipe -> IO ()
|
|
754
|
763
|
closePipe Pipe{..} = do
|
|
755
|
|
- killThread (asyncDrainTid pipeAsyncMngr) -- releases handle lock taken by reading
|
|
756
|
|
- pipeCleanupRW -- hClose won't deadlock now that the drainThread is killed
|
|
|
764
|
+
|
|
|
765
|
+ isAlreadyClosed <- atomically $ swapTVar pipeIsClosed True
|
|
|
766
|
+
|
|
|
767
|
+ unless isAlreadyClosed $ do
|
|
|
768
|
+
|
|
|
769
|
+ -- releases handle lock taken by reading, and cleans up by replying to every
|
|
|
770
|
+ -- pending request with 'PipeClosedException'.
|
|
|
771
|
+ throwTo (asyncDrainTid pipeAsyncMngr) PipeClosedException
|
|
757
|
772
|
|
|
758
|
773
|
-- | Send a 'Message' request to and wait for a reply from the interpreter
|
|
759
|
774
|
-- (blocks on the calling side, doesn't necessarily block the interpreter)
|
| ... |
... |
@@ -798,20 +813,28 @@ remoteAsyncReq pipe msg = do |
|
798
|
813
|
|
|
799
|
814
|
-- Mark pending request
|
|
800
|
815
|
uq <- freshReqId (pipeAsyncMngr pipe)
|
|
801
|
|
- wv <- newEmptyMVar
|
|
802
|
|
- atomicModifyIORef' pendingReplies $
|
|
803
|
|
- \m -> (IntMap.insert uq wv m, ())
|
|
|
816
|
+ wv <- newEmptyTMVarIO
|
|
|
817
|
+
|
|
|
818
|
+ isClosed <- atomically $ do
|
|
|
819
|
+ isClosed <- readTVar (pipeIsClosed pipe)
|
|
|
820
|
+ if isClosed
|
|
|
821
|
+ then pure True
|
|
|
822
|
+ else do
|
|
|
823
|
+ modifyTVar pendingReplies (IntMap.insert uq wv)
|
|
|
824
|
+ pure False
|
|
|
825
|
+
|
|
|
826
|
+ when isClosed $ do
|
|
|
827
|
+ throwIO PipeClosedException
|
|
804
|
828
|
|
|
805
|
829
|
-- Write request
|
|
806
|
830
|
writePipe pipe (Request uq (runPut msg))
|
|
807
|
831
|
`onException`
|
|
808
|
832
|
-- the req wasn't done, or no one will be reading the reply:
|
|
809
|
833
|
-- in either case, drop it from the pending map.
|
|
810
|
|
- atomicModifyIORef' pendingReplies
|
|
811
|
|
- (\m -> (IntMap.delete uq m, ()))
|
|
|
834
|
+ atomically (modifyTVar pendingReplies (IntMap.delete uq))
|
|
812
|
835
|
|
|
813
|
836
|
-- Block waiting for reply
|
|
814
|
|
- takeMVar wv
|
|
|
837
|
+ atomically (takeTMVar wv)
|
|
815
|
838
|
|
|
816
|
839
|
-- | Receive a single asynchronous request and send a matching reply
|
|
817
|
840
|
-- asynchronously (the handler may fork a computation that eventually replies
|
| ... |
... |
@@ -826,32 +849,40 @@ handleAsyncReq pipe getit k = do |
|
826
|
849
|
ack <- newEmptyMVar
|
|
827
|
850
|
|
|
828
|
851
|
-- Block waiting for request
|
|
829
|
|
- (uq, bs) <- atomically (readTQueue (requestsQueue (pipeAsyncMngr pipe)))
|
|
830
|
|
-
|
|
831
|
|
- let reply p = do
|
|
832
|
|
- writePipe pipe (Reply uq (bimap toSerializableException runPut p))
|
|
833
|
|
- putMVar ack ()
|
|
834
|
|
-
|
|
835
|
|
- -- Run the continuation which must reply exactly once with 'writeReply'
|
|
836
|
|
- case runGetOrFail getit bs of
|
|
837
|
|
- Left (_, _, err) -> do
|
|
838
|
|
- let exc = DecodeAsyncReqException err
|
|
839
|
|
- reply (Left (toException exc))
|
|
840
|
|
- throwIO exc
|
|
841
|
|
- Right (_, _, it) ->
|
|
842
|
|
- k (WriteReply reply) it
|
|
843
|
|
-
|
|
844
|
|
- -- If there's an exception before the reply is sent, reply with an error instead.
|
|
845
|
|
- `catch` \(e :: SomeException) -> do
|
|
846
|
|
-
|
|
847
|
|
- tryTakeMVar ack >>= \case
|
|
848
|
|
- Just () ->
|
|
849
|
|
- -- Already replied
|
|
850
|
|
- throwIO e
|
|
851
|
|
- Nothing -> do
|
|
852
|
|
- -- Reply with an error
|
|
853
|
|
- reply (Left e)
|
|
854
|
|
- throwIO e
|
|
|
852
|
+ mbreq <- atomically $ do
|
|
|
853
|
+ isClosed <- readTVar (pipeIsClosed pipe)
|
|
|
854
|
+ isEmpty <- isEmptyTQueue (requestsQueue (pipeAsyncMngr pipe))
|
|
|
855
|
+ if isClosed && isEmpty
|
|
|
856
|
+ then return Nothing
|
|
|
857
|
+ else Just <$> readTQueue (requestsQueue (pipeAsyncMngr pipe))
|
|
|
858
|
+
|
|
|
859
|
+ case mbreq of
|
|
|
860
|
+ Nothing -> throwIO PipeClosedException
|
|
|
861
|
+ Just (uq, bs) -> do
|
|
|
862
|
+ let reply p = do
|
|
|
863
|
+ writePipe pipe (Reply uq (bimap toSerializableException runPut p))
|
|
|
864
|
+ putMVar ack ()
|
|
|
865
|
+
|
|
|
866
|
+ -- Run the continuation which must reply exactly once with 'writeReply'
|
|
|
867
|
+ case runGetOrFail getit bs of
|
|
|
868
|
+ Left (_, _, err) -> do
|
|
|
869
|
+ let exc = DecodeAsyncReqException err
|
|
|
870
|
+ reply (Left (toException exc))
|
|
|
871
|
+ throwIO exc
|
|
|
872
|
+ Right (_, _, it) ->
|
|
|
873
|
+ k (WriteReply reply) it
|
|
|
874
|
+
|
|
|
875
|
+ -- If there's an exception before the reply is sent, reply with an error instead.
|
|
|
876
|
+ `catch` \(e :: SomeException) -> do
|
|
|
877
|
+
|
|
|
878
|
+ tryTakeMVar ack >>= \case
|
|
|
879
|
+ Just () ->
|
|
|
880
|
+ -- Already replied
|
|
|
881
|
+ throwIO e
|
|
|
882
|
+ Nothing -> do
|
|
|
883
|
+ -- Reply with an error
|
|
|
884
|
+ reply (Left e)
|
|
|
885
|
+ throwIO e
|
|
855
|
886
|
|
|
856
|
887
|
-- | An exception that can be thrown on the host process representing an
|
|
857
|
888
|
-- expection that happened on the remote one.
|
| ... |
... |
@@ -862,12 +893,16 @@ instance Exception RemoteProcessException |
|
862
|
893
|
newtype DecodeAsyncReqException = DecodeAsyncReqException String deriving Show
|
|
863
|
894
|
instance Exception DecodeAsyncReqException
|
|
864
|
895
|
|
|
|
896
|
+-- | We've closed the pipe on our end and we terminate all pending requests with this.
|
|
|
897
|
+data PipeClosedException = PipeClosedException deriving Show
|
|
|
898
|
+instance Exception PipeClosedException
|
|
|
899
|
+
|
|
865
|
900
|
-- ** Async matching "internals" -----------------------------------------------
|
|
866
|
901
|
|
|
867
|
902
|
data AsyncManager = AsyncManager
|
|
868
|
|
- { pendingReply :: !(IORef (IntMap (MVar (Either SomeException LB.ByteString))))
|
|
|
903
|
+ { pendingReply :: !(TVar (IntMap (TMVar (Either SomeException LB.ByteString))))
|
|
869
|
904
|
-- ^ When a request is sent, keep track that we're expecting a reply. The
|
|
870
|
|
- -- MVar is signaled when the reply arrives. The exception can originate
|
|
|
905
|
+ -- TMVar is signaled when the reply arrives. The exception can originate
|
|
871
|
906
|
-- from the host process, or from the remote one (with 'RemoteProcessException').
|
|
872
|
907
|
, requestsQueue :: !(TQueue (CorrelationId, LB.ByteString))
|
|
873
|
908
|
-- ^ A queue of pending received requests. When a request is received,
|
| ... |
... |
@@ -904,45 +939,58 @@ instance Binary IddMsg where |
|
904
|
939
|
newAsyncManager :: Pipe -> IO AsyncManager
|
|
905
|
940
|
newAsyncManager pipe = do
|
|
906
|
941
|
asyncNextReq <- newIORef 0
|
|
907
|
|
- pendingReply <- newIORef IntMap.empty
|
|
|
942
|
+ pendingReply <- newTVarIO IntMap.empty
|
|
908
|
943
|
requestsQueue <- newTQueueIO
|
|
909
|
944
|
|
|
910
|
|
- mdo
|
|
911
|
|
- -- Fork thread to drain read end.
|
|
912
|
|
- -- (cleaned up on 'closePipe')
|
|
913
|
|
- asyncDrainTid <- forkIO (drainPipe pipe am)
|
|
|
945
|
+ -- Fork thread to drain read end.
|
|
|
946
|
+ -- (cleaned up on 'closePipe')
|
|
|
947
|
+ asyncDrainTid <- forkIO (drainPipe pipe)
|
|
914
|
948
|
|
|
915
|
|
- let am = AsyncManager{..}
|
|
916
|
|
- pure am
|
|
|
949
|
+ let am = AsyncManager{..}
|
|
|
950
|
+ pure am
|
|
917
|
951
|
|
|
918
|
952
|
-- | Drains the read end of the Pipe and match 'Reply'es against pending 'Request's.
|
|
919
|
953
|
--
|
|
920
|
954
|
-- In pratice, we'll have both a thread draining the read end of the host's
|
|
921
|
955
|
-- 'Pipe' and a thread draining the read end of the interpreter's 'Pipe', since
|
|
922
|
|
--- both processes construct a 'Pipe' and send and receive messages:
|
|
923
|
|
-drainPipe :: Pipe -> AsyncManager -> IO ()
|
|
924
|
|
-drainPipe p am = flip catch cancelPending $ forever $ do
|
|
925
|
|
- readPipe p >>= \case
|
|
926
|
|
- -- Receive a reply to a pending notification
|
|
927
|
|
- Reply uq bs -> do
|
|
928
|
|
- mwv <- atomicModifyIORef' (pendingReply am) $ \m -> do
|
|
929
|
|
- case IntMap.lookup uq m of
|
|
930
|
|
- Nothing -> (m, Nothing)
|
|
931
|
|
- Just wv -> (IntMap.delete uq m, Just wv)
|
|
932
|
|
- case mwv of
|
|
933
|
|
- Just wv ->
|
|
934
|
|
- putMVar wv (bimap (toException . RemoteProcessException) id bs) -- signal matching request
|
|
935
|
|
- Nothing ->
|
|
936
|
|
- -- the req was dropped from pending bc there was an exception, and no
|
|
937
|
|
- -- one's listening any more. the reply is ignored
|
|
938
|
|
- pure ()
|
|
939
|
|
- -- Receive a request and queue it
|
|
940
|
|
- Request uq bs ->
|
|
941
|
|
- atomically $ writeTQueue (requestsQueue am) (uq,bs)
|
|
942
|
|
- where
|
|
943
|
|
- cancelPending (e::SomeException) = do
|
|
944
|
|
- pendings <- readIORef (pendingReply am)
|
|
945
|
|
- mapM_ (\p -> putMVar p (Left e {-host exception-})) pendings
|
|
|
956
|
+-- both processes construct a 'Pipe' and send and receive messages
|
|
|
957
|
+drainPipe :: Pipe -> IO ()
|
|
|
958
|
+drainPipe p = catch loop replyToPendings where
|
|
|
959
|
+ loop = do
|
|
|
960
|
+ readPipe p >>= \case
|
|
|
961
|
+ -- Receive a reply to a pending notification
|
|
|
962
|
+ Just (Reply uq bs) -> do
|
|
|
963
|
+ mwv <- atomically $ do
|
|
|
964
|
+ m <- readTVar (pendingReply am)
|
|
|
965
|
+ case IntMap.lookup uq m of
|
|
|
966
|
+ Nothing -> pure Nothing
|
|
|
967
|
+ Just wv -> do
|
|
|
968
|
+ writeTVar (pendingReply am) (IntMap.delete uq m)
|
|
|
969
|
+ pure (Just wv)
|
|
|
970
|
+ case mwv of
|
|
|
971
|
+ Just wv -> atomically $
|
|
|
972
|
+ putTMVar wv (bimap (toException . RemoteProcessException) id bs) -- signal matching request
|
|
|
973
|
+ Nothing ->
|
|
|
974
|
+ -- the req was dropped from pending bc there was an exception, and no
|
|
|
975
|
+ -- one's listening any more. the reply is ignored
|
|
|
976
|
+ pure ()
|
|
|
977
|
+ loop
|
|
|
978
|
+ -- Receive a request and queue it
|
|
|
979
|
+ Just (Request uq bs) -> do
|
|
|
980
|
+ atomically $ writeTQueue (requestsQueue am) (uq,bs)
|
|
|
981
|
+ loop
|
|
|
982
|
+ -- EOF on read handle
|
|
|
983
|
+ Nothing -> do
|
|
|
984
|
+ -- clean up pipe then exit loop
|
|
|
985
|
+ replyToPendings (toException PipeClosedException)
|
|
|
986
|
+ pure ()
|
|
|
987
|
+
|
|
|
988
|
+ replyToPendings e = atomically $ do
|
|
|
989
|
+ writeTVar (pipeIsClosed p) True
|
|
|
990
|
+ pendings <- swapTVar (pendingReply am) IntMap.empty -- take pending
|
|
|
991
|
+ mapM_ (\p -> putTMVar p (Left e {-host exception-})) pendings
|
|
|
992
|
+
|
|
|
993
|
+ am = pipeAsyncMngr p
|
|
946
|
994
|
|
|
947
|
995
|
freshReqId :: AsyncManager -> IO CorrelationId
|
|
948
|
996
|
freshReqId AsyncManager{asyncNextReq} =
|
| ... |
... |
@@ -960,16 +1008,17 @@ writePipe p@Pipe{..} = withLock p . putAll . execPut . put |
|
960
|
1008
|
-- | Only the 'drainPipe' thread should read from the pipe directly. Other
|
|
961
|
1009
|
-- functions should wait for the matching reply to be delivered asynchronously.
|
|
962
|
1010
|
-- (see 'withAsyncReq'). It doesn't lock to read bc it assumes single reader is 'drainPipe'.
|
|
963
|
|
-readPipe :: Pipe -> IO IddMsg
|
|
|
1011
|
+--
|
|
|
1012
|
+-- Returns @Nothing@ on EOF.
|
|
|
1013
|
+readPipe :: Pipe -> IO (Maybe IddMsg)
|
|
964
|
1014
|
readPipe Pipe{..} = do
|
|
965
|
1015
|
leftovers <- readIORef pipeLeftovers
|
|
966
|
1016
|
m <- getBin getSome get leftovers
|
|
967
|
1017
|
case m of
|
|
968
|
|
- Nothing -> throw $
|
|
969
|
|
- mkIOError eofErrorType "GHCi.Message.readPipe" Nothing Nothing
|
|
|
1018
|
+ Nothing -> return Nothing
|
|
970
|
1019
|
Just (result, new_leftovers) -> do
|
|
971
|
1020
|
writeIORef pipeLeftovers new_leftovers
|
|
972
|
|
- return result
|
|
|
1021
|
+ return (Just result)
|
|
973
|
1022
|
|
|
974
|
1023
|
getBin
|
|
975
|
1024
|
:: (IO ByteString) -> Get a -> Maybe ByteString
|