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.py223
-rw-r--r--bitbake/lib/bb/server/xmlrpc.py365
3 files changed, 684 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..aa072020af
--- /dev/null
+++ b/bitbake/lib/bb/server/process.py
@@ -0,0 +1,223 @@
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, args=(featurelist))
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.keep_running = Event()
91 self.keep_running.set()
92 self.event_handle = multiprocessing.Value("i")
93
94 def run(self):
95 for event in bb.event.ui_queue:
96 self.event_queue.put(event)
97 self.event_handle.value = bb.event.register_UIHhandler(self)
98
99 # process any feature changes based on what UI requested
100 original_featureset = list(self.cooker.featureset)
101 while len(self.featurelist)> 0:
102 self.cooker.featureset.setFeature(self.featurelist.pop())
103 if (original_featureset != list(self.cooker.featureset)):
104 self.cooker.reset()
105
106 bb.cooker.server_main(self.cooker, self.main)
107
108 def main(self):
109 # Ignore SIGINT within the server, as all SIGINT handling is done by
110 # the UI and communicated to us
111 signal.signal(signal.SIGINT, signal.SIG_IGN)
112 while self.keep_running.is_set():
113 try:
114 if self.command_channel.poll():
115 command = self.command_channel.recv()
116 self.runCommand(command)
117
118 self.idle_commands(.1, [self.event_queue._reader, self.command_channel])
119 except Exception:
120 logger.exception('Running command %s', command)
121
122 self.event_queue.close()
123 bb.event.unregister_UIHhandler(self.event_handle.value)
124 self.command_channel.close()
125 self.cooker.shutdown(True)
126 self.idle_commands(.1)
127
128 def idle_commands(self, delay, fds = []):
129 nextsleep = delay
130
131 for function, data in self._idlefuns.items():
132 try:
133 retval = function(self, data, False)
134 if retval is False:
135 del self._idlefuns[function]
136 elif retval is True:
137 nextsleep = None
138 elif nextsleep is None:
139 continue
140 else:
141 fds = fds + retval
142 except SystemExit:
143 raise
144 except Exception:
145 logger.exception('Running idle function')
146
147 if nextsleep is not None:
148 select.select(fds,[],[],nextsleep)
149
150 def runCommand(self, command):
151 """
152 Run a cooker command on the server
153 """
154 self.command_channel.send(self.cooker.command.runCommand(command))
155
156 def stop(self):
157 self.keep_running.clear()
158
159class BitBakeProcessServerConnection(BitBakeBaseServerConnection):
160 def __init__(self, serverImpl, ui_channel, event_queue):
161 self.procserver = serverImpl
162 self.ui_channel = ui_channel
163 self.event_queue = event_queue
164 self.connection = ServerCommunicator(self.ui_channel, self.procserver.event_handle)
165 self.events = self.event_queue
166
167 def terminate(self):
168 def flushevents():
169 while True:
170 try:
171 event = self.event_queue.get(block=False)
172 except (Empty, IOError):
173 break
174 if isinstance(event, logging.LogRecord):
175 logger.handle(event)
176
177 signal.signal(signal.SIGINT, signal.SIG_IGN)
178 self.procserver.stop()
179
180 while self.procserver.is_alive():
181 flushevents()
182 self.procserver.join(0.1)
183
184 self.ui_channel.close()
185 self.event_queue.close()
186
187# Wrap Queue to provide API which isn't server implementation specific
188class ProcessEventQueue(multiprocessing.queues.Queue):
189 def waitEvent(self, timeout):
190 try:
191 return self.get(True, timeout)
192 except Empty:
193 return None
194
195 def getEvent(self):
196 try:
197 return self.get(False)
198 except Empty:
199 return None
200
201
202class BitBakeServer(BitBakeBaseServer):
203 def initServer(self):
204 # establish communication channels. We use bidirectional pipes for
205 # ui <--> server command/response pairs
206 # and a queue for server -> ui event notifications
207 #
208 self.ui_channel, self.server_channel = Pipe()
209 self.event_queue = ProcessEventQueue(0)
210 manager = Manager()
211 self.featurelist = manager.list()
212 self.serverImpl = ProcessServer(self.server_channel, self.event_queue, self.featurelist)
213
214 def detach(self):
215 self.serverImpl.start()
216 return
217
218 def establishConnection(self, featureset):
219 for f in featureset:
220 self.featurelist.append(f)
221 self.connection = BitBakeProcessServerConnection(self.serverImpl, self.ui_channel, self.event_queue)
222 signal.signal(signal.SIGTERM, lambda i, s: self.connection.terminate())
223 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..82c0e8d8a6
--- /dev/null
+++ b/bitbake/lib/bb/server/xmlrpc.py
@@ -0,0 +1,365 @@
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, featureset = []):
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 original_featureset = list(self.cooker.featureset)
103 for f in featureset:
104 self.cooker.featureset.setFeature(f)
105
106 if (original_featureset != list(self.cooker.featureset)):
107 self.cooker.reset()
108
109 self.event_handle = bb.event.register_UIHhandler(s)
110 return self.event_handle
111
112 def unregisterEventHandler(self, handlerNum):
113 """
114 Unregister a remote UI Event Handler
115 """
116 return bb.event.unregister_UIHhandler(handlerNum)
117
118 def runCommand(self, command):
119 """
120 Run a cooker command on the server
121 """
122 return self.cooker.command.runCommand(command, self.server.readonly)
123
124 def getEventHandle(self):
125 return self.event_handle
126
127 def terminateServer(self):
128 """
129 Trigger the server to quit
130 """
131 self.server.quit = True
132 print("Server (cooker) exiting")
133 return
134
135 def addClient(self):
136 if self.has_client:
137 return None
138 token = hashlib.md5(str(time.time())).hexdigest()
139 self.server.set_connection_token(token)
140 self.has_client = True
141 return token
142
143 def removeClient(self):
144 if self.has_client:
145 self.server.set_connection_token(None)
146 self.has_client = False
147 if self.server.single_use:
148 self.server.quit = True
149
150# This request handler checks if the request has a "Bitbake-token" header
151# field (this comes from the client side) and compares it with its internal
152# "Bitbake-token" field (this comes from the server). If the two are not
153# equal, it is assumed that a client is trying to connect to the server
154# while another client is connected to the server. In this case, a 503 error
155# ("service unavailable") is returned to the client.
156class BitBakeXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
157 def __init__(self, request, client_address, server):
158 self.server = server
159 SimpleXMLRPCRequestHandler.__init__(self, request, client_address, server)
160
161 def do_POST(self):
162 try:
163 remote_token = self.headers["Bitbake-token"]
164 except:
165 remote_token = None
166 if remote_token != self.server.connection_token and remote_token != "observer":
167 self.report_503()
168 else:
169 if remote_token == "observer":
170 self.server.readonly = True
171 else:
172 self.server.readonly = False
173 SimpleXMLRPCRequestHandler.do_POST(self)
174
175 def report_503(self):
176 self.send_response(503)
177 response = 'No more client allowed'
178 self.send_header("Content-type", "text/plain")
179 self.send_header("Content-length", str(len(response)))
180 self.end_headers()
181 self.wfile.write(response)
182
183
184class XMLRPCProxyServer(BaseImplServer):
185 """ not a real working server, but a stub for a proxy server connection
186
187 """
188 def __init__(self, host, port):
189 self.host = host
190 self.port = port
191
192class XMLRPCServer(SimpleXMLRPCServer, BaseImplServer):
193 # remove this when you're done with debugging
194 # allow_reuse_address = True
195
196 def __init__(self, interface):
197 """
198 Constructor
199 """
200 BaseImplServer.__init__(self)
201 SimpleXMLRPCServer.__init__(self, interface,
202 requestHandler=BitBakeXMLRPCRequestHandler,
203 logRequests=False, allow_none=True)
204 self.host, self.port = self.socket.getsockname()
205 self.connection_token = None
206 #self.register_introspection_functions()
207 self.commands = BitBakeServerCommands(self)
208 self.autoregister_all_functions(self.commands, "")
209 self.interface = interface
210 self.single_use = False
211 if (interface[1] == 0): # anonymous port, not getting reused
212 self.single_use = True
213
214 def addcooker(self, cooker):
215 BaseImplServer.addcooker(self, cooker)
216 self.commands.cooker = cooker
217
218 def autoregister_all_functions(self, context, prefix):
219 """
220 Convenience method for registering all functions in the scope
221 of this class that start with a common prefix
222 """
223 methodlist = inspect.getmembers(context, inspect.ismethod)
224 for name, method in methodlist:
225 if name.startswith(prefix):
226 self.register_function(method, name[len(prefix):])
227
228
229 def serve_forever(self):
230 # Start the actual XMLRPC server
231 bb.cooker.server_main(self.cooker, self._serve_forever)
232
233 def _serve_forever(self):
234 """
235 Serve Requests. Overloaded to honor a quit command
236 """
237 self.quit = False
238 while not self.quit:
239 fds = [self]
240 nextsleep = 0.1
241 for function, data in self._idlefuns.items():
242 try:
243 retval = function(self, data, False)
244 if retval is False:
245 del self._idlefuns[function]
246 elif retval is True:
247 nextsleep = 0
248 else:
249 fds = fds + retval
250 except SystemExit:
251 raise
252 except:
253 import traceback
254 traceback.print_exc()
255 pass
256
257 socktimeout = self.socket.gettimeout() or nextsleep
258 socktimeout = min(socktimeout, nextsleep)
259 # Mirror what BaseServer handle_request would do
260 fd_sets = select.select(fds, [], [], socktimeout)
261 if fd_sets[0] and self in fd_sets[0]:
262 self._handle_request_noblock()
263
264 # Tell idle functions we're exiting
265 for function, data in self._idlefuns.items():
266 try:
267 retval = function(self, data, True)
268 except:
269 pass
270 self.server_close()
271 return
272
273 def set_connection_token(self, token):
274 self.connection_token = token
275
276class BitBakeXMLRPCServerConnection(BitBakeBaseServerConnection):
277 def __init__(self, serverImpl, clientinfo=("localhost", 0), observer_only = False, featureset = []):
278 self.connection, self.transport = _create_server(serverImpl.host, serverImpl.port)
279 self.clientinfo = clientinfo
280 self.serverImpl = serverImpl
281 self.observer_only = observer_only
282 self.featureset = featureset
283
284 def connect(self):
285 if not self.observer_only:
286 token = self.connection.addClient()
287 else:
288 token = "observer"
289 if token is None:
290 return None
291 self.transport.set_connection_token(token)
292
293 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo, self.featureset)
294 for event in bb.event.ui_queue:
295 self.events.queue_event(event)
296 return self
297
298 def removeClient(self):
299 if not self.observer_only:
300 self.connection.removeClient()
301
302 def terminate(self):
303 # Don't wait for server indefinitely
304 import socket
305 socket.setdefaulttimeout(2)
306 try:
307 self.events.system_quit()
308 except:
309 pass
310 try:
311 self.connection.removeClient()
312 except:
313 pass
314
315class BitBakeServer(BitBakeBaseServer):
316 def initServer(self, interface = ("localhost", 0)):
317 self.interface = interface
318 self.serverImpl = XMLRPCServer(interface)
319
320 def detach(self):
321 daemonize.createDaemon(self.serverImpl.serve_forever, "bitbake-cookerdaemon.log")
322 del self.cooker
323
324 def establishConnection(self, featureset):
325 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, self.interface, False, featureset)
326 return self.connection.connect()
327
328 def set_connection_token(self, token):
329 self.connection.transport.set_connection_token(token)
330
331class BitBakeXMLRPCClient(BitBakeBaseServer):
332
333 def __init__(self, observer_only = False):
334 self.observer_only = observer_only
335 # if we need extra caches, just tell the server to load them all
336 pass
337
338 def saveConnectionDetails(self, remote):
339 self.remote = remote
340
341 def establishConnection(self, featureset):
342 # The format of "remote" must be "server:port"
343 try:
344 [host, port] = self.remote.split(":")
345 port = int(port)
346 except:
347 return None
348 # We need our IP for the server connection. We get the IP
349 # by trying to connect with the server
350 try:
351 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
352 s.connect((host, port))
353 ip = s.getsockname()[0]
354 s.close()
355 except:
356 return None
357 try:
358 self.serverImpl = XMLRPCProxyServer(host, port)
359 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, (ip, 0), self.observer_only, featureset)
360 return self.connection.connect()
361 except Exception as e:
362 bb.fatal("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
363
364 def endSession(self):
365 self.connection.removeClient()