blob: dc4be5b620665d0711c2b4f3ae714ce193e7929d [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
Patrick Williams92b42cb2022-09-03 06:53:57 -05002# Copyright BitBake Contributors
3#
Brad Bishopc342db32019-05-15 21:57:59 -04004# SPDX-License-Identifier: GPL-2.0-only
5#
6
Patrick Williamsc124f4f2015-09-15 14:41:29 -05007import os,sys,logging
8import signal, time
Patrick Williamsf1e5d692016-03-30 15:21:19 -05009import socket
Patrick Williamsc0f7c042017-02-23 20:41:17 -060010import io
Brad Bishop6e60e8b2018-02-01 10:27:11 -050011import sqlite3
Patrick Williamsc124f4f2015-09-15 14:41:29 -050012import prserv
13import prserv.db
14import errno
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050015import bb.asyncrpc
Patrick Williamsc124f4f2015-09-15 14:41:29 -050016
17logger = logging.getLogger("BitBake.PRserv")
18
Patrick Williamsc124f4f2015-09-15 14:41:29 -050019PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
20singleton = None
21
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050022class PRServerClient(bb.asyncrpc.AsyncServerConnection):
Patrick Williams44b3caf2024-04-12 16:51:14 -050023 def __init__(self, socket, server):
24 super().__init__(socket, "PRSERVICE", server.logger)
25 self.server = server
26
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050027 self.handlers.update({
Patrick Williams44b3caf2024-04-12 16:51:14 -050028 "get-pr": self.handle_get_pr,
29 "test-pr": self.handle_test_pr,
30 "test-package": self.handle_test_package,
31 "max-package-pr": self.handle_max_package_pr,
32 "import-one": self.handle_import_one,
33 "export": self.handle_export,
34 "is-readonly": self.handle_is_readonly,
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050035 })
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050037 def validate_proto_version(self):
38 return (self.proto_version == (1, 0))
39
40 async def dispatch_message(self, msg):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050041 try:
Patrick Williamsac13d5f2023-11-24 18:59:46 -060042 return await super().dispatch_message(msg)
Andrew Geisslerc926e172021-05-07 16:11:35 -050043 except:
Patrick Williams44b3caf2024-04-12 16:51:14 -050044 self.server.table.sync()
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050045 raise
Patrick Williamsac13d5f2023-11-24 18:59:46 -060046 else:
Patrick Williams44b3caf2024-04-12 16:51:14 -050047 self.server.table.sync_if_dirty()
48
49 async def handle_test_pr(self, request):
50 '''Finds the PR value corresponding to the request. If not found, returns None and doesn't insert a new value'''
51 version = request["version"]
52 pkgarch = request["pkgarch"]
53 checksum = request["checksum"]
54
55 value = self.server.table.find_value(version, pkgarch, checksum)
56 return {"value": value}
57
58 async def handle_test_package(self, request):
59 '''Tells whether there are entries for (version, pkgarch) in the db. Returns True or False'''
60 version = request["version"]
61 pkgarch = request["pkgarch"]
62
63 value = self.server.table.test_package(version, pkgarch)
64 return {"value": value}
65
66 async def handle_max_package_pr(self, request):
67 '''Finds the greatest PR value for (version, pkgarch) in the db. Returns None if no entry was found'''
68 version = request["version"]
69 pkgarch = request["pkgarch"]
70
71 value = self.server.table.find_max_value(version, pkgarch)
72 return {"value": value}
Andrew Geisslerc926e172021-05-07 16:11:35 -050073
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050074 async def handle_get_pr(self, request):
Patrick Williams44b3caf2024-04-12 16:51:14 -050075 version = request["version"]
76 pkgarch = request["pkgarch"]
77 checksum = request["checksum"]
Andrew Geisslerc926e172021-05-07 16:11:35 -050078
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050079 response = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -050080 try:
Patrick Williams44b3caf2024-04-12 16:51:14 -050081 value = self.server.table.get_value(version, pkgarch, checksum)
82 response = {"value": value}
Patrick Williamsc124f4f2015-09-15 14:41:29 -050083 except prserv.NotFoundError:
Patrick Williams44b3caf2024-04-12 16:51:14 -050084 self.logger.error("failure storing value in database for (%s, %s)",version, checksum)
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050085
Patrick Williamsac13d5f2023-11-24 18:59:46 -060086 return response
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050087
88 async def handle_import_one(self, request):
89 response = None
Patrick Williams44b3caf2024-04-12 16:51:14 -050090 if not self.server.read_only:
91 version = request["version"]
92 pkgarch = request["pkgarch"]
93 checksum = request["checksum"]
94 value = request["value"]
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050095
Patrick Williams44b3caf2024-04-12 16:51:14 -050096 value = self.server.table.importone(version, pkgarch, checksum, value)
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050097 if value is not None:
Patrick Williams44b3caf2024-04-12 16:51:14 -050098 response = {"value": value}
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050099
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600100 return response
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500101
102 async def handle_export(self, request):
Patrick Williams44b3caf2024-04-12 16:51:14 -0500103 version = request["version"]
104 pkgarch = request["pkgarch"]
105 checksum = request["checksum"]
106 colinfo = request["colinfo"]
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500107
108 try:
Patrick Williams44b3caf2024-04-12 16:51:14 -0500109 (metainfo, datainfo) = self.server.table.export(version, pkgarch, checksum, colinfo)
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500110 except sqlite3.Error as exc:
Patrick Williams44b3caf2024-04-12 16:51:14 -0500111 self.logger.error(str(exc))
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500112 metainfo = datainfo = None
113
Patrick Williams44b3caf2024-04-12 16:51:14 -0500114 return {"metainfo": metainfo, "datainfo": datainfo}
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500115
116 async def handle_is_readonly(self, request):
Patrick Williams44b3caf2024-04-12 16:51:14 -0500117 return {"readonly": self.server.read_only}
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500118
119class PRServer(bb.asyncrpc.AsyncServer):
120 def __init__(self, dbfile, read_only=False):
121 super().__init__(logger)
122 self.dbfile = dbfile
123 self.table = None
124 self.read_only = read_only
125
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600126 def accept_client(self, socket):
Patrick Williams44b3caf2024-04-12 16:51:14 -0500127 return PRServerClient(socket, self)
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500128
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600129 def start(self):
130 tasks = super().start()
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500131 self.db = prserv.db.PRData(self.dbfile, read_only=self.read_only)
132 self.table = self.db["PRMAIN"]
133
Patrick Williams44b3caf2024-04-12 16:51:14 -0500134 self.logger.info("Started PRServer with DBfile: %s, Address: %s, PID: %s" %
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500135 (self.dbfile, self.address, str(os.getpid())))
136
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600137 return tasks
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500138
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600139 async def stop(self):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500140 self.table.sync_if_dirty()
141 self.db.disconnect()
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600142 await super().stop()
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500143
144 def signal_handler(self):
145 super().signal_handler()
146 if self.table:
147 self.table.sync()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500149class PRServSingleton(object):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500150 def __init__(self, dbfile, logfile, host, port):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500151 self.dbfile = dbfile
152 self.logfile = logfile
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500153 self.host = host
154 self.port = port
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500155
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500156 def start(self):
157 self.prserv = PRServer(self.dbfile)
158 self.prserv.start_tcp_server(socket.gethostbyname(self.host), self.port)
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600159 self.process = self.prserv.serve_as_process(log_level=logging.WARNING)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500160
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500161 if not self.prserv.address:
162 raise PRServiceConfigError
163 if not self.port:
Patrick Williams44b3caf2024-04-12 16:51:14 -0500164 self.port = int(self.prserv.address.rsplit(":", 1)[1])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500165
Andrew Geisslerc926e172021-05-07 16:11:35 -0500166def run_as_daemon(func, pidfile, logfile):
167 """
168 See Advanced Programming in the UNIX, Sec 13.3
169 """
170 try:
171 pid = os.fork()
172 if pid > 0:
173 os.waitpid(pid, 0)
174 #parent return instead of exit to give control
175 return pid
176 except OSError as e:
177 raise Exception("%s [%d]" % (e.strerror, e.errno))
178
179 os.setsid()
180 """
181 fork again to make sure the daemon is not session leader,
182 which prevents it from acquiring controlling terminal
183 """
184 try:
185 pid = os.fork()
186 if pid > 0: #parent
187 os._exit(0)
188 except OSError as e:
189 raise Exception("%s [%d]" % (e.strerror, e.errno))
190
191 os.chdir("/")
192
193 sys.stdout.flush()
194 sys.stderr.flush()
195
196 # We could be called from a python thread with io.StringIO as
197 # stdout/stderr or it could be 'real' unix fd forking where we need
198 # to physically close the fds to prevent the program launching us from
199 # potentially hanging on a pipe. Handle both cases.
Patrick Williams44b3caf2024-04-12 16:51:14 -0500200 si = open("/dev/null", "r")
Andrew Geisslerc926e172021-05-07 16:11:35 -0500201 try:
Patrick Williams44b3caf2024-04-12 16:51:14 -0500202 os.dup2(si.fileno(), sys.stdin.fileno())
Andrew Geisslerc926e172021-05-07 16:11:35 -0500203 except (AttributeError, io.UnsupportedOperation):
204 sys.stdin = si
Patrick Williams44b3caf2024-04-12 16:51:14 -0500205 so = open(logfile, "a+")
Andrew Geisslerc926e172021-05-07 16:11:35 -0500206 try:
Patrick Williams44b3caf2024-04-12 16:51:14 -0500207 os.dup2(so.fileno(), sys.stdout.fileno())
Andrew Geisslerc926e172021-05-07 16:11:35 -0500208 except (AttributeError, io.UnsupportedOperation):
209 sys.stdout = so
210 try:
Patrick Williams44b3caf2024-04-12 16:51:14 -0500211 os.dup2(so.fileno(), sys.stderr.fileno())
Andrew Geisslerc926e172021-05-07 16:11:35 -0500212 except (AttributeError, io.UnsupportedOperation):
213 sys.stderr = so
214
215 # Clear out all log handlers prior to the fork() to avoid calling
216 # event handlers not part of the PRserver
217 for logger_iter in logging.Logger.manager.loggerDict.keys():
218 logging.getLogger(logger_iter).handlers = []
219
220 # Ensure logging makes it to the logfile
221 streamhandler = logging.StreamHandler()
222 streamhandler.setLevel(logging.DEBUG)
223 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
224 streamhandler.setFormatter(formatter)
225 logger.addHandler(streamhandler)
226
227 # write pidfile
228 pid = str(os.getpid())
Patrick Williams44b3caf2024-04-12 16:51:14 -0500229 with open(pidfile, "w") as pf:
Andrew Geisslerc926e172021-05-07 16:11:35 -0500230 pf.write("%s\n" % pid)
231
232 func()
233 os.remove(pidfile)
234 os._exit(0)
235
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500236def start_daemon(dbfile, host, port, logfile, read_only=False):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500237 ip = socket.gethostbyname(host)
238 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500239 try:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500240 with open(pidfile) as pf:
241 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500242 except IOError:
243 pid = None
244
245 if pid:
246 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
247 % pidfile)
248 return 1
249
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500250 dbfile = os.path.abspath(dbfile)
251 def daemon_main():
252 server = PRServer(dbfile, read_only=read_only)
253 server.start_tcp_server(ip, port)
254 server.serve_forever()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500255
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500256 run_as_daemon(daemon_main, pidfile, os.path.abspath(logfile))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500257 return 0
258
259def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500260 import glob
261 ip = socket.gethostbyname(host)
262 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500263 try:
Brad Bishop64c979e2019-11-04 13:55:29 -0500264 with open(pidfile) as pf:
265 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500266 except IOError:
267 pid = None
268
269 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500270 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
271 # so at least advise the user which ports the corresponding server is listening
272 ports = []
273 portstr = ""
Patrick Williams44b3caf2024-04-12 16:51:14 -0500274 for pf in glob.glob(PIDPREFIX % (ip, "*")):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500275 bn = os.path.basename(pf)
276 root, _ = os.path.splitext(bn)
Patrick Williams44b3caf2024-04-12 16:51:14 -0500277 ports.append(root.split("_")[-1])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500278 if len(ports):
Patrick Williams44b3caf2024-04-12 16:51:14 -0500279 portstr = "Wrong port? Other ports listening at %s: %s" % (host, " ".join(ports))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500280
281 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
Patrick Williams44b3caf2024-04-12 16:51:14 -0500282 % (pidfile, portstr))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500283 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500284
285 try:
Andrew Geisslerc926e172021-05-07 16:11:35 -0500286 if is_running(pid):
287 print("Sending SIGTERM to pr-server.")
288 os.kill(pid, signal.SIGTERM)
289 time.sleep(0.1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500290
Patrick Williamsb58112e2024-03-07 11:16:36 -0600291 try:
Andrew Geisslerc926e172021-05-07 16:11:35 -0500292 os.remove(pidfile)
Patrick Williamsb58112e2024-03-07 11:16:36 -0600293 except FileNotFoundError:
294 # The PID file might have been removed by the exiting process
295 pass
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296
297 except OSError as e:
298 err = str(e)
299 if err.find("No such process") <= 0:
300 raise e
301
302 return 0
303
304def is_running(pid):
305 try:
306 os.kill(pid, 0)
307 except OSError as err:
308 if err.errno == errno.ESRCH:
309 return False
310 return True
311
312def is_local_special(host, port):
Patrick Williams44b3caf2024-04-12 16:51:14 -0500313 if (host == "localhost" or host == "127.0.0.1") and not port:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500314 return True
315 else:
316 return False
317
318class PRServiceConfigError(Exception):
319 pass
320
321def auto_start(d):
322 global singleton
323
Patrick Williams44b3caf2024-04-12 16:51:14 -0500324 host_params = list(filter(None, (d.getVar("PRSERV_HOST") or "").split(":")))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325 if not host_params:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500326 # Shutdown any existing PR Server
327 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500328 return None
329
330 if len(host_params) != 2:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500331 # Shutdown any existing PR Server
332 auto_shutdown()
Patrick Williams44b3caf2024-04-12 16:51:14 -0500333 logger.critical("\n".join(["PRSERV_HOST: incorrect format",
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
335 raise PRServiceConfigError
336
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500337 host = host_params[0].strip().lower()
338 port = int(host_params[1])
339 if is_local_special(host, port):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500340 import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500341 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500342 if not cachedir:
343 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
344 raise PRServiceConfigError
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500345 dbfile = os.path.join(cachedir, "prserv.sqlite3")
346 logfile = os.path.join(cachedir, "prserv.log")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500347 if singleton:
348 if singleton.dbfile != dbfile:
349 # Shutdown any existing PR Server as doesn't match config
350 auto_shutdown()
351 if not singleton:
352 bb.utils.mkdirhier(cachedir)
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500353 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), host, port)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500354 singleton.start()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500355 if singleton:
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500356 host = singleton.host
357 port = singleton.port
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500358
359 try:
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500360 ping(host, port)
361 return str(host) + ":" + str(port)
362
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500363 except Exception:
364 logger.critical("PRservice %s:%d not available" % (host, port))
365 raise PRServiceConfigError
366
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500367def auto_shutdown():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500368 global singleton
Andrew Geisslerc926e172021-05-07 16:11:35 -0500369 if singleton and singleton.process:
370 singleton.process.terminate()
371 singleton.process.join()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500372 singleton = None
373
374def ping(host, port):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500375 from . import client
376
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600377 with client.PRClient() as conn:
378 conn.connect_tcp(host, port)
379 return conn.ping()
Andrew Geisslerc926e172021-05-07 16:11:35 -0500380
381def connect(host, port):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500382 from . import client
383
384 global singleton
385
Patrick Williams44b3caf2024-04-12 16:51:14 -0500386 if host.strip().lower() == "localhost" and not port:
387 host = "localhost"
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500388 port = singleton.port
389
390 conn = client.PRClient()
391 conn.connect_tcp(host, port)
392 return conn