blob: 25dcf8a0ee7f1256ecea14ec8ea4d4b4cda95fa3 [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:
Brad Bishop1d80a2e2019-11-15 16:35:03 -0500246 self.socket.close() # avoid ResourceWarning in parent
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500247 return pid
248 except OSError as e:
249 raise Exception("%s [%d]" % (e.strerror, e.errno))
250
251 bb.utils.signal_on_parent_exit("SIGTERM")
252 self.cleanup_handles()
253 os._exit(0)
254
255 def cleanup_handles(self):
256 signal.signal(signal.SIGINT, self.sigint_handler)
257 signal.signal(signal.SIGTERM, self.sigterm_handler)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500258 os.chdir("/")
259
260 sys.stdout.flush()
261 sys.stderr.flush()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500262
263 # We could be called from a python thread with io.StringIO as
264 # stdout/stderr or it could be 'real' unix fd forking where we need
265 # to physically close the fds to prevent the program launching us from
266 # potentially hanging on a pipe. Handle both cases.
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600267 si = open('/dev/null', 'r')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500268 try:
269 os.dup2(si.fileno(),sys.stdin.fileno())
270 except (AttributeError, io.UnsupportedOperation):
271 sys.stdin = si
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600272 so = open(self.logfile, 'a+')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500273 try:
274 os.dup2(so.fileno(),sys.stdout.fileno())
275 except (AttributeError, io.UnsupportedOperation):
276 sys.stdout = so
277 try:
278 os.dup2(so.fileno(),sys.stderr.fileno())
279 except (AttributeError, io.UnsupportedOperation):
280 sys.stderr = so
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500281
282 # Clear out all log handlers prior to the fork() to avoid calling
283 # event handlers not part of the PRserver
284 for logger_iter in logging.Logger.manager.loggerDict.keys():
285 logging.getLogger(logger_iter).handlers = []
286
287 # Ensure logging makes it to the logfile
288 streamhandler = logging.StreamHandler()
289 streamhandler.setLevel(logging.DEBUG)
290 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
291 streamhandler.setFormatter(formatter)
292 logger.addHandler(streamhandler)
293
294 # write pidfile
Andrew Geissler82c905d2020-04-13 13:39:40 -0500295 pid = str(os.getpid())
296 with open(self.pidfile, 'w') as pf:
297 pf.write("%s\n" % pid)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500298
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:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500355 with open(pidfile) as pf:
356 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500357 except IOError:
358 pid = None
359
360 if pid:
361 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
362 % pidfile)
363 return 1
364
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500365 server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500366 server.start()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500367
368 # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with
369 # the one the server actually is listening, so at least warn the user about it
370 _,rport = server.getinfo()
371 if port != rport:
372 sys.stdout.write("Server is listening at port %s instead of %s\n"
373 % (rport,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500374 return 0
375
376def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500377 import glob
378 ip = socket.gethostbyname(host)
379 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500380 try:
Brad Bishop64c979e2019-11-04 13:55:29 -0500381 with open(pidfile) as pf:
382 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500383 except IOError:
384 pid = None
385
386 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500387 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
388 # so at least advise the user which ports the corresponding server is listening
389 ports = []
390 portstr = ""
391 for pf in glob.glob(PIDPREFIX % (ip,'*')):
392 bn = os.path.basename(pf)
393 root, _ = os.path.splitext(bn)
394 ports.append(root.split('_')[-1])
395 if len(ports):
396 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
397
398 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
399 % (pidfile,portstr))
400 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500401
402 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500403 PRServerConnection(ip, port).terminate()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500404 except:
405 logger.critical("Stop PRService %s:%d failed" % (host,port))
406
407 try:
408 if pid:
409 wait_timeout = 0
410 print("Waiting for pr-server to exit.")
411 while is_running(pid) and wait_timeout < 50:
412 time.sleep(0.1)
413 wait_timeout += 1
414
415 if is_running(pid):
416 print("Sending SIGTERM to pr-server.")
417 os.kill(pid,signal.SIGTERM)
418 time.sleep(0.1)
419
420 if os.path.exists(pidfile):
421 os.remove(pidfile)
422
423 except OSError as e:
424 err = str(e)
425 if err.find("No such process") <= 0:
426 raise e
427
428 return 0
429
430def is_running(pid):
431 try:
432 os.kill(pid, 0)
433 except OSError as err:
434 if err.errno == errno.ESRCH:
435 return False
436 return True
437
438def is_local_special(host, port):
439 if host.strip().upper() == 'localhost'.upper() and (not port):
440 return True
441 else:
442 return False
443
444class PRServiceConfigError(Exception):
445 pass
446
447def auto_start(d):
448 global singleton
449
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500450 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':')))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500451 if not host_params:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500452 # Shutdown any existing PR Server
453 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500454 return None
455
456 if len(host_params) != 2:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500457 # Shutdown any existing PR Server
458 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500459 logger.critical('\n'.join(['PRSERV_HOST: incorrect format',
460 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
461 raise PRServiceConfigError
462
Andrew Geissler82c905d2020-04-13 13:39:40 -0500463 if is_local_special(host_params[0], int(host_params[1])):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500464 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500469 dbfile = os.path.join(cachedir, "prserv.sqlite3")
470 logfile = os.path.join(cachedir, "prserv.log")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500471 if singleton:
472 if singleton.dbfile != dbfile:
473 # Shutdown any existing PR Server as doesn't match config
474 auto_shutdown()
475 if not singleton:
476 bb.utils.mkdirhier(cachedir)
477 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), ("localhost",0))
478 singleton.start()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500479 if singleton:
480 host, port = singleton.getinfo()
481 else:
482 host = host_params[0]
483 port = int(host_params[1])
484
485 try:
486 connection = PRServerConnection(host,port)
487 connection.ping()
488 realhost, realport = connection.getinfo()
489 return str(realhost) + ":" + str(realport)
490
491 except Exception:
492 logger.critical("PRservice %s:%d not available" % (host, port))
493 raise PRServiceConfigError
494
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500495def auto_shutdown():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500496 global singleton
497 if singleton:
498 host, port = singleton.getinfo()
499 try:
500 PRServerConnection(host, port).terminate()
501 except:
502 logger.critical("Stop PRService %s:%d failed" % (host,port))
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500503
504 try:
505 os.waitpid(singleton.prserv.pid, 0)
506 except ChildProcessError:
507 pass
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500508 singleton = None
509
510def ping(host, port):
511 conn=PRServerConnection(host, port)
512 return conn.ping()