Allow external plugins to return no value

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>
master
martin f. krafft 2008-03-21 19:12:12 +01:00 committed by Joey Hess
parent 99fce0af0d
commit e3624de63c
3 changed files with 69 additions and 20 deletions

View File

@ -68,7 +68,22 @@ sub rpc_call ($$;@) { #{{{
return @{$value->value}; return @{$value->value};
} }
elsif ($value->isa('RPC::XML::struct')) { 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 { else {
return $value->value; return $value->value;
@ -92,6 +107,14 @@ sub rpc_call ($$;@) { #{{{
error("XML RPC call error, unknown function: $name"); 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 }; my $string=eval { RPC::XML::response->new($ret)->as_string };
if ($@ && ref $ret) { if ($@ && ref $ret) {
# One common reason for serialisation to # One common reason for serialisation to

View File

@ -13,8 +13,6 @@ __author__ = 'martin f. krafft <madduck@madduck.net>'
__copyright__ = 'Copyright © ' + __author__ __copyright__ = 'Copyright © ' + __author__
__licence__ = 'GPLv2' __licence__ = 'GPLv2'
LOOP_DELAY = 0.1
import sys import sys
import time import time
import xmlrpclib import xmlrpclib
@ -31,6 +29,9 @@ class _IkiWikiExtPluginXMLRPCDispatcher(SimpleXMLRPCDispatcher):
# python2.4 and before only took one argument # python2.4 and before only took one argument
SimpleXMLRPCDispatcher.__init__(self) SimpleXMLRPCDispatcher.__init__(self)
def dispatch(self, method, params):
return self._dispatch(method, params)
class _XMLStreamParser(object): class _XMLStreamParser(object):
def __init__(self): def __init__(self):
@ -77,8 +78,8 @@ class _XMLStreamParser(object):
class _IkiWikiExtPluginXMLRPCHandler(object): class _IkiWikiExtPluginXMLRPCHandler(object):
def __init__(self, debug_fn, allow_none=False, encoding=None): def __init__(self, debug_fn):
self._dispatcher = _IkiWikiExtPluginXMLRPCDispatcher(allow_none, encoding) self._dispatcher = _IkiWikiExtPluginXMLRPCDispatcher()
self.register_function = self._dispatcher.register_function self.register_function = self._dispatcher.register_function
self._debug_fn = debug_fn self._debug_fn = debug_fn
@ -125,20 +126,28 @@ class _IkiWikiExtPluginXMLRPCHandler(object):
def handle_rpc(self, in_fd, out_fd): def handle_rpc(self, in_fd, out_fd):
self._debug_fn('waiting for procedure calls from ikiwiki...') self._debug_fn('waiting for procedure calls from ikiwiki...')
ret = _IkiWikiExtPluginXMLRPCHandler._read(in_fd) xml = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
if ret is None: if xml is None:
# ikiwiki is going down # ikiwiki is going down
self._debug_fn('ikiwiki is going down, and so are we...') self._debug_fn('ikiwiki is going down, and so are we...')
return return
self._debug_fn('received procedure call from ikiwiki: [%s]' % ret) self._debug_fn('received procedure call from ikiwiki: [%s]' % xml)
ret = self._dispatcher._marshaled_dispatch(ret) params, method = xmlrpclib.loads(xml)
self._debug_fn('sending procedure response to ikiwiki: [%s]' % ret) ret = self._dispatcher.dispatch(method, params)
_IkiWikiExtPluginXMLRPCHandler._write(out_fd, ret) xml = xmlrpclib.dumps((ret,), methodresponse=True)
self._debug_fn('sending procedure response to ikiwiki: [%s]' % xml)
_IkiWikiExtPluginXMLRPCHandler._write(out_fd, xml)
return ret return ret
class IkiWikiProcedureProxy(object): 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): def __init__(self, id, in_fd=sys.stdin, out_fd=sys.stdout, debug_fn=None):
self._id = id self._id = id
self._in_fd = in_fd self._in_fd = in_fd
@ -151,9 +160,25 @@ class IkiWikiProcedureProxy(object):
self._xmlrpc_handler = _IkiWikiExtPluginXMLRPCHandler(self._debug_fn) self._xmlrpc_handler = _IkiWikiExtPluginXMLRPCHandler(self._debug_fn)
self._xmlrpc_handler.register_function(self._importme, name='import') self._xmlrpc_handler.register_function(self._importme, name='import')
def hook(self, type, function): def hook(self, type, function, name=None):
self._hooks.append((type, function.__name__)) if name is None:
self._xmlrpc_handler.register_function(function) 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): def _importme(self):
self._debug_fn('importing...') self._debug_fn('importing...')
@ -161,7 +186,7 @@ class IkiWikiProcedureProxy(object):
self._debug_fn('hooking %s into %s chain...' % (function, type)) self._debug_fn('hooking %s into %s chain...' % (function, type))
self._xmlrpc_handler.send_rpc('hook', self._in_fd, self._out_fd, self._xmlrpc_handler.send_rpc('hook', self._in_fd, self._out_fd,
id=self._id, type=type, call=function) id=self._id, type=type, call=function)
return 0 return IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
def run(self): def run(self):
try: try:
@ -169,10 +194,13 @@ class IkiWikiProcedureProxy(object):
ret = self._xmlrpc_handler.handle_rpc(self._in_fd, self._out_fd) ret = self._xmlrpc_handler.handle_rpc(self._in_fd, self._out_fd)
if ret is None: if ret is None:
return return
time.sleep(LOOP_DELAY) time.sleep(IkiWikiProcedureProxy._LOOP_DELAY)
except Exception, e: except Exception, e:
print >>sys.stderr, 'uncaught exception: %s' % e print >>sys.stderr, 'uncaught exception: %s' % e
import traceback import traceback
print >>sys.stderr, traceback.format_exc(sys.exc_info()[2]) print >>sys.stderr, traceback.format_exc(sys.exc_info()[2])
import posix import posix
sys.exit(posix.EX_SOFTWARE) sys.exit(posix.EX_SOFTWARE)
class InvalidReturnValue(Exception):
pass

View File

@ -151,7 +151,7 @@ def htmlize_demo(*args):
kwargs = _arglist_to_dict(args) kwargs = _arglist_to_dict(args)
debug("hook `htmlize' called with arguments %s" % kwargs) debug("hook `htmlize' called with arguments %s" % kwargs)
return kwargs['content'] return kwargs['content']
proxy.hook('htmlize', htmlize_demo) #proxy.hook('htmlize', htmlize_demo)
def pagetemplate_demo(*args): def pagetemplate_demo(*args):
# Templates are filled out for many different things in ikiwiki, like # 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 # change the default ("page.tmpl"). Template files are looked for in
# /usr/share/ikiwiki/templates by default. # /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) kwargs = _arglist_to_dict(args)
debug("hook `templatefile' called with arguments %s" % kwargs) debug("hook `templatefile' called with arguments %s" % kwargs)
raise NotImplementedError
return None return None
#proxy.hook('templatefile', templatefile_demo) proxy.hook('templatefile', templatefile_demo)
def sanitize_demo(*args): def sanitize_demo(*args):
# Use this to implement html sanitization or anything else that needs to # Use this to implement html sanitization or anything else that needs to