blob: be3acec36a0872b67055ab54e07ac6c41d045fd4 [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4
Patrick Williamsc124f4f2015-09-15 14:41:29 -05005import os,sys,logging
6import signal, time
Patrick Williamsc0f7c042017-02-23 20:41:17 -06007from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
Patrick Williamsc124f4f2015-09-15 14:41:29 -05008import threading
Patrick Williamsc0f7c042017-02-23 20:41:17 -06009import queue
Patrick Williamsf1e5d692016-03-30 15:21:19 -050010import socket
Patrick Williamsc0f7c042017-02-23 20:41:17 -060011import io
Brad Bishop6e60e8b2018-02-01 10:27:11 -050012import sqlite3
Brad Bishopd7bf8c12018-02-25 22:55:05 -050013import bb.server.xmlrpcclient
Patrick Williamsc124f4f2015-09-15 14:41:29 -050014import prserv
15import prserv.db
16import errno
Brad Bishopd7bf8c12018-02-25 22:55:05 -050017import select
Patrick Williamsc124f4f2015-09-15 14:41:29 -050018
19logger = logging.getLogger("BitBake.PRserv")
20
21if sys.hexversion < 0x020600F0:
22 print("Sorry, python 2.6 or later is required.")
23 sys.exit(1)
24
25class Handler(SimpleXMLRPCRequestHandler):
26 def _dispatch(self,method,params):
27 try:
28 value=self.server.funcs[method](*params)
29 except:
30 import traceback
31 traceback.print_exc()
32 raise
33 return value
34
35PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
36singleton = None
37
38
39class PRServer(SimpleXMLRPCServer):
40 def __init__(self, dbfile, logfile, interface, daemon=True):
41 ''' constructor '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -050042 try:
43 SimpleXMLRPCServer.__init__(self, interface,
44 logRequests=False, allow_none=True)
45 except socket.error:
46 ip=socket.gethostbyname(interface[0])
47 port=interface[1]
48 msg="PR Server unable to bind to %s:%s\n" % (ip, port)
49 sys.stderr.write(msg)
50 raise PRServiceConfigError
51
52 self.dbfile=dbfile
53 self.daemon=daemon
54 self.logfile=logfile
55 self.working_thread=None
56 self.host, self.port = self.socket.getsockname()
57 self.pidfile=PIDPREFIX % (self.host, self.port)
58
59 self.register_function(self.getPR, "getPR")
60 self.register_function(self.quit, "quit")
61 self.register_function(self.ping, "ping")
62 self.register_function(self.export, "export")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050063 self.register_function(self.dump_db, "dump_db")
Patrick Williamsc124f4f2015-09-15 14:41:29 -050064 self.register_function(self.importone, "importone")
65 self.register_introspection_functions()
66
Brad Bishopd7bf8c12018-02-25 22:55:05 -050067 self.quitpipein, self.quitpipeout = os.pipe()
68
Patrick Williamsc0f7c042017-02-23 20:41:17 -060069 self.requestqueue = queue.Queue()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050070 self.handlerthread = threading.Thread(target = self.process_request_thread)
71 self.handlerthread.daemon = False
72
73 def process_request_thread(self):
74 """Same as in BaseServer but as a thread.
75
76 In addition, exception handling is done here.
77
78 """
79 iter_count = 1
80 # 60 iterations between syncs or sync if dirty every ~30 seconds
81 iterations_between_sync = 60
82
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050083 bb.utils.set_process_name("PRServ Handler")
84
Brad Bishopd7bf8c12018-02-25 22:55:05 -050085 while not self.quitflag:
Patrick Williamsc124f4f2015-09-15 14:41:29 -050086 try:
87 (request, client_address) = self.requestqueue.get(True, 30)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060088 except queue.Empty:
Patrick Williamsc124f4f2015-09-15 14:41:29 -050089 self.table.sync_if_dirty()
90 continue
Brad Bishopd7bf8c12018-02-25 22:55:05 -050091 if request is None:
92 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -050093 try:
94 self.finish_request(request, client_address)
95 self.shutdown_request(request)
96 iter_count = (iter_count + 1) % iterations_between_sync
97 if iter_count == 0:
98 self.table.sync_if_dirty()
99 except:
100 self.handle_error(request, client_address)
101 self.shutdown_request(request)
102 self.table.sync()
103 self.table.sync_if_dirty()
104
105 def sigint_handler(self, signum, stack):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500106 if self.table:
107 self.table.sync()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500108
109 def sigterm_handler(self, signum, stack):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500110 if self.table:
111 self.table.sync()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500112 self.quit()
113 self.requestqueue.put((None, None))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500114
115 def process_request(self, request, client_address):
116 self.requestqueue.put((request, client_address))
117
118 def export(self, version=None, pkgarch=None, checksum=None, colinfo=True):
119 try:
120 return self.table.export(version, pkgarch, checksum, colinfo)
121 except sqlite3.Error as exc:
122 logger.error(str(exc))
123 return None
124
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500125 def dump_db(self):
126 """
127 Returns a script (string) that reconstructs the state of the
128 entire database at the time this function is called. The script
129 language is defined by the backing database engine, which is a
130 function of server configuration.
131 Returns None if the database engine does not support dumping to
132 script or if some other error is encountered in processing.
133 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600134 buff = io.StringIO()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500135 try:
136 self.table.sync()
137 self.table.dump_db(buff)
138 return buff.getvalue()
139 except Exception as exc:
140 logger.error(str(exc))
141 return None
142 finally:
143 buff.close()
144
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500145 def importone(self, version, pkgarch, checksum, value):
146 return self.table.importone(version, pkgarch, checksum, value)
147
148 def ping(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500149 return not self.quitflag
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500150
151 def getinfo(self):
152 return (self.host, self.port)
153
154 def getPR(self, version, pkgarch, checksum):
155 try:
156 return self.table.getValue(version, pkgarch, checksum)
157 except prserv.NotFoundError:
158 logger.error("can not find value for (%s, %s)",version, checksum)
159 return None
160 except sqlite3.Error as exc:
161 logger.error(str(exc))
162 return None
163
164 def quit(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500165 self.quitflag=True
166 os.write(self.quitpipeout, b"q")
167 os.close(self.quitpipeout)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168 return
169
170 def work_forever(self,):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500171 self.quitflag = False
172 # This timeout applies to the poll in TCPServer, we need the select
173 # below to wake on our quit pipe closing. We only ever call into handle_request
174 # if there is data there.
175 self.timeout = 0.01
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500176
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500177 bb.utils.set_process_name("PRServ")
178
179 # DB connection must be created after all forks
180 self.db = prserv.db.PRData(self.dbfile)
181 self.table = self.db["PRMAIN"]
182
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500183 logger.info("Started PRServer with DBfile: %s, IP: %s, PORT: %s, PID: %s" %
184 (self.dbfile, self.host, self.port, str(os.getpid())))
185
186 self.handlerthread.start()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500187 while not self.quitflag:
188 ready = select.select([self.fileno(), self.quitpipein], [], [], 30)
189 if self.quitflag:
190 break
191 if self.fileno() in ready[0]:
192 self.handle_request()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500193 self.handlerthread.join()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500194 self.db.disconnect()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500195 logger.info("PRServer: stopping...")
196 self.server_close()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500197 os.close(self.quitpipein)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500198 return
199
200 def start(self):
201 if self.daemon:
202 pid = self.daemonize()
203 else:
204 pid = self.fork()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500205 self.pid = pid
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500206
207 # Ensure both the parent sees this and the child from the work_forever log entry above
208 logger.info("Started PRServer with DBfile: %s, IP: %s, PORT: %s, PID: %s" %
209 (self.dbfile, self.host, self.port, str(pid)))
210
211 def delpid(self):
212 os.remove(self.pidfile)
213
214 def daemonize(self):
215 """
216 See Advanced Programming in the UNIX, Sec 13.3
217 """
218 try:
219 pid = os.fork()
220 if pid > 0:
221 os.waitpid(pid, 0)
222 #parent return instead of exit to give control
223 return pid
224 except OSError as e:
225 raise Exception("%s [%d]" % (e.strerror, e.errno))
226
227 os.setsid()
228 """
229 fork again to make sure the daemon is not session leader,
230 which prevents it from acquiring controlling terminal
231 """
232 try:
233 pid = os.fork()
234 if pid > 0: #parent
235 os._exit(0)
236 except OSError as e:
237 raise Exception("%s [%d]" % (e.strerror, e.errno))
238
239 self.cleanup_handles()
240 os._exit(0)
241
242 def fork(self):
243 try:
244 pid = os.fork()
245 if pid > 0:
246 return pid
247 except OSError as e:
248 raise Exception("%s [%d]" % (e.strerror, e.errno))
249
250 bb.utils.signal_on_parent_exit("SIGTERM")
251 self.cleanup_handles()
252 os._exit(0)
253
254 def cleanup_handles(self):
255 signal.signal(signal.SIGINT, self.sigint_handler)
256 signal.signal(signal.SIGTERM, self.sigterm_handler)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500257 os.chdir("/")
258
259 sys.stdout.flush()
260 sys.stderr.flush()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500261
262 # We could be called from a python thread with io.StringIO as
263 # stdout/stderr or it could be 'real' unix fd forking where we need
264 # to physically close the fds to prevent the program launching us from
265 # potentially hanging on a pipe. Handle both cases.
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600266 si = open('/dev/null', 'r')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500267 try:
268 os.dup2(si.fileno(),sys.stdin.fileno())
269 except (AttributeError, io.UnsupportedOperation):
270 sys.stdin = si
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600271 so = open(self.logfile, 'a+')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500272 try:
273 os.dup2(so.fileno(),sys.stdout.fileno())
274 except (AttributeError, io.UnsupportedOperation):
275 sys.stdout = so
276 try:
277 os.dup2(so.fileno(),sys.stderr.fileno())
278 except (AttributeError, io.UnsupportedOperation):
279 sys.stderr = so
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500280
281 # Clear out all log handlers prior to the fork() to avoid calling
282 # event handlers not part of the PRserver
283 for logger_iter in logging.Logger.manager.loggerDict.keys():
284 logging.getLogger(logger_iter).handlers = []
285
286 # Ensure logging makes it to the logfile
287 streamhandler = logging.StreamHandler()
288 streamhandler.setLevel(logging.DEBUG)
289 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
290 streamhandler.setFormatter(formatter)
291 logger.addHandler(streamhandler)
292
293 # write pidfile
294 pid = str(os.getpid())
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600295 pf = open(self.pidfile, 'w')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296 pf.write("%s\n" % pid)
297 pf.close()
298
299 self.work_forever()
300 self.delpid()
301
302class PRServSingleton(object):
303 def __init__(self, dbfile, logfile, interface):
304 self.dbfile = dbfile
305 self.logfile = logfile
306 self.interface = interface
307 self.host = None
308 self.port = None
309
310 def start(self):
311 self.prserv = PRServer(self.dbfile, self.logfile, self.interface, daemon=False)
312 self.prserv.start()
313 self.host, self.port = self.prserv.getinfo()
314
315 def getinfo(self):
316 return (self.host, self.port)
317
318class PRServerConnection(object):
319 def __init__(self, host, port):
320 if is_local_special(host, port):
321 host, port = singleton.getinfo()
322 self.host = host
323 self.port = port
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500324 self.connection, self.transport = bb.server.xmlrpcclient._create_server(self.host, self.port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325
326 def terminate(self):
327 try:
328 logger.info("Terminating PRServer...")
329 self.connection.quit()
330 except Exception as exc:
331 sys.stderr.write("%s\n" % str(exc))
332
333 def getPR(self, version, pkgarch, checksum):
334 return self.connection.getPR(version, pkgarch, checksum)
335
336 def ping(self):
337 return self.connection.ping()
338
339 def export(self,version=None, pkgarch=None, checksum=None, colinfo=True):
340 return self.connection.export(version, pkgarch, checksum, colinfo)
341
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500342 def dump_db(self):
343 return self.connection.dump_db()
344
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500345 def importone(self, version, pkgarch, checksum, value):
346 return self.connection.importone(version, pkgarch, checksum, value)
347
348 def getinfo(self):
349 return self.host, self.port
350
351def start_daemon(dbfile, host, port, logfile):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500352 ip = socket.gethostbyname(host)
353 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500354 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600355 pf = open(pidfile,'r')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500356 pid = int(pf.readline().strip())
357 pf.close()
358 except IOError:
359 pid = None
360
361 if pid:
362 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
363 % pidfile)
364 return 1
365
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500366 server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500367 server.start()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500368
369 # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with
370 # the one the server actually is listening, so at least warn the user about it
371 _,rport = server.getinfo()
372 if port != rport:
373 sys.stdout.write("Server is listening at port %s instead of %s\n"
374 % (rport,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500375 return 0
376
377def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500378 import glob
379 ip = socket.gethostbyname(host)
380 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500381 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600382 pf = open(pidfile,'r')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500383 pid = int(pf.readline().strip())
384 pf.close()
385 except IOError:
386 pid = None
387
388 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500389 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
390 # so at least advise the user which ports the corresponding server is listening
391 ports = []
392 portstr = ""
393 for pf in glob.glob(PIDPREFIX % (ip,'*')):
394 bn = os.path.basename(pf)
395 root, _ = os.path.splitext(bn)
396 ports.append(root.split('_')[-1])
397 if len(ports):
398 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
399
400 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
401 % (pidfile,portstr))
402 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500403
404 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500405 PRServerConnection(ip, port).terminate()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500406 except:
407 logger.critical("Stop PRService %s:%d failed" % (host,port))
408
409 try:
410 if pid:
411 wait_timeout = 0
412 print("Waiting for pr-server to exit.")
413 while is_running(pid) and wait_timeout < 50:
414 time.sleep(0.1)
415 wait_timeout += 1
416
417 if is_running(pid):
418 print("Sending SIGTERM to pr-server.")
419 os.kill(pid,signal.SIGTERM)
420 time.sleep(0.1)
421
422 if os.path.exists(pidfile):
423 os.remove(pidfile)
424
425 except OSError as e:
426 err = str(e)
427 if err.find("No such process") <= 0:
428 raise e
429
430 return 0
431
432def is_running(pid):
433 try:
434 os.kill(pid, 0)
435 except OSError as err:
436 if err.errno == errno.ESRCH:
437 return False
438 return True
439
440def is_local_special(host, port):
441 if host.strip().upper() == 'localhost'.upper() and (not port):
442 return True
443 else:
444 return False
445
446class PRServiceConfigError(Exception):
447 pass
448
449def auto_start(d):
450 global singleton
451
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500452 # Shutdown any existing PR Server
453 auto_shutdown()
454
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500455 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':')))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500456 if not host_params:
457 return None
458
459 if len(host_params) != 2:
460 logger.critical('\n'.join(['PRSERV_HOST: incorrect format',
461 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
462 raise PRServiceConfigError
463
464 if is_local_special(host_params[0], int(host_params[1])) and not singleton:
465 import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500466 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500467 if not cachedir:
468 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
469 raise PRServiceConfigError
470 bb.utils.mkdirhier(cachedir)
471 dbfile = os.path.join(cachedir, "prserv.sqlite3")
472 logfile = os.path.join(cachedir, "prserv.log")
473 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), ("localhost",0))
474 singleton.start()
475 if singleton:
476 host, port = singleton.getinfo()
477 else:
478 host = host_params[0]
479 port = int(host_params[1])
480
481 try:
482 connection = PRServerConnection(host,port)
483 connection.ping()
484 realhost, realport = connection.getinfo()
485 return str(realhost) + ":" + str(realport)
486
487 except Exception:
488 logger.critical("PRservice %s:%d not available" % (host, port))
489 raise PRServiceConfigError
490
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500491def auto_shutdown():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500492 global singleton
493 if singleton:
494 host, port = singleton.getinfo()
495 try:
496 PRServerConnection(host, port).terminate()
497 except:
498 logger.critical("Stop PRService %s:%d failed" % (host,port))
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500499
500 try:
501 os.waitpid(singleton.prserv.pid, 0)
502 except ChildProcessError:
503 pass
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500504 singleton = None
505
506def ping(host, port):
507 conn=PRServerConnection(host, port)
508 return conn.ping()