nvim-hs-0.0.1: library/Neovim/RPC/SocketReader.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{- |
Module : Neovim.RPC.SocketReader
Description : The component which reads RPC messages from the neovim instance
Copyright : (c) Sebastian Witte
License : Apache-2.0
Maintainer : woozletoff@gmail.com
Stability : experimental
-}
module Neovim.RPC.SocketReader (
runSocketReader,
) where
import Neovim.Classes
import Neovim.Context hiding (ask, asks)
import Neovim.Plugin.IPC
import Neovim.Plugin.IPC.Internal
import Neovim.RPC.Common
import Neovim.RPC.FunctionCall
import Control.Applicative
import Control.Concurrent (forkIO)
import Control.Concurrent.STM
import Control.Monad (void)
import Control.Monad.Reader (MonadReader, ask, asks)
import Control.Monad.Trans.Resource
import Data.Conduit as C
import Data.Conduit.Binary
import Data.Conduit.Cereal
import Data.Foldable (forM_)
import qualified Data.Map as Map
import Data.MessagePack
import Data.Monoid
import qualified Data.Serialize (get)
import Data.Text (Text, unpack)
import System.IO (IOMode (ReadMode))
import System.Log.Logger
import Prelude
logger :: String
logger = "Socket Reader"
-- | This function will establish a connection to the given socket and read
-- msgpack-rpc events from it.
runSocketReader :: SocketType -> ConfigWrapper RPCConfig -> IO ()
runSocketReader socketType env = do
h <- createHandle ReadMode socketType
runSocketHandler env $
-- addCleanup (cleanUpHandle h) (sourceHandle h)
-- TODO test whether/how this should be handled
-- this has been commented out because I think that restarting the
-- plugin provider should not cause the stdin and stdout handles to be
-- closed since that would cause neovim to stop the plugin provider (I
-- think).
sourceHandle h
$= conduitGet Data.Serialize.get
$$ messageHandlerSink
-- | Convenient transformer stack for the socket reader.
newtype SocketHandler a =
SocketHandler (ResourceT (Neovim RPCConfig ()) a)
deriving ( Functor, Applicative, Monad , MonadIO
, MonadReader (ConfigWrapper RPCConfig), MonadThrow)
runSocketHandler :: ConfigWrapper RPCConfig -> SocketHandler a -> IO ()
runSocketHandler r (SocketHandler a) =
void $ runNeovim r () (runResourceT a >> quit)
-- | Sink that delegates the messages depending on their type.
-- <https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md>
messageHandlerSink :: Sink Object SocketHandler ()
messageHandlerSink = awaitForever $ \rpc -> do
liftIO . debugM logger $ "Received: " <> show rpc
case rpc of
ObjectArray [ObjectInt msgType, ObjectInt fi, e, result] ->
handleResponseOrRequest msgType fi e result
ObjectArray [ObjectInt msgType, method, params] ->
handleNotification msgType method params
obj -> liftIO . errorM logger $
"Unhandled rpc message: " <> show obj
handleResponseOrRequest :: Int64 -> Int64 -> Object -> Object
-> Sink a SocketHandler ()
handleResponseOrRequest msgType i
| msgType == 1 = handleResponse i
| msgType == 0 = handleRequestOrNotification (Just i)
| otherwise = \_ _ -> do
liftIO . errorM logger $ "Invalid message type: " <> show msgType
return ()
handleResponse :: Int64 -> Object -> Object -> Sink a SocketHandler ()
handleResponse i e result = do
answerMap <- asks (recipients . customConfig)
mReply <- Map.lookup i <$> liftIO (readTVarIO answerMap)
case mReply of
Nothing -> liftIO $ warningM logger
"Received response but could not find a matching recipient."
Just (_,reply) -> do
atomically' . modifyTVar' answerMap $ Map.delete i
atomically' . putTMVar reply $ case e of
ObjectNil -> Right result
_ -> Left e
-- | Act upon the received request or notification. The main difference between
-- the two is that a notification does not generate a reply. The distinction
-- between those two cases is done via the first paramater which is 'Maybe' the
-- function call identifier.
handleRequestOrNotification :: Maybe Int64 -> Object -> Object -> Sink a SocketHandler ()
handleRequestOrNotification mi method (ObjectArray params) = case fromObject method of
Left e -> liftIO . errorM logger $ show e
-- Fork everything so that we do not block.
--
-- XXX If the functions never return, we may end up with a lot of idle
-- threads. Maybe we should gather the ThreadIds and kill them after some
-- amount of time if they are still around. Maybe resourcet provides such a
-- facility for threads already.
Right m -> void . liftIO . forkIO . handle m =<< ask
where
lookupFunction :: Text -> RPCConfig -> STM (Maybe FunctionType)
lookupFunction m rpc = Map.lookup m <$> readTMVar (functions rpc)
handle m rpc = atomically (lookupFunction m (customConfig rpc)) >>= \case
Nothing -> do
let errM = "No provider for: " <> unpack m
debugM logger errM
forM_ mi $ \i -> atomically' . writeTQueue (_eventQueue rpc)
. SomeMessage $ Response i (toObject errM) ObjectNil
Just (Stateless f) -> do
liftIO . debugM logger $ "Executing stateless function with ID: " <> show mi
-- Stateless function: Create a boring state object for the
-- Neovim context.
-- drop the state of the result with (fmap fst <$>)
res <- fmap fst <$> runNeovim (rpc { customConfig = () }) () (f $ parseParams params)
-- Send the result to the event handler
forM_ mi $ \i -> atomically' . writeTQueue (_eventQueue rpc)
. SomeMessage . uncurry (Response i) $ responseResult res
Just (Stateful c) -> do
now <- liftIO getCurrentTime
reply <- liftIO newEmptyTMVarIO
let q = (recipients . customConfig) rpc
liftIO . debugM logger $ "Executing stateful function with ID: " <> show mi
case mi of
Just i -> do
atomically' . modifyTVar q $ Map.insert i (now, reply)
atomically' . writeTQueue c . SomeMessage $ Request m i (parseParams params)
Nothing ->
atomically' . writeTQueue c . SomeMessage $ Notification m (parseParams params)
responseResult (Left !e) = (toObject e, ObjectNil)
responseResult (Right !res) = (ObjectNil, toObject res)
handleRequestOrNotification _ _ params = liftIO . errorM logger $
"Parmaeters in request are not in an object array: " <> show params
-- TODO implement proper handling for additional parameters
parseParams :: [Object] -> [Object]
parseParams = \case
-- Defining a function on the remote host creates a function that, that
-- passes all arguments in a list. At the time of this writing, no other
-- arguments are passed for such a function.
--
-- The function generating the function on neovim side is called:
-- @remote#define#FunctionOnHost@
[ObjectArray fArgs] -> fArgs
args -> args
handleNotification :: Int64 -> Object -> Object -> Sink a SocketHandler ()
handleNotification 2 method params = do
liftIO . debugM logger $ "Received notification: " <> show method <> show params
handleRequestOrNotification Nothing method params
handleNotification msgType fn params = liftIO . errorM logger $
"Message is not a noticiation. Received msgType: " <> show msgType
<> " The expected value was 2. The following arguments were given: "
<> show fn <> " and " <> show params