summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/server/xmlrpc.py
diff options
context:
space:
mode:
authorRichard Purdie <richard.purdie@linuxfoundation.org>2017-07-18 22:28:40 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2017-07-21 08:41:11 +0100
commit4602408c69132315c3784718fe4ce155b12464cf (patch)
tree2262f6d8b5e89930b4da4efd72e226ae8f740611 /bitbake/lib/bb/server/xmlrpc.py
parent21a19e0e0bf1b39969f6f2ec37a5784d0069715b (diff)
downloadpoky-4602408c69132315c3784718fe4ce155b12464cf.tar.gz
bitbake: server: Rework the server API so process and xmlrpc servers coexist
This changes the way bitbake server works quite radically. Now, the server is always a process based server with the option of starting an XMLRPC listener on a specific inferface/port. Behind the scenes this is done with a "bitbake.sock" file alongside the bitbake.lock file. If we can obtain the lock, we know we need to start a server. The server always listens on the socket and UIs can then connect to this. UIs connect by sending a set of three file descriptors over the domain socket, one for sending commands, one for receiving command results and the other for receiving events. These changes meant we can throw away all the horrid server abstraction code, the plugable transport option to bitbake and the code becomes much more readable and debuggable. It also likely removes a ton of ways you could hang the UI/cooker in weird ways due to all the race conditions that existed with previous processes. Changes: * The foreground option for bitbake-server was dropped. Just tail the log if you really want this, the codepaths were complicated enough without adding one for this. * BBSERVER="autodetect" was dropped. The server will autostart and autoconnect in process mode. You have to specify an xmlrpc server address since that can't be autodetected. I can't see a use case for autodetect now. * The transport/servetype option to bitbake was dropped. * A BB_SERVER_TIMEOUT variable is added which allows the server to stay resident for a period of time after the last client disconnects before unloading. This is used if the -T/--idle-timeout option is not passed to bitbake. This change is invasive and may well introduce new issues however I believe the codebase is in a much better position for further development and debugging. (Bitbake rev: 72a3dbe13a23588e24c0baca6d58c35cdeba3f63) Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/bb/server/xmlrpc.py')
-rw-r--r--bitbake/lib/bb/server/xmlrpc.py492
1 files changed, 0 insertions, 492 deletions
diff --git a/bitbake/lib/bb/server/xmlrpc.py b/bitbake/lib/bb/server/xmlrpc.py
deleted file mode 100644
index 6874765136..0000000000
--- a/bitbake/lib/bb/server/xmlrpc.py
+++ /dev/null
@@ -1,492 +0,0 @@
1#
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 os
35import sys
36
37import hashlib
38import time
39import socket
40import signal
41import threading
42import pickle
43import inspect
44import select
45import http.client
46import xmlrpc.client
47from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
48
49import bb
50from bb import daemonize
51from bb.ui import uievent
52
53DEBUG = False
54
55class BBTransport(xmlrpc.client.Transport):
56 def __init__(self, timeout):
57 self.timeout = timeout
58 self.connection_token = None
59 xmlrpc.client.Transport.__init__(self)
60
61 # Modified from default to pass timeout to HTTPConnection
62 def make_connection(self, host):
63 #return an existing connection if possible. This allows
64 #HTTP/1.1 keep-alive.
65 if self._connection and host == self._connection[0]:
66 return self._connection[1]
67
68 # create a HTTP connection object from a host descriptor
69 chost, self._extra_headers, x509 = self.get_host_info(host)
70 #store the host argument along with the connection object
71 self._connection = host, http.client.HTTPConnection(chost, timeout=self.timeout)
72 return self._connection[1]
73
74 def set_connection_token(self, token):
75 self.connection_token = token
76
77 def send_content(self, h, body):
78 if self.connection_token:
79 h.putheader("Bitbake-token", self.connection_token)
80 xmlrpc.client.Transport.send_content(self, h, body)
81
82def _create_server(host, port, timeout = 60):
83 t = BBTransport(timeout)
84 s = xmlrpc.client.ServerProxy("http://%s:%d/" % (host, port), transport=t, allow_none=True, use_builtin_types=True)
85 return s, t
86
87def check_connection(remote, timeout):
88 try:
89 host, port = remote.split(":")
90 port = int(port)
91 except Exception as e:
92 bb.warn("Failed to read remote definition (%s)" % str(e))
93 raise e
94
95 server, _transport = _create_server(host, port, timeout)
96 try:
97 ret, err = server.runCommand(['getVariable', 'TOPDIR'])
98 if err or not ret:
99 return False
100 except ConnectionError:
101 return False
102 return True
103
104class BitBakeServerCommands():
105
106 def __init__(self, server):
107 self.server = server
108 self.has_client = False
109
110 def registerEventHandler(self, host, port):
111 """
112 Register a remote UI Event Handler
113 """
114 s, t = _create_server(host, port)
115
116 # we don't allow connections if the cooker is running
117 if (self.cooker.state in [bb.cooker.state.parsing, bb.cooker.state.running]):
118 return None, "Cooker is busy: %s" % bb.cooker.state.get_name(self.cooker.state)
119
120 self.event_handle = bb.event.register_UIHhandler(s, True)
121 return self.event_handle, 'OK'
122
123 def unregisterEventHandler(self, handlerNum):
124 """
125 Unregister a remote UI Event Handler
126 """
127 return bb.event.unregister_UIHhandler(handlerNum, True)
128
129 def runCommand(self, command):
130 """
131 Run a cooker command on the server
132 """
133 return self.cooker.command.runCommand(command, self.server.readonly)
134
135 def getEventHandle(self):
136 return self.event_handle
137
138 def terminateServer(self):
139 """
140 Trigger the server to quit
141 """
142 self.server.quit = True
143 print("Server (cooker) exiting")
144 return
145
146 def addClient(self):
147 if self.has_client:
148 return None
149 token = hashlib.md5(str(time.time()).encode("utf-8")).hexdigest()
150 self.server.set_connection_token(token)
151 self.has_client = True
152 return token
153
154 def removeClient(self):
155 if self.has_client:
156 self.server.set_connection_token(None)
157 self.has_client = False
158 if self.server.single_use:
159 self.server.quit = True
160
161# This request handler checks if the request has a "Bitbake-token" header
162# field (this comes from the client side) and compares it with its internal
163# "Bitbake-token" field (this comes from the server). If the two are not
164# equal, it is assumed that a client is trying to connect to the server
165# while another client is connected to the server. In this case, a 503 error
166# ("service unavailable") is returned to the client.
167class BitBakeXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
168 def __init__(self, request, client_address, server):
169 self.server = server
170 SimpleXMLRPCRequestHandler.__init__(self, request, client_address, server)
171
172 def do_POST(self):
173 try:
174 remote_token = self.headers["Bitbake-token"]
175 except:
176 remote_token = None
177 if remote_token != self.server.connection_token and remote_token != "observer":
178 self.report_503()
179 else:
180 if remote_token == "observer":
181 self.server.readonly = True
182 else:
183 self.server.readonly = False
184 SimpleXMLRPCRequestHandler.do_POST(self)
185
186 def report_503(self):
187 self.send_response(503)
188 response = 'No more client allowed'
189 self.send_header("Content-type", "text/plain")
190 self.send_header("Content-length", str(len(response)))
191 self.end_headers()
192 self.wfile.write(bytes(response, 'utf-8'))
193
194
195class XMLRPCProxyServer(object):
196 """ not a real working server, but a stub for a proxy server connection
197
198 """
199 def __init__(self, host, port, use_builtin_types=True):
200 self.host = host
201 self.port = port
202 self._idlefuns = {}
203
204 def addcooker(self, cooker):
205 self.cooker = cooker
206
207 def register_idle_function(self, function, data):
208 """Register a function to be called while the server is idle"""
209 assert hasattr(function, '__call__')
210 self._idlefuns[function] = data
211
212
213class XMLRPCServer(SimpleXMLRPCServer):
214 # remove this when you're done with debugging
215 # allow_reuse_address = True
216
217 def __init__(self, interface, single_use=False, idle_timeout=0):
218 """
219 Constructor
220 """
221 self._idlefuns = {}
222 self.single_use = single_use
223 # Use auto port configuration
224 if (interface[1] == -1):
225 interface = (interface[0], 0)
226 SimpleXMLRPCServer.__init__(self, interface,
227 requestHandler=BitBakeXMLRPCRequestHandler,
228 logRequests=False, allow_none=True)
229 self.host, self.port = self.socket.getsockname()
230 self.connection_token = None
231 #self.register_introspection_functions()
232 self.commands = BitBakeServerCommands(self)
233 self.autoregister_all_functions(self.commands, "")
234 self.interface = interface
235 self.time = time.time()
236 self.idle_timeout = idle_timeout
237 if idle_timeout:
238 self.register_idle_function(self.handle_idle_timeout, self)
239 self.heartbeat_seconds = 1 # default, BB_HEARTBEAT_EVENT will be checked once we have a datastore.
240 self.next_heartbeat = time.time()
241
242 def addcooker(self, cooker):
243 self.cooker = cooker
244 self.commands.cooker = cooker
245
246 def autoregister_all_functions(self, context, prefix):
247 """
248 Convenience method for registering all functions in the scope
249 of this class that start with a common prefix
250 """
251 methodlist = inspect.getmembers(context, inspect.ismethod)
252 for name, method in methodlist:
253 if name.startswith(prefix):
254 self.register_function(method, name[len(prefix):])
255
256 def handle_idle_timeout(self, server, data, abort):
257 if not abort:
258 if time.time() - server.time > server.idle_timeout:
259 server.quit = True
260 print("Server idle timeout expired")
261 return []
262
263 def serve_forever(self):
264 heartbeat_event = self.cooker.data.getVar('BB_HEARTBEAT_EVENT')
265 if heartbeat_event:
266 try:
267 self.heartbeat_seconds = float(heartbeat_event)
268 except:
269 # Throwing an exception here causes bitbake to hang.
270 # Just warn about the invalid setting and continue
271 bb.warn('Ignoring invalid BB_HEARTBEAT_EVENT=%s, must be a float specifying seconds.' % heartbeat_event)
272
273 # Start the actual XMLRPC server
274 bb.cooker.server_main(self.cooker, self._serve_forever)
275
276 def _serve_forever(self):
277 """
278 Serve Requests. Overloaded to honor a quit command
279 """
280 self.quit = False
281 while not self.quit:
282 fds = [self]
283 nextsleep = 0.1
284 for function, data in list(self._idlefuns.items()):
285 retval = None
286 try:
287 retval = function(self, data, False)
288 if retval is False:
289 del self._idlefuns[function]
290 elif retval is True:
291 nextsleep = 0
292 elif isinstance(retval, float):
293 if (retval < nextsleep):
294 nextsleep = retval
295 else:
296 fds = fds + retval
297 except SystemExit:
298 raise
299 except:
300 import traceback
301 traceback.print_exc()
302 if retval == None:
303 # the function execute failed; delete it
304 del self._idlefuns[function]
305 pass
306
307 socktimeout = self.socket.gettimeout() or nextsleep
308 socktimeout = min(socktimeout, nextsleep)
309 # Mirror what BaseServer handle_request would do
310 try:
311 fd_sets = select.select(fds, [], [], socktimeout)
312 if fd_sets[0] and self in fd_sets[0]:
313 if self.idle_timeout:
314 self.time = time.time()
315 self._handle_request_noblock()
316 except IOError:
317 # we ignore interrupted calls
318 pass
319
320 # Create new heartbeat event?
321 now = time.time()
322 if now >= self.next_heartbeat:
323 # We might have missed heartbeats. Just trigger once in
324 # that case and continue after the usual delay.
325 self.next_heartbeat += self.heartbeat_seconds
326 if self.next_heartbeat <= now:
327 self.next_heartbeat = now + self.heartbeat_seconds
328 heartbeat = bb.event.HeartbeatEvent(now)
329 bb.event.fire(heartbeat, self.cooker.data)
330 if nextsleep and now + nextsleep > self.next_heartbeat:
331 # Shorten timeout so that we we wake up in time for
332 # the heartbeat.
333 nextsleep = self.next_heartbeat - now
334
335 # Tell idle functions we're exiting
336 for function, data in list(self._idlefuns.items()):
337 try:
338 retval = function(self, data, True)
339 except:
340 pass
341 self.server_close()
342 return
343
344 def set_connection_token(self, token):
345 self.connection_token = token
346
347 def register_idle_function(self, function, data):
348 """Register a function to be called while the server is idle"""
349 assert hasattr(function, '__call__')
350 self._idlefuns[function] = data
351
352
353class BitBakeXMLRPCServerConnection(object):
354 def __init__(self, serverImpl, clientinfo=("localhost", 0), observer_only = False, featureset = None):
355 self.connection, self.transport = _create_server(serverImpl.host, serverImpl.port)
356 self.clientinfo = clientinfo
357 self.serverImpl = serverImpl
358 self.observer_only = observer_only
359 if featureset:
360 self.featureset = featureset
361 else:
362 self.featureset = []
363
364 def connect(self, token = None):
365 if token is None:
366 if self.observer_only:
367 token = "observer"
368 else:
369 token = self.connection.addClient()
370
371 if token is None:
372 return None
373
374 self.transport.set_connection_token(token)
375 return self
376
377 def setupEventQueue(self):
378 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo)
379 for event in bb.event.ui_queue:
380 self.events.queue_event(event)
381
382 _, error = self.connection.runCommand(["setFeatures", self.featureset])
383 if error:
384 # disconnect the client, we can't make the setFeature work
385 self.connection.removeClient()
386 # no need to log it here, the error shall be sent to the client
387 raise BaseException(error)
388
389 def removeClient(self):
390 if not self.observer_only:
391 self.connection.removeClient()
392
393 def terminate(self):
394 # Don't wait for server indefinitely
395 import socket
396 socket.setdefaulttimeout(2)
397 try:
398 self.events.system_quit()
399 except:
400 pass
401 try:
402 self.connection.removeClient()
403 except:
404 pass
405
406class BitBakeServer(object):
407 def initServer(self, interface = ("localhost", 0),
408 single_use = False, idle_timeout=0):
409 self.interface = interface
410 self.serverImpl = XMLRPCServer(interface, single_use, idle_timeout)
411
412 def detach(self):
413 daemonize.createDaemon(self.serverImpl.serve_forever, "bitbake-cookerdaemon.log")
414 del self.cooker
415
416 def establishConnection(self, featureset):
417 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, self.interface, False, featureset)
418 return self.connection.connect()
419
420 def set_connection_token(self, token):
421 self.connection.transport.set_connection_token(token)
422
423 def addcooker(self, cooker):
424 self.cooker = cooker
425 self.serverImpl.addcooker(cooker)
426
427 def getServerIdleCB(self):
428 return self.serverImpl.register_idle_function
429
430 def saveConnectionDetails(self):
431 return
432
433 def endSession(self):
434 self.connection.terminate()
435
436class BitBakeXMLRPCClient(object):
437
438 def __init__(self, observer_only = False, token = None):
439 self.token = token
440
441 self.observer_only = observer_only
442 # if we need extra caches, just tell the server to load them all
443 pass
444
445 def saveConnectionDetails(self, remote):
446 self.remote = remote
447
448 def establishConnection(self, featureset):
449 # The format of "remote" must be "server:port"
450 try:
451 [host, port] = self.remote.split(":")
452 port = int(port)
453 except Exception as e:
454 bb.warn("Failed to read remote definition (%s)" % str(e))
455 raise e
456
457 # We need our IP for the server connection. We get the IP
458 # by trying to connect with the server
459 try:
460 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
461 s.connect((host, port))
462 ip = s.getsockname()[0]
463 s.close()
464 except Exception as e:
465 bb.warn("Could not create socket for %s:%s (%s)" % (host, port, str(e)))
466 raise e
467 try:
468 self.serverImpl = XMLRPCProxyServer(host, port, use_builtin_types=True)
469 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, (ip, 0), self.observer_only, featureset)
470 return self.connection.connect(self.token)
471 except Exception as e:
472 bb.warn("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
473 raise e
474
475 def endSession(self):
476 self.connection.removeClient()
477
478 def initServer(self):
479 self.serverImpl = None
480 self.connection = None
481 return
482
483 def addcooker(self, cooker):
484 self.cooker = cooker
485 self.serverImpl.addcooker(cooker)
486
487 def getServerIdleCB(self):
488 return self.serverImpl.register_idle_function
489
490 def detach(self):
491 return
492