Files
archy/scripts/os_update_lock.py

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