2 # -*- coding: utf-8 -*-
4 # proxy.py — helper for Python-based external (xml-rpc) ikiwiki plugins
6 # Copyright © martin f. krafft <madduck@madduck.net>
7 # Released under the terms of the GNU GPL version 2
10 __description__
= 'helper for Python-based external (xml-rpc) ikiwiki plugins'
12 __author__
= 'martin f. krafft <madduck@madduck.net>'
13 __copyright__
= 'Copyright © ' + __author__
19 import xml
.parsers
.expat
20 from SimpleXMLRPCServer
import SimpleXMLRPCDispatcher
22 class _IkiWikiExtPluginXMLRPCDispatcher(SimpleXMLRPCDispatcher
):
24 def __init__(self
, allow_none
=False, encoding
=None):
26 SimpleXMLRPCDispatcher
.__init
__(self
, allow_none
, encoding
)
28 # see http://bugs.debian.org/470645
29 # python2.4 and before only took one argument
30 SimpleXMLRPCDispatcher
.__init
__(self
)
32 def dispatch(self
, method
, params
):
33 return self
._dispatch
(method
, params
)
35 class XMLStreamParser(object):
38 self
._parser
= xml
.parsers
.expat
.ParserCreate()
39 self
._parser
.StartElementHandler
= self
._push
_tag
40 self
._parser
.EndElementHandler
= self
._pop
_tag
41 self
._parser
.XmlDeclHandler
= self
._check
_pipelining
47 self
._first
_tag
_received
= False
49 def _push_tag(self
, tag
, attrs
):
50 self
._stack
.append(tag
)
51 self
._first
_tag
_received
= True
53 def _pop_tag(self
, tag
):
54 top
= self
._stack
.pop()
56 raise ParseError
, 'expected %s closing tag, got %s' % (top
, tag
)
58 def _request_complete(self
):
59 return self
._first
_tag
_received
and len(self
._stack
) == 0
61 def _check_pipelining(self
, *args
):
62 if self
._first
_tag
_received
:
63 raise PipeliningDetected
, 'need a new line between XML documents'
65 def parse(self
, data
):
66 self
._parser
.Parse(data
, False)
68 if self
._request
_complete
():
73 class ParseError(Exception):
76 class PipeliningDetected(Exception):
79 class _IkiWikiExtPluginXMLRPCHandler(object):
81 def __init__(self
, debug_fn
):
82 self
._dispatcher
= _IkiWikiExtPluginXMLRPCDispatcher()
83 self
.register_function
= self
._dispatcher
.register_function
84 self
._debug
_fn
= debug_fn
86 def register_function(self
, function
, name
=None):
87 # will be overwritten by __init__
91 def _write(out_fd
, data
):
92 out_fd
.write(str(data
))
98 parser
= XMLStreamParser()
100 line
= in_fd
.readline()
102 # ikiwiki exited, EOF received
105 ret
= parser
.parse(line
)
106 # unless this returns non-None, we need to loop again
110 def send_rpc(self
, cmd
, in_fd
, out_fd
, *args
, **kwargs
):
111 xml
= xmlrpclib
.dumps(sum(kwargs
.iteritems(), args
), cmd
)
112 self
._debug
_fn
("calling ikiwiki procedure `%s': [%s]" % (cmd
, xml
))
113 _IkiWikiExtPluginXMLRPCHandler
._write
(out_fd
, xml
)
115 self
._debug
_fn
('reading response from ikiwiki...')
117 xml
= _IkiWikiExtPluginXMLRPCHandler
._read
(in_fd
)
118 self
._debug
_fn
('read response to procedure %s from ikiwiki: [%s]' % (cmd
, xml
))
120 # ikiwiki is going down
121 self
._debug
_fn
('ikiwiki is going down, and so are we...')
122 raise _IkiWikiExtPluginXMLRPCHandler
._GoingDown
124 data
= xmlrpclib
.loads(xml
)[0][0]
125 self
._debug
_fn
('parsed data from response to procedure %s: [%s]' % (cmd
, data
))
128 def handle_rpc(self
, in_fd
, out_fd
):
129 self
._debug
_fn
('waiting for procedure calls from ikiwiki...')
130 xml
= _IkiWikiExtPluginXMLRPCHandler
._read
(in_fd
)
132 # ikiwiki is going down
133 self
._debug
_fn
('ikiwiki is going down, and so are we...')
134 raise _IkiWikiExtPluginXMLRPCHandler
._GoingDown
136 self
._debug
_fn
('received procedure call from ikiwiki: [%s]' % xml
)
137 params
, method
= xmlrpclib
.loads(xml
)
138 ret
= self
._dispatcher
.dispatch(method
, params
)
139 xml
= xmlrpclib
.dumps((ret
,), methodresponse
=True)
140 self
._debug
_fn
('sending procedure response to ikiwiki: [%s]' % xml
)
141 _IkiWikiExtPluginXMLRPCHandler
._write
(out_fd
, xml
)
147 class IkiWikiProcedureProxy(object):
149 # how to communicate None to ikiwiki
150 _IKIWIKI_NIL_SENTINEL
= {'null':''}
152 # sleep during each iteration
155 def __init__(self
, id, in_fd
=sys
.stdin
, out_fd
=sys
.stdout
, debug_fn
=None):
158 self
._out
_fd
= out_fd
160 self
._functions
= list()
161 self
._imported
= False
162 if debug_fn
is not None:
163 self
._debug
_fn
= debug_fn
165 self
._debug
_fn
= lambda s
: None
166 self
._xmlrpc
_handler
= _IkiWikiExtPluginXMLRPCHandler(self
._debug
_fn
)
167 self
._xmlrpc
_handler
.register_function(self
._importme
, name
='import')
169 def rpc(self
, cmd
, *args
, **kwargs
):
173 yield IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
177 args
= list(subst_none(args
))
178 kwargs
= dict(zip(kwargs
.keys(), list(subst_none(kwargs
.itervalues()))))
179 ret
= self
._xmlrpc
_handler
.send_rpc(cmd
, self
._in
_fd
, self
._out
_fd
,
181 if ret
== IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
:
185 def hook(self
, type, function
, name
=None, id=None, last
=False):
187 raise IkiWikiProcedureProxy
.AlreadyImported
190 name
= function
.__name
__
195 def hook_proxy(*args
):
197 # kwargs = dict([args[i:i+2] for i in xrange(1, len(args), 2)])
198 ret
= function(self
, *args
)
199 self
._debug
_fn
("%s hook `%s' returned: [%s]" % (type, name
, ret
))
200 if ret
== IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
:
201 raise IkiWikiProcedureProxy
.InvalidReturnValue
, \
202 'hook functions are not allowed to return %s' \
203 % IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
205 ret
= IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
208 self
._hooks
.append((id, type, name
, last
))
209 self
._xmlrpc
_handler
.register_function(hook_proxy
, name
=name
)
211 def inject(self
, rname
, function
, name
=None, memoize
=True):
213 raise IkiWikiProcedureProxy
.AlreadyImported
216 name
= function
.__name
__
218 self
._functions
.append((rname
, name
, memoize
))
219 self
._xmlrpc
_handler
.register_function(function
, name
=name
)
222 return self
.rpc('getargv')
224 def setargv(self
, argv
):
225 return self
.rpc('setargv', argv
)
227 def getvar(self
, hash, key
):
228 return self
.rpc('getvar', hash, key
)
230 def setvar(self
, hash, key
, value
):
231 return self
.rpc('setvar', hash, key
, value
)
233 def getstate(self
, page
, id, key
):
234 return self
.rpc('getstate', page
, id, key
)
236 def setstate(self
, page
, id, key
, value
):
237 return self
.rpc('setstate', page
, id, key
, value
)
239 def pagespec_match(self
, spec
):
240 return self
.rpc('pagespec_match', spec
)
242 def error(self
, msg
):
244 self
.rpc('error', msg
)
249 sys
.exit(posix
.EX_SOFTWARE
)
254 ret
= self
._xmlrpc
_handler
.handle_rpc(self
._in
_fd
, self
._out
_fd
)
255 time
.sleep(IkiWikiProcedureProxy
._LOOP
_DELAY
)
256 except _IkiWikiExtPluginXMLRPCHandler
._GoingDown
:
261 self
.error('uncaught exception: %s\n%s' \
262 % (e
, traceback
.format_exc(sys
.exc_info()[2])))
266 self
._debug
_fn
('importing...')
267 for id, type, function
, last
in self
._hooks
:
268 self
._debug
_fn
('hooking %s/%s into %s chain...' % (id, function
, type))
269 self
.rpc('hook', id=id, type=type, call
=function
, last
=last
)
270 for rname
, function
, memoize
in self
._functions
:
271 self
._debug
_fn
('injecting %s as %s...' % (function
, rname
))
272 self
.rpc('inject', name
=rname
, call
=function
, memoize
=memoize
)
273 self
._imported
= True
274 return IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
276 class InvalidReturnValue(Exception):
279 class AlreadyImported(Exception):