blob: ace1cf646b9ae8c231b1bb645e6db25ffcd638ea [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# BitBake XMLRPC Server
3#
4# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer
5# Copyright (C) 2006 - 2008 Richard Purdie
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 2 as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program; if not, write to the Free Software Foundation, Inc.,
18# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20"""
21 This module implements an xmlrpc server for BitBake.
22
23 Use this by deriving a class from BitBakeXMLRPCServer and then adding
24 methods which you want to "export" via XMLRPC. If the methods have the
25 prefix xmlrpc_, then registering those function will happen automatically,
26 if not, you need to call register_function.
27
28 Use register_idle_function() to add a function which the xmlrpc server
29 calls from within server_forever when no requests are pending. Make sure
30 that those functions are non-blocking or else you will introduce latency
31 in the server's main loop.
32"""
33
34import bb
35import xmlrpclib, sys
36from bb import daemonize
37from bb.ui import uievent
38import hashlib, time
39import socket
40import os, signal
41import threading
42try:
43 import cPickle as pickle
44except ImportError:
45 import pickle
46
47DEBUG = False
48
49from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
50import inspect, select, httplib
51
52from . import BitBakeBaseServer, BitBakeBaseServerConnection, BaseImplServer
53
54class BBTransport(xmlrpclib.Transport):
55 def __init__(self, timeout):
56 self.timeout = timeout
57 self.connection_token = None
58 xmlrpclib.Transport.__init__(self)
59
60 # Modified from default to pass timeout to HTTPConnection
61 def make_connection(self, host):
62 #return an existing connection if possible. This allows
63 #HTTP/1.1 keep-alive.
64 if self._connection and host == self._connection[0]:
65 return self._connection[1]
66
67 # create a HTTP connection object from a host descriptor
68 chost, self._extra_headers, x509 = self.get_host_info(host)
69 #store the host argument along with the connection object
70 self._connection = host, httplib.HTTPConnection(chost, timeout=self.timeout)
71 return self._connection[1]
72
73 def set_connection_token(self, token):
74 self.connection_token = token
75
76 def send_content(self, h, body):
77 if self.connection_token:
78 h.putheader("Bitbake-token", self.connection_token)
79 xmlrpclib.Transport.send_content(self, h, body)
80
81def _create_server(host, port, timeout = 60):
82 t = BBTransport(timeout)
83 s = xmlrpclib.ServerProxy("http://%s:%d/" % (host, port), transport=t, allow_none=True)
84 return s, t
85
86class BitBakeServerCommands():
87
88 def __init__(self, server):
89 self.server = server
90 self.has_client = False
91
92 def registerEventHandler(self, host, port):
93 """
94 Register a remote UI Event Handler
95 """
96 s, t = _create_server(host, port)
97
98 # we don't allow connections if the cooker is running
99 if (self.cooker.state in [bb.cooker.state.parsing, bb.cooker.state.running]):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500100 return None, "Cooker is busy: %s" % bb.cooker.state.get_name(self.cooker.state)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500101
102 self.event_handle = bb.event.register_UIHhandler(s, True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500103 return self.event_handle, 'OK'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500104
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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500189 def __init__(self, interface, single_use=False):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500190 """
191 Constructor
192 """
193 BaseImplServer.__init__(self)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500194 self.single_use = single_use
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500195 # Use auto port configuration
196 if (interface[1] == -1):
197 interface = (interface[0], 0)
198 SimpleXMLRPCServer.__init__(self, interface,
199 requestHandler=BitBakeXMLRPCRequestHandler,
200 logRequests=False, allow_none=True)
201 self.host, self.port = self.socket.getsockname()
202 self.connection_token = None
203 #self.register_introspection_functions()
204 self.commands = BitBakeServerCommands(self)
205 self.autoregister_all_functions(self.commands, "")
206 self.interface = interface
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500207
208 def addcooker(self, cooker):
209 BaseImplServer.addcooker(self, cooker)
210 self.commands.cooker = cooker
211
212 def autoregister_all_functions(self, context, prefix):
213 """
214 Convenience method for registering all functions in the scope
215 of this class that start with a common prefix
216 """
217 methodlist = inspect.getmembers(context, inspect.ismethod)
218 for name, method in methodlist:
219 if name.startswith(prefix):
220 self.register_function(method, name[len(prefix):])
221
222
223 def serve_forever(self):
224 # Start the actual XMLRPC server
225 bb.cooker.server_main(self.cooker, self._serve_forever)
226
227 def _serve_forever(self):
228 """
229 Serve Requests. Overloaded to honor a quit command
230 """
231 self.quit = False
232 while not self.quit:
233 fds = [self]
234 nextsleep = 0.1
235 for function, data in self._idlefuns.items():
236 retval = None
237 try:
238 retval = function(self, data, False)
239 if retval is False:
240 del self._idlefuns[function]
241 elif retval is True:
242 nextsleep = 0
243 elif isinstance(retval, float):
244 if (retval < nextsleep):
245 nextsleep = retval
246 else:
247 fds = fds + retval
248 except SystemExit:
249 raise
250 except:
251 import traceback
252 traceback.print_exc()
253 if retval == None:
254 # the function execute failed; delete it
255 del self._idlefuns[function]
256 pass
257
258 socktimeout = self.socket.gettimeout() or nextsleep
259 socktimeout = min(socktimeout, nextsleep)
260 # Mirror what BaseServer handle_request would do
261 try:
262 fd_sets = select.select(fds, [], [], socktimeout)
263 if fd_sets[0] and self in fd_sets[0]:
264 self._handle_request_noblock()
265 except IOError:
266 # we ignore interrupted calls
267 pass
268
269 # Tell idle functions we're exiting
270 for function, data in self._idlefuns.items():
271 try:
272 retval = function(self, data, True)
273 except:
274 pass
275 self.server_close()
276 return
277
278 def set_connection_token(self, token):
279 self.connection_token = token
280
281class BitBakeXMLRPCServerConnection(BitBakeBaseServerConnection):
282 def __init__(self, serverImpl, clientinfo=("localhost", 0), observer_only = False, featureset = None):
283 self.connection, self.transport = _create_server(serverImpl.host, serverImpl.port)
284 self.clientinfo = clientinfo
285 self.serverImpl = serverImpl
286 self.observer_only = observer_only
287 if featureset:
288 self.featureset = featureset
289 else:
290 self.featureset = []
291
292 def connect(self, token = None):
293 if token is None:
294 if self.observer_only:
295 token = "observer"
296 else:
297 token = self.connection.addClient()
298
299 if token is None:
300 return None
301
302 self.transport.set_connection_token(token)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500303 return self
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500304
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500305 def setupEventQueue(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306 self.events = uievent.BBUIEventQueue(self.connection, self.clientinfo)
307 for event in bb.event.ui_queue:
308 self.events.queue_event(event)
309
310 _, error = self.connection.runCommand(["setFeatures", self.featureset])
311 if error:
312 # disconnect the client, we can't make the setFeature work
313 self.connection.removeClient()
314 # no need to log it here, the error shall be sent to the client
315 raise BaseException(error)
316
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500317 def removeClient(self):
318 if not self.observer_only:
319 self.connection.removeClient()
320
321 def terminate(self):
322 # Don't wait for server indefinitely
323 import socket
324 socket.setdefaulttimeout(2)
325 try:
326 self.events.system_quit()
327 except:
328 pass
329 try:
330 self.connection.removeClient()
331 except:
332 pass
333
334class BitBakeServer(BitBakeBaseServer):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500335 def initServer(self, interface = ("localhost", 0), single_use = False):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500336 self.interface = interface
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500337 self.serverImpl = XMLRPCServer(interface, single_use)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500338
339 def detach(self):
340 daemonize.createDaemon(self.serverImpl.serve_forever, "bitbake-cookerdaemon.log")
341 del self.cooker
342
343 def establishConnection(self, featureset):
344 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, self.interface, False, featureset)
345 return self.connection.connect()
346
347 def set_connection_token(self, token):
348 self.connection.transport.set_connection_token(token)
349
350class BitBakeXMLRPCClient(BitBakeBaseServer):
351
352 def __init__(self, observer_only = False, token = None):
353 self.token = token
354
355 self.observer_only = observer_only
356 # if we need extra caches, just tell the server to load them all
357 pass
358
359 def saveConnectionDetails(self, remote):
360 self.remote = remote
361
362 def establishConnection(self, featureset):
363 # The format of "remote" must be "server:port"
364 try:
365 [host, port] = self.remote.split(":")
366 port = int(port)
367 except Exception as e:
368 bb.warn("Failed to read remote definition (%s)" % str(e))
369 raise e
370
371 # We need our IP for the server connection. We get the IP
372 # by trying to connect with the server
373 try:
374 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
375 s.connect((host, port))
376 ip = s.getsockname()[0]
377 s.close()
378 except Exception as e:
379 bb.warn("Could not create socket for %s:%s (%s)" % (host, port, str(e)))
380 raise e
381 try:
382 self.serverImpl = XMLRPCProxyServer(host, port)
383 self.connection = BitBakeXMLRPCServerConnection(self.serverImpl, (ip, 0), self.observer_only, featureset)
384 return self.connection.connect(self.token)
385 except Exception as e:
386 bb.warn("Could not connect to server at %s:%s (%s)" % (host, port, str(e)))
387 raise e
388
389 def endSession(self):
390 self.connection.removeClient()