blob: b7647c198f94a64bd5550e40859285f2ca50b1a7 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# BitBake XMLRPC Server
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2008 Richard Purdie
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20"""
21 This module implements an xmlrpc server for BitBake.
22
23 Use this by deriving a class from BitBakeXMLRPCServer and then adding
24 methods which you want to "export" via XMLRPC. If the methods have the
25 prefix xmlrpc_, then registering those function will happen automatically,
26 if not, you need to call register_function.
27
28 Use register_idle_function() to add a function which the xmlrpc server
29 calls from within server_forever when no requests are pending. Make sure
30 that those functions are non-blocking or else you will introduce latency
31 in the server's main loop.
32"""
33
34import bb
35import xmlrpclib, sys
36from bb import daemonize
37from bb.ui import uievent
38import hashlib, time
39import socket
40import os, signal
41import threading
42try:
43 import cPickle as pickle
44except ImportError:
45 import pickle
46
47DEBUG = False
48
49from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
50import inspect, select, httplib
51
52from . import BitBakeBaseServer, BitBakeBaseServerConnection, BaseImplServer
53
54class BBTransport(xmlrpclib.Transport):
55 def __init__(self, timeout):
56 self.timeout = timeout
57 self.connection_token = None
58 xmlrpclib.Transport.__init__(self)
59
60 # Modified from default to pass timeout to HTTPConnection
61 def make_connection(self, host):
62 #return an existing connection if possible. This allows
63 #HTTP/1.1 keep-alive.
64 if self._connection and host == self._connection[0]:
65 return self._connection[1]
66
67 # create a HTTP connection object from a host descriptor
68 chost, self._extra_headers, x509 = self.get_host_info(host)
69 #store the host argument along with the connection object
70 self._connection = host, httplib.HTTPConnection(chost, timeout=self.timeout)
71 return self._connection[1]
72
73 def set_connection_token(self, token):
74 self.connection_token = token
75
76 def send_content(self, h, body):
77 if self.connection_token:
78 h.putheader("Bitbake-token", self.connection_token)
79 xmlrpclib.Transport.send_content(self, h, body)
80
81def _create_server(host, port, timeout = 60):
82 t = BBTransport(timeout)
83 s = xmlrpclib.ServerProxy("http://%s:%d/" % (host, port), transport=t, allow_none=True)
84 return s, t
85
86class BitBakeServerCommands():
87
88 def __init__(self, server):
89 self.server = server
90 self.has_client = False
91
92 def registerEventHandler(self, host, port):
93 """
94 Register a remote UI Event Handler
95 """
96 s, t = _create_server(host, port)
97
98 # we don't allow connections if the cooker is running
99 if (self.cooker.state in [bb.cooker.state.parsing, bb.cooker.state.running]):
100 return None
101
102 self.event_handle = bb.event.register_UIHhandler(s, True)
103 return self.event_handle
104
105 def unregisterEventHandler(self, handlerNum):
106 """
107 Unregister a remote UI Event Handler
108 """
109 return bb.event.unregister_UIHhandler(handlerNum)
110
111 def runCommand(self, command):
112 """
113 Run a cooker command on the server
114 """
115 return self.cooker.command.runCommand(command, self.server.readonly)
116
117 def getEventHandle(self):
118 return self.event_handle
119
120 def terminateServer(self):
121 """
122 Trigger the server to quit
123 """
124 self.server.quit = True
125 print("Server (cooker) exiting")
126 return
127
128 def addClient(self):
129 if self.has_client:
130 return None
131 token = hashlib.md5(str(time.time())).hexdigest()
132 self.server.set_connection_token(token)
133 self.has_client = True
134 return token
135
136 def removeClient(self):
137 if self.has_client:
138 self.server.set_connection_token(None)
139 self.has_client = False
140 if self.server.single_use:
141 self.server.quit = True
142
143# This request handler checks if the request has a "Bitbake-token" header
144# field (this comes from the client side) and compares it with its internal
145# "Bitbake-token" field (this comes from the server). If the two are not
146# equal, it is assumed that a client is trying to connect to the server
147# while another client is connected to the server. In this case, a 503 error
148# ("service unavailable") is returned to the client.
149class BitBakeXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
150 def __init__(self, request, client_address, server):
151 self.server = server
152 SimpleXMLRPCRequestHandler.__init__(self, request, client_address, server)
153
154 def do_POST(self):
155 try:
156 remote_token = self.headers["Bitbake-token"]
157 except:
158 remote_token = None
159 if remote_token != self.server.connection_token and remote_token != "observer":
160 self.report_503()
161 else:
162 if remote_token == "observer":
163 self.server.readonly = True
164 else:
165 self.server.readonly = False
166 SimpleXMLRPCRequestHandler.do_POST(self)
167
168 def report_503(self):
169 self.send_response(503)
170 response = 'No more client allowed'
171 self.send_header("Content-type", "text/plain")
172 self.send_header("Content-length", str(len(response)))
173 self.end_headers()
174 self.wfile.write(response)
175
176
177class XMLRPCProxyServer(BaseImplServer):
178 """ not a real working server, but a stub for a proxy server connection
179
180 """
181 def __init__(self, host, port):
182 self.host = host
183 self.port = port
184
185class XMLRPCServer(SimpleXMLRPCServer, BaseImplServer):
186 # remove this when you're done with debugging
187 # allow_reuse_address = True
188
189 def __init__(self, interface):
190 """
191 Constructor
192 """
193 BaseImplServer.__init__(self)
194 if (interface[1] == 0): # anonymous port, not getting reused
195 self.single_use = True
196 # Use auto port configuration
197 if (interface[1] == -1):
198 interface = (interface[0], 0)
199 SimpleXMLRPCServer.__init__(self, interface,
200 requestHandler=BitBakeXMLRPCRequestHandler,
201 logRequests=False, allow_none=True)
202 self.host, self.port = self.socket.getsockname()
203 self.connection_token = None
204 #self.register_introspection_functions()
205 self.commands = BitBakeServerCommands(self)
206 self.autoregister_all_functions(self.commands, "")
207 self.interface = interface
208 self.single_use = False
209
210 def addcooker(self, cooker):
211 BaseImplServer.addcooker(self, cooker)
212 self.commands.cooker = cooker
213
214 def autoregister_all_functions(self, context, prefix):
215 """
216 Convenience method for registering all functions in the scope
217 of this class that start with a common prefix
218 """
219 methodlist = inspect.getmembers(context, inspect.ismethod)
220 for name, method in methodlist:
221 if name.startswith(prefix):
222 self.register_function(method, name[len(prefix):])
223
224
225 def serve_forever(self):
226 # Start the actual XMLRPC server
227 bb.cooker.server_main(self.cooker, self._serve_forever)
228
229 def _serve_forever(self):
230 """
231 Serve Requests. Overloaded to honor a quit command
232 """
233 self.quit = False
234 while not self.quit:
235 fds = [self]
236 nextsleep = 0.1
237 for function, data in self._idlefuns.items():
238 retval = None
239 try:
240 retval = function(self, data, False)
241 if retval is False:
242 del self._idlefuns[function]
243 elif retval is True:
244 nextsleep = 0
245 elif isinstance(retval, float):
246 if (retval < nextsleep):
247 nextsleep = retval
248 else:
249 fds = fds + retval
250 except SystemExit:
251 raise
252 except:
253 import traceback
254 traceback.print_exc()
255 if retval == None:
256 # the function execute failed; delete it
257 del self._idlefuns[function]
258 pass
259
260 socktimeout = self.socket.gettimeout() or nextsleep
261 socktimeout = min(socktimeout, nextsleep)
262 # Mirror what BaseServer handle_request would do
263 try:
264 fd_sets = select.select(fds, [], [], socktimeout)
265 if fd_sets[0] and self in fd_sets[0]:
266 self._handle_request_noblock()
267 except IOError:
268 # we ignore interrupted calls
269 pass
270
271 # Tell idle functions we're exiting
272 for function, data in self._idlefuns.items():
273 try:
274 retval = function(self, data, True)
275 except:
276 pass
277 self.server_close()
278 return
279
280 def set_connection_token(self, token):
281 self.connection_token = token
282
283class BitBakeXMLRPCServerConnection(BitBakeBaseServerConnection):
284 def __init__(self, serverImpl, clientinfo=("localhost", 0), observer_only = False, featureset = None):
285 self.connection, self.transport = _create_server(serverImpl.host, serverImpl.port)
286 self.clientinfo = clientinfo
287 self.serverImpl = serverImpl
288 self.observer_only = observer_only
289 if featureset:
290 self.featureset = featureset
291 else:
292 self.featureset = []
293
294 def connect(self, token = None):
295 if token is None:
296 if self.observer_only:
297 token = "observer"
298 else:
299 token = self.connection.addClient()
300
301 if token is None:
302 return None
303
304 self.transport.set_connection_token(token)
305
306 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo)
307 for event in bb.event.ui_queue:
308 self.events.queue_event(event)
309
310 _, error = self.connection.runCommand(["setFeatures", self.featureset])
311 if error:
312 # disconnect the client, we can't make the setFeature work
313 self.connection.removeClient()
314 # no need to log it here, the error shall be sent to the client
315 raise BaseException(error)
316
317 return self
318
319 def removeClient(self):
320 if not self.observer_only:
321 self.connection.removeClient()
322
323 def terminate(self):
324 # Don't wait for server indefinitely
325 import socket
326 socket.setdefaulttimeout(2)
327 try:
328 self.events.system_quit()
329 except:
330 pass
331 try:
332 self.connection.removeClient()
333 except:
334 pass
335
336class BitBakeServer(BitBakeBaseServer):
337 def initServer(self, interface = ("localhost", 0)):
338 self.interface = interface
339 self.serverImpl = XMLRPCServer(interface)
340
341 def detach(self):
342 daemonize.createDaemon(self.serverImpl.serve_forever, "bitbake-cookerdaemon.log")
343 del self.cooker
344
345 def establishConnection(self, featureset):
346 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, self.interface, False, featureset)
347 return self.connection.connect()
348
349 def set_connection_token(self, token):
350 self.connection.transport.set_connection_token(token)
351
352class BitBakeXMLRPCClient(BitBakeBaseServer):
353
354 def __init__(self, observer_only = False, token = None):
355 self.token = token
356
357 self.observer_only = observer_only
358 # if we need extra caches, just tell the server to load them all
359 pass
360
361 def saveConnectionDetails(self, remote):
362 self.remote = remote
363
364 def establishConnection(self, featureset):
365 # The format of "remote" must be "server:port"
366 try:
367 [host, port] = self.remote.split(":")
368 port = int(port)
369 except Exception as e:
370 bb.warn("Failed to read remote definition (%s)" % str(e))
371 raise e
372
373 # We need our IP for the server connection. We get the IP
374 # by trying to connect with the server
375 try:
376 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
377 s.connect((host, port))
378 ip = s.getsockname()[0]
379 s.close()
380 except Exception as e:
381 bb.warn("Could not create socket for %s:%s (%s)" % (host, port, str(e)))
382 raise e
383 try:
384 self.serverImpl = XMLRPCProxyServer(host, port)
385 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, (ip, 0), self.observer_only, featureset)
386 return self.connection.connect(self.token)
387 except Exception as e:
388 bb.warn("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
389 raise e
390
391 def endSession(self):
392 self.connection.removeClient()