2 # -*- coding: utf-8 -*-
4 # proxy.py — helper for Python-based external (xml-rpc) ikiwiki plugins
6 # Copyright © martin f. krafft <madduck@madduck.net>
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
17 # THIS SOFTWARE IS PROVIDED BY IKIWIKI AND CONTRIBUTORS ``AS IS''
18 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20 # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
21 # OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
27 # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 __description__
= 'helper for Python-based external (xml-rpc) ikiwiki plugins'
33 __author__
= 'martin f. krafft <madduck@madduck.net>'
34 __copyright__
= 'Copyright © ' + __author__
35 __licence__
= 'BSD-2-clause'
40 import xml
.parsers
.expat
41 from SimpleXMLRPCServer
import SimpleXMLRPCDispatcher
43 class _IkiWikiExtPluginXMLRPCDispatcher(SimpleXMLRPCDispatcher
):
45 def __init__(self
, allow_none
=False, encoding
=None):
47 SimpleXMLRPCDispatcher
.__init
__(self
, allow_none
, encoding
)
49 # see http://bugs.debian.org/470645
50 # python2.4 and before only took one argument
51 SimpleXMLRPCDispatcher
.__init
__(self
)
53 def dispatch(self
, method
, params
):
54 return self
._dispatch
(method
, params
)
56 class XMLStreamParser(object):
59 self
._parser
= xml
.parsers
.expat
.ParserCreate()
60 self
._parser
.StartElementHandler
= self
._push
_tag
61 self
._parser
.EndElementHandler
= self
._pop
_tag
62 self
._parser
.XmlDeclHandler
= self
._check
_pipelining
68 self
._first
_tag
_received
= False
70 def _push_tag(self
, tag
, attrs
):
71 self
._stack
.append(tag
)
72 self
._first
_tag
_received
= True
74 def _pop_tag(self
, tag
):
75 top
= self
._stack
.pop()
77 raise ParseError
, 'expected %s closing tag, got %s' % (top
, tag
)
79 def _request_complete(self
):
80 return self
._first
_tag
_received
and len(self
._stack
) == 0
82 def _check_pipelining(self
, *args
):
83 if self
._first
_tag
_received
:
84 raise PipeliningDetected
, 'need a new line between XML documents'
86 def parse(self
, data
):
87 self
._parser
.Parse(data
, False)
89 if self
._request
_complete
():
94 class ParseError(Exception):
97 class PipeliningDetected(Exception):
100 class _IkiWikiExtPluginXMLRPCHandler(object):
102 def __init__(self
, debug_fn
):
103 self
._dispatcher
= _IkiWikiExtPluginXMLRPCDispatcher()
104 self
.register_function
= self
._dispatcher
.register_function
105 self
._debug
_fn
= debug_fn
107 def register_function(self
, function
, name
=None):
108 # will be overwritten by __init__
112 def _write(out_fd
, data
):
113 out_fd
.write(str(data
))
119 parser
= XMLStreamParser()
121 line
= in_fd
.readline()
123 # ikiwiki exited, EOF received
126 ret
= parser
.parse(line
)
127 # unless this returns non-None, we need to loop again
131 def send_rpc(self
, cmd
, in_fd
, out_fd
, *args
, **kwargs
):
132 xml
= xmlrpclib
.dumps(sum(kwargs
.iteritems(), args
), cmd
)
133 self
._debug
_fn
("calling ikiwiki procedure `%s': [%s]" % (cmd
, xml
))
134 _IkiWikiExtPluginXMLRPCHandler
._write
(out_fd
, xml
)
136 self
._debug
_fn
('reading response from ikiwiki...')
138 xml
= _IkiWikiExtPluginXMLRPCHandler
._read
(in_fd
)
139 self
._debug
_fn
('read response to procedure %s from ikiwiki: [%s]' % (cmd
, xml
))
141 # ikiwiki is going down
142 self
._debug
_fn
('ikiwiki is going down, and so are we...')
143 raise _IkiWikiExtPluginXMLRPCHandler
._GoingDown
145 data
= xmlrpclib
.loads(xml
)[0][0]
146 self
._debug
_fn
('parsed data from response to procedure %s: [%s]' % (cmd
, data
))
149 def handle_rpc(self
, in_fd
, out_fd
):
150 self
._debug
_fn
('waiting for procedure calls from ikiwiki...')
151 xml
= _IkiWikiExtPluginXMLRPCHandler
._read
(in_fd
)
153 # ikiwiki is going down
154 self
._debug
_fn
('ikiwiki is going down, and so are we...')
155 raise _IkiWikiExtPluginXMLRPCHandler
._GoingDown
157 self
._debug
_fn
('received procedure call from ikiwiki: [%s]' % xml
)
158 params
, method
= xmlrpclib
.loads(xml
)
159 ret
= self
._dispatcher
.dispatch(method
, params
)
160 xml
= xmlrpclib
.dumps((ret
,), methodresponse
=True)
161 self
._debug
_fn
('sending procedure response to ikiwiki: [%s]' % xml
)
162 _IkiWikiExtPluginXMLRPCHandler
._write
(out_fd
, xml
)
168 class IkiWikiProcedureProxy(object):
170 # how to communicate None to ikiwiki
171 _IKIWIKI_NIL_SENTINEL
= {'null':''}
173 # sleep during each iteration
176 def __init__(self
, id, in_fd
=sys
.stdin
, out_fd
=sys
.stdout
, debug_fn
=None):
179 self
._out
_fd
= out_fd
181 self
._functions
= list()
182 self
._imported
= False
183 if debug_fn
is not None:
184 self
._debug
_fn
= debug_fn
186 self
._debug
_fn
= lambda s
: None
187 self
._xmlrpc
_handler
= _IkiWikiExtPluginXMLRPCHandler(self
._debug
_fn
)
188 self
._xmlrpc
_handler
.register_function(self
._importme
, name
='import')
190 def rpc(self
, cmd
, *args
, **kwargs
):
194 yield IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
198 args
= list(subst_none(args
))
199 kwargs
= dict(zip(kwargs
.keys(), list(subst_none(kwargs
.itervalues()))))
200 ret
= self
._xmlrpc
_handler
.send_rpc(cmd
, self
._in
_fd
, self
._out
_fd
,
202 if ret
== IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
:
206 def hook(self
, type, function
, name
=None, id=None, last
=False):
208 raise IkiWikiProcedureProxy
.AlreadyImported
211 name
= function
.__name
__
216 def hook_proxy(*args
):
218 # kwargs = dict([args[i:i+2] for i in xrange(1, len(args), 2)])
219 ret
= function(self
, *args
)
220 self
._debug
_fn
("%s hook `%s' returned: [%s]" % (type, name
, ret
))
221 if ret
== IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
:
222 raise IkiWikiProcedureProxy
.InvalidReturnValue
, \
223 'hook functions are not allowed to return %s' \
224 % IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
226 ret
= IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
229 self
._hooks
.append((id, type, name
, last
))
230 self
._xmlrpc
_handler
.register_function(hook_proxy
, name
=name
)
232 def inject(self
, rname
, function
, name
=None, memoize
=True):
234 raise IkiWikiProcedureProxy
.AlreadyImported
237 name
= function
.__name
__
239 self
._functions
.append((rname
, name
, memoize
))
240 self
._xmlrpc
_handler
.register_function(function
, name
=name
)
243 return self
.rpc('getargv')
245 def setargv(self
, argv
):
246 return self
.rpc('setargv', argv
)
248 def getvar(self
, hash, key
):
249 return self
.rpc('getvar', hash, key
)
251 def setvar(self
, hash, key
, value
):
252 return self
.rpc('setvar', hash, key
, value
)
254 def getstate(self
, page
, id, key
):
255 return self
.rpc('getstate', page
, id, key
)
257 def setstate(self
, page
, id, key
, value
):
258 return self
.rpc('setstate', page
, id, key
, value
)
260 def pagespec_match(self
, spec
):
261 return self
.rpc('pagespec_match', spec
)
263 def error(self
, msg
):
265 self
.rpc('error', msg
)
270 sys
.exit(posix
.EX_SOFTWARE
)
275 ret
= self
._xmlrpc
_handler
.handle_rpc(self
._in
_fd
, self
._out
_fd
)
276 time
.sleep(IkiWikiProcedureProxy
._LOOP
_DELAY
)
277 except _IkiWikiExtPluginXMLRPCHandler
._GoingDown
:
282 self
.error('uncaught exception: %s\n%s' \
283 % (e
, traceback
.format_exc(sys
.exc_info()[2])))
287 self
._debug
_fn
('importing...')
288 for id, type, function
, last
in self
._hooks
:
289 self
._debug
_fn
('hooking %s/%s into %s chain...' % (id, function
, type))
290 self
.rpc('hook', id=id, type=type, call
=function
, last
=last
)
291 for rname
, function
, memoize
in self
._functions
:
292 self
._debug
_fn
('injecting %s as %s...' % (function
, rname
))
293 self
.rpc('inject', name
=rname
, call
=function
, memoize
=memoize
)
294 self
._imported
= True
295 return IkiWikiProcedureProxy
._IKIWIKI
_NIL
_SENTINEL
297 class InvalidReturnValue(Exception):
300 class AlreadyImported(Exception):