commit: fb1d0a22f65747b750143080536a4129e8654f97 Author: Zac Medico <zmedico <AT> gentoo <DOT> org> AuthorDate: Fri Dec 29 00:29:40 2023 +0000 Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> CommitDate: Sat Dec 30 21:46:04 2023 +0000 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=fb1d0a22
dbapi: KeyError tolerance during package moves Raise a new CorruptionKeyError exception type instead of a plain KeyError when os.stat fails. Treat this type of exception as a warning during package moves. Also fix this error that the test case triggered in the binarytree.remove method: NameError: name 'binpkg_path' is not defined Bug: https://bugs.gentoo.org/920828 Signed-off-by: Zac Medico <zmedico <AT> gentoo.org> lib/portage/dbapi/__init__.py | 14 ++++-- lib/portage/dbapi/bintree.py | 28 +++++++++--- lib/portage/dbapi/vartree.py | 9 ++-- lib/portage/exception.py | 6 ++- lib/portage/tests/update/test_update_dbentry.py | 60 +++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 14 deletions(-) diff --git a/lib/portage/dbapi/__init__.py b/lib/portage/dbapi/__init__.py index 09163e94d8..6f95b93a21 100644 --- a/lib/portage/dbapi/__init__.py +++ b/lib/portage/dbapi/__init__.py @@ -25,7 +25,11 @@ from portage.const import MERGING_IDENTIFIER from portage import os from portage import auxdbkeys from portage.eapi import _get_eapi_attrs -from portage.exception import InvalidBinaryPackageFormat, InvalidData +from portage.exception import ( + CorruptionKeyError, + InvalidBinaryPackageFormat, + InvalidData, +) from portage.localization import _ from _emerge.Package import Package @@ -424,7 +428,7 @@ class dbapi: if metadata_updates: try: aux_update(cpv, metadata_updates) - except InvalidBinaryPackageFormat as e: + except (InvalidBinaryPackageFormat, CorruptionKeyError) as e: warnings.warn(e) if onUpdate: onUpdate(maxval, i + 1) @@ -470,5 +474,9 @@ class dbapi: ): newslot = f"{newslot}/{mycpv.sub_slot}" mydata = {"SLOT": newslot + "\n"} - self.aux_update(mycpv, mydata) + try: + self.aux_update(mycpv, mydata) + except CorruptionKeyError as e: + warnings.warn(e) + continue return moves diff --git a/lib/portage/dbapi/bintree.py b/lib/portage/dbapi/bintree.py index a20c8dfe26..a139e37659 100644 --- a/lib/portage/dbapi/bintree.py +++ b/lib/portage/dbapi/bintree.py @@ -37,6 +37,7 @@ from portage.dbapi.virtual import fakedbapi from portage.dep import Atom, use_reduce, paren_enclose from portage.exception import ( AlarmSignal, + CorruptionKeyError, InvalidPackageName, InvalidBinaryPackageFormat, ParseError, @@ -210,9 +211,9 @@ class bindbapi(fakedbapi): raise KeyError(mycpv) binpkg_path = os.path.join(self.bintree.pkgdir, binpkg_path) try: - st = os.lstat(binpkg_path) - except OSError: - raise KeyError(mycpv) + st = os.stat(binpkg_path) + except OSError as oe: + raise CorruptionKeyError(mycpv) from oe binpkg_format = get_binpkg_format(binpkg_path) if binpkg_format == "xpak": @@ -283,8 +284,10 @@ class bindbapi(fakedbapi): cpv_str += f"-{build_id}" binpkg_path = self.bintree.getname(cpv) - if not os.path.exists(binpkg_path): - raise KeyError(cpv) + try: + os.stat(binpkg_path) + except OSError as oe: + raise CorruptionKeyError(cpv) from oe binpkg_format = get_binpkg_format(binpkg_path) if binpkg_format == "xpak": @@ -694,7 +697,18 @@ class binarytree: continue binpkg_path = self.getname(mycpv) - if os.path.exists(binpkg_path) and not os.access(binpkg_path, os.W_OK): + try: + os.stat(binpkg_path) + except FileNotFoundError: + writemsg(_("!!! File not found: %s\n") % binpkg_path, noiselevel=-1) + continue + except OSError as oe: + writemsg( + _("!!! File os error (path %s): %s\n") % (binpkg_path, oe), + noiselevel=-1, + ) + continue + if not os.access(binpkg_path, os.W_OK): writemsg( _("!!! Cannot update readonly binary: %s\n") % mycpv, noiselevel=-1 ) @@ -1829,7 +1843,7 @@ class binarytree: writemsg( colorize( "WARN", - f"Failed to remove package: {binpkg_path} {str(err)}", + f"Failed to remove package: {pkg_path} {str(err)}", ) ) finally: diff --git a/lib/portage/dbapi/vartree.py b/lib/portage/dbapi/vartree.py index 11767a9f91..5d39ca1965 100644 --- a/lib/portage/dbapi/vartree.py +++ b/lib/portage/dbapi/vartree.py @@ -1,4 +1,4 @@ -# Copyright 1998-2021 Gentoo Authors +# Copyright 1998-2023 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 __all__ = ["vardbapi", "vartree", "dblink"] + ["write_contents", "tar_contents"] @@ -60,6 +60,7 @@ from portage.const import ( from portage.dbapi import dbapi from portage.exception import ( CommandNotFound, + CorruptionKeyError, InvalidData, InvalidLocation, InvalidPackageName, @@ -990,8 +991,10 @@ class vardbapi(dbapi): def aux_update(self, cpv, values): mylink = self._dblink(cpv) - if not mylink.exists(): - raise KeyError(cpv) + try: + os.stat(mylink.dbdir) + except OSError as oe: + raise CorruptionKeyError(cpv) from oe self._bump_mtime(cpv) self._clear_pkg_cache(mylink) for k, v in values.items(): diff --git a/lib/portage/exception.py b/lib/portage/exception.py index 153a9f9a55..7b48aa919e 100644 --- a/lib/portage/exception.py +++ b/lib/portage/exception.py @@ -1,4 +1,4 @@ -# Copyright 1998-2020 Gentoo Authors +# Copyright 1998-2023 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 import signal @@ -30,6 +30,10 @@ class CorruptionError(PortageException): """Corruption indication""" +class CorruptionKeyError(CorruptionError, PortageKeyError): + """KeyError raised when corruption is detected (cause should be accesssible as __cause__)""" + + class InvalidDependString(PortageException): """An invalid depend string has been encountered""" diff --git a/lib/portage/tests/update/test_update_dbentry.py b/lib/portage/tests/update/test_update_dbentry.py index a3c9a37e8c..3b3f0caae6 100644 --- a/lib/portage/tests/update/test_update_dbentry.py +++ b/lib/portage/tests/update/test_update_dbentry.py @@ -1,6 +1,7 @@ # Copyright 2012-2023 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 +import shutil import sys import re import textwrap @@ -9,6 +10,7 @@ import portage from portage import os from portage.const import SUPPORTED_GENTOO_BINPKG_FORMATS from portage.dep import Atom +from portage.exception import CorruptionKeyError from portage.tests import TestCase from portage.tests.resolver.ResolverPlayground import ResolverPlayground from portage.update import update_dbentry @@ -186,6 +188,11 @@ class UpdateDbentryTestCase(TestCase): "EAPI": "4", "SLOT": "2", }, + "dev-libs/B-2::test_repo": { + "SLOT": "2", + "RDEPEND": "dev-libs/M dev-libs/N dev-libs/P", + "EAPI": "4", + }, "dev-libs/B-1::test_repo": { "RDEPEND": "dev-libs/M dev-libs/N dev-libs/P", "EAPI": "4", @@ -215,6 +222,11 @@ class UpdateDbentryTestCase(TestCase): "RDEPEND": "dev-libs/M dev-libs/N dev-libs/P", "EAPI": "4", }, + "dev-libs/B-2::test_repo": { + "SLOT": "2", + "RDEPEND": "dev-libs/M dev-libs/N dev-libs/P", + "EAPI": "4", + }, } world = ["dev-libs/M", "dev-libs/N"] @@ -269,6 +281,34 @@ class UpdateDbentryTestCase(TestCase): ) ) + # Delete some things in order to trigger CorruptionKeyError during package moves. + corruption_atom = Atom("dev-libs/B:2") + # Demonstrate initial state. + self.assertEqual(bindb.match(corruption_atom), ["dev-libs/B-2"]) + for cpv in bindb.match(corruption_atom): + os.unlink(bindb.bintree.getname(cpv)) + self.assertRaises( + CorruptionKeyError, + bindb.aux_update, + cpv, + {"RDEPEND": "dev-libs/M-moved"}, + ) + # Demonstrate corrupt state. + self.assertEqual(bindb.match(corruption_atom), ["dev-libs/B-2"]) + + # Demonstrate initial state. + self.assertEqual(vardb.match(corruption_atom), ["dev-libs/B-2"]) + for cpv in vardb.match(corruption_atom): + shutil.rmtree(vardb.getpath(cpv)) + self.assertRaises( + CorruptionKeyError, + vardb.aux_update, + cpv, + {"RDEPEND": "dev-libs/M-moved"}, + ) + # Demonstrate correct state because vardbapi checks the disk. + self.assertEqual(vardb.match(corruption_atom), []) + global_noiselimit = portage.util.noiselimit portage.util.noiselimit = -2 try: @@ -304,6 +344,26 @@ class UpdateDbentryTestCase(TestCase): self.assertTrue("dev-libs/M" in rdepend) self.assertTrue("dev-libs/M-moved" not in rdepend) + # Demonstrate that match still returns stale results + # due to intentional corruption. + self.assertEqual(bindb.match(corruption_atom), ["dev-libs/B-2"]) + + # Update bintree state so aux_get will properly raise KeyError. + for cpv in bindb.match(corruption_atom): + # Demonstrate that aux_get returns stale results. + self.assertEqual( + ["dev-libs/M dev-libs/N dev-libs/P"], + bindb.aux_get(cpv, ["RDEPEND"]), + ) + bindb.bintree.remove(cpv) + self.assertEqual(bindb.match(corruption_atom), []) + self.assertRaises( + KeyError, bindb.aux_get, "dev-libs/B-2", ["RDEPEND"] + ) + self.assertRaises( + KeyError, vardb.aux_get, "dev-libs/B-2", ["RDEPEND"] + ) + selected_set.load() self.assertTrue("dev-libs/M" not in selected_set) self.assertTrue("dev-libs/M-moved" in selected_set)