clplug-1.0.0.0: src/Plugin/Connect.hs
{-# LANGUAGE
LambdaCase
, OverloadedStrings
, BlockArguments
, RecordWildCards
, DuplicateRecordFields
, DeriveAnyClass
, FlexibleContexts
#-}
module Plugin.Connect (
plugin,
release,
reject,
respond,
notify,
Notify(..),
PluginApp,
PluginMonad,
InitMonad,
Req(..),
Plug(..),
Init(..)
) where
import Plugin.Internal.Conduit
import Plugin.Hooks
import Control.Exception
import Data.Conduit
import Data.Conduit.Combinators (sourceHandle, sinkHandle)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.Aeson
import Data.Text (Text, unpack)
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.State
import Control.Monad.Reader
import Control.Concurrent hiding (yield)
import Network.Socket as N
import System.IO
import Control.Concurrent.STM.TVar
type PluginApp a = Req -> PluginMonad a ()
type InitMonad a = ReaderT Plug IO a
type PluginMonad a = ReaderT Plug (StateT a IO)
-- | Handles to lightning-rpc and plugin, configuration object and global id.
data Plug = Plug {
rpc :: Handle
, out :: Handle
, conf :: Init
, plugId :: TVar Int
}
data StartErr = ExpectManifest | ExpectInit deriving (Show, Exception)
-- | Create executable that can be installed as core lightning plugin.
-- 1st arg is the manifest that configures the interface, 2nd arg is a function
-- that runs on startup and initializes/defines app state, and 3rd arg is a function
-- that is called each time data is received.
plugin :: Value -> InitMonad s -> PluginApp s -> IO ()
plugin manifest start app = do
liftIO $ mapM_ (`hSetBuffering` LineBuffering) [stdin,stdout]
runOnce $ await >>= \case
(Just (Right (Req (Just i) "getmanifest" _))) -> yield $ Res manifest i
_ -> throw ExpectManifest
runOnce $ await >>= \case
(Just (Right (Req (Just i) "init" v))) -> case fromJSON v of
Success xi@(Init{..}) -> do
h <- liftIO $ getrpc $ getRpcPath configuration
tv <- liftIO $ newTVarIO 1
let plug = Plug h stdout xi tv
s' <- liftIO $ runReaderT start plug
_ <- liftIO . forkIO $ runPlugin plug s' app
yield . Res (object ["result" .= ("continue" :: Text)]) $ i
_ -> throw ExpectInit
_ -> throw ExpectInit
threadDelay maxBound
-- | allow node to continue default behaviour
release :: Int -> PluginMonad a ()
release i = do
o <- asks out
liftIO . runRes o $ Res (object ["result" .= ("continue" :: Text)]) i
-- | prevent node default behaviour
reject :: Int -> PluginMonad a ()
reject i = do
o <- asks out
liftIO . runRes o $ Res (object ["result" .= ("reject" :: Text)]) i
-- | Respond with arbitrary Value, custom rpc hooks will pass through
respond :: Value -> Int -> PluginMonad a ()
respond v i = do
o <- asks out
liftIO $ runRes o $ Res v i
-- | Produce a notification that can be subscribed to by other plugins.
notify :: (MonadReader Plug m, MonadIO m) => Notify -> m ()
notify n = do
h <- asks out
liftIO $ runRes h n
runPlugin :: Plug -> s -> PluginApp s -> IO ()
runPlugin re st = (`evalStateT` st) . (`runReaderT` re) . runConduit . runner
where
runner app = sourceHandle stdin .| inConduit .| entry .| appInsert app
appInsert :: PluginApp a -> ConduitT (Either Res Req) Void (PluginMonad a) ()
appInsert app = awaitForever \case
Left failed -> do
out <- asks out
liftIO $ runRes out failed
Right req -> void (lift (app req))
runOnce :: ConduitT (Either Res Req) Res IO () -> IO ()
runOnce = runConduit.runner
where
runner d = sourceHandle stdin .| inConduit .| entry .| d .| exit .| sinkHandle stdout
runRes :: (ToJSON v) => Handle -> v -> IO ()
runRes o r = runConduit $ yield r .| exit .| sinkHandle o
entry :: (Monad n) => ConduitT (ParseResult (Req)) (Either Res Req) n ()
entry = awaitForever (\case
Correct v -> yield . Right $ v -- Req (getReqId v) (getMethod v), getParams v)
InvalidReq -> yield $ Left $ ErrRes ("Request Error"::Text) 0 Nothing 0 --Nothing
ParseErr -> yield $ Left $ ErrRes ("Request Error"::Text) 0 Nothing 0 --Nothing
)
exit :: (Monad n, ToJSON v) => ConduitT v S.ByteString n ()
exit = await >>= maybe mempty (yield. L.toStrict . encode)
data Notify = Notify {
method :: Text
, params :: Value
} deriving (Show)
instance ToJSON Notify where
toJSON (Notify m p) =
object [ "jsonrpc" .= ("2.0" :: Text)
, "method" .= m
, "params" .= p
]
getrpc :: Text -> IO Handle
getrpc d = do
soc <- socket AF_UNIX Stream 0
N.connect soc $ SockAddrUnix $ unpack d
socketToHandle soc ReadWriteMode
getRpcPath :: InitConfig -> Text
getRpcPath conf = lightning5dir conf <> "/" <> rpc5file conf