blob: 2bc68904f332f69b2eba7b1e635448ef31fc780f [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:
Brad Bishop64c979e2019-11-04 13:55:29 -0500382 with open(pidfile) as pf:
383 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500384 except IOError:
385 pid = None
386
387 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500388 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
389 # so at least advise the user which ports the corresponding server is listening
390 ports = []
391 portstr = ""
392 for pf in glob.glob(PIDPREFIX % (ip,'*')):
393 bn = os.path.basename(pf)
394 root, _ = os.path.splitext(bn)
395 ports.append(root.split('_')[-1])
396 if len(ports):
397 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
398
399 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
400 % (pidfile,portstr))
401 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500402
403 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500404 PRServerConnection(ip, port).terminate()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500405 except:
406 logger.critical("Stop PRService %s:%d failed" % (host,port))
407
408 try:
409 if pid:
410 wait_timeout = 0
411 print("Waiting for pr-server to exit.")
412 while is_running(pid) and wait_timeout < 50:
413 time.sleep(0.1)
414 wait_timeout += 1
415
416 if is_running(pid):
417 print("Sending SIGTERM to pr-server.")
418 os.kill(pid,signal.SIGTERM)
419 time.sleep(0.1)
420
421 if os.path.exists(pidfile):
422 os.remove(pidfile)
423
424 except OSError as e:
425 err = str(e)
426 if err.find("No such process") <= 0:
427 raise e
428
429 return 0
430
431def is_running(pid):
432 try:
433 os.kill(pid, 0)
434 except OSError as err:
435 if err.errno == errno.ESRCH:
436 return False
437 return True
438
439def is_local_special(host, port):
440 if host.strip().upper() == 'localhost'.upper() and (not port):
441 return True
442 else:
443 return False
444
445class PRServiceConfigError(Exception):
446 pass
447
448def auto_start(d):
449 global singleton
450
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500451 # Shutdown any existing PR Server
452 auto_shutdown()
453
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500454 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':')))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500455 if not host_params:
456 return None
457
458 if len(host_params) != 2:
459 logger.critical('\n'.join(['PRSERV_HOST: incorrect format',
460 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
461 raise PRServiceConfigError
462
463 if is_local_special(host_params[0], int(host_params[1])) and not singleton:
464 import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500465 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500466 if not cachedir:
467 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
468 raise PRServiceConfigError
469 bb.utils.mkdirhier(cachedir)
470 dbfile = os.path.join(cachedir, "prserv.sqlite3")
471 logfile = os.path.join(cachedir, "prserv.log")
472 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), ("localhost",0))
473 singleton.start()
474 if singleton:
475 host, port = singleton.getinfo()
476 else:
477 host = host_params[0]
478 port = int(host_params[1])
479
480 try:
481 connection = PRServerConnection(host,port)
482 connection.ping()
483 realhost, realport = connection.getinfo()
484 return str(realhost) + ":" + str(realport)
485
486 except Exception:
487 logger.critical("PRservice %s:%d not available" % (host, port))
488 raise PRServiceConfigError
489
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500490def auto_shutdown():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500491 global singleton
492 if singleton:
493 host, port = singleton.getinfo()
494 try:
495 PRServerConnection(host, port).terminate()
496 except:
497 logger.critical("Stop PRService %s:%d failed" % (host,port))
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500498
499 try:
500 os.waitpid(singleton.prserv.pid, 0)
501 except ChildProcessError:
502 pass
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500503 singleton = None
504
505def ping(host, port):
506 conn=PRServerConnection(host, port)
507 return conn.ping()