blob: a7efa58bc765b466c27e7375646aac1e5a2e39b6 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001import os,sys,logging
2import signal, time
Patrick Williamsc0f7c042017-02-23 20:41:17 -06003from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004import threading
Patrick Williamsc0f7c042017-02-23 20:41:17 -06005import queue
Patrick Williamsf1e5d692016-03-30 15:21:19 -05006import socket
Patrick Williamsc0f7c042017-02-23 20:41:17 -06007import io
Brad Bishop6e60e8b2018-02-01 10:27:11 -05008import sqlite3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05009import bb.server.xmlrpc
10import prserv
11import prserv.db
12import errno
13
14logger = logging.getLogger("BitBake.PRserv")
15
16if sys.hexversion < 0x020600F0:
17 print("Sorry, python 2.6 or later is required.")
18 sys.exit(1)
19
20class Handler(SimpleXMLRPCRequestHandler):
21 def _dispatch(self,method,params):
22 try:
23 value=self.server.funcs[method](*params)
24 except:
25 import traceback
26 traceback.print_exc()
27 raise
28 return value
29
30PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
31singleton = None
32
33
34class PRServer(SimpleXMLRPCServer):
35 def __init__(self, dbfile, logfile, interface, daemon=True):
36 ''' constructor '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -050037 try:
38 SimpleXMLRPCServer.__init__(self, interface,
39 logRequests=False, allow_none=True)
40 except socket.error:
41 ip=socket.gethostbyname(interface[0])
42 port=interface[1]
43 msg="PR Server unable to bind to %s:%s\n" % (ip, port)
44 sys.stderr.write(msg)
45 raise PRServiceConfigError
46
47 self.dbfile=dbfile
48 self.daemon=daemon
49 self.logfile=logfile
50 self.working_thread=None
51 self.host, self.port = self.socket.getsockname()
52 self.pidfile=PIDPREFIX % (self.host, self.port)
53
54 self.register_function(self.getPR, "getPR")
55 self.register_function(self.quit, "quit")
56 self.register_function(self.ping, "ping")
57 self.register_function(self.export, "export")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050058 self.register_function(self.dump_db, "dump_db")
Patrick Williamsc124f4f2015-09-15 14:41:29 -050059 self.register_function(self.importone, "importone")
60 self.register_introspection_functions()
61
Patrick Williamsc0f7c042017-02-23 20:41:17 -060062 self.requestqueue = queue.Queue()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050063 self.handlerthread = threading.Thread(target = self.process_request_thread)
64 self.handlerthread.daemon = False
65
66 def process_request_thread(self):
67 """Same as in BaseServer but as a thread.
68
69 In addition, exception handling is done here.
70
71 """
72 iter_count = 1
73 # 60 iterations between syncs or sync if dirty every ~30 seconds
74 iterations_between_sync = 60
75
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050076 bb.utils.set_process_name("PRServ Handler")
77
Patrick Williamsc124f4f2015-09-15 14:41:29 -050078 while not self.quit:
79 try:
80 (request, client_address) = self.requestqueue.get(True, 30)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060081 except queue.Empty:
Patrick Williamsc124f4f2015-09-15 14:41:29 -050082 self.table.sync_if_dirty()
83 continue
84 try:
85 self.finish_request(request, client_address)
86 self.shutdown_request(request)
87 iter_count = (iter_count + 1) % iterations_between_sync
88 if iter_count == 0:
89 self.table.sync_if_dirty()
90 except:
91 self.handle_error(request, client_address)
92 self.shutdown_request(request)
93 self.table.sync()
94 self.table.sync_if_dirty()
95
96 def sigint_handler(self, signum, stack):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050097 if self.table:
98 self.table.sync()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050099
100 def sigterm_handler(self, signum, stack):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500101 if self.table:
102 self.table.sync()
103 self.quit=True
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500104
105 def process_request(self, request, client_address):
106 self.requestqueue.put((request, client_address))
107
108 def export(self, version=None, pkgarch=None, checksum=None, colinfo=True):
109 try:
110 return self.table.export(version, pkgarch, checksum, colinfo)
111 except sqlite3.Error as exc:
112 logger.error(str(exc))
113 return None
114
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500115 def dump_db(self):
116 """
117 Returns a script (string) that reconstructs the state of the
118 entire database at the time this function is called. The script
119 language is defined by the backing database engine, which is a
120 function of server configuration.
121 Returns None if the database engine does not support dumping to
122 script or if some other error is encountered in processing.
123 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600124 buff = io.StringIO()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500125 try:
126 self.table.sync()
127 self.table.dump_db(buff)
128 return buff.getvalue()
129 except Exception as exc:
130 logger.error(str(exc))
131 return None
132 finally:
133 buff.close()
134
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500135 def importone(self, version, pkgarch, checksum, value):
136 return self.table.importone(version, pkgarch, checksum, value)
137
138 def ping(self):
139 return not self.quit
140
141 def getinfo(self):
142 return (self.host, self.port)
143
144 def getPR(self, version, pkgarch, checksum):
145 try:
146 return self.table.getValue(version, pkgarch, checksum)
147 except prserv.NotFoundError:
148 logger.error("can not find value for (%s, %s)",version, checksum)
149 return None
150 except sqlite3.Error as exc:
151 logger.error(str(exc))
152 return None
153
154 def quit(self):
155 self.quit=True
156 return
157
158 def work_forever(self,):
159 self.quit = False
160 self.timeout = 0.5
161
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500162 bb.utils.set_process_name("PRServ")
163
164 # DB connection must be created after all forks
165 self.db = prserv.db.PRData(self.dbfile)
166 self.table = self.db["PRMAIN"]
167
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168 logger.info("Started PRServer with DBfile: %s, IP: %s, PORT: %s, PID: %s" %
169 (self.dbfile, self.host, self.port, str(os.getpid())))
170
171 self.handlerthread.start()
172 while not self.quit:
173 self.handle_request()
174 self.handlerthread.join()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500175 self.db.disconnect()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500176 logger.info("PRServer: stopping...")
177 self.server_close()
178 return
179
180 def start(self):
181 if self.daemon:
182 pid = self.daemonize()
183 else:
184 pid = self.fork()
185
186 # Ensure both the parent sees this and the child from the work_forever log entry above
187 logger.info("Started PRServer with DBfile: %s, IP: %s, PORT: %s, PID: %s" %
188 (self.dbfile, self.host, self.port, str(pid)))
189
190 def delpid(self):
191 os.remove(self.pidfile)
192
193 def daemonize(self):
194 """
195 See Advanced Programming in the UNIX, Sec 13.3
196 """
197 try:
198 pid = os.fork()
199 if pid > 0:
200 os.waitpid(pid, 0)
201 #parent return instead of exit to give control
202 return pid
203 except OSError as e:
204 raise Exception("%s [%d]" % (e.strerror, e.errno))
205
206 os.setsid()
207 """
208 fork again to make sure the daemon is not session leader,
209 which prevents it from acquiring controlling terminal
210 """
211 try:
212 pid = os.fork()
213 if pid > 0: #parent
214 os._exit(0)
215 except OSError as e:
216 raise Exception("%s [%d]" % (e.strerror, e.errno))
217
218 self.cleanup_handles()
219 os._exit(0)
220
221 def fork(self):
222 try:
223 pid = os.fork()
224 if pid > 0:
225 return pid
226 except OSError as e:
227 raise Exception("%s [%d]" % (e.strerror, e.errno))
228
229 bb.utils.signal_on_parent_exit("SIGTERM")
230 self.cleanup_handles()
231 os._exit(0)
232
233 def cleanup_handles(self):
234 signal.signal(signal.SIGINT, self.sigint_handler)
235 signal.signal(signal.SIGTERM, self.sigterm_handler)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500236 os.chdir("/")
237
238 sys.stdout.flush()
239 sys.stderr.flush()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500240
241 # We could be called from a python thread with io.StringIO as
242 # stdout/stderr or it could be 'real' unix fd forking where we need
243 # to physically close the fds to prevent the program launching us from
244 # potentially hanging on a pipe. Handle both cases.
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600245 si = open('/dev/null', 'r')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500246 try:
247 os.dup2(si.fileno(),sys.stdin.fileno())
248 except (AttributeError, io.UnsupportedOperation):
249 sys.stdin = si
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600250 so = open(self.logfile, 'a+')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500251 try:
252 os.dup2(so.fileno(),sys.stdout.fileno())
253 except (AttributeError, io.UnsupportedOperation):
254 sys.stdout = so
255 try:
256 os.dup2(so.fileno(),sys.stderr.fileno())
257 except (AttributeError, io.UnsupportedOperation):
258 sys.stderr = so
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500259
260 # Clear out all log handlers prior to the fork() to avoid calling
261 # event handlers not part of the PRserver
262 for logger_iter in logging.Logger.manager.loggerDict.keys():
263 logging.getLogger(logger_iter).handlers = []
264
265 # Ensure logging makes it to the logfile
266 streamhandler = logging.StreamHandler()
267 streamhandler.setLevel(logging.DEBUG)
268 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
269 streamhandler.setFormatter(formatter)
270 logger.addHandler(streamhandler)
271
272 # write pidfile
273 pid = str(os.getpid())
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600274 pf = open(self.pidfile, 'w')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500275 pf.write("%s\n" % pid)
276 pf.close()
277
278 self.work_forever()
279 self.delpid()
280
281class PRServSingleton(object):
282 def __init__(self, dbfile, logfile, interface):
283 self.dbfile = dbfile
284 self.logfile = logfile
285 self.interface = interface
286 self.host = None
287 self.port = None
288
289 def start(self):
290 self.prserv = PRServer(self.dbfile, self.logfile, self.interface, daemon=False)
291 self.prserv.start()
292 self.host, self.port = self.prserv.getinfo()
293
294 def getinfo(self):
295 return (self.host, self.port)
296
297class PRServerConnection(object):
298 def __init__(self, host, port):
299 if is_local_special(host, port):
300 host, port = singleton.getinfo()
301 self.host = host
302 self.port = port
303 self.connection, self.transport = bb.server.xmlrpc._create_server(self.host, self.port)
304
305 def terminate(self):
306 try:
307 logger.info("Terminating PRServer...")
308 self.connection.quit()
309 except Exception as exc:
310 sys.stderr.write("%s\n" % str(exc))
311
312 def getPR(self, version, pkgarch, checksum):
313 return self.connection.getPR(version, pkgarch, checksum)
314
315 def ping(self):
316 return self.connection.ping()
317
318 def export(self,version=None, pkgarch=None, checksum=None, colinfo=True):
319 return self.connection.export(version, pkgarch, checksum, colinfo)
320
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500321 def dump_db(self):
322 return self.connection.dump_db()
323
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500324 def importone(self, version, pkgarch, checksum, value):
325 return self.connection.importone(version, pkgarch, checksum, value)
326
327 def getinfo(self):
328 return self.host, self.port
329
330def start_daemon(dbfile, host, port, logfile):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500331 ip = socket.gethostbyname(host)
332 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500333 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600334 pf = open(pidfile,'r')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500335 pid = int(pf.readline().strip())
336 pf.close()
337 except IOError:
338 pid = None
339
340 if pid:
341 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
342 % pidfile)
343 return 1
344
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500345 server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500346 server.start()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500347
348 # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with
349 # the one the server actually is listening, so at least warn the user about it
350 _,rport = server.getinfo()
351 if port != rport:
352 sys.stdout.write("Server is listening at port %s instead of %s\n"
353 % (rport,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500354 return 0
355
356def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500357 import glob
358 ip = socket.gethostbyname(host)
359 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500360 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600361 pf = open(pidfile,'r')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500362 pid = int(pf.readline().strip())
363 pf.close()
364 except IOError:
365 pid = None
366
367 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500368 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
369 # so at least advise the user which ports the corresponding server is listening
370 ports = []
371 portstr = ""
372 for pf in glob.glob(PIDPREFIX % (ip,'*')):
373 bn = os.path.basename(pf)
374 root, _ = os.path.splitext(bn)
375 ports.append(root.split('_')[-1])
376 if len(ports):
377 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
378
379 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
380 % (pidfile,portstr))
381 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500382
383 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500384 PRServerConnection(ip, port).terminate()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500385 except:
386 logger.critical("Stop PRService %s:%d failed" % (host,port))
387
388 try:
389 if pid:
390 wait_timeout = 0
391 print("Waiting for pr-server to exit.")
392 while is_running(pid) and wait_timeout < 50:
393 time.sleep(0.1)
394 wait_timeout += 1
395
396 if is_running(pid):
397 print("Sending SIGTERM to pr-server.")
398 os.kill(pid,signal.SIGTERM)
399 time.sleep(0.1)
400
401 if os.path.exists(pidfile):
402 os.remove(pidfile)
403
404 except OSError as e:
405 err = str(e)
406 if err.find("No such process") <= 0:
407 raise e
408
409 return 0
410
411def is_running(pid):
412 try:
413 os.kill(pid, 0)
414 except OSError as err:
415 if err.errno == errno.ESRCH:
416 return False
417 return True
418
419def is_local_special(host, port):
420 if host.strip().upper() == 'localhost'.upper() and (not port):
421 return True
422 else:
423 return False
424
425class PRServiceConfigError(Exception):
426 pass
427
428def auto_start(d):
429 global singleton
430
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500431 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':')))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500432 if not host_params:
433 return None
434
435 if len(host_params) != 2:
436 logger.critical('\n'.join(['PRSERV_HOST: incorrect format',
437 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
438 raise PRServiceConfigError
439
440 if is_local_special(host_params[0], int(host_params[1])) and not singleton:
441 import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500442 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500443 if not cachedir:
444 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
445 raise PRServiceConfigError
446 bb.utils.mkdirhier(cachedir)
447 dbfile = os.path.join(cachedir, "prserv.sqlite3")
448 logfile = os.path.join(cachedir, "prserv.log")
449 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), ("localhost",0))
450 singleton.start()
451 if singleton:
452 host, port = singleton.getinfo()
453 else:
454 host = host_params[0]
455 port = int(host_params[1])
456
457 try:
458 connection = PRServerConnection(host,port)
459 connection.ping()
460 realhost, realport = connection.getinfo()
461 return str(realhost) + ":" + str(realport)
462
463 except Exception:
464 logger.critical("PRservice %s:%d not available" % (host, port))
465 raise PRServiceConfigError
466
467def auto_shutdown(d=None):
468 global singleton
469 if singleton:
470 host, port = singleton.getinfo()
471 try:
472 PRServerConnection(host, port).terminate()
473 except:
474 logger.critical("Stop PRService %s:%d failed" % (host,port))
475 singleton = None
476
477def ping(host, port):
478 conn=PRServerConnection(host, port)
479 return conn.ping()