blob: b854ba14b7ee0bc232b80b63b3c0f44149917102 [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
295 pid = str(os.getpid())
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600296 pf = open(self.pidfile, 'w')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500297 pf.write("%s\n" % pid)
298 pf.close()
299
300 self.work_forever()
301 self.delpid()
302
303class PRServSingleton(object):
304 def __init__(self, dbfile, logfile, interface):
305 self.dbfile = dbfile
306 self.logfile = logfile
307 self.interface = interface
308 self.host = None
309 self.port = None
310
311 def start(self):
312 self.prserv = PRServer(self.dbfile, self.logfile, self.interface, daemon=False)
313 self.prserv.start()
314 self.host, self.port = self.prserv.getinfo()
315
316 def getinfo(self):
317 return (self.host, self.port)
318
319class PRServerConnection(object):
320 def __init__(self, host, port):
321 if is_local_special(host, port):
322 host, port = singleton.getinfo()
323 self.host = host
324 self.port = port
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500325 self.connection, self.transport = bb.server.xmlrpcclient._create_server(self.host, self.port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500326
327 def terminate(self):
328 try:
329 logger.info("Terminating PRServer...")
330 self.connection.quit()
331 except Exception as exc:
332 sys.stderr.write("%s\n" % str(exc))
333
334 def getPR(self, version, pkgarch, checksum):
335 return self.connection.getPR(version, pkgarch, checksum)
336
337 def ping(self):
338 return self.connection.ping()
339
340 def export(self,version=None, pkgarch=None, checksum=None, colinfo=True):
341 return self.connection.export(version, pkgarch, checksum, colinfo)
342
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500343 def dump_db(self):
344 return self.connection.dump_db()
345
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500346 def importone(self, version, pkgarch, checksum, value):
347 return self.connection.importone(version, pkgarch, checksum, value)
348
349 def getinfo(self):
350 return self.host, self.port
351
352def start_daemon(dbfile, host, port, logfile):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500353 ip = socket.gethostbyname(host)
354 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500355 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600356 pf = open(pidfile,'r')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500357 pid = int(pf.readline().strip())
358 pf.close()
359 except IOError:
360 pid = None
361
362 if pid:
363 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
364 % pidfile)
365 return 1
366
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500367 server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500368 server.start()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500369
370 # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with
371 # the one the server actually is listening, so at least warn the user about it
372 _,rport = server.getinfo()
373 if port != rport:
374 sys.stdout.write("Server is listening at port %s instead of %s\n"
375 % (rport,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500376 return 0
377
378def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500379 import glob
380 ip = socket.gethostbyname(host)
381 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500382 try:
Brad Bishop64c979e2019-11-04 13:55:29 -0500383 with open(pidfile) as pf:
384 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500385 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()