Allow external plugins to return no value
authormartin f. krafft <madduck@madduck.net>
Fri, 21 Mar 2008 18:12:12 +0000 (19:12 +0100)
committerJoey Hess <joey@kodama.kitenet.net>
Fri, 21 Mar 2008 19:07:10 +0000 (15:07 -0400)
Instead of using the XML-RPC v2 extension <nil/>, which Perl's
XML::RPC::Parser does not (yet) support (Joey's patch is pending), we
agreed on a sentinel: {'null':''}, that is, a hash with a single key
"null" pointing to the empty string.

The Python proxy automatically converts None appropriately and raises an
exception if a hook function should, by weird coincidence, attempt to
return {'null':''}.

Signed-off-by: martin f. krafft <madduck@madduck.net>
IkiWiki/Plugin/external.pm
plugins/proxy.py
plugins/pythondemo

index 4c2e5d2feb28ce5d842dcf75dab275a3e7855493..204442c1e0b731db6d985cef61181e0c7b0fc424 100644 (file)
@@ -68,7 +68,22 @@ sub rpc_call ($$;@) { #{{{
                                        return @{$value->value};
                                }
                                elsif ($value->isa('RPC::XML::struct')) {
-                                       return %{$value->value};
+                                       my %hash=%{$value->value};
+
+                                       # XML-RPC v1 does not allow for
+                                       # nil/null/None/undef values to be
+                                       # transmitted, so until
+                                       # XML::RPC::Parser honours v2
+                                       # (<nil/>), external plugins send
+                                       # a hash with one key "null" pointing
+                                       # to an empty string.
+                                       if (exists $hash{null} &&
+                                           $hash{null} eq "" &&
+                                           int(keys(%hash)) == 1) {
+                                               return undef;
+                                       }
+
+                                       return %hash;
                                }
                                else {
                                        return $value->value;
@@ -92,6 +107,14 @@ sub rpc_call ($$;@) { #{{{
                                error("XML RPC call error, unknown function: $name");
                        }
 
+                       # XML-RPC v1 does not allow for nil/null/None/undef
+                       # values to be transmitted, so until XML::RPC::Parser
+                       # honours v2 (<nil/>), send a hash with one key "null"
+                       # pointing to an empty string.
+                       if (! defined $ret) {
+                               $ret={"null" => ""};
+                       }
+
                        my $string=eval { RPC::XML::response->new($ret)->as_string };
                        if ($@ && ref $ret) {
                                # One common reason for serialisation to
index 6f9b1f8525821645d1dee6580864ec3b9e320473..9042f389d079c64df573d59d8e63d8de95cd0c8c 100644 (file)
@@ -13,8 +13,6 @@ __author__ = 'martin f. krafft <madduck@madduck.net>'
 __copyright__ = 'Copyright © ' + __author__
 __licence__ = 'GPLv2'
 
-LOOP_DELAY = 0.1
-
 import sys
 import time
 import xmlrpclib
@@ -31,6 +29,9 @@ class _IkiWikiExtPluginXMLRPCDispatcher(SimpleXMLRPCDispatcher):
             # python2.4 and before only took one argument
             SimpleXMLRPCDispatcher.__init__(self)
 
+    def dispatch(self, method, params):
+        return self._dispatch(method, params)
+
 class _XMLStreamParser(object):
 
     def __init__(self):
@@ -77,8 +78,8 @@ class _XMLStreamParser(object):
 
 class _IkiWikiExtPluginXMLRPCHandler(object):
 
-    def __init__(self, debug_fn, allow_none=False, encoding=None):
-        self._dispatcher = _IkiWikiExtPluginXMLRPCDispatcher(allow_none, encoding)
+    def __init__(self, debug_fn):
+        self._dispatcher = _IkiWikiExtPluginXMLRPCDispatcher()
         self.register_function = self._dispatcher.register_function
         self._debug_fn = debug_fn
 
@@ -125,20 +126,28 @@ class _IkiWikiExtPluginXMLRPCHandler(object):
 
     def handle_rpc(self, in_fd, out_fd):
         self._debug_fn('waiting for procedure calls from ikiwiki...')
-        ret = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
-        if ret is None:
+        xml = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
+        if xml is None:
             # ikiwiki is going down
             self._debug_fn('ikiwiki is going down, and so are we...')
             return
 
-        self._debug_fn('received procedure call from ikiwiki: [%s]' % ret)
-        ret = self._dispatcher._marshaled_dispatch(ret)
-        self._debug_fn('sending procedure response to ikiwiki: [%s]' % ret)
-        _IkiWikiExtPluginXMLRPCHandler._write(out_fd, ret)
+        self._debug_fn('received procedure call from ikiwiki: [%s]' % xml)
+        params, method = xmlrpclib.loads(xml)
+        ret = self._dispatcher.dispatch(method, params)
+        xml = xmlrpclib.dumps((ret,), methodresponse=True)
+        self._debug_fn('sending procedure response to ikiwiki: [%s]' % xml)
+        _IkiWikiExtPluginXMLRPCHandler._write(out_fd, xml)
         return ret
 
 class IkiWikiProcedureProxy(object):
 
+    # how to communicate None to ikiwiki
+    _IKIWIKI_NIL_SENTINEL = {'null':''}
+
+    # sleep during each iteration
+    _LOOP_DELAY = 0.1
+
     def __init__(self, id, in_fd=sys.stdin, out_fd=sys.stdout, debug_fn=None):
         self._id = id
         self._in_fd = in_fd
@@ -151,9 +160,25 @@ class IkiWikiProcedureProxy(object):
         self._xmlrpc_handler = _IkiWikiExtPluginXMLRPCHandler(self._debug_fn)
         self._xmlrpc_handler.register_function(self._importme, name='import')
 
-    def hook(self, type, function):
-        self._hooks.append((type, function.__name__))
-        self._xmlrpc_handler.register_function(function)
+    def hook(self, type, function, name=None):
+        if name is None:
+            name = function.__name__
+        self._hooks.append((type, name))
+
+        def hook_proxy(*args):
+#            curpage = args[0]
+#            kwargs = dict([args[i:i+2] for i in xrange(1, len(args), 2)])
+            ret = function(*args)
+            self._debug_fn("%s hook `%s' returned: [%s]" % (type, name, ret))
+            if ret == IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL:
+                raise IkiWikiProcedureProxy.InvalidReturnValue, \
+                        'hook functions are not allowed to return %s' \
+                        % IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
+            if ret is None:
+                ret = IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
+            return ret
+
+        self._xmlrpc_handler.register_function(hook_proxy, name=name)
 
     def _importme(self):
         self._debug_fn('importing...')
@@ -161,7 +186,7 @@ class IkiWikiProcedureProxy(object):
             self._debug_fn('hooking %s into %s chain...' % (function, type))
             self._xmlrpc_handler.send_rpc('hook', self._in_fd, self._out_fd,
                                           id=self._id, type=type, call=function)
-        return 0
+        return IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
 
     def run(self):
         try:
@@ -169,10 +194,13 @@ class IkiWikiProcedureProxy(object):
                 ret = self._xmlrpc_handler.handle_rpc(self._in_fd, self._out_fd)
                 if ret is None:
                     return
-                time.sleep(LOOP_DELAY)
+                time.sleep(IkiWikiProcedureProxy._LOOP_DELAY)
         except Exception, e:
             print >>sys.stderr, 'uncaught exception: %s' % e
             import traceback
             print >>sys.stderr, traceback.format_exc(sys.exc_info()[2])
             import posix
             sys.exit(posix.EX_SOFTWARE)
+
+    class InvalidReturnValue(Exception):
+        pass
index 6d632d5247ab866ca8af3eb9f4448705c0ce3ce8..5038c603bfa3cd9aaa1b8ac279643e225187b472 100755 (executable)
@@ -151,7 +151,7 @@ def htmlize_demo(*args):
     kwargs = _arglist_to_dict(args)
     debug("hook `htmlize' called with arguments %s" % kwargs)
     return kwargs['content']
-proxy.hook('htmlize', htmlize_demo)
+#proxy.hook('htmlize', htmlize_demo)
 
 def pagetemplate_demo(*args):
     # Templates are filled out for many different things in ikiwiki, like
@@ -178,12 +178,10 @@ def templatefile_demo(*args):
     # change the default ("page.tmpl"). Template files are looked for in
     # /usr/share/ikiwiki/templates by default.
     #
-    # TODO: we cannot really pass undef/None via xml-rpc, so what to do?
     kwargs = _arglist_to_dict(args)
     debug("hook `templatefile' called with arguments %s" % kwargs)
-    raise NotImplementedError
     return None
-#proxy.hook('templatefile', templatefile_demo)
+proxy.hook('templatefile', templatefile_demo)
 
 def sanitize_demo(*args):
     # Use this to implement html sanitization or anything else that needs to