blob: 0a20b927c7e1d8223627951b0d0c4f2f7bfe5d78 [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 Williamsf1e5d692016-03-30 15:21:19 -05007import socket
Patrick Williamsc0f7c042017-02-23 20:41:17 -06008import io
Brad Bishop6e60e8b2018-02-01 10:27:11 -05009import sqlite3
Patrick Williamsc124f4f2015-09-15 14:41:29 -050010import prserv
11import prserv.db
12import errno
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050013import bb.asyncrpc
Patrick Williamsc124f4f2015-09-15 14:41:29 -050014
15logger = logging.getLogger("BitBake.PRserv")
16
Patrick Williamsc124f4f2015-09-15 14:41:29 -050017PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
18singleton = None
19
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050020class PRServerClient(bb.asyncrpc.AsyncServerConnection):
21 def __init__(self, reader, writer, table, read_only):
22 super().__init__(reader, writer, 'PRSERVICE', logger)
23 self.handlers.update({
24 'get-pr': self.handle_get_pr,
25 'import-one': self.handle_import_one,
26 'export': self.handle_export,
27 'is-readonly': self.handle_is_readonly,
28 })
29 self.table = table
30 self.read_only = read_only
Patrick Williamsc124f4f2015-09-15 14:41:29 -050031
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050032 def validate_proto_version(self):
33 return (self.proto_version == (1, 0))
34
35 async def dispatch_message(self, msg):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036 try:
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050037 await super().dispatch_message(msg)
Andrew Geisslerc926e172021-05-07 16:11:35 -050038 except:
Andrew Geisslerc926e172021-05-07 16:11:35 -050039 self.table.sync()
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050040 raise
41
Andrew Geisslerc926e172021-05-07 16:11:35 -050042 self.table.sync_if_dirty()
43
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050044 async def handle_get_pr(self, request):
45 version = request['version']
46 pkgarch = request['pkgarch']
47 checksum = request['checksum']
Andrew Geisslerc926e172021-05-07 16:11:35 -050048
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050049 response = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -050050 try:
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050051 value = self.table.getValue(version, pkgarch, checksum)
52 response = {'value': value}
Patrick Williamsc124f4f2015-09-15 14:41:29 -050053 except prserv.NotFoundError:
54 logger.error("can not find value for (%s, %s)",version, checksum)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050055 except sqlite3.Error as exc:
56 logger.error(str(exc))
Andrew Geisslerd159c7f2021-09-02 21:05:58 -050057
58 self.write_message(response)
59
60 async def handle_import_one(self, request):
61 response = None
62 if not self.read_only:
63 version = request['version']
64 pkgarch = request['pkgarch']
65 checksum = request['checksum']
66 value = request['value']
67
68 value = self.table.importone(version, pkgarch, checksum, value)
69 if value is not None:
70 response = {'value': value}
71
72 self.write_message(response)
73
74 async def handle_export(self, request):
75 version = request['version']
76 pkgarch = request['pkgarch']
77 checksum = request['checksum']
78 colinfo = request['colinfo']
79
80 try:
81 (metainfo, datainfo) = self.table.export(version, pkgarch, checksum, colinfo)
82 except sqlite3.Error as exc:
83 logger.error(str(exc))
84 metainfo = datainfo = None
85
86 response = {'metainfo': metainfo, 'datainfo': datainfo}
87 self.write_message(response)
88
89 async def handle_is_readonly(self, request):
90 response = {'readonly': self.read_only}
91 self.write_message(response)
92
93class PRServer(bb.asyncrpc.AsyncServer):
94 def __init__(self, dbfile, read_only=False):
95 super().__init__(logger)
96 self.dbfile = dbfile
97 self.table = None
98 self.read_only = read_only
99
100 def accept_client(self, reader, writer):
101 return PRServerClient(reader, writer, self.table, self.read_only)
102
103 def _serve_forever(self):
104 self.db = prserv.db.PRData(self.dbfile, read_only=self.read_only)
105 self.table = self.db["PRMAIN"]
106
107 logger.info("Started PRServer with DBfile: %s, Address: %s, PID: %s" %
108 (self.dbfile, self.address, str(os.getpid())))
109
110 super()._serve_forever()
111
112 self.table.sync_if_dirty()
113 self.db.disconnect()
114
115 def signal_handler(self):
116 super().signal_handler()
117 if self.table:
118 self.table.sync()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500119
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500120class PRServSingleton(object):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500121 def __init__(self, dbfile, logfile, host, port):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500122 self.dbfile = dbfile
123 self.logfile = logfile
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500124 self.host = host
125 self.port = port
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500127 def start(self):
128 self.prserv = PRServer(self.dbfile)
129 self.prserv.start_tcp_server(socket.gethostbyname(self.host), self.port)
130 self.process = self.prserv.serve_as_process()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500131
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500132 if not self.prserv.address:
133 raise PRServiceConfigError
134 if not self.port:
135 self.port = int(self.prserv.address.rsplit(':', 1)[1])
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500136
Andrew Geisslerc926e172021-05-07 16:11:35 -0500137def run_as_daemon(func, pidfile, logfile):
138 """
139 See Advanced Programming in the UNIX, Sec 13.3
140 """
141 try:
142 pid = os.fork()
143 if pid > 0:
144 os.waitpid(pid, 0)
145 #parent return instead of exit to give control
146 return pid
147 except OSError as e:
148 raise Exception("%s [%d]" % (e.strerror, e.errno))
149
150 os.setsid()
151 """
152 fork again to make sure the daemon is not session leader,
153 which prevents it from acquiring controlling terminal
154 """
155 try:
156 pid = os.fork()
157 if pid > 0: #parent
158 os._exit(0)
159 except OSError as e:
160 raise Exception("%s [%d]" % (e.strerror, e.errno))
161
162 os.chdir("/")
163
164 sys.stdout.flush()
165 sys.stderr.flush()
166
167 # We could be called from a python thread with io.StringIO as
168 # stdout/stderr or it could be 'real' unix fd forking where we need
169 # to physically close the fds to prevent the program launching us from
170 # potentially hanging on a pipe. Handle both cases.
171 si = open('/dev/null', 'r')
172 try:
173 os.dup2(si.fileno(),sys.stdin.fileno())
174 except (AttributeError, io.UnsupportedOperation):
175 sys.stdin = si
176 so = open(logfile, 'a+')
177 try:
178 os.dup2(so.fileno(),sys.stdout.fileno())
179 except (AttributeError, io.UnsupportedOperation):
180 sys.stdout = so
181 try:
182 os.dup2(so.fileno(),sys.stderr.fileno())
183 except (AttributeError, io.UnsupportedOperation):
184 sys.stderr = so
185
186 # Clear out all log handlers prior to the fork() to avoid calling
187 # event handlers not part of the PRserver
188 for logger_iter in logging.Logger.manager.loggerDict.keys():
189 logging.getLogger(logger_iter).handlers = []
190
191 # Ensure logging makes it to the logfile
192 streamhandler = logging.StreamHandler()
193 streamhandler.setLevel(logging.DEBUG)
194 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
195 streamhandler.setFormatter(formatter)
196 logger.addHandler(streamhandler)
197
198 # write pidfile
199 pid = str(os.getpid())
200 with open(pidfile, 'w') as pf:
201 pf.write("%s\n" % pid)
202
203 func()
204 os.remove(pidfile)
205 os._exit(0)
206
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500207def start_daemon(dbfile, host, port, logfile, read_only=False):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500208 ip = socket.gethostbyname(host)
209 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500210 try:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500211 with open(pidfile) as pf:
212 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500213 except IOError:
214 pid = None
215
216 if pid:
217 sys.stderr.write("pidfile %s already exist. Daemon already running?\n"
218 % pidfile)
219 return 1
220
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500221 dbfile = os.path.abspath(dbfile)
222 def daemon_main():
223 server = PRServer(dbfile, read_only=read_only)
224 server.start_tcp_server(ip, port)
225 server.serve_forever()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500226
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500227 run_as_daemon(daemon_main, pidfile, os.path.abspath(logfile))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500228 return 0
229
230def stop_daemon(host, port):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500231 import glob
232 ip = socket.gethostbyname(host)
233 pidfile = PIDPREFIX % (ip, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500234 try:
Brad Bishop64c979e2019-11-04 13:55:29 -0500235 with open(pidfile) as pf:
236 pid = int(pf.readline().strip())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500237 except IOError:
238 pid = None
239
240 if not pid:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500241 # when server starts at port=0 (i.e. localhost:0), server actually takes another port,
242 # so at least advise the user which ports the corresponding server is listening
243 ports = []
244 portstr = ""
245 for pf in glob.glob(PIDPREFIX % (ip,'*')):
246 bn = os.path.basename(pf)
247 root, _ = os.path.splitext(bn)
248 ports.append(root.split('_')[-1])
249 if len(ports):
250 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports))
251
252 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
253 % (pidfile,portstr))
254 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500255
256 try:
Andrew Geisslerc926e172021-05-07 16:11:35 -0500257 if is_running(pid):
258 print("Sending SIGTERM to pr-server.")
259 os.kill(pid, signal.SIGTERM)
260 time.sleep(0.1)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500261
Andrew Geisslerc926e172021-05-07 16:11:35 -0500262 if os.path.exists(pidfile):
263 os.remove(pidfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500264
265 except OSError as e:
266 err = str(e)
267 if err.find("No such process") <= 0:
268 raise e
269
270 return 0
271
272def is_running(pid):
273 try:
274 os.kill(pid, 0)
275 except OSError as err:
276 if err.errno == errno.ESRCH:
277 return False
278 return True
279
280def is_local_special(host, port):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500281 if (host == 'localhost' or host == '127.0.0.1') and not port:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500282 return True
283 else:
284 return False
285
286class PRServiceConfigError(Exception):
287 pass
288
289def auto_start(d):
290 global singleton
291
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500292 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':')))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500293 if not host_params:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500294 # Shutdown any existing PR Server
295 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296 return None
297
298 if len(host_params) != 2:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500299 # Shutdown any existing PR Server
300 auto_shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500301 logger.critical('\n'.join(['PRSERV_HOST: incorrect format',
302 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
303 raise PRServiceConfigError
304
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500305 host = host_params[0].strip().lower()
306 port = int(host_params[1])
307 if is_local_special(host, port):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500308 import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500309 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500310 if not cachedir:
311 logger.critical("Please set the 'PERSISTENT_DIR' or 'CACHE' variable")
312 raise PRServiceConfigError
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500313 dbfile = os.path.join(cachedir, "prserv.sqlite3")
314 logfile = os.path.join(cachedir, "prserv.log")
Andrew Geissler82c905d2020-04-13 13:39:40 -0500315 if singleton:
316 if singleton.dbfile != dbfile:
317 # Shutdown any existing PR Server as doesn't match config
318 auto_shutdown()
319 if not singleton:
320 bb.utils.mkdirhier(cachedir)
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500321 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), host, port)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500322 singleton.start()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500323 if singleton:
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500324 host = singleton.host
325 port = singleton.port
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500326
327 try:
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500328 ping(host, port)
329 return str(host) + ":" + str(port)
330
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500331 except Exception:
332 logger.critical("PRservice %s:%d not available" % (host, port))
333 raise PRServiceConfigError
334
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500335def auto_shutdown():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500336 global singleton
Andrew Geisslerc926e172021-05-07 16:11:35 -0500337 if singleton and singleton.process:
338 singleton.process.terminate()
339 singleton.process.join()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500340 singleton = None
341
342def ping(host, port):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500343 from . import client
344
345 conn = client.PRClient()
346 conn.connect_tcp(host, port)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500347 return conn.ping()
Andrew Geisslerc926e172021-05-07 16:11:35 -0500348
349def connect(host, port):
Andrew Geisslerd159c7f2021-09-02 21:05:58 -0500350 from . import client
351
352 global singleton
353
354 if host.strip().lower() == 'localhost' and not port:
355 host = 'localhost'
356 port = singleton.port
357
358 conn = client.PRClient()
359 conn.connect_tcp(host, port)
360 return conn