diff options
Diffstat (limited to 'bitbake/lib/prserv')
-rw-r--r-- | bitbake/lib/prserv/__init__.py | 99 | ||||
-rw-r--r-- | bitbake/lib/prserv/client.py | 72 | ||||
-rw-r--r-- | bitbake/lib/prserv/db.py | 427 | ||||
-rw-r--r-- | bitbake/lib/prserv/serv.py | 684 | ||||
-rw-r--r-- | bitbake/lib/prserv/tests.py | 386 |
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 | ||
7 | import os, time | 8 | __version__ = "2.0.0" |
8 | import sys,logging | 9 | |
10 | import logging | ||
11 | logger = logging.getLogger("BitBake.PRserv") | ||
12 | |||
13 | from bb.asyncrpc.client import parse_address, ADDR_TYPE_UNIX, ADDR_TYPE_WS | ||
14 | |||
15 | def 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 | |||
24 | def 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 | |||
39 | def _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 | |||
60 | def 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 | |||
65 | def 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 | |||
70 | def 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 | |||
83 | async def create_async_client(addr): | ||
84 | from . import client | ||
85 | |||
86 | c = client.PRAsyncClient() | ||
9 | 87 | ||
10 | def 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 | ||
17 | class 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 | |||
7 | import logging | ||
8 | import bb.asyncrpc | ||
9 | from . import create_async_client | ||
10 | |||
11 | logger = logging.getLogger("BitBake.PRserv") | ||
12 | |||
13 | class 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 | |||
66 | class 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 | |||
6 | import os.path | 8 | import os.path |
7 | import errno | 9 | import errno |
8 | import prserv | 10 | import prserv |
9 | import time | 11 | import sqlite3 |
10 | 12 | ||
11 | try: | 13 | from contextlib import closing |
12 | import sqlite3 | 14 | from . import increase_revision, revision_greater, revision_smaller |
13 | except ImportError: | ||
14 | from pysqlite2 import dbapi2 as sqlite3 | ||
15 | 15 | ||
16 | logger = logging.getLogger("BitBake.PRserv") | 16 | logger = logging.getLogger("BitBake.PRserv") |
17 | 17 | ||
18 | sqlversion = sqlite3.sqlite_version_info | ||
19 | if 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 | ||
32 | class PRTable(object): | 27 | class 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 | ||
246 | class PRData(object): | 262 | class 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 | ||
5 | import os,sys,logging | 7 | import os,sys,logging |
6 | import signal, time | 8 | import signal, time |
7 | from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler | ||
8 | import threading | ||
9 | import queue | ||
10 | import socket | 9 | import socket |
11 | import io | 10 | import io |
12 | import sqlite3 | 11 | import sqlite3 |
13 | import bb.server.xmlrpcclient | ||
14 | import prserv | 12 | import prserv |
15 | import prserv.db | 13 | import prserv.db |
16 | import errno | 14 | import errno |
17 | import select | 15 | from . import create_async_client, revision_smaller, increase_revision |
16 | import bb.asyncrpc | ||
18 | 17 | ||
19 | logger = logging.getLogger("BitBake.PRserv") | 18 | logger = logging.getLogger("BitBake.PRserv") |
20 | 19 | ||
21 | if sys.hexversion < 0x020600F0: | 20 | PIDPREFIX = "/tmp/PRServer_%s_%s.pid" |
22 | print("Sorry, python 2.6 or later is required.") | 21 | singleton = None |
23 | sys.exit(1) | ||
24 | 22 | ||
25 | class Handler(SimpleXMLRPCRequestHandler): | 23 | class 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 | ||
35 | PIDPREFIX = "/tmp/PRServer_%s_%s.pid" | 47 | async def handle_test_pr(self, request): |
36 | singleton = 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 | ||
39 | class 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 | ||
302 | class 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 | |||
207 | class 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 | ||
318 | class 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 | |||
235 | class 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): | 253 | def 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 | ||
351 | def 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 | |||
323 | def 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 | ||
376 | def stop_daemon(host, port): | 346 | def 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 | ||
438 | def is_local_special(host, port): | 399 | def 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): | |||
447 | def auto_start(d): | 408 | def 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 | ||
495 | def auto_shutdown(): | 457 | def 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 | ||
510 | def ping(host, port): | 464 | def 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 | |||
471 | def 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 | |||
8 | from . import create_server, create_client, increase_revision, revision_greater, revision_smaller, _revision_greater_or_equal | ||
9 | import prserv.db as db | ||
10 | from bb.asyncrpc import InvokeError | ||
11 | import logging | ||
12 | import os | ||
13 | import sys | ||
14 | import tempfile | ||
15 | import unittest | ||
16 | import socket | ||
17 | import subprocess | ||
18 | from pathlib import Path | ||
19 | |||
20 | THIS_DIR = Path(__file__).parent | ||
21 | BIN_DIR = THIS_DIR.parent.parent / "bin" | ||
22 | |||
23 | version = "dummy-1.0-r0" | ||
24 | pkgarch = "core2-64" | ||
25 | other_arch = "aarch64" | ||
26 | |||
27 | checksumX = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4f0" | ||
28 | checksum0 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a0" | ||
29 | checksum1 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a1" | ||
30 | checksum2 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a2" | ||
31 | checksum3 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a3" | ||
32 | checksum4 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a4" | ||
33 | checksum5 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a5" | ||
34 | checksum6 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a6" | ||
35 | checksum7 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a7" | ||
36 | checksum8 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a8" | ||
37 | checksum9 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4a9" | ||
38 | checksum10 = "51bf8189dbe9ea81fa6dd89608bf19380c437a9cf12f6c6239887801ba4ab4aa" | ||
39 | |||
40 | def 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 | |||
47 | class 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 | |||
76 | class 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 | |||
144 | class 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 | |||
259 | class 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 | |||
368 | class 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) | ||