summaryrefslogtreecommitdiff
path: root/plugins/proxy.py
blob: 477a365f8d6b4c7030c5af7abedeb7c2ae1fdfaf (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, *args, **kwargs):
  90. xml = xmlrpclib.dumps(sum(kwargs.iteritems(), args), 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. self._debug_fn('ikiwiki is going down, and so are we...')
  99. raise _IkiWikiExtPluginXMLRPCHandler._GoingDown
  100. data = xmlrpclib.loads(xml)[0][0]
  101. self._debug_fn('parsed data from response to procedure %s: [%s]' % (cmd, data))
  102. return data
  103. def handle_rpc(self, in_fd, out_fd):
  104. self._debug_fn('waiting for procedure calls from ikiwiki...')
  105. xml = _IkiWikiExtPluginXMLRPCHandler._read(in_fd)
  106. if xml is None:
  107. # ikiwiki is going down
  108. self._debug_fn('ikiwiki is going down, and so are we...')
  109. raise _IkiWikiExtPluginXMLRPCHandler._GoingDown
  110. self._debug_fn('received procedure call from ikiwiki: [%s]' % xml)
  111. params, method = xmlrpclib.loads(xml)
  112. ret = self._dispatcher.dispatch(method, params)
  113. xml = xmlrpclib.dumps((ret,), methodresponse=True)
  114. self._debug_fn('sending procedure response to ikiwiki: [%s]' % xml)
  115. _IkiWikiExtPluginXMLRPCHandler._write(out_fd, xml)
  116. return ret
  117. class _GoingDown:
  118. pass
  119. class IkiWikiProcedureProxy(object):
  120. # how to communicate None to ikiwiki
  121. _IKIWIKI_NIL_SENTINEL = {'null':''}
  122. # sleep during each iteration
  123. _LOOP_DELAY = 0.1
  124. def __init__(self, id, in_fd=sys.stdin, out_fd=sys.stdout, debug_fn=None):
  125. self._id = id
  126. self._in_fd = in_fd
  127. self._out_fd = out_fd
  128. self._hooks = list()
  129. self._functions = list()
  130. self._imported = False
  131. if debug_fn is not None:
  132. self._debug_fn = debug_fn
  133. else:
  134. self._debug_fn = lambda s: None
  135. self._xmlrpc_handler = _IkiWikiExtPluginXMLRPCHandler(self._debug_fn)
  136. self._xmlrpc_handler.register_function(self._importme, name='import')
  137. def rpc(self, cmd, *args, **kwargs):
  138. def subst_none(seq):
  139. for i in seq:
  140. if i is None:
  141. yield IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  142. else:
  143. yield i
  144. args = list(subst_none(args))
  145. kwargs = dict(zip(kwargs.keys(), list(subst_none(kwargs.itervalues()))))
  146. ret = self._xmlrpc_handler.send_rpc(cmd, self._in_fd, self._out_fd,
  147. *args, **kwargs)
  148. if ret == IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL:
  149. ret = None
  150. return ret
  151. def hook(self, type, function, name=None, id=None, last=False):
  152. if self._imported:
  153. raise IkiWikiProcedureProxy.AlreadyImported
  154. if name is None:
  155. name = function.__name__
  156. if id is None:
  157. id = self._id
  158. def hook_proxy(*args):
  159. # curpage = args[0]
  160. # kwargs = dict([args[i:i+2] for i in xrange(1, len(args), 2)])
  161. ret = function(self, *args)
  162. self._debug_fn("%s hook `%s' returned: [%s]" % (type, name, ret))
  163. if ret == IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL:
  164. raise IkiWikiProcedureProxy.InvalidReturnValue, \
  165. 'hook functions are not allowed to return %s' \
  166. % IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  167. if ret is None:
  168. ret = IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  169. return ret
  170. self._hooks.append((id, type, name, last))
  171. self._xmlrpc_handler.register_function(hook_proxy, name=name)
  172. def inject(self, rname, function, name=None, memoize=True):
  173. if self._imported:
  174. raise IkiWikiProcedureProxy.AlreadyImported
  175. if name is None:
  176. name = function.__name__
  177. self._functions.append((rname, name, memoize))
  178. self._xmlrpc_handler.register_function(function, name=name)
  179. def getargv(self):
  180. return self.rpc('getargv')
  181. def setargv(self, argv):
  182. return self.rpc('setargv', argv)
  183. def getvar(self, hash, key):
  184. return self.rpc('getvar', hash, key)
  185. def setvar(self, hash, key, value):
  186. return self.rpc('setvar', hash, key, value)
  187. def getstate(self, page, id, key):
  188. return self.rpc('getstate', page, id, key)
  189. def setstate(self, page, id, key, value):
  190. return self.rpc('setstate', page, id, key, value)
  191. def pagespec_match(self, spec):
  192. return self.rpc('pagespec_match', spec)
  193. def error(self, msg):
  194. try:
  195. self.rpc('error', msg)
  196. except IOError, e:
  197. if e.errno != 32:
  198. raise
  199. import posix
  200. sys.exit(posix.EX_SOFTWARE)
  201. def run(self):
  202. try:
  203. while True:
  204. ret = self._xmlrpc_handler.handle_rpc(self._in_fd, self._out_fd)
  205. time.sleep(IkiWikiProcedureProxy._LOOP_DELAY)
  206. except _IkiWikiExtPluginXMLRPCHandler._GoingDown:
  207. return
  208. except Exception, e:
  209. import traceback
  210. self.error('uncaught exception: %s\n%s' \
  211. % (e, traceback.format_exc(sys.exc_info()[2])))
  212. return
  213. def _importme(self):
  214. self._debug_fn('importing...')
  215. for id, type, function, last in self._hooks:
  216. self._debug_fn('hooking %s/%s into %s chain...' % (id, function, type))
  217. self.rpc('hook', id=id, type=type, call=function, last=last)
  218. for rname, function, memoize in self._functions:
  219. self._debug_fn('injecting %s as %s...' % (function, rname))
  220. self.rpc('inject', name=rname, call=function, memoize=memoize)
  221. self._imported = True
  222. return IkiWikiProcedureProxy._IKIWIKI_NIL_SENTINEL
  223. class InvalidReturnValue(Exception):
  224. pass
  225. class AlreadyImported(Exception):
  226. pass