106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""Real Debian frontend/inner locks; no boolean exclusion attestations."""
|
|
import contextlib
|
|
import fcntl
|
|
import os
|
|
import pathlib
|
|
|
|
from d09_store import secure
|
|
from os_update_plan import require
|
|
|
|
LOCKS = (pathlib.Path('/var/lib/dpkg/lock-frontend'), pathlib.Path('/var/lib/dpkg/lock'))
|
|
|
|
|
|
def evidence(paths=LOCKS):
|
|
rows = pathlib.Path('/proc/locks').read_text().splitlines()
|
|
result = []
|
|
for path in paths:
|
|
st = path.stat()
|
|
key = '%02x:%02x:%d' % (os.major(st.st_dev), os.minor(st.st_dev), st.st_ino)
|
|
matches = [line.split() for line in rows if key in line.split()]
|
|
require(any(row[1:5] == ['POSIX', 'ADVISORY', 'WRITE', str(os.getpid())] for row in matches),
|
|
'Debian lock not owned by current process: ' + str(path))
|
|
result.append(dict(path=str(path), device=st.st_dev, inode=st.st_ino, pid=os.getpid()))
|
|
return result
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def probe(paths=LOCKS):
|
|
"""Admission probe only. Execution must reacquire and retain its own lease."""
|
|
descriptors = []
|
|
try:
|
|
for path in paths:
|
|
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o640)
|
|
descriptors.append(fd)
|
|
os.close(secure(path))
|
|
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
yield evidence(paths)
|
|
finally:
|
|
for fd in reversed(descriptors):
|
|
os.close(fd)
|
|
|
|
|
|
class PackageLease:
|
|
"""libapt owns frontend continuously; dpkg gets only the inner-lock handoff.
|
|
|
|
Same-process libapt avoids an unsafe unlock/relock gap before apt-get. Never
|
|
use Debug::NoLocking or release frontend to accommodate a subprocess.
|
|
"""
|
|
def __init__(self, config):
|
|
self.config = config
|
|
|
|
def __enter__(self):
|
|
import apt
|
|
import apt_pkg
|
|
self.apt, self.apt_pkg = apt, apt_pkg
|
|
self.previous = os.environ.get('APT_CONFIG')
|
|
os.environ['APT_CONFIG'] = str(self.config)
|
|
# python3-apt clear requires a key; clearing every existing subtree also
|
|
# removes any host-loaded hook/source children from apt's import-time init.
|
|
for key in list(apt_pkg.config.keys()):
|
|
apt_pkg.config.clear(key)
|
|
apt_pkg.init_config()
|
|
apt_pkg.init_system()
|
|
self.lock = apt_pkg.SystemLock()
|
|
self.lock.__enter__()
|
|
try:
|
|
self.observation = evidence()
|
|
self.cache = apt.Cache()
|
|
self.cache.upgrade(dist_upgrade=True)
|
|
return self
|
|
except BaseException:
|
|
self.__exit__(None, None, None)
|
|
raise
|
|
|
|
def plan(self, expected):
|
|
changes = set()
|
|
for package in self.cache.get_changes():
|
|
if package.marked_delete:
|
|
changes.add(('Remv', package.name.split(':')[0], package.installed.version))
|
|
else:
|
|
changes.add(('Inst', package.name.split(':')[0], package.candidate.version))
|
|
wanted = {(p['operation'], p['name'].split(':')[0], p['version']) for p in expected if p['operation'] != 'Conf'}
|
|
require(changes == wanted, 'locked libapt transaction differs from authenticated plan')
|
|
self.pm = self.apt_pkg.PackageManager(self.cache._depcache)
|
|
self.fetcher = self.apt_pkg.Acquire(self.apt.progress.base.AcquireProgress())
|
|
# Offline file mirror, authenticated with Debian keyring; no install yet.
|
|
self.cache._fetch_archives(self.fetcher, self.pm, False)
|
|
evidence()
|
|
|
|
def install(self):
|
|
import apt.progress.base
|
|
os.environ.update(DEBIAN_FRONTEND='noninteractive', NEEDRESTART_MODE='l',
|
|
PATH='/usr/sbin:/usr/bin:/sbin:/bin', LC_ALL='C')
|
|
result = self.cache.install_archives(self.pm, apt.progress.base.InstallProgress())
|
|
# No media/retry loop: partial or unknown outcome always reconciles.
|
|
require(result == self.pm.RESULT_COMPLETED, 'dpkg partial/unknown result')
|
|
evidence()
|
|
|
|
def __exit__(self, *exc):
|
|
try:
|
|
self.lock.__exit__(*exc)
|
|
finally:
|
|
if self.previous is None:
|
|
os.environ.pop('APT_CONFIG', None)
|
|
else:
|
|
os.environ['APT_CONFIG'] = self.previous
|