summaryrefslogtreecommitdiff
path: root/src/Network/KRPC.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Network/KRPC.hs')
-rw-r--r--src/Network/KRPC.hs362
1 files changed, 362 insertions, 0 deletions
diff --git a/src/Network/KRPC.hs b/src/Network/KRPC.hs
new file mode 100644
index 00000000..5c913daa
--- /dev/null
+++ b/src/Network/KRPC.hs
@@ -0,0 +1,362 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module provides safe remote procedure call. One important
9-- point is exceptions and errors, so be able handle them properly
10-- we need to investigate a bit about how this all works.
11-- Internally, in order to make method invokation KRPC makes the
12-- following steps:
13--
14-- * Caller serialize arguments to bencoded bytestrings;
15--
16-- * Caller send bytestring data over UDP to the callee;
17--
18-- * Callee receive and decode arguments to the method and method
19-- name. If it can't decode then it send 'ProtocolError' back to the
20-- caller;
21--
22-- * Callee search for the @method name@ in the method table.
23-- If it not present in the table then callee send 'MethodUnknown'
24-- back to the caller;
25--
26-- * Callee check if argument names match. If not it send
27-- 'ProtocolError' back;
28--
29-- * Callee make the actuall call to the plain old haskell
30-- function. If the function throw exception then callee send
31-- 'ServerError' back.
32--
33-- * Callee serialize result of the function to bencoded bytestring.
34--
35-- * Callee encode result to bencoded bytestring and send it back
36-- to the caller.
37--
38-- * Caller check if return values names match with the signature
39-- it called in the first step.
40--
41-- * Caller extracts results and finally return results of the
42-- procedure call as ordinary haskell values.
43--
44-- If every other error occurred caller get the 'GenericError'. All
45-- errors returned by callee are throwed as ordinary haskell
46-- exceptions at caller side. Make sure that both callee and caller
47-- uses the same method signatures and everything should be ok: this
48-- KRPC implementation provides some level of safety through
49-- types. Also note that both caller and callee use plain UDP, so
50-- KRPC is unreliable.
51--
52-- Consider one tiny example. From now @caller = client@ and
53-- @callee = server or remote@.
54--
55-- Somewhere we have to define all procedure signatures. Imagine
56-- that this is a library shared between client and server:
57--
58-- > factorialMethod :: Method Int Int
59-- > factorialMethod = method "factorial" ["x"] ["y"]
60--
61-- Otherwise you can define this code in both client and server of
62-- course. But in this case you might get into troubles: you can get
63-- 'MethodUnknown' or 'ProtocolError' if name or type of method
64-- will mismatch after not synced changes in client or server code.
65--
66-- Now let's define our client-side:
67--
68-- > main = withRemote $ \remote -> do
69-- > result <- call remote (0, 6000) factorialMethod 4
70-- > assert (result == 24) $ print "Success!"
71--
72-- It basically open socket with 'withRemote' and make all the other
73-- steps in 'call' as describe above. And finally our server-side:
74--
75-- > factorialImpl :: Int -> Int
76-- > factorialImpl n = product [1..n]
77-- >
78-- > main = runServer [factorialMethod $ return . factorialImpl]
79--
80-- Here we implement method signature from that shared lib and run
81-- server with runServer by passing method table in.
82--
83-- For async API use /async/ package, old API have been removed.
84--
85-- For more examples see @exsamples@ or @tests@ directories.
86--
87-- For protocol details see 'Remote.KRPC.Protocol' module.
88--
89{-# LANGUAGE OverloadedStrings #-}
90{-# LANGUAGE ViewPatterns #-}
91{-# LANGUAGE FlexibleContexts #-}
92{-# LANGUAGE DeriveDataTypeable #-}
93{-# LANGUAGE ExplicitForAll #-}
94{-# LANGUAGE KindSignatures #-}
95{-# LANGUAGE ScopedTypeVariables #-}
96{-# LANGUAGE DeriveGeneric #-}
97module Remote.KRPC
98 ( -- * Method
99 Method(..)
100 , method, idM
101
102 -- * Client
103 , RemoteAddr
104 , RPCException(..)
105 , call
106
107 -- * Server
108 , MethodHandler
109 , (==>)
110 , (==>@)
111 , server
112
113 -- * Internal
114 , call_
115 , withRemote
116 ) where
117
118import Control.Applicative
119import Control.Exception
120import Control.Monad.Trans.Control
121import Control.Monad.IO.Class
122import Data.BEncode
123import Data.ByteString.Char8 as BC
124import Data.List as L
125import Data.Map as M
126import Data.Monoid
127import Data.Typeable
128import Network
129import GHC.Generics
130
131import Remote.KRPC.Protocol
132
133
134-- | Method datatype used to describe name, parameters and return
135-- values of procedure. Client use a method to /invoke/, server
136-- /implements/ the method to make the actual work.
137--
138-- We use the following fantom types to ensure type-safiety:
139--
140-- * param: Type of method parameters. Ordinary Tuple type used
141-- to specify more than one parameter, so for example @Method
142-- (Int, Int) result@ will take two arguments.
143--
144-- * result: Type of return value of the method. Similarly,
145-- tuple used to specify more than one return value, so for
146-- exsample @Method (Foo, Bar) (Bar, Foo)@ will take two arguments
147-- and return two values.
148--
149-- To pass raw dictionaries you should specify empty param list:
150--
151-- > method "my_method" [] [] :: Method BEncode BEncode
152--
153-- In this case you should handle dictionary extraction by hand, both
154-- in client and server.
155--
156data Method param result = Method {
157 -- | Name used in query.
158 methodName :: MethodName
159
160 -- | Name of each parameter in /right to left/ order.
161 , methodParams :: [ParamName]
162
163 -- | Name of each return value in /right to left/ order.
164 , methodVals :: [ValName]
165 } deriving (Eq, Ord, Generic)
166
167instance BEncode (Method a b)
168
169instance (Typeable a, Typeable b) => Show (Method a b) where
170 showsPrec _ = showsMethod
171
172showsMethod
173 :: forall a. forall b.
174 Typeable a => Typeable b
175 => Method a b -> ShowS
176showsMethod Method {..} =
177 showString (BC.unpack methodName) <>
178 showString " :: " <>
179 showsTuple methodParams paramsTy <>
180 showString " -> " <>
181 showsTuple methodVals valuesTy
182 where
183 paramsTy = typeOf (error "KRPC.showsMethod: impossible" :: a)
184 valuesTy = typeOf (error "KRPC.showsMethod: impossible" :: b)
185
186 showsTuple ns ty
187 = showChar '('
188 <> mconcat (L.intersperse (showString ", ") $
189 L.zipWith showsTyArgName ns (detuple ty))
190 <> showChar ')'
191
192 showsTyArgName ns ty
193 = showString (BC.unpack ns)
194 <> showString " :: "
195 <> showString (show ty)
196
197 detuple tyRep
198 | L.null args = [tyRep]
199 | otherwise = args
200 where
201 args = typeRepArgs tyRep
202
203
204-- | Identity procedure signature. Could be used for echo
205-- servers. Implemented as:
206--
207-- > idM = method "id" ["x"] ["y"]
208--
209idM :: Method a a
210idM = method "id" ["x"] ["y"]
211{-# INLINE idM #-}
212
213-- | Makes method signature. Note that order of parameters and return
214-- values are not important as long as corresponding names and types
215-- are match. For exsample this is the equal definitions:
216--
217-- > methodA : Method (Foo, Bar) (Baz, Quux)
218-- > methodA = method "mymethod" ["a", "b"] ["c", "d"]
219--
220-- > methodA : Method (Bar, Foo) (Quux, Baz)
221-- > methodB = method "mymethod" ["b", "a"] ["d", "c"]
222--
223method :: MethodName -> [ParamName] -> [ValName] -> Method param result
224method = Method
225{-# INLINE method #-}
226
227lookupKey :: ParamName -> BDict -> Result BValue
228lookupKey x = maybe (Left ("not found key " ++ BC.unpack x)) Right . M.lookup x
229
230extractArgs :: [ParamName] -> BDict -> Result BValue
231extractArgs [] d = Right $ if M.null d then BList [] else BDict d
232extractArgs [x] d = lookupKey x d
233extractArgs xs d = BList <$> mapM (`lookupKey` d) xs
234{-# INLINE extractArgs #-}
235
236injectVals :: [ParamName] -> BValue -> [(ParamName, BValue)]
237injectVals [] (BList []) = []
238injectVals [] (BDict d ) = M.toList d
239injectVals [] be = invalidParamList [] be
240injectVals [p] arg = [(p, arg)]
241injectVals ps (BList as) = L.zip ps as
242injectVals ps be = invalidParamList ps be
243{-# INLINE injectVals #-}
244
245invalidParamList :: [ParamName] -> BValue -> a
246invalidParamList pl be
247 = error $ "KRPC invalid parameter list: " ++ show pl ++ "\n" ++
248 "while procedure args are: " ++ show be
249
250-- | Alias to Socket, through might change in future.
251type Remote = Socket
252
253-- | Represent any error mentioned by protocol specification that
254-- 'call', 'await' might throw.
255-- For more details see 'Remote.KRPC.Protocol'.
256--
257data RPCException = RPCException KError
258 deriving (Show, Eq, Typeable)
259
260instance Exception RPCException
261
262-- | Address of remote can be called by client.
263type RemoteAddr = KRemoteAddr
264
265queryCall :: BEncode param
266 => KRemote -> KRemoteAddr
267 -> Method param result -> param -> IO ()
268queryCall sock addr m arg = sendMessage q addr sock
269 where
270 q = kquery (methodName m) (injectVals (methodParams m) (toBEncode arg))
271
272getResult :: BEncode result
273 => KRemote
274 -> Method param result -> IO result
275getResult sock m = do
276 resp <- recvResponse sock
277 case resp of
278 Left e -> throw (RPCException e)
279 Right (respVals -> dict) -> do
280 case fromBEncode =<< extractArgs (methodVals m) dict of
281 Right vals -> return vals
282 Left e -> throw (RPCException (ProtocolError (BC.pack e)))
283
284
285-- | Makes remote procedure call. Throws RPCException on any error
286-- occurred.
287call :: (MonadBaseControl IO host, MonadIO host)
288 => (BEncode param, BEncode result)
289 => RemoteAddr -- ^ Address of callee.
290 -> Method param result -- ^ Procedure to call.
291 -> param -- ^ Arguments passed by callee to procedure.
292 -> host result -- ^ Values returned by callee from the procedure.
293call addr m arg = liftIO $ withRemote $ \sock -> do call_ sock addr m arg
294
295-- | The same as 'call' but use already opened socket.
296call_ :: (MonadBaseControl IO host, MonadIO host)
297 => (BEncode param, BEncode result)
298 => Remote -- ^ Socket to use
299 -> RemoteAddr -- ^ Address of callee.
300 -> Method param result -- ^ Procedure to call.
301 -> param -- ^ Arguments passed by callee to procedure.
302 -> host result -- ^ Values returned by callee from the procedure.
303call_ sock addr m arg = liftIO $ do
304 queryCall sock addr m arg
305 getResult sock m
306
307
308type HandlerBody remote = KRemoteAddr -> KQuery -> remote (Either KError KResponse)
309
310-- | Procedure signature and implementation binded up.
311type MethodHandler remote = (MethodName, HandlerBody remote)
312
313-- we can safely erase types in (==>)
314-- | Assign method implementation to the method signature.
315(==>) :: forall (remote :: * -> *) (param :: *) (result :: *).
316 (BEncode param, BEncode result)
317 => Monad remote
318 => Method param result -- ^ Signature.
319 -> (param -> remote result) -- ^ Implementation.
320 -> MethodHandler remote -- ^ Handler used by server.
321{-# INLINE (==>) #-}
322m ==> body = m ==>@ const body
323infix 1 ==>
324
325-- | Similar to '==>@' but additionally pass caller address.
326(==>@) :: forall (remote :: * -> *) (param :: *) (result :: *).
327 (BEncode param, BEncode result)
328 => Monad remote
329 => Method param result -- ^ Signature.
330 -> (KRemoteAddr -> param -> remote result) -- ^ Implementation.
331 -> MethodHandler remote -- ^ Handler used by server.
332{-# INLINE (==>@) #-}
333m ==>@ body = (methodName m, newbody)
334 where
335 {-# INLINE newbody #-}
336 newbody addr q =
337 case fromBEncode =<< extractArgs (methodParams m) (queryArgs q) of
338 Left e -> return (Left (ProtocolError (BC.pack e)))
339 Right a -> do
340 r <- body addr a
341 return (Right (kresponse (injectVals (methodVals m) (toBEncode r))))
342
343infix 1 ==>@
344
345-- TODO: allow forkIO
346
347-- | Run RPC server on specified port by using list of handlers.
348-- Server will dispatch procedure specified by callee, but note that
349-- it will not create new thread for each connection.
350--
351server :: (MonadBaseControl IO remote, MonadIO remote)
352 => PortNumber -- ^ Port used to accept incoming connections.
353 -> [MethodHandler remote] -- ^ Method table.
354 -> remote ()
355server servport handlers = do
356 remoteServer servport $ \addr q -> do
357 case dispatch (queryMethod q) of
358 Nothing -> return $ Left $ MethodUnknown (queryMethod q)
359 Just m -> m addr q
360 where
361 handlerMap = M.fromList handlers
362 dispatch s = M.lookup s handlerMap