summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/prserv
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/prserv')
-rw-r--r--bitbake/lib/prserv/__init__.py99
-rw-r--r--bitbake/lib/prserv/client.py72
-rw-r--r--bitbake/lib/prserv/db.py427
-rw-r--r--bitbake/lib/prserv/serv.py684
-rw-r--r--bitbake/lib/prserv/tests.py386
5 files changed, 1097 insertions, 571 deletions
diff --git a/bitbake/lib/prserv/__init__.py b/bitbake/lib/prserv/__init__.py
index 9961040b58..a817b03c1e 100644
--- a/bitbake/lib/prserv/__init__.py
+++ b/bitbake/lib/prserv/__init__.py
@@ -1,18 +1,95 @@
1# 1#
2# Copyright BitBake Contributors
3#
2# SPDX-License-Identifier: GPL-2.0-only 4# SPDX-License-Identifier: GPL-2.0-only
3# 5#
4 6
5__version__ = "1.0.0"
6 7
7import os, time 8__version__ = "2.0.0"
8import sys,logging 9
10import logging
11logger = logging.getLogger("BitBake.PRserv")
12
13from bb.asyncrpc.client import parse_address, ADDR_TYPE_UNIX, ADDR_TYPE_WS
14
15def create_server(addr, dbpath, upstream=None, read_only=False):
16 from . import serv
17
18 s = serv.PRServer(dbpath, upstream=upstream, read_only=read_only)
19 host, port = addr.split(":")
20 s.start_tcp_server(host, int(port))
21
22 return s
23
24def increase_revision(ver):
25 """Take a revision string such as "1" or "1.2.3" or even a number and increase its last number
26 This fails if the last number is not an integer"""
27
28 fields=str(ver).split('.')
29 last = fields[-1]
30
31 try:
32 val = int(last)
33 except Exception as e:
34 logger.critical("Unable to increase revision value %s: %s" % (ver, e))
35 raise e
36
37 return ".".join(fields[0:-1] + list(str(val + 1)))
38
39def _revision_greater_or_equal(rev1, rev2):
40 """Compares x.y.z revision numbers, using integer comparison
41 Returns True if rev1 is greater or equal to rev2"""
42
43 fields1 = rev1.split(".")
44 fields2 = rev2.split(".")
45 l1 = len(fields1)
46 l2 = len(fields2)
47
48 for i in range(l1):
49 val1 = int(fields1[i])
50 if i < l2:
51 val2 = int(fields2[i])
52 if val2 < val1:
53 return True
54 elif val2 > val1:
55 return False
56 else:
57 return True
58 return True
59
60def revision_smaller(rev1, rev2):
61 """Compares x.y.z revision numbers, using integer comparison
62 Returns True if rev1 is strictly smaller than rev2"""
63 return not(_revision_greater_or_equal(rev1, rev2))
64
65def revision_greater(rev1, rev2):
66 """Compares x.y.z revision numbers, using integer comparison
67 Returns True if rev1 is strictly greater than rev2"""
68 return _revision_greater_or_equal(rev1, rev2) and (rev1 != rev2)
69
70def create_client(addr):
71 from . import client
72
73 c = client.PRClient()
74
75 try:
76 (typ, a) = parse_address(addr)
77 c.connect_tcp(*a)
78 return c
79 except Exception as e:
80 c.close()
81 raise e
82
83async def create_async_client(addr):
84 from . import client
85
86 c = client.PRAsyncClient()
9 87
10def init_logger(logfile, loglevel): 88 try:
11 numeric_level = getattr(logging, loglevel.upper(), None) 89 (typ, a) = parse_address(addr)
12 if not isinstance(numeric_level, int): 90 await c.connect_tcp(*a)
13 raise ValueError('Invalid log level: %s' % loglevel) 91 return c
14 FORMAT = '%(asctime)-15s %(message)s'
15 logging.basicConfig(level=numeric_level, filename=logfile, format=FORMAT)
16 92
17class NotFoundError(Exception): 93 except Exception as e:
18 pass 94 await c.close()
95 raise e
diff --git a/bitbake/lib/prserv/client.py b/bitbake/lib/prserv/client.py
new file mode 100644
index 0000000000..9f5794c433
--- /dev/null
+++ b/bitbake/lib/prserv/client.py
@@ -0,0 +1,72 @@
1#
2# Copyright BitBake Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import logging
8import bb.asyncrpc
9from . import create_async_client
10
11logger = logging.getLogger("BitBake.PRserv")
12
13class PRAsyncClient(bb.asyncrpc.AsyncClient):
14 def __init__(self):
15 super().__init__("PRSERVICE", "1.0", logger)
16
17 async def getPR(self, version, pkgarch, checksum, history=False):
18 response = await self.invoke(
19 {"get-pr": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "history": history}}
20 )
21 if response:
22 return response["value"]
23
24 async def test_pr(self, version, pkgarch, checksum, history=False):
25 response = await self.invoke(
26 {"test-pr": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "history": history}}
27 )
28 if response:
29 return response["value"]
30
31 async def test_package(self, version, pkgarch):
32 response = await self.invoke(
33 {"test-package": {"version": version, "pkgarch": pkgarch}}
34 )
35 if response:
36 return response["value"]
37
38 async def max_package_pr(self, version, pkgarch):
39 response = await self.invoke(
40 {"max-package-pr": {"version": version, "pkgarch": pkgarch}}
41 )
42 if response:
43 return response["value"]
44
45 async def importone(self, version, pkgarch, checksum, value):
46 response = await self.invoke(
47 {"import-one": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "value": value}}
48 )
49 if response:
50 return response["value"]
51
52 async def export(self, version, pkgarch, checksum, colinfo, history=False):
53 response = await self.invoke(
54 {"export": {"version": version, "pkgarch": pkgarch, "checksum": checksum, "colinfo": colinfo, "history": history}}
55 )
56 if response:
57 return (response["metainfo"], response["datainfo"])
58
59 async def is_readonly(self):
60 response = await self.invoke(
61 {"is-readonly": {}}
62 )
63 if response:
64 return response["readonly"]
65
66class PRClient(bb.asyncrpc.Client):
67 def __init__(self):
68 super().__init__()
69 self._add_methods("getPR", "test_pr", "test_package", "max_package_pr", "importone", "export", "is_readonly")
70
71 def _get_async_client(self):
72 return PRAsyncClient()
diff --git a/bitbake/lib/prserv/db.py b/bitbake/lib/prserv/db.py
index cb2a2461e0..2da493ddf5 100644
--- a/bitbake/lib/prserv/db.py
+++ b/bitbake/lib/prserv/db.py
@@ -1,4 +1,6 @@
1# 1#
2# Copyright BitBake Contributors
3#
2# SPDX-License-Identifier: GPL-2.0-only 4# SPDX-License-Identifier: GPL-2.0-only
3# 5#
4 6
@@ -6,19 +8,13 @@ import logging
6import os.path 8import os.path
7import errno 9import errno
8import prserv 10import prserv
9import time 11import sqlite3
10 12
11try: 13from contextlib import closing
12 import sqlite3 14from . import increase_revision, revision_greater, revision_smaller
13except ImportError:
14 from pysqlite2 import dbapi2 as sqlite3
15 15
16logger = logging.getLogger("BitBake.PRserv") 16logger = logging.getLogger("BitBake.PRserv")
17 17
18sqlversion = sqlite3.sqlite_version_info
19if sqlversion[0] < 3 or (sqlversion[0] == 3 and sqlversion[1] < 3):
20 raise Exception("sqlite3 version 3.3.0 or later is required.")
21
22# 18#
23# "No History" mode - for a given query tuple (version, pkgarch, checksum), 19# "No History" mode - for a given query tuple (version, pkgarch, checksum),
24# the returned value will be the largest among all the values of the same 20# the returned value will be the largest among all the values of the same
@@ -27,212 +23,232 @@ if sqlversion[0] < 3 or (sqlversion[0] == 3 and sqlversion[1] < 3):
27# "History" mode - Return a new higher value for previously unseen query 23# "History" mode - Return a new higher value for previously unseen query
28# tuple (version, pkgarch, checksum), otherwise return historical value. 24# tuple (version, pkgarch, checksum), otherwise return historical value.
29# Value can decrement if returning to a previous build. 25# Value can decrement if returning to a previous build.
30#
31 26
32class PRTable(object): 27class PRTable(object):
33 def __init__(self, conn, table, nohist): 28 def __init__(self, conn, table, read_only):
34 self.conn = conn 29 self.conn = conn
35 self.nohist = nohist 30 self.read_only = read_only
36 self.dirty = False 31 self.table = table
37 if nohist: 32
38 self.table = "%s_nohist" % table 33 # Creating the table even if the server is read-only.
39 else: 34 # This avoids a race condition if a shared database
40 self.table = "%s_hist" % table 35 # is accessed by a read-only server first.
41 36
42 self._execute("CREATE TABLE IF NOT EXISTS %s \ 37 with closing(self.conn.cursor()) as cursor:
43 (version TEXT NOT NULL, \ 38 cursor.execute("CREATE TABLE IF NOT EXISTS %s \
44 pkgarch TEXT NOT NULL, \ 39 (version TEXT NOT NULL, \
45 checksum TEXT NOT NULL, \ 40 pkgarch TEXT NOT NULL, \
46 value INTEGER, \ 41 checksum TEXT NOT NULL, \
47 PRIMARY KEY (version, pkgarch, checksum));" % self.table) 42 value TEXT, \
48 43 PRIMARY KEY (version, pkgarch, checksum, value));" % self.table)
49 def _execute(self, *query): 44 self.conn.commit()
50 """Execute a query, waiting to acquire a lock if necessary""" 45
51 start = time.time() 46 def _extremum_value(self, rows, is_max):
52 end = start + 20 47 value = None
53 while True: 48
54 try: 49 for row in rows:
55 return self.conn.execute(*query) 50 current_value = row[0]
56 except sqlite3.OperationalError as exc: 51 if value is None:
57 if 'is locked' in str(exc) and end > time.time(): 52 value = current_value
58 continue 53 else:
59 raise exc 54 if is_max:
60 55 is_new_extremum = revision_greater(current_value, value)
61 def sync(self): 56 else:
62 self.conn.commit() 57 is_new_extremum = revision_smaller(current_value, value)
63 self._execute("BEGIN EXCLUSIVE TRANSACTION") 58 if is_new_extremum:
64 59 value = current_value
65 def sync_if_dirty(self): 60 return value
66 if self.dirty: 61
67 self.sync() 62 def _max_value(self, rows):
68 self.dirty = False 63 return self._extremum_value(rows, True)
69 64
70 def _getValueHist(self, version, pkgarch, checksum): 65 def _min_value(self, rows):
71 data=self._execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table, 66 return self._extremum_value(rows, False)
72 (version, pkgarch, checksum)) 67
73 row=data.fetchone() 68 def test_package(self, version, pkgarch):
74 if row is not None: 69 """Returns whether the specified package version is found in the database for the specified architecture"""
75 return row[0] 70
76 else: 71 # Just returns the value if found or None otherwise
77 #no value found, try to insert 72 with closing(self.conn.cursor()) as cursor:
78 try: 73 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=?;" % self.table,
79 self._execute("INSERT INTO %s VALUES (?, ?, ?, (select ifnull(max(value)+1,0) from %s where version=? AND pkgarch=?));" 74 (version, pkgarch))
80 % (self.table,self.table),
81 (version,pkgarch, checksum,version, pkgarch))
82 except sqlite3.IntegrityError as exc:
83 logger.error(str(exc))
84
85 self.dirty = True
86
87 data=self._execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table,
88 (version, pkgarch, checksum))
89 row=data.fetchone() 75 row=data.fetchone()
90 if row is not None: 76 if row is not None:
91 return row[0] 77 return True
92 else: 78 else:
93 raise prserv.NotFoundError 79 return False
94 80
95 def _getValueNohist(self, version, pkgarch, checksum): 81 def test_checksum_value(self, version, pkgarch, checksum, value):
96 data=self._execute("SELECT value FROM %s \ 82 """Returns whether the specified value is found in the database for the specified package, architecture and checksum"""
97 WHERE version=? AND pkgarch=? AND checksum=? AND \ 83
98 value >= (select max(value) from %s where version=? AND pkgarch=?);" 84 with closing(self.conn.cursor()) as cursor:
99 % (self.table, self.table), 85 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and checksum=? and value=?;" % self.table,
100 (version, pkgarch, checksum, version, pkgarch)) 86 (version, pkgarch, checksum, value))
101 row=data.fetchone()
102 if row is not None:
103 return row[0]
104 else:
105 #no value found, try to insert
106 try:
107 self._execute("INSERT OR REPLACE INTO %s VALUES (?, ?, ?, (select ifnull(max(value)+1,0) from %s where version=? AND pkgarch=?));"
108 % (self.table,self.table),
109 (version, pkgarch, checksum, version, pkgarch))
110 except sqlite3.IntegrityError as exc:
111 logger.error(str(exc))
112 self.conn.rollback()
113
114 self.dirty = True
115
116 data=self._execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table,
117 (version, pkgarch, checksum))
118 row=data.fetchone() 87 row=data.fetchone()
119 if row is not None: 88 if row is not None:
120 return row[0] 89 return True
121 else: 90 else:
122 raise prserv.NotFoundError 91 return False
123 92
124 def getValue(self, version, pkgarch, checksum): 93 def test_value(self, version, pkgarch, value):
125 if self.nohist: 94 """Returns whether the specified value is found in the database for the specified package and architecture"""
126 return self._getValueNohist(version, pkgarch, checksum) 95
127 else: 96 # Just returns the value if found or None otherwise
128 return self._getValueHist(version, pkgarch, checksum) 97 with closing(self.conn.cursor()) as cursor:
129 98 data=cursor.execute("SELECT value FROM %s WHERE version=? AND pkgarch=? and value=?;" % self.table,
130 def _importHist(self, version, pkgarch, checksum, value): 99 (version, pkgarch, value))
131 val = None 100 row=data.fetchone()
132 data = self._execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table, 101 if row is not None:
133 (version, pkgarch, checksum)) 102 return True
134 row = data.fetchone() 103 else:
135 if row is not None: 104 return False
136 val=row[0] 105
106
107 def find_package_max_value(self, version, pkgarch):
108 """Returns the greatest value for (version, pkgarch), or None if not found. Doesn't create a new value"""
109
110 with closing(self.conn.cursor()) as cursor:
111 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=?;" % (self.table),
112 (version, pkgarch))
113 rows = data.fetchall()
114 value = self._max_value(rows)
115 return value
116
117 def find_value(self, version, pkgarch, checksum, history=False):
118 """Returns the value for the specified checksum if found or None otherwise."""
119
120 if history:
121 return self.find_min_value(version, pkgarch, checksum)
137 else: 122 else:
138 #no value found, try to insert 123 return self.find_max_value(version, pkgarch, checksum)
139 try: 124
140 self._execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table), 125
126 def _find_extremum_value(self, version, pkgarch, checksum, is_max):
127 """Returns the maximum (if is_max is True) or minimum (if is_max is False) value
128 for (version, pkgarch, checksum), or None if not found. Doesn't create a new value"""
129
130 with closing(self.conn.cursor()) as cursor:
131 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND checksum=?;" % (self.table),
132 (version, pkgarch, checksum))
133 rows = data.fetchall()
134 return self._extremum_value(rows, is_max)
135
136 def find_max_value(self, version, pkgarch, checksum):
137 return self._find_extremum_value(version, pkgarch, checksum, True)
138
139 def find_min_value(self, version, pkgarch, checksum):
140 return self._find_extremum_value(version, pkgarch, checksum, False)
141
142 def find_new_subvalue(self, version, pkgarch, base):
143 """Take and increase the greatest "<base>.y" value for (version, pkgarch), or return "<base>.0" if not found.
144 This doesn't store a new value."""
145
146 with closing(self.conn.cursor()) as cursor:
147 data = cursor.execute("SELECT value FROM %s where version=? AND pkgarch=? AND value LIKE '%s.%%';" % (self.table, base),
148 (version, pkgarch))
149 rows = data.fetchall()
150 value = self._max_value(rows)
151
152 if value is not None:
153 return increase_revision(value)
154 else:
155 return base + ".0"
156
157 def store_value(self, version, pkgarch, checksum, value):
158 """Store value in the database"""
159
160 if not self.read_only and not self.test_checksum_value(version, pkgarch, checksum, value):
161 with closing(self.conn.cursor()) as cursor:
162 cursor.execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table),
141 (version, pkgarch, checksum, value)) 163 (version, pkgarch, checksum, value))
142 except sqlite3.IntegrityError as exc: 164 self.conn.commit()
143 logger.error(str(exc))
144 165
145 self.dirty = True 166 def _get_value(self, version, pkgarch, checksum, history):
146 167
147 data = self._execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=?;" % self.table, 168 max_value = self.find_package_max_value(version, pkgarch)
148 (version, pkgarch, checksum))
149 row = data.fetchone()
150 if row is not None:
151 val = row[0]
152 return val
153 169
154 def _importNohist(self, version, pkgarch, checksum, value): 170 if max_value is None:
155 try: 171 # version, pkgarch completely unknown. Return initial value.
156 #try to insert 172 return "0"
157 self._execute("INSERT INTO %s VALUES (?, ?, ?, ?);" % (self.table), 173
158 (version, pkgarch, checksum,value)) 174 value = self.find_value(version, pkgarch, checksum, history)
159 except sqlite3.IntegrityError as exc: 175
160 #already have the record, try to update 176 if value is None:
161 try: 177 # version, pkgarch found but not checksum. Create a new value from the maximum one
162 self._execute("UPDATE %s SET value=? WHERE version=? AND pkgarch=? AND checksum=? AND value<?" 178 return increase_revision(max_value)
163 % (self.table), 179
164 (value,version,pkgarch,checksum,value)) 180 if history:
165 except sqlite3.IntegrityError as exc: 181 return value
166 logger.error(str(exc)) 182
167 183 # "no history" mode - If the value is not the maximum value for the package, need to increase it.
168 self.dirty = True 184 if max_value > value:
169 185 return increase_revision(max_value)
170 data = self._execute("SELECT value FROM %s WHERE version=? AND pkgarch=? AND checksum=? AND value>=?;" % self.table,
171 (version,pkgarch,checksum,value))
172 row=data.fetchone()
173 if row is not None:
174 return row[0]
175 else: 186 else:
176 return None 187 return value
188
189 def get_value(self, version, pkgarch, checksum, history):
190 value = self._get_value(version, pkgarch, checksum, history)
191 if not self.read_only:
192 self.store_value(version, pkgarch, checksum, value)
193 return value
177 194
178 def importone(self, version, pkgarch, checksum, value): 195 def importone(self, version, pkgarch, checksum, value):
179 if self.nohist: 196 self.store_value(version, pkgarch, checksum, value)
180 return self._importNohist(version, pkgarch, checksum, value) 197 return value
181 else:
182 return self._importHist(version, pkgarch, checksum, value)
183 198
184 def export(self, version, pkgarch, checksum, colinfo): 199 def export(self, version, pkgarch, checksum, colinfo, history=False):
185 metainfo = {} 200 metainfo = {}
186 #column info 201 with closing(self.conn.cursor()) as cursor:
187 if colinfo: 202 #column info
188 metainfo['tbl_name'] = self.table 203 if colinfo:
189 metainfo['core_ver'] = prserv.__version__ 204 metainfo["tbl_name"] = self.table
190 metainfo['col_info'] = [] 205 metainfo["core_ver"] = prserv.__version__
191 data = self._execute("PRAGMA table_info(%s);" % self.table) 206 metainfo["col_info"] = []
207 data = cursor.execute("PRAGMA table_info(%s);" % self.table)
208 for row in data:
209 col = {}
210 col["name"] = row["name"]
211 col["type"] = row["type"]
212 col["notnull"] = row["notnull"]
213 col["dflt_value"] = row["dflt_value"]
214 col["pk"] = row["pk"]
215 metainfo["col_info"].append(col)
216
217 #data info
218 datainfo = []
219
220 if history:
221 sqlstmt = "SELECT * FROM %s as T1 WHERE 1=1 " % self.table
222 else:
223 sqlstmt = "SELECT T1.version, T1.pkgarch, T1.checksum, T1.value FROM %s as T1, \
224 (SELECT version, pkgarch, max(value) as maxvalue FROM %s GROUP BY version, pkgarch) as T2 \
225 WHERE T1.version=T2.version AND T1.pkgarch=T2.pkgarch AND T1.value=T2.maxvalue " % (self.table, self.table)
226 sqlarg = []
227 where = ""
228 if version:
229 where += "AND T1.version=? "
230 sqlarg.append(str(version))
231 if pkgarch:
232 where += "AND T1.pkgarch=? "
233 sqlarg.append(str(pkgarch))
234 if checksum:
235 where += "AND T1.checksum=? "
236 sqlarg.append(str(checksum))
237
238 sqlstmt += where + ";"
239
240 if len(sqlarg):
241 data = cursor.execute(sqlstmt, tuple(sqlarg))
242 else:
243 data = cursor.execute(sqlstmt)
192 for row in data: 244 for row in data:
193 col = {} 245 if row["version"]:
194 col['name'] = row['name'] 246 col = {}
195 col['type'] = row['type'] 247 col["version"] = row["version"]
196 col['notnull'] = row['notnull'] 248 col["pkgarch"] = row["pkgarch"]
197 col['dflt_value'] = row['dflt_value'] 249 col["checksum"] = row["checksum"]
198 col['pk'] = row['pk'] 250 col["value"] = row["value"]
199 metainfo['col_info'].append(col) 251 datainfo.append(col)
200
201 #data info
202 datainfo = []
203
204 if self.nohist:
205 sqlstmt = "SELECT T1.version, T1.pkgarch, T1.checksum, T1.value FROM %s as T1, \
206 (SELECT version,pkgarch,max(value) as maxvalue FROM %s GROUP BY version,pkgarch) as T2 \
207 WHERE T1.version=T2.version AND T1.pkgarch=T2.pkgarch AND T1.value=T2.maxvalue " % (self.table, self.table)
208 else:
209 sqlstmt = "SELECT * FROM %s as T1 WHERE 1=1 " % self.table
210 sqlarg = []
211 where = ""
212 if version:
213 where += "AND T1.version=? "
214 sqlarg.append(str(version))
215 if pkgarch:
216 where += "AND T1.pkgarch=? "
217 sqlarg.append(str(pkgarch))
218 if checksum:
219 where += "AND T1.checksum=? "
220 sqlarg.append(str(checksum))
221
222 sqlstmt += where + ";"
223
224 if len(sqlarg):
225 data = self._execute(sqlstmt, tuple(sqlarg))
226 else:
227 data = self._execute(sqlstmt)
228 for row in data:
229 if row['version']:
230 col = {}
231 col['version'] = row['version']
232 col['pkgarch'] = row['pkgarch']
233 col['checksum'] = row['checksum']
234 col['value'] = row['value']
235 datainfo.append(col)
236 return (metainfo, datainfo) 252 return (metainfo, datainfo)
237 253
238 def dump_db(self, fd): 254 def dump_db(self, fd):
@@ -240,41 +256,46 @@ class PRTable(object):
240 for line in self.conn.iterdump(): 256 for line in self.conn.iterdump():
241 writeCount = writeCount + len(line) + 1 257 writeCount = writeCount + len(line) + 1
242 fd.write(line) 258 fd.write(line)
243 fd.write('\n') 259 fd.write("\n")
244 return writeCount 260 return writeCount
245 261
246class PRData(object): 262class PRData(object):
247 """Object representing the PR database""" 263 """Object representing the PR database"""
248 def __init__(self, filename, nohist=True): 264 def __init__(self, filename, read_only=False):
249 self.filename=os.path.abspath(filename) 265 self.filename=os.path.abspath(filename)
250 self.nohist=nohist 266 self.read_only = read_only
251 #build directory hierarchy 267 #build directory hierarchy
252 try: 268 try:
253 os.makedirs(os.path.dirname(self.filename)) 269 os.makedirs(os.path.dirname(self.filename))
254 except OSError as e: 270 except OSError as e:
255 if e.errno != errno.EEXIST: 271 if e.errno != errno.EEXIST:
256 raise e 272 raise e
257 self.connection=sqlite3.connect(self.filename, isolation_level="EXCLUSIVE", check_same_thread = False) 273 uri = "file:%s%s" % (self.filename, "?mode=ro" if self.read_only else "")
274 logger.debug("Opening PRServ database '%s'" % (uri))
275 self.connection=sqlite3.connect(uri, uri=True)
258 self.connection.row_factory=sqlite3.Row 276 self.connection.row_factory=sqlite3.Row
259 self.connection.execute("pragma synchronous = off;") 277 self.connection.execute("PRAGMA synchronous = OFF;")
260 self.connection.execute("PRAGMA journal_mode = MEMORY;") 278 self.connection.execute("PRAGMA journal_mode = WAL;")
279 self.connection.commit()
261 self._tables={} 280 self._tables={}
262 281
263 def disconnect(self): 282 def disconnect(self):
283 self.connection.commit()
264 self.connection.close() 284 self.connection.close()
265 285
266 def __getitem__(self,tblname): 286 def __getitem__(self, tblname):
267 if not isinstance(tblname, str): 287 if not isinstance(tblname, str):
268 raise TypeError("tblname argument must be a string, not '%s'" % 288 raise TypeError("tblname argument must be a string, not '%s'" %
269 type(tblname)) 289 type(tblname))
270 if tblname in self._tables: 290 if tblname in self._tables:
271 return self._tables[tblname] 291 return self._tables[tblname]
272 else: 292 else:
273 tableobj = self._tables[tblname] = PRTable(self.connection, tblname, self.nohist) 293 tableobj = self._tables[tblname] = PRTable(self.connection, tblname, self.read_only)
274 return tableobj 294 return tableobj
275 295
276 def __delitem__(self, tblname): 296 def __delitem__(self, tblname):
277 if tblname in self._tables: 297 if tblname in self._tables:
278 del self._tables[tblname] 298 del self._tables[tblname]
279 logger.info("drop table %s" % (tblname)) 299 logger.info("drop table %s" % (tblname))
280 self.connection.execute("DROP TABLE IF EXISTS %s;" % tblname) 300 self.connection.execute("DROP TABLE IF EXISTS %s;" % tblname)
301 self.connection.commit()
diff --git a/bitbake/lib/prserv/serv.py b/bitbake/lib/prserv/serv.py
index 25dcf8a0ee..e175886308 100644
--- a/bitbake/lib/prserv/serv.py
+++ b/bitbake/lib/prserv/serv.py
@@ -1,354 +1,326 @@
1# 1#
2# Copyright BitBake Contributors
3#
2# SPDX-License-Identifier: GPL-2.0-only 4# SPDX-License-Identifier: GPL-2.0-only
3# 5#
4 6
5import os,sys,logging 7import os,sys,logging
6import signal, time 8import signal, time
7from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
8import threading
9import queue
10import socket 9import socket
11import io 10import io
12import sqlite3 11import sqlite3
13import bb.server.xmlrpcclient
14import prserv 12import prserv
15import prserv.db 13import prserv.db
16import errno 14import errno
17import select 15from . import create_async_client, revision_smaller, increase_revision
16import bb.asyncrpc
18 17
19logger = logging.getLogger("BitBake.PRserv") 18logger = logging.getLogger("BitBake.PRserv")
20 19
21if sys.hexversion < 0x020600F0: 20PIDPREFIX = "/tmp/PRServer_%s_%s.pid"
22 print("Sorry, python 2.6 or later is required.") 21singleton = None
23 sys.exit(1)
24 22
25class Handler(SimpleXMLRPCRequestHandler): 23class PRServerClient(bb.asyncrpc.AsyncServerConnection):
26 def _dispatch(self,method,params): 24 def __init__(self, socket, server):
25 super().__init__(socket, "PRSERVICE", server.logger)
26 self.server = server
27
28 self.handlers.update({
29 "get-pr": self.handle_get_pr,
30 "test-pr": self.handle_test_pr,
31 "test-package": self.handle_test_package,
32 "max-package-pr": self.handle_max_package_pr,
33 "import-one": self.handle_import_one,
34 "export": self.handle_export,
35 "is-readonly": self.handle_is_readonly,
36 })
37
38 def validate_proto_version(self):
39 return (self.proto_version == (1, 0))
40
41 async def dispatch_message(self, msg):
27 try: 42 try:
28 value=self.server.funcs[method](*params) 43 return await super().dispatch_message(msg)
29 except: 44 except:
30 import traceback
31 traceback.print_exc()
32 raise 45 raise
33 return value
34 46
35PIDPREFIX = "/tmp/PRServer_%s_%s.pid" 47 async def handle_test_pr(self, request):
36singleton = None 48 '''Finds the PR value corresponding to the request. If not found, returns None and doesn't insert a new value'''
49 version = request["version"]
50 pkgarch = request["pkgarch"]
51 checksum = request["checksum"]
52 history = request["history"]
37 53
54 value = self.server.table.find_value(version, pkgarch, checksum, history)
55 return {"value": value}
38 56
39class PRServer(SimpleXMLRPCServer): 57 async def handle_test_package(self, request):
40 def __init__(self, dbfile, logfile, interface, daemon=True): 58 '''Tells whether there are entries for (version, pkgarch) in the db. Returns True or False'''
41 ''' constructor ''' 59 version = request["version"]
42 try: 60 pkgarch = request["pkgarch"]
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 61
52 self.dbfile=dbfile 62 value = self.server.table.test_package(version, pkgarch)
53 self.daemon=daemon 63 return {"value": value}
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")
63 self.register_function(self.dump_db, "dump_db")
64 self.register_function(self.importone, "importone")
65 self.register_introspection_functions()
66
67 self.quitpipein, self.quitpipeout = os.pipe()
68
69 self.requestqueue = queue.Queue()
70 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
83 bb.utils.set_process_name("PRServ Handler")
84
85 while not self.quitflag:
86 try:
87 (request, client_address) = self.requestqueue.get(True, 30)
88 except queue.Empty:
89 self.table.sync_if_dirty()
90 continue
91 if request is None:
92 continue
93 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):
106 if self.table:
107 self.table.sync()
108
109 def sigterm_handler(self, signum, stack):
110 if self.table:
111 self.table.sync()
112 self.quit()
113 self.requestqueue.put((None, None))
114
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
125 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 """
134 buff = io.StringIO()
135 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 64
145 def importone(self, version, pkgarch, checksum, value): 65 async def handle_max_package_pr(self, request):
146 return self.table.importone(version, pkgarch, checksum, value) 66 '''Finds the greatest PR value for (version, pkgarch) in the db. Returns None if no entry was found'''
67 version = request["version"]
68 pkgarch = request["pkgarch"]
147 69
148 def ping(self): 70 value = self.server.table.find_package_max_value(version, pkgarch)
149 return not self.quitflag 71 return {"value": value}
150 72
151 def getinfo(self): 73 async def handle_get_pr(self, request):
152 return (self.host, self.port) 74 version = request["version"]
75 pkgarch = request["pkgarch"]
76 checksum = request["checksum"]
77 history = request["history"]
153 78
154 def getPR(self, version, pkgarch, checksum): 79 if self.upstream_client is None:
155 try: 80 value = self.server.table.get_value(version, pkgarch, checksum, history)
156 return self.table.getValue(version, pkgarch, checksum) 81 return {"value": value}
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):
165 self.quitflag=True
166 os.write(self.quitpipeout, b"q")
167 os.close(self.quitpipeout)
168 return
169
170 def work_forever(self,):
171 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
176
177 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 82
183 logger.info("Started PRServer with DBfile: %s, IP: %s, PORT: %s, PID: %s" % 83 # We have an upstream server.
184 (self.dbfile, self.host, self.port, str(os.getpid()))) 84 # Check whether the local server already knows the requested configuration.
185 85 # If the configuration is a new one, the generated value we will add will
186 self.handlerthread.start() 86 # depend on what's on the upstream server. That's why we're calling find_value()
187 while not self.quitflag: 87 # instead of get_value() directly.
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()
193 self.handlerthread.join()
194 self.db.disconnect()
195 logger.info("PRServer: stopping...")
196 self.server_close()
197 os.close(self.quitpipein)
198 return
199 88
200 def start(self): 89 value = self.server.table.find_value(version, pkgarch, checksum, history)
201 if self.daemon: 90 upstream_max = await self.upstream_client.max_package_pr(version, pkgarch)
202 pid = self.daemonize()
203 else:
204 pid = self.fork()
205 self.pid = pid
206 91
207 # Ensure both the parent sees this and the child from the work_forever log entry above 92 if value is not None:
208 logger.info("Started PRServer with DBfile: %s, IP: %s, PORT: %s, PID: %s" %
209 (self.dbfile, self.host, self.port, str(pid)))
210 93
211 def delpid(self): 94 # The configuration is already known locally.
212 os.remove(self.pidfile)
213 95
214 def daemonize(self): 96 if history:
215 """ 97 value = self.server.table.get_value(version, pkgarch, checksum, history)
216 See Advanced Programming in the UNIX, Sec 13.3 98 else:
217 """ 99 existing_value = value
218 try: 100 # In "no history", we need to make sure the value doesn't decrease
219 pid = os.fork() 101 # and is at least greater than the maximum upstream value
220 if pid > 0: 102 # and the maximum local value
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 103
239 self.cleanup_handles() 104 local_max = self.server.table.find_package_max_value(version, pkgarch)
240 os._exit(0) 105 if revision_smaller(value, local_max):
106 value = increase_revision(local_max)
107
108 if revision_smaller(value, upstream_max):
109 # Ask upstream whether it knows the checksum
110 upstream_value = await self.upstream_client.test_pr(version, pkgarch, checksum)
111 if upstream_value is None:
112 # Upstream doesn't have our checksum, let create a new one
113 value = upstream_max + ".0"
114 else:
115 # Fine to take the same value as upstream
116 value = upstream_max
117
118 if not value == existing_value and not self.server.read_only:
119 self.server.table.store_value(version, pkgarch, checksum, value)
120
121 return {"value": value}
122
123 # The configuration is a new one for the local server
124 # Let's ask the upstream server whether it knows it
125
126 known_upstream = await self.upstream_client.test_package(version, pkgarch)
127
128 if not known_upstream:
129
130 # The package is not known upstream, must be a local-only package
131 # Let's compute the PR number using the local-only method
132
133 value = self.server.table.get_value(version, pkgarch, checksum, history)
134 return {"value": value}
135
136 # The package is known upstream, let's ask the upstream server
137 # whether it knows our new output hash
138
139 value = await self.upstream_client.test_pr(version, pkgarch, checksum)
140
141 if value is not None:
142
143 # Upstream knows this output hash, let's store it and use it too.
144
145 if not self.server.read_only:
146 self.server.table.store_value(version, pkgarch, checksum, value)
147 # If the local server is read only, won't be able to store the new
148 # value in the database and will have to keep asking the upstream server
149 return {"value": value}
150
151 # The output hash doesn't exist upstream, get the most recent number from upstream (x)
152 # Then, we want to have a new PR value for the local server: x.y
153
154 upstream_max = await self.upstream_client.max_package_pr(version, pkgarch)
155 # Here we know that the package is known upstream, so upstream_max can't be None
156 subvalue = self.server.table.find_new_subvalue(version, pkgarch, upstream_max)
157
158 if not self.server.read_only:
159 self.server.table.store_value(version, pkgarch, checksum, subvalue)
160
161 return {"value": subvalue}
162
163 async def process_requests(self):
164 if self.server.upstream is not None:
165 self.upstream_client = await create_async_client(self.server.upstream)
166 else:
167 self.upstream_client = None
241 168
242 def fork(self):
243 try:
244 pid = os.fork()
245 if pid > 0:
246 self.socket.close() # avoid ResourceWarning in parent
247 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)
258 os.chdir("/")
259
260 sys.stdout.flush()
261 sys.stderr.flush()
262
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.
267 si = open('/dev/null', 'r')
268 try:
269 os.dup2(si.fileno(),sys.stdin.fileno())
270 except (AttributeError, io.UnsupportedOperation):
271 sys.stdin = si
272 so = open(self.logfile, 'a+')
273 try: 169 try:
274 os.dup2(so.fileno(),sys.stdout.fileno()) 170 await super().process_requests()
275 except (AttributeError, io.UnsupportedOperation): 171 finally:
276 sys.stdout = so 172 if self.upstream_client is not None:
173 await self.upstream_client.close()
174
175 async def handle_import_one(self, request):
176 response = None
177 if not self.server.read_only:
178 version = request["version"]
179 pkgarch = request["pkgarch"]
180 checksum = request["checksum"]
181 value = request["value"]
182
183 value = self.server.table.importone(version, pkgarch, checksum, value)
184 if value is not None:
185 response = {"value": value}
186
187 return response
188
189 async def handle_export(self, request):
190 version = request["version"]
191 pkgarch = request["pkgarch"]
192 checksum = request["checksum"]
193 colinfo = request["colinfo"]
194 history = request["history"]
195
277 try: 196 try:
278 os.dup2(so.fileno(),sys.stderr.fileno()) 197 (metainfo, datainfo) = self.server.table.export(version, pkgarch, checksum, colinfo, history)
279 except (AttributeError, io.UnsupportedOperation): 198 except sqlite3.Error as exc:
280 sys.stderr = so 199 self.logger.error(str(exc))
281 200 metainfo = datainfo = None
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())
296 with open(self.pidfile, 'w') as pf:
297 pf.write("%s\n" % pid)
298
299 self.work_forever()
300 self.delpid()
301 201
302class PRServSingleton(object): 202 return {"metainfo": metainfo, "datainfo": datainfo}
303 def __init__(self, dbfile, logfile, interface): 203
204 async def handle_is_readonly(self, request):
205 return {"readonly": self.server.read_only}
206
207class PRServer(bb.asyncrpc.AsyncServer):
208 def __init__(self, dbfile, read_only=False, upstream=None):
209 super().__init__(logger)
304 self.dbfile = dbfile 210 self.dbfile = dbfile
305 self.logfile = logfile 211 self.table = None
306 self.interface = interface 212 self.read_only = read_only
307 self.host = None 213 self.upstream = upstream
308 self.port = None 214
215 def accept_client(self, socket):
216 return PRServerClient(socket, self)
309 217
310 def start(self): 218 def start(self):
311 self.prserv = PRServer(self.dbfile, self.logfile, self.interface, daemon=False) 219 tasks = super().start()
312 self.prserv.start() 220 self.db = prserv.db.PRData(self.dbfile, read_only=self.read_only)
313 self.host, self.port = self.prserv.getinfo() 221 self.table = self.db["PRMAIN"]
222
223 self.logger.info("Started PRServer with DBfile: %s, Address: %s, PID: %s" %
224 (self.dbfile, self.address, str(os.getpid())))
314 225
315 def getinfo(self): 226 if self.upstream is not None:
316 return (self.host, self.port) 227 self.logger.info("And upstream PRServer: %s " % (self.upstream))
317 228
318class PRServerConnection(object): 229 return tasks
319 def __init__(self, host, port): 230
320 if is_local_special(host, port): 231 async def stop(self):
321 host, port = singleton.getinfo() 232 self.db.disconnect()
233 await super().stop()
234
235class PRServSingleton(object):
236 def __init__(self, dbfile, logfile, host, port, upstream):
237 self.dbfile = dbfile
238 self.logfile = logfile
322 self.host = host 239 self.host = host
323 self.port = port 240 self.port = port
324 self.connection, self.transport = bb.server.xmlrpcclient._create_server(self.host, self.port) 241 self.upstream = upstream
325
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 242
333 def getPR(self, version, pkgarch, checksum): 243 def start(self):
334 return self.connection.getPR(version, pkgarch, checksum) 244 self.prserv = PRServer(self.dbfile, upstream=self.upstream)
245 self.prserv.start_tcp_server(socket.gethostbyname(self.host), self.port)
246 self.process = self.prserv.serve_as_process(log_level=logging.WARNING)
335 247
336 def ping(self): 248 if not self.prserv.address:
337 return self.connection.ping() 249 raise PRServiceConfigError
250 if not self.port:
251 self.port = int(self.prserv.address.rsplit(":", 1)[1])
338 252
339 def export(self,version=None, pkgarch=None, checksum=None, colinfo=True): 253def run_as_daemon(func, pidfile, logfile):
340 return self.connection.export(version, pkgarch, checksum, colinfo) 254 """
255 See Advanced Programming in the UNIX, Sec 13.3
256 """
257 try:
258 pid = os.fork()
259 if pid > 0:
260 os.waitpid(pid, 0)
261 #parent return instead of exit to give control
262 return pid
263 except OSError as e:
264 raise Exception("%s [%d]" % (e.strerror, e.errno))
341 265
342 def dump_db(self): 266 os.setsid()
343 return self.connection.dump_db() 267 """
268 fork again to make sure the daemon is not session leader,
269 which prevents it from acquiring controlling terminal
270 """
271 try:
272 pid = os.fork()
273 if pid > 0: #parent
274 os._exit(0)
275 except OSError as e:
276 raise Exception("%s [%d]" % (e.strerror, e.errno))
344 277
345 def importone(self, version, pkgarch, checksum, value): 278 os.chdir("/")
346 return self.connection.importone(version, pkgarch, checksum, value)
347 279
348 def getinfo(self): 280 sys.stdout.flush()
349 return self.host, self.port 281 sys.stderr.flush()
350 282
351def start_daemon(dbfile, host, port, logfile): 283 # We could be called from a python thread with io.StringIO as
284 # stdout/stderr or it could be 'real' unix fd forking where we need
285 # to physically close the fds to prevent the program launching us from
286 # potentially hanging on a pipe. Handle both cases.
287 si = open("/dev/null", "r")
288 try:
289 os.dup2(si.fileno(), sys.stdin.fileno())
290 except (AttributeError, io.UnsupportedOperation):
291 sys.stdin = si
292 so = open(logfile, "a+")
293 try:
294 os.dup2(so.fileno(), sys.stdout.fileno())
295 except (AttributeError, io.UnsupportedOperation):
296 sys.stdout = so
297 try:
298 os.dup2(so.fileno(), sys.stderr.fileno())
299 except (AttributeError, io.UnsupportedOperation):
300 sys.stderr = so
301
302 # Clear out all log handlers prior to the fork() to avoid calling
303 # event handlers not part of the PRserver
304 for logger_iter in logging.Logger.manager.loggerDict.keys():
305 logging.getLogger(logger_iter).handlers = []
306
307 # Ensure logging makes it to the logfile
308 streamhandler = logging.StreamHandler()
309 streamhandler.setLevel(logging.DEBUG)
310 formatter = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
311 streamhandler.setFormatter(formatter)
312 logger.addHandler(streamhandler)
313
314 # write pidfile
315 pid = str(os.getpid())
316 with open(pidfile, "w") as pf:
317 pf.write("%s\n" % pid)
318
319 func()
320 os.remove(pidfile)
321 os._exit(0)
322
323def start_daemon(dbfile, host, port, logfile, read_only=False, upstream=None):
352 ip = socket.gethostbyname(host) 324 ip = socket.gethostbyname(host)
353 pidfile = PIDPREFIX % (ip, port) 325 pidfile = PIDPREFIX % (ip, port)
354 try: 326 try:
@@ -362,15 +334,13 @@ def start_daemon(dbfile, host, port, logfile):
362 % pidfile) 334 % pidfile)
363 return 1 335 return 1
364 336
365 server = PRServer(os.path.abspath(dbfile), os.path.abspath(logfile), (ip,port)) 337 dbfile = os.path.abspath(dbfile)
366 server.start() 338 def daemon_main():
339 server = PRServer(dbfile, read_only=read_only, upstream=upstream)
340 server.start_tcp_server(ip, port)
341 server.serve_forever()
367 342
368 # Sometimes, the port (i.e. localhost:0) indicated by the user does not match with 343 run_as_daemon(daemon_main, pidfile, os.path.abspath(logfile))
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))
374 return 0 344 return 0
375 345
376def stop_daemon(host, port): 346def stop_daemon(host, port):
@@ -388,37 +358,28 @@ def stop_daemon(host, port):
388 # so at least advise the user which ports the corresponding server is listening 358 # so at least advise the user which ports the corresponding server is listening
389 ports = [] 359 ports = []
390 portstr = "" 360 portstr = ""
391 for pf in glob.glob(PIDPREFIX % (ip,'*')): 361 for pf in glob.glob(PIDPREFIX % (ip, "*")):
392 bn = os.path.basename(pf) 362 bn = os.path.basename(pf)
393 root, _ = os.path.splitext(bn) 363 root, _ = os.path.splitext(bn)
394 ports.append(root.split('_')[-1]) 364 ports.append(root.split("_")[-1])
395 if len(ports): 365 if len(ports):
396 portstr = "Wrong port? Other ports listening at %s: %s" % (host, ' '.join(ports)) 366 portstr = "Wrong port? Other ports listening at %s: %s" % (host, " ".join(ports))
397 367
398 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n" 368 sys.stderr.write("pidfile %s does not exist. Daemon not running? %s\n"
399 % (pidfile,portstr)) 369 % (pidfile, portstr))
400 return 1 370 return 1
401 371
402 try: 372 try:
403 PRServerConnection(ip, port).terminate() 373 if is_running(pid):
404 except: 374 print("Sending SIGTERM to pr-server.")
405 logger.critical("Stop PRService %s:%d failed" % (host,port)) 375 os.kill(pid, signal.SIGTERM)
406 376 time.sleep(0.1)
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 377
415 if is_running(pid): 378 try:
416 print("Sending SIGTERM to pr-server.") 379 os.remove(pidfile)
417 os.kill(pid,signal.SIGTERM) 380 except FileNotFoundError:
418 time.sleep(0.1) 381 # The PID file might have been removed by the exiting process
419 382 pass
420 if os.path.exists(pidfile):
421 os.remove(pidfile)
422 383
423 except OSError as e: 384 except OSError as e:
424 err = str(e) 385 err = str(e)
@@ -436,7 +397,7 @@ def is_running(pid):
436 return True 397 return True
437 398
438def is_local_special(host, port): 399def is_local_special(host, port):
439 if host.strip().upper() == 'localhost'.upper() and (not port): 400 if (host == "localhost" or host == "127.0.0.1") and not port:
440 return True 401 return True
441 else: 402 else:
442 return False 403 return False
@@ -447,7 +408,7 @@ class PRServiceConfigError(Exception):
447def auto_start(d): 408def auto_start(d):
448 global singleton 409 global singleton
449 410
450 host_params = list(filter(None, (d.getVar('PRSERV_HOST') or '').split(':'))) 411 host_params = list(filter(None, (d.getVar("PRSERV_HOST") or "").split(":")))
451 if not host_params: 412 if not host_params:
452 # Shutdown any existing PR Server 413 # Shutdown any existing PR Server
453 auto_shutdown() 414 auto_shutdown()
@@ -456,11 +417,16 @@ def auto_start(d):
456 if len(host_params) != 2: 417 if len(host_params) != 2:
457 # Shutdown any existing PR Server 418 # Shutdown any existing PR Server
458 auto_shutdown() 419 auto_shutdown()
459 logger.critical('\n'.join(['PRSERV_HOST: incorrect format', 420 logger.critical("\n".join(["PRSERV_HOST: incorrect format",
460 'Usage: PRSERV_HOST = "<hostname>:<port>"'])) 421 'Usage: PRSERV_HOST = "<hostname>:<port>"']))
461 raise PRServiceConfigError 422 raise PRServiceConfigError
462 423
463 if is_local_special(host_params[0], int(host_params[1])): 424 host = host_params[0].strip().lower()
425 port = int(host_params[1])
426
427 upstream = d.getVar("PRSERV_UPSTREAM") or None
428
429 if is_local_special(host, port):
464 import bb.utils 430 import bb.utils
465 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE")) 431 cachedir = (d.getVar("PERSISTENT_DIR") or d.getVar("CACHE"))
466 if not cachedir: 432 if not cachedir:
@@ -474,39 +440,43 @@ def auto_start(d):
474 auto_shutdown() 440 auto_shutdown()
475 if not singleton: 441 if not singleton:
476 bb.utils.mkdirhier(cachedir) 442 bb.utils.mkdirhier(cachedir)
477 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), ("localhost",0)) 443 singleton = PRServSingleton(os.path.abspath(dbfile), os.path.abspath(logfile), host, port, upstream)
478 singleton.start() 444 singleton.start()
479 if singleton: 445 if singleton:
480 host, port = singleton.getinfo() 446 host = singleton.host
481 else: 447 port = singleton.port
482 host = host_params[0]
483 port = int(host_params[1])
484 448
485 try: 449 try:
486 connection = PRServerConnection(host,port) 450 ping(host, port)
487 connection.ping() 451 return str(host) + ":" + str(port)
488 realhost, realport = connection.getinfo() 452
489 return str(realhost) + ":" + str(realport)
490
491 except Exception: 453 except Exception:
492 logger.critical("PRservice %s:%d not available" % (host, port)) 454 logger.critical("PRservice %s:%d not available" % (host, port))
493 raise PRServiceConfigError 455 raise PRServiceConfigError
494 456
495def auto_shutdown(): 457def auto_shutdown():
496 global singleton 458 global singleton
497 if singleton: 459 if singleton and singleton.process:
498 host, port = singleton.getinfo() 460 singleton.process.terminate()
499 try: 461 singleton.process.join()
500 PRServerConnection(host, port).terminate()
501 except:
502 logger.critical("Stop PRService %s:%d failed" % (host,port))
503
504 try:
505 os.waitpid(singleton.prserv.pid, 0)
506 except ChildProcessError:
507 pass
508 singleton = None 462 singleton = None
509 463
510def ping(host, port): 464def ping(host, port):
511 conn=PRServerConnection(host, port) 465 from . import client
512 return conn.ping() 466
467 with client.PRClient() as conn:
468 conn.connect_tcp(host, port)
469 return conn.ping()
470
471def connect(host, port):
472 from . import client
473
474 global singleton
475
476 if host.strip().lower() == "localhost" and not port:
477 host = "localhost"
478 port = singleton.port
479
480 conn = client.PRClient()
481 conn.connect_tcp(host, port)
482 return conn
diff --git a/bitbake/lib/prserv/tests.py b/bitbake/lib/prserv/tests.py
new file mode 100644
index 0000000000..8765b129f2
--- /dev/null
+++ b/bitbake/lib/prserv/tests.py
@@ -0,0 +1,386 @@
1#! /usr/bin/env python3
2#
3# Copyright (C) 2024 BitBake Contributors
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8from . import create_server, create_client, increase_revision, revision_greater, revision_smaller, _revision_greater_or_equal
9import prserv.db as db
10from bb.asyncrpc import InvokeError
11import logging
12import os
13import sys
14import tempfile
15import unittest
16import socket
17import subprocess
18from pathlib import Path
19
20THIS_DIR = Path(__file__).parent
21BIN_DIR = THIS_DIR.parent.parent / "bin"
22
23version = "dummy-1.0-r0"
24pkgarch = "core2-64"
25other_arch = "aarch64"
26
27checksumX = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4f0"
28checksum0 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a0"
29checksum1 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a1"
30checksum2 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a2"
31checksum3 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a3"
32checksum4 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a4"
33checksum5 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a5"
34checksum6 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a6"
35checksum7 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a7"
36checksum8 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a8"
37checksum9 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a9"
38checksum10 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4aa"
39
40def server_prefunc(server, name):
41 logging.basicConfig(level=logging.DEBUG, filename='prserv-%s.log' % name, filemode='w',
42 format='%(levelname)s %(filename)s:%(lineno)d %(message)s')
43 server.logger.debug("Running server %s" % name)
44 sys.stdout = open('prserv-stdout-%s.log' % name, 'w')
45 sys.stderr = sys.stdout
46
47class PRTestSetup(object):
48
49 def start_server(self, name, dbfile, upstream=None, read_only=False, prefunc=server_prefunc):
50
51 def cleanup_server(server):
52 if server.process.exitcode is not None:
53 return
54 server.process.terminate()
55 server.process.join()
56
57 server = create_server(socket.gethostbyname("localhost") + ":0",
58 dbfile,
59 upstream=upstream,
60 read_only=read_only)
61
62 server.serve_as_process(prefunc=prefunc, args=(name,))
63 self.addCleanup(cleanup_server, server)
64
65 return server
66
67 def start_client(self, server_address):
68 def cleanup_client(client):
69 client.close()
70
71 client = create_client(server_address)
72 self.addCleanup(cleanup_client, client)
73
74 return client
75
76class FunctionTests(unittest.TestCase):
77
78 def setUp(self):
79 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
80 self.addCleanup(self.temp_dir.cleanup)
81
82 def test_increase_revision(self):
83 self.assertEqual(increase_revision("1"), "2")
84 self.assertEqual(increase_revision("1.0"), "1.1")
85 self.assertEqual(increase_revision("1.1.1"), "1.1.2")
86 self.assertEqual(increase_revision("1.1.1.3"), "1.1.1.4")
87 self.assertRaises(ValueError, increase_revision, "1.a")
88 self.assertRaises(ValueError, increase_revision, "1.")
89 self.assertRaises(ValueError, increase_revision, "")
90
91 def test_revision_greater_or_equal(self):
92 self.assertTrue(_revision_greater_or_equal("2", "2"))
93 self.assertTrue(_revision_greater_or_equal("2", "1"))
94 self.assertTrue(_revision_greater_or_equal("10", "2"))
95 self.assertTrue(_revision_greater_or_equal("1.10", "1.2"))
96 self.assertFalse(_revision_greater_or_equal("1.2", "1.10"))
97 self.assertTrue(_revision_greater_or_equal("1.10", "1"))
98 self.assertTrue(_revision_greater_or_equal("1.10.1", "1.10"))
99 self.assertFalse(_revision_greater_or_equal("1.10.1", "1.10.2"))
100 self.assertTrue(_revision_greater_or_equal("1.10.1", "1.10.1"))
101 self.assertTrue(_revision_greater_or_equal("1.10.1", "1"))
102 self.assertTrue(revision_greater("1.20", "1.3"))
103 self.assertTrue(revision_smaller("1.3", "1.20"))
104
105 # DB tests
106
107 def test_db(self):
108 dbfile = os.path.join(self.temp_dir.name, "testtable.sqlite3")
109
110 self.db = db.PRData(dbfile)
111 self.table = self.db["PRMAIN"]
112
113 self.table.store_value(version, pkgarch, checksum0, "0")
114 self.table.store_value(version, pkgarch, checksum1, "1")
115 # "No history" mode supports multiple PRs for the same checksum
116 self.table.store_value(version, pkgarch, checksum0, "2")
117 self.table.store_value(version, pkgarch, checksum2, "1.0")
118
119 self.assertTrue(self.table.test_package(version, pkgarch))
120 self.assertFalse(self.table.test_package(version, other_arch))
121
122 self.assertTrue(self.table.test_value(version, pkgarch, "0"))
123 self.assertTrue(self.table.test_value(version, pkgarch, "1"))
124 self.assertTrue(self.table.test_value(version, pkgarch, "2"))
125
126 self.assertEqual(self.table.find_package_max_value(version, pkgarch), "2")
127
128 self.assertEqual(self.table.find_min_value(version, pkgarch, checksum0), "0")
129 self.assertEqual(self.table.find_max_value(version, pkgarch, checksum0), "2")
130
131 # Test history modes
132 self.assertEqual(self.table.find_value(version, pkgarch, checksum0, True), "0")
133 self.assertEqual(self.table.find_value(version, pkgarch, checksum0, False), "2")
134
135 self.assertEqual(self.table.find_new_subvalue(version, pkgarch, "3"), "3.0")
136 self.assertEqual(self.table.find_new_subvalue(version, pkgarch, "1"), "1.1")
137
138 # Revision comparison tests
139 self.table.store_value(version, pkgarch, checksum1, "1.3")
140 self.table.store_value(version, pkgarch, checksum1, "1.20")
141 self.assertEqual(self.table.find_min_value(version, pkgarch, checksum1), "1")
142 self.assertEqual(self.table.find_max_value(version, pkgarch, checksum1), "1.20")
143
144class PRBasicTests(PRTestSetup, unittest.TestCase):
145
146 def setUp(self):
147 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
148 self.addCleanup(self.temp_dir.cleanup)
149
150 dbfile = os.path.join(self.temp_dir.name, "prtest-basic.sqlite3")
151
152 self.server1 = self.start_server("basic", dbfile)
153 self.client1 = self.start_client(self.server1.address)
154
155 def test_basic(self):
156
157 # Checks on non existing configuration
158
159 result = self.client1.test_pr(version, pkgarch, checksum0)
160 self.assertIsNone(result, "test_pr should return 'None' for a non existing PR")
161
162 result = self.client1.test_package(version, pkgarch)
163 self.assertFalse(result, "test_package should return 'False' for a non existing PR")
164
165 result = self.client1.max_package_pr(version, pkgarch)
166 self.assertIsNone(result, "max_package_pr should return 'None' for a non existing PR")
167
168 # Add a first configuration
169
170 result = self.client1.getPR(version, pkgarch, checksum0)
171 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'")
172
173 result = self.client1.test_pr(version, pkgarch, checksum0)
174 self.assertEqual(result, "0", "test_pr should return '0' here, matching the result of getPR")
175
176 result = self.client1.test_package(version, pkgarch)
177 self.assertTrue(result, "test_package should return 'True' for an existing PR")
178
179 result = self.client1.max_package_pr(version, pkgarch)
180 self.assertEqual(result, "0", "max_package_pr should return '0' in the current test series")
181
182 # Check that the same request gets the same value
183
184 result = self.client1.getPR(version, pkgarch, checksum0)
185 self.assertEqual(result, "0", "getPR: asking for the same PR a second time in a row should return the same value.")
186
187 # Add new configurations
188
189 result = self.client1.getPR(version, pkgarch, checksum1)
190 self.assertEqual(result, "1", "getPR: second PR of a package should be '1'")
191
192 result = self.client1.test_pr(version, pkgarch, checksum1)
193 self.assertEqual(result, "1", "test_pr should return '1' here, matching the result of getPR")
194
195 result = self.client1.max_package_pr(version, pkgarch)
196 self.assertEqual(result, "1", "max_package_pr should return '1' in the current test series")
197
198 result = self.client1.getPR(version, pkgarch, checksum2)
199 self.assertEqual(result, "2", "getPR: second PR of a package should be '2'")
200
201 result = self.client1.test_pr(version, pkgarch, checksum2)
202 self.assertEqual(result, "2", "test_pr should return '2' here, matching the result of getPR")
203
204 result = self.client1.max_package_pr(version, pkgarch)
205 self.assertEqual(result, "2", "max_package_pr should return '2' in the current test series")
206
207 result = self.client1.getPR(version, pkgarch, checksum3)
208 self.assertEqual(result, "3", "getPR: second PR of a package should be '3'")
209
210 result = self.client1.test_pr(version, pkgarch, checksum3)
211 self.assertEqual(result, "3", "test_pr should return '3' here, matching the result of getPR")
212
213 result = self.client1.max_package_pr(version, pkgarch)
214 self.assertEqual(result, "3", "max_package_pr should return '3' in the current test series")
215
216 # Ask again for the first configuration
217
218 result = self.client1.getPR(version, pkgarch, checksum0)
219 self.assertEqual(result, "4", "getPR: should return '4' in this configuration")
220
221 # Ask again with explicit "no history" mode
222
223 result = self.client1.getPR(version, pkgarch, checksum0, False)
224 self.assertEqual(result, "4", "getPR: should return '4' in this configuration")
225
226 # Ask again with explicit "history" mode. This should return the first recorded PR for checksum0
227
228 result = self.client1.getPR(version, pkgarch, checksum0, True)
229 self.assertEqual(result, "0", "getPR: should return '0' in this configuration")
230
231 # Check again that another pkgarg resets the counters
232
233 result = self.client1.test_pr(version, other_arch, checksum0)
234 self.assertIsNone(result, "test_pr should return 'None' for a non existing PR")
235
236 result = self.client1.test_package(version, other_arch)
237 self.assertFalse(result, "test_package should return 'False' for a non existing PR")
238
239 result = self.client1.max_package_pr(version, other_arch)
240 self.assertIsNone(result, "max_package_pr should return 'None' for a non existing PR")
241
242 # Now add the configuration
243
244 result = self.client1.getPR(version, other_arch, checksum0)
245 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'")
246
247 result = self.client1.test_pr(version, other_arch, checksum0)
248 self.assertEqual(result, "0", "test_pr should return '0' here, matching the result of getPR")
249
250 result = self.client1.test_package(version, other_arch)
251 self.assertTrue(result, "test_package should return 'True' for an existing PR")
252
253 result = self.client1.max_package_pr(version, other_arch)
254 self.assertEqual(result, "0", "max_package_pr should return '0' in the current test series")
255
256 result = self.client1.is_readonly()
257 self.assertFalse(result, "Server should not be described as 'read-only'")
258
259class PRUpstreamTests(PRTestSetup, unittest.TestCase):
260
261 def setUp(self):
262
263 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
264 self.addCleanup(self.temp_dir.cleanup)
265
266 dbfile2 = os.path.join(self.temp_dir.name, "prtest-upstream2.sqlite3")
267 self.server2 = self.start_server("upstream2", dbfile2)
268 self.client2 = self.start_client(self.server2.address)
269
270 dbfile1 = os.path.join(self.temp_dir.name, "prtest-upstream1.sqlite3")
271 self.server1 = self.start_server("upstream1", dbfile1, upstream=self.server2.address)
272 self.client1 = self.start_client(self.server1.address)
273
274 dbfile0 = os.path.join(self.temp_dir.name, "prtest-local.sqlite3")
275 self.server0 = self.start_server("local", dbfile0, upstream=self.server1.address)
276 self.client0 = self.start_client(self.server0.address)
277 self.shared_db = dbfile0
278
279 def test_upstream_and_readonly(self):
280
281 # For identical checksums, all servers should return the same PR
282
283 result = self.client2.getPR(version, pkgarch, checksum0)
284 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0'")
285
286 result = self.client1.getPR(version, pkgarch, checksum0)
287 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0' (same as upstream)")
288
289 result = self.client0.getPR(version, pkgarch, checksum0)
290 self.assertEqual(result, "0", "getPR: initial PR of a package should be '0' (same as upstream)")
291
292 # Now introduce new checksums on server1 for, same version
293
294 result = self.client1.getPR(version, pkgarch, checksum1)
295 self.assertEqual(result, "0.0", "getPR: first PR of a package which has a different checksum upstream should be '0.0'")
296
297 result = self.client1.getPR(version, pkgarch, checksum2)
298 self.assertEqual(result, "0.1", "getPR: second PR of a package that has a different checksum upstream should be '0.1'")
299
300 # Now introduce checksums on server0 for, same version
301
302 result = self.client1.getPR(version, pkgarch, checksum1)
303 self.assertEqual(result, "0.2", "getPR: can't decrease for known PR")
304
305 result = self.client1.getPR(version, pkgarch, checksum2)
306 self.assertEqual(result, "0.3")
307
308 result = self.client1.max_package_pr(version, pkgarch)
309 self.assertEqual(result, "0.3")
310
311 result = self.client0.getPR(version, pkgarch, checksum3)
312 self.assertEqual(result, "0.3.0", "getPR: first PR of a package that doesn't exist upstream should be '0.3.0'")
313
314 result = self.client0.getPR(version, pkgarch, checksum4)
315 self.assertEqual(result, "0.3.1", "getPR: second PR of a package that doesn't exist upstream should be '0.3.1'")
316
317 result = self.client0.getPR(version, pkgarch, checksum3)
318 self.assertEqual(result, "0.3.2")
319
320 # More upstream updates
321 # Here, we assume no communication between server2 and server0. server2 only impacts server0
322 # after impacting server1
323
324 self.assertEqual(self.client2.getPR(version, pkgarch, checksum5), "1")
325 self.assertEqual(self.client1.getPR(version, pkgarch, checksum6), "1.0")
326 self.assertEqual(self.client1.getPR(version, pkgarch, checksum7), "1.1")
327 self.assertEqual(self.client0.getPR(version, pkgarch, checksum8), "1.1.0")
328 self.assertEqual(self.client0.getPR(version, pkgarch, checksum9), "1.1.1")
329
330 # "history" mode tests
331
332 self.assertEqual(self.client2.getPR(version, pkgarch, checksum0, True), "0")
333 self.assertEqual(self.client1.getPR(version, pkgarch, checksum2, True), "0.1")
334 self.assertEqual(self.client0.getPR(version, pkgarch, checksum3, True), "0.3.0")
335
336 # More "no history" mode tests
337
338 self.assertEqual(self.client2.getPR(version, pkgarch, checksum0), "2")
339 self.assertEqual(self.client1.getPR(version, pkgarch, checksum0), "2") # Same as upstream
340 self.assertEqual(self.client0.getPR(version, pkgarch, checksum0), "2") # Same as upstream
341 self.assertEqual(self.client1.getPR(version, pkgarch, checksum7), "3") # This could be surprising, but since the previous revision was "2", increasing it yields "3".
342 # We don't know how many upstream servers we have
343 # Start read-only server with server1 as upstream
344 self.server_ro = self.start_server("local-ro", self.shared_db, upstream=self.server1.address, read_only=True)
345 self.client_ro = self.start_client(self.server_ro.address)
346
347 self.assertTrue(self.client_ro.is_readonly(), "Database should be described as 'read-only'")
348
349 # Checks on non existing configurations
350 self.assertIsNone(self.client_ro.test_pr(version, pkgarch, checksumX))
351 self.assertFalse(self.client_ro.test_package("unknown", pkgarch))
352
353 # Look up existing configurations
354 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum0), "3") # "no history" mode
355 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum0, True), "0") # "history" mode
356 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum3), "3")
357 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum3, True), "0.3.0")
358 self.assertEqual(self.client_ro.max_package_pr(version, pkgarch), "2") # normal as "3" was never saved
359
360 # Try to insert a new value. Here this one is know upstream.
361 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum7), "3")
362 # Try to insert a completely new value. As the max upstream value is already "3", it should be "3.0"
363 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum10), "3.0")
364 # Same with another value which only exists in the upstream upstream server
365 # This time, as the upstream server doesn't know it, it will ask its upstream server. So that's a known one.
366 self.assertEqual(self.client_ro.getPR(version, pkgarch, checksum9), "3")
367
368class ScriptTests(unittest.TestCase):
369
370 def setUp(self):
371
372 self.temp_dir = tempfile.TemporaryDirectory(prefix='bb-prserv')
373 self.addCleanup(self.temp_dir.cleanup)
374 self.dbfile = os.path.join(self.temp_dir.name, "prtest.sqlite3")
375
376 def test_1_start_bitbake_prserv(self):
377 try:
378 subprocess.check_call([BIN_DIR / "bitbake-prserv", "--start", "-f", self.dbfile])
379 except subprocess.CalledProcessError as e:
380 self.fail("Failed to start bitbake-prserv: %s" % e.returncode)
381
382 def test_2_stop_bitbake_prserv(self):
383 try:
384 subprocess.check_call([BIN_DIR / "bitbake-prserv", "--stop"])
385 except subprocess.CalledProcessError as e:
386 self.fail("Failed to stop bitbake-prserv: %s" % e.returncode)