(module rpc-client mzscheme
(require (planet "fifo.scm" ("oesterholt" "datastructs.plt" 1 0)))
(require (planet "spod.scm" ("oesterholt" "roos.plt" 1 0)))
(require "rpc-marshal.scm")
(require "rpc-io.scm")
(require "rpc-log.scm")
(provide rpc-call
rpc-symbol-call
rpc-connect
rpc-disconnect
rpc-global-connect
rpc-global-disconnect
rpc-global-handle!
rpc-shutdown
rpc-force-shutdown
rpc-connected
rpc-chalenge
rpc-message
rpc-notifications-handler
rpc-valid?
rpc-diagnose
rpc-client-define
rpc-client-documentation
rpc-local-define
rpc-global-handle
)
(define GLOBAL-CLIENT-HANDLE #f)
(define-struct %mzrpc
(valid message from-server to-server hasher notifications-handler reader-fifo reader diagnose))
(define-syntax rpc-error?
(syntax-rules ()
((_ h) (eq? (car h) 'error))))
(define-syntax rpc-error-message
(syntax-rules ()
((_ h) (cadr h))))
(define-syntax rpc-value
(syntax-rules ()
((_ h) (cadr h))))
(define (internal-rpc-call handle f args)
(if (rpc-valid? handle)
(let ((A (map rpc-marshal args)))
(let ((R (write* (cons f A) (%mzrpc-to-server handle) )))
(if (io-error? R)
(begin
(set-%mzrpc-valid! handle #f)
(set-%mzrpc-diagnose! handle 'io-error)
(set-%mzrpc-message! handle "mzrpc: i/o error with host while writing.")
(rpc-log-error "Result of rpc call: io-error; message: " (%mzrpc-message handle))
'%rpc-error)
(begin
(let ((R (fifo-(%mzrpc-reader-fifo handle))))
(if (wrong-input? R)
(begin
(set-%mzrpc-valid! handle #f)
(set-%mzrpc-diagnose! handle 'io-error)
(set-%mzrpc-message! handle "mzrpc: i/o error with host while reading.")
(rpc-log-error "Result of rpc call: wrong-input; message: " (%mzrpc-message handle))
'%rpc-error)
(if (rpc-error? R)
(begin
(set-%mzrpc-diagnose! handle 'call-error)
(set-%mzrpc-message! handle (format "mzrpc: error returned from server: ~a" (rpc-error-message R)))
(rpc-log-error "Result of rpc call: rpc-error; message: " (%mzrpc-message handle))
'%rpc-error)
(rpc-de-marshal (rpc-value R)))))))))
(error "mzrpc: invalid handle.")))
(spod-module-def)
(spod-module-add (s= "rpc-client"))
(define (reader handle)
(let ((R (read* (%mzrpc-from-server handle))))
(if (or (eof-object? R) (eq? R '%rpc-wrong-input%))
(begin
(set-%mzrpc-valid! handle #f)
(set-%mzrpc-diagnose! handle 'io-error)
(set-%mzrpc-message! handle "reader thead: connection with server lost")
#f)
(if (rpc-notify? R)
(let ((h (%mzrpc-notifications-handler handle)))
(if (procedure? h) (h (rpc-de-marshal (cadr R))))
(reader handle))
(begin
(fifo+ (%mzrpc-reader-fifo handle) R)
(reader handle))))))
(spod-module-add (s=== "(rpc-connect host port user pass notification-handler . pass-hasher) : rpc-handle")
(sp "Connects to host <host> on port <port> with account <user> and password <pass>")
(sp "The notification-handler must be #f if no notifications from the server are handled, otherwise "
"the notification handler must be a function of one argument. The server can send notifications "
"to clients. They depend on the server. The client can handle these notifications using the given "
"notification handler.")
(sp "The pass-hasher is a function that is called with a password and a chalenge. The function must "
"be used to hash the password and the chalenge together.")
(sp "This function tries to login right away. If the login fails, " (s% "rpc-valid?") " will return #f.")
(sp "The result of " (s% "rpc-connect") " is a handle. " (s% "rpc-valid?") " returns #t, if the handle "
"is valid, #f otherwise.")
)
(define (rpc-connect host port user pass notification-handler . pass-hasher)
(let ((hasher (if (null? pass-hasher)
(lambda (pass chalenge) pass))))
(let ((handle (with-handlers ((exn:fail:network? (lambda (exn)
(make-%mzrpc #f
(format "Cannot connect to host ~s on port ~s" host port)
#f #f
hasher
notification-handler
(fifo)
#f
'no-connect)) ))
(call-with-values (lambda () (tcp-connect host port))
(lambda (from-server to-server)
(make-%mzrpc #t ""
from-server to-server
hasher
notification-handler
(fifo)
#f
'ok
))))))
(if (rpc-valid? handle)
(begin
(let ((welcome (read* (%mzrpc-from-server handle))))
(set-%mzrpc-reader! handle (thread (lambda () (reader handle))))
(let ((chalenge (rpc-chalenge handle)))
(let ((R (rpc-call handle rpc-login user ((%mzrpc-hasher handle) pass chalenge))))
(if (not (eq? R #f))
handle
(begin
(set-%mzrpc-valid! handle #f)
(set-%mzrpc-diagnose! handle 'wrong-password)
(set-%mzrpc-message! handle (format "Cannot login for user ~s" user))
handle)))))))
handle)))
(spod-module-add (s=== "(rpc-notifications-handler handle F)")
(sp "Sets the notification handler for the given client handle to a new function.")
(sp "Returns handle."))
(define (rpc-notifications-handler handle F)
(set-%mzrpc-notifications-handler! handle F)
handle)
(spod-module-add (s=== "(rpc-disconnect handle)")
(sp "Disconnects from the mzrpc server.")
(sp "Returns 'io-error if a not valid handle is given, but closes the TCP connection, and"
"kills a started thread")
(sp "Returns 'disconnected"))
(define (rpc-disconnect handle)
(let ((R (if (rpc-valid? handle)
(let ((r (rpc-call handle rpc-end)))
(kill-thread (%mzrpc-reader handle))
'disconnected)
(begin
(if (not (eq? (%mzrpc-reader handle) #f ))
(kill-thread (%mzrpc-reader handle)))
'io-error))))
(if (eq? (rpc-diagnose handle) 'no-connect)
R
(begin
(close-input-port (%mzrpc-from-server handle))
(close-output-port (%mzrpc-to-server handle))
R))))
(spod-module-add (s=== "(rpc-shutdown handle)")
(sp "If the user is autorised to do so (i.e. has 'admin level), enables the user to "
"shutdown the server if no other clients are connected.")
(sp "Returns 'forbidden, if the user is not of autorisation type 'admin")
(sp "Returns 'clients-connected, if more than 1 client is connected.")
(sp "Returns 'io-error if a not valid handle is given, but closes the TCP connection")
(sp "Returns 'ok and shuts down the server, otherwise")
)
(define (rpc-shutdown handle)
(let ((R (if (rpc-valid? handle)
(let ((r (rpc-call handle rpc-shutdown)))
(kill-thread (%mzrpc-reader handle))
r)
'io-error)))
(close-input-port (%mzrpc-from-server handle))
(close-output-port (%mzrpc-to-server handle))
R))
(spod-module-add (s=== "(rpc-shutdown handle)")
(sp "If the user is autorised to do so (i.e. has 'admin level), enables the user to "
"shutdown the server if no other clients are connected.")
(sp "Returns 'forbidden, if the user is not of autorisation type 'admin")
(sp "Returns 'clients-connected, if more than 1 client is connected.")
(sp "Returns 'io-error if a not valid handle is given, but closes the TCP connection")
(sp "Returns 'ok and shuts down the server, otherwise"))
(define (rpc-force-shutdown handle)
(let ((R (if (rpc-valid? handle)
(let ((r (rpc-call handle rpc-force-shutdown)))
(kill-thread (%mzrpc-reader handle))
r)
'io-error)))
(close-input-port (%mzrpc-from-server handle))
(close-output-port (%mzrpc-to-server handle))
R))
(define (rpc-chalenge handle)
(rpc-call handle rpc-chalenge))
(spod-module-add (s=== "(rpc-valid? handle)")
(sp "Returns #t, if the handle is valid.")
(sp "Returns #f, otherwise."))
(define (rpc-valid? handle)
(%mzrpc-valid handle))
(spod-module-add (s=== "(rpc-diagnose handle)")
(sp "Returns 'no-connect, if the server could not be connected (on the tcp/ip level)")
(sp "Returns 'wrong-password, if one cannot login with the given user (on mzrpc level)")
(sp "Returns 'io-error, if an io-error occured during operation (on the tcp/ip level)")
(sp "Returns 'call-error, if an error occured during a call to the server (on the rpc level)")
(sp "Returns 'ok, if nothing is wrong"))
(define (rpc-diagnose handle)
(%mzrpc-diagnose handle))
(spod-module-add (s=== "(rpc-message handle)")
(sp "Returns the last error message associated with the handle."))
(define (rpc-message handle)
(%mzrpc-message handle))
(spod-module-add (s=== "(rpc-symbol-call handle f:symbol ...)")
(sp "RPCs function f with arguments ...")
(sp "Returns whatever f would return if no error occured.")
(sp "Returns '%rpc-error, if an error is returned (wrong function call).")
(sp "Returns '%rpc-fatal, if a problem some other problem has occured, handle will have been invalidated.")
(sp "Raises an error if called with an invalid handle."))
(define (rpc-symbol-call handle f . args)
(internal-rpc-call handle f args))
(spod-module-add (s=== "(rpc-call handle f ...)")
(sp "Scheme syntax that calls " (s% "rpc-symbol-call") " with 'f."))
(define-syntax rpc-call
(syntax-rules ()
((_ handle f)
(rpc-symbol-call handle 'f))
((_ handle f a1 ...)
(rpc-symbol-call handle 'f a1 ...))))
(spod-module-add (s=== "(rpc-connected handle)")
(sp "Returns the number of clients connected to the server."))
(define (rpc-connected handle)
(rpc-call handle rpc-connected))
(spod-module-add (s=== "(rpc-global-connect host port user pass notification-handler . pass-hasher) : handle")
(sp "Calls " (s% "rpc-connect") " and sets the global handler for this program to the returned handle.")
(sp "Returns the global handle."))
(define (rpc-global-connect . args)
(set! GLOBAL-CLIENT-HANDLE (apply rpc-connect args))
GLOBAL-CLIENT-HANDLE)
(spod-module-add (s=== "(rpc-global-handle)")
(sp "Returns the current global RPC handle (or #f if no global handle is set)."))
(define (rpc-global-handle)
GLOBAL-CLIENT-HANDLE)
(spod-module-add (s=== "(rpc-global-disconnect)")
(sp "Calls rpc-disconnect with the global handle from the mzrpc server."))
(define (rpc-global-disconnect)
(rpc-disconnect GLOBAL-CLIENT-HANDLE))
(spod-module-add (s=== "(rpc-global-handle! handle)")
(sp "(Re)set the global handle to <handle>")
(sp "Returns handle."))
(define (rpc-global-handle! h)
(set! GLOBAL-CLIENT-HANDLE h)
h)
(spod-module-add (s=== "(rpc-client-define)")
(sp "The same as define, but defines an rpc function that uses the "
"global client handler for calling the equivalent with " (s% "rpc-define") " "
"defined function at the server side."))
(define-syntax rpc-client-define
(syntax-rules ()
((_ (f))
(define (f)
(rpc-symbol-call GLOBAL-CLIENT-HANDLE 'f)))
((_ (f a1 ...))
(define (f a1 ...)
(rpc-symbol-call GLOBAL-CLIENT-HANDLE 'f a1 ...)))
))
(spod-module-add (s=== "(rpc-local-define)")
(sp "The same as define, but defines an rpc function that uses a local "
"client handler for calling the equivalent with " (s% "rpc-define") " "
"defined function at the server side.")
(sp "Example: ")
(ssyn "scm" 8)
(sp " (rpc-local-define (rpc-plus a b))\n"
" (let ((handle (rpc-connect \"server.server.org\" 4304 \"user\" \"pass\" #f)))\n"
" (display (format \"~a~%\" (rpc-plus handle 3 4))\n"
" (rpc-disconnect handle))"))
(define-syntax rpc-local-define
(syntax-rules (handle)
((_ (f))
(define (f handle)
(rpc-symbol-call handle 'f)))
((_ (f a1 ...))
(define (f handle a1 ...)
(rpc-symbol-call handle 'f a1 ...)))
))
(define %module-doc (spod-module-doc))
(define (rpc-client-documentation)
%module-doc)
)