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