summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/server
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
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')
-rw-r--r--bitbake/lib/bb/server/__init__.py6
-rw-r--r--bitbake/lib/bb/server/process.py596
-rw-r--r--bitbake/lib/bb/server/xmlrpc.py492
-rw-r--r--bitbake/lib/bb/server/xmlrpcclient.py154
-rw-r--r--bitbake/lib/bb/server/xmlrpcserver.py158
5 files changed, 705 insertions, 701 deletions
diff --git a/bitbake/lib/bb/server/__init__.py b/bitbake/lib/bb/server/__init__.py
index 345691e40f..5a3fba968f 100644
--- a/bitbake/lib/bb/server/__init__.py
+++ b/bitbake/lib/bb/server/__init__.py
@@ -18,10 +18,4 @@
18# with this program; if not, write to the Free Software Foundation, Inc., 18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 20
21""" Base code for Bitbake server process
22
23Have a common base for that all Bitbake server classes ensures a consistent
24approach to the interface, and minimize risks associated with code duplication.
25
26"""
27 21
diff --git a/bitbake/lib/bb/server/process.py b/bitbake/lib/bb/server/process.py
index 48da7fe46c..6edb0213ad 100644
--- a/bitbake/lib/bb/server/process.py
+++ b/bitbake/lib/bb/server/process.py
@@ -22,128 +22,205 @@
22 22
23import bb 23import bb
24import bb.event 24import bb.event
25import itertools
26import logging 25import logging
27import multiprocessing 26import multiprocessing
27import threading
28import array
28import os 29import os
29import signal
30import sys 30import sys
31import time 31import time
32import select 32import select
33from queue import Empty 33import socket
34from multiprocessing import Event, Process, util, Queue, Pipe, queues, Manager 34import subprocess
35import errno
36import bb.server.xmlrpcserver
37from bb import daemonize
38from multiprocessing import queues
35 39
36logger = logging.getLogger('BitBake') 40logger = logging.getLogger('BitBake')
37 41
38class ServerCommunicator(): 42class ProcessTimeout(SystemExit):
39 def __init__(self, connection, event_handle, server): 43 pass
40 self.connection = connection
41 self.event_handle = event_handle
42 self.server = server
43
44 def runCommand(self, command):
45 # @todo try/except
46 self.connection.send(command)
47
48 if not self.server.is_alive():
49 raise SystemExit
50
51 while True:
52 # don't let the user ctrl-c while we're waiting for a response
53 try:
54 for idx in range(0,4): # 0, 1, 2, 3
55 if self.connection.poll(5):
56 return self.connection.recv()
57 else:
58 bb.warn("Timeout while attempting to communicate with bitbake server")
59 bb.fatal("Gave up; Too many tries: timeout while attempting to communicate with bitbake server")
60 except KeyboardInterrupt:
61 pass
62
63 def getEventHandle(self):
64 handle, error = self.runCommand(["getUIHandlerNum"])
65 if error:
66 logger.error("Unable to get UI Handler Number: %s" % error)
67 raise BaseException(error)
68 44
69 return handle 45class ProcessServer(multiprocessing.Process):
70
71class EventAdapter():
72 """
73 Adapter to wrap our event queue since the caller (bb.event) expects to
74 call a send() method, but our actual queue only has put()
75 """
76 def __init__(self, queue):
77 self.queue = queue
78
79 def send(self, event):
80 try:
81 self.queue.put(event)
82 except Exception as err:
83 print("EventAdapter puked: %s" % str(err))
84
85
86class ProcessServer(Process):
87 profile_filename = "profile.log" 46 profile_filename = "profile.log"
88 profile_processed_filename = "profile.log.processed" 47 profile_processed_filename = "profile.log.processed"
89 48
90 def __init__(self, command_channel, event_queue, featurelist): 49 def __init__(self, lock, sock, sockname):
91 self._idlefuns = {} 50 multiprocessing.Process.__init__(self)
92 Process.__init__(self) 51 self.command_channel = False
93 self.command_channel = command_channel 52 self.command_channel_reply = False
94 self.event_queue = event_queue
95 self.event = EventAdapter(event_queue)
96 self.featurelist = featurelist
97 self.quit = False 53 self.quit = False
98 self.heartbeat_seconds = 1 # default, BB_HEARTBEAT_EVENT will be checked once we have a datastore. 54 self.heartbeat_seconds = 1 # default, BB_HEARTBEAT_EVENT will be checked once we have a datastore.
99 self.next_heartbeat = time.time() 55 self.next_heartbeat = time.time()
100 56
101 self.quitin, self.quitout = Pipe() 57 self.event_handle = None
102 self.event_handle = multiprocessing.Value("i") 58 self.haveui = False
59 self.lastui = False
60 self.xmlrpc = False
61
62 self._idlefuns = {}
63
64 self.bitbake_lock = lock
65 self.sock = sock
66 self.sockname = sockname
67
68 def register_idle_function(self, function, data):
69 """Register a function to be called while the server is idle"""
70 assert hasattr(function, '__call__')
71 self._idlefuns[function] = data
103 72
104 def run(self): 73 def run(self):
105 for event in bb.event.ui_queue: 74
106 self.event_queue.put(event) 75 if self.xmlrpcinterface[0]:
107 self.event_handle.value = bb.event.register_UIHhandler(self, True) 76 self.xmlrpc = bb.server.xmlrpcserver.BitBakeXMLRPCServer(self.xmlrpcinterface, self.cooker, self)
77
78 print("Bitbake XMLRPC server address: %s, server port: %s" % (self.xmlrpc.host, self.xmlrpc.port))
108 79
109 heartbeat_event = self.cooker.data.getVar('BB_HEARTBEAT_EVENT') 80 heartbeat_event = self.cooker.data.getVar('BB_HEARTBEAT_EVENT')
110 if heartbeat_event: 81 if heartbeat_event:
111 try: 82 try:
112 self.heartbeat_seconds = float(heartbeat_event) 83 self.heartbeat_seconds = float(heartbeat_event)
113 except: 84 except:
114 # Throwing an exception here causes bitbake to hang.
115 # Just warn about the invalid setting and continue
116 bb.warn('Ignoring invalid BB_HEARTBEAT_EVENT=%s, must be a float specifying seconds.' % heartbeat_event) 85 bb.warn('Ignoring invalid BB_HEARTBEAT_EVENT=%s, must be a float specifying seconds.' % heartbeat_event)
86
87 self.timeout = self.server_timeout or self.cooker.data.getVar('BB_SERVER_TIMEOUT')
88 try:
89 if self.timeout:
90 self.timeout = float(self.timeout)
91 except:
92 bb.warn('Ignoring invalid BB_SERVER_TIMEOUT=%s, must be a float specifying seconds.' % self.timeout)
93
94
95 try:
96 self.bitbake_lock.seek(0)
97 self.bitbake_lock.truncate()
98 if self.xmlrpcinterface[0]:
99 self.bitbake_lock.write("%s %s:%s\n" % (os.getpid(), configuration.interface[0], configuration.interface[1]))
100 else:
101 self.bitbake_lock.write("%s\n" % (os.getpid()))
102 self.bitbake_lock.flush()
103 except:
104 pass
105
117 bb.cooker.server_main(self.cooker, self.main) 106 bb.cooker.server_main(self.cooker, self.main)
118 107
119 def main(self): 108 def main(self):
120 # Ignore SIGINT within the server, as all SIGINT handling is done by
121 # the UI and communicated to us
122 self.quitin.close()
123 signal.signal(signal.SIGINT, signal.SIG_IGN)
124 bb.utils.set_process_name("Cooker") 109 bb.utils.set_process_name("Cooker")
110
111 ready = []
112
113 self.controllersock = False
114 fds = [self.sock]
115 if self.xmlrpc:
116 fds.append(self.xmlrpc)
125 while not self.quit: 117 while not self.quit:
126 try: 118 if self.command_channel in ready:
127 if self.command_channel.poll(): 119 command = self.command_channel.get()
128 command = self.command_channel.recv() 120 if command[0] == "terminateServer":
129 self.runCommand(command)
130 if self.quitout.poll():
131 self.quitout.recv()
132 self.quit = True 121 self.quit = True
133 try: 122 continue
134 self.runCommand(["stateForceShutdown"]) 123 try:
135 except: 124 print("Running command %s" % command)
136 pass 125 self.command_channel_reply.send(self.cooker.command.runCommand(command))
137 126 except Exception as e:
138 self.idle_commands(.1, [self.command_channel, self.quitout]) 127 logger.exception('Exception in server main event loop running command %s (%s)' % (command, str(e)))
139 except Exception: 128
140 logger.exception('Running command %s', command) 129 if self.xmlrpc in ready:
130 self.xmlrpc.handle_requests()
131 if self.sock in ready:
132 self.controllersock, address = self.sock.accept()
133 if self.haveui:
134 print("Dropping connection attempt as we have a UI %s" % (str(ready)))
135 self.controllersock.close()
136 else:
137 print("Accepting %s" % (str(ready)))
138 fds.append(self.controllersock)
139 if self.controllersock in ready:
140 try:
141 print("Connecting Client")
142 ui_fds = recvfds(self.controllersock, 3)
143
144 # Where to write events to
145 writer = ConnectionWriter(ui_fds[0])
146 self.event_handle = bb.event.register_UIHhandler(writer, True)
147 self.event_writer = writer
148
149 # Where to read commands from
150 reader = ConnectionReader(ui_fds[1])
151 fds.append(reader)
152 self.command_channel = reader
153
154 # Where to send command return values to
155 writer = ConnectionWriter(ui_fds[2])
156 self.command_channel_reply = writer
157
158 self.haveui = True
159
160 except EOFError:
161 print("Disconnecting Client")
162 fds.remove(self.controllersock)
163 fds.remove(self.command_channel)
164 bb.event.unregister_UIHhandler(self.event_handle, True)
165 self.command_channel_reply.writer.close()
166 self.event_writer.writer.close()
167 del self.event_writer
168 self.controllersock.close()
169 self.haveui = False
170 self.lastui = time.time()
171 self.cooker.clientComplete()
172 if self.timeout is None:
173 print("No timeout, exiting.")
174 self.quit = True
175 if not self.haveui and self.lastui and self.timeout and (self.lastui + self.timeout) < time.time():
176 print("Server timeout, exiting.")
177 self.quit = True
141 178
142 self.event_queue.close() 179 ready = self.idle_commands(.1, fds)
143 bb.event.unregister_UIHhandler(self.event_handle.value, True) 180
144 self.command_channel.close() 181 print("Exiting")
145 self.cooker.shutdown(True) 182 try:
146 self.quitout.close() 183 self.cooker.shutdown(True)
184 except:
185 pass
186
187 # Remove the socket file so we don't get any more connections to avoid races
188 os.unlink(self.sockname)
189 self.sock.close()
190
191 # Finally release the lockfile but warn about other processes holding it open
192 lock = self.bitbake_lock
193 lockfile = lock.name
194 lock.close()
195 lock = None
196
197 while not lock:
198 with bb.utils.timeout(3):
199 lock = bb.utils.lockfile(lockfile, shared=False, retry=False, block=True)
200 if not lock:
201 # Some systems may not have lsof available
202 procs = None
203 try:
204 procs = subprocess.check_output(["lsof", '-w', lockfile], stderr=subprocess.STDOUT)
205 except OSError as e:
206 if e.errno != errno.ENOENT:
207 raise
208 if procs is None:
209 # Fall back to fuser if lsof is unavailable
210 try:
211 procs = subprocess.check_output(["fuser", '-v', lockfile], stderr=subprocess.STDOUT)
212 except OSError as e:
213 if e.errno != errno.ENOENT:
214 raise
215
216 msg = "Delaying shutdown due to active processes which appear to be holding bitbake.lock"
217 if procs:
218 msg += ":\n%s" % str(procs)
219 print(msg)
220 return
221 # We hold the lock so we can remove the file (hide stale pid data)
222 bb.utils.remove(lockfile)
223 bb.utils.unlockfile(lock)
147 224
148 def idle_commands(self, delay, fds=None): 225 def idle_commands(self, delay, fds=None):
149 nextsleep = delay 226 nextsleep = delay
@@ -189,140 +266,253 @@ class ProcessServer(Process):
189 nextsleep = self.next_heartbeat - now 266 nextsleep = self.next_heartbeat - now
190 267
191 if nextsleep is not None: 268 if nextsleep is not None:
269 if self.xmlrpc:
270 nextsleep = self.xmlrpc.get_timeout(nextsleep)
192 try: 271 try:
193 select.select(fds,[],[],nextsleep) 272 return select.select(fds,[],[],nextsleep)[0]
194 except InterruptedError: 273 except InterruptedError:
195 # ignore EINTR error, nextsleep only used for wait 274 # Ignore EINTR
196 # certain time 275 return []
197 pass 276 else:
277 return []
278
279
280class ServerCommunicator():
281 def __init__(self, connection, recv):
282 self.connection = connection
283 self.recv = recv
198 284
199 def runCommand(self, command): 285 def runCommand(self, command):
200 """
201 Run a cooker command on the server
202 """
203 self.command_channel.send(self.cooker.command.runCommand(command))
204 286
205 def stop(self): 287 self.connection.send(command)
206 self.quitin.send("quit") 288 while True:
207 self.quitin.close() 289 # don't let the user ctrl-c while we're waiting for a response
290 try:
291 for idx in range(0,4): # 0, 1, 2, 3
292 if self.recv.poll(1):
293 return self.recv.get()
294 else:
295 bb.warn("Timeout while attempting to communicate with bitbake server")
296 raise ProcessTimeout("Gave up; Too many tries: timeout while attempting to communicate with bitbake server")
297 except KeyboardInterrupt:
298 pass
299
300 def updateFeatureSet(self, featureset):
301 _, error = self.runCommand(["setFeatures", featureset])
302 if error:
303 logger.error("Unable to set the cooker to the correct featureset: %s" % error)
304 raise BaseException(error)
305
306 def getEventHandle(self):
307 handle, error = self.runCommand(["getUIHandlerNum"])
308 if error:
309 logger.error("Unable to get UI Handler Number: %s" % error)
310 raise BaseException(error)
208 311
209 def addcooker(self, cooker): 312 return handle
210 self.cooker = cooker
211 313
212 def register_idle_function(self, function, data): 314 def terminateServer(self):
213 """Register a function to be called while the server is idle""" 315 self.connection.send(['terminateServer'])
214 assert hasattr(function, '__call__') 316 return
215 self._idlefuns[function] = data
216 317
217class BitBakeProcessServerConnection(object): 318class BitBakeProcessServerConnection(object):
218 def __init__(self, serverImpl, ui_channel, event_queue): 319 def __init__(self, ui_channel, recv, eq):
219 self.procserver = serverImpl 320 self.connection = ServerCommunicator(ui_channel, recv)
220 self.ui_channel = ui_channel 321 self.events = eq
221 self.event_queue = event_queue
222 self.connection = ServerCommunicator(self.ui_channel, self.procserver.event_handle, self.procserver)
223 self.events = self.event_queue
224 self.terminated = False
225
226 def sigterm_terminate(self):
227 bb.error("UI received SIGTERM")
228 self.terminate()
229 322
230 def terminate(self): 323 def terminate(self):
231 if self.terminated: 324 self.socket_connection.close()
232 return 325 return
233 self.terminated = True 326
234 def flushevents(): 327class BitBakeServer(object):
235 while True: 328 def __init__(self, lock, sockname, configuration, featureset):
236 try: 329
237 event = self.event_queue.get(block=False) 330 self.configuration = configuration
238 except (Empty, IOError): 331 self.featureset = featureset
239 break 332 self.sockname = sockname
240 if isinstance(event, logging.LogRecord): 333 self.bitbake_lock = lock
241 logger.handle(event) 334
242 335 # Create server control socket
243 self.procserver.stop() 336 if os.path.exists(sockname):
244 337 os.unlink(sockname)
245 while self.procserver.is_alive(): 338
246 flushevents() 339 self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
247 self.procserver.join(0.1) 340 # AF_UNIX has path length issues so chdir here to workaround
248 341 cwd = os.getcwd()
249 self.ui_channel.close()
250 self.event_queue.close()
251 self.event_queue.setexit()
252 # XXX: Call explicity close in _writer to avoid
253 # fd leakage because isn't called on Queue.close()
254 self.event_queue._writer.close()
255
256 def setupEventQueue(self):
257 pass
258
259# Wrap Queue to provide API which isn't server implementation specific
260class ProcessEventQueue(multiprocessing.queues.Queue):
261 def __init__(self, maxsize):
262 multiprocessing.queues.Queue.__init__(self, maxsize, ctx=multiprocessing.get_context())
263 self.exit = False
264 bb.utils.set_process_name("ProcessEQueue")
265
266 def setexit(self):
267 self.exit = True
268
269 def waitEvent(self, timeout):
270 if self.exit:
271 return self.getEvent()
272 try: 342 try:
273 if not self.server.is_alive(): 343 os.chdir(os.path.dirname(sockname))
274 return self.getEvent() 344 self.sock.bind(os.path.basename(sockname))
275 if timeout == 0: 345 finally:
276 return self.get(False) 346 os.chdir(cwd)
277 return self.get(True, timeout) 347 self.sock.listen(1)
278 except Empty: 348
279 return None 349 os.set_inheritable(self.sock.fileno(), True)
350 bb.daemonize.createDaemon(self._startServer, "bitbake-cookerdaemon.log")
351 self.sock.close()
352 self.bitbake_lock.close()
353
354 def _startServer(self):
355 server = ProcessServer(self.bitbake_lock, self.sock, self.sockname)
356 self.configuration.setServerRegIdleCallback(server.register_idle_function)
357
358 # Copy prefile and postfile to _server variants
359 for param in ('prefile', 'postfile'):
360 value = getattr(self.configuration, param)
361 if value:
362 setattr(self.configuration, "%s_server" % param, value)
363
364 self.cooker = bb.cooker.BBCooker(self.configuration, self.featureset)
365 server.cooker = self.cooker
366 server.server_timeout = self.configuration.server_timeout
367 server.xmlrpcinterface = self.configuration.xmlrpcinterface
368 print("Started bitbake server pid %d" % os.getpid())
369 server.start()
370
371def connectProcessServer(sockname, featureset):
372 # Connect to socket
373 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
374 # AF_UNIX has path length issues so chdir here to workaround
375 cwd = os.getcwd()
376
377 try:
378 os.chdir(os.path.dirname(sockname))
379 sock.connect(os.path.basename(sockname))
380 finally:
381 os.chdir(cwd)
382
383 try:
384 # Send an fd for the remote to write events to
385 readfd, writefd = os.pipe()
386 eq = BBUIEventQueue(readfd)
387 # Send an fd for the remote to recieve commands from
388 readfd1, writefd1 = os.pipe()
389 command_chan = ConnectionWriter(writefd1)
390 # Send an fd for the remote to write commands results to
391 readfd2, writefd2 = os.pipe()
392 command_chan_recv = ConnectionReader(readfd2)
393
394 sendfds(sock, [writefd, readfd1, writefd2])
395
396 server_connection = BitBakeProcessServerConnection(command_chan, command_chan_recv, eq)
397
398 server_connection.connection.updateFeatureSet(featureset)
399
400 # Save sock so it doesn't get gc'd for the life of our connection
401 server_connection.socket_connection = sock
402 except:
403 sock.close()
404 raise
405
406 return server_connection
407
408def sendfds(sock, fds):
409 '''Send an array of fds over an AF_UNIX socket.'''
410 fds = array.array('i', fds)
411 msg = bytes([len(fds) % 256])
412 sock.sendmsg([msg], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)])
413
414def recvfds(sock, size):
415 '''Receive an array of fds over an AF_UNIX socket.'''
416 a = array.array('i')
417 bytes_size = a.itemsize * size
418 msg, ancdata, flags, addr = sock.recvmsg(1, socket.CMSG_LEN(bytes_size))
419 if not msg and not ancdata:
420 raise EOFError
421 try:
422 if len(ancdata) != 1:
423 raise RuntimeError('received %d items of ancdata' %
424 len(ancdata))
425 cmsg_level, cmsg_type, cmsg_data = ancdata[0]
426 if (cmsg_level == socket.SOL_SOCKET and
427 cmsg_type == socket.SCM_RIGHTS):
428 if len(cmsg_data) % a.itemsize != 0:
429 raise ValueError
430 a.frombytes(cmsg_data)
431 assert len(a) % 256 == msg[0]
432 return list(a)
433 except (ValueError, IndexError):
434 pass
435 raise RuntimeError('Invalid data received')
436
437class BBUIEventQueue:
438 def __init__(self, readfd):
439
440 self.eventQueue = []
441 self.eventQueueLock = threading.Lock()
442 self.eventQueueNotify = threading.Event()
443
444 self.reader = ConnectionReader(readfd)
445
446 self.t = threading.Thread()
447 self.t.setDaemon(True)
448 self.t.run = self.startCallbackHandler
449 self.t.start()
280 450
281 def getEvent(self): 451 def getEvent(self):
282 try: 452 self.eventQueueLock.acquire()
283 if not self.server.is_alive(): 453
284 self.setexit() 454 if len(self.eventQueue) == 0:
285 return self.get(False) 455 self.eventQueueLock.release()
286 except Empty:
287 if self.exit:
288 sys.exit(1)
289 return None 456 return None
290 457
291class BitBakeServer(object): 458 item = self.eventQueue.pop(0)
292 def initServer(self, single_use=True):
293 # establish communication channels. We use bidirectional pipes for
294 # ui <--> server command/response pairs
295 # and a queue for server -> ui event notifications
296 #
297 self.ui_channel, self.server_channel = Pipe()
298 self.event_queue = ProcessEventQueue(0)
299 self.serverImpl = ProcessServer(self.server_channel, self.event_queue, None)
300 self.event_queue.server = self.serverImpl
301
302 def detach(self):
303 self.serverImpl.start()
304 return
305 459
306 def establishConnection(self, featureset): 460 if len(self.eventQueue) == 0:
461 self.eventQueueNotify.clear()
307 462
308 self.connection = BitBakeProcessServerConnection(self.serverImpl, self.ui_channel, self.event_queue) 463 self.eventQueueLock.release()
464 return item
309 465
310 _, error = self.connection.connection.runCommand(["setFeatures", featureset]) 466 def waitEvent(self, delay):
311 if error: 467 self.eventQueueNotify.wait(delay)
312 logger.error("Unable to set the cooker to the correct featureset: %s" % error) 468 return self.getEvent()
313 raise BaseException(error)
314 signal.signal(signal.SIGTERM, lambda i, s: self.connection.sigterm_terminate())
315 return self.connection
316 469
317 def addcooker(self, cooker): 470 def queue_event(self, event):
318 self.cooker = cooker 471 self.eventQueueLock.acquire()
319 self.serverImpl.addcooker(cooker) 472 self.eventQueue.append(event)
473 self.eventQueueNotify.set()
474 self.eventQueueLock.release()
320 475
321 def getServerIdleCB(self): 476 def send_event(self, event):
322 return self.serverImpl.register_idle_function 477 self.queue_event(pickle.loads(event))
323 478
324 def saveConnectionDetails(self): 479 def startCallbackHandler(self):
325 return 480 bb.utils.set_process_name("UIEventQueue")
481 while True:
482 self.reader.wait()
483 event = self.reader.get()
484 self.queue_event(event)
485
486class ConnectionReader(object):
487
488 def __init__(self, fd):
489 self.reader = multiprocessing.connection.Connection(fd, writable=False)
490 self.rlock = multiprocessing.Lock()
491
492 def wait(self, timeout=None):
493 return multiprocessing.connection.wait([self.reader], timeout)
494
495 def poll(self, timeout=None):
496 return self.reader.poll(timeout)
497
498 def get(self):
499 with self.rlock:
500 res = self.reader.recv_bytes()
501 return multiprocessing.reduction.ForkingPickler.loads(res)
502
503 def fileno(self):
504 return self.reader.fileno()
505
506class ConnectionWriter(object):
507
508 def __init__(self, fd):
509 self.writer = multiprocessing.connection.Connection(fd, readable=False)
510 self.wlock = multiprocessing.Lock()
511 # Why bb.event needs this I have no idea
512 self.event = self
513
514 def send(self, obj):
515 obj = multiprocessing.reduction.ForkingPickler.dumps(obj)
516 with self.wlock:
517 self.writer.send_bytes(obj)
326 518
327 def endSession(self):
328 self.connection.terminate()
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
diff --git a/bitbake/lib/bb/server/xmlrpcclient.py b/bitbake/lib/bb/server/xmlrpcclient.py
new file mode 100644
index 0000000000..4661a9e5a0
--- /dev/null
+++ b/bitbake/lib/bb/server/xmlrpcclient.py
@@ -0,0 +1,154 @@
1#
2# BitBake XMLRPC Client Interface
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
20import os
21import sys
22
23import socket
24import http.client
25import xmlrpc.client
26
27import bb
28from bb.ui import uievent
29
30class BBTransport(xmlrpc.client.Transport):
31 def __init__(self, timeout):
32 self.timeout = timeout
33 self.connection_token = None
34 xmlrpc.client.Transport.__init__(self)
35
36 # Modified from default to pass timeout to HTTPConnection
37 def make_connection(self, host):
38 #return an existing connection if possible. This allows
39 #HTTP/1.1 keep-alive.
40 if self._connection and host == self._connection[0]:
41 return self._connection[1]
42
43 # create a HTTP connection object from a host descriptor
44 chost, self._extra_headers, x509 = self.get_host_info(host)
45 #store the host argument along with the connection object
46 self._connection = host, http.client.HTTPConnection(chost, timeout=self.timeout)
47 return self._connection[1]
48
49 def set_connection_token(self, token):
50 self.connection_token = token
51
52 def send_content(self, h, body):
53 if self.connection_token:
54 h.putheader("Bitbake-token", self.connection_token)
55 xmlrpc.client.Transport.send_content(self, h, body)
56
57def _create_server(host, port, timeout = 60):
58 t = BBTransport(timeout)
59 s = xmlrpc.client.ServerProxy("http://%s:%d/" % (host, port), transport=t, allow_none=True, use_builtin_types=True)
60 return s, t
61
62def check_connection(remote, timeout):
63 try:
64 host, port = remote.split(":")
65 port = int(port)
66 except Exception as e:
67 bb.warn("Failed to read remote definition (%s)" % str(e))
68 raise e
69
70 server, _transport = _create_server(host, port, timeout)
71 try:
72 ret, err = server.runCommand(['getVariable', 'TOPDIR'])
73 if err or not ret:
74 return False
75 except ConnectionError:
76 return False
77 return True
78
79class BitBakeXMLRPCServerConnection(object):
80 def __init__(self, host, port, clientinfo=("localhost", 0), observer_only = False, featureset = None):
81 self.connection, self.transport = _create_server(host, port)
82 self.clientinfo = clientinfo
83 self.observer_only = observer_only
84 if featureset:
85 self.featureset = featureset
86 else:
87 self.featureset = []
88
89 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo)
90
91 _, error = self.connection.runCommand(["setFeatures", self.featureset])
92 if error:
93 # disconnect the client, we can't make the setFeature work
94 self.connection.removeClient()
95 # no need to log it here, the error shall be sent to the client
96 raise BaseException(error)
97
98 def connect(self, token = None):
99 if token is None:
100 if self.observer_only:
101 token = "observer"
102 else:
103 token = self.connection.addClient()
104
105 if token is None:
106 return None
107
108 self.transport.set_connection_token(token)
109 return self
110
111 def removeClient(self):
112 if not self.observer_only:
113 self.connection.removeClient()
114
115 def terminate(self):
116 # Don't wait for server indefinitely
117 socket.setdefaulttimeout(2)
118 try:
119 self.events.system_quit()
120 except:
121 pass
122 try:
123 self.connection.removeClient()
124 except:
125 pass
126
127def connectXMLRPC(remote, featureset, observer_only = False, token = None):
128 # The format of "remote" must be "server:port"
129 try:
130 [host, port] = remote.split(":")
131 port = int(port)
132 except Exception as e:
133 bb.warn("Failed to parse remote definition %s (%s)" % (remote, str(e)))
134 raise e
135
136 # We need our IP for the server connection. We get the IP
137 # by trying to connect with the server
138 try:
139 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
140 s.connect((host, port))
141 ip = s.getsockname()[0]
142 s.close()
143 except Exception as e:
144 bb.warn("Could not create socket for %s:%s (%s)" % (host, port, str(e)))
145 raise e
146 try:
147 connection = BitBakeXMLRPCServerConnection(host, port, (ip, 0), observer_only, featureset)
148 return connection.connect(token)
149 except Exception as e:
150 bb.warn("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
151 raise e
152
153
154
diff --git a/bitbake/lib/bb/server/xmlrpcserver.py b/bitbake/lib/bb/server/xmlrpcserver.py
new file mode 100644
index 0000000000..875b1282e5
--- /dev/null
+++ b/bitbake/lib/bb/server/xmlrpcserver.py
@@ -0,0 +1,158 @@
1#
2# BitBake XMLRPC Server Interface
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
20import os
21import sys
22
23import hashlib
24import time
25import inspect
26from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
27
28import bb
29
30# This request handler checks if the request has a "Bitbake-token" header
31# field (this comes from the client side) and compares it with its internal
32# "Bitbake-token" field (this comes from the server). If the two are not
33# equal, it is assumed that a client is trying to connect to the server
34# while another client is connected to the server. In this case, a 503 error
35# ("service unavailable") is returned to the client.
36class BitBakeXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
37 def __init__(self, request, client_address, server):
38 self.server = server
39 SimpleXMLRPCRequestHandler.__init__(self, request, client_address, server)
40
41 def do_POST(self):
42 try:
43 remote_token = self.headers["Bitbake-token"]
44 except:
45 remote_token = None
46 if 0 and remote_token != self.server.connection_token and remote_token != "observer":
47 self.report_503()
48 else:
49 if remote_token == "observer":
50 self.server.readonly = True
51 else:
52 self.server.readonly = False
53 SimpleXMLRPCRequestHandler.do_POST(self)
54
55 def report_503(self):
56 self.send_response(503)
57 response = 'No more client allowed'
58 self.send_header("Content-type", "text/plain")
59 self.send_header("Content-length", str(len(response)))
60 self.end_headers()
61 self.wfile.write(bytes(response, 'utf-8'))
62
63class BitBakeXMLRPCServer(SimpleXMLRPCServer):
64 # remove this when you're done with debugging
65 # allow_reuse_address = True
66
67 def __init__(self, interface, cooker, parent):
68 # Use auto port configuration
69 if (interface[1] == -1):
70 interface = (interface[0], 0)
71 SimpleXMLRPCServer.__init__(self, interface,
72 requestHandler=BitBakeXMLRPCRequestHandler,
73 logRequests=False, allow_none=True)
74 self.host, self.port = self.socket.getsockname()
75 self.interface = interface
76
77 self.connection_token = None
78 self.commands = BitBakeXMLRPCServerCommands(self)
79 self.register_functions(self.commands, "")
80
81 self.cooker = cooker
82 self.parent = parent
83
84
85 def register_functions(self, context, prefix):
86 """
87 Convenience method for registering all functions in the scope
88 of this class that start with a common prefix
89 """
90 methodlist = inspect.getmembers(context, inspect.ismethod)
91 for name, method in methodlist:
92 if name.startswith(prefix):
93 self.register_function(method, name[len(prefix):])
94
95 def get_timeout(self, delay):
96 socktimeout = self.socket.gettimeout() or delay
97 return min(socktimeout, delay)
98
99 def handle_requests(self):
100 self._handle_request_noblock()
101
102class BitBakeXMLRPCServerCommands():
103
104 def __init__(self, server):
105 self.server = server
106 self.has_client = False
107
108 def registerEventHandler(self, host, port):
109 """
110 Register a remote UI Event Handler
111 """
112 s, t = bb.server.xmlrpcclient._create_server(host, port)
113
114 # we don't allow connections if the cooker is running
115 if (self.server.cooker.state in [bb.cooker.state.parsing, bb.cooker.state.running]):
116 return None, "Cooker is busy: %s" % bb.cooker.state.get_name(self.server.cooker.state)
117
118 self.event_handle = bb.event.register_UIHhandler(s, True)
119 return self.event_handle, 'OK'
120
121 def unregisterEventHandler(self, handlerNum):
122 """
123 Unregister a remote UI Event Handler
124 """
125 ret = bb.event.unregister_UIHhandler(handlerNum, True)
126 self.event_handle = None
127 return ret
128
129 def runCommand(self, command):
130 """
131 Run a cooker command on the server
132 """
133 return self.server.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.parent.quit = True
143 print("XMLRPC Server triggering exit")
144 return
145
146 def addClient(self):
147 if self.server.parent.haveui:
148 return None
149 token = hashlib.md5(str(time.time()).encode("utf-8")).hexdigest()
150 self.server.connection_token = token
151 self.server.parent.haveui = True
152 return token
153
154 def removeClient(self):
155 if self.server.parent.haveui:
156 self.server.connection_token = None
157 self.server.parent.haveui = False
158