summaryrefslogtreecommitdiff
path: root/plugins/proxy.py
blob: 6a7847fbeb4cbf2bc90988e46ebfe16c687282a4 (plain)
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # proxy.py — helper for Python-based external (xml-rpc) ikiwiki plugins
  5. #
  6. # Copyright © martin f. krafft <madduck@madduck.net>
  7. # Released under the terms of the GNU GPL version 2
  8. #
  9. __name__ = 'proxy.py'
  10. __description__ = 'helper for Python-based external (xml-rpc) ikiwiki plugins'
  11. __version__ = '0.1'
  12. __author__ = 'martin f. krafft <madduck@madduck.net>'
  13. __copyright__ = 'Copyright © ' + __author__
  14. __licence__ = 'GPLv2'
  15. import sys
  16. import time
  17. import xmlrpclib
  18. import xml.parsers.expat
  19. from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
  20. class _IkiWikiExtPluginXMLRPCDispatcher(SimpleXMLRPCDispatcher):
  21. def __init__(self, allow_none=False, encoding=None):
  22. try:
  23. SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
  24. except TypeError:
  25. # see http://bugs.debian.org/470645
  26. # python2.4 and before only took one argument
  27. SimpleXMLRPCDispatcher.__init__(self)
  28. def dispatch(self, method, params):
  29. return self._dispatch(method, params)
  30. class _XMLStreamParser(object):
  31. def __init__(self):
  32. self._parser = xml.parsers.expat.ParserCreate()
  33. self._parser.StartElementHandler = self._push_tag
  34. self._parser.EndElementHandler = self._pop_tag
  35. self._parser.XmlDeclHandler = self._check_pipelining
  36. self._reset()
  37. def _reset(self):
  38. self._stack = list()
  39. self._acc = r''
  40. self._first_tag_received = False
  41. def _push_tag(self, tag, attrs):
  42. self._stack.append(tag)
  43. self._first_tag_received = True
  44. def _pop_tag(self, tag):
  45. top = self._stack.pop()
  46. if top != tag:
  47. raise ParseError, 'expected %s closing tag, got %s' % (top, tag)
  48. def _request_complete(self):
  49. return self._first_tag_received and len(self._stack) == 0
  50. def _check_pipelining(self, *args):
  51. if self._first_tag_received:
  52. raise PipeliningDetected, 'need a new line between XML documents'
  53. def parse(self, data):
  54. self._parser.Parse(data, False)
  55. self._acc += data
  56. if self._request_complete():
  57. ret = self._acc
  58. self._reset()
  59. return ret
  60. class ParseError(Exception):
  61. pass
  62. class PipeliningDetected(Exception):
  63. pass
  64. class _IkiWikiExtPluginXMLRPCHandler(object):
  65. def __init__(self, debug_fn):
  66. self._dispatcher = _IkiWikiExtPluginXMLRPCDispatcher()
  67. self.register_function = self._dispatcher.register_function
  68. self._debug_fn = debug_fn
  69. def register_function(self, function, name=None):
  70. # will be overwritten by __init__
  71. pass
  72. @staticmethod
  73. def _write(out_fd, data):
  74. out_fd.write(str(data))
  75. out_fd.flush()
  76. @staticmethod
  77. def _read(in_fd):
  78. ret = None
  79. parser = _XMLStreamParser()
  80. while True:
  81. line = in_fd.readline()
  82. if len(line) == 0:
  83. # ikiwiki exited, EOF received
  84. return None
  85. ret = parser.parse(line)
  86. # unless this returns non-None, we need to loop again
  87. if ret is not None:
  88. return ret
  89. def send_rpc(self, cmd, in_fd, out_fd, **kwargs):
  90. xml = xmlrpclib.dumps(sum(kwargs.iteritems(), ()), cmd)
  91. self._debug_fn("calling ikiwiki procedure `%s': [%s]" % (cmd, xml))
  92. _IkiWikiExtPluginXMLRPCHandler._write(out_fd, xml)
  93. self._debug_fn('reading response from ikiwiki...')
  94. xml = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
  95. self._debug_fn('read response to procedure %s from ikiwiki: [%s]' % (cmd, xml))
  96. if xml is None:
  97. # ikiwiki is going down
  98. return None
  99. data = xmlrpclib.loads(xml)[0]
  100. self._debug_fn('parsed data from response to procedure %s: [%s]' % (cmd, data))
  101. return data
  102. def handle_rpc(self, in_fd, out_fd):
  103. self._debug_fn('waiting for procedure calls from ikiwiki...')
  104. xml = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
  105. if xml is None:
  106. # ikiwiki is going down
  107. self._debug_fn('ikiwiki is going down, and so are we...')
  108. return
  109. self._debug_fn('received procedure call from ikiwiki: [%s]' % xml)
  110. params, method = xmlrpclib.loads(xml)
  111. ret = self._dispatcher.dispatch(method, params)
  112. xml = xmlrpclib.dumps((ret,), methodresponse=True)
  113. self._debug_fn('sending procedure response to ikiwiki: [%s]' % xml)
  114. _IkiWikiExtPluginXMLRPCHandler._write(out_fd, xml)
  115. return ret
  116. class IkiWikiProcedureProxy(object):
  117. # how to communicate None to ikiwiki
  118. _IKIWIKI_NIL_SENTINEL = {'null':''}
  119. # sleep during each iteration
  120. _LOOP_DELAY = 0.1
  121. def __init__(self, id, in_fd=sys.stdin, out_fd=sys.stdout, debug_fn=None):
  122. self._id = id
  123. self._in_fd = in_fd
  124. self._out_fd = out_fd
  125. self._hooks = list()
  126. if debug_fn is not None:
  127. self._debug_fn = debug_fn
  128. else:
  129. self._debug_fn = lambda s: None
  130. self._xmlrpc_handler = _IkiWikiExtPluginXMLRPCHandler(self._debug_fn)
  131. self._xmlrpc_handler.register_function(self._importme, name='import')
  132. def hook(self, type, function, name=None, last=False):
  133. if name is None:
  134. name = function.__name__
  135. self._hooks.append((type, name, last))
  136. def hook_proxy(*args):
  137. # curpage = args[0]
  138. # kwargs = dict([args[i:i+2] for i in xrange(1, len(args), 2)])
  139. ret = function(self, *args)
  140. self._debug_fn("%s hook `%s' returned: [%s]" % (type, name, ret))
  141. if ret == IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL:
  142. raise IkiWikiProcedureProxy.InvalidReturnValue, \
  143. 'hook functions are not allowed to return %s' \
  144. % IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  145. if ret is None:
  146. ret = IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  147. return ret
  148. self._xmlrpc_handler.register_function(hook_proxy, name=name)
  149. def _importme(self):
  150. self._debug_fn('importing...')
  151. for type, function, last in self._hooks:
  152. self._debug_fn('hooking %s into %s chain...' % (function, type))
  153. self._xmlrpc_handler.send_rpc('hook', self._in_fd, self._out_fd,
  154. id=self._id, type=type, call=function,
  155. last=last)
  156. return IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  157. def run(self):
  158. try:
  159. while True:
  160. ret = self._xmlrpc_handler.handle_rpc(self._in_fd, self._out_fd)
  161. if ret is None:
  162. return
  163. time.sleep(IkiWikiProcedureProxy._LOOP_DELAY)
  164. except Exception, e:
  165. print >>sys.stderr, 'uncaught exception: %s' % e
  166. import traceback
  167. print >>sys.stderr, traceback.format_exc(sys.exc_info()[2])
  168. import posix
  169. sys.exit(posix.EX_SOFTWARE)
  170. class InvalidReturnValue(Exception):
  171. pass