diff options
Diffstat (limited to 'src/Network/KRPC.hs')
-rw-r--r-- | src/Network/KRPC.hs | 362 |
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 #-} | ||
97 | module 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 | |||
118 | import Control.Applicative | ||
119 | import Control.Exception | ||
120 | import Control.Monad.Trans.Control | ||
121 | import Control.Monad.IO.Class | ||
122 | import Data.BEncode | ||
123 | import Data.ByteString.Char8 as BC | ||
124 | import Data.List as L | ||
125 | import Data.Map as M | ||
126 | import Data.Monoid | ||
127 | import Data.Typeable | ||
128 | import Network | ||
129 | import GHC.Generics | ||
130 | |||
131 | import 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 | -- | ||
156 | data 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 | |||
167 | instance BEncode (Method a b) | ||
168 | |||
169 | instance (Typeable a, Typeable b) => Show (Method a b) where | ||
170 | showsPrec _ = showsMethod | ||
171 | |||
172 | showsMethod | ||
173 | :: forall a. forall b. | ||
174 | Typeable a => Typeable b | ||
175 | => Method a b -> ShowS | ||
176 | showsMethod 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 | -- | ||
209 | idM :: Method a a | ||
210 | idM = 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 | -- | ||
223 | method :: MethodName -> [ParamName] -> [ValName] -> Method param result | ||
224 | method = Method | ||
225 | {-# INLINE method #-} | ||
226 | |||
227 | lookupKey :: ParamName -> BDict -> Result BValue | ||
228 | lookupKey x = maybe (Left ("not found key " ++ BC.unpack x)) Right . M.lookup x | ||
229 | |||
230 | extractArgs :: [ParamName] -> BDict -> Result BValue | ||
231 | extractArgs [] d = Right $ if M.null d then BList [] else BDict d | ||
232 | extractArgs [x] d = lookupKey x d | ||
233 | extractArgs xs d = BList <$> mapM (`lookupKey` d) xs | ||
234 | {-# INLINE extractArgs #-} | ||
235 | |||
236 | injectVals :: [ParamName] -> BValue -> [(ParamName, BValue)] | ||
237 | injectVals [] (BList []) = [] | ||
238 | injectVals [] (BDict d ) = M.toList d | ||
239 | injectVals [] be = invalidParamList [] be | ||
240 | injectVals [p] arg = [(p, arg)] | ||
241 | injectVals ps (BList as) = L.zip ps as | ||
242 | injectVals ps be = invalidParamList ps be | ||
243 | {-# INLINE injectVals #-} | ||
244 | |||
245 | invalidParamList :: [ParamName] -> BValue -> a | ||
246 | invalidParamList 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. | ||
251 | type 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 | -- | ||
257 | data RPCException = RPCException KError | ||
258 | deriving (Show, Eq, Typeable) | ||
259 | |||
260 | instance Exception RPCException | ||
261 | |||
262 | -- | Address of remote can be called by client. | ||
263 | type RemoteAddr = KRemoteAddr | ||
264 | |||
265 | queryCall :: BEncode param | ||
266 | => KRemote -> KRemoteAddr | ||
267 | -> Method param result -> param -> IO () | ||
268 | queryCall sock addr m arg = sendMessage q addr sock | ||
269 | where | ||
270 | q = kquery (methodName m) (injectVals (methodParams m) (toBEncode arg)) | ||
271 | |||
272 | getResult :: BEncode result | ||
273 | => KRemote | ||
274 | -> Method param result -> IO result | ||
275 | getResult 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. | ||
287 | call :: (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. | ||
293 | call addr m arg = liftIO $ withRemote $ \sock -> do call_ sock addr m arg | ||
294 | |||
295 | -- | The same as 'call' but use already opened socket. | ||
296 | call_ :: (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. | ||
303 | call_ sock addr m arg = liftIO $ do | ||
304 | queryCall sock addr m arg | ||
305 | getResult sock m | ||
306 | |||
307 | |||
308 | type HandlerBody remote = KRemoteAddr -> KQuery -> remote (Either KError KResponse) | ||
309 | |||
310 | -- | Procedure signature and implementation binded up. | ||
311 | type 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 (==>) #-} | ||
322 | m ==> body = m ==>@ const body | ||
323 | infix 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 (==>@) #-} | ||
333 | m ==>@ 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 | |||
343 | infix 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 | -- | ||
351 | server :: (MonadBaseControl IO remote, MonadIO remote) | ||
352 | => PortNumber -- ^ Port used to accept incoming connections. | ||
353 | -> [MethodHandler remote] -- ^ Method table. | ||
354 | -> remote () | ||
355 | server 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 | ||