blob: 5e322bf83de12e79b7b3fff9e718c169f29e5369 [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 Williamsf1e5d692016-03-30 15:21:19 -05008import socket
Patrick Williamsc0f7c042017-02-23 20:41:17 -06009import io
Brad Bishop6e60e8b2018-02-01 10:27:11 -050010import sqlite3
Brad Bishopd7bf8c12018-02-25 22:55:05 -050011import bb.server.xmlrpcclient
Patrick Williamsc124f4f2015-09-15 14:41:29 -050012import prserv
13import prserv.db
14import errno
Andrew Geisslerc926e172021-05-07 16:11:35 -050015import multiprocessing
Patrick Williamsc124f4f2015-09-15 14:41:29 -050016
17logger = logging.getLogger("BitBake.PRserv")
18
Patrick Williamsc124f4f2015-09-15 14:41:29 -050019class Handler(SimpleXMLRPCRequestHandler):
20 def _dispatch(self,method,params):
21 try:
22 value=self.server.funcs[method](*params)
23 except:
24 import traceback
25 traceback.print_exc()
26 raise
27 return value
28
29PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
30singleton = None
31
32
33class PRServer(SimpleXMLRPCServer):
Andrew Geisslerc926e172021-05-07 16:11:35 -050034 def __init__(self, dbfile, logfile, interface):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050035 ''' constructor '''
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036 try:
37 SimpleXMLRPCServer.__init__(self, interface,
38 logRequests=False, allow_none=True)
39 except socket.error:
40 ip=socket.gethostbyname(interface[0])
41 port=interface[1]
42 msg="PR Server unable to bind to %s:%s\n" % (ip, port)
43 sys.stderr.write(msg)
44 raise PRServiceConfigError
45
46 self.dbfile=dbfile
Patrick Williamsc124f4f2015-09-15 14:41:29 -050047 self.logfile=logfile
Patrick Williamsc124f4f2015-09-15 14:41:29 -050048 self.host, self.port = self.socket.getsockname()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050049
50 self.register_function(self.getPR, "getPR")
Patrick Williamsc124f4f2015-09-15 14:41:29 -050051 self.register_function(self.ping, "ping")
52 self.register_function(self.export, "export")
53 self.register_function(self.importone, "importone")
54 self.register_introspection_functions()
55
Andrew Geisslerc926e172021-05-07 16:11:35 -050056 self.iter_count = 0
Patrick Williamsc124f4f2015-09-15 14:41:29 -050057 # 60 iterations between syncs or sync if dirty every ~30 seconds
Andrew Geisslerc926e172021-05-07 16:11:35 -050058 self.iterations_between_sync = 60
Patrick Williamsc124f4f2015-09-15 14:41:29 -050059
60 def sigint_handler(self, signum, stack):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050061 if self.table:
62 self.table.sync()
Patrick Williamsc124f4f2015-09-15 14:41:29 -050063
64 def sigterm_handler(self, signum, stack):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050065 if self.table:
66 self.table.sync()
Andrew Geisslerc926e172021-05-07 16:11:35 -050067 raise(SystemExit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050068
69 def process_request(self, request, client_address):
Andrew Geisslerc926e172021-05-07 16:11:35 -050070 if request is None:
71 return
72 try:
73 self.finish_request(request, client_address)
74 self.shutdown_request(request)
75 self.iter_count = (self.iter_count + 1) % self.iterations_between_sync
76 if self.iter_count == 0:
77 self.table.sync_if_dirty()
78 except:
79 self.handle_error(request, client_address)
80 self.shutdown_request(request)
81 self.table.sync()
82 self.table.sync_if_dirty()
83
84 def serve_forever(self, poll_interval=0.5):
85 signal.signal(signal.SIGINT, self.sigint_handler)
86 signal.signal(signal.SIGTERM, self.sigterm_handler)
87
88 self.db = prserv.db.PRData(self.dbfile)
89 self.table = self.db["PRMAIN"]
90 return super().serve_forever(poll_interval)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050091
92 def export(self, version=None, pkgarch=None, checksum=None, colinfo=True):
93 try:
94 return self.table.export(version, pkgarch, checksum, colinfo)
95 except sqlite3.Error as exc:
96 logger.error(str(exc))
97 return None
98
99 def importone(self, version, pkgarch, checksum, value):
100 return self.table.importone(version, pkgarch, checksum, value)
101
102 def ping(self):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500103 return True
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500104
105 def getinfo(self):
106 return (self.host, self.port)
107
108 def getPR(self, version, pkgarch, checksum):
109 try:
110 return self.table.getValue(version, pkgarch, checksum)
111 except prserv.NotFoundError:
112 logger.error("can not find value for (%s, %s)",version, checksum)
113 return None
114 except sqlite3.Error as exc:
115 logger.error(str(exc))
116 return None
117
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500118class PRServSingleton(object):
119 def __init__(self, dbfile, logfile, interface):
120 self.dbfile = dbfile
121 self.logfile = logfile
122 self.interface = interface
123 self.host = None
124 self.port = None
125
126 def start(self):
Andrew Geisslerc926e172021-05-07 16:11:35 -0500127 self.prserv = PRServer(self.dbfile, self.logfile, self.interface)
128 self.process = multiprocessing.Process(target=self.prserv.serve_forever)
129 self.process.start()
130
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500131 self.host, self.port = self.prserv.getinfo()
132
133 def getinfo(self):
134 return (self.host, self.port)
135
136class PRServerConnection(object):
137 def __init__(self, host, port):
138 if is_local_special(host, port):
139 host, port = singleton.getinfo()
140 self.host = host
141 self.port = port
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500142 self.connection, self.transport = bb.server.xmlrpcclient._create_server(self.host, self.port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500144 def getPR(self, version, pkgarch, checksum):
145 return self.connection.getPR(version, pkgarch, checksum)
146
147 def ping(self):
148 return self.connection.ping()
149
150 def export(self,version=None, pkgarch=None, checksum=None, colinfo=True):
151 return self.connection.export(version, pkgarch, checksum, colinfo)
152
153 def importone(self, version, pkgarch, checksum, value):
154 return self.connection.importone(version, pkgarch, checksum, value)
155
156 def getinfo(self):
157 return self.host, self.port
158
Andrew Geisslerc926e172021-05-07 16:11:35 -0500159def run_as_daemon(func, pidfile, logfile):
160 """
161 See Advanced Programming in the UNIX, Sec 13.3
162 """
163 try:
164 pid = os.fork()
165 if pid > 0:
166 os.waitpid(pid, 0)
167 #parent return instead of exit to give control
168 return pid
169 except OSError as e:
170 raise Exception("%s [%d]" % (e.strerror, e.errno))
171
172 os.setsid()
173 """
174 fork again to make sure the daemon is not session leader,
175 which prevents it from acquiring controlling terminal
176 """
177 try:
178 pid = os.fork()
179 if pid > 0: #parent
180 os._exit(0)
181 except OSError as e:
182 raise Exception("%s [%d]" % (e.strerror, e.errno))
183
184 os.chdir("/")
185
186 sys.stdout.flush()
187 sys.stderr.flush()
188
189 # We could be called from a python thread with io.StringIO as
190 # stdout/stderr or it could be 'real' unix fd forking where we need
191 # to physically close the fds to prevent the program launching us from
192 # potentially hanging on a pipe. Handle both cases.
193 si = open('/dev/null', 'r')
194 try:
195 os.dup2(si.fileno(),sys.stdin.fileno())
196 except (AttributeError, io.UnsupportedOperation):
197 sys.stdin = si
198 so = open(logfile, 'a+')
199 try:
200 os.dup2(so.fileno(),sys.stdout.fileno())
201 except (AttributeError, io.UnsupportedOperation):
202 sys.stdout = so
203 try:
204 os.dup2(so.fileno(),sys.stderr.fileno())
205 except (AttributeError, io.UnsupportedOperation):
206 sys.stderr = so
207
208 # Clear out all log handlers prior to the fork() to avoid calling
209 # event handlers not part of the PRserver
210 for logger_iter in logging.Logger.manager.loggerDict.keys():
211 logging.getLogger(logger_iter).handlers = []
212
213 # Ensure logging makes it to the logfile
214 streamhandler = logging.StreamHandler()
215 streamhandler.setLevel(logging.DEBUG)
216 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
217 streamhandler.setFormatter(formatter)
218 logger.addHandler(streamhandler)
219
220 # write pidfile
221 pid = str(os.getpid())
222 with open(pidfile, 'w') as pf:
223 pf.write("%s\n" % pid)
224
225 func()
226 os.remove(pidfile)
227 os._exit(0)
228
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500229def start_daemon(dbfile, host, port, logfile):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500230 ip = socket.gethostbyname(host)
231 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500232 try:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500233 with open(pidfile) as pf:
234 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500235 except IOError:
236 pid = None
237
238 if pid:
239 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
240 % pidfile)
241 return 1
242
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500243 server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port))
Andrew Geisslerc926e172021-05-07 16:11:35 -0500244 run_as_daemon(server.serve_forever, pidfile, os.path.abspath(logfile))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500245
246 # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with
247 # the one the server actually is listening, so at least warn the user about it
248 _,rport = server.getinfo()
249 if port != rport:
250 sys.stdout.write("Server is listening at port %s instead of %s\n"
251 % (rport,port))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500252 return 0
253
254def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500255 import glob
256 ip = socket.gethostbyname(host)
257 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500258 try:
Brad Bishop64c979e2019-11-04 13:55:29 -0500259 with open(pidfile) as pf:
260 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500261 except IOError:
262 pid = None
263
264 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500265 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
266 # so at least advise the user which ports the corresponding server is listening
267 ports = []
268 portstr = ""
269 for pf in glob.glob(PIDPREFIX % (ip,'*')):
270 bn = os.path.basename(pf)
271 root, _ = os.path.splitext(bn)
272 ports.append(root.split('_')[-1])
273 if len(ports):
274 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
275
276 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
277 % (pidfile,portstr))
278 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500279
280 try:
Andrew Geisslerc926e172021-05-07 16:11:35 -0500281 if is_running(pid):
282 print("Sending SIGTERM to pr-server.")
283 os.kill(pid, signal.SIGTERM)
284 time.sleep(0.1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500285
Andrew Geisslerc926e172021-05-07 16:11:35 -0500286 if os.path.exists(pidfile):
287 os.remove(pidfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500288
289 except OSError as e:
290 err = str(e)
291 if err.find("No such process") <= 0:
292 raise e
293
294 return 0
295
296def is_running(pid):
297 try:
298 os.kill(pid, 0)
299 except OSError as err:
300 if err.errno == errno.ESRCH:
301 return False
302 return True
303
304def is_local_special(host, port):
305 if host.strip().upper() == 'localhost'.upper() and (not port):
306 return True
307 else:
308 return False
309
310class PRServiceConfigError(Exception):
311 pass
312
313def auto_start(d):
314 global singleton
315
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500316 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':')))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500317 if not host_params:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500318 # Shutdown any existing PR Server
319 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500320 return None
321
322 if len(host_params) != 2:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500323 # Shutdown any existing PR Server
324 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325 logger.critical('\n'.join(['PRSERV_HOST: incorrect format',
326 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
327 raise PRServiceConfigError
328
Andrew Geissler82c905d2020-04-13 13:39:40 -0500329 if is_local_special(host_params[0], int(host_params[1])):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500330 import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500331 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500332 if not cachedir:
333 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
334 raise PRServiceConfigError
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500335 dbfile = os.path.join(cachedir, "prserv.sqlite3")
336 logfile = os.path.join(cachedir, "prserv.log")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500337 if singleton:
338 if singleton.dbfile != dbfile:
339 # Shutdown any existing PR Server as doesn't match config
340 auto_shutdown()
341 if not singleton:
342 bb.utils.mkdirhier(cachedir)
343 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), ("localhost",0))
344 singleton.start()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500345 if singleton:
346 host, port = singleton.getinfo()
347 else:
348 host = host_params[0]
349 port = int(host_params[1])
350
351 try:
352 connection = PRServerConnection(host,port)
353 connection.ping()
354 realhost, realport = connection.getinfo()
355 return str(realhost) + ":" + str(realport)
356
357 except Exception:
358 logger.critical("PRservice %s:%d not available" % (host, port))
359 raise PRServiceConfigError
360
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500361def auto_shutdown():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500362 global singleton
Andrew Geisslerc926e172021-05-07 16:11:35 -0500363 if singleton and singleton.process:
364 singleton.process.terminate()
365 singleton.process.join()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500366 singleton = None
367
368def ping(host, port):
369 conn=PRServerConnection(host, port)
370 return conn.ping()
Andrew Geisslerc926e172021-05-07 16:11:35 -0500371
372def connect(host, port):
373 return PRServerConnection(host, port)