summaryrefslogtreecommitdiff
path: root/plugins/proxy.py
blob: 55c552ab22e30aab59c2a10c3192bd9a30e7ebea (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. LOOP_DELAY = 0.1
  16. import sys
  17. import time
  18. import xmlrpclib
  19. import xml.parsers.expat
  20. from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
  21. class _IkiWikiExtPluginXMLRPCDispatcher(SimpleXMLRPCDispatcher):
  22. def __init__(self, allow_none=False, encoding=None):
  23. try:
  24. SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
  25. except TypeError:
  26. # see http://bugs.debian.org/470645
  27. # python2.4 and before only took one argument
  28. SimpleXMLRPCDispatcher.__init__(self)
  29. class _XMLStreamParser(object):
  30. def __init__(self):
  31. self._parser = xml.parsers.expat.ParserCreate()
  32. self._parser.StartElementHandler = self._push_tag
  33. self._parser.EndElementHandler = self._pop_tag
  34. self._parser.XmlDeclHandler = self._check_pipelining
  35. self._reset()
  36. def _reset(self):
  37. self._stack = list()
  38. self._acc = r''
  39. self._first_tag_received = False
  40. def _push_tag(self, tag, attrs):
  41. self._stack.append(tag)
  42. self._first_tag_received = True
  43. def _pop_tag(self, tag):
  44. top = self._stack.pop()
  45. if top != tag:
  46. raise ParseError, 'expected %s closing tag, got %s' % (top, tag)
  47. def _request_complete(self):
  48. return self._first_tag_received and len(self._stack) == 0
  49. def _check_pipelining(self, *args):
  50. if self._first_tag_received:
  51. raise PipeliningDetected, 'need a new line between XML documents'
  52. def parse(self, data):
  53. self._parser.Parse(data, False)
  54. self._acc += data
  55. if self._request_complete():
  56. ret = self._acc
  57. self._reset()
  58. return ret
  59. class ParseError(Exception):
  60. pass
  61. class PipeliningDetected(Exception):
  62. pass
  63. class _IkiWikiExtPluginXMLRPCHandler(object):
  64. def __init__(self, debug_fn, allow_none=False, encoding=None):
  65. self._dispatcher = _IkiWikiExtPluginXMLRPCDispatcher(allow_none, encoding)
  66. self.register_function = self._dispatcher.register_function
  67. self._debug_fn = debug_fn
  68. def register_function(self, function, name=None):
  69. # will be overwritten by __init__
  70. pass
  71. @staticmethod
  72. def _write(out_fd, data):
  73. out_fd.write(data)
  74. out_fd.flush()
  75. @staticmethod
  76. def _read(in_fd):
  77. ret = None
  78. parser = _XMLStreamParser()
  79. while True:
  80. line = in_fd.readline()
  81. if len(line) == 0:
  82. # ikiwiki exited, EOF received
  83. return None
  84. ret = parser.parse(line)
  85. # unless this returns non-None, we need to loop again
  86. if ret is not None:
  87. return ret
  88. def send_rpc(self, cmd, in_fd, out_fd, **kwargs):
  89. xml = xmlrpclib.dumps(sum(kwargs.iteritems(), ()), cmd)
  90. self._debug_fn("calling ikiwiki procedure `%s': [%s]" % (cmd, xml))
  91. _IkiWikiExtPluginXMLRPCHandler._write(out_fd, xml)
  92. self._debug_fn('reading response from ikiwiki...')
  93. xml = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
  94. self._debug_fn('read response to procedure %s from ikiwiki: [%s]' % (cmd, xml))
  95. if xml is None:
  96. # ikiwiki is going down
  97. return None
  98. data = xmlrpclib.loads(xml)[0]
  99. self._debug_fn('parsed data from response to procedure %s: [%s]' % (cmd, data))
  100. return data
  101. def handle_rpc(self, in_fd, out_fd):
  102. self._debug_fn('waiting for procedure calls from ikiwiki...')
  103. ret = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
  104. if ret is None:
  105. # ikiwiki is going down
  106. self._debug_fn('ikiwiki is going down, and so are we...')
  107. return
  108. self._debug_fn('received procedure call from ikiwiki: [%s]' % ret)
  109. ret = self._dispatcher._marshaled_dispatch(ret)
  110. self._debug_fn('sending procedure response to ikiwiki: [%s]' % ret)
  111. _IkiWikiExtPluginXMLRPCHandler._write(out_fd, ret)
  112. return ret
  113. class IkiWikiProcedureProxy(object):
  114. def __init__(self, id, in_fd=sys.stdin, out_fd=sys.stdout, debug_fn=None):
  115. self._id = id
  116. self._in_fd = in_fd
  117. self._out_fd = out_fd
  118. self._hooks = list()
  119. if debug_fn is not None:
  120. self._debug_fn = debug_fn
  121. else:
  122. self._debug_fn = lambda s: None
  123. self._xmlrpc_handler = _IkiWikiExtPluginXMLRPCHandler(self._debug_fn)
  124. self._xmlrpc_handler.register_function(self._importme, name='import')
  125. def register_hook(self, type, function):
  126. self._hooks.append((type, function.__name__))
  127. self._xmlrpc_handler.register_function(function)
  128. def _importme(self):
  129. self._debug_fn('importing...')
  130. for type, function in self._hooks:
  131. self._debug_fn('hooking %s into %s chain...' % (function, type))
  132. self._xmlrpc_handler.send_rpc('hook', self._in_fd, self._out_fd,
  133. id=self._id, type=type, call=function)
  134. return 0
  135. def run(self):
  136. try:
  137. while True:
  138. ret = self._xmlrpc_handler.handle_rpc(self._in_fd, self._out_fd)
  139. if ret is None:
  140. return
  141. time.sleep(LOOP_DELAY)
  142. except Exception, e:
  143. self._debug_fn('uncaught exception: %s' % e)
  144. sys.exit(posix.EX_SOFTWARE)