summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/server
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/bb/server')
-rw-r--r--bitbake/lib/bb/server/__init__.py96
-rw-r--r--bitbake/lib/bb/server/process.py236
-rw-r--r--bitbake/lib/bb/server/xmlrpc.py392
3 files changed, 724 insertions, 0 deletions
diff --git a/bitbake/lib/bb/server/__init__.py b/bitbake/lib/bb/server/__init__.py
new file mode 100644
index 0000000000..da5e480740
--- /dev/null
+++ b/bitbake/lib/bb/server/__init__.py
@@ -0,0 +1,96 @@
1#
2# BitBake Base Server Code
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2008 Richard Purdie
6# Copyright (C) 2013 Alexandru Damian
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as
10# published by the Free Software Foundation.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License along
18# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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
28""" BaseImplServer() the base class for all XXServer() implementations.
29
30 These classes contain the actual code that runs the server side, i.e.
31 listens for the commands and executes them. Although these implementations
32 contain all the data of the original bitbake command, i.e the cooker instance,
33 they may well run on a different process or even machine.
34
35"""
36
37class BaseImplServer():
38 def __init__(self):
39 self._idlefuns = {}
40
41 def addcooker(self, cooker):
42 self.cooker = cooker
43
44 def register_idle_function(self, function, data):
45 """Register a function to be called while the server is idle"""
46 assert hasattr(function, '__call__')
47 self._idlefuns[function] = data
48
49
50
51""" BitBakeBaseServerConnection class is the common ancestor to all
52 BitBakeServerConnection classes.
53
54 These classes control the remote server. The only command currently
55 implemented is the terminate() command.
56
57"""
58
59class BitBakeBaseServerConnection():
60 def __init__(self, serverImpl):
61 pass
62
63 def terminate(self):
64 pass
65
66
67""" BitBakeBaseServer class is the common ancestor to all Bitbake servers
68
69 Derive this class in order to implement a BitBakeServer which is the
70 controlling stub for the actual server implementation
71
72"""
73class BitBakeBaseServer(object):
74 def initServer(self):
75 self.serverImpl = None # we ensure a runtime crash if not overloaded
76 self.connection = None
77 return
78
79 def addcooker(self, cooker):
80 self.cooker = cooker
81 self.serverImpl.addcooker(cooker)
82
83 def getServerIdleCB(self):
84 return self.serverImpl.register_idle_function
85
86 def saveConnectionDetails(self):
87 return
88
89 def detach(self):
90 return
91
92 def establishConnection(self, featureset):
93 raise "Must redefine the %s.establishConnection()" % self.__class__.__name__
94
95 def endSession(self):
96 self.connection.terminate()
diff --git a/bitbake/lib/bb/server/process.py b/bitbake/lib/bb/server/process.py
new file mode 100644
index 0000000000..577c2503ac
--- /dev/null
+++ b/bitbake/lib/bb/server/process.py
@@ -0,0 +1,236 @@
1#
2# BitBake Process based server.
3#
4# Copyright (C) 2010 Bob Foerster <robert@erafx.com>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19"""
20 This module implements a multiprocessing.Process based server for bitbake.
21"""
22
23import bb
24import bb.event
25import itertools
26import logging
27import multiprocessing
28import os
29import signal
30import sys
31import time
32import select
33from Queue import Empty
34from multiprocessing import Event, Process, util, Queue, Pipe, queues, Manager
35
36from . import BitBakeBaseServer, BitBakeBaseServerConnection, BaseImplServer
37
38logger = logging.getLogger('BitBake')
39
40class ServerCommunicator():
41 def __init__(self, connection, event_handle):
42 self.connection = connection
43 self.event_handle = event_handle
44
45 def runCommand(self, command):
46 # @todo try/except
47 self.connection.send(command)
48
49 while True:
50 # don't let the user ctrl-c while we're waiting for a response
51 try:
52 if self.connection.poll(20):
53 return self.connection.recv()
54 else:
55 bb.fatal("Timeout while attempting to communicate with bitbake server")
56 except KeyboardInterrupt:
57 pass
58
59 def getEventHandle(self):
60 return self.event_handle.value
61
62class EventAdapter():
63 """
64 Adapter to wrap our event queue since the caller (bb.event) expects to
65 call a send() method, but our actual queue only has put()
66 """
67 def __init__(self, queue):
68 self.queue = queue
69
70 def send(self, event):
71 try:
72 self.queue.put(event)
73 except Exception as err:
74 print("EventAdapter puked: %s" % str(err))
75
76
77class ProcessServer(Process, BaseImplServer):
78 profile_filename = "profile.log"
79 profile_processed_filename = "profile.log.processed"
80
81 def __init__(self, command_channel, event_queue, featurelist):
82 BaseImplServer.__init__(self)
83 Process.__init__(self)
84 self.command_channel = command_channel
85 self.event_queue = event_queue
86 self.event = EventAdapter(event_queue)
87 self.featurelist = featurelist
88 self.quit = False
89
90 self.quitin, self.quitout = Pipe()
91 self.event_handle = multiprocessing.Value("i")
92
93 def run(self):
94 for event in bb.event.ui_queue:
95 self.event_queue.put(event)
96 self.event_handle.value = bb.event.register_UIHhandler(self)
97
98 bb.cooker.server_main(self.cooker, self.main)
99
100 def main(self):
101 # Ignore SIGINT within the server, as all SIGINT handling is done by
102 # the UI and communicated to us
103 self.quitin.close()
104 signal.signal(signal.SIGINT, signal.SIG_IGN)
105 while not self.quit:
106 try:
107 if self.command_channel.poll():
108 command = self.command_channel.recv()
109 self.runCommand(command)
110 if self.quitout.poll():
111 self.quitout.recv()
112 self.quit = True
113
114 self.idle_commands(.1, [self.event_queue._reader, self.command_channel, self.quitout])
115 except Exception:
116 logger.exception('Running command %s', command)
117
118 self.event_queue.close()
119 bb.event.unregister_UIHhandler(self.event_handle.value)
120 self.command_channel.close()
121 self.cooker.shutdown(True)
122
123 def idle_commands(self, delay, fds = []):
124 nextsleep = delay
125
126 for function, data in self._idlefuns.items():
127 try:
128 retval = function(self, data, False)
129 if retval is False:
130 del self._idlefuns[function]
131 nextsleep = None
132 elif retval is True:
133 nextsleep = None
134 elif nextsleep is None:
135 continue
136 else:
137 fds = fds + retval
138 except SystemExit:
139 raise
140 except Exception:
141 logger.exception('Running idle function')
142
143 if nextsleep is not None:
144 select.select(fds,[],[],nextsleep)
145
146 def runCommand(self, command):
147 """
148 Run a cooker command on the server
149 """
150 self.command_channel.send(self.cooker.command.runCommand(command))
151
152 def stop(self):
153 self.quitin.send("quit")
154 self.quitin.close()
155
156class BitBakeProcessServerConnection(BitBakeBaseServerConnection):
157 def __init__(self, serverImpl, ui_channel, event_queue):
158 self.procserver = serverImpl
159 self.ui_channel = ui_channel
160 self.event_queue = event_queue
161 self.connection = ServerCommunicator(self.ui_channel, self.procserver.event_handle)
162 self.events = self.event_queue
163
164 def sigterm_terminate(self):
165 bb.error("UI received SIGTERM")
166 self.terminate()
167
168 def terminate(self):
169 def flushevents():
170 while True:
171 try:
172 event = self.event_queue.get(block=False)
173 except (Empty, IOError):
174 break
175 if isinstance(event, logging.LogRecord):
176 logger.handle(event)
177
178 signal.signal(signal.SIGINT, signal.SIG_IGN)
179 self.procserver.stop()
180
181 while self.procserver.is_alive():
182 flushevents()
183 self.procserver.join(0.1)
184
185 self.ui_channel.close()
186 self.event_queue.close()
187 self.event_queue.setexit()
188
189# Wrap Queue to provide API which isn't server implementation specific
190class ProcessEventQueue(multiprocessing.queues.Queue):
191 def __init__(self, maxsize):
192 multiprocessing.queues.Queue.__init__(self, maxsize)
193 self.exit = False
194
195 def setexit(self):
196 self.exit = True
197
198 def waitEvent(self, timeout):
199 if self.exit:
200 raise KeyboardInterrupt()
201 try:
202 return self.get(True, timeout)
203 except Empty:
204 return None
205
206 def getEvent(self):
207 try:
208 return self.get(False)
209 except Empty:
210 return None
211
212
213class BitBakeServer(BitBakeBaseServer):
214 def initServer(self):
215 # establish communication channels. We use bidirectional pipes for
216 # ui <--> server command/response pairs
217 # and a queue for server -> ui event notifications
218 #
219 self.ui_channel, self.server_channel = Pipe()
220 self.event_queue = ProcessEventQueue(0)
221 self.serverImpl = ProcessServer(self.server_channel, self.event_queue, None)
222
223 def detach(self):
224 self.serverImpl.start()
225 return
226
227 def establishConnection(self, featureset):
228
229 self.connection = BitBakeProcessServerConnection(self.serverImpl, self.ui_channel, self.event_queue)
230
231 _, error = self.connection.connection.runCommand(["setFeatures", featureset])
232 if error:
233 logger.error("Unable to set the cooker to the correct featureset: %s" % error)
234 raise BaseException(error)
235 signal.signal(signal.SIGTERM, lambda i, s: self.connection.sigterm_terminate())
236 return self.connection
diff --git a/bitbake/lib/bb/server/xmlrpc.py b/bitbake/lib/bb/server/xmlrpc.py
new file mode 100644
index 0000000000..5dcaa6c7b0
--- /dev/null
+++ b/bitbake/lib/bb/server/xmlrpc.py
@@ -0,0 +1,392 @@
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 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.Server("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)
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 try:
239 retval = function(self, data, False)
240 if retval is False:
241 del self._idlefuns[function]
242 elif retval is True:
243 nextsleep = 0
244 else:
245 fds = fds + retval
246 except SystemExit:
247 raise
248 except:
249 import traceback
250 traceback.print_exc()
251 pass
252
253 socktimeout = self.socket.gettimeout() or nextsleep
254 socktimeout = min(socktimeout, nextsleep)
255 # Mirror what BaseServer handle_request would do
256 fd_sets = select.select(fds, [], [], socktimeout)
257 if fd_sets[0] and self in fd_sets[0]:
258 self._handle_request_noblock()
259
260 # Tell idle functions we're exiting
261 for function, data in self._idlefuns.items():
262 try:
263 retval = function(self, data, True)
264 except:
265 pass
266 self.server_close()
267 return
268
269 def set_connection_token(self, token):
270 self.connection_token = token
271
272class BitBakeXMLRPCServerConnection(BitBakeBaseServerConnection):
273 def __init__(self, serverImpl, clientinfo=("localhost", 0), observer_only = False, featureset = []):
274 self.connection, self.transport = _create_server(serverImpl.host, serverImpl.port)
275 self.clientinfo = clientinfo
276 self.serverImpl = serverImpl
277 self.observer_only = observer_only
278 self.featureset = featureset
279
280 def connect(self):
281 if not self.observer_only:
282 token = self.connection.addClient()
283 else:
284 token = "observer"
285 if token is None:
286 return None
287 self.transport.set_connection_token(token)
288
289 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo)
290 for event in bb.event.ui_queue:
291 self.events.queue_event(event)
292
293 _, error = self.connection.runCommand(["setFeatures", self.featureset])
294 if error:
295 # no need to log it here, the error shall be sent to the client
296 raise BaseException(error)
297
298 return self
299
300 def removeClient(self):
301 if not self.observer_only:
302 self.connection.removeClient()
303
304 def terminate(self):
305 # Don't wait for server indefinitely
306 import socket
307 socket.setdefaulttimeout(2)
308 try:
309 self.events.system_quit()
310 except:
311 pass
312 try:
313 self.connection.removeClient()
314 except:
315 pass
316
317class BitBakeServer(BitBakeBaseServer):
318 def initServer(self, interface = ("localhost", 0)):
319 self.interface = interface
320 self.serverImpl = XMLRPCServer(interface)
321
322 def detach(self):
323 daemonize.createDaemon(self.serverImpl.serve_forever, "bitbake-cookerdaemon.log")
324 del self.cooker
325
326 def establishConnection(self, featureset):
327 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, self.interface, False, featureset)
328 return self.connection.connect()
329
330 def set_connection_token(self, token):
331 self.connection.transport.set_connection_token(token)
332
333class BitBakeXMLRPCClient(BitBakeBaseServer):
334
335 def __init__(self, observer_only = False):
336 self.observer_only = observer_only
337 # if we need extra caches, just tell the server to load them all
338 pass
339
340 def saveConnectionDetails(self, remote):
341 self.remote = remote
342
343 def saveConnectionConfigParams(self, configParams):
344 self.configParams = configParams
345
346 def establishConnection(self, featureset):
347 # The format of "remote" must be "server:port"
348 try:
349 [host, port] = self.remote.split(":")
350 port = int(port)
351 except Exception as e:
352 bb.fatal("Failed to read remote definition (%s)" % str(e))
353
354 # use automatic port if port set to -1, meaning read it from
355 # the bitbake.lock file
356 if port == -1:
357 lock_location = "%s/bitbake.lock" % self.configParams.environment.get('BUILDDIR')
358 lock = bb.utils.lockfile(lock_location, False, False)
359 if lock:
360 # This means there is no server running which we can
361 # connect to on the local system.
362 bb.utils.unlockfile(lock)
363 return None
364
365 try:
366 lf = open(lock_location, 'r')
367 remotedef = lf.readline()
368 [host, port] = remotedef.split(":")
369 port = int(port)
370 lf.close()
371 self.remote = remotedef
372 except Exception as e:
373 bb.fatal("Failed to read bitbake.lock (%s)" % str(e))
374
375 # We need our IP for the server connection. We get the IP
376 # by trying to connect with the server
377 try:
378 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
379 s.connect((host, port))
380 ip = s.getsockname()[0]
381 s.close()
382 except Exception as e:
383 bb.fatal("Could not create socket for %s:%s (%s)" % (host, port, str(e)))
384 try:
385 self.serverImpl = XMLRPCProxyServer(host, port)
386 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, (ip, 0), self.observer_only, featureset)
387 return self.connection.connect()
388 except Exception as e:
389 bb.fatal("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
390
391 def endSession(self):
392 self.connection.removeClient()