./Changelog 0000644 0003721 0004705 00000015323 14176516235 012336 0 ustar buildd buildd 2007-02-14:
- automatically generate the .py file from the .ui files
2007-02-14:
- fix the $dist-proposed handling
- fix in $dist-commercial handling (LP#66783)
- updated demotions
2007-02-13:
- fix in the reboot code (lp: #84538)
- make some string more distro neutral
2007-02-07:
- add support for the cdrom upgrader to update itself
2007-02-05:
- use the new python-apt aptsources api
- server mode has magic to deal with runing under ssh
2007-02-02:
- merged the KDE frontend (thanks Riddell)
- added tasks support
- add dist-upgrade --mode=server
2007-02-01:
- fix apport integration
2007-01-29:
- fixes in the InstallProgress.error() method
- DistUpgrade/DistUpgradeView.py: fix InstallProgress refactoring
- updated obsoletes
- auto-generate the codename for the upgrade based on the build-system coden (lp: #77620)
- various bugfixes
- apport support
2006-12-12:
- rewrote the _checkFreeSpace() and add better checking
for /boot (#70683)
2006-11-28:
- add quirks rule to install ndiswrapper-utils-1.9 if 1.8 is
installed
2006-11-27:
- fix caption in main glade file
- use "Dpkg::StopOnError","False" option
2006-11-23:
- initial feisty upload
2006-10-28:
- catch errors when load_icon() does not work
2006-10-27:
- reset self.read so that we do not loop endlessly when dpkg
sends unexpected data (lp: #68553)
2006-10-26:
- make sure that xserver-xorg-video-all get installed if
xserver-xorg-driver-all was installed before (lp: #58424)
2006-10-21:
- comment out old cdrom sources
- demotions updated
2006-10-21:
- fix incorrect arguments in fixup logging (lp: #67311)
- more error logging
- fix upgrade problems for people with unofficial compiz
repositories (lp: #58424)
- rosetta i18n updates
- uploaded
2006-10-17:
- ensure bzr, tomboy and xserver-xorg-input-* are properly
upgraded
- don't fail if dpkg sents unexpected status lines (lp: #66013)
2006-10-16:
- remove leftover references to ubuntu-base and
use ubuntu-minimal and ubuntu-standard instead
- updated translations from rosetta
2006-10-13:
- log held-back as well
2006-10-12:
- check if cdrom.lst actually exists before copying it
2006-10-11:
- keep pixbuf loader reference around so that we
have one after the upgrade when the old
/usr/lib/gtk-2.0/loader/2.4.0/ loader is gone.
This fixes the problem of missing stock-icons
after the upgrade. Also revalidate the theme
in each step.
2006-10-10:
- fix time calculation
- fix kubuntu upgrade case
2006-10-06:
- fix source.list rewrite corner case bug (#64159)
2006-10-04:
- improve the space checking/logging
2006-09-29:
- typo fix (thanks to Jane Silber) (lp: #62946)
2006-09-28:
- bugfix in the cdromupgrade script
2006-09-27:
- uploaded a version that only reverts the backport fetching
but no other changes compared to 2006-09-23
2006-09-27:
- embarrassing bug cdromupgrade.sh
2006-09-26:
- comment out the getRequiredBackport code because we will
not use Breaks for the dapper->edgy upgrade yet
(see #54234 for the rationale)
- updated demotions.cfg for dapper->edgy
- special case the packages affected by the Breaks changes
- make sure that no translations get lost during the upgrade
(thanks to mdz for pointing this out)
- bugfixes
2006-09-23:
- support fetching backports of selected packages first and
use them for the upgrade (needed to support Breaks)
- fetch/use apt/dpkg/python-apt backports for the upgrade
2006-09-06:
- increased the "free-space-savety-buffer" to 100MB
2006-09-05:
- added "RemoveEssentialOk" option and put "sysvinit" into it
2006-09-04:
- set Debug::pkgDepCache::AutoInstall as debug option too
- be more robust against failure from the locales
- remove libgl1-mesa (no longer needed on edgy)
2006-09-03:
- fix in the cdromupgrade script path detection
2006-09-01:
- make the cdromupgrade wrapper work with the compressed version
of the upgrader as put onto the CD
- uploaded
2006-08-30:
- fixes to the cdromupgrade wrapper
2006-08-29:
- always enable the "main" component to make sure it is available
- add download estimated time
- add --cdrom switch to make cdrom based dist-upgrades possible
- better error reporting
- moved the logging into the /var/log/dist-upgrade/ dir
- change the obsoletes calculation when run without network and
consider demotions as obsoletes then (because we can't really
use the "pkg.downloadable" hint without network)
- uploaded
2006-08-18:
- sort the demoted software list
2006-07-31:
- updated to edgy
- uploadedd
2006-05-31:
- fix bug in the free space calculation (#47092)
- updated ReleaseAnnouncement
- updated translations
- fix a missing bindtextdomain
- fix a incorrect ngettext usage
- added quirks handler to fix nvidia-glx issue (#47017)
Thanks to the amazing Kiko for helping improve this!
2006-05-24:
- if the initial "update()" fails, just exit, don't try
to restore the old sources.list (nothing was modified yet)
Ubuntu: #46712
- fix a bug in the sourcesList rewriting (ubuntu: #46245)
- expand the terminal when no libgnome2-perl is installed
because debconf might want to ask questions (ubuntu: #46214)
- disable the breezy cdrom source to make removal of demoted
packages work properly (ubuntu: #46336)
- translations updated from rosetta
- fixed a bug in the demotions calculation (ubuntu: #46245)
- typos fixed and translations unfuzzied (ubuntu: #46792,#46464)
- upload
2006-05-12:
- space checking improved (ubuntu: #43948)
- show software that was demoted from main -> universe
- improve the remaining time reporting
- translation updates
- upload
2006-05-09:
- upload
2006-05-08:
- fix error when asking for media-change (ubuntu: 43442,43728)
2006-05-02:
- upload
2006-04-28:
- add more sanity checking, if no valid mirror is found in the
sources.list ask for "dumb" rewrite
- if nothing valid was found after a dumb rewrite, add official
sources
- don't report install TIMEOUT over and over in the log
- report what package caused a install TIMEOUT
2006-04-27:
- add a additonal sanity check after the rewriting of the sources.list
(check for BaseMetaPkgs still in the cache)
- on abort reopen() the cache to force writing a new
/var/cache/apt/pkgcache.bin
- use a much more compelte mirror list (based on the information
from https://wiki.ubuntu.com/Archive)
2006-04-25:
- make sure that DistUpgradeView.getTerminal().call() actually
waits until the command has finished (dpkg --configure -a)
2006-04-18:
- add logging to the sources.list modification code
- general logging improvements (thanks to Xavier Poinsard)
./DevelReleaseAnnouncement 0000644 0003721 0004705 00000004334 15003755605 015356 0 ustar buildd buildd = Welcome to the Ubuntu 'Questing Quokka' development release =
''This release is still in development.''
Thanks for your interest in this development release of Ubuntu.
The Ubuntu developers are moving very quickly to bring you the
absolute latest and greatest software the Open Source Community has to
offer. This development release brings you a taste of the newest features
for the next version of Ubuntu.
== Testing ==
Please help to test this development snapshot and report problems back to the
developers. For more information about testing Ubuntu, please read:
https://ubuntu.com/testing
== Reporting Bugs ==
This development release of Ubuntu contains bugs. If you want to help
out with bugs, the Bug Squad is always looking for help. Please read the
following information about reporting bugs:
https://help.ubuntu.com/community/ReportingBugs
Then report bugs using apport in Ubuntu. For example:
ubuntu-bug linux
will open a bug report in Launchpad regarding the linux package. Your
comments, bug reports, patches and suggestions will help fix bugs and improve
future releases.
If you have a question, or if you think you may have found a bug but aren't
sure, first try to reach out on one of the communication channels. Matrix is the
go-to for instant chatting, while Discourse would be more approriate for long
discussions in a more asynchronous way. Otherwise you can still join the #ubuntu
IRC channel on Libera.Chat, send an email to the Ubuntu Users mailing list, or
find some help on the Ubuntu forums:
https://ubuntu.com/community/communications/matrix
https://discourse.ubuntu.com/
https://help.ubuntu.com/community/InternetRelayChat
https://lists.ubuntu.com/mailman/listinfo/ubuntu-users
https://ubuntuforums.org/
== Participate in Ubuntu ==
If you would like to help shape Ubuntu, take a look at the list of
ways you can participate at
https://ubuntu.com/community/contribute
== More Information ==
You can find out more about Ubuntu on the Ubuntu website and Ubuntu
wiki.
https://ubuntu.com/
https://wiki.ubuntu.com/
To sign up for Ubuntu development announcements, please
subscribe to Ubuntu's development announcement list at:
https://lists.ubuntu.com/mailman/listinfo/ubuntu-devel-announce
./DevelReleaseAnnouncement.html 0000644 0003721 0004705 00000006402 15003755605 016317 0 ustar buildd buildd
Welcome to the Ubuntu 'Questing Quokka' development release
Welcome to the Ubuntu 'Questing Quokka' development release
This release is still in development.
Thanks for your interest in this development release of Ubuntu.
The Ubuntu developers are moving very quickly to bring you the
absolute latest and greatest software the Open Source Community has to
offer. This development release brings you a taste of the newest features
for the next version of Ubuntu.
Testing
Please help to test this development snapshot and report problems back to the
developers. For more information about testing Ubuntu, please read:
https://ubuntu.com/testing
Reporting Bugs
This development release of Ubuntu contains bugs. If you want to help
out with bugs, the Bug Squad is always looking for help. Please read the
following information about reporting bugs:
https://help.ubuntu.com/community/ReportingBugs
Then report bugs using apport in Ubuntu. For example:
ubuntu-bug linux
will open a bug report in Launchpad regarding the linux package. Your
comments, bug reports, patches and suggestions will help fix bugs and improve
future releases.
If you have a question, or if you think you may have found a bug but aren't
sure, first try to reach out on one of the communication channels. Matrix is the
go-to for instant chatting, while Discourse would be more approriate for long
discussions in a more asynchronous way. Otherwise you can still join the #ubuntu
IRC channel on Libera.Chat, send an email to the Ubuntu Users mailing list, or
find some help on the Ubuntu forums:
https://ubuntu.com/community/communications/matrix
https://discourse.ubuntu.com/
https://help.ubuntu.com/community/InternetRelayChat
https://lists.ubuntu.com/mailman/listinfo/ubuntu-users
https://ubuntuforums.org/
Participate in Ubuntu
If you would like to help shape Ubuntu, take a look at the list of
ways you can participate at
https://ubuntu.com/community/contribute
More Information
You can find out more about Ubuntu on the Ubuntu website and Ubuntu
wiki.
https://ubuntu.com/
https://wiki.ubuntu.com/
To sign up for Ubuntu development announcements, please
subscribe to Ubuntu's development announcement list at:
https://lists.ubuntu.com/mailman/listinfo/ubuntu-devel-announce
./DistUpgradeApport.py 0000644 0003721 0004705 00000011647 15000447260 014470 0 ustar buildd buildd
import os
import logging
import subprocess
import sys
import gettext
import errno
APPORT_ALLOWLIST = {
"apt.log": "Aptlog",
"apt-term.log": "Apttermlog",
"apt-clone_system_state.tar.gz": "Aptclonesystemstate.tar.gz",
"history.log": "Historylog",
"lspci.txt": "Lspcitxt",
"main.log": "Mainlog",
"term.log": "Termlog",
"screenlog.0": "Screenlog",
"xorg_fixup.log": "Xorgfixup",
}
def _apport_append_logfiles(report, logdir="/var/log/dist-upgrade/"):
dirname = 'VarLogDistupgrade'
for fname in APPORT_ALLOWLIST:
f = os.path.join(logdir, fname)
if not os.path.isfile(f) or os.path.getsize(f) == 0:
continue
ident = dirname + APPORT_ALLOWLIST[fname]
if os.access(f, os.R_OK):
report[ident] = (f, )
elif os.path.exists(f):
try:
from apport.hookutils import root_command_output
report[ident] = root_command_output(
["cat", '%s' % f], decode_utf8=False)
except ImportError:
logging.error("failed to import apport python module, "
"can't include: %s" % ident)
def apport_crash(type, value, tb):
logging.debug("running apport_crash()")
if "RELEASE_UPGRADER_NO_APPORT" in os.environ:
logging.debug("RELEASE_UPGRADER_NO_APPORT env set")
return False
try:
# we don't depend on python3-apport because of servers
from apport_python_hook import apport_excepthook
from apport.report import Report
except ImportError as e:
logging.error("failed to import apport python module, can't "
"generate crash: %s" % e)
return False
from .DistUpgradeVersion import VERSION
# we pretend we are do-release-upgrade
sys.argv[0] = "/usr/bin/do-release-upgrade"
apport_excepthook('/usr/bin/do-release-upgrade', type, value, tb)
# now add the files in /var/log/dist-upgrade/*
if os.path.exists('/var/crash/_usr_bin_do-release-upgrade.0.crash'):
report = Report()
report.setdefault('Tags', 'dist-upgrade')
release = 'Ubuntu %s' % VERSION[0:5]
report.setdefault('DistroRelease', release)
# use the version of the release-upgrader tarball, not the installed
# package
report.setdefault('Package', 'ubuntu-release-upgrader-core 1:%s' %
VERSION)
_apport_append_logfiles(report)
report.add_to_existing(
'/var/crash/_usr_bin_do-release-upgrade.0.crash')
return True
def apport_pkgfailure(pkg, errormsg):
logging.debug("running apport_pkgfailure() %s: %s", pkg, errormsg)
if "RELEASE_UPGRADER_NO_APPORT" in os.environ:
logging.debug("RELEASE_UPGRADER_NO_APPORT env set")
return False
LOGDIR = "/var/log/dist-upgrade/"
s = "/usr/share/apport/package_hook"
# we do not report followup errors from earlier failures
# dpkg messages will not be translated if DPKG_UNTRANSLATED_MESSAGES is
# set which it is by default so check for the English message first
if "dependency problems - leaving unconfigured" in errormsg:
logging.debug("dpkg error because of dependency problems, not "
"reporting against %s " % pkg)
return False
needle = gettext.dgettext(
'dpkg', "dependency problems - leaving unconfigured")
if needle in errormsg:
logging.debug("dpkg error because of dependency problems, not "
"reporting against %s " % pkg)
return False
# we do not run apport_pkgfailure for full disk errors
if os.strerror(errno.ENOSPC) in errormsg:
logging.debug("dpkg error because of full disk, not reporting "
"against %s " % pkg)
return False
if os.path.exists(s):
args = [s, "-p", pkg]
args.extend(["--tags", "dist-upgrade"])
for fname in APPORT_ALLOWLIST:
args.extend(["-l", os.path.join(LOGDIR, fname)])
try:
p = subprocess.Popen(args, stdin=subprocess.PIPE,
universal_newlines=True)
p.stdin.write(errormsg)
p.stdin.close()
#p.wait()
except Exception as e:
logging.warning("Failed to run apport (%s)" % e)
return False
return True
return False
def run_apport():
" run apport, check if we have a display "
if "RELEASE_UPGRADER_NO_APPORT" in os.environ:
logging.debug("RELEASE_UPGRADER_NO_APPORT env set")
return False
if "DISPLAY" in os.environ:
# update-notifier will notify about the crash
return True
elif os.path.exists("/usr/bin/apport-cli"):
try:
return (subprocess.call("/usr/bin/apport-cli") == 0)
except Exception:
logging.exception("Unable to launch '/usr/bin/apport-cli'")
return False
logging.debug("can't find apport")
return False
./DistUpgradeCache.py 0000644 0003721 0004705 00000144537 15002456363 014241 0 ustar buildd buildd # DistUpgradeCache.py
#
# Copyright (c) 2004-2008 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt
import apt_pkg
import glob
import locale
import os
import re
import logging
import time
import datetime
import threading
import configparser
from subprocess import Popen, PIPE
from .DistUpgradeGettext import gettext as _
from .DistUpgradeGettext import ngettext
from .utils import inside_chroot
class CacheException(Exception):
pass
class CacheExceptionLockingFailed(CacheException):
pass
class CacheExceptionDpkgInterrupted(CacheException):
pass
class PackageRemovalDeniedException(Exception):
def __init__(self, package):
self.package = package
msg = _(
f'The package {self.package} is marked for removal '
'but it is in the removal deny list.'
)
# If additional details can be given to the user explaining the
# situtation, provide them here.
if re.compile(r'^postgresql-[0-9]*$').match(self.package):
msg += _(
'\n\nTo prevent data loss, postgresql packages are not removed '
'automatically during the upgrade. If you are certain you '
f'no longer need {self.package}, you can manually remove it '
'and try the upgrade again.'
)
super().__init__(msg)
def estimate_kernel_initrd_size_in_boot():
"""estimate the amount of space used by the kernel and initramfs in /boot,
including a safety margin
"""
kernel = 0
initrd = 0
kver = os.uname()[2]
for f in glob.glob("/boot/*%s*" % kver):
if f == '/boot/initrd.img-%s' % kver:
initrd += os.path.getsize(f)
# don't include in the estimate any files that are left behind by
# an interrupted package manager run
elif (f.find('initrd.img') >= 0 or f.find('.bak') >= 0
or f.find('.dpkg-') >= 0):
continue
else:
kernel += os.path.getsize(f)
if kernel == 0:
logging.warning(
"estimate_kernel_initrd_size_in_boot() returned '0' for kernel?")
kernel = 16*1024*1024
if initrd == 0:
logging.warning(
"estimate_kernel_initrd_size_in_boot() returned '0' for initrd?")
initrd = 175*1024*1024
# add small safety buffer
kernel += 1*1024*1024
# safety buffer as a percentage of the existing initrd's size
initrd_buffer = 1*1024*1024
if initrd * 0.05 > initrd_buffer:
initrd_buffer = initrd * 0.05
initrd += initrd_buffer
return kernel,initrd
KERNEL_SIZE, INITRD_SIZE = estimate_kernel_initrd_size_in_boot()
class FreeSpaceRequired(object):
""" FreeSpaceRequired object:
This exposes:
- the total size required (size_total)
- the dir that requires the space (dir)
- the additional space that is needed (size_needed)
"""
def __init__(self, size_total, dir, size_needed):
self.size_total = size_total
self.dir = dir
self.size_needed = size_needed
def __str__(self):
return "FreeSpaceRequired Object: Dir: %s size_total: %s size_needed: %s" % (self.dir, self.size_total, self.size_needed)
class NotEnoughFreeSpaceError(CacheException):
"""
Exception if there is not enough free space for this operation
"""
def __init__(self, free_space_required_list):
self.free_space_required_list = free_space_required_list
class MyCache(apt.Cache):
ReInstReq = 1
HoldReInstReq = 3
# init
def __init__(self,
config,
view,
quirks,
progress=None,
lock=True,
from_dist=None,
to_dist=None):
self.to_install = []
self.to_remove = []
self.view = view
self.quirks = quirks
self.lock = False
self.partialUpgrade = False
self.config = config
self.metapkgs = self.config.getlist("Distro", "MetaPkgs")
self.from_dist = from_dist
self.to_dist = to_dist
# acquire lock
self._listsLock = -1
if lock:
try:
apt_pkg.pkgsystem_lock()
self.lock_lists_dir()
self.lock = True
except SystemError as e:
# checking for this is ok, its not translatable
if "dpkg --configure -a" in str(e):
raise CacheExceptionDpkgInterrupted(e)
raise CacheExceptionLockingFailed(e)
# Do not create the cache until we know it is not locked
apt.Cache.__init__(self, progress)
# a list of regexp that are not allowed to be removed
self.removal_denylist = config.getListFromFile("Distro", "RemovalDenylistFile")
# the linux metapackage should not be removed
self.linux_metapackage = self.quirks._get_linux_metapackage(self, False)
self.uname = Popen(["uname", "-r"], stdout=PIPE,
universal_newlines=True).communicate()[0].strip()
self._initAptLog()
apt_pkg.config.set("APT::AutoRemove::SuggestsImportant", "false")
@property
def req_reinstall_pkgs(self):
" return the packages not downloadable packages in reqreinst state "
reqreinst = set()
for pkg in self:
if ((not pkg.candidate or not pkg.candidate.downloadable)
and
(pkg._pkg.inst_state == self.ReInstReq or
pkg._pkg.inst_state == self.HoldReInstReq)):
reqreinst.add(pkg.name)
return reqreinst
def fix_req_reinst(self, view):
" check for reqreinst state and offer to fix it "
reqreinst = self.req_reinstall_pkgs
if len(reqreinst) > 0:
header = ngettext("Remove package in bad state",
"Remove packages in bad state",
len(reqreinst))
summary = ngettext("The package '%s' is in an inconsistent "
"state and needs to be reinstalled, but "
"no archive can be found for it. "
"Do you want to remove this package "
"now to continue?",
"The packages '%s' are in an inconsistent "
"state and need to be reinstalled, but "
"no archives can be found for them. Do you "
"want to remove these packages now to "
"continue?",
len(reqreinst)) % ", ".join(reqreinst)
if view.askYesNoQuestion(header, summary):
self.release_lock()
cmd = ["/usr/bin/dpkg", "--remove", "--force-remove-reinstreq"] + list(reqreinst)
view.getTerminal().call(cmd)
self.get_lock()
return True
return False
# logging stuff
def _initAptLog(self):
" init logging, create log file"
logdir = self.config.getWithDefault("Files", "LogDir",
"/var/log/dist-upgrade")
if not os.path.exists(logdir):
os.makedirs(logdir)
apt_pkg.config.set("Dir::Log", logdir)
apt_pkg.config.set("Dir::Log::Terminal", "apt-term.log")
self.logfd = os.open(os.path.join(logdir, "apt.log"),
os.O_RDWR | os.O_CREAT | os.O_APPEND, 0o644)
now = datetime.datetime.now()
header = "Log time: %s\n" % now
os.write(self.logfd, header.encode("utf-8"))
# turn on debugging in the cache
apt_pkg.config.set("Debug::pkgProblemResolver", "true")
apt_pkg.config.set("Debug::pkgDepCache::Marker", "true")
apt_pkg.config.set("Debug::pkgDepCache::AutoInstall", "true")
def _startAptResolverLog(self):
if hasattr(self, "old_stdout"):
os.close(self.old_stdout)
os.close(self.old_stderr)
self.old_stdout = os.dup(1)
self.old_stderr = os.dup(2)
os.dup2(self.logfd, 1)
os.dup2(self.logfd, 2)
def _stopAptResolverLog(self):
os.fsync(1)
os.fsync(2)
os.dup2(self.old_stdout, 1)
os.dup2(self.old_stderr, 2)
# use this decorator instead of the _start/_stop stuff directly
# FIXME: this should probably be a decorator class where all
# logging is moved into?
def withResolverLog(f):
" decorator to ensure that the apt output is logged "
def wrapper(*args, **kwargs):
args[0]._startAptResolverLog()
res = f(*args, **kwargs)
args[0]._stopAptResolverLog()
return res
return wrapper
# properties
@property
def required_download(self):
""" get the size of the packages that are required to download """
pm = apt_pkg.PackageManager(self._depcache)
fetcher = apt_pkg.Acquire()
pm.get_archives(fetcher, self._list, self._records)
return fetcher.fetch_needed
@property
def additional_required_space(self):
""" get the size of the additional required space on the fs """
return self._depcache.usr_size
@property
def additional_required_space_for_snaps(self):
""" get the extra size needed to install the snap replacements """
try:
# update-manager uses DistUpgradeCache.MyCache as the base class
# of its own MyCache version - but without actually calling our
# constructor at all. This causes that the MyCache version from
# update-manager has no self.quirks attribute while still calling
# our default version of checkFreeSpace(). Since extra_snap_space
# is only used on dist-upgrades, let's just not care and return 0
# in this weird, undocumented case.
return self.quirks.extra_snap_space
except AttributeError:
return 0
@property
def is_broken(self):
""" is the cache broken """
return self._depcache.broken_count > 0
# methods
def lock_lists_dir(self):
name = apt_pkg.config.find_dir("Dir::State::Lists") + "lock"
self._listsLock = apt_pkg.get_lock(name)
if self._listsLock < 0:
e = "Can not lock '%s' " % name
raise CacheExceptionLockingFailed(e)
def unlock_lists_dir(self):
if self._listsLock > 0:
os.close(self._listsLock)
self._listsLock = -1
def update(self, fprogress=None):
"""
our own update implementation is required because we keep the lists
dir lock
"""
self.unlock_lists_dir()
res = apt.Cache.update(self, fprogress)
self.lock_lists_dir()
if fprogress and fprogress.release_file_download_error:
# FIXME: not ideal error message, but we just reuse a
# existing one here to avoid a new string
raise IOError(_("The server may be overloaded"))
if res == False:
raise IOError("apt.cache.update() returned False, but did not raise exception?!?")
def commit(self, fprogress, iprogress):
logging.info("cache.commit()")
if self.lock:
self.release_lock()
apt.Cache.commit(self, fprogress, iprogress)
def release_lock(self):
if self.lock:
try:
apt_pkg.pkgsystem_unlock()
self.lock = False
except SystemError as e:
logging.debug("failed to SystemUnLock() (%s) " % e)
def get_lock(self):
if not self.lock:
try:
apt_pkg.pkgsystem_lock()
self.lock = True
except SystemError as e:
logging.debug("failed to SystemLock() (%s) " % e)
def downloadable(self, pkg, useCandidate=True):
" check if the given pkg can be downloaded "
if useCandidate:
ver = self._depcache.get_candidate_ver(pkg._pkg)
else:
ver = pkg._pkg.current_ver
if ver == None:
logging.warning("no version information for '%s' (useCandidate=%s)" % (pkg.name, useCandidate))
return False
return ver.downloadable
def fix_broken(self):
""" try to fix broken dependencies on the system, may throw
SystemError when it can't"""
return self._depcache.fix_broken()
def create_snapshot(self):
""" create a snapshot of the current changes """
self.to_install = []
self.to_remove = []
for pkg in self.get_changes():
if pkg.marked_install or pkg.marked_upgrade:
self.to_install.append(pkg.name)
if pkg.marked_delete:
self.to_remove.append(pkg.name)
def clear(self):
self._depcache.init()
def restore_snapshot(self):
""" restore a snapshot """
actiongroup = apt_pkg.ActionGroup(self._depcache)
# just make pyflakes shut up, later we need to use
# with self.actiongroup():
actiongroup
self.clear()
for name in self.to_remove:
pkg = self[name]
pkg.mark_delete()
for name in self.to_install:
pkg = self[name]
pkg.mark_install(auto_fix=False, auto_inst=False)
def need_server_mode(self):
"""
This checks if we run on a desktop or a server install.
A server install has more freedoms, for a desktop install
we force a desktop meta package to be install on the upgrade.
We look for a installed desktop meta pkg and for key
dependencies, if none of those are installed we assume
server mode
"""
#logging.debug("need_server_mode() run")
# check for the MetaPkgs (e.g. ubuntu-desktop)
metapkgs = self.config.getlist("Distro", "MetaPkgs")
for key in metapkgs:
# if it is installed we are done
if key in self and self[key].is_installed:
logging.debug("need_server_mode(): run in 'desktop' mode, (because of pkg '%s')" % key)
return False
# if it is not installed, but its key depends are installed
# we are done too (we auto-select the package later)
deps_found = True
for pkg in self.config.getlist(key, "KeyDependencies"):
deps_found &= pkg in self and self[pkg].is_installed
if deps_found:
logging.debug("need_server_mode(): run in 'desktop' mode, (because of key deps for '%s')" % key)
return False
logging.debug("need_server_mode(): can not find a desktop meta package or key deps, running in server mode")
return True
def coherence_check(self, view):
""" check if the cache is ok and if the required metapkgs
are installed
"""
if self.is_broken:
try:
logging.debug("Have broken pkgs, trying to fix them")
self.fix_broken()
except SystemError:
view.error(_("Broken packages"),
_("Your system contains broken packages "
"that couldn't be fixed with this "
"software. "
"Please fix them first using synaptic or "
"apt-get before proceeding."))
return False
return True
def mark_install(self, pkg, reason="", **flags):
logging.debug("Installing '%s' (%s)" % (pkg, reason))
if pkg in self:
self[pkg].mark_install(**flags)
if not (self[pkg].marked_install or self[pkg].marked_upgrade):
logging.error("Installing/upgrading '%s' failed" % pkg)
#raise SystemError("Installing '%s' failed" % pkg)
return False
return True
def mark_upgrade(self, pkg, reason=""):
logging.debug("Upgrading '%s' (%s)" % (pkg, reason))
if pkg in self and self[pkg].is_installed:
self[pkg].mark_upgrade()
if not self[pkg].marked_upgrade:
logging.error("Upgrading '%s' failed" % pkg)
return False
return True
def mark_remove(self, pkg, reason="", **flags):
logging.debug("Removing '%s' (%s)" % (pkg, reason))
if pkg in self:
self[pkg].mark_delete(**flags)
def mark_purge(self, pkg, reason=""):
logging.debug("Purging '%s' (%s)" % (pkg, reason))
if pkg in self:
self._depcache.mark_delete(self[pkg]._pkg, True)
def _keep_installed(self, pkgname, reason):
if (pkgname in self
and self[pkgname].is_installed
and self[pkgname].marked_delete):
self.mark_install(pkgname, reason)
def keep_installed_rule(self):
""" run after the dist-upgrade to ensure that certain
packages are kept installed """
# first the global list
for pkgname in self.config.getlist("Distro", "KeepInstalledPkgs"):
self._keep_installed(pkgname, "Distro KeepInstalledPkgs rule")
# the the per-metapkg rules
for key in self.metapkgs:
if key in self and (self[key].is_installed or
self[key].marked_install):
for pkgname in self.config.getlist(key, "KeepInstalledPkgs"):
self._keep_installed(pkgname, "%s KeepInstalledPkgs rule" % key)
logging.debug("Running KeepInstalledSection rules")
# now the KeepInstalledSection code
for section in self.config.getlist("Distro", "KeepInstalledSection"):
for pkg in self:
if (pkg.candidate and pkg.candidate.downloadable
and pkg.marked_delete
and pkg.candidate.section == section):
self._keep_installed(pkg.name, "Distro KeepInstalledSection rule: %s" % section)
for key in self.metapkgs:
if key in self and (self[key].is_installed or
self[key].marked_install):
for section in self.config.getlist(key, "KeepInstalledSection"):
for pkg in self:
if (pkg.candidate and pkg.candidate.downloadable
and pkg.marked_delete and
pkg.candidate.section == section):
self._keep_installed(pkg.name, "%s KeepInstalledSection rule: %s" % (key, section))
def pre_upgrade_rule(self):
" run before the upgrade was done in the cache "
# run the quirks handlers
if not self.partialUpgrade:
self.quirks.PreDistUpgradeCache()
def post_upgrade_rule(self):
" run after the upgrade was done in the cache "
for (rule, action) in [("Install", self.mark_install),
("Upgrade", self.mark_upgrade),
("Remove", self.mark_remove),
("Purge", self.mark_purge)]:
# first the global list
for pkg in self.config.getlist("Distro", "PostUpgrade%s" % rule):
action(pkg, "Distro PostUpgrade%s rule" % rule)
for key in self.metapkgs:
if key in self and (self[key].is_installed or
self[key].marked_install):
for pkg in self.config.getlist(key, "PostUpgrade%s" % rule):
action(pkg, "%s PostUpgrade%s rule" % (key, rule))
# run the quirks handlers
if not self.partialUpgrade:
self.quirks.PostDistUpgradeCache()
def checkForNvidia(self):
"""
this checks for nvidia hardware and checks what driver is needed
"""
logging.debug("nvidiaUpdate()")
# if the free drivers would give us a equally hard time, we would
# never be able to release
try:
from NvidiaDetector.nvidiadetector import NvidiaDetection
except (ImportError, SyntaxError) as e:
# SyntaxError is temporary until the port of NvidiaDetector to
# Python 3 is in the archive.
logging.error("NvidiaDetector can not be imported %s" % e)
return False
try:
# get new detection module and use the modalises files
# from within the release-upgrader
nv = NvidiaDetection(obsolete="./ubuntu-drivers-obsolete.pkgs")
#nv = NvidiaDetection()
# check if a binary driver is installed now
for oldDriver in nv.oldPackages:
if oldDriver in self and self[oldDriver].is_installed:
self.mark_remove(oldDriver, "old nvidia driver")
break
else:
logging.info("no old nvidia driver installed, installing no new")
return False
# check which one to use
driver = nv.selectDriver()
logging.debug("nv.selectDriver() returned '%s'" % driver)
if not driver in self:
logging.warning("no '%s' found" % driver)
return False
if not (self[driver].marked_install or self[driver].marked_upgrade):
self[driver].mark_install()
logging.info("installing %s as suggested by NvidiaDetector" % driver)
return True
except Exception as e:
logging.error("NvidiaDetection returned a error: %s" % e)
return False
def checkForKernel(self):
""" check for the running kernel and try to ensure that we have
an updated version
"""
logging.debug("Kernel uname: '%s' " % self.uname)
try:
(version, _, _) = self.uname.split("-")
except Exception as e:
logging.warning("Can't parse kernel uname: '%s' (self compiled?)" % e)
return False
# now check if we have a SMP system
dmesg = Popen(["dmesg"], stdout=PIPE).communicate()[0]
if b"WARNING: NR_CPUS limit" in dmesg:
logging.debug("UP kernel on SMP system!?!")
return True
def checkPriority(self):
# tuple of priorities we require to be installed
need = ('required', )
# stuff that its ok not to have
removeEssentialOk = self.config.getlist("Distro", "RemoveEssentialOk")
# check now
for pkg in self:
# WORKAROUND bug on the CD/python-apt #253255
ver = pkg._pcache._depcache.get_candidate_ver(pkg._pkg)
if ver and ver.priority == 0:
logging.error("Package %s has no priority set" % pkg.name)
continue
if (pkg.candidate and pkg.candidate.downloadable and
not (pkg.is_installed or pkg.marked_install) and
not pkg.name in removeEssentialOk and
# ignore multiarch priority required packages
not ":" in pkg.name and
pkg.candidate.priority in need):
self.mark_install(pkg.name, "priority in required set '%s' but not scheduled for install" % need)
# FIXME: make this a decorator (just like the withResolverLog())
def updateGUI(self, view, lock):
i = 0
while lock.locked():
if i % 15 == 0:
view.pulseProgress()
view.processEvents()
time.sleep(0.02)
i += 1
view.pulseProgress(finished=True)
view.processEvents()
@withResolverLog
def distUpgrade(self, view, serverMode, partialUpgrade):
# keep the GUI alive
lock = threading.Lock()
lock.acquire()
t = threading.Thread(target=self.updateGUI, args=(self.view, lock,))
t.start()
try:
# run PreDistUpgradeCache quirks
self.pre_upgrade_rule()
# install missing meta-packages (if not in server upgrade mode)
self._keepBaseMetaPkgsInstalled(view)
if not serverMode:
# if this fails, a system error is raised
self._installMetaPkgs(view)
# upgrade (and make sure this way that the cache is ok)
self.upgrade(True)
# check that everything in priority required is installed
self.checkPriority()
# see if our KeepInstalled rules are honored
self.keep_installed_rule()
# check if we got a new kernel (if we are not inside a
# chroot)
if inside_chroot():
logging.warning("skipping kernel checks because we run inside a chroot")
else:
self.checkForKernel()
# check for nvidia stuff
self.checkForNvidia()
# and if we have some special rules
self.post_upgrade_rule()
# see if it all makes sense, if not this function raises
self._verifyChanges()
if self.is_broken:
raise SystemError(_("Broken packages after upgrade: %s") % ", ".join(p.name for p in self if p.is_inst_broken or p.is_now_broken))
except (SystemError, PackageRemovalDeniedException) as e:
details = _("An unresolvable problem occurred while "
"calculating the upgrade.\n\n ")
foreign_packages = self.foreign_packages()
# we never have partialUpgrades (including removes) on a stable system
# with only ubuntu sources so we do not recommend reporting a bug
if partialUpgrade:
details += _("This is most likely a transient problem, "
"please try again later.")
elif isinstance(e, PackageRemovalDeniedException):
details += str(e) + '\n\n'
elif foreign_packages:
details += _(
"This may be caused by unofficial packages installed on "
"the system. Please try replacing the following packages "
"with official versions from the Ubuntu archive, and then "
"try the upgrade again.\n\n"
)
details += '\n'.join(foreign_packages)
else:
details += _("If none of this applies, then please report this bug using "
"the command 'ubuntu-bug ubuntu-release-upgrader-core' in a terminal. ")
details += _("If you want to investigate this yourself the log files in "
"'/var/log/dist-upgrade' will contain details about the upgrade. "
"Specifically, look at 'main.log' and 'apt.log'.")
# make the error text available again on stdout for the
# text frontend
self._stopAptResolverLog()
view.error(_("Could not calculate the upgrade"), details)
# may contain utf-8 (LP: #1310053)
error_msg = str(e)
logging.error("Dist-upgrade failed: '%s'", error_msg)
# start the resolver log again because this is run with
# the withResolverLog decorator
self._startAptResolverLog()
return False
finally:
# wait for the gui-update thread to exit
lock.release()
t.join()
# check the trust of the packages that are going to change
untrusted = []
downgrade = []
for pkg in self.get_changes():
if pkg.marked_delete:
continue
# special case because of a bug in pkg.candidate.origins
if pkg.marked_downgrade:
downgrade.append(pkg.name)
for ver in pkg._pkg.version_list:
# version is lower than installed one
if apt_pkg.version_compare(
ver.ver_str, pkg.installed.version) < 0:
for (verFileIter, index) in ver.file_list:
indexfile = pkg._pcache._list.find_index(verFileIter)
if indexfile and not indexfile.is_trusted:
untrusted.append(pkg.name)
break
continue
origins = pkg.candidate.origins
trusted = False
for origin in origins:
#print(origin)
trusted |= origin.trusted
if not trusted:
untrusted.append(pkg.name)
# check if the user overwrote the unauthenticated warning
try:
b = self.config.getboolean("Distro", "AllowUnauthenticated")
if b:
logging.warning("AllowUnauthenticated set!")
return True
except configparser.NoOptionError:
pass
if len(downgrade) > 0:
downgrade.sort()
logging.error("Packages to downgrade found: '%s'" %
" ".join(downgrade))
if len(untrusted) > 0:
untrusted.sort()
logging.error("Unauthenticated packages found: '%s'" %
" ".join(untrusted))
# FIXME: maybe ask a question here? instead of failing?
self._stopAptResolverLog()
view.error(_("Error authenticating some packages"),
_("It was not possible to authenticate some "
"packages. This may be a transient network problem. "
"You may want to try again later. See below for a "
"list of unauthenticated packages."),
"\n".join(untrusted))
# start the resolver log again because this is run with
# the withResolverLog decorator
self._startAptResolverLog()
return False
return True
def _verifyChanges(self):
""" this function tests if the current changes don't violate
our constraints (deny listed removals etc)
"""
main_arch = apt_pkg.config.find("APT::Architecture")
removeEssentialOk = self.config.getlist("Distro", "RemoveEssentialOk")
# check changes
for pkg in self.get_changes():
if pkg.marked_delete and self._inRemovalDenylist(pkg.name):
logging.debug("The package '%s' is marked for removal but it's in the removal deny list", pkg.name)
raise PackageRemovalDeniedException(pkg.name)
if pkg.marked_delete and (
pkg._pkg.essential == True and
pkg.installed.architecture in (main_arch, "all") and
not pkg.name in removeEssentialOk):
logging.debug("The package '%s' is marked for removal but it's an ESSENTIAL package", pkg.name)
raise SystemError(_("The essential package '%s' is marked for removal.") % pkg.name)
# check bad-versions deny list
badVersions = self.config.getlist("Distro", "BadVersions")
for bv in badVersions:
(pkgname, ver) = bv.split("_")
if (pkgname in self and self[pkgname].candidate and
self[pkgname].candidate.version == ver and
(self[pkgname].marked_install or
self[pkgname].marked_upgrade)):
raise SystemError(_("Trying to install deny listed version '%s'") % bv)
return True
def _lookupPkgRecord(self, pkg):
"""
helper to make sure that the pkg._records is pointing to the right
location - needed because python-apt 0.7.9 dropped the python-apt
version but we can not yet use the new version because on upgrade
the old version is still installed
"""
ver = pkg._pcache._depcache.get_candidate_ver(pkg._pkg)
if ver is None:
print("No candidate ver: ", pkg.name)
return False
if ver.file_list is None:
print("No file_list for: %s " % self._pkg.name())
return False
f, index = ver.file_list.pop(0)
pkg._pcache._records.lookup((f, index))
return True
@property
def installedTasks(self):
tasks = {}
installed_tasks = set()
for pkg in self:
if not self._lookupPkgRecord(pkg):
logging.debug("no PkgRecord found for '%s', skipping " % pkg.name)
continue
for line in pkg._pcache._records.record.split("\n"):
if line.startswith("Task:"):
for task in (line[len("Task:"):]).split(","):
task = task.strip()
if task not in tasks:
tasks[task] = set()
tasks[task].add(pkg.name)
for task in tasks:
installed = True
ignored_tasks = self.config.getlist("Distro", "IgnoredTasks")
if task in ignored_tasks:
installed = False
for pkgname in tasks[task]:
if not self[pkgname].is_installed:
installed = False
break
if installed:
installed_tasks.add(task)
return installed_tasks
def installTasks(self, tasks):
logging.debug("running installTasks")
for pkg in self:
if pkg.marked_install or pkg.is_installed:
continue
self._lookupPkgRecord(pkg)
if not (hasattr(pkg._pcache._records, "record") and pkg._pcache._records.record):
logging.warning("can not find Record for '%s'" % pkg.name)
continue
for line in pkg._pcache._records.record.split("\n"):
if line.startswith("Task:"):
for task in (line[len("Task:"):]).split(","):
task = task.strip()
if task in tasks:
pkg.mark_install()
return True
def _keepBaseMetaPkgsInstalled(self, view):
for pkg in self.config.getlist("Distro", "BaseMetaPkgs"):
self._keep_installed(pkg, "base meta package keep installed rule")
def _installMetaPkgs(self, view):
def metaPkgInstalled():
"""
internal helper that checks if at least one meta-pkg is
installed or marked install
"""
for key in metapkgs:
if key in self:
pkg = self[key]
if pkg.is_installed and pkg.marked_delete:
logging.debug("metapkg '%s' installed but marked_delete" % pkg.name)
if ((pkg.is_installed and not pkg.marked_delete)
or self[key].marked_install):
return True
return False
# now check for ubuntu-desktop, kubuntu-desktop, edubuntu-desktop
metapkgs = self.config.getlist("Distro", "MetaPkgs")
# we never go without ubuntu-base
for pkg in self.config.getlist("Distro", "BaseMetaPkgs"):
self[pkg].mark_install()
apt.ProblemResolver(self).protect(self[pkg])
# every meta-pkg that is installed currently, will be marked
# install (that result in a upgrade and removes a mark_delete)
for key in metapkgs:
try:
if (key in self and
self[key].is_installed and
self[key].is_upgradable):
logging.debug("Marking '%s' for upgrade" % key)
self[key].mark_upgrade()
apt.ProblemResolver(self).protect(self[key])
except SystemError as e:
# warn here, but don't fail, its possible that meta-packages
# conflict (like ubuntu-desktop vs xubuntu-desktop) LP: #775411
logging.warning("Can't mark '%s' for upgrade (%s)" % (key, e))
# check if we have a meta-pkg, if not, try to guess which one to pick
if not metaPkgInstalled():
logging.debug("none of the '%s' meta-pkgs installed" % metapkgs)
for key in metapkgs:
deps_found = True
for pkg in self.config.getlist(key, "KeyDependencies"):
deps_found &= pkg in self and self[pkg].is_installed
if deps_found:
logging.debug("guessing '%s' as missing meta-pkg" % key)
try:
self[key].mark_install()
except (SystemError, KeyError) as e:
logging.error("failed to mark '%s' for install (%s)" %
(key, e))
view.error(_("Can't install '%s'") % key,
_("It was impossible to install a "
"required package. Please report "
"this as a bug using "
"'ubuntu-bug ubuntu-release-upgrader-core' in "
"a terminal."))
return False
logging.debug("marked_install: '%s' -> '%s'" % (key, self[key].marked_install))
break
# check if we actually found one
if not metaPkgInstalled():
meta_pkgs = ', '.join(metapkgs[0:-1])
view.error(_("Can't guess meta-package"),
_("Your system does not contain a "
"%s or %s package and it was not "
"possible to detect which version of "
"Ubuntu you are running.\n "
"Please install one of the packages "
"above first using synaptic or "
"apt-get before proceeding.") %
(meta_pkgs, metapkgs[-1]))
return False
return True
def _inRemovalDenylist(self, pkgname):
for expr in self.removal_denylist:
if re.compile(expr).match(pkgname):
logging.debug("denylist expr '%s' matches '%s'" %
(expr, pkgname))
return True
return False
def isRemoveCandidate(self, pkgname, foreign_pkgs):
# coherence check, first see if it looks like a running kernel pkg
if pkgname in foreign_pkgs:
logging.debug("skipping foreign pkg '%s'" % pkgname)
return False
if pkgname.endswith(self.uname):
logging.debug("skipping running kernel pkg '%s'" % pkgname)
return False
if pkgname == self.linux_metapackage:
logging.debug("skipping kernel metapackage '%s'" % pkgname)
return False
if self._inRemovalDenylist(pkgname):
logging.debug("skipping '%s' (in removalDenylist)" % pkgname)
return False
# ensure we honor KeepInstalledSection here as well
for section in self.config.getlist("Distro", "KeepInstalledSection"):
if (pkgname in self and self[pkgname].installed and
self[pkgname].installed.section == section):
logging.debug("skipping '%s' (in KeepInstalledSection)" % pkgname)
return False
return True
@withResolverLog
def tryMarkObsoleteForRemoval(self, pkgname, remove_candidates, forced_obsoletes, auto_fix):
#logging.debug("tryMarkObsoleteForRemoval(): %s" % pkgname)
# if we don't have the package anyway, we are fine (this can
# happen when forced_obsoletes are specified in the config file)
if pkgname not in remove_candidates:
return False
if pkgname not in self:
#logging.debug("package '%s' not in cache" % pkgname)
return True
# check if we want to purge
try:
purge = self.config.getboolean("Distro", "PurgeObsoletes")
except configparser.NoOptionError:
purge = False
# if this package has not been forced obsolete, only
# delete it if it doesn't remove other dependents
# that are not obsolete as well
if auto_fix:
self.create_snapshot()
try:
self[pkgname].mark_delete(purge=purge, auto_fix=auto_fix)
self.view.processEvents()
if auto_fix:
if pkgname in forced_obsoletes:
return True
#logging.debug("marking '%s' for removal" % pkgname)
for pkg in self.get_changes():
if pkg.name not in remove_candidates:
logging.debug("package '%s' produces an unwanted removal '%s', skipping" % (pkgname, pkg.name))
self.restore_snapshot()
return False
except (SystemError, KeyError) as e:
logging.warning("_tryMarkObsoleteForRemoval failed for '%s' (%s: %s)" % (pkgname, repr(e), e))
if auto_fix:
self.restore_snapshot()
return False
return True
def _getObsoletesPkgs(self):
" get all package names that are not downloadable "
obsolete_pkgs = set()
for pkg in self:
if pkg.is_installed:
# check if any version is downloadable. we need to check
# for older ones too, because there might be
# cases where e.g. firefox in gutsy-updates is newer
# than hardy
if not self.anyVersionDownloadable(pkg):
obsolete_pkgs.add(pkg.name)
return obsolete_pkgs
def anyVersionDownloadable(self, pkg):
" helper that checks if any of the version of pkg is downloadable "
for ver in pkg._pkg.version_list:
if ver.downloadable:
return True
return False
def _getUnusedDependencies(self):
" get all package names that are not downloadable "
unused_dependencies = set()
for pkg in self:
if pkg.is_installed and self._depcache.is_garbage(pkg._pkg):
unused_dependencies.add(pkg.name)
return unused_dependencies
def foreign_packages(self):
'''
Check for packages that do not have 'Ubuntu' as their origin.
Return this as a dict with -> .
If an installed package has a candidate that is not downloadable,
but *some* version of the package is downloadable, consider that
package foreign. In such cases, the package will have a value of None
in the returned dict.
'''
foreign = dict()
for pkg in self:
if not (pkg.is_installed and pkg.candidate):
continue
if self.downloadable(pkg):
for (pkg_file, _) in pkg.candidate._cand.file_list:
# Only consider configured apt sources
if pkg_file.not_source:
continue
if not (
pkg_file.origin == 'Ubuntu' and
(
pkg_file.archive.startswith(self.from_dist) or
pkg_file.archive.startswith(self.to_dist)
)
):
# This is a foreign package
foreign[pkg.name] = pkg_file
break
elif self.anyVersionDownloadable(pkg):
# This can happen, for example, if a package is installed from
# a PPA, and that version is newer than what is in the archive.
foreign[pkg.name] = None
return foreign
def checkFreeSpace(self, snapshots_in_use=False):
"""
this checks if we have enough free space on /var, /boot and /usr
with the given cache
Note: this can not be fully accurate if there are multiple
mountpoints for /usr, /var, /boot
"""
class FreeSpace(object):
" helper class that represents the free space on each mounted fs "
def __init__(self, initialFree):
self.free = initialFree
self.need = 0
def make_fs_id(d):
""" return 'id' of a directory so that directories on the
same filesystem get the same id (simply the mount_point)
"""
for mount_point in mounted:
if d.startswith(mount_point):
return mount_point
return "/"
# this is all a bit complicated
# 1) check what is mounted (in mounted)
# 2) create FreeSpace objects for the dirs we are interested in
# (mnt_map)
# 3) use the mnt_map to check if we have enough free space and
# if not tell the user how much is missing
mounted = []
mnt_map = {}
fs_free = {}
with open("/proc/mounts") as mounts:
for line in mounts:
try:
(_, where, _, options, a, b) = line.split()
except ValueError as e:
logging.debug("line '%s' in /proc/mounts not understood (%s)" % (line, e))
continue
if not where in mounted:
mounted.append(where)
# make sure mounted is sorted by longest path
mounted.sort(key=len, reverse=True)
archivedir = apt_pkg.config.find_dir("Dir::Cache::archives")
for d in ["/", "/usr", "/var", "/boot", archivedir, "/home", "/tmp/"]:
d = os.path.realpath(d)
fs_id = make_fs_id(d)
if os.path.exists(d):
st = os.statvfs(d)
free = st.f_bavail * st.f_frsize
else:
logging.warning("directory '%s' does not exists" % d)
free = 0
if fs_id in mnt_map:
logging.debug("Dir %s mounted on %s" %
(d, mnt_map[fs_id]))
fs_free[d] = fs_free[mnt_map[fs_id]]
else:
logging.debug("Free space on %s: %s" %
(d, free))
mnt_map[fs_id] = d
fs_free[d] = FreeSpace(free)
del mnt_map
logging.debug("fs_free contains: '%s'" % fs_free)
# now calculate the space that is required on /boot
# we do this by checking how many linux-image-$ver packages
# are installed or going to be installed
kernel_count = 0
for pkg in self:
# we match against everything that looks like a kernel
# and add space check to filter out metapackages
if re.match("^linux-(image|image-debug)-[0-9.]*-.*", pkg.name):
# upgrade because early in the release cycle the major version
# may be the same or they might be -lts- kernels
if pkg.marked_install or pkg.marked_upgrade:
logging.debug("%s (new-install) added with %s to boot space" % (pkg.name, KERNEL_SIZE))
kernel_count += 1
# space calculated per LP: #1646222
space_in_boot = (kernel_count * KERNEL_SIZE
+ (kernel_count + 1) * INITRD_SIZE)
# we check for various sizes:
# archivedir is where we download the debs
# /usr is assumed to get *all* of the install space (incorrect,
# but as good as we can do currently + safety buffer
# / has a small safety buffer as well
# add old size of the package if we use snapshots
required_for_snapshots = 0.0
if snapshots_in_use:
for pkg in self:
if (pkg.is_installed and
(pkg.marked_upgrade or pkg.marked_delete)):
required_for_snapshots += pkg.installed.installed_size
logging.debug("additional space for the snapshots: %s" % required_for_snapshots)
# sum up space requirements
for (dir, size) in [(archivedir, self.required_download),
("/usr", self.additional_required_space),
# this is only >0 for the deb-to-snap quirks
("/var", self.additional_required_space_for_snaps),
# plus 50M safety buffer in /usr
("/usr", 50*1024*1024),
("/boot", space_in_boot),
("/tmp", 5*1024*1024), # /tmp for dkms LP: #427035
("/", 10*1024*1024), # small safety buffer /
# if snapshots are in use
("/usr", required_for_snapshots),
]:
# we are ensuring we have more than enough free space not less
if size < 0:
continue
dir = os.path.realpath(dir)
logging.debug("dir '%s' needs '%s' of '%s' (%f)" % (dir, size, fs_free[dir], fs_free[dir].free))
fs_free[dir].free -= size
fs_free[dir].need += size
# check for space required violations
required_list = {}
for dir in fs_free:
if fs_free[dir].free < 0:
# ensure unicode here (LP: #1172740)
free_at_least = apt_pkg.size_to_str(float(abs(fs_free[dir].free)+1))
if isinstance(free_at_least, bytes):
free_at_least = free_at_least.decode(
locale.getpreferredencoding())
free_needed = apt_pkg.size_to_str(fs_free[dir].need)
if isinstance(free_needed, bytes):
free_needed = free_needed.decode(
locale.getpreferredencoding())
# make_fs_id ensures we only get stuff on the same
# mountpoint, so we report the requirements only once
# per mountpoint
required_list[make_fs_id(dir)] = FreeSpaceRequired(free_needed, make_fs_id(dir), free_at_least)
# raise exception if free space check fails
if len(required_list) > 0:
logging.error("Not enough free space: %s" % [str(i) for i in required_list])
raise NotEnoughFreeSpaceError(list(required_list.values()))
return True
./DistUpgradeConfigParser.py 0000644 0003721 0004705 00000006523 15000447260 015602 0 ustar buildd buildd # DistUpgradeConfigParser.py
#
# Copyright (c) 2004-2014 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
from configparser import NoOptionError, NoSectionError
from configparser import ConfigParser as SafeConfigParser
import os.path
import logging
import glob
import platform
CONFIG_OVERRIDE_DIR = "/etc/update-manager/release-upgrades.d"
class DistUpgradeConfig(SafeConfigParser):
def __init__(self, datadir, name="DistUpgrade.cfg",
override_dir=None, defaults_dir=None):
SafeConfigParser.__init__(self)
# we support a config overwrite, if DistUpgrade.cfg.dapper exists
# and the user runs dapper, that one will be used
from_release = platform.freedesktop_os_release().get(
'VERSION_CODENAME'
)
self.datadir = datadir
maincfg = os.path.join(datadir, name)
if os.path.exists(maincfg + "." + from_release):
maincfg += "." + from_release
# defaults are read first
self.config_files = []
if defaults_dir:
for cfg in glob.glob(defaults_dir + "/*.cfg"):
self.config_files.append(cfg)
# our config file
self.config_files += [maincfg]
# overrides are read later
if override_dir is None:
override_dir = CONFIG_OVERRIDE_DIR
if override_dir is not None:
for cfg in glob.glob(override_dir + "/*.cfg"):
self.config_files.append(cfg)
self.read(self.config_files)
def getWithDefault(self, section, option, default):
try:
if isinstance(default, bool):
return self.getboolean(section, option)
elif isinstance(default, float):
return self.getfloat(section, option)
elif isinstance(default, int):
return self.getint(section, option)
return self.get(section, option)
except (NoSectionError, NoOptionError):
return default
def getlist(self, section, option):
try:
tmp = self.get(section, option)
except (NoSectionError, NoOptionError):
return []
items = [x.strip() for x in tmp.split(",")]
return items
def getListFromFile(self, section, option):
try:
filename = self.get(section, option)
except NoOptionError:
return []
p = os.path.join(self.datadir, filename)
if not os.path.exists(p):
logging.error("getListFromFile: no '%s' found" % p)
with open(p) as f:
items = [x.strip() for x in f]
return [s for s in items if not s.startswith("#") and not s == ""]
./DistUpgradeController.py 0000644 0003721 0004705 00000263352 15002457041 015350 0 ustar buildd buildd # DistUpgradeController.py
#
# Copyright (c) 2004-2022 Canonical Ltd.
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt
import apt_pkg
import base64
import distro_info
import glob
import sys
import os
import subprocess
import locale
import logging
import tempfile
import time
import copy
import platform
import pwd
import errno
from configparser import NoOptionError
from configparser import ConfigParser as SafeConfigParser
from .telemetry import get as get_telemetry
from .utils import (url_downloadable,
check_and_fix_xbit,
iptables_active,
inside_chroot,
get_string_with_no_auth_from_source_entry,
is_child_of_process_name,
inhibit_sleep)
from urllib.parse import urlsplit
from .DistUpgradeView import Step
from .DistUpgradeCache import MyCache
from .DistUpgradeConfigParser import DistUpgradeConfig
from .DistUpgradeQuirks import DistUpgradeQuirks
# workaround broken relative import in python-apt (LP: #871007), we
# want the local version of distinfo.py from oneiric, but because of
# a bug in python-apt we will get the natty version that does not
# know about "Component.parent_component" leading to a crash
from aptsources import distinfo
from aptsources import sourceslist
sourceslist.DistInfo = distinfo.DistInfo
from aptsources.sourceslist import (SourcesList,
SourceEntry,
Deb822SourceEntry,
is_mirror)
from .DistUpgradeGettext import gettext as _
from .DistUpgradeGettext import ngettext
import gettext
from .DistUpgradeCache import (CacheExceptionDpkgInterrupted,
CacheExceptionLockingFailed,
NotEnoughFreeSpaceError)
from .DistUpgradeApport import run_apport
REBOOT_REQUIRED_FILE = "/var/run/reboot-required"
def component_ordering_key(a):
""" key() function for sorted to ensure "correct" component ordering """
ordering = ["main", "restricted", "universe", "multiverse"]
try:
return ordering.index(a)
except ValueError:
# ensure to sort behind the "official" components, order is not
# really important for those
return len(ordering)+1
def suite_ordering_key(a):
""" key() function for sorted to ensure "correct" suite ordering """
ordering = ["", "updates", "security", "backports", "proposed"]
pocket = a.partition("-")[2]
try:
return ordering.index(pocket)
except ValueError:
return len(ordering)+1
def gpg_keyring_to_ascii(keyring_path):
out = []
out.append('-----BEGIN PGP PUBLIC KEY BLOCK-----')
out.append('')
with open(keyring_path, 'rb') as f:
data = base64.b64encode(f.read())
out += [data[i:i+64].decode('us-ascii') for i in range(0, len(data), 64)]
out.append('-----END PGP PUBLIC KEY BLOCK-----')
return out
class DistUpgradeController(object):
""" this is the controller that does most of the work """
def __init__(self, distUpgradeView, options=None, datadir=None):
# setup the paths
localedir = "/usr/share/locale/"
if datadir == None or datadir == '.':
datadir = os.getcwd()
localedir = os.path.join(datadir,"mo")
self.datadir = datadir
self.options = options
try:
self._invoking_user_pwd = pwd.getpwuid(
int(os.getenv('SUDO_UID', os.getenv('PKEXEC_UID')))
)
except TypeError:
logging.debug(
'Failed to determine invoking user, '
'environment variables may be incomplete.'
)
self._invoking_user_pwd = None
# init gettext
gettext.bindtextdomain("ubuntu-release-upgrader",localedir)
gettext.textdomain("ubuntu-release-upgrader")
# setup the view
logging.debug("Using '%s' view" % distUpgradeView.__class__.__name__)
self._view = distUpgradeView
self._view.updateStatus(_("Reading cache"))
self.cache = None
self.fetcher = None
# the configuration
self.config = DistUpgradeConfig(datadir)
self.sources_backup_ext = "."+self.config.get("Files","BackupExt")
# move some of the options stuff into the self.config,
# ConfigParser deals only with strings it seems *sigh*
self.config.add_section("Options")
self.config.set("Options","devRelease", "False")
if self.options:
if self.options.devel_release:
self.config.set("Options","devRelease", "True")
# Add context about the versions we are upgrading from/to.
os_release = platform.freedesktop_os_release()
self.fromDist = os_release.get('VERSION_CODENAME')
self.fromVersion = os_release.get('VERSION_ID')
di = distro_info.UbuntuDistroInfo()
self.toDist = self.config.get('Sources', 'To')
self.toVersion = di.version(self.toDist)
self.isFromDistEOL = self.fromDist not in di.supported()
self.supportedFromDists = self.config.getlist('Sources', 'From')
# Defaults for deb sources
self.default_sources_filepath = os.path.join(
apt_pkg.config.find_dir("Dir::Etc::sourceparts"),
"ubuntu.sources"
)
self.arch = apt_pkg.config.find("APT::Architecture")
if self.arch in ("amd64", "i386"):
self.default_source_uri = "http://archive.ubuntu.com/ubuntu"
self.security_source_uri = "http://security.ubuntu.com/ubuntu"
else:
self.default_source_uri = "http://ports.ubuntu.com/ubuntu-ports"
self.security_source_uri = "http://ports.ubuntu.com/ubuntu-ports"
# we run with --force-overwrite by default
if "RELEASE_UPGRADE_NO_FORCE_OVERWRITE" not in os.environ:
logging.debug("enable dpkg --force-overwrite")
apt_pkg.config.set("DPkg::Options::","--force-overwrite")
# we run in full upgrade mode by default
self._partialUpgrade = False
# install the quirks handler
self.quirks = DistUpgradeQuirks(self, self.config)
# install a logind sleep inhibitor
self.inhibitor_fd = inhibit_sleep()
# setup env var
os.environ["RELEASE_UPGRADE_IN_PROGRESS"] = "1"
os.environ["PYCENTRAL_FORCE_OVERWRITE"] = "1"
# set max retries
maxRetries = self.config.getint("Network","MaxRetries")
apt_pkg.config.set("Acquire::Retries", str(maxRetries))
# max sizes for dpkgpm for large installs (see linux/limits.h and
# linux/binfmts.h)
apt_pkg.config.set("Dpkg::MaxArgs", str(64*1024))
apt_pkg.config.set("Dpkg::MaxArgBytes", str(128*1024))
# smaller to avoid hangs
apt_pkg.config.set("Acquire::http::Timeout","20")
apt_pkg.config.set("Acquire::ftp::Timeout","20")
# no list cleanup here otherwise a "cancel" in the upgrade
# will not restore the full state (lists will be missing)
apt_pkg.config.set("Apt::Get::List-Cleanup", "false")
# install phased updates during upgrades
apt_pkg.config.set("APT::Get::Always-Include-Phased-Updates", "yes")
# forced obsoletes
self.forced_obsoletes = self.config.getlist("Distro","ForcedObsoletes")
# list of valid mirrors that we can add
self.valid_mirrors = self.config.getListFromFile("Sources","ValidMirrors")
# third party mirrors
self.valid_3p_mirrors = []
if self.config.has_section('ThirdPartyMirrors'):
self.valid_3p_mirrors = [pair[1] for pair in
self.config.items('ThirdPartyMirrors')]
# for inhibiting idle
self._session_bus = None
# Sources files that we generated during deb822 migration. Used restore
# to original state on abort.
self._generated_sources_files = set()
# Keep track of foreign packages before and after re-writing sources.
# This helps track down upgrade calculation errors related to PPAs etc.
self._foreign_packages_pre_rewrite = dict()
self._foreign_packages_post_rewrite = dict()
def openCache(self, lock=True, restore_sources_list_on_fail=False):
logging.debug("openCache()")
if self.cache is None:
self.quirks.PreCacheOpen()
else:
self.cache.release_lock()
self.cache.unlock_lists_dir()
# this loop will try getting the lock a couple of times
MAX_LOCK_RETRIES = 20
lock_retry = 0
while True:
try:
# exit here once the cache is ready
return self._openCache(lock)
except CacheExceptionLockingFailed as e:
# wait a bit
lock_retry += 1
self._view.processEvents()
time.sleep(0.1)
logging.debug(
"failed to lock the cache, retrying (%i)" % lock_retry)
# and give up after some time
if lock_retry > MAX_LOCK_RETRIES:
logging.error("Cache can not be locked (%s)" % e)
self._view.error(_("Unable to get exclusive lock"),
_("This usually means that another "
"package management application "
"(like apt-get or aptitude) "
"already running. Please close that "
"application first."));
if restore_sources_list_on_fail:
self.abort()
else:
sys.exit(1)
def _openCache(self, lock):
try:
self.cache = MyCache(self.config,
self._view,
self.quirks,
self._view.getOpCacheProgress(),
lock,
from_dist=self.fromDist,
to_dist=self.toDist)
# alias name for the plugin interface code
self.apt_cache = self.cache
# if we get a dpkg error that it was interrupted, just
# run dpkg --configure -a
except CacheExceptionDpkgInterrupted:
logging.warning("dpkg interrupted, calling dpkg --configure -a")
cmd = ["/usr/bin/dpkg","--configure","-a"]
if os.environ.get("DEBIAN_FRONTEND") == "noninteractive":
cmd.append("--force-confold")
self._view.getTerminal().call(cmd)
self.cache = MyCache(self.config,
self._view,
self.quirks,
self._view.getOpCacheProgress(),
from_dist=self.fromDist,
to_dist=self.toDist)
self.cache.partialUpgrade = self._partialUpgrade
logging.debug("/openCache(), new cache size %i" % len(self.cache))
def _viewSupportsSSH(self):
"""
Returns True if this view support upgrades over ssh.
In theory all views should support it, but for savety
we do only allow text ssh upgrades (see LP: #322482)
"""
supported = self.config.getlist("View","SupportSSH")
if self._view.__class__.__name__ in supported:
return True
return False
def _sshMagic(self):
""" this will check for server mode and if we run over ssh.
if this is the case, we will ask and spawn a additional
daemon (to be sure we have a spare one around in case
of trouble)
"""
pidfile = os.path.join("/var/run/release-upgrader-sshd.pid")
if (not os.path.exists(pidfile) and
os.path.isdir("/proc") and
is_child_of_process_name("sshd")):
# check if the frontend supports ssh upgrades (see lp: #322482)
if not self._viewSupportsSSH():
logging.error("upgrade over ssh not allowed")
self._view.error(_("Upgrading over remote connection not supported"),
_("You are running the upgrade over a "
"remote ssh connection with a frontend "
"that does "
"not support this. Please try a text "
"mode upgrade with 'do-release-upgrade'."
"\n\n"
"The upgrade will "
"abort now. Please try without ssh.")
)
sys.exit(1)
return False
# ask for a spare one to start (and below 1024)
port = 1022
res = self._view.askYesNoQuestion(
_("Continue running under SSH?"),
_("This session appears to be running under ssh. "
"It is not recommended to perform a upgrade "
"over ssh currently because in case of failure "
"it is harder to recover.\n\n"
"If you continue, an additional ssh daemon will be "
"started at port '%s'.\n"
"Do you want to continue?") % port)
# abort
if res == False:
sys.exit(1)
res = subprocess.call(["/usr/sbin/sshd",
"-o", "PidFile=%s" % pidfile,
"-p",str(port)])
if res == 0:
summary = _("Starting additional sshd")
descr = _("To make recovery in case of failure easier, an "
"additional sshd will be started on port '%s'. "
"If anything goes wrong with the running ssh "
"you can still connect to the additional one.\n"
) % port
if iptables_active():
cmd = "iptables -I INPUT -p tcp --dport %s -j ACCEPT" % port
descr += _(
"If you run a firewall, you may need to "
"temporarily open this port. As this is "
"potentially dangerous it's not done automatically. "
"You can open the port with e.g.:\n'%s'") % cmd
self._view.information(summary, descr)
return True
def _pythonSymlinkCheck(self):
""" check that /usr/bin/python3 points to the default python version.
Users tend to modify this symlink, which breaks stuff in obscure
ways (Ubuntu #75557).
"""
logging.debug("_pythonSymlinkCheck run")
binaries_and_dirnames = [("python3", "python3")]
for binary, dirname in binaries_and_dirnames:
debian_defaults = '/usr/share/%s/debian_defaults' % dirname
if os.path.exists(debian_defaults):
config = SafeConfigParser()
with open(debian_defaults) as f:
config.read_file(f)
try:
expected_default = config.get('DEFAULT', 'default-version')
except NoOptionError:
logging.debug("no default version for %s found in '%s'" %
(binary, config))
return False
try:
fs_default_version = os.path.realpath('/usr/bin/%s' % binary)
except OSError as e:
logging.error("os.path.realpath failed (%s)" % e)
return False
if not fs_default_version in (expected_default, os.path.join('/usr/bin', expected_default)):
logging.debug("%s symlink points to: '%s', but expected is '%s' or '%s'" %
(binary, fs_default_version, expected_default, os.path.join('/usr/bin', expected_default)))
return False
return True
def prepare(self):
""" initial cache opening, coherence checking, network checking """
# first check if that is a good upgrade
if not (self.fromDist in self.supportedFromDists or self.fromDist == self.toDist):
self._view.error(_("Can not upgrade"),
_("An upgrade from '%s' to '%s' is not "
"supported with this tool." % (self.fromDist, self.toDist)))
sys.exit(1)
logging.debug(
'Upgrading from {} to {}'.format(
self.fromDist + (' (EOL)' if self.isFromDistEOL else ''),
self.toDist,
)
)
# do the ssh check and warn if we run under ssh
self._sshMagic()
# check python version
if not self._pythonSymlinkCheck():
logging.error("pythonSymlinkCheck() failed, aborting")
self._view.error(_("Can not upgrade"),
_("Your python3 install is corrupted. "
"Please fix the '/usr/bin/python3' symlink."))
sys.exit(1)
# open cache
try:
self.openCache()
except SystemError as e:
logging.error("openCache() failed: '%s'" % e)
return False
if not self.cache.coherence_check(self._view):
return False
# now figure out if we need to go into desktop or
# server mode - we use a heuristic for this
self.serverMode = self.cache.need_server_mode()
if self.serverMode:
os.environ["RELEASE_UPGRADE_MODE"] = "server"
else:
os.environ["RELEASE_UPGRADE_MODE"] = "desktop"
if not self.checkViewDepends():
logging.error("checkViewDepends() failed")
return False
from .DistUpgradeMain import SYSTEM_DIRS
for systemdir in SYSTEM_DIRS:
if os.path.exists(systemdir) and not os.access(systemdir, os.W_OK):
logging.error("%s not writable" % systemdir)
self._view.error(
_("Can not write to '%s'") % systemdir,
_("Its not possible to write to the system directory "
"'%s' on your system. The upgrade can not "
"continue.\n"
"Please make sure that the system directory is "
"writable.") % systemdir)
self.abort()
return True
def _deb822SourceEntryDownloadable(self, entry):
"""
Check if deb822 source points to downloadable archive(s).
Returns a tuple (bool, list).
The bool is True if any combination of URI and suite was downloadable,
or False if no combination was.
The list contains tuples of URI and suite that were not downloadable
together.
"""
logging.debug("verifySourcesListEntry: %s" % entry)
failed = []
downloadable = False
for uri in entry.uris:
for suite in entry.suites:
release_file = "{}/dists/{}/Release".format(uri, suite)
if url_downloadable(release_file, logging.debug):
downloadable = True
else:
failed.append((uri,suite))
return (downloadable, failed)
def migratedToDeb822(self):
"""
Return an integer indicating if sources are migrated to deb822.
Possible return values are:
-1: not migrated to deb822 sources
0: partially migrated to deb822 sources
1: fully migrated to deb822 sources
"""
sources = SourcesList(matcherPath=self.datadir, deb822=True)
deb822 = [s for s in sources if isinstance(s, Deb822SourceEntry)]
nondeb822 = [s for s in sources if not isinstance(s, Deb822SourceEntry)]
# On migration, we leave behind an empty (i.e. invalid)
# /etc/apt/sources.list to explain the migration. Ignore this file.
sourcelist_file = os.path.join(
apt_pkg.config.find_dir("Dir::Etc"),
apt_pkg.config.find("Dir::Etc::sourcelist")
)
nondeb822 = [s for s in nondeb822 \
if not (s.file == sourcelist_file and s.invalid)]
if deb822 and not nondeb822:
# Fully migrated to deb822 sources.
return 1
elif deb822 and nondeb822:
# Partially migrated. A mix of .list and .sources are configured.
return 0
else:
# Either no deb822 sources, or no sources at all.
return -1
def migrateToDeb822Sources(self):
"""
Migrate .list files to corresponding .sources files.
"""
logging.debug("migrateToDeb822Sources()")
sourcelist_file = os.path.join(
apt_pkg.config.find_dir("Dir::Etc"),
apt_pkg.config.find("Dir::Etc::sourcelist")
)
sourceparts_dir = apt_pkg.config.find_dir('Dir::Etc::sourceparts')
trustedparts_dir = apt_pkg.config.find_dir('Dir::Etc::trustedparts')
migrated_sources_list = False
self.sources = SourcesList(matcherPath=self.datadir)
self.sources.backup('.migrate')
index = {}
for entry in self.sources:
if not isinstance(entry, SourceEntry) or entry.invalid:
continue
# Remove disabled deb-src entries, because stylistically it makes
# more sense to add/remove deb-src in the Types: field, rather than
# having a deb-src entry with Enabled: no.
if entry.type == 'deb-src' and entry.disabled:
continue
# Figure out where this new entry is going.
if entry.file == sourcelist_file:
migrated_sources_list = True
if self.isMirror(entry.uri):
# sources.list -> sources.list.d/ubuntu.sources
new_filepath = os.path.join(sourceparts_dir, 'ubuntu.sources')
else:
# sources.list -> sources.list.d/third-party.sources
new_filepath = os.path.join(sourceparts_dir, 'third-party.sources')
else:
# sources.list.d/foo.list -> sources.list.d/foo.sources
new_filepath = os.path.splitext(entry.file)[0] + '.sources'
# Start by making the existing sources as "flat" as possible. Later
# we can consolidate by suite and type if possible.
key = (new_filepath, entry.disabled, entry.type, entry.uri, entry.dist)
try:
e = index[key]
e['comps'] = list(set(e['comps'] + entry.comps))
e['comps'].sort(key=component_ordering_key)
except KeyError:
e = {}
e['filepath'] = new_filepath
e['disabled'] = entry.disabled
e['types'] = [entry.type]
e['uris'] = [entry.uri]
e['suites'] = [entry.dist]
e['comps'] = list(set(entry.comps))
e['comps'].sort(key=component_ordering_key)
index[key] = e
for suite in [k[-1] for k in index.keys()]:
for k in [k for k in index.keys() if k[-1] != suite]:
try:
e = index[(*k[:-1], suite)]
if e['comps'] == index[k]['comps']:
e['suites'] += index[k]['suites']
e['suites'].sort(key=suite_ordering_key)
del index[k]
except KeyError:
continue
for (ks, se) in [(k,e) for (k,e) in index.items() if k[2] == 'deb-src']:
for (kb, be) in [(k,e) for (k,e) in index.items() if k[2] == 'deb']:
can_combine = True
can_combine &= se['filepath'] == be['filepath']
can_combine &= se['disabled'] == be['disabled']
can_combine &= se['uris'] == be['uris']
can_combine &= se['suites'] == be['suites']
can_combine &= se['comps'] == be['comps']
if can_combine:
be['types'] = ['deb', 'deb-src']
del index[ks]
# Consolidate GPG keys from trusted.gpg.d into their respective .sources files.
for entry in index.values():
filepath = entry['filepath']
if filepath == os.path.join(sourceparts_dir, 'ubuntu.sources'):
entry['signed-by'] = ' /usr/share/keyrings/ubuntu-archive-keyring.gpg'
else:
# Check if there is a ppa.gpg corresponding to ppa.list.
keyring = os.path.basename(os.path.splitext(filepath)[0])
keyring = os.path.join(trustedparts_dir, keyring + '.gpg')
if not os.path.exists(keyring):
# apt-add-repository names the list files as $user-ubuntu-$ppa-$release.list,
# but the .gpg files are named $user-ubuntu-$ppa.gpg.
keyring = os.path.basename(os.path.splitext(filepath)[0])
keyring = keyring.rsplit('-', 1)[0] + '.gpg'
keyring = os.path.join(trustedparts_dir, keyring)
if os.path.exists(keyring) and not entry.get('signed-by'):
lines = gpg_keyring_to_ascii(keyring)
lines = [' ' + (l if l.strip() else '.') for l in lines]
entry['signed-by'] = '\n' + '\n'.join(lines)
# Generate the new .sources files. We write the files manually rather
# than using python-apt because the currently loaded version of
# aptsources.sourceslist might not have Deb822SourceEntry yet.
for path in set([e['filepath'] for e in index.values()]):
stanzas = []
for e in [e for e in index.values() if e['filepath'] == path]:
stanza = ''
if e['disabled']:
stanza += 'Enabled: no\n'
stanza += 'Types: {}\n'.format(' '.join(e['types']))
stanza += 'URIs: {}\n'.format(' '.join(e['uris']))
stanza += 'Suites: {}\n'.format(' '.join(e['suites']))
stanza += 'Components: {}\n'.format(' '.join(e['comps']))
if e.get('signed-by'):
stanza += 'Signed-By:{}\n'.format(e['signed-by'])
stanzas.append(stanza)
with open(path, 'w') as f:
f.write('\n'.join(stanzas))
self._generated_sources_files.add(path)
# Remove the old .list files.
for entry in [e for e in self.sources if isinstance(e, SourceEntry)]:
if entry.file == sourcelist_file and not migrated_sources_list:
# If we didn't migrate sources.list, then it's because it's not
# valid. Probably because it already contains just a comment
# about the move to ubuntu.sources. Leave it alone.
continue
if os.path.exists(entry.file):
os.remove(entry.file)
self.sources.remove(entry)
self.sources.save()
if migrated_sources_list:
# Finally, leave a comment in the old sources.list file explaining
# the migration.
with open(sourcelist_file, 'w') as f:
f.write('# Ubuntu sources have moved to {}\n'
.format(os.path.join(sourceparts_dir, 'ubuntu.sources')))
def restoreMigratedSources(self):
if not self._generated_sources_files:
return
sourcelist_file = os.path.join(
apt_pkg.config.find_dir("Dir::Etc"),
apt_pkg.config.find("Dir::Etc::sourcelist")
)
sourceparts_dir = apt_pkg.config.find_dir('Dir::Etc::sourceparts')
for path in glob.glob(f'{sourceparts_dir}/*.migrate'):
try:
os.rename(path, path.removesuffix('.migrate'))
except OSError as e:
logging.debug(f'Failed to restore {path}: {e}')
try:
os.rename(
f'{sourcelist_file}.migrate',
sourcelist_file
)
except FileNotFoundError:
pass
except OSError as e:
logging.debug(f'Failed to restore {sourcelist_file}: {e}')
for path in self._generated_sources_files:
try:
os.remove(path)
except OSError as e:
logging.debug(f'Failed to remove {path}: {e}')
def cleanBackupSources(self):
sourcelist_file = os.path.join(
apt_pkg.config.find_dir("Dir::Etc"),
apt_pkg.config.find("Dir::Etc::sourcelist")
)
sourceparts_dir = apt_pkg.config.find_dir('Dir::Etc::sourceparts')
paths = [
f'{sourcelist_file}{self.sources_backup_ext}',
f'{sourcelist_file}.migrate',
]
paths += glob.glob(f'{sourceparts_dir}/*{self.sources_backup_ext}')
paths += glob.glob(f'{sourceparts_dir}/*.migrate')
for path in paths:
try:
os.remove(path)
except OSError:
pass
def _addDefaultSources(self):
e = self.sources.add(
file=self.default_sources_filepath,
type='deb',
uri=self.default_source_uri,
dist=self.toDist,
orig_comps=['main', 'restricted', 'universe', 'multiverse']
)
e.suites = sorted([self.toDist, self.toDist + '-updates'],
key=suite_ordering_key)
e.section['Signed-By'] = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'
def _addSecuritySources(self):
e = self.sources.add(
file=self.default_sources_filepath,
type='deb',
uri=self.security_source_uri,
dist=self.toDist + '-security',
orig_comps=['main', 'restricted', 'universe', 'multiverse']
)
e.section['Signed-By'] = '/usr/share/keyrings/ubuntu-archive-keyring.gpg'
def _allowThirdParty(self):
return any((
self.config.getWithDefault("Sources","AllowThirdParty",False),
"RELEASE_UPGRADER_ALLOW_THIRD_PARTY" in os.environ,
))
def _mirrorCheck(self):
# skip mirror check if special environment is set
# (useful for server admins with internal repos)
if self._allowThirdParty():
logging.warning("mirror check skipped, *overriden* via config")
return True
# check if we need to enable main
# now check if the base-meta pkgs are available in
# the archive or only available as "now"
# -> if not that means that "main" is missing and we
# need to enable it
logging.debug(self.config.getlist("Distro", "BaseMetaPkgs"))
for pkgname in self.config.getlist("Distro", "BaseMetaPkgs"):
logging.debug("Checking pkg: %s" % pkgname)
if ((not pkgname in self.cache or
not self.cache[pkgname].candidate or
len(self.cache[pkgname].candidate.origins) == 0)
or
(self.cache[pkgname].candidate and
len(self.cache[pkgname].candidate.origins) == 1 and
self.cache[pkgname].candidate.origins[0].archive == "now")
):
logging.debug("BaseMetaPkg '%s' has no candidate.origins" % pkgname)
return False
return True
def rewriteDeb822Sources(self):
"""
deb822-aware version of rewriteSourcesList()
Return True if we found a valid dist to ugprade to, and return False
otherwise.
"""
found_components = {}
disabled_unknown_mirror = []
disabled_no_release_file = []
disabled_unknown_dist = []
# Map suites from current release to next release.
suite_mapping = {self.fromDist: self.toDist}
for pocket in self.config.getlist("Sources", "Pockets"):
f = '{}-{}'.format(self.fromDist, pocket)
t = '{}-{}'.format(self.toDist, pocket)
suite_mapping[f] = t
sources = [
e for e in self.sources
if not any((
e.invalid,
e.disabled,
not isinstance(e, Deb822SourceEntry),
))
]
for entry in sources:
# Disable -proposed when upgrading to -devel release.
if self.options and self.options.devel_release:
logging.debug("upgrade to development release, disabling proposed")
no_proposed = set(entry.suites) - set([self.fromDist + "-proposed"])
if not no_proposed:
# -proposed is the only pocket for this source, so just
# disable it.
entry.disabled = True
continue
else:
# If there are other suites, just remove -proposed.
entry.suites = no_proposed
# Remove/replace old-releases.ubuntu.com sources as needed.
entry.uris = set([u for u in entry.uris \
if "old-releases.ubuntu.com/" not in u])
if not entry.uris:
if [s for s in entry.suites if s.endswith("-security")]:
entry.uris = [self.security_source_uri]
else:
entry.uris = [self.default_source_uri]
logging.debug("examining: '%s'" %
get_string_with_no_auth_from_source_entry(copy.deepcopy(entry)))
# Disable sources that do not contain valid mirrors.
known_mirrors = [
u for u in entry.uris
if any((
self.isMirror(u),
self.isThirdPartyMirror(u),
self._allowThirdParty(),
))
]
if not known_mirrors:
entry.disabled = True
disabled_unknown_mirror.append(entry)
logging.debug("entry '%s' was disabled (unknown mirror)"
% get_string_with_no_auth_from_source_entry(copy.deepcopy(entry)))
continue
# Move suites to the next release.
new_suites = []
for s in entry.suites:
try:
new_suites.append(suite_mapping[s])
except KeyError:
if s in suite_mapping.values():
new_suites.append(s)
# If this did not yield any suites, disable this source.
if not new_suites:
entry.disabled = True
disabled_unknown_dist.append(entry)
logging.debug("entry '%s' was disabled (unknown dist)"
% get_string_with_no_auth_from_source_entry(copy.deepcopy(entry)))
continue
else:
entry.suites = sorted(list(set(new_suites)), key=suite_ordering_key)
# deb-src entries, security archive URIs, and sources without only
# -security or -backports enabled are not valid "to" sources.
valid_uris = [u for u in entry.uris \
if "/security.ubuntu.com" not in u]
valid_suites = [s for s in entry.suites \
if s.rsplit('-', 1)[-1] not in ["backports", "security"]]
valid_to = "deb" in entry.types and valid_uris and valid_suites
if not valid_to:
continue
# Finally, test the archive to make sure it provides the new dist.
(downloadable, failed) = self._deb822SourceEntryDownloadable(entry)
if not downloadable:
entry.disabled = True
disabled_no_release_file.append(entry)
logging.debug("entry '%s' was disabled (no Release file)"
% get_string_with_no_auth_from_source_entry(copy.deepcopy(entry)))
continue
elif failed:
logging.debug("some Release files were not downloadable for '%s"
% get_string_with_no_auth_from_source_entry(copy.deepcopy(entry)))
else:
# We can upgrade using this source.
found_to_dist = True
for suite in entry.suites:
try:
found_components[suite] |= set(entry.comps)
except KeyError:
found_components[suite] = set(entry.comps)
if 'main' not in found_components.get(self.toDist, set()):
if disabled_no_release_file:
details = _(
'This is probably because one or more of the following '
'mirrors are out-of-date or unreachable:\n\n'
)
details += '\n'.join(
sorted(e.uri for e in disabled_no_release_file)
)
elif disabled_unknown_mirror:
details = _(
'This is probably because of one or more of the following '
'unsupported mirrors was disabled:\n\n'
)
details += '\n'.join(
sorted(e.uri for e in disabled_unknown_mirror)
)
else:
details = _(
'This is probably because the current apt sources '
'configuration is unsupported.'
)
if disabled_unknown_dist:
details += _(
'\n\nThe following suites were disabled because they are '
'out-of-date or unknown:\n\n'
)
details += '\n'.join(sorted(
set().union(*[e.uri for e in disabled_unknown_dist])
))
details += _(
'\n\nWould you like to continue the upgrade using default '
'sources? If you select \'No\', the upgrade will be aborted.'
)
res = self._view.askYesNoQuestion(
_('Required apt sources are missing'),
details,
)
if not res:
return False
self._addDefaultSources()
self._addSecuritySources()
return True
def updateDeb822Sources(self):
"""
deb822-aware version of updateSourcesList()
"""
logging.debug("updateDeb822Sources()")
self.sources = SourcesList(matcherPath=self.datadir, deb822=True)
self.sources.backup(self.sources_backup_ext)
if not self.rewriteDeb822Sources():
self.abort()
# Ensure suites and components are sorted.
for entry in self.sources:
if entry.disabled or entry.invalid:
continue
entry.comps = sorted(entry.comps, key=component_ordering_key)
entry.suites = sorted(entry.suites, key=suite_ordering_key)
self.sources.save()
return True
def _logChanges(self):
# debugging output
logging.debug("About to apply the following changes")
inst = []
up = []
rm = []
held = []
keep = []
for pkg in self.cache:
if pkg.marked_install: inst.append(pkg.name)
elif pkg.marked_upgrade: up.append(pkg.name)
elif pkg.marked_delete: rm.append(pkg.name)
elif (pkg.is_installed and pkg.is_upgradable): held.append(pkg.name)
elif pkg.is_installed and pkg.marked_keep: keep.append(pkg.name)
logging.debug("Keep at same version: %s" % " ".join(keep))
logging.debug("Upgradable, but held- back: %s" % " ".join(held))
logging.debug("Remove: %s" % " ".join(rm))
logging.debug("Install: %s" % " ".join(inst))
logging.debug("Upgrade: %s" % " ".join(up))
def doPostInitialUpdate(self):
# check if we have packages in ReqReinst state that are not
# downloadable
logging.debug("doPostInitialUpdate")
self.quirks.PostInitialUpdate()
if not self.cache:
return False
# Check foreign packages before we rewrite sources. This will give us a
# better chance of tracking the source of foreign packages, i.e. which
# PPA they come from.
self._foreign_packages_pre_rewrite = self.cache.foreign_packages()
if len(self.cache.req_reinstall_pkgs) > 0:
logging.warning("packages in reqReinstall state, trying to fix")
self.cache.fix_req_reinst(self._view)
self.openCache()
if len(self.cache.req_reinstall_pkgs) > 0:
reqreinst = self.cache.req_reinstall_pkgs
header = ngettext("Package in inconsistent state",
"Packages in inconsistent state",
len(reqreinst))
summary = ngettext("The package '%s' is in an inconsistent "
"state and needs to be reinstalled, but "
"no archive can be found for it. "
"Please reinstall the package manually "
"or remove it from the system.",
"The packages '%s' are in an inconsistent "
"state and need to be reinstalled, but "
"no archive can be found for them. "
"Please reinstall the packages manually "
"or remove them from the system.",
len(reqreinst)) % ", ".join(reqreinst)
self._view.error(header, summary)
return False
# Log MetaPkgs installed to see if there is more than one.
meta_pkgs = []
for pkg in self.config.getlist("Distro","MetaPkgs"):
if pkg in self.cache and self.cache[pkg].is_installed:
meta_pkgs.append(pkg)
logging.debug("MetaPkgs: %s" % " ".join(sorted(meta_pkgs)))
# FIXME: check out what packages are downloadable etc to
# compare the list after the update again
self.obsolete_pkgs = self.cache._getObsoletesPkgs()
logging.debug("Obsolete: %s" % " ".join(sorted(self.obsolete_pkgs)))
return True
def doUpdate(self, showErrors=True, forceRetries=None):
logging.debug("running doUpdate() (showErrors=%s)" % showErrors)
self.cache._list.read_main_list()
progress = self._view.getAcquireProgress()
# FIXME: also remove all files from the lists partial dir!
currentRetry = 0
if forceRetries is not None:
maxRetries=forceRetries
else:
maxRetries = self.config.getint("Network","MaxRetries")
# LP: #1321959
error_msg = ""
while currentRetry < maxRetries:
try:
self.cache.update(progress)
except (SystemError, IOError) as e:
error_msg = str(e)
logging.error("IOError/SystemError in cache.update(): '%s'. Retrying (currentRetry: %s)" % (e,currentRetry))
currentRetry += 1
continue
# no exception, so all was fine, we are done
return True
logging.error("doUpdate() failed completely")
if showErrors:
self._view.error(_("Error during update"),
_("A problem occurred during the update. "
"This is usually some sort of network "
"problem, please check your network "
"connection and retry."), "%s" % error_msg)
return False
def _checkBootEfi(self):
" check that /boot/efi is a mounted partition on an EFI system"
# Not an UEFI system
if not os.path.exists("/sys/firmware/efi"):
logging.debug("Not an UEFI system")
return True
# Stuff we know about that would write to the ESP
bootloaders = ["shim-signed", "grub-efi-amd64", "grub-efi-ia32", "grub-efi-arm", "grub-efi-arm64", "sicherboot"]
if not any(bl in self.cache and self.cache[bl].is_installed for bl in bootloaders):
logging.debug("UEFI system, but no UEFI grub installed")
return True
mounted=False
with open("/proc/mounts") as mounts:
for line in mounts:
line=line.strip()
try:
(what, where, fs, options, a, b) = line.split()
except ValueError as e:
logging.debug("line '%s' in /proc/mounts not understood (%s)" % (line, e))
continue
if where != "/boot/efi":
continue
mounted=True
if "rw" in options.split(","):
logging.debug("Found writable ESP %s", line)
return True
if not mounted:
self._view.error(_("EFI System Partition (ESP) not usable"),
_("Your EFI System Partition (ESP) is not "
"mounted at /boot/efi. Please ensure that "
"it is properly configured and try again."))
else:
self._view.error(_("EFI System Partition (ESP) not usable"),
_("The EFI System Partition (ESP) mounted at "
"/boot/efi is not writable. Please mount "
"this partition read-write and try again."))
return False
def _checkFreeSpace(self):
" this checks if we have enough free space on /var and /usr"
err_sum = _("Not enough free disk space")
# TRANSLATORS: you can change the order of the sentence,
# make sure to keep all {str_*} string untranslated.
err_msg = _("The upgrade has aborted. "
"The upgrade needs a total of {str_total} free space on disk '{str_dir}'. "
"Please free at least an additional {str_needed} of disk "
"space on '{str_dir}'. {str_remedy}")
# specific ways to resolve lack of free space
remedy_archivedir = _("Remove temporary packages of former "
"installations using 'sudo apt clean'.")
remedy_boot = _("You can remove old kernels using "
"'sudo apt autoremove' and you could also "
"set COMPRESS=xz in "
"/etc/initramfs-tools/initramfs.conf to "
"reduce the size of your initramfs.")
remedy_root = _("Empty your trash and remove temporary "
"packages of former installations using "
"'sudo apt-get clean'.")
remedy_tmp = _("Reboot to clean up files in /tmp.")
remedy_usr = _("")
# allow override
if self.config.getWithDefault("FreeSpace","SkipCheck",False):
logging.warning("free space check skipped via config override")
return True
# do the check
with_snapshots = self._is_apt_btrfs_snapshot_supported()
try:
self.cache.checkFreeSpace(with_snapshots)
except NotEnoughFreeSpaceError as e:
# ok, showing multiple error dialog sucks from the UI
# perspective, but it means we do not need to break the
# string freeze
archivedir = apt_pkg.config.find_dir("Dir::Cache::archives")
err_long = ""
remedy = {archivedir: remedy_archivedir,
'/var': remedy_archivedir,
'/boot': remedy_boot,
'/': remedy_root,
'/tmp': remedy_tmp,
'/usr': remedy_usr}
for req in e.free_space_required_list:
if err_long != "":
err_long += " "
if req.dir in remedy:
err_long += err_msg.format(str_total=req.size_total, str_dir=req.dir,
str_needed=req.size_needed,
str_remedy=remedy[req.dir])
else:
err_long += err_msg.format(str_total=req.size_total, str_dir=req.dir,
str_needed=req.size_needed,
str_remedy='')
self._view.error(err_sum, err_long)
return False
return True
def calcDistUpgrade(self):
self._view.updateStatus(_("Calculating the changes"))
if not self.cache.distUpgrade(self._view, self.serverMode, self._partialUpgrade):
return False
if self.serverMode:
if not self.cache.installTasks(self.cache.installedTasks):
return False
# show changes and confirm
changes = self.cache.get_changes()
self._view.processEvents()
# log the changes for debugging
self._logChanges()
self._view.processEvents()
# check if we have enough free space
if not self._checkFreeSpace():
return False
# check that ESP is sane
if not self._checkBootEfi():
return False
self._view.processEvents()
# flush UI
self._view.processEvents()
return changes
def askDistUpgrade(self):
changes = self.calcDistUpgrade()
if not changes:
return False
# ask the user
res = self._view.confirmChanges(_("Do you want to start the upgrade?"),
changes,
self.cache.required_download)
return res
def _isLivepatchEnabled(self):
di = distro_info.UbuntuDistroInfo()
return di.is_lts(self.fromDist) and os.path.isfile('/var/snap/canonical-livepatch/common/machine-token')
def askLivepatch(self):
di = distro_info.UbuntuDistroInfo()
if not self._isLivepatchEnabled() or di.is_lts(self.toDist):
return True
version = next((r.version for r in di.get_all("object") if r.series == self.toDist), self.toDist)
res = self._view.askCancelContinueQuestion(None,
_("Livepatch security updates are not available for Ubuntu %s. "
"If you upgrade, Livepatch will turn off.") % version)
return res
def doDistUpgradeFetching(self):
# get the upgrade
currentRetry = 0
fprogress = self._view.getAcquireProgress()
#iprogress = self._view.getInstallProgress(self.cache)
# start slideshow
url = self.config.getWithDefault("Distro","SlideshowUrl",None)
if url:
try:
lang = locale.getdefaultlocale()[0].split('_')[0]
except:
logging.exception("getdefaultlocale")
lang = "en"
self._view.getHtmlView().open("%s#locale=%s" % (url, lang))
# retry the fetching in case of errors
maxRetries = self.config.getint("Network","MaxRetries")
# FIXME: we get errors like
# "I wasn't able to locate file for the %s package"
# here sometimes. its unclear why and not reproducible, the
# current theory is that for some reason the file is not
# considered trusted at the moment
# pkgAcquireArchive::QueueNext() runs debReleaseIndex::IsTrused()
# (the later just checks for the existence of the .gpg file)
# OR
# the fact that we get a pm and fetcher here confuses something
# in libapt?
# POSSIBLE workaround: keep the list-dir locked so that
# no apt-get update can run outside from the release
# upgrader
user_canceled = False
# LP: #1102593 - In Python 3, the targets of except clauses get `del`d
# from the current namespace after the exception is handled, so we
# must assign it to a different variable in order to use it after
# the while loop.
exception = None
while currentRetry < maxRetries:
try:
pm = apt_pkg.PackageManager(self.cache._depcache)
self.fetcher = apt_pkg.Acquire(fprogress)
self.cache._fetch_archives(self.fetcher, pm)
except apt.cache.FetchCancelledException as e:
logging.info("user canceled")
user_canceled = True
exception = e
break
except IOError as e:
# fetch failed, will be retried
logging.error("IOError in cache.commit(): '%s'. Retrying (currentTry: %s)" % (e,currentRetry))
currentRetry += 1
exception = e
continue
return True
# maximum fetch-retries reached without a successful commit
if user_canceled:
self._view.information(_("Upgrade canceled"),
_("The upgrade will cancel now and the "
"original system state will be restored. "
"You can resume the upgrade at a later "
"time."))
else:
logging.error("giving up on fetching after maximum retries")
self._view.error(_("Could not download the upgrades"),
_("The upgrade has aborted. Please check your "
"Internet connection or "
"installation media and try again. All files "
"downloaded so far have been kept."),
"%s" % exception)
# abort here because we want our sources.list back
self.abort()
def _is_apt_btrfs_snapshot_supported(self):
""" check if apt-btrfs-snapshot is usable """
try:
import apt_btrfs_snapshot
except ImportError:
return
try:
apt_btrfs = apt_btrfs_snapshot.AptBtrfsSnapshot()
res = apt_btrfs.snapshots_supported()
except:
logging.exception("failed to check btrfs support")
return False
logging.debug("apt btrfs snapshots supported: %s" % res)
return res
def _maybe_create_apt_btrfs_snapshot(self):
""" create btrfs snapshot (if btrfs layout is there) """
if not self._is_apt_btrfs_snapshot_supported():
return
import apt_btrfs_snapshot
apt_btrfs = apt_btrfs_snapshot.AptBtrfsSnapshot()
prefix = "release-upgrade-%s-" % self.toDist
res = apt_btrfs.create_btrfs_root_snapshot(prefix)
logging.info("creating snapshot '%s' (success=%s)" % (prefix, res))
def doDistUpgradeSimulation(self):
backups = {}
backups["dir::bin::dpkg"] = [apt_pkg.config["dir::bin::dpkg"]]
apt_pkg.config["dir::bin::dpkg"] = "/bin/true"
# If we remove automatically installed packages in the upgrade, we'd lose their auto bit
# here in the simulation as we'd write the simulated end result to the file, so let's
# not write the file for the simulation.
backups["Dir::State::extended_states"] = [apt_pkg.config["Dir::State::extended_states"]]
with tempfile.NamedTemporaryFile(prefix='apt_extended_states_') as f:
apt_pkg.config["Dir::State::extended_states"] = f.name
for lst in "dpkg::pre-invoke", "dpkg::pre-install-pkgs", "dpkg::post-invoke", "dpkg::post-install-pkgs":
backups[lst + "::"] = apt_pkg.config.value_list(lst)
apt_pkg.config.clear(lst)
try:
return self.doDistUpgrade()
finally:
for lst in backups:
for item in backups[lst]:
apt_pkg.config.set(lst, item)
def doDistUpgrade(self):
# add debug code only here
#apt_pkg.config.set("Debug::pkgDpkgPM", "1")
#apt_pkg.config.set("Debug::pkgOrderList", "1")
#apt_pkg.config.set("Debug::pkgPackageManager", "1")
# get the upgrade
currentRetry = 0
fprogress = self._view.getAcquireProgress()
iprogress = self._view.getInstallProgress(self.cache)
# retry the fetching in case of errors
maxRetries = self.config.getint("Network","MaxRetries")
if not self._partialUpgrade:
self.quirks.StartUpgrade()
# FIXME: take this into account for diskspace calculation
self._maybe_create_apt_btrfs_snapshot()
res = False
exception = None
while currentRetry < maxRetries:
try:
res = self.cache.commit(fprogress,iprogress)
logging.debug("cache.commit() returned %s" % res)
except SystemError as e:
logging.error("SystemError from cache.commit(): %s" % e)
exception = e
# if its a ordering bug we can cleanly revert to
# the previous release, no packages have been installed
# yet (LP: #328655, #356781)
if os.path.exists("/var/run/ubuntu-release-upgrader-apt-exception"):
with open("/var/run/ubuntu-release-upgrader-apt-exception") as f:
e = f.read()
logging.error("found exception: '%s'" % e)
# if its a ordering bug we can cleanly revert but we need to write
# a marker for the parent process to know its this kind of error
pre_configure_errors = [
"E:Internal Error, Could not perform immediate configuration",
"E:Couldn't configure pre-depend "]
for preconf_error in pre_configure_errors:
if str(e).startswith(preconf_error):
logging.debug("detected preconfigure error, restorting state")
# FIXME: strings are not good, but we are in string freeze
# currently
msg = _("Error during commit")
msg += "\n'%s'\n" % str(e)
msg += _("Restoring original system state")
self._view.error(_("Could not install the upgrades"), msg)
# abort() exits cleanly
self.abort()
# invoke the frontend now and show a error message
msg = _("The upgrade has aborted. Your system "
"could be in an unusable state. A recovery "
"will run now (dpkg --configure -a).")
if not self._partialUpgrade:
if not run_apport():
msg += _("\n\nPlease report this bug in a browser at "
"http://bugs.launchpad.net/ubuntu/+source/ubuntu-release-upgrader/+filebug "
"and attach the files in /var/log/dist-upgrade/ "
"to the bug report.\n"
"%s" % e)
self._view.error(_("Could not install the upgrades"), msg)
# installing the packages failed, can't be retried
cmd = ["/usr/bin/dpkg","--configure","-a"]
if os.environ.get("DEBIAN_FRONTEND") == "noninteractive":
cmd.append("--force-confold")
self._view.getTerminal().call(cmd)
return False
except IOError as e:
# fetch failed, will be retried
logging.error("IOError in cache.commit(): '%s'. Retrying (currentTry: %s)" % (e,currentRetry))
currentRetry += 1
exception = e
continue
except OSError as e:
logging.exception("cache.commit()")
# deal gracefully with:
# OSError: [Errno 12] Cannot allocate memory
exception = e
if e.errno == 12:
msg = _("Error during commit")
msg += "\n'%s'\n" % str(e)
msg += _("Restoring original system state")
self._view.error(_("Could not install the upgrades"), msg)
# abort() exits cleanly
self.abort()
# no exception, so all was fine, we are done
return True
# maximum fetch-retries reached without a successful commit
logging.error("giving up on fetching after maximum retries")
self._view.error(_("Could not download the upgrades"),
_("The upgrade has aborted. Please check your "\
"Internet connection or "\
"installation media and try again. "),
"%s" % exception)
# abort here because we want our sources.list back
self.abort()
def doPostUpgrade(self):
get_telemetry().add_stage('POSTUPGRADE')
# clean up downloaded packages
archivedir = os.path.dirname(
apt_pkg.config.find_dir("Dir::Cache::archives"))
for item in self.fetcher.items:
if os.path.dirname(os.path.abspath(item.destfile)) == archivedir:
try:
os.unlink(item.destfile)
except OSError:
pass
# reopen cache
self.openCache()
# run the quirks handler that does does like things adding
# missing groups or similar work arounds, only do it on real
# upgrades
self.quirks.PostUpgrade()
# check out what packages are cruft now
self._view.setStep(Step.CLEANUP)
self._view.updateStatus(_("Searching for obsolete software"))
now_obsolete = self.cache._getObsoletesPkgs()
logging.debug("Obsolete: %s" % " ".join(sorted(now_obsolete)))
# now coherence check - if a base meta package is in the obsolete list
# now, that means that something went wrong (see #335154) badly with
# the network. this should never happen, but it did happen at least
# once so we add extra paranoia here
for pkg in self.config.getlist("Distro","BaseMetaPkgs"):
if pkg in now_obsolete:
logging.error("the BaseMetaPkg '%s' is in the obsolete list, something is wrong, ignoring the obsoletes" % pkg)
now_obsolete = set()
break
# check if we actually want obsolete removal
if not self.config.getWithDefault("Distro","RemoveObsoletes", True):
logging.debug("Skipping obsolete Removal")
return True
# now get the meta-pkg specific obsoletes and purges
for pkg in self.config.getlist("Distro","MetaPkgs"):
if pkg in self.cache and self.cache[pkg].is_installed:
self.forced_obsoletes.extend(self.config.getlist(pkg,"ForcedObsoletes"))
logging.debug("forced_obsoletes: %s" % self.forced_obsoletes)
# mark packages that are now obsolete (and were not obsolete
# before) to be deleted.
remove_candidates = now_obsolete - self.obsolete_pkgs
remove_candidates |= set(self.forced_obsoletes)
# now go for the unused dependencies
unused_dependencies = self.cache._getUnusedDependencies()
logging.debug("Unused dependencies: %s" %" ".join(unused_dependencies))
remove_candidates |= set(unused_dependencies)
# see if we actually have to do anything here
if not self.config.getWithDefault("Distro","RemoveObsoletes", True):
logging.debug("Skipping RemoveObsoletes as stated in the config")
remove_candidates = set()
logging.debug("remove_candidates: '%s'" % remove_candidates)
logging.debug("Start checking for obsolete pkgs")
progress = self._view.getOpCacheProgress()
scheduled_remove = set()
# Remove all remove candidates that should not actually be ones from the list
for pkgname in list(remove_candidates):
if not self.cache.isRemoveCandidate(pkgname, self._foreign_packages_pre_rewrite):
remove_candidates.remove(pkgname)
with self.cache.actiongroup():
# Forced obsoletes we remove, removing any of their dependencies, hence do a first loop with auto_fix=True
for (i, pkgname) in enumerate(remove_candidates):
progress.update((i/float(len(remove_candidates)))*100.0 / 2)
if pkgname in self.forced_obsoletes:
self._view.processEvents()
if not self.cache.tryMarkObsoleteForRemoval(pkgname, remove_candidates, self.forced_obsoletes, auto_fix=True):
logging.debug("'%s' scheduled for remove but not safe to remove, skipping", pkgname)
else:
scheduled_remove.add(pkgname)
# Now let's try to remove other packages
for (i, pkgname) in enumerate(remove_candidates):
progress.update((i/float(len(remove_candidates)))*100.0 / 2 + 50)
self._view.processEvents()
if not self.cache.tryMarkObsoleteForRemoval(pkgname, remove_candidates, self.forced_obsoletes, auto_fix=False):
logging.debug("'%s' scheduled for remove but not safe to remove, skipping", pkgname)
else:
scheduled_remove.add(pkgname)
# We have scheduled their removals, but not any reverse-dependencies. If anything is broken now,
# resolve them by keeping back the obsolete packages.
self.cache._startAptResolverLog()
pr = apt.ProblemResolver(self.cache)
try:
pr.resolve_by_keep()
except Exception:
pass
self.cache._stopAptResolverLog()
if self.cache.broken_count > 0:
logging.debug("resolve_by_keep() failed to resolve conflicts from removing obsolete packages, falling back to slower implementation.")
self.cache.clear()
scheduled_remove = set()
with self.cache.actiongroup():
for (i, pkgname) in enumerate(remove_candidates):
progress.update((i/float(len(remove_candidates)))*100.0)
self._view.processEvents()
if not self.cache.tryMarkObsoleteForRemoval(pkgname, remove_candidates, self.forced_obsoletes, auto_fix=True):
logging.debug("'%s' scheduled for remove but not safe to remove, skipping", pkgname)
else:
scheduled_remove.add(pkgname)
# resolve_by_keep() will revert any unsafe removals, so we need to list them here again.
for pkgname in scheduled_remove:
if (
pkgname in self.cache and
not self.cache[pkgname].marked_delete
):
logging.debug("obsolete package '%s' could not be removed", pkgname)
logging.debug("Finish checking for obsolete pkgs")
progress.done()
# get changes
changes = self.cache.get_changes()
logging.debug("The following packages are marked for removal: %s" % " ".join([pkg.name for pkg in changes]))
summary = _("Remove obsolete packages?")
actions = [_("_Keep"), _("_Remove")]
# FIXME Add an explanation about what obsolete packages are
#explanation = _("")
if (len(changes) > 0 and
self._view.confirmChanges(summary, changes, 0, actions, False)):
fprogress = self._view.getAcquireProgress()
iprogress = self._view.getInstallProgress(self.cache)
try:
self.cache.commit(fprogress,iprogress)
except (SystemError, IOError) as e:
logging.error("cache.commit() in doPostUpgrade() failed: %s" % e)
self._view.error(_("Error during commit"),
_("A problem occurred during the clean-up. "
"Please see the below message for more "
"information. "),
"%s" % e)
self.cleanBackupSources()
# run stuff after cleanup
self.quirks.PostCleanup()
# run the post upgrade scripts that can do fixup like xorg.conf
# fixes etc - only do on real upgrades
if not self._partialUpgrade:
self.runPostInstallScripts()
return True
def runPostInstallScripts(self):
"""
scripts that are run in any case after the distupgrade finished
whether or not it was successful
Cache lock is released during script runs in the event that the
PostInstallScripts require apt or dpkg changes.
"""
if self.cache:
self.cache.release_lock()
self.cache.unlock_lists_dir()
# now run the post-upgrade fixup scripts (if any)
for script in self.config.getlist("Distro","PostInstallScripts"):
if not os.path.exists(script):
logging.warning("PostInstallScript: '%s' not found" % script)
continue
logging.debug("Running PostInstallScript: '%s'" % script)
try:
# work around kde tmpfile problem where it eats permissions
check_and_fix_xbit(script)
self._view.getTerminal().call([script], hidden=True)
except Exception as e:
logging.error("got error from PostInstallScript %s (%s)" % (script, e))
if self.cache:
self.cache.get_lock()
def abort(self):
""" abort the upgrade, cleanup (as much as possible) """
logging.debug("abort called")
if hasattr(self, "sources"):
self.sources.restore_backup(self.sources_backup_ext)
self.restoreMigratedSources()
self.cleanBackupSources()
# generate a new cache
self._view.updateStatus(_("Restoring original system state"))
self._view.abort()
self.openCache()
sys.exit(1)
def _checkDep(self, depstr):
" check if a given depends can be satisfied "
for or_group in apt_pkg.parse_depends(depstr):
logging.debug("checking: '%s' " % or_group)
for dep in or_group:
depname = dep[0]
ver = dep[1]
oper = dep[2]
if depname not in self.cache:
logging.error("_checkDep: '%s' not in cache" % depname)
return False
inst = self.cache[depname]
instver = getattr(inst.installed, "version", None)
if (instver != None and
apt_pkg.check_dep(instver,oper,ver) == True):
return True
logging.error("depends '%s' is not satisfied" % depstr)
return False
def checkViewDepends(self):
" check if depends are satisfied "
logging.debug("checkViewDepends()")
res = True
# now check if anything from $foo-updates is required
depends = self.config.getlist("View","Depends")
depends.extend(self.config.getlist(self._view.__class__.__name__,
"Depends"))
for dep in depends:
logging.debug("depends: '%s'", dep)
res &= self._checkDep(dep)
if not res:
# FIXME: instead of error out, fetch and install it
# here
self._view.error(_("Required depends is not installed"),
_("The required dependency '%s' is not "
"installed. " % dep))
sys.exit(1)
return res
def isMirror(self, uri):
""" check if uri is a known mirror """
# deal with username:password in a netloc
raw_uri = uri.rstrip("/")
scheme, netloc, path, query, fragment = urlsplit(raw_uri)
if "@" in netloc:
netloc = netloc.split("@")[1]
# construct new mirror url without the username/pw
uri = "%s://%s%s" % (scheme, netloc, path)
for mirror in self.valid_mirrors:
mirror = mirror.rstrip("/")
if is_mirror(mirror, uri):
return True
# deal with mirrors like
# deb http://localhost:9977/security.ubuntu.com/ubuntu intrepid-security main restricted
# both apt-debtorrent and apt-cacher use this (LP: #365537)
mirror_host_part = mirror.split("//")[1]
if uri.endswith(mirror_host_part):
logging.debug("found apt-cacher/apt-torrent style uri %s" % uri)
return True
return False
def isThirdPartyMirror(self, uri):
" check if uri is an allowed third-party mirror "
uri = uri.rstrip("/")
for mirror in self.valid_3p_mirrors:
mirror = mirror.rstrip("/")
if is_mirror(mirror, uri):
return True
return False
# this is the core
def fullUpgrade(self):
# coherence check (check for ubuntu-desktop, brokenCache etc)
self._view.updateStatus(_("Checking package manager"))
self._view.setStep(Step.PREPARE)
if not self.prepare():
logging.error("self.prepare() failed")
if os.path.exists("/usr/bin/apport-bug"):
self._view.error(_("Preparing the upgrade failed"),
_("Preparing the system for the upgrade "
"failed so a bug reporting process is "
"being started."))
subprocess.Popen(["apport-bug", "ubuntu-release-upgrader-core"])
else:
self._view.error(_("Preparing the upgrade failed"),
_("Preparing the system for the upgrade "
"failed. To report a bug install apport "
"and then execute 'apport-bug "
"ubuntu-release-upgrader'."))
logging.error("Missing apport-bug, bug report not "
"autocreated")
self.abort()
if not self.askLivepatch():
self.abort()
# run a "apt-get update" now, its ok to ignore errors,
# because
# a) we disable any third party sources later
# b) we check if we have valid ubuntu sources later
# after we rewrite the sources.list and do a
# apt-get update there too
# because the (unmodified) sources.list of the user
# may contain bad/unreachable entries we run only
# with a single retry
self.doUpdate(showErrors=False, forceRetries=1)
self.openCache()
# do pre-upgrade stuff (calc list of obsolete pkgs etc)
if not self.doPostInitialUpdate():
self.abort()
try:
# update sources.list
self._view.setStep(Step.MODIFY_SOURCES)
self._view.updateStatus(_("Updating repository information"))
# Deb822 sources are the default on Ubuntu, and we don't want to
# maintain two ways to re-write sources. Migrate any .list sources
# to .sources before the re-write.
if self.migratedToDeb822() <= 0:
self.migrateToDeb822Sources()
if not self.updateDeb822Sources():
self.abort()
# then update the package index files
if not self.doUpdate():
self.abort()
# then open the cache (again)
self._view.updateStatus(_("Checking package manager"))
# if something fails here (e.g. locking the cache) we need to
# restore the system state (LP: #1052605)
self.openCache(restore_sources_list_on_fail=True)
# Check foreign packages now, and abort the upgrade if there are
# any left. If third-party sources are allowed, issue a warning
# instead.
self._foreign_packages_post_rewrite = self.cache.foreign_packages()
self._warn_about_foreign_packages()
# re-check server mode because we got new packages (it may happen
# that the system had no sources.list entries and therefore no
# desktop file information)
# switch from server to desktop but not the other way
if self.serverMode:
self.serverMode = self.cache.need_server_mode()
# do it here as we need to know if we are in server or client mode
self.quirks.ensure_recommends_are_installed_on_desktops()
# now check if we still have some key packages available/downloadable
# after the update - if not something went seriously wrong
# (this happend e.g. during the intrepid->jaunty upgrade for some
# users when de.archive.ubuntu.com was overloaded)
for pkg in self.config.getlist("Distro","BaseMetaPkgs"):
if (pkg not in self.cache or
not self.cache.anyVersionDownloadable(self.cache[pkg])):
# FIXME: we could offer to add default source entries here,
# but we need to be careful to not duplicate them
# (i.e. the error here could be something else than
# missing sources entries but network errors etc)
logging.error("No '%s' available/downloadable after sources.list rewrite+update" % pkg)
if pkg not in self.cache:
logging.error("'%s' was not in the cache" % pkg)
elif not self.cache.anyVersionDownloadable(self.cache[pkg]):
logging.error("'%s' was not downloadable" % pkg)
self._view.error(_("Invalid package information"),
_("After updating your package "
"information, the essential package '%s' "
"could not be located. This may be "
"because you have no official mirrors "
"listed in your software sources, or "
"because of excessive load on the mirror "
"you are using. See /etc/apt/sources.list "
"for the current list of configured "
"software sources."
"\n"
"In the case of an overloaded mirror, you "
"may want to try the upgrade again later.")
% pkg)
if os.path.exists("/usr/bin/apport-bug"):
subprocess.Popen(["apport-bug", "ubuntu-release-upgrader-core"])
else:
logging.error("Missing apport-bug, bug report not "
"autocreated")
self.abort()
# calc the dist-upgrade and see if the removals are ok/expected
# do the dist-upgrade
self._view.updateStatus(_("Calculating the changes"))
if not self.askDistUpgrade():
self.abort()
self._inhibitIdle()
# fetch the stuff
self._view.setStep(Step.FETCH)
self._view.updateStatus(_("Fetching"))
if not self.doDistUpgradeFetching():
self.abort()
# simulate an upgrade
self._view.setStep(Step.INSTALL)
self._view.updateStatus(_("Upgrading"))
if not self.doDistUpgradeSimulation():
self._view.error(_("Upgrade infeasible"),
_("The upgrade could not be completed, there "
"were errors during the upgrade "
"process."))
self.abort()
except KeyboardInterrupt:
self.abort()
# Reopen ask above
self.openCache(restore_sources_list_on_fail=True)
self.serverMode = self.cache.need_server_mode()
self.quirks.ensure_recommends_are_installed_on_desktops()
self._view.updateStatus(_("Calculating the changes"))
if not self.calcDistUpgrade():
self.abort()
# now do the upgrade
self._view.setStep(Step.INSTALL)
self._view.updateStatus(_("Upgrading"))
if not self.doDistUpgrade():
# run the post install scripts (for stuff like UUID conversion)
self.runPostInstallScripts()
# don't abort here, because it would restore the sources.list
self._view.information(_("Upgrade complete"),
_("The upgrade has completed but there "
"were errors during the upgrade "
"process."))
# do not abort because we are part of the way through the process
sys.exit(1)
# do post-upgrade stuff
self.doPostUpgrade()
# remove upgrade-available notice
if os.path.exists("/var/lib/ubuntu-release-upgrader/release-upgrade-available"):
os.unlink("/var/lib/ubuntu-release-upgrader/release-upgrade-available")
# done, ask for reboot
self._view.setStep(Step.REBOOT)
self._view.updateStatus(_("System upgrade is complete."))
get_telemetry().done()
# FIXME should we look into /var/run/reboot-required here?
if not inside_chroot():
if self._inside_WSL():
self._view.adviseExitOtherWSL()
with open("/run/launcher-command", "w+", encoding="utf-8") as f:
f.write("action: reboot\n")
self._view.adviseRestartWSL()
elif self._view.confirmRestart():
subprocess.Popen(["/usr/bin/systemctl", "reboot", "--check-inhibitors=no"])
sys.exit(0)
return True
def run(self):
self._view.processEvents()
return self.fullUpgrade()
def doPartialUpgrade(self):
" partial upgrade mode, useful for repairing "
self._view.setStep(Step.PREPARE)
self._view.hideStep(Step.MODIFY_SOURCES)
self._view.hideStep(Step.REBOOT)
self._partialUpgrade = True
self.prepare()
if not self.doPostInitialUpdate():
return False
if not self.askDistUpgrade():
return False
self._view.setStep(Step.FETCH)
self._view.updateStatus(_("Fetching"))
if not self.doDistUpgradeFetching():
return False
self._view.setStep(Step.INSTALL)
self._view.updateStatus(_("Upgrading"))
if not self.doDistUpgrade():
self._view.information(_("Upgrade complete"),
_("The upgrade has completed but there "
"were errors during the upgrade "
"process."))
return False
if not self.doPostUpgrade():
self._view.information(_("Upgrade complete"),
_("The upgrade has completed but there "
"were errors during the upgrade "
"process."))
return False
if os.path.exists(REBOOT_REQUIRED_FILE):
# we can not talk to session management here, we run as root
if self._view.confirmRestart():
subprocess.Popen(["/usr/bin/systemctl", "reboot", "--check-inhibitors=no"])
else:
self._view.information(_("Upgrade complete"),
_("The partial upgrade was completed."))
return True
def _inhibitIdle(self):
logging.debug('inhibit screensaver')
try:
import dbus
# Temporarily override DBUS_SESSION_BUS_ADDRESS so that we get the
# invoking user's session bus.
bus_address_copy = os.getenv('DBUS_SESSION_BUS_ADDRESS', '')
bus_address_user = self.get_user_env(
'DBUS_SESSION_BUS_ADDRESS',
bus_address_copy,
)
os.environ['DBUS_SESSION_BUS_ADDRESS'] = bus_address_user
xdg_session_type = self.get_user_env('XDG_SESSION_TYPE', '')
if (
os.getuid() == 0 and
(uid := self.get_user_uid()) is not None
):
os.seteuid(uid)
# The org.freedesktop.ScreenSaver.Inhibit effect lasts only
# as long as the dbus connection remains open. Once u-r-u
# exits, the connection will be closed and screen inhibition
# will be removed.
self._session_bus = dbus.SessionBus()
proxy = self._session_bus.get_object('org.freedesktop.ScreenSaver',
'/org/freedesktop/ScreenSaver')
screensaver = dbus.Interface(proxy, dbus_interface='org.freedesktop.ScreenSaver')
screensaver.Inhibit('ubuntu-release-upgrader', 'Upgrading Ubuntu')
summary = _("Lock screen disabled")
message = _("Your lock screen has been "
"disabled and will remain "
"disabled during the upgrade.")
except Exception as e:
if xdg_session_type in ('', 'tty'):
return
logging.debug('failed to inhibit screensaver: ' + str(e))
summary = _("Unable to disable lock screen")
message = _("It is highly recommended that the "
"lock screen be disabled during the "
"upgrade to prevent later issues. "
"Please ensure your screen lock is "
"disabled before continuing.")
finally:
os.environ['DBUS_SESSION_BUS_ADDRESS'] = bus_address_copy
os.seteuid(os.getuid())
self._view.information(summary, message)
def _inside_WSL(self):
return os.path.exists("/proc/sys/fs/binfmt_misc/WSLInterop")
def _warn_about_foreign_packages(self):
logging.debug(
'Foreign (before rewriting sources): {}'
.format(' '.join(self._foreign_packages_pre_rewrite))
)
logging.debug(
'Foreign (after rewriting sources): {}'
.format(' '.join(self._foreign_packages_post_rewrite))
)
if not self._foreign_packages_post_rewrite:
return
header = _('Foreign Packages Installed')
body = _(
'The following unofficial packages are currently installed:\n\n'
)
for (name, pkg_file) in self._foreign_packages_post_rewrite.items():
# The "before" dict may contain a package file for the sources,
# while the "after" dict probably won't, unless --allow-third-party
# is set.
if pkg_file is None:
pkg_file = self._foreign_packages_pre_rewrite.get(name)
if pkg_file is not None:
origin = pkg_file.origin
else:
origin = _('unknown origin')
body += _(f'{name:.<25}Installed from: {origin}\n')
body += _(
'\n\nIt is recommended to install supported versions from '
'the Ubuntu archive, and try the upgrade again.\n\n'
'Do you want to continue the upgrade anyways?'
)
if self._view.askYesNoQuestion(header, body):
return
else:
self.abort()
def get_user_uid(self):
if self._invoking_user_pwd is None:
return None
return self._invoking_user_pwd.pw_uid
def get_user_gid(self):
if self._invoking_user_pwd is None:
return None
return self._invoking_user_pwd.pw_gid
def get_user_name(self):
if self._invoking_user_pwd is None:
return None
return self._invoking_user_pwd.pw_name
def get_user_home(self):
if self._invoking_user_pwd is None:
return None
return self._invoking_user_pwd.pw_dir
def run_as_user(self, args, **kwargs):
user = self.get_user_name()
if user is None:
raise OSError(errno.EINVAL, os.strerror(errno.EINVAL))
return subprocess.run(
[
'systemd-run',
'--user',
'-M', f'{user}@.host',
'--wait',
'--pipe',
'-q',
'--',
*args,
],
**kwargs,
)
def get_user_env(self, key, default=None):
"""
Helper to access variables from the invoking user's environment.
"""
try:
v = self.run_as_user(
['echo', f'${key}'],
stdout=subprocess.PIPE,
).stdout.decode().strip()
if not v:
return default
return v
except OSError:
return default
def systemctl_as_user(self, args):
user = self.get_user_name()
if user is None:
raise OSError(errno.EINVAL, os.strerror(errno.EINVAL))
return subprocess.run(
[
'systemctl',
'--user',
'-M', f'{user}@.host',
*args,
]
)
./DistUpgradeFetcher.py 0000644 0003721 0004705 00000013550 15000447260 014576 0 ustar buildd buildd # DistUpgradeFetcher.py
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
#
# Copyright (c) 2006 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk
from .ReleaseNotesViewer import ReleaseNotesViewer
from .utils import error
from .DistUpgradeFetcherCore import DistUpgradeFetcherCore
from .SimpleGtk3builderApp import SimpleGtkbuilderApp
from gettext import gettext as _
from urllib.request import urlopen
from urllib.error import HTTPError
import os
import socket
class DistUpgradeFetcherGtk(DistUpgradeFetcherCore):
def __init__(self, new_dist, progress, parent, datadir):
DistUpgradeFetcherCore.__init__(self, new_dist, progress)
uifile = os.path.join(datadir, "gtkbuilder", "ReleaseNotes.ui")
self.widgets = SimpleGtkbuilderApp(uifile, "ubuntu-release-upgrader")
self.window_main = parent
def error(self, summary, message):
return error(self.window_main, summary, message)
def runDistUpgrader(self):
os.execv(self.script, [self.script] + self.run_options)
def showReleaseNotes(self):
# first try showing the webkit version, this may fail (return None
# because e.g. there is no webkit installed)
res = self._try_show_release_notes_webkit()
if res is not None:
return res
else:
# fallback to text
return self._try_show_release_notes_textview()
def _try_show_release_notes_webkit(self):
if self.new_dist.releaseNotesHtmlUri is not None:
try:
from .ReleaseNotesViewerWebkit import ReleaseNotesViewerWebkit
webkit_release_notes = ReleaseNotesViewerWebkit(
self.new_dist.releaseNotesHtmlUri)
webkit_release_notes.show()
self.widgets.scrolled_notes.add(webkit_release_notes)
res = self.widgets.dialog_release_notes.run()
self.widgets.dialog_release_notes.hide()
if res == Gtk.ResponseType.OK:
return True
return False
except ImportError:
pass
return None
def _try_show_release_notes_textview(self):
# FIXME: care about i18n! (append -$lang or something)
if self.new_dist.releaseNotesURI is not None:
uri = self.new_dist.releaseNotesURI
if self.window_main:
self.window_main.set_sensitive(False)
self.window_main.get_window().set_cursor(
Gdk.Cursor.new(Gdk.CursorType.WATCH))
while Gtk.events_pending():
Gtk.main_iteration()
# download/display the release notes
# FIXME: add some progress reporting here
res = Gtk.ResponseType.CANCEL
timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(5)
release_notes = urlopen(uri)
notes = release_notes.read().decode("UTF-8", "replace")
textview_release_notes = ReleaseNotesViewer(notes)
textview_release_notes.show()
self.widgets.scrolled_notes.add(textview_release_notes)
release_widget = self.widgets.dialog_release_notes
release_widget.set_transient_for(self.window_main)
res = self.widgets.dialog_release_notes.run()
self.widgets.dialog_release_notes.hide()
except HTTPError:
primary = "%s" % \
_("Could not find the release notes")
secondary = _("The server may be overloaded. ")
dialog = Gtk.MessageDialog(self.window_main,
Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE, "")
dialog.set_title("")
dialog.set_markup(primary)
dialog.format_secondary_text(secondary)
dialog.run()
dialog.destroy()
except IOError:
primary = "%s" % \
_("Could not download the release notes")
secondary = _("Please check your internet connection.")
dialog = Gtk.MessageDialog(self.window_main,
Gtk.DialogFlags.MODAL,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE, "")
dialog.set_title("")
dialog.set_markup(primary)
dialog.format_secondary_text(secondary)
dialog.run()
dialog.destroy()
socket.setdefaulttimeout(timeout)
if self.window_main:
self.window_main.set_sensitive(True)
self.window_main.get_window().set_cursor(None)
# user clicked cancel
if res == Gtk.ResponseType.OK:
return True
return False
./DistUpgradeFetcherCore.py 0000644 0003721 0004705 00000023076 15000447260 015413 0 ustar buildd buildd # DistUpgradeFetcherCore.py
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
#
# Copyright (c) 2006 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
from string import Template
import os
import apt_pkg
import logging
import tarfile
import tempfile
import shutil
import socket
import sys
import subprocess
from gettext import gettext as _
from aptsources.sourceslist import SourcesList
from urllib.request import urlopen
from urllib.error import HTTPError
from .utils import get_dist
from .DistUpgradeViewText import readline
class DistUpgradeFetcherCore(object):
" base class (without GUI) for the upgrade fetcher "
DEFAULT_MIRROR = "http://archive.ubuntu.com/ubuntu"
DEFAULT_COMPONENT = "main"
DEBUG = "DEBUG_UPDATE_MANAGER" in os.environ
def __init__(self, new_dist, progress):
self.new_dist = new_dist
self.current_dist_name = get_dist()
self._progress = progress
# options to pass to the release upgrader when it is run
self.run_options = []
def _debug(self, msg):
" helper to show debug information "
if self.DEBUG:
sys.stderr.write(msg + "\n")
def showReleaseNotes(self):
if '--frontend=DistUpgradeViewNonInteractive' in self.run_options:
return True
if self.new_dist.releaseNotesURI is not None:
uri = self.new_dist.releaseNotesURI
timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(5)
release_notes = urlopen(uri)
notes = release_notes.read().decode("UTF-8", "replace")
except HTTPError:
self.error(_("Could not find the release announcement"),
_("The server may be overloaded."))
return False
except IOError:
self.error(_("Could not download the release announcement"),
_("Please check your internet connection."))
return False
socket.setdefaulttimeout(timeout)
print()
print(notes)
print(_("Continue [yN] "), end="")
res = readline()
if res.strip().lower().startswith(_("y")):
return True
return False
def error(self, summary, message):
""" minimal implementation for error display, should be overwriten
by subclasses that want to more fancy method
"""
print(summary)
print(message)
return False
def authenticate(self):
if self.new_dist.upgradeToolSig:
f = self.tmpdir + "/" + os.path.basename(self.new_dist.upgradeTool)
sig = self.tmpdir + "/" + os.path.basename(
self.new_dist.upgradeToolSig)
print(_("authenticate '%(file)s' against '%(signature)s' ") % {
'file': os.path.basename(f),
'signature': os.path.basename(sig)})
if self.gpgauthenticate(f, sig):
return True
return False
def gpgauthenticate(self, file, signature):
""" authenticate a file against a given signature """
gpg = [
'gpg',
'--verify',
'--no-default-keyring',
'--keyring', '/usr/share/keyrings/ubuntu-archive-keyring.gpg',
signature,
file,
]
ret = subprocess.call(gpg, stderr=subprocess.PIPE)
return ret == 0
def extractDistUpgrader(self):
# extract the tarball
fname = os.path.join(self.tmpdir, os.path.basename(self.uri))
print(_("extracting '%s'") % os.path.basename(fname))
if not os.path.exists(fname):
return False
try:
tar = tarfile.open(self.tmpdir + "/" +
os.path.basename(self.uri), "r")
for tarinfo in tar:
tar.extract(tarinfo)
tar.close()
except tarfile.ReadError as e:
logging.error("failed to open tarfile (%s)" % e)
return False
return True
def verifyDistUpgrader(self):
# FIXME: check an internal dependency file to make sure
# that the script will run correctly
# see if we have a script file that we can run
self.script = script = "%s/%s" % (self.tmpdir, self.new_dist.name)
if not os.path.exists(script):
return self.error(_("Could not run the upgrade tool"),
_("Could not run the upgrade tool") + ". " +
_("This is most likely a bug in the upgrade "
"tool. Please report it as a bug using the "
"command 'ubuntu-bug "
"ubuntu-release-upgrader-core'."))
return True
def fetchDistUpgrader(self):
" download the tarball with the upgrade script "
tmpdir = tempfile.mkdtemp(prefix="ubuntu-release-upgrader-")
self.tmpdir = tmpdir
os.chdir(tmpdir)
logging.debug("using tmpdir: '%s'" % tmpdir)
# turn debugging on here (if required)
if self.DEBUG > 0:
apt_pkg.config.set("Debug::Acquire::http", "1")
apt_pkg.config.set("Debug::Acquire::ftp", "1")
#os.listdir(tmpdir)
fetcher = apt_pkg.Acquire(self._progress)
if self.new_dist.upgradeToolSig is not None:
uri = self.new_dist.upgradeToolSig
af1 = apt_pkg.AcquireFile(fetcher,
uri,
descr=_("Upgrade tool signature"))
# reference it here to shut pyflakes up
af1
if self.new_dist.upgradeTool is not None:
self.uri = self.new_dist.upgradeTool
af2 = apt_pkg.AcquireFile(fetcher,
self.uri,
descr=_("Upgrade tool"))
# reference it here to shut pyflakes up
af2
result = fetcher.run()
if result != fetcher.RESULT_CONTINUE:
logging.warning("fetch result != continue (%s)" % result)
return False
# check that both files are really there and non-null
for f in [os.path.basename(self.new_dist.upgradeToolSig),
os.path.basename(self.new_dist.upgradeTool)]:
if not (os.path.exists(f) and os.path.getsize(f) > 0):
logging.warning("file '%s' missing" % f)
return False
return True
return False
def runDistUpgrader(self):
args = [self.script] + self.run_options
if os.getuid() != 0:
os.execv("/usr/bin/sudo", ["sudo", "-E"] + args)
else:
os.execv(self.script, args)
def cleanup(self):
# cleanup
os.chdir("..")
# del tmpdir
shutil.rmtree(self.tmpdir)
def run(self):
# see if we have release notes
if not self.showReleaseNotes():
return
if not self.fetchDistUpgrader():
self.error(_("Failed to fetch"),
_("Fetching the upgrade failed. There may be a network "
"problem. "))
return
if not self.authenticate():
self.error(_("Authentication failed"),
_("Authenticating the upgrade failed. There may be a "
"problem with the network or with the server. "))
self.cleanup()
return
if not self.extractDistUpgrader():
self.error(_("Failed to extract"),
_("Extracting the upgrade failed. There may be a "
"problem with the network or with the server. "))
return
if not self.verifyDistUpgrader():
self.error(_("Verification failed"),
_("Verifying the upgrade failed. There may be a "
"problem with the network or with the server. "))
self.cleanup()
return
try:
# check if we can execute, if we run it via sudo we will
# not know otherwise, pkexec will not raise a exception
if not os.access(self.script, os.X_OK):
ex = OSError("Can not execute '%s'" % self.script)
ex.errno = 13
raise ex
self.runDistUpgrader()
except OSError as e:
if e.errno == 13:
self.error(_("Can not run the upgrade"),
_("This usually is caused by a system where /tmp "
"is mounted noexec. Please remount without "
"noexec and run the upgrade again."))
return False
else:
self.error(_("Can not run the upgrade"),
_("The error message is '%s'.") % e.strerror)
return True
./DistUpgradeFetcherKDE.py 0000644 0003721 0004705 00000020415 15000447260 015120 0 ustar buildd buildd # DistUpgradeFetcherKDE.py
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
#
# Copyright (c) 2008 Canonical Ltd
# Copyright (c) 2014-2018 Harald Sitter
# Copyright (c) 2024 Simon Quigley
#
# Author: Jonathan Riddell
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from PyQt6 import uic
from PyQt6.QtCore import QTranslator, QLocale
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, \
QApplication
import apt_pkg
from DistUpgrade.DistUpgradeFetcherCore import DistUpgradeFetcherCore
from gettext import gettext as _
from urllib.request import urlopen
from urllib.error import HTTPError
import os
import apt
from .QUrlOpener import QUrlOpener
# TODO: uifile resolution is an utter mess and should be revised globally for
# both the fetcher and the upgrader GUI.
# TODO: make this a singleton
# We have no globally constructed QApplication available so we need to
# make sure that one is created when needed. Since from a module POV
# this can be happening in any order of the two classes this function takes
# care of it for the classes, the classes only hold a ref to the qapp returned
# to prevent it from getting GC'd, so in essence this is a singleton scoped to
# the longest lifetime of an instance from the Qt GUI. Since the lifetime is
# pretty much equal to the process' one we might as well singleton up.
def _ensureQApplication():
if not QApplication.instance():
# Force environment to make sure Qt uses suitable theming and UX.
os.environ["QT_PLATFORM_PLUGIN"] = "kde"
# For above settings to apply automatically we need to indicate that we
# are inside a full KDE session.
os.environ["KDE_FULL_SESSION"] = "TRUE"
# We also need to indicate version as otherwise KDElibs3 compatibility
# might kick in such as in QIconLoader.cpp:QString fallbackTheme.
os.environ["KDE_SESSION_VERSION"] = "6"
# Pretty much all of the above but for Qt6
os.environ["QT_QPA_PLATFORMTHEME"] = "kde"
app = QApplication(["ubuntu-release-upgrader"])
# Try to load default Qt translations so we don't have to worry about
# QStandardButton translations.
# FIXME: make sure we dep on l10n
translator = QTranslator(app)
translator.load(QLocale.system(), 'qt', '_',
'/usr/share/qt6/translations')
app.installTranslator(translator)
return app
return QApplication.instance()
def _warning(text):
QMessageBox.warning(None, "", text)
def _icon(name):
return QIcon.fromTheme(name)
class DistUpgradeFetcherKDE(DistUpgradeFetcherCore):
def __init__(self, new_dist, progress, parent, datadir):
DistUpgradeFetcherCore.__init__(self, new_dist, progress)
self.app = _ensureQApplication()
self.app.setWindowIcon(_icon("system-software-update"))
self.datadir = datadir
QUrlOpener().setupUrlHandles()
self.app.aboutToQuit.connect(QUrlOpener().teardownUrlHandles)
QApplication.processEvents()
def error(self, summary, message):
QMessageBox.critical(None, summary, message)
def runDistUpgrader(self):
# now run it with sudo
if os.getuid() != 0:
os.execv("/usr/bin/pkexec",
["pkexec",
self.script + " --frontend=DistUpgradeViewKDE"])
else:
os.execv(self.script,
[self.script, "--frontend=DistUpgradeViewKDE"] +
self.run_options)
def showReleaseNotes(self):
# FIXME: care about i18n! (append -$lang or something)
# TODO: ^ what is this supposed to mean?
self.dialog = QDialog()
uic.loadUi(self.datadir + "/dialog_release_notes.ui", self.dialog)
upgradeButton = self.dialog.buttonBox.button(
QDialogButtonBox.StandardButton.Ok
)
upgradeButton.setText(_("&Upgrade"))
upgradeButton.setIcon(_icon("dialog-ok"))
cancelButton = self.dialog.buttonBox.button(
QDialogButtonBox.StandardButton.Cancel
)
cancelButton.setText(_("&Cancel"))
cancelButton.setIcon(_icon("dialog-cancel"))
self.dialog.setWindowTitle(_("Release Notes"))
self.dialog.show()
if self.new_dist.releaseNotesHtmlUri is not None:
uri = self.new_dist.releaseNotesURI
# download/display the release notes
# TODO: add some progress reporting here
result = None
try:
release_notes = urlopen(uri)
notes = release_notes.read().decode("UTF-8", "replace")
self.dialog.scrolled_notes.setText(notes)
result = self.dialog.exec()
except HTTPError:
primary = "%s" % \
_("Could not find the release notes")
secondary = _("The server may be overloaded. ")
_warning(primary + "
" + secondary)
except IOError:
primary = "%s" % \
_("Could not download the release notes")
secondary = _("Please check your internet connection.")
_warning(primary + "
" + secondary)
# user clicked cancel
if result == QDialog.DialogCode.Accepted:
return True
return False
class KDEAcquireProgressAdapter(apt.progress.base.AcquireProgress):
def __init__(self, parent, datadir, label):
self.app = _ensureQApplication()
self.dialog = QDialog()
uiFile = os.path.join(datadir, "fetch-progress.ui")
uic.loadUi(uiFile, self.dialog)
self.dialog.setWindowTitle(_("Upgrade"))
self.dialog.installingLabel.setText(label)
self.dialog.buttonBox.rejected.connect(self.abort)
# This variable is used as return value for AcquireProgress pulses.
# Setting it to False will abort the Acquire and consequently the
# entire fetcher.
self._continue = True
QApplication.processEvents()
def abort(self):
self._continue = False
def start(self):
self.dialog.installingLabel.setText(
_("Downloading additional package files..."))
self.dialog.installationProgress.setValue(0)
self.dialog.show()
def stop(self):
self.dialog.hide()
def pulse(self, owner):
apt.progress.base.AcquireProgress.pulse(self, owner)
self.dialog.installationProgress.setValue(int(
(self.current_bytes + self.current_items) /
float(self.total_bytes + self.total_items) * 100))
current_item = self.current_items + 1
if current_item > self.total_items:
current_item = self.total_items
label_text = _("Downloading additional package files...")
if self.current_cps > 0:
label_text += _("File %s of %s at %sB/s") % (
self.current_items, self.total_items,
apt_pkg.size_to_str(self.current_cps))
else:
label_text += _("File %s of %s") % (
self.current_items, self.total_items)
self.dialog.installingLabel.setText(label_text)
QApplication.processEvents()
return self._continue
def mediaChange(self, medium, drive):
msg = _("Please insert '%s' into the drive '%s'") % (medium, drive)
change = QMessageBox.question(None, _("Media Change"), msg,
QMessageBox.Ok, QMessageBox.Cancel)
if change == QMessageBox.Ok:
return True
return False
./DistUpgradeGettext.py 0000644 0003721 0004705 00000005747 14772566125 014674 0 ustar buildd buildd # DistUpgradeGettext.py - safe wrapper around gettext
#
# Copyright (c) 2008 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import logging
import gettext as mygettext
_gettext_method = "gettext"
_ngettext_method = "ngettext"
def _verify(message, translated):
"""
helper that verifies that the message and the translated
message have the same number (and type) of % args
"""
arguments_in_message = message.count(r"%") - message.count(r"\%")
arguments_in_translation = translated.count(r"%") - translated.count(r"\%")
return arguments_in_message == arguments_in_translation
_translation_singleton = None
def _translation():
"""Return a suitable gettext.*Translations instance."""
global _translation_singleton
if _translation_singleton is None:
domain = mygettext.textdomain()
_translation_singleton = mygettext.translation(
domain, mygettext.bindtextdomain(domain), fallback=True)
return _translation_singleton
def unicode_gettext(translation, message):
return getattr(translation, _gettext_method)(message)
def unicode_ngettext(translation, singular, plural, n):
return getattr(translation, _ngettext_method)(singular, plural, n)
def gettext(message):
"""
version of gettext that logs errors but does not crash on incorrect
number of arguments
"""
if message == "":
return ""
translated_msg = unicode_gettext(_translation(), message)
if not _verify(message, translated_msg):
logging.error(
"incorrect translation for message '%s' to '%s' "
"(wrong number of arguments)" % (message, translated_msg))
return message
return translated_msg
def ngettext(msgid1, msgid2, n):
"""
version of ngettext that logs errors but does not crash on incorrect
number of arguments
"""
translated_msg = unicode_ngettext(_translation(), msgid1, msgid2, n)
if not _verify(msgid1, translated_msg):
logging.error(
"incorrect translation for ngettext message "
"'%s' plural: '%s' to '%s' (wrong number of arguments)" % (
msgid1, msgid2, translated_msg))
# dumb fallback to not crash
if n == 1:
return msgid1
return msgid2
return translated_msg
./DistUpgradeMain.py 0000644 0003721 0004705 00000021442 15000447260 014101 0 ustar buildd buildd # DistUpgradeMain.py
#
# Copyright (c) 2004-2008 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt
import gettext
import glob
import logging
import os
import shutil
import subprocess
import sys
from datetime import datetime
from optparse import OptionParser
from gettext import gettext as _
# dirs that the packages will touch, this is needed for the coherence check
# before the upgrade
SYSTEM_DIRS = ["/bin",
"/boot",
"/etc",
"/initrd",
"/lib",
"/lib32", # ???
"/lib64", # ???
"/sbin",
"/usr",
"/var",
]
from .DistUpgradeConfigParser import DistUpgradeConfig
def do_commandline():
" setup option parser and parse the commandline "
parser = OptionParser()
parser.add_option("--frontend", dest="frontend",default=None,
help=_("Use frontend. Currently available: \n"\
"DistUpgradeViewText, DistUpgradeViewGtk, DistUpgradeViewKDE"))
parser.add_option("--mode", dest="mode",default="desktop",
help=_("*DEPRECATED* this option will be ignored"))
parser.add_option("--partial", dest="partial", default=False,
action="store_true",
help=_("Perform a partial upgrade only (no sources.list rewriting)"))
parser.add_option("--disable-gnu-screen", action="store_true",
default=False,
help=_("Disable GNU screen support"))
parser.add_option("--datadir", dest="datadir", default=".",
help=_("Set datadir"))
parser.add_option("--devel-release", action="store_true",
dest="devel_release", default=False,
help=_("Upgrade to the development release"))
return parser.parse_args()
def setup_logging(options, config):
" setup the logging "
logdir = config.getWithDefault("Files","LogDir","/var/log/dist-upgrade/")
if not os.path.exists(logdir):
os.mkdir(logdir)
# check if logs exists and move logs into place
if glob.glob(logdir+"/*.log"):
now = datetime.now()
backup_dir = logdir+"/%04i%02i%02i-%02i%02i" % (now.year,now.month,now.day,now.hour,now.minute)
if not os.path.exists(backup_dir):
os.mkdir(backup_dir)
for f in glob.glob(logdir+"/*.log"):
shutil.move(f, os.path.join(backup_dir,os.path.basename(f)))
fname = os.path.join(logdir,"main.log")
# do not overwrite the default main.log
if options.partial:
fname += ".partial"
with open(fname, "a"):
pass
logging.basicConfig(level=logging.DEBUG,
filename=fname,
format='%(asctime)s %(levelname)s %(message)s',
filemode='w')
# log what config files are in use here to detect user
# changes
logging.info("Using config files '%s'" % config.config_files)
logging.info("uname information: '%s'" % " ".join(os.uname()))
cache = apt.apt_pkg.Cache(None)
apt_version = cache['apt'].current_ver.ver_str
logging.info("apt version: '%s'" % apt_version)
logging.info("python version: '%s'" % sys.version)
return logdir
def save_system_state(logdir):
# save package state to be able to re-create failures
try:
from apt_clone import AptClone
except ImportError:
logging.error("failed to import AptClone")
return
target = os.path.join(logdir, "apt-clone_system_state.tar.gz")
logging.debug("creating statefile: '%s'" % target)
# this file may contain sensitive data so ensure we create with the
# right umask
old_umask = os.umask(0o0066)
clone = AptClone()
clone.save_state(sourcedir="/", target=target, with_dpkg_status=True,
scrub_sources=True)
# reset umask
os.umask(old_umask)
# lspci output
try:
s=subprocess.Popen(["lspci","-nn"], stdout=subprocess.PIPE,
universal_newlines=True).communicate()[0]
with open(os.path.join(logdir, "lspci.txt"), "w") as f:
f.write(s)
except OSError as e:
logging.debug("lspci failed: %s" % e)
def setup_view(options, config, logdir):
" setup view based on the config and commandline "
# the commandline overwrites the configfile
for requested_view in [options.frontend]+config.getlist("View","View"):
if not requested_view:
continue
try:
# this should work with py3 and py2.7
from importlib import import_module
# use relative imports
view_modul = import_module("."+requested_view, "DistUpgrade")
# won't work with py3
#view_modul = __import__(requested_view, globals())
view_class = getattr(view_modul, requested_view)
instance = view_class(logdir=logdir, datadir=options.datadir)
break
except Exception as e:
logging.warning("can't import view '%s' (%s)" % (requested_view,e))
print("can't load %s (%s)" % (requested_view, e))
else:
logging.error("No view can be imported, aborting")
print("No view can be imported, aborting")
sys.exit(1)
return instance
def run_new_gnu_screen_window_or_reattach():
""" check if there is a upgrade already running inside gnu screen,
if so, reattach
if not, create new screen window
"""
SCREENNAME = "ubuntu-release-upgrade-screen-window"
# get the active screen sockets
try:
out = subprocess.Popen(
["screen","-ls"], stdout=subprocess.PIPE,
universal_newlines=True).communicate()[0]
logging.debug("screen returned: '%s'" % out)
except OSError:
logging.info("screen could not be run")
return
# check if a release upgrade is among them
if SCREENNAME in out:
logging.info("found active screen session, re-attaching")
# if we have it, attach to it
os.execv("/usr/bin/screen", ["screen", "-d", "-r", "-p", SCREENNAME])
# otherwise re-exec inside screen with (-L) for logging enabled
os.environ["RELEASE_UPGRADER_NO_SCREEN"]="1"
# unset escape key to avoid confusing people who are not used to
# screen. people who already run screen will not be affected by this
# unset escape key with -e, enable log with -L, set name with -S
cmd = ["screen",
"-e", "\\0\\0",
"-c", "screenrc",
"-S", SCREENNAME]+sys.argv
logging.info("re-exec inside screen: '%s'" % cmd)
os.execv("/usr/bin/screen", cmd)
def main():
""" main method """
# commandline setup and config
(options, args) = do_commandline()
config = DistUpgradeConfig(options.datadir)
logdir = setup_logging(options, config)
from .DistUpgradeVersion import VERSION
logging.info("release-upgrader version '%s' started" % VERSION)
# ensure that DistUpgradeView translations are displayed
gettext.textdomain("ubuntu-release-upgrader")
if options.datadir is None or options.datadir == '.':
localedir = os.path.join(os.getcwd(), "mo")
gettext.bindtextdomain("ubuntu-release-upgrader", localedir)
# create view and app objects
view = setup_view(options, config, logdir)
# gnu screen support
if (view.needs_screen and
not "RELEASE_UPGRADER_NO_SCREEN" in os.environ and
not options.disable_gnu_screen):
run_new_gnu_screen_window_or_reattach()
# A reboot is required at the end of the upgrade,
# so there is no need to run needrestart during upgrade.
if not os.getenv('NEEDRESTART_SUSPEND'):
os.environ['NEEDRESTART_SUSPEND'] = 'y'
from .DistUpgradeController import DistUpgradeController
app = DistUpgradeController(view, options, datadir=options.datadir)
# partial upgrade only
if options.partial:
if not app.doPartialUpgrade():
sys.exit(1)
sys.exit(0)
# save system state (only if not doing just a partial upgrade)
save_system_state(logdir)
# full upgrade, return error code for success/failure
if app.run():
return 0
return 1
./DistUpgradeQuirks.py 0000644 0003721 0004705 00000135171 15000447260 014500 0 ustar buildd buildd # DistUpgradeQuirks.py
#
# Copyright (c) 2004-2010 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt
import atexit
import distro_info
import glob
import logging
import os
import re
import subprocess
import pathlib
from subprocess import PIPE, Popen
from .DistUpgradeGettext import gettext as _
class DistUpgradeQuirks(object):
"""
This class collects the various quirks handlers that can
be hooked into to fix/work around issues that the individual
releases have.
The following handlers are supported:
- PreCacheOpen: run *before* the apt cache is opened the first time
to set options that affect the cache
- PostInitialUpdate: run *before* the sources.list is rewritten but
after an initial apt-get update
- PreDistUpgradeCache: run *right before* the dist-upgrade is
calculated in the cache
- PostDistUpgradeCache: run *after* the dist-upgrade was calculated
in the cache
- StartUpgrade: before the first package gets installed (but the
download is finished)
- PostUpgrade: run *after* the upgrade is finished successfully and
packages got installed
- PostCleanup: run *after* the cleanup (orphaned etc) is finished
"""
def __init__(self, controller, config):
self.controller = controller
self._view = controller._view
self.config = config
self.uname = Popen(["uname", "-r"], stdout=PIPE,
universal_newlines=True).communicate()[0].strip()
self.extra_snap_space = 0
self._poke = None
self._snapstore_reachable = False
self._snap_list = None
self._did_change_font = False
# individual quirks handler that run *before* the cache is opened
def PreCacheOpen(self):
""" run before the apt cache is opened the first time """
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
logging.debug("running Quirks.PreCacheOpen")
self._add_apport_ignore_list()
# individual quirks handler that run *after* the cache is opened
def PostInitialUpdate(self):
# PreCacheOpen would be better but controller.abort fails terribly
""" run after the apt cache is opened the first time """
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
logging.debug("running Quirks.PostInitialUpdate")
self._test_and_fail_on_tpm_fde()
cache = self.controller.cache
self._test_and_warn_if_ros_installed(cache)
self._maybe_prevent_flatpak_auto_removal()
if 'snapd' not in cache:
logging.debug("package required for Quirk not in cache")
return
if cache['snapd'].is_installed and \
(os.path.exists('/run/snapd.socket') or
os.path.exists('/run/snapd-snap.socket')):
self._checkStoreConnectivity()
# If the snap store is accessible, at the same time calculate the
# extra size needed by to-be-installed snaps. This also prepares
# the snaps-to-install list for the actual upgrade.
if self._snapstore_reachable:
self._calculateSnapSizeRequirements()
def PostUpgrade(self):
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
logging.debug("running Quirks.PostUpgrade")
cache = self.controller.cache
if 'snapd' not in cache:
logging.debug("package required for Quirk not in cache")
return
if cache['snapd'].is_installed and \
self._snap_list:
self._replaceDebsAndSnaps()
if 'ubuntu-desktop-raspi' in cache:
if cache['ubuntu-desktop-raspi'].is_installed:
self._replace_fkms_overlay()
if 'ubuntu-server-raspi' in cache:
if cache['ubuntu-server-raspi'].is_installed:
self._add_kms_overlay()
# individual quirks handler when the dpkg run is finished ---------
def PostCleanup(self):
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
" run after cleanup "
logging.debug("running Quirks.PostCleanup")
self._remove_apport_ignore_list()
# Try to refresh snaps, but ignore errors.
try:
subprocess.check_call(['snap', 'refresh'])
except Exception as e:
logging.debug(f'Failed to refresh snaps : {e}')
# run right before the first packages get installed
def StartUpgrade(self):
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
logging.debug("running Quirks.StartUpgrade")
cache = self.controller.cache
self._removeOldApportCrashes()
self._killUpdateNotifier()
self._pokeScreensaver()
self._set_generic_font()
self._disable_kdump_tools_on_install(cache)
# individual quirks handler that run *right before* the dist-upgrade
# is calculated in the cache
def PreDistUpgradeCache(self):
""" run right before calculating the dist-upgrade """
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
logging.debug("running Quirks.PreDistUpgradeCache")
self._maybe_remove_gpg_wks_server()
self._install_linux_sysctl_defaults()
# individual quirks handler that run *after* the dist-upgrade was
# calculated in the cache
def PostDistUpgradeCache(self):
""" run after calculating the dist-upgrade """
# we do not run any quirks in partialUpgrade mode
if self.controller._partialUpgrade:
logging.info("not running quirks in partialUpgrade mode")
return
logging.debug("running Quirks.PostDistUpgradeCache")
self._install_linux_metapackage()
def _test_and_warn_if_ros_installed(self, cache):
"""
Test and warn if ROS is installed. A given ROS release only
supports specific Ubuntu releases, and can cause the upgrade
to fail in an overly-cryptic manner.
"""
# These are the root ROS 1 and 2 dependencies as of 07/27/2020
ros_package_patterns = set()
for package_name in (
"catkin",
"rosboost-cfg",
"rosclean",
"ros-environment",
"ros-workspace"):
ros_package_patterns.add(
re.compile(r"ros-[^\-]+-%s" % package_name))
ros_is_installed = False
for pkg in cache:
if ros_is_installed:
break
for pattern in ros_package_patterns:
if pattern.match(pkg.name):
if pkg.is_installed or pkg.marked_install:
ros_is_installed = True
break
if ros_is_installed:
res = self._view.askYesNoQuestion(
_("The Robot Operating System (ROS) is installed"),
_("It appears that ROS is currently installed. Each ROS "
"release is very strict about the versions of Ubuntu "
"it supports, and Ubuntu upgrades can fail if that "
"guidance isn't followed. Before continuing, please "
"either uninstall ROS, or ensure the ROS release you "
"have installed supports the version of Ubuntu to "
"which you're upgrading.\n\n"
"For ROS 1 releases, refer to REP 3:\n"
"https://www.ros.org/reps/rep-0003.html\n\n"
"For ROS 2 releases, refer to REP 2000:\n"
"https://www.ros.org/reps/rep-2000.html\n\n"
"Are you sure you want to continue?"))
if not res:
self.controller.abort()
def _maybe_prevent_flatpak_auto_removal(self):
"""
If flatpak is installed, and there are either active remotes, or
flatpak apps installed, prevent flatpak's auto-removal on upgrade.
"""
prevent_auto_removal = False
if "flatpak" not in self.controller.cache:
return
if not self.controller.cache["flatpak"].is_installed:
return
if not os.path.exists("/usr/bin/flatpak"):
return
for subcmd in ["remotes", "list"]:
r = subprocess.run(
["/usr/bin/flatpak", subcmd],
stdout=subprocess.PIPE
)
if r.stdout.decode("utf-8").strip():
prevent_auto_removal = True
break
logging.debug("flatpak will{}be marked as manually installed"
.format(" " if prevent_auto_removal else " NOT "))
if not prevent_auto_removal:
return
self.controller.cache["flatpak"].mark_auto(auto=False)
for pkg in ("plasma-discover-backend-flatpak",
"gnome-software-plugin-flatpak"):
if pkg not in self.controller.cache:
continue
if not self.controller.cache[pkg].is_installed:
continue
logging.debug("{} will be marked as manually installed"
.format(pkg))
self.controller.cache[pkg].mark_auto(auto=False)
self.controller.cache.commit(
self._view.getAcquireProgress(),
self._view.getInstallProgress(self.controller.cache)
)
def _add_apport_ignore_list(self):
ignore_list = [
'/usr/libexec/tracker-extract-3', # LP: #2012638
'/usr/sbin/update-apt-xapian-index', # LP: #2058227
]
path = '/etc/apport/blacklist.d/upgrade-quirks-ignore-list'
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
for bin in ignore_list:
f.write(f'{bin}\n')
except Exception as e:
logging.debug(f'Failed to create {path}: {e}')
def _remove_apport_ignore_list(self):
path = '/etc/apport/blacklist.d/upgrade-quirks-ignore-list'
try:
os.remove(path)
except Exception as e:
logging.debug(f'Failed to remove {path}: {e}')
def _killUpdateNotifier(self):
"kill update-notifier"
# kill update-notifier now to suppress reboot required
if os.path.exists("/usr/bin/killall"):
logging.debug("killing update-notifier")
subprocess.call(["killall", "-q", "update-notifier"])
def _pokeScreensaver(self):
if (os.path.exists("/usr/bin/xdg-screensaver") and
os.environ.get('DISPLAY')):
logging.debug("setup poke timer for the screensaver")
cmd = "while true;"
cmd += " do /usr/bin/xdg-screensaver reset >/dev/null 2>&1;"
cmd += " sleep 30; done"
try:
self._poke = subprocess.Popen(cmd, shell=True)
atexit.register(self._stopPokeScreensaver)
except (OSError, ValueError):
logging.exception("failed to setup screensaver poke")
def _stopPokeScreensaver(self):
res = False
if self._poke is not None:
try:
self._poke.terminate()
res = self._poke.wait()
except OSError:
logging.exception("failed to stop screensaver poke")
self._poke = None
return res
def _removeOldApportCrashes(self):
" remove old apport crash files and whoopsie control files "
try:
for ext in ['.crash', '.upload', '.uploaded']:
for f in glob.glob("/var/crash/*%s" % ext):
logging.debug("removing old %s file '%s'" % (ext, f))
os.unlink(f)
except Exception as e:
logging.warning("error during unlink of old crash files (%s)" % e)
def _checkStoreConnectivity(self):
""" check for connectivity to the snap store to install snaps"""
res = False
snap_env = os.environ.copy()
snap_env["LANG"] = "C.UTF-8"
try:
connected = Popen(["snap", "debug", "connectivity"], stdout=PIPE,
stderr=PIPE, env=snap_env,
universal_newlines=True).communicate()
except Exception as e:
logging.debug(
f'Failed to check snap store connectivity, assuming none: {e}'
)
self._snapstore_reachable = False
return
if re.search(r"^ \* PASS", connected[0], re.MULTILINE):
self._snapstore_reachable = True
return
# can't connect
elif re.search(r"^ \*.*unreachable", connected[0], re.MULTILINE):
logging.error("No snap store connectivity")
old_lxd_deb_installed = False
cache = self.controller.cache
if 'lxd' in cache:
# epoch 1 is the transitional deb
if cache['lxd'].is_installed and not \
cache['lxd'].candidate.version.startswith("1:"):
logging.error("lxd is installed")
old_lxd_deb_installed = True
if old_lxd_deb_installed:
summary = _("Connection to the Snap Store failed")
msg = _("You have the package lxd installed but your "
"system is unable to reach the Snap Store. "
"lxd is now provided via a snap and the release "
"upgrade will fail if snapd is not functional. "
"Please make sure you're connected to the "
"Internet and update any firewall or proxy "
"settings as needed so that you can reach "
"api.snapcraft.io. If you are an enterprise "
"with a firewall setup you may want to configure "
"a Snap Store proxy."
)
self._view.error(summary, msg)
self.controller.abort()
else:
res = self._view.askYesNoQuestion(
_("Connection to Snap Store failed"),
_("Your system does not have a connection to the Snap "
"Store. For the best upgrade experience make sure "
"that your system can connect to api.snapcraft.io.\n"
"Do you still want to continue with the upgrade?")
)
# debug command not available
elif 'error: unknown command' in connected[1]:
logging.error("snap debug command not available")
res = self._view.askYesNoQuestion(
_("Outdated snapd package"),
_("Your system does not have the latest version of snapd. "
"Please update the version of snapd on your system to "
"improve the upgrade experience.\n"
"Do you still want to continue with the upgrade?")
)
# not running as root
elif 'error: access denied' in connected[1]:
res = False
logging.error("Not running as root!")
else:
logging.error("Unhandled error connecting to the snap store.")
if not res:
self.controller.abort()
def _calculateSnapSizeRequirements(self):
import json
import urllib.request
from urllib.error import URLError
# first fetch the list of snap-deb replacements that will be needed
# and store them for future reference, along with other data we'll
# need in the process
self._prepare_snap_replacement_data()
# now perform direct API calls to the store, requesting size
# information for each of the snaps needing installation
self._view.updateStatus(_("Calculating snap size requirements"))
for snap, snap_object in self._snap_list.items():
if snap_object['command'] != 'install':
continue
action = {
"instance-key": "upgrade-size-check",
"action": "download",
"snap-id": snap_object['snap-id'],
"channel": snap_object['channel'],
}
data = {
"context": [],
"actions": [action],
}
req = urllib.request.Request(
url='https://api.snapcraft.io/v2/snaps/refresh',
data=bytes(json.dumps(data), encoding='utf-8'))
req.add_header('Snap-Device-Series', '16')
req.add_header('Content-type', 'application/json')
req.add_header('Snap-Device-Architecture', self.controller.arch)
try:
response = urllib.request.urlopen(req).read()
info = json.loads(response)
size = int(info['results'][0]['snap']['download']['size'])
except (KeyError, URLError, ValueError):
logging.debug("Failed fetching size of snap %s" % snap)
continue
self.extra_snap_space += size
def _replaceDebsAndSnaps(self):
""" install a snap and mark its corresponding package for removal """
self._view.updateStatus(_("Processing snap replacements"))
# _snap_list should be populated by the earlier
# _calculateSnapSizeRequirements call.
for snap, snap_object in self._snap_list.items():
command = snap_object['command']
if command == 'switch':
channel = snap_object['channel']
self._view.updateStatus(
_("switching channel for snap %s" % snap)
)
popenargs = ["snap", command, "--channel", channel, snap]
elif command == 'remove':
self._view.updateStatus(_("removing snap %s" % snap))
popenargs = ["snap", command, snap]
else:
self._view.updateStatus(_("installing snap %s" % snap))
popenargs = ["snap", command,
"--channel", snap_object['channel'], snap]
try:
self._view.processEvents()
proc = subprocess.run(
popenargs,
stdout=subprocess.PIPE,
check=True)
self._view.processEvents()
except subprocess.CalledProcessError:
logging.debug("%s of snap %s failed" % (command, snap))
continue
if proc.returncode == 0:
logging.debug("%s of snap %s succeeded" % (command, snap))
if command == 'install' and snap_object['deb']:
self.controller.forced_obsoletes.append(snap_object['deb'])
def _is_greater_than(self, term1, term2):
""" copied from ubuntu-drivers common """
# We don't want to take into account
# the flavour
pattern = re.compile('(.+)-([0-9]+)-(.+)')
match1 = pattern.match(term1)
match2 = pattern.match(term2)
if match1 and match2:
term1 = '%s-%s' % (match1.group(1),
match1.group(2))
term2 = '%s-%s' % (match2.group(1),
match2.group(2))
logging.debug('Comparing %s with %s' % (term1, term2))
return apt.apt_pkg.version_compare(term1, term2) > 0
def _get_linux_metapackage(self, cache, headers):
""" Get the linux headers or linux metapackage
copied from ubuntu-drivers-common
"""
suffix = headers and '-headers' or ''
pattern = re.compile('linux-image-(.+)-([0-9]+)-(.+)')
source_pattern = re.compile('linux-(.+)')
metapackage = ''
version = ''
for pkg in cache:
if ('linux-image' in pkg.name and 'extra' not in pkg.name and
(pkg.is_installed or pkg.marked_install)):
match = pattern.match(pkg.name)
# Here we filter out packages such as
# linux-generic-lts-quantal
if match:
source = pkg.candidate.record['Source']
current_version = '%s-%s' % (match.group(1),
match.group(2))
# See if the current version is greater than
# the greatest that we've found so far
if self._is_greater_than(current_version,
version):
version = current_version
match_source = source_pattern.match(source)
# Set the linux-headers metapackage
if '-lts-' in source and match_source:
# This is the case of packages such as
# linux-image-3.5.0-18-generic which
# comes from linux-lts-quantal.
# Therefore the linux-headers-generic
# metapackage would be wrong here and
# we should use
# linux-headers-generic-lts-quantal
# instead
metapackage = 'linux%s-%s-%s' % (
suffix,
match.group(3),
match_source.group(1))
else:
# The scheme linux-headers-$flavour works
# well here
metapackage = 'linux%s-%s' % (
suffix,
match.group(3))
return metapackage
def _install_linux_metapackage(self):
""" Ensure the linux metapackage is installed for the newest_kernel
installed. (LP: #1509305)
"""
cache = self.controller.cache
linux_metapackage = self._get_linux_metapackage(cache, False)
# Seen on errors.u.c with linux-rpi2 metapackage
# https://errors.ubuntu.com/problem/994bf05fae85fbcd44f721495db6518f2d5a126d
if linux_metapackage not in cache:
logging.info("linux metapackage (%s) not available" %
linux_metapackage)
return
# install the package if it isn't installed
if not cache[linux_metapackage].is_installed:
logging.info("installing linux metapackage: %s" %
linux_metapackage)
reason = "linux metapackage may have been accidentally uninstalled"
cache.mark_install(linux_metapackage, reason)
def ensure_recommends_are_installed_on_desktops(self):
""" ensure that on a desktop install recommends are installed
(LP: #759262)
"""
if not self.controller.serverMode:
if not apt.apt_pkg.config.find_b("Apt::Install-Recommends"):
msg = "Apt::Install-Recommends was disabled,"
msg += " enabling it just for the upgrade"
logging.warning(msg)
apt.apt_pkg.config.set("Apt::Install-Recommends", "1")
def _is_deb2snap_metapkg_installed(self, deb2snap_entry):
""" Helper function that checks if the given deb2snap entry
has at least one metapkg which is installed on the system.
"""
metapkg_list = deb2snap_entry.get("metapkg", None)
if not isinstance(metapkg_list, list):
metapkg_list = [metapkg_list]
for metapkg in metapkg_list:
if metapkg not in self.controller.cache:
continue
if metapkg and \
self.controller.cache[metapkg].is_installed is False:
continue
return True
return False
def _parse_deb2snap_json(self):
import json
seeded_snaps = {}
unseeded_snaps = {}
try:
deb2snap_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'deb2snap.json'
)
with open(deb2snap_path, 'r') as f:
d2s = json.loads(f.read())
for snap in d2s["seeded"]:
seed = d2s["seeded"][snap]
if not self._is_deb2snap_metapkg_installed(seed):
continue
deb = seed.get("deb", None)
# Support strings like stable/ubuntu-{FROM_VERSION} in
# deb2snap.json so that (a) we don't need to update the file
# for every release, and (b) so that from_channel is not bound
# to only one release.
from_chan = seed.get(
'from_channel',
'stable/ubuntu-{FROM_VERSION}'
).format(
FROM_VERSION=self.controller.fromVersion
)
to_chan = seed.get(
'to_channel',
'stable/ubuntu-{TO_VERSION}'
).format(
TO_VERSION=self.controller.toVersion
)
force_switch = seed.get("force_switch", False)
seeded_snaps[snap] = (deb, from_chan, to_chan, force_switch)
for snap in d2s["unseeded"]:
unseed = d2s["unseeded"][snap]
deb = unseed.get("deb", None)
if not self._is_deb2snap_metapkg_installed(unseed):
continue
from_chan = seed.get(
'from_channel',
'stable/ubuntu-{FROM_VERSION}'
).format(
FROM_VERSION=self.controller.fromVersion
)
unseeded_snaps[snap] = (deb, from_chan)
except Exception as e:
logging.warning("error reading deb2snap.json file (%s)" % e)
return seeded_snaps, unseeded_snaps
def _prepare_snap_replacement_data(self):
""" Helper function fetching all required info for the deb-to-snap
migration: version strings for upgrade (from and to) and the list
of snaps (with actions).
"""
self._snap_list = {}
seeded_snaps, unseeded_snaps = self._parse_deb2snap_json()
snap_list = ''
# list the installed snaps and add them to seeded ones
snap_list = subprocess.Popen(["snap", "list"],
universal_newlines=True,
stdout=subprocess.PIPE).communicate()
if snap_list:
# first line of output is a header and the last line is empty
snaps_installed = [line.split()[0]
for line in snap_list[0].split('\n')[1:-1]]
for snap in snaps_installed:
if snap in seeded_snaps or snap in unseeded_snaps:
continue
else:
seeded_snaps[snap] = (
None,
f'stable/ubuntu-{self.controller.fromVersion}',
f'stable/ubuntu-{self.controller.toVersion}',
False
)
self._view.updateStatus(_("Checking for installed snaps"))
for snap, props in seeded_snaps.items():
(deb, from_channel, to_channel, force_switch) = props
snap_object = {}
# check to see if the snap is already installed
snap_info = subprocess.Popen(["snap", "info", snap],
universal_newlines=True,
stdout=subprocess.PIPE).communicate()
self._view.processEvents()
if re.search(r"^installed: ", snap_info[0], re.MULTILINE):
logging.debug("Snap %s is installed" % snap)
if not re.search(r"^tracking:.*%s" % from_channel,
snap_info[0], re.MULTILINE):
logging.debug("Snap %s is not tracking the release channel"
% snap)
if not force_switch:
# It is not tracking the release channel, and we were
# not told to force so don't switch.
continue
snap_object['command'] = 'switch'
else:
# Do not replace packages not installed
cache = self.controller.cache
if (deb and (deb not in cache or not cache[deb].is_installed)):
logging.debug("Deb package %s is not installed. Skipping "
"snap package %s installation" % (deb, snap))
continue
match = re.search(r"snap-id:\s*(\w*)", snap_info[0])
if not match:
logging.debug("Could not parse snap-id for the %s snap"
% snap)
continue
snap_object['command'] = 'install'
snap_object['deb'] = deb
snap_object['snap-id'] = match[1]
snap_object['channel'] = to_channel
self._snap_list[snap] = snap_object
for snap, (deb, from_channel) in unseeded_snaps.items():
snap_object = {}
# check to see if the snap is already installed
snap_info = subprocess.Popen(["snap", "info", snap],
universal_newlines=True,
stdout=subprocess.PIPE).communicate()
self._view.processEvents()
if re.search(r"^installed: ", snap_info[0], re.MULTILINE):
logging.debug("Snap %s is installed" % snap)
# its not tracking the release channel so don't remove
if not re.search(r"^tracking:.*%s" % from_channel,
snap_info[0], re.MULTILINE):
logging.debug("Snap %s is not tracking the release channel"
% snap)
continue
snap_object['command'] = 'remove'
# check if this snap is being used by any other snaps
conns = subprocess.Popen(["snap", "connections", snap],
universal_newlines=True,
stdout=subprocess.PIPE).communicate()
self._view.processEvents()
for conn in conns[0].split('\n'):
conn_cols = conn.split()
if len(conn_cols) != 4:
continue
plug = conn_cols[1]
slot = conn_cols[2]
if slot.startswith(snap + ':'):
plug_snap = plug.split(':')[0]
if plug_snap != '-' and \
plug_snap not in unseeded_snaps:
logging.debug("Snap %s is being used by %s. "
"Switching it to stable track"
% (snap, plug_snap))
snap_object['command'] = 'switch'
snap_object['channel'] = 'stable'
break
self._snap_list[snap] = snap_object
return self._snap_list
def _replace_pi_boot_config(self, old_config, new_config,
boot_config_filename, failure_action):
try:
boot_backup_filename = boot_config_filename + '.distUpgrade'
with open(boot_backup_filename, 'w', encoding='utf-8') as f:
f.write(old_config)
except IOError as exc:
logging.error("unable to write boot config backup to %s: %s; %s",
boot_backup_filename, exc, failure_action)
return
try:
with open(boot_config_filename, 'w', encoding='utf-8') as f:
f.write(new_config)
except IOError as exc:
logging.error("unable to write new boot config to %s: %s; %s",
boot_config_filename, exc, failure_action)
def _replace_fkms_overlay(self, boot_dir='/boot/firmware'):
failure_action = (
"You may need to replace the vc4-fkms-v3d overlay with "
"vc4-kms-v3d in config.txt on your boot partition")
try:
boot_config_filename = os.path.join(boot_dir, 'config.txt')
with open(boot_config_filename, 'r', encoding='utf-8') as f:
boot_config = f.read()
except FileNotFoundError:
logging.error("failed to open boot configuration in %s; %s",
boot_config_filename, failure_action)
return
new_config = ''.join(
# startswith and replace used to cope with (and preserve) any
# trailing d-t parameters, and any use of the -pi4 suffix
'# changed by do-release-upgrade (LP: #1923673)\n#' + line +
line.replace('dtoverlay=vc4-fkms-v3d', 'dtoverlay=vc4-kms-v3d')
if line.startswith('dtoverlay=vc4-fkms-v3d') else
# camera firmware disabled due to incompatibility with "full" kms
# overlay; without the camera firmware active it's also better to
# disable gpu_mem leaving the default (64MB) to allow as much as
# possible for the KMS driver
'# disabled by do-release-upgrade (LP: #1923673)\n#' + line
if line.startswith('gpu_mem=') or line.rstrip() == 'start_x=1' else
line
for line in boot_config.splitlines(keepends=True)
)
if new_config == boot_config:
logging.warning("no fkms overlay or camera firmware line found "
"in %s", boot_config_filename)
return
self._replace_pi_boot_config(
boot_config, new_config, boot_config_filename, failure_action)
def _add_kms_overlay(self, boot_dir='/boot/firmware'):
failure_action = (
"You may need to add dtoverlay=vc4-kms-v3d to an [all] section "
"in config.txt on your boot partition")
added_lines = [
'# added by do-release-upgrade (LP: #2065051)',
'dtoverlay=vc4-kms-v3d',
'disable_fw_kms_setup=1',
'',
'[pi3+]',
'dtoverlay=vc4-kms-v3d,cma-128',
'',
'[pi02]',
'dtoverlay=vc4-kms-v3d,cma-128',
'',
'[all]',
]
try:
boot_config_filename = os.path.join(boot_dir, 'config.txt')
with open(boot_config_filename, 'r', encoding='utf-8') as f:
boot_config = f.read()
except FileNotFoundError:
logging.error("failed to open boot configuration in %s; %s",
boot_config_filename, failure_action)
return
def find_insertion_point(lines):
# Returns the zero-based index of the dtoverlay=vc4-kms-v3d line in
# an [all] section, if one exists, or the last line of the last
# [all] section of the file, if one does not exist
in_all = True
last = 0
for index, line in enumerate(lines):
line = line.rstrip()
if in_all:
last = index
# startswith used to cope with any trailing dtparams
if line.startswith('dtoverlay=vc4-kms-v3d'):
return last
elif line.startswith('[') and line.endswith(']'):
in_all = line == '[all]'
elif line.startswith('include '):
# [sections] are included from includes verbatim, hence
# (without reading the included file) we must assume
# we're no longer in an [all] section
in_all = False
else:
in_all = line == '[all]'
return last
def add_kms_overlay(lines):
insert_point = find_insertion_point(lines)
try:
if lines[insert_point].startswith('dtoverlay=vc4-kms-v3d'):
return lines
except IndexError:
# Empty config, apparently!
pass
lines[insert_point:insert_point] = added_lines
return lines
lines = [line.rstrip() for line in boot_config.splitlines()]
lines = add_kms_overlay(lines)
new_config = ''.join(line + '\n' for line in lines)
if new_config == boot_config:
logging.warning("no addition of KMS overlay required in %s",
boot_config_filename)
return
self._replace_pi_boot_config(
boot_config, new_config, boot_config_filename, failure_action)
def _set_generic_font(self):
""" Due to changes to the Ubuntu font we enable a generic font
(in practice DejaVu or Noto) during the upgrade.
See https://launchpad.net/bugs/2034986
"""
temp_font = 'Sans'
if self._did_change_font:
return
if self.controller.get_user_env('XDG_SESSION_TYPE', '') in ('', 'tty'):
# Avoid running this on server systems or when the upgrade
# is done over ssh.
return
if self.controller.get_user_uid() is None:
logging.debug(
'Cannot determine non-root UID, will not change font'
)
return
schema = 'org.gnome.desktop.interface'
desktops = self.controller.get_user_env(
'XDG_CURRENT_DESKTOP', ''
).split(':')
# Some flavors use other schemas for the desktop font.
if 'MATE' in desktops or 'UKUI' in desktops:
schema = 'org.mate.interface'
elif 'X-Cinnamon' in desktops:
schema = 'org.cinnamon.desktop.interface'
# Some flavors lack the systemd integration needed for a
# user service, so we create an autostart file instead.
use_autostart = bool(
set(['Budgie', 'LXQt', 'MATE', 'UKUI', 'X-Cinnamon', 'XFCE'])
& set(desktops)
)
r = self.controller.run_as_user(
['/usr/bin/gsettings', 'get',
f'{schema}', 'font-name'],
stdout=subprocess.PIPE,
encoding='utf-8',
)
(font, _, size) = r.stdout.strip('\'\n').rpartition(' ')
font = font or 'Ubuntu'
try:
int(size)
except ValueError:
size = '11'
logging.debug(f'Setting generic font {temp_font} {size} during the '
f'upgrade. Original font is {font} {size}.')
r = self.controller.run_as_user([
'/usr/bin/gsettings', 'set', f'{schema}',
'font-name', f'"{temp_font} {size}"'
])
if r.returncode != 0:
logging.debug(f'Failed to change font to {temp_font} {size}')
return
self._did_change_font = True
# Touch a file to indiate that the font should be restored on the next
# boot.
need_font_restore_file = os.path.join(
self.controller.get_user_home(),
'.config/upgrade-need-font-restore'
)
os.makedirs(os.path.dirname(need_font_restore_file), exist_ok=True)
pathlib.Path(need_font_restore_file).touch(mode=0o666)
os.chown(
need_font_restore_file,
self.controller.get_user_uid(),
self.controller.get_user_gid(),
)
if use_autostart:
autostart_file = '/etc/xdg/autostart/upgrade-restore-font.desktop'
os.makedirs(os.path.dirname(autostart_file), exist_ok=True)
flag = '$HOME/.config/upgrade-need-font-restore'
with open(autostart_file, 'w') as f:
f.write(
'[Desktop Entry]\n'
'Name=Restore font after upgrade\n'
'Comment=Auto-generated by ubuntu-release-upgrader\n'
'Type=Application\n'
f'Exec=sh -c \'if [ -e "{flag}" ]; then gsettings set '
f'{schema} font-name "{font} {size}";'
f'rm -f "{flag}"; fi\'\n'
'NoDisplay=true\n'
)
return
# If we set the font back to normal before a reboot, the font will
# still get all messed up. To allow normal usage whether the user
# reboots immediately or not, create a service that will run only if a
# ~/.config/upgrade-need-font-restore exists, and then remove that file
# in ExecStart. This has the effect of creating a one-time service on
# the next boot.
unit_file = '/usr/lib/systemd/user/upgrade-restore-font.service'
os.makedirs(os.path.dirname(unit_file), exist_ok=True)
with open(unit_file, 'w') as f:
f.write(
'# Auto-generated by ubuntu-release-upgrader\n'
'[Unit]\n'
'Description=Restore font after upgrade\n'
'After=graphical-session.target dconf.service\n'
'ConditionPathExists=%h/.config/upgrade-need-font-restore\n'
'\n'
'[Service]\n'
'Type=oneshot\n'
'ExecStart=/usr/bin/gsettings set '
f'{schema} font-name \'{font} {size}\'\n'
'ExecStart=/usr/bin/rm -f '
'%h/.config/upgrade-need-font-restore\n'
'\n'
'[Install]\n'
'WantedBy=graphical-session.target\n'
)
self.controller.systemctl_as_user(['daemon-reload'])
r = self.controller.systemctl_as_user(
['enable', os.path.basename(unit_file)]
)
if r.returncode != 0:
logging.debug(f'Failed to enable {os.path.basename(unit_file)}. '
'Font will not be restored on reboot')
def _maybe_remove_gpg_wks_server(self):
"""
Prevent postfix from being unnecessarily installed, and leading to a
debconf prompt (LP: #2060578).
"""
# We want to use attributes of the cache that are only exposed in
# apt_pkg.Cache, not the higher level apt.Cache. Hence we operate on
# apt.Cache._cache.
cache = self.controller.cache._cache
try:
if not cache['gpg-wks-server'].current_ver:
# Not installed, nothing to do.
return
provides_mta = cache['mail-transport-agent'].provides_list
installed_mta = [
ver for _, _, ver in provides_mta
if ver.parent_pkg.current_ver
]
except KeyError:
# Something wasn't in the cache, ignore.
return
if not any(installed_mta):
logging.info(
'No mail-transport-agent installed, '
'marking gpg-wks-server for removal'
)
self.controller.cache['gpg-wks-server'].mark_delete(auto_fix=False)
apt.ProblemResolver(self.controller.cache).protect(
self.controller.cache['gpg-wks-server']
)
def _test_and_fail_on_tpm_fde(self):
"""
LP: #2065229
"""
if (
os.path.exists('/snap/pc-kernel') and
'ubuntu-desktop-minimal' in self.controller.cache and
self.controller.cache['ubuntu-desktop-minimal'].is_installed
):
logging.debug('Detected TPM FDE system')
di = distro_info.UbuntuDistroInfo()
version = di.version(self.controller.toDist) or 'next release'
self._view.error(
_(
f'Sorry, cannot upgrade this system to {version}'
),
_(
'Upgrades for desktop systems running TPM FDE are not '
'currently supported. '
'Please see https://launchpad.net/bugs/2065229 '
'for more information.'
),
)
self.controller.abort()
def _disable_kdump_tools_on_install(self, cache):
"""Disable kdump-tools if installed during upgrade."""
if 'kdump-tools' not in cache:
# Not installed or requested, nothing to do.
return
pkg = cache['kdump-tools']
if pkg.is_installed:
logging.info("kdump-tools already installed. Not disabling.")
return
elif pkg.marked_install:
logging.info("installing kdump-tools due to upgrade. Disabling.")
proc = subprocess.run(
(
'echo "kdump-tools kdump-tools/use_kdump boolean false"'
' | debconf-set-selections'
),
shell=True,
)
ret_code = proc.returncode
if ret_code != 0:
logging.debug(
(
"kdump-tools debconf-set-selections "
f"returned: {ret_code}"
)
)
def _install_linux_sysctl_defaults(self):
""" LP: #2089759 """
if self.controller.fromDist != 'oracular':
return
if (
'linux-sysctl-defaults' in self.controller.cache and
not self.controller.cache['linux-sysctl-defaults'].is_installed
):
logging.debug('Installing linux-sysctl-defaults')
self.controller.cache['linux-sysctl-defaults'].mark_install(
auto_fix=False
)
./DistUpgradeVersion.py 0000644 0003721 0004705 00000000024 15003760707 014642 0 ustar buildd buildd VERSION = '25.10.1'
./DistUpgradeView.py 0000644 0003721 0004705 00000040143 15000447260 014126 0 ustar buildd buildd # DistUpgradeView.py
#
# Copyright (c) 2004,2005 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
from .DistUpgradeGettext import gettext as _
from .DistUpgradeGettext import ngettext
from .telemetry import get as get_telemetry
import apt
from enum import Enum
import errno
import os
import apt_pkg
import locale
import logging
import signal
import select
from .DistUpgradeApport import apport_pkgfailure
try:
locale.setlocale(locale.LC_ALL, "")
(code, ENCODING) = locale.getdefaultlocale()
except:
logging.exception("getting the encoding failed")
ENCODING = "utf-8" #pyflakes
# if there is no encoding, setup UTF-8
if not ENCODING:
ENCODING = "utf-8"
os.putenv("LC_CTYPE", "C.UTF-8")
try:
locale.setlocale(locale.LC_CTYPE, "C.UTF-8")
except locale.error:
pass
# log locale information
logging.info("locale: '%s' '%s'" % locale.getlocale())
def FuzzyTimeToStr(sec):
" return the time a bit fuzzy (no seconds if time > 60 secs "
#print("FuzzyTimeToStr: ", sec)
sec = int(sec)
days = sec//(60*60*24)
hours = sec//(60*60) % 24
minutes = (sec//60) % 60
seconds = sec % 60
# 0 seonds remaining looks wrong and its "fuzzy" anyway
if seconds == 0:
seconds = 1
# string map to make the re-ordering possible
map = { "str_days" : "",
"str_hours" : "",
"str_minutes" : "",
"str_seconds" : ""
}
# get the fragments, this is not ideal i18n wise, but its
# difficult to do it differently
if days > 0:
map["str_days"] = ngettext("%li day","%li days", days) % days
if hours > 0:
map["str_hours"] = ngettext("%li hour","%li hours", hours) % hours
if minutes > 0:
map["str_minutes"] = ngettext("%li minute","%li minutes", minutes) % minutes
map["str_seconds"] = ngettext("%li second","%li seconds", seconds) % seconds
# now assemble the string
if days > 0:
# Don't print str_hours if it's an empty string, see LP: #288912
if map["str_hours"] == '':
return map["str_days"]
# TRANSLATORS: you can alter the ordering of the remaining time
# information here if you shuffle %(str_days)s %(str_hours)s %(str_minutes)s
# around. Make sure to keep all '$(str_*)s' in the translated string
# and do NOT change anything appart from the ordering.
#
# %(str_hours)s will be either "1 hour" or "2 hours" depending on the
# plural form
#
# Note: most western languages will not need to change this
return _("%(str_days)s %(str_hours)s") % map
# display no minutes for time > 3h, see LP: #144455
elif hours > 3:
return map["str_hours"]
# when we are near the end, become more precise again
elif hours > 0:
# Don't print str_minutes if it's an empty string, see LP: #288912
if map["str_minutes"] == '':
return map["str_hours"]
# TRANSLATORS: you can alter the ordering of the remaining time
# information here if you shuffle %(str_hours)s %(str_minutes)s
# around. Make sure to keep all '$(str_*)s' in the translated string
# and do NOT change anything appart from the ordering.
#
# %(str_hours)s will be either "1 hour" or "2 hours" depending on the
# plural form
#
# Note: most western languages will not need to change this
return _("%(str_hours)s %(str_minutes)s") % map
elif minutes > 0:
return map["str_minutes"]
return map["str_seconds"]
class AcquireProgress(apt.progress.base.AcquireProgress):
def __init__(self):
super(AcquireProgress, self).__init__()
self.est_speed = 0.0
def start(self):
super(AcquireProgress, self).start()
self.est_speed = 0.0
self.eta = 0.0
self.percent = 0.0
self.release_file_download_error = False
def update_status(self, uri, descr, shortDescr, status):
super(AcquireProgress, self).update_status(uri, descr, shortDescr, status)
# FIXME: workaround issue in libapt/python-apt that does not
# raise a exception if *all* files fails to download
if status == apt_pkg.STAT_FAILED:
logging.warning("update_status: dlFailed on '%s' " % uri)
if uri.endswith("Release.gpg") or uri.endswith("Release"):
# only care about failures from network, not gpg, bzip, those
# are different issues
for net in ["http","ftp","mirror"]:
if uri.startswith(net):
self.release_file_download_error = True
break
# required, otherwise the lucid version of python-apt gets really
# unhappy, its expecting this function for apt.progress.base.AcquireProgress
def pulse_items(self, arg):
return True
def pulse(self, owner=None):
super(AcquireProgress, self).pulse(owner)
self.percent = (((self.current_bytes + self.current_items) * 100.0) /
float(self.total_bytes + self.total_items))
if self.current_cps > self.est_speed:
self.est_speed = (self.est_speed+self.current_cps)/2.0
if self.current_cps > 0:
self.eta = ((self.total_bytes - self.current_bytes) /
float(self.current_cps))
return True
def isDownloadSpeedEstimated(self):
return (self.est_speed != 0)
def estimatedDownloadTime(self, required_download):
""" get the estimated download time """
if self.est_speed == 0:
time5Mbit = required_download/(5*1000*1000/8) # 1Mbit = 1000 kbit
time40Mbit = required_download/(40*1000*1000/8)
s= _("This download will take about %s with a 40Mbit connection "
"and about %s with a 5Mbit connection.") % (FuzzyTimeToStr(time40Mbit), FuzzyTimeToStr(time5Mbit))
return s
# if we have a estimated speed, use it
s = _("This download will take about %s with your connection. ") % FuzzyTimeToStr(required_download/self.est_speed)
return s
class InstallProgress(apt.progress.base.InstallProgress):
""" Base class for InstallProgress that supports some fancy
stuff like apport integration
"""
def __init__(self):
apt.progress.base.InstallProgress.__init__(self)
self.master_fd = None
def wait_child(self):
"""Wait for child progress to exit.
The return values is the full status returned from os.waitpid()
(not only the return code).
"""
while True:
try:
select.select([self.statusfd], [], [], self.select_timeout)
except select.error as e:
if e.args[0] != errno.EINTR:
raise
self.update_interface()
try:
(pid, res) = os.waitpid(self.child_pid, os.WNOHANG)
if pid == self.child_pid:
break
except OSError as e:
if e.errno != errno.EINTR:
raise
if e.errno == errno.ECHILD:
break
return res
def run(self, pm):
pid = self.fork()
if pid == 0:
# child, ignore sigpipe, there are broken scripts out there
# like etckeeper (LP: #283642)
signal.signal(signal.SIGPIPE,signal.SIG_IGN)
try:
res = pm.do_install(self.writefd)
except Exception as e:
print("Exception during pm.DoInstall(): ", e)
logging.exception("Exception during pm.DoInstall()")
with open("/var/run/ubuntu-release-upgrader-apt-exception","w") as f:
f.write(str(e))
os._exit(pm.RESULT_FAILED)
os._exit(res)
self.child_pid = pid
res = os.WEXITSTATUS(self.wait_child())
return res
def error(self, pkg, errormsg):
" install error from a package "
apt.progress.base.InstallProgress.error(self, pkg, errormsg)
logging.error("got an error from dpkg for pkg: '%s': '%s'" % (pkg, errormsg))
if "/" in pkg:
pkg = os.path.basename(pkg)
if pkg.split('-')[0].isdigit():
pkg = ('-').join(pkg.split('-')[1:])
if "_" in pkg:
pkg = pkg.split("_")[0]
# now run apport
apport_pkgfailure(pkg, errormsg)
class DumbTerminal(object):
def call(self, cmd, hidden=False):
" expects a command in the subprocess style (as a list) "
import subprocess
subprocess.call(cmd)
class SampleHtmlView(object):
def open(self, url):
pass
def show(self):
pass
def hide(self):
pass
class Step(Enum):
PREPARE = 1
MODIFY_SOURCES = 2
FETCH = 3
INSTALL = 4
CLEANUP = 5
REBOOT = 6
N = 7
# Declare these translatable strings from the .ui files here so that
# xgettext picks them up.
( _("Preparing to upgrade"),
_("Getting new software channels"),
_("Getting new packages"),
_("Installing the upgrades"),
_("Cleaning up"),
)
class DistUpgradeView(object):
" abstraction for the upgrade view "
def __init__(self):
self.needs_screen = False
pass
def getOpCacheProgress(self):
" return a OpProgress() subclass for the given graphic"
return apt.progress.base.OpProgress()
def getAcquireProgress(self):
" return an acquire progress object "
return AcquireProgress()
def getInstallProgress(self, cache=None):
" return a install progress object "
return InstallProgress()
def getTerminal(self):
return DumbTerminal()
def getHtmlView(self):
return SampleHtmlView()
def updateStatus(self, msg):
""" update the current status of the distUpgrade based
on the current view
"""
pass
def abort(self):
""" provide a visual feedback that the upgrade was aborted """
pass
def setStep(self, step):
""" we have 6 steps current for a upgrade:
1. Analyzing the system
2. Updating repository information
3. fetch packages
3. Performing the upgrade
4. Post upgrade stuff
5. Complete
"""
get_telemetry().add_stage(step.name)
pass
def hideStep(self, step):
" hide a certain step from the GUI "
pass
def showStep(self, step):
" show a certain step from the GUI "
pass
def confirmChanges(self, summary, changes, downloadSize,
actions=None, removal_bold=True):
""" display the list of changed packages (apt.Package) and
return if the user confirms them
"""
self.confirmChangesMessage = ""
self.toInstall = []
self.toReinstall = []
self.toUpgrade = []
self.toRemove = []
self.toRemoveAuto = []
self.toDowngrade = []
for pkg in changes:
if pkg.marked_install:
self.toInstall.append(pkg)
elif pkg.marked_upgrade:
self.toUpgrade.append(pkg)
elif pkg.marked_reinstall:
self.toReinstall.append(pkg)
elif pkg.marked_delete:
if pkg._pcache._depcache.is_auto_installed(pkg._pkg):
self.toRemoveAuto.append(pkg)
else:
self.toRemove.append(pkg)
elif pkg.marked_downgrade:
self.toDowngrade.append(pkg)
# do not bother the user with a different treeview
self.toInstall = self.toInstall + self.toReinstall
# sort it
self.toInstall.sort()
self.toUpgrade.sort()
self.toRemove.sort()
self.toRemoveAuto.sort()
self.toDowngrade.sort()
# now build the message (the same for all frontends)
msg = "\n"
pkgs_remove = len(self.toRemove) + len(self.toRemoveAuto)
pkgs_inst = len(self.toInstall) + len(self.toReinstall)
pkgs_upgrade = len(self.toUpgrade)
if pkgs_remove > 0:
# FIXME: make those two separate lines to make it clear
# that the "%" applies to the result of ngettext
msg += ngettext("%d package is going to be removed.",
"%d packages are going to be removed.",
pkgs_remove) % pkgs_remove
msg += " "
if pkgs_inst > 0:
msg += ngettext("%d new package is going to be "
"installed.",
"%d new packages are going to be "
"installed.",pkgs_inst) % pkgs_inst
msg += " "
if pkgs_upgrade > 0:
msg += ngettext("%d package is going to be upgraded.",
"%d packages are going to be upgraded.",
pkgs_upgrade) % pkgs_upgrade
msg +=" "
if downloadSize > 0:
downloadSizeStr = apt_pkg.size_to_str(downloadSize)
if isinstance(downloadSizeStr, bytes):
downloadSizeStr = downloadSizeStr.decode(ENCODING)
msg += _("\n\nYou have to download a total of %s. ") % (
downloadSizeStr)
msg += self.getAcquireProgress().estimatedDownloadTime(downloadSize)
if ((pkgs_upgrade + pkgs_inst) > 0) and ((pkgs_upgrade + pkgs_inst + pkgs_remove) > 100):
if self.getAcquireProgress().isDownloadSpeedEstimated():
msg += "\n\n%s" % _( "Installing the upgrade "
"can take several hours. Once the download "
"has finished, the process cannot be canceled.")
else:
msg += "\n\n%s" % _( "Fetching and installing the upgrade "
"can take several hours. Once the download "
"has finished, the process cannot be canceled.")
else:
if pkgs_remove > 100:
msg += "\n\n%s" % _( "Removing the packages "
"can take several hours. ")
# Show an error if no actions are planned
if (pkgs_upgrade + pkgs_inst + pkgs_remove) < 1:
# FIXME: this should go into DistUpgradeController
summary = _("The software on this computer is up to date.")
msg = _("There are no upgrades available for your system. "
"The upgrade will now be canceled.")
self.error(summary, msg)
return False
# set the message
self.confirmChangesMessage = msg
return True
def askYesNoQuestion(self, summary, msg, default='No'):
" ask a Yes/No question and return True on 'Yes' "
pass
def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
" ask a Cancel/Continue question and return True on 'Continue'"
pass
def confirmRestart(self):
" generic ask about the restart, can be overridden "
summary = _("Reboot required")
msg = _("The upgrade is finished and "
"a reboot is required. "
"Do you want to do this "
"now?")
return self.askYesNoQuestion(summary, msg)
def adviseExitOtherWSL(self):
summary = _("Action required")
msg = _("Exit all other instances of Ubuntu WSL before continuing.")
extended = _("Unsaved progress may otherwise be lost.")
return self.information(summary, msg, extended)
def adviseRestartWSL(self):
summary = _("WSL restart required")
msg = _("Exit this instance of Ubuntu WSL.")
extended = _("The upgrade will then be complete.")
return self.information(summary, msg, extended)
def error(self, summary, msg, extended_msg=None):
" display a error "
pass
def information(self, summary, msg, extended_msg=None):
" display a information msg"
pass
def processEvents(self):
""" process gui events (to keep the gui alive during a long
computation """
pass
def pulseProgress(self, finished=False):
""" do a progress pulse (e.g. bounce a bar back and forth, show
a spinner)
"""
pass
./DistUpgradeViewGtk3.py 0000644 0003721 0004705 00000077734 15000447260 014677 0 ustar buildd buildd # DistUpgradeViewGtk3.py
#
# Copyright (c) 2011 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import gi
vte291 = False
try:
gi.require_version("Vte", "2.91")
from gi.repository import Vte
vte291 = True
except Exception:
gi.require_version("Vte", "2.90")
# COMPAT: Dear upstream, this compat code below will be duplicated in
# all python-vte using applications. Love, Michael
from gi.repository import Vte
Vte.Pty.new_sync = Vte.Pty.new
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Pango
import sys
import locale
import logging
import time
import subprocess
import apt
import apt_pkg
import distro_info
import os
from .DistUpgradeApport import run_apport, apport_crash
from .DistUpgradeView import DistUpgradeView, FuzzyTimeToStr, InstallProgress, AcquireProgress
from .DistUpgradeConfigParser import DistUpgradeConfig
from .telemetry import get as get_telemetry
from .SimpleGtk3builderApp import SimpleGtkbuilderApp
import gettext
from .DistUpgradeGettext import gettext as _
class GtkOpProgress(apt.progress.base.OpProgress):
def __init__(self, progressbar):
self.progressbar = progressbar
#self.progressbar.set_pulse_step(0.01)
#self.progressbar.pulse()
self.fraction = 0.0
def update(self, percent=None):
super(GtkOpProgress, self).update(percent)
#if self.percent > 99:
# self.progressbar.set_fraction(1)
#else:
# self.progressbar.pulse()
new_fraction = self.percent/100.0
if abs(self.fraction-new_fraction) > 0.1:
self.fraction = new_fraction
self.progressbar.set_fraction(self.fraction)
while Gtk.events_pending():
Gtk.main_iteration()
def done(self):
self.progressbar.set_text(" ")
class GtkAcquireProgressAdapter(AcquireProgress):
# FIXME: we really should have some sort of "we are at step"
# xy in the gui
# FIXME2: we need to thing about mediaCheck here too
def __init__(self, parent):
super(GtkAcquireProgressAdapter, self).__init__()
# if this is set to false the download will cancel
self.status = parent.label_status
self.progress = parent.progressbar_cache
self.parent = parent
self.canceled = False
self.button_cancel = parent.button_fetch_cancel
self.button_cancel.connect('clicked', self.cancelClicked)
def cancelClicked(self, widget):
logging.debug("cancelClicked")
self.canceled = True
def media_change(self, medium, drive):
#print("mediaChange %s %s" % (medium, drive))
msg = _("Please insert '%s' into the drive '%s'") % (medium,drive)
dialog = Gtk.MessageDialog(parent=self.parent.window_main,
flags=Gtk.DialogFlags.MODAL,
type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.OK_CANCEL)
dialog.set_markup(msg)
res = dialog.run()
dialog.set_title("")
dialog.destroy()
if res == Gtk.ResponseType.OK:
return True
return False
def start(self):
#logging.debug("start")
super(GtkAcquireProgressAdapter, self).start()
self.progress.set_fraction(0)
self.status.show()
self.button_cancel.show()
def stop(self):
#logging.debug("stop")
self.progress.set_text(" ")
self.status.set_text(_("Fetching is complete"))
self.button_cancel.hide()
def pulse(self, owner):
super(GtkAcquireProgressAdapter, self).pulse(owner)
# only update if there is a noticable change
if abs(self.percent-self.progress.get_fraction()*100.0) > 0.1:
self.progress.set_fraction(self.percent/100.0)
currentItem = self.current_items + 1
if currentItem > self.total_items:
currentItem = self.total_items
if self.current_cps > 0:
current_cps = apt_pkg.size_to_str(self.current_cps)
if isinstance(current_cps, bytes):
current_cps = current_cps.decode(
locale.getpreferredencoding())
self.status.set_text(_("Fetching file %li of %li at %sB/s") % (
currentItem, self.total_items, current_cps))
self.progress.set_text(_("About %s remaining") % FuzzyTimeToStr(
self.eta))
else:
self.status.set_text(_("Fetching file %li of %li") % (
currentItem, self.total_items))
self.progress.set_text(" ")
while Gtk.events_pending():
Gtk.main_iteration()
return (not self.canceled)
class GtkInstallProgressAdapter(InstallProgress):
# timeout with no status change when the terminal is expanded
# automatically
TIMEOUT_TERMINAL_ACTIVITY = 300
def __init__(self,parent):
InstallProgress.__init__(self)
self._cache = None
self.label_status = parent.label_status
self.progress = parent.progressbar_cache
self.expander = parent.expander_terminal
self.term = parent._term
self.term.connect("child-exited", self.child_exited)
self.parent = parent
# setup the child waiting
# some options for dpkg to make it die less easily
apt_pkg.config.set("DPkg::StopOnError","False")
def start_update(self):
InstallProgress.start_update(self)
self.finished = False
# FIXME: add support for the timeout
# of the terminal (to display something useful then)
# -> longer term, move this code into python-apt
self.label_status.set_text(_("Applying changes"))
self.progress.set_fraction(0.0)
self.progress.set_text(" ")
self.expander.set_sensitive(True)
self.term.show()
self.term.connect("contents-changed", self._on_term_content_changed)
# if no libgtk3-perl is installed show the terminal
frontend= os.environ.get("DEBIAN_FRONTEND") or "gnome"
if frontend == "gnome" and self._cache:
if (not "libgtk3-perl" in self._cache or
not self._cache["libgtk3-perl"].is_installed):
frontend = "dialog"
self.expander.set_expanded(True)
self.env = ["VTE_PTY_KEEP_FD=%s"% self.writefd,
"APT_LISTCHANGES_FRONTEND=none"]
if "DEBIAN_FRONTEND" not in os.environ:
self.env.append("DEBIAN_FRONTEND=%s" % frontend)
# do a bit of time-keeping
self.start_time = 0.0
self.time_ui = 0.0
self.last_activity = 0.0
def error(self, pkg, errormsg):
InstallProgress.error(self, pkg, errormsg)
logging.error("got an error from dpkg for pkg: '%s': '%s'" % (pkg, errormsg))
# we do not report followup errors from earlier failures
if gettext.dgettext('dpkg', "dependency problems - leaving unconfigured") in errormsg:
return False
#self.expander_terminal.set_expanded(True)
self.parent.dialog_error.set_transient_for(self.parent.window_main)
summary = _("Could not install '%s'") % pkg
msg = _("The upgrade will continue but the '%s' package may not "
"be in a working state. Please consider submitting a "
"bug report about it.") % pkg
markup="%s\n\n%s" % (summary, msg)
self.parent.dialog_error.realize()
self.parent.dialog_error.set_title("")
self.parent.dialog_error.get_window().set_functions(Gdk.WMFunction.MOVE)
self.parent.label_error.set_markup(markup)
self.parent.textview_error.get_buffer().set_text(errormsg)
self.parent.scroll_error.show()
self.parent.dialog_error.run()
self.parent.dialog_error.hide()
def conffile(self, current, new):
logging.debug("got a conffile-prompt from dpkg for file: '%s'" % current)
start = time.time()
#self.expander.set_expanded(True)
prim = _("Replace the customized configuration file\n'%s'?") % current
sec = _("You will lose any changes you have made to this "
"configuration file if you choose to replace it with "
"a newer version.")
markup = "%s \n\n%s" % (prim, sec)
self.parent.label_conffile.set_markup(markup)
self.parent.dialog_conffile.set_title("")
self.parent.dialog_conffile.set_transient_for(self.parent.window_main)
# workaround silly dpkg
if not os.path.exists(current):
current = current+".dpkg-dist"
# now get the diff
if os.path.exists("/usr/bin/diff"):
cmd = ["/usr/bin/diff", "-u", current, new]
diff = subprocess.Popen(
cmd, stdout=subprocess.PIPE).communicate()[0]
diff = diff.decode("UTF-8", "replace")
self.parent.textview_conffile.get_buffer().set_text(diff)
else:
self.parent.textview_conffile.get_buffer().set_text(_("The 'diff' command was not found"))
res = self.parent.dialog_conffile.run()
self.parent.dialog_conffile.hide()
self.time_ui += time.time() - start
# if replace, send this to the terminal
if res == Gtk.ResponseType.YES:
response = "y\n"
else:
response = "n\n"
try:
self.term.feed_child(response.encode("utf-8"))
except:
self.term.feed_child(response, -1)
def fork(self):
pty = Vte.Pty.new_sync(Vte.PtyFlags.DEFAULT)
pid = os.fork()
if pid == 0:
# WORKAROUND for broken feisty vte where envv does not work)
for env in self.env:
(key, value) = env.split("=")
os.environ[key] = value
# MUST be called
pty.child_setup()
# force dpkg terminal messages untranslated for better bug
# duplication detection
os.environ["DPKG_UNTRANSLATED_MESSAGES"] = "1"
else:
self.term.set_pty(pty)
self.term.watch_child(pid)
return pid
def _on_term_content_changed(self, term):
""" helper function that is called when the terminal changed
to ensure that we have a accurate idea when something hangs
"""
self.last_activity = time.time()
self.activity_timeout_reported = False
def status_change(self, pkg, percent, status):
# start the timer when the first package changes its status
if self.start_time == 0.0:
#print("setting start time to %s" % self.start_time)
self.start_time = time.time()
# only update if there is a noticable change
if abs(percent-self.progress.get_fraction()*100.0) > 0.1:
self.progress.set_fraction(float(percent)/100.0)
self.label_status.set_text(status.strip())
# start showing when we gathered some data
if percent > 1.0:
delta = self.last_activity - self.start_time
# time wasted in conffile questions (or other ui activity)
delta -= self.time_ui
time_per_percent = (float(delta)/percent)
eta = (100.0 - percent) * time_per_percent
# only show if we have some sensible data (60sec < eta < 2days)
if eta > 61.0 and eta < (60*60*24*2):
self.progress.set_text(_("About %s remaining") % FuzzyTimeToStr(eta))
else:
self.progress.set_text(" ")
# 2 == WEBKIT_LOAD_FINISHED - the enums is not exposed via python
if (self.parent._webkit_view and
self.parent._webkit_view.get_property("load-status") == 2):
self.parent._webkit_view.execute_script('progress("%s")' % percent)
def child_exited(self, term, status=None):
# we need to capture the full status here (not only the WEXITSTATUS)
if status is None:
# COMPAT we must keep until 16.04
self.apt_status = term.get_child_exit_status()
else:
self.apt_status = status
self.finished = True
def wait_child(self):
while not self.finished:
self.update_interface()
return self.apt_status
def finish_update(self):
self.label_status.set_text("")
def update_interface(self):
InstallProgress.update_interface(self)
# check if we haven't started yet with packages, pulse then
if self.start_time == 0.0:
self.progress.pulse()
time.sleep(0.2)
# check about terminal activity
if self.last_activity > 0 and \
(self.last_activity + self.TIMEOUT_TERMINAL_ACTIVITY) < time.time():
if not self.activity_timeout_reported:
logging.warning("no activity on terminal for %s seconds (%s)" % (self.TIMEOUT_TERMINAL_ACTIVITY, self.label_status.get_text()))
self.activity_timeout_reported = True
self.parent.expander_terminal.set_expanded(True)
# process events
while Gtk.events_pending():
Gtk.main_iteration()
time.sleep(0.01)
class DistUpgradeVteTerminal(object):
def __init__(self, parent, term):
self.term = term
self.parent = parent
def call(self, cmd, hidden=False):
if vte291:
def wait_for_child(terminal, status):
#print("wait for child finished")
self.finished=True
else:
def wait_for_child(widget):
#print("wait for child finished")
self.finished=True
self.term.show()
self.term.connect("child-exited", wait_for_child)
self.parent.expander_terminal.set_sensitive(True)
if hidden==False:
self.parent.expander_terminal.set_expanded(True)
self.finished = False
if vte291:
(success, pid) = self.term.spawn_sync(
Vte.PtyFlags.DEFAULT,
"/",
cmd,
None,
0, # GLib.SpawnFlags
None, # child_setup
None, # child_setup_data
None, # GCancellable
)
else:
(success, pid) = self.term.fork_command_full(
Vte.PtyFlags.DEFAULT,
"/",
cmd,
None,
0, # GLib.SpawnFlags
None, # child_setup
None, # child_setup_data
)
if not success or pid < 0:
# error
return
while not self.finished:
while Gtk.events_pending():
Gtk.main_iteration()
time.sleep(0.1)
del self.finished
class HtmlView(object):
def __init__(self, webkit_view):
self._webkit_view = webkit_view
def open(self, url):
if not self._webkit_view:
return
try:
from gi.repository import WebKit2
assert WebKit2 # silence pep8
self._webkit_view.load_uri(url)
self._webkit_view.connect("load-changed", self._on_load_changed)
except ImportError:
self._webkit_view.open(url)
self._webkit_view.connect("load-finished", self._on_load_finished)
def show(self):
self._webkit_view.show()
def hide(self):
self._webkit_view.hide()
def _on_load_finished(self, view, frame):
view.show()
def _on_load_changed(self, view, event, data):
from gi.repository import WebKit2
if event == WebKit2.LoadEvent.LOAD_FINISHED:
view.show()
class DistUpgradeViewGtk3(DistUpgradeView,SimpleGtkbuilderApp):
" gtk frontend of the distUpgrade tool "
def __init__(self, datadir=None, logdir=None):
DistUpgradeView.__init__(self)
self.logdir = logdir
if not datadir or datadir == '.':
localedir=os.path.join(os.getcwd(),"mo")
gladedir=os.getcwd()
self.config = DistUpgradeConfig(os.getcwd())
else:
localedir="/usr/share/locale/"
gladedir=os.path.join(datadir, "gtkbuilder")
self.config = DistUpgradeConfig(datadir)
# check if we have a display etc
Gtk.init_check(sys.argv)
get_telemetry().set_updater_type('GTK')
try:
locale.bindtextdomain("ubuntu-release-upgrader",localedir)
gettext.textdomain("ubuntu-release-upgrader")
except Exception as e:
logging.warning("Error setting locales (%s)" % e)
SimpleGtkbuilderApp.__init__(self,
gladedir+"/DistUpgrade.ui",
"ubuntu-release-upgrader")
icons = Gtk.IconTheme.get_default()
try:
self.window_main.set_default_icon(icons.load_icon("system-software-update", 32, 0))
except GObject.GError as e:
logging.debug("error setting default icon, ignoring (%s)" % e)
pass
to_dist = self.config.get("Sources", "To")
to_version = distro_info.UbuntuDistroInfo().version(to_dist)
title_string = self.label_title.get_label()
title_string = title_string.replace("%s", to_version)
self.label_title.set_label(title_string)
# terminal stuff
self.create_terminal()
self.prev_step = None # keep a record of the latest step
# we don't use this currently
#self.window_main.set_keep_above(True)
self.icontheme = Gtk.IconTheme.get_default()
self._webkit_view = None
self.window_main.realize()
self.window_main.get_window().set_functions(Gdk.WMFunction.MOVE)
self._opCacheProgress = GtkOpProgress(self.progressbar_cache)
self._acquireProgress = GtkAcquireProgressAdapter(self)
self._installProgress = GtkInstallProgressAdapter(self)
# details dialog
self.details_list = Gtk.TreeStore(GObject.TYPE_STRING)
column = Gtk.TreeViewColumn("")
render = Gtk.CellRendererText()
column.pack_start(render, True)
column.add_attribute(render, "markup", 0)
self.treeview_details.append_column(column)
self.details_list.set_sort_column_id(0, Gtk.SortType.ASCENDING)
self.treeview_details.set_model(self.details_list)
# lp: #1072460
self.dialog_changes.set_resizable(False)
def _activated(w):
# the *current* expanded state which will change after the signal
expanded = self.expander_details.get_expanded()
self.dialog_changes.set_resizable(not expanded)
self.expander_details.connect("activate", _activated)
# FIXME: portme
# Use italic style in the status labels
#attrlist=Pango.AttrList()
#attr = Pango.AttrStyle(Pango.Style.ITALIC, 0, -1)
#attr = Pango.AttrScale(Pango.SCALE_SMALL, 0, -1)
#attrlist.insert(attr)
#self.label_status.set_property("attributes", attrlist)
# reasonable fault handler
sys.excepthook = self._handleException
def _handleException(self, type, value, tb):
# we handle the exception here, hand it to apport and run the
# apport gui manually after it because we kill u-n during the upgrade
# to prevent it from poping up for reboot notifications or FF restart
# notifications or somesuch
import traceback
lines = traceback.format_exception(type, value, tb)
logging.error("not handled exception:\n%s" % "\n".join(lines))
# we can't be sure that apport will run in the middle of a upgrade
# so we still show a error message here
apport_crash(type, value, tb)
if not run_apport():
self.error(_("A fatal error occurred"),
_("Please report this as a bug (if you haven't already) and include the "
"files /var/log/dist-upgrade/main.log and "
"/var/log/dist-upgrade/apt.log "
"in your report. The upgrade has aborted.\n"
"Your original sources.list was saved in "
"/etc/apt/sources.list.distUpgrade."),
"\n".join(lines))
sys.exit(1)
def getTerminal(self):
return DistUpgradeVteTerminal(self, self._term)
def getHtmlView(self):
if self._webkit_view is None:
try:
try:
from gi.repository import WebKit2 as WebKit
except ImportError:
from gi.repository import WebKit
self._webkit_view = WebKit.WebView()
settings = self._webkit_view.get_settings()
settings.set_property("enable-plugins", False)
self.vbox_main.pack_end(self._webkit_view, True, True, 0)
except:
logging.exception("html widget")
return DistUpgradeView.SampleHtmlView()
return HtmlView(self._webkit_view)
def _key_press_handler(self, widget, keyev):
# user pressed ctrl-c
if len(keyev.string) == 1 and ord(keyev.string) == 3:
summary = _("Ctrl-c pressed")
msg = _("This will abort the operation and may leave the system "
"in a broken state. Are you sure you want to do that?")
res = self.askYesNoQuestion(summary, msg)
logging.warning("ctrl-c press detected, user decided to pass it "
"on: %s", res)
return not res
return False
def create_terminal(self):
" helper to create a vte terminal "
self._term = Vte.Terminal.new()
# COMPAT that must be kept until 16.04
if not hasattr(self._term, "set_pty"):
self._term.set_pty = self._term.set_pty_object
self._term.connect("key-press-event", self._key_press_handler)
fontdesc = Pango.font_description_from_string("monospace 10")
self._term.set_font(fontdesc)
self._terminal_lines = []
self.hbox_custom.pack_start(self._term, True, True, 0)
self._term.realize()
self.vscrollbar_terminal = Gtk.VScrollbar()
self.vscrollbar_terminal.show()
self.hbox_custom.pack_start(self.vscrollbar_terminal, True, True, 0)
self.vscrollbar_terminal.set_adjustment(self._term.get_vadjustment())
try:
self._terminal_log = open(os.path.join(self.logdir,"term.log"),"w")
except Exception:
# if something goes wrong (permission denied etc), use stdout
self._terminal_log = sys.stdout
return self._term
def getAcquireProgress(self):
return self._acquireProgress
def getInstallProgress(self, cache):
self._installProgress._cache = cache
return self._installProgress
def getOpCacheProgress(self):
return self._opCacheProgress
def updateStatus(self, msg):
self.label_status.set_text("%s" % msg)
def hideStep(self, step):
image = getattr(self,"image_step%i" % step.value)
label = getattr(self,"label_step%i" % step.value)
#arrow = getattr(self,"arrow_step%i" % step.value)
image.hide()
label.hide()
def showStep(self, step):
image = getattr(self,"image_step%i" % step.value)
label = getattr(self,"label_step%i" % step.value)
image.show()
label.show()
def abort(self):
size = Gtk.IconSize.MENU
step = self.prev_step
if step:
image = getattr(self,"image_step%i" % step.value)
arrow = getattr(self,"arrow_step%i" % step.value)
image.set_from_stock(Gtk.STOCK_CANCEL, size)
image.show()
arrow.hide()
def setStep(self, step):
super(DistUpgradeViewGtk3, self).setStep(step)
if self.icontheme.rescan_if_needed():
logging.debug("icon theme changed, re-reading")
# first update the "previous" step as completed
size = Gtk.IconSize.MENU
attrlist=Pango.AttrList()
if self.prev_step:
image = getattr(self,"image_step%i" % self.prev_step.value)
label = getattr(self,"label_step%i" % self.prev_step.value)
arrow = getattr(self,"arrow_step%i" % self.prev_step.value)
label.set_property("attributes",attrlist)
image.set_from_stock(Gtk.STOCK_APPLY, size)
image.show()
arrow.hide()
self.prev_step = step
# show the an arrow for the current step and make the label bold
image = getattr(self,"image_step%i" % step.value)
label = getattr(self,"label_step%i" % step.value)
arrow = getattr(self,"arrow_step%i" % step.value)
# check if that step was not hidden with hideStep()
if not label.get_property("visible"):
return
arrow.show()
image.hide()
# FIXME: portme
#attr = Pango.AttrWeight(Pango.Weight.BOLD, 0, -1)
#attrlist.insert(attr)
#label.set_property("attributes",attrlist)
def information(self, summary, msg, extended_msg=None):
self.dialog_information.set_title("")
self.dialog_information.set_transient_for(self.window_main)
msg = "%s\n\n%s" % (summary,msg)
self.label_information.set_markup(msg)
if extended_msg != None:
buffer = self.textview_information.get_buffer()
buffer.set_text(extended_msg)
self.scroll_information.show()
else:
self.scroll_information.hide()
self.dialog_information.realize()
self.dialog_information.get_window().set_functions(Gdk.WMFunction.MOVE)
self.dialog_information.run()
self.dialog_information.hide()
while Gtk.events_pending():
Gtk.main_iteration()
def error(self, summary, msg, extended_msg=None):
self.dialog_error.set_title("")
self.dialog_error.set_transient_for(self.window_main)
#self.expander_terminal.set_expanded(True)
msg="%s\n\n%s" % (summary, msg)
self.label_error.set_markup(msg)
if extended_msg != None:
buffer = self.textview_error.get_buffer()
buffer.set_text(extended_msg)
self.scroll_error.show()
else:
self.scroll_error.hide()
self.dialog_error.realize()
self.dialog_error.get_window().set_functions(Gdk.WMFunction.MOVE)
self.dialog_error.run()
self.dialog_error.hide()
return False
def confirmChanges(self, summary, changes, downloadSize,
actions=None, removal_bold=True):
# FIXME: add an allow list here for packages that we expect to be
# removed (how to calc this automatically?)
if not DistUpgradeView.confirmChanges(self, summary, changes,
downloadSize):
return False
# append warning
self.confirmChangesMessage += "\n\n%s" % \
_("To prevent data loss close all open "
"applications and documents.")
if actions != None:
self.button_cancel_changes.set_use_stock(False)
self.button_cancel_changes.set_use_underline(True)
self.button_cancel_changes.set_label(actions[0])
self.button_confirm_changes.set_label(actions[1])
self.label_summary.set_markup("%s" % summary)
self.label_changes.set_markup(self.confirmChangesMessage)
# fill in the details
self.details_list.clear()
for (parent_text, details_list) in (
( _("Downgrade (%s)"), self.toDowngrade),
( _("Remove (%s)"), self.toRemove),
( _("No longer needed (%s)"), self.toRemoveAuto),
( _("Install (%s)"), self.toInstall),
( _("Upgrade (%s)"), self.toUpgrade),
):
if details_list:
node = self.details_list.append(None,
[parent_text % len(details_list)])
for pkg in details_list:
self.details_list.append(node, ["%s - %s" % (
pkg.name, GLib.markup_escape_text(getattr(pkg.candidate, "summary", None)))])
# prepare dialog
self.dialog_changes.realize()
self.dialog_changes.set_transient_for(self.window_main)
self.dialog_changes.set_title("")
self.dialog_changes.get_window().set_functions(Gdk.WMFunction.MOVE|
Gdk.WMFunction.RESIZE)
res = self.dialog_changes.run()
self.dialog_changes.hide()
if res == Gtk.ResponseType.YES:
return True
return False
def askYesNoQuestion(self, summary, msg, default='No'):
msg = "%s\n\n%s" % (summary,msg)
dialog = Gtk.MessageDialog(parent=self.window_main,
flags=Gtk.DialogFlags.MODAL,
type=Gtk.MessageType.QUESTION,
buttons=Gtk.ButtonsType.YES_NO)
dialog.set_title("")
if default == 'No':
dialog.set_default_response(Gtk.ResponseType.NO)
else:
dialog.set_default_response(Gtk.ResponseType.YES)
dialog.set_markup(msg)
res = dialog.run()
dialog.destroy()
if res == Gtk.ResponseType.YES:
return True
return False
def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
if summary:
msg = "%s\n\n%s" % (summary,msg)
dialog = Gtk.MessageDialog(parent=self.window_main,
flags=Gtk.DialogFlags.MODAL,
type=Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.NONE)
dialog.set_title("")
dialog.set_markup(msg)
dialog.add_buttons(_('Cancel'), Gtk.ResponseType.CANCEL,
_('Continue'), Gtk.ResponseType.ACCEPT)
if default == 'Cancel':
dialog.set_default_response(Gtk.ResponseType.CANCEL)
else:
dialog.set_default_response(Gtk.ResponseType.ACCEPT)
res = dialog.run()
dialog.destroy()
if res == Gtk.ResponseType.ACCEPT:
return True
return False
def confirmRestart(self):
self.dialog_restart.set_transient_for(self.window_main)
self.dialog_restart.set_title("")
self.dialog_restart.realize()
self.dialog_restart.get_window().set_functions(Gdk.WMFunction.MOVE)
res = self.dialog_restart.run()
self.dialog_restart.hide()
if res == Gtk.ResponseType.YES:
return True
return False
def processEvents(self):
while Gtk.events_pending():
Gtk.main_iteration()
def pulseProgress(self, finished=False):
self.progressbar_cache.pulse()
if finished:
self.progressbar_cache.set_fraction(1.0)
def on_window_main_delete_event(self, widget, event):
self.dialog_cancel.set_transient_for(self.window_main)
self.dialog_cancel.set_title("")
self.dialog_cancel.realize()
self.dialog_cancel.get_window().set_functions(Gdk.WMFunction.MOVE)
res = self.dialog_cancel.run()
self.dialog_cancel.hide()
if res == Gtk.ResponseType.CANCEL:
sys.exit(1)
return True
./DistUpgradeViewKDE.py 0000644 0003721 0004705 00000113016 15000447260 014452 0 ustar buildd buildd # DistUpgradeViewKDE.py
#
# Copyright (c) 2007 Canonical Ltd
# Copyright (c) 2014-2018 Harald Sitter
# Copyright (c) 2024 Simon Quigley
#
# Author: Jonathan Riddell
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
from PyQt6 import uic
from PyQt6.QtCore import Qt, QLocale, QTranslator, QTimer
from PyQt6.QtWidgets import QTextEdit, QApplication, QDialog,\
QMessageBox, QDialogButtonBox, QTreeWidgetItem, QPushButton, QWidget,\
QHBoxLayout, QLabel
from PyQt6.QtGui import QTextOption, QPixmap, QIcon, QTextCursor
import sys
import locale
import logging
import time
import subprocess
import traceback
import apt
import apt_pkg
import distro_info
import shlex # for osrelease
import os
import pty
from .DistUpgradeApport import run_apport, apport_crash
from .DistUpgradeView import DistUpgradeView, FuzzyTimeToStr, InstallProgress, AcquireProgress
from .DistUpgradeConfigParser import DistUpgradeConfig
from .telemetry import get as get_telemetry
import select
import gettext
from .DistUpgradeGettext import gettext as _
from .DistUpgradeGettext import unicode_gettext
from .QUrlOpener import QUrlOpener
# FIXME: what's the purpose?
def utf8(s, errors="strict"):
if isinstance(s, bytes):
return s.decode("UTF-8", errors)
else:
return s
# FIXME: what's the purpose?
def loadUi(file, parent):
if os.path.exists(file):
uic.loadUi(file, parent)
else:
#FIXME find file
print("error, can't find file: " + file)
def _find_pixmap(path):
if os.path.exists(path):
return QPixmap(path)
return None
def _icon(name, fallbacks = []):
return QIcon.fromTheme(name)
# QWidget adjustSize when run on a maximized window will make Qt 5.9, earlier,
# and probably also later, lose its state. Qt will think the window is no longer
# maximized, while in fact it is. This results in parts of the window no longer
# getting redrawn as the window manager will think it maximized but Qt thinks it
# is not and thus not send repaints for the regions it thinks do not exist.
# To prevent this from happening monkey patch adjustSize to not ever run on
# maximized windows.
def adjustSize(self):
if not self.isMaximized():
self.origAdjustSize(self)
QWidget.origAdjustSize = QWidget.adjustSize
QWidget.adjustSize = adjustSize
class _OSRelease:
DEFAULT_OS_RELEASE_FILE = '/etc/os-release'
OS_RELEASE_FILE = '/etc/os-release'
def __init__(self, lsb_compat=True):
self.result = {}
self.valid = False
self.file = _OSRelease.OS_RELEASE_FILE
if not os.path.isfile(self.file):
return
self.parse()
self.valid = True
if lsb_compat:
self.inject_lsb_compat()
def inject_lsb_compat(self):
self.result['Distributor ID'] = self.result['ID']
self.result['Description'] = self.result['PRETTY_NAME']
# Optionals as per os-release spec.
self.result['Codename'] = self.result.get('VERSION_CODENAME')
if not self.result['Codename']:
# Transient Ubuntu 16.04 field (LP: #1598212)
self.result['Codename'] = self.result.get('UBUNTU_CODENAME')
self.result['Release'] = self.result.get('VERSION_ID')
def parse(self):
f = open(self.file, 'r')
for line in f:
line = line.strip()
if not line:
continue
self.parse_entry(*line.split('=', 1))
f.close()
def parse_entry(self, key, value):
value = self.parse_value(value) # Values can be shell strings...
if key == "ID_LIKE" and isinstance(value, str):
# ID_LIKE is specified as quoted space-separated list. This will
# be parsed as string that we need to split manually.
value = value.split(' ')
self.result[key] = value
def parse_value(self, value):
values = shlex.split(value)
if len(values) == 1:
return values[0]
return values
class DumbTerminal(QTextEdit):
""" A very dumb terminal """
def __init__(self, installProgress, parent_frame):
" really dumb terminal with simple editing support "
QTextEdit.__init__(self, "", parent_frame)
self.installProgress = installProgress
self.setFontFamily("Monospace")
# FIXME: fixed font size set!!!
self.setFontPointSize(8)
self.setWordWrapMode(QTextOption.WrapMode.NoWrap)
self.setUndoRedoEnabled(False)
self.setOverwriteMode(True)
self._block = False
#self.connect(self, SIGNAL("cursorPositionChanged()"),
# self.onCursorPositionChanged)
def fork(self):
"""pty voodoo"""
(self.child_pid, self.installProgress.master_fd) = pty.fork()
if self.child_pid == 0:
os.environ["TERM"] = "dumb"
return self.child_pid
def update_interface(self):
(rlist, wlist, xlist) = select.select([self.installProgress.master_fd],[],[], 0)
if len(rlist) > 0:
line = os.read(self.installProgress.master_fd, 255)
self.insertWithTermCodes(utf8(line))
QApplication.processEvents()
def insertWithTermCodes(self, text):
""" support basic terminal codes """
display_text = ""
for c in text:
# \b - backspace - this seems to comes as "^H" now ??!
if ord(c) == 8:
self.insertPlainText(display_text)
self.textCursor().deletePreviousChar()
display_text=""
# \r - is filtered out
elif c == chr(13):
pass
# \a - bell - ignore for now
elif c == chr(7):
pass
else:
display_text += c
self.insertPlainText(display_text)
def keyPressEvent(self, ev):
""" send (ascii) key events to the pty """
# no master_fd yet
if not hasattr(self.installProgress, "master_fd"):
return
# special handling for backspace
if ev.key() == Qt.Key.Key_Backspace:
#print("sent backspace")
os.write(self.installProgress.master_fd, b'\x08')
return
# do nothing for events like "shift"
if not ev.text():
return
# now send the key event to the terminal as utf-8
os.write(self.installProgress.master_fd, ev.text().encode('utf-8'))
def onCursorPositionChanged(self):
""" helper that ensures that the cursor is always at the end """
if self._block:
return
# block signals so that we do not run into a recursion
self._block = True
self.moveCursor(QTextCursor.End)
self._block = False
class KDEOpProgress(apt.progress.base.OpProgress):
""" methods on the progress bar """
def __init__(self, progressbar, progressbar_label):
self.progressbar = progressbar
self.progressbar_label = progressbar_label
#self.progressbar.set_pulse_step(0.01)
#self.progressbar.pulse()
def update(self, percent=None):
super(KDEOpProgress, self).update(percent)
#if self.percent > 99:
# self.progressbar.set_fraction(1)
#else:
# self.progressbar.pulse()
#self.progressbar.set_fraction(self.percent/100.0)
self.progressbar.setValue(int(self.percent))
QApplication.processEvents()
def done(self):
self.progressbar_label.setText("")
class KDEAcquireProgressAdapter(AcquireProgress):
""" methods for updating the progress bar while fetching packages """
# FIXME: we really should have some sort of "we are at step"
# xy in the gui
# FIXME2: we need to thing about mediaCheck here too
def __init__(self, parent):
AcquireProgress.__init__(self)
# if this is set to false the download will cancel
self.status = parent.window_main.label_status
self.progress = parent.window_main.progressbar_cache
self.parent = parent
def media_change(self, medium, drive):
msg = _("Please insert '%s' into the drive '%s'") % (medium,drive)
change = QMessageBox.question(self.parent.window_main, _("Media Change"), msg, QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Cancel)
if change == QMessageBox.StandardButton.Ok:
return True
return False
def start(self):
AcquireProgress.start(self)
#self.progress.show()
self.progress.setValue(0)
self.status.show()
def stop(self):
self.parent.window_main.progress_text.setText(" ")
self.status.setText(_("Fetching is complete"))
def pulse(self, owner):
""" we don't have a mainloop in this application, we just call processEvents here and elsewhere"""
# FIXME: move the status_str and progress_str into python-apt
# (python-apt need i18n first for this)
AcquireProgress.pulse(self, owner)
self.progress.setValue(int(self.percent))
current_item = self.current_items + 1
if current_item > self.total_items:
current_item = self.total_items
if self.current_cps > 0:
current_cps = apt_pkg.size_to_str(self.current_cps)
if isinstance(current_cps, bytes):
current_cps = current_cps.decode(locale.getpreferredencoding())
self.status.setText(_("Fetching file %li of %li at %sB/s") % (current_item, self.total_items, current_cps))
self.parent.window_main.progress_text.setText("" + _("About %s remaining") % FuzzyTimeToStr(self.eta) + "")
else:
self.status.setText(_("Fetching file %li of %li") % (current_item, self.total_items))
self.parent.window_main.progress_text.setText(" ")
QApplication.processEvents()
return True
class KDEInstallProgressAdapter(InstallProgress):
"""methods for updating the progress bar while installing packages"""
# timeout with no status change when the terminal is expanded
# automatically
TIMEOUT_TERMINAL_ACTIVITY = 240
def __init__(self,parent):
InstallProgress.__init__(self)
self._cache = None
self.label_status = parent.window_main.label_status
self.progress = parent.window_main.progressbar_cache
self.progress_text = parent.window_main.progress_text
self.parent = parent
try:
self._terminal_log = open("/var/log/dist-upgrade/term.log","wb")
except Exception as e:
# if something goes wrong (permission denied etc), use stdout
logging.error("Can not open terminal log: '%s'" % e)
if sys.version >= '3':
self._terminal_log = sys.stdout.buffer
else:
self._terminal_log = sys.stdout
# some options for dpkg to make it die less easily
apt_pkg.config.set("DPkg::StopOnError","False")
def start_update(self):
InstallProgress.start_update(self)
self.finished = False
# FIXME: add support for the timeout
# of the terminal (to display something useful then)
# -> longer term, move this code into python-apt
self.label_status.setText(_("Applying changes"))
self.progress.setValue(0)
self.progress_text.setText(" ")
# do a bit of time-keeping
self.start_time = 0.0
self.time_ui = 0.0
self.last_activity = 0.0
self.parent.window_main.showTerminalButton.setEnabled(True)
def error(self, pkg, errormsg):
InstallProgress.error(self, pkg, errormsg)
logging.error("got an error from dpkg for pkg: '%s': '%s'" % (pkg, errormsg))
# we do not report followup errors from earlier failures
if gettext.dgettext('dpkg', "dependency problems - leaving unconfigured") in errormsg:
return False
summary = _("Could not install '%s'") % pkg
msg = _("The upgrade will continue but the '%s' package may not "
"be in a working state. Please consider submitting a "
"bug report about it.") % pkg
msg = "%s
%s" % (summary, msg)
dialogue = QDialog(self.parent.window_main)
loadUi("dialog_error.ui", dialogue)
self.parent.translate_widget_children(dialogue)
dialogue.label_error.setText(msg)
if errormsg != None:
dialogue.textview_error.setText(errormsg)
dialogue.textview_error.show()
else:
dialogue.textview_error.hide()
# Make sure we have a suitable size depending on whether or not the view is shown
dialogue.adjustSize()
dialogue.exec()
def conffile(self, current, new):
"""ask question in case conffile has been changed by user"""
logging.debug("got a conffile-prompt from dpkg for file: '%s'" % current)
start = time.time()
prim = _("Replace the customized configuration file\n'%s'?") % current
sec = _("You will lose any changes you have made to this "
"configuration file if you choose to replace it with "
"a newer version.")
markup = "%s \n\n%s" % (prim, sec)
self.confDialogue = QDialog(self.parent.window_main)
loadUi("dialog_conffile.ui", self.confDialogue)
self.confDialogue.label_conffile.setText(markup)
self.confDialogue.textview_conffile.hide()
#FIXME, below to be tested
#self.confDialogue.resize(self.confDialogue.minimumSizeHint())
self.confDialogue.show_difference_button.clicked.connect(self.showConffile)
# workaround silly dpkg
if not os.path.exists(current):
current = current+".dpkg-dist"
# now get the diff
if os.path.exists("/usr/bin/diff"):
cmd = ["/usr/bin/diff", "-u", current, new]
diff = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
diff = diff.decode("UTF-8", "replace")
self.confDialogue.textview_conffile.setText(diff)
else:
self.confDialogue.textview_conffile.setText(_("The 'diff' command was not found"))
result = self.confDialogue.exec()
self.time_ui += time.time() - start
# if replace, send this to the terminal
if result == QDialog.DialogCode.Accepted:
os.write(self.master_fd, b"y\n")
else:
os.write(self.master_fd, b"n\n")
def showConffile(self):
if self.confDialogue.textview_conffile.isVisible():
self.confDialogue.textview_conffile.hide()
self.confDialogue.show_difference_button.setText(_("Show Difference >>>"))
else:
self.confDialogue.textview_conffile.show()
self.confDialogue.show_difference_button.setText(_("<<< Hide Difference"))
def fork(self):
"""pty voodoo"""
(self.child_pid, self.master_fd) = pty.fork()
if self.child_pid == 0:
os.environ["TERM"] = "dumb"
if ("DEBIAN_FRONTEND" not in os.environ or
os.environ["DEBIAN_FRONTEND"] == "kde"):
os.environ["DEBIAN_FRONTEND"] = "noninteractive"
os.environ["APT_LISTCHANGES_FRONTEND"] = "none"
logging.debug(" fork pid is: %s" % self.child_pid)
return self.child_pid
def status_change(self, pkg, percent, status):
"""update progress bar and label"""
# start the timer when the first package changes its status
if self.start_time == 0.0:
#print("setting start time to %s" % self.start_time)
self.start_time = time.time()
self.progress.setValue(int(self.percent))
self.label_status.setText(utf8(status.strip()))
# start showing when we gathered some data
if percent > 1.0:
self.last_activity = time.time()
self.activity_timeout_reported = False
delta = self.last_activity - self.start_time
# time wasted in conffile questions (or other ui activity)
delta -= self.time_ui
time_per_percent = (float(delta)/percent)
eta = (100.0 - self.percent) * time_per_percent
# only show if we have some sensible data (60sec < eta < 2days)
if eta > 61.0 and eta < (60*60*24*2):
self.progress_text.setText(_("About %s remaining") % FuzzyTimeToStr(eta))
else:
self.progress_text.setText(" ")
def finish_update(self):
self.label_status.setText("")
def update_interface(self):
"""
no mainloop in this application, just call processEvents lots here
it's also important to sleep for a minimum amount of time
"""
# log the output of dpkg (on the master_fd) to the terminal log
while True:
try:
(rlist, wlist, xlist) = select.select([self.master_fd],[],[], 0)
if len(rlist) > 0:
line = os.read(self.master_fd, 255)
self._terminal_log.write(line)
self.parent.terminal_text.insertWithTermCodes(
utf8(line, errors="replace"))
else:
break
except Exception as e:
print(e)
logging.debug("error reading from self.master_fd '%s'" % e)
break
# now update the GUI
try:
InstallProgress.update_interface(self)
except ValueError as e:
logging.error("got ValueError from InstallProgress.update_interface. Line was '%s' (%s)" % (self.read, e))
# reset self.read so that it can continue reading and does not loop
self.read = ""
# check about terminal activity
if self.last_activity > 0 and \
(self.last_activity + self.TIMEOUT_TERMINAL_ACTIVITY) < time.time():
if not self.activity_timeout_reported:
#FIXME bug 95465, I can't recreate this, so here's a hacky fix
try:
logging.warning("no activity on terminal for %s seconds (%s)" % (self.TIMEOUT_TERMINAL_ACTIVITY, self.label_status.text()))
except UnicodeEncodeError:
logging.warning("no activity on terminal for %s seconds" % (self.TIMEOUT_TERMINAL_ACTIVITY))
self.activity_timeout_reported = True
self.parent.window_main.konsole_frame.show()
QApplication.processEvents()
time.sleep(0.02)
def wait_child(self):
while True:
self.update_interface()
(pid, res) = os.waitpid(self.child_pid,os.WNOHANG)
if pid == self.child_pid:
break
# we need the full status here (not just WEXITSTATUS)
return res
# inherit from the class created in window_main.ui
# to add the handler for closing the window
class UpgraderMainWindow(QWidget):
def __init__(self):
super().__init__()
loadUi("window_main.ui", self)
def setParent(self, parentRef):
self.parent = parentRef
def closeEvent(self, event):
close = self.parent.on_window_main_delete_event()
if close:
event.accept()
else:
event.ignore()
class DistUpgradeViewKDE(DistUpgradeView):
"""KDE frontend of the distUpgrade tool"""
def __init__(self, datadir=None, logdir=None):
DistUpgradeView.__init__(self)
get_telemetry().set_updater_type('KDE')
# silence the PyQt6 logger
logger = logging.getLogger("PyQt6")
logger.setLevel(logging.INFO)
if not datadir or datadir == '.':
localedir=os.path.join(os.getcwd(),"mo")
self.config = DistUpgradeConfig(os.getcwd())
else:
localedir="/usr/share/locale/ubuntu-release-upgrader"
self.config = DistUpgradeConfig(datadir)
# FIXME: i18n must be somewhere relative do this dir
try:
gettext.bindtextdomain("ubuntu-release-upgrader", localedir)
gettext.textdomain("ubuntu-release-upgrader")
except Exception as e:
logging.warning("Error setting locales (%s)" % e)
# we test for DISPLAY here, QApplication does not throw a
# exception when run without DISPLAY but dies instead
if not "DISPLAY" in os.environ:
raise Exception("No DISPLAY in os.environ found")
# Force environment to make sure Qt uses suitable theming and UX.
os.environ["QT_PLATFORM_PLUGIN"] = "kde"
# For above settings to apply automatically we need to indicate that we
# are inside a full KDE session.
os.environ["KDE_FULL_SESSION"] = "TRUE"
# We also need to indicate version as otherwise KDElibs3 compatibility
# might kick in such as in QIconLoader.cpp:QString fallbackTheme.
os.environ["KDE_SESSION_VERSION"] = "6"
# Pretty much all of the above but for Qt6
os.environ["QT_QPA_PLATFORMTHEME"] = "kde"
self.app = QApplication(["ubuntu-release-upgrader"])
# Try to load default Qt translations so we don't have to worry about
# QStandardButton translations.
translator = QTranslator(self.app)
translator.load(QLocale.system(), 'qt', '_', '/usr/share/qt6/translations')
self.app.installTranslator(translator)
QUrlOpener().setupUrlHandles()
self.app.aboutToQuit.connect(QUrlOpener().teardownUrlHandles)
messageIcon = _icon("system-software-update",
fallbacks=["/usr/share/icons/oxygen/48x48/apps/system-software-update.png",
"/usr/share/icons/hicolor/48x48/apps/adept_manager.png"])
self.app.setWindowIcon(messageIcon)
self.window_main = UpgraderMainWindow()
self.window_main.show()
self.prev_step = None # keep a record of the latest step
self._opCacheProgress = KDEOpProgress(self.window_main.progressbar_cache, self.window_main.progress_text)
self._acquireProgress = KDEAcquireProgressAdapter(self)
self._installProgress = KDEInstallProgressAdapter(self)
# reasonable fault handler
sys.excepthook = self._handleException
self.window_main.showTerminalButton.setEnabled(False)
self.window_main.showTerminalButton.clicked.connect(self.showTerminal)
# init gettext
gettext.bindtextdomain("ubuntu-release-upgrader",localedir)
gettext.textdomain("ubuntu-release-upgrader")
self.translate_widget_children()
name = _OSRelease().result["PRETTY_NAME"]
if not name or name == "Ubuntu":
name = "Kubuntu"
to_dist = self.config.get("Sources", "To")
to_version = distro_info.UbuntuDistroInfo().version(to_dist)
title_string = self.window_main.label_title.text()
title_string = title_string.replace("Ubuntu", name)
title_string = title_string.replace("%s", to_version)
self.window_main.label_title.setText(title_string)
# setup terminal text in hidden by default spot
self.window_main.konsole_frame.hide()
self.konsole_frame_layout = QHBoxLayout(self.window_main.konsole_frame)
self.window_main.konsole_frame.setMinimumSize(600, 400)
self.terminal_text = DumbTerminal(self._installProgress, self.window_main.konsole_frame)
self.konsole_frame_layout.addWidget(self.terminal_text)
self.terminal_text.show()
# for some reason we need to start the main loop to get everything displayed
# this app mostly works with processEvents but run main loop briefly to keep it happily displaying all widgets
QTimer.singleShot(10, self.exitMainLoopMidFlight)
self.app.exec()
def exitMainLoopMidFlight(self):
# This is run shortly after startup. Do not add actual exit logic here!
print("exitMainLoopMidFlight")
self.app.exit()
def translate_widget_children(self, parentWidget=None):
if parentWidget == None:
parentWidget = self.window_main
if isinstance(parentWidget, QDialog) or isinstance(parentWidget, QWidget):
if str(parentWidget.windowTitle()) == "Error":
parentWidget.setWindowTitle( gettext.dgettext("kdelibs", "Error"))
else:
parentWidget.setWindowTitle(_( str(parentWidget.windowTitle()) ))
if parentWidget.children() != None:
for widget in parentWidget.children():
self.translate_widget(widget)
self.translate_widget_children(widget)
def translate_widget(self, widget):
if isinstance(widget, QLabel) or isinstance(widget, QPushButton):
if str(widget.text()) == "&Cancel":
kdelibs = gettext.translation(
"kdelibs", gettext.textdomain("kdelibs"), fallback=True)
widget.setText(unicode_gettext(kdelibs, "&Cancel"))
elif str(widget.text()) == "&Close":
kdelibs = gettext.translation(
"kdelibs", gettext.textdomain("kdelibs"), fallback=True)
widget.setText(unicode_gettext(kdelibs, "&Close"))
elif str(widget.text()) != "":
widget.setText( _(str(widget.text())).replace("_", "&") )
def _handleException(self, exctype, excvalue, exctb):
"""Crash handler."""
if (issubclass(exctype, KeyboardInterrupt) or
issubclass(exctype, SystemExit)):
return
# we handle the exception here, hand it to apport and run the
# apport gui manually after it because we kill u-m during the upgrade
# to prevent it from popping up for reboot notifications or FF restart
# notifications or somesuch
lines = traceback.format_exception(exctype, excvalue, exctb)
logging.error("not handled exception in KDE frontend:\n%s" % "\n".join(lines))
# we can't be sure that apport will run in the middle of a upgrade
# so we still show a error message here
apport_crash(exctype, excvalue, exctb)
if not run_apport():
tbtext = ''.join(traceback.format_exception(exctype, excvalue, exctb))
dialog = QDialog(self.window_main)
loadUi("dialog_error.ui", dialog)
self.translate_widget_children(self.dialog)
dialog.crash_detail.setText(tbtext)
# Make sure we have a suitable size depending on whether or not the view is shown
dialog.adjustSize()
dialog.exec()
sys.exit(1)
def showTerminal(self):
if self.window_main.konsole_frame.isVisible():
self.window_main.konsole_frame.hide()
self.window_main.showTerminalButton.setText(_("Show Terminal >>>"))
else:
self.window_main.konsole_frame.show()
self.window_main.showTerminalButton.setText(_("<<< Hide Terminal"))
self.window_main.adjustSize()
def getAcquireProgress(self):
return self._acquireProgress
def getInstallProgress(self, cache):
self._installProgress._cache = cache
return self._installProgress
def getOpCacheProgress(self):
return self._opCacheProgress
def update_status(self, msg):
self.window_main.label_status.setText(msg)
def hideStep(self, step):
image = getattr(self.window_main,"image_step%i" % step.value)
label = getattr(self.window_main,"label_step%i" % step.value)
image.hide()
label.hide()
def abort(self):
step = self.prev_step
if step:
image = getattr(self.window_main,"image_step%i" % step.value)
cancelIcon = _icon("dialog-cancel",
fallbacks=["/usr/share/icons/oxygen/base/16x16/actions/dialog-cancel.png",
"/usr/share/icons/crystalsvg/16x16/actions/cancel.png"])
image.setPixmap(cancelIcon.pixmap(16, 16))
image.show()
def setStep(self, step):
super(DistUpgradeViewKDE , self).setStep(step)
okIcon = _icon("dialog-ok",
fallbacks=["/usr/share/icons/oxygen/base/16x16/actions/dialog-ok.png",
"/usr/share/icons/crystalsvg/16x16/actions/ok.png"])
arrowIcon = _icon("arrow-right",
fallbacks=["/usr/share/icons/oxygen/base/16x16/actions/arrow-right.png",
"/usr/share/icons/crystalsvg/16x16/actions/1rightarrow.png"])
if self.prev_step:
image = getattr(self.window_main,"image_step%i" % self.prev_step.value)
label = getattr(self.window_main,"label_step%i" % self.prev_step.value)
image.setPixmap(okIcon.pixmap(16, 16))
image.show()
##arrow.hide()
self.prev_step = step
# show the an arrow for the current step and make the label bold
image = getattr(self.window_main,"image_step%i" % step.value)
label = getattr(self.window_main,"label_step%i" % step.value)
image.setPixmap(arrowIcon.pixmap(16, 16))
image.show()
label.setText("" + label.text() + "")
def information(self, summary, msg, extended_msg=None):
msg = "%s
%s" % (summary,msg)
dialogue = QDialog(self.window_main)
loadUi("dialog_error.ui", dialogue)
self.translate_widget_children(dialogue)
dialogue.label_error.setText(msg)
if extended_msg != None:
dialogue.textview_error.setText(extended_msg)
dialogue.textview_error.show()
else:
dialogue.textview_error.hide()
dialogue.setWindowTitle(_("Information"))
messageIcon = _icon("dialog-information",
fallbacks=["/usr/share/icons/oxygen/base/48x48/status/dialog-information.png",
"/usr/share/icons/crystalsvg/32x32/actions/messagebox_info.png"])
dialogue.image.setPixmap(messageIcon.pixmap(48, 48))
# Make sure we have a suitable size depending on whether or not the view is shown
dialogue.adjustSize()
dialogue.exec()
def error(self, summary, msg, extended_msg=None):
msg="%s
%s" % (summary, msg)
dialogue = QDialog(self.window_main)
loadUi("dialog_error.ui", dialogue)
self.translate_widget_children(dialogue)
dialogue.label_error.setText(msg)
if extended_msg != None:
dialogue.textview_error.setText(extended_msg)
dialogue.textview_error.show()
else:
dialogue.textview_error.hide()
messageIcon = _icon("dialog-error",
fallbacks=["/usr/share/icons/oxygen/base/48x48/status/dialog-error.png",
"/usr/share/icons/crystalsvg/32x32/actions/messagebox_critical.png"])
dialogue.image.setPixmap(messageIcon.pixmap(48, 48))
# Make sure we have a suitable size depending on whether or not the view is shown
dialogue.adjustSize()
dialogue.exec()
return False
def confirmChanges(self, summary, changes, downloadSize,
actions=None, removal_bold=True):
"""show the changes dialogue"""
# FIXME: add an allow list here for packages that we expect to be
# removed (how to calc this automatically?)
DistUpgradeView.confirmChanges(self, summary, changes,
downloadSize)
self.changesDialogue = QDialog(self.window_main)
loadUi("dialog_changes.ui", self.changesDialogue)
self.changesDialogue.treeview_details.hide()
self.changesDialogue.buttonBox.helpRequested.connect(self.showChangesDialogueDetails)
self.translate_widget_children(self.changesDialogue)
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(_("&Start Upgrade"))
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setIcon(QIcon())
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setText(_("Details") + " >>>")
messageIcon = _icon("dialog-warning",
fallbacks=["/usr/share/icons/oxygen/base/48x48/status/dialog-warning.png",
"/usr/share/icons/crystalsvg/32x32/actions/messagebox_warning.png"])
self.changesDialogue.question_pixmap.setPixmap(messageIcon.pixmap(48, 48))
if actions != None:
cancel = actions[0].replace("_", "")
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).setText(cancel)
confirm = actions[1].replace("_", "")
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText(confirm)
summaryText = "%s" % summary
self.changesDialogue.label_summary.setText(summaryText)
self.changesDialogue.label_changes.setText(self.confirmChangesMessage)
# fill in the details
self.changesDialogue.treeview_details.clear()
self.changesDialogue.treeview_details.setHeaderLabels(["Packages"])
self.changesDialogue.treeview_details.header().hide()
for rm in self.toRemove:
self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Remove %s") % rm.name]) )
for rm in self.toRemoveAuto:
self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Remove (was auto installed) %s") % rm.name]) )
for inst in self.toInstall:
self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Install %s") % inst.name]) )
for up in self.toUpgrade:
self.changesDialogue.treeview_details.insertTopLevelItem(0, QTreeWidgetItem(self.changesDialogue.treeview_details, [_("Upgrade %s") % up.name]) )
# Use a suitable size for the window given the current content.
self.changesDialogue.adjustSize()
#FIXME resize label, stop it being shrinkable
res = self.changesDialogue.exec()
if res == QDialog.DialogCode.Accepted:
return True
return False
def showChangesDialogueDetails(self):
if self.changesDialogue.treeview_details.isVisible():
self.changesDialogue.treeview_details.hide()
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setText(_("Details") + " >>>")
else:
self.changesDialogue.treeview_details.show()
self.changesDialogue.buttonBox.button(QDialogButtonBox.StandardButton.Help).setText("<<< " + _("Details"))
self.changesDialogue.adjustSize()
def askYesNoQuestion(self, summary, msg, default='No'):
answer = QMessageBox.question(self.window_main, summary, "" + msg, QMessageBox.StandardButton.Yes|QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
if answer == QMessageBox.StandardButton.Yes:
return True
return False
def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
messageBox = QMessageBox(QMessageBox.Warning, summary, msg, QMessageBox.StandardButton.NoButton, self.window_main)
continueButton = messageBox.addButton(QMessageBox.StandardButton.Apply)
cancelButton = messageBox.addButton(QMessageBox.StandardButton.Cancel)
continueButton.setText(_("Continue"))
if default == 'Cancel':
messageBox.setDefaultButton(cancelButton)
else:
messageBox.setDefaultButton(continueButton)
if summary is None:
flags = messageBox.windowFlags()
messageBox.setWindowFlags(flags | Qt.FramelessWindowHint)
answer = messageBox.exec()
if answer == QMessageBox.StandardButton.Apply:
return True
return False
def confirmRestart(self):
messageBox = QMessageBox(QMessageBox.Icon.Question, _("Restart required"), _("Restart the system to complete the upgrade"), QMessageBox.StandardButton.NoButton, self.window_main)
yesButton = messageBox.addButton(QMessageBox.StandardButton.Yes)
noButton = messageBox.addButton(QMessageBox.StandardButton.No)
yesButton.setText(_("_Restart Now").replace("_", "&"))
noButton.setText(gettext.dgettext("kdelibs", "&Close"))
answer = messageBox.exec()
if answer == QMessageBox.StandardButton.Yes:
return True
return False
def processEvents(self):
QApplication.processEvents()
def pulseProgress(self, finished=False):
# FIXME: currently we do nothing here because this is
# run in a different python thread and QT explodes if the UI is
# touched from a non QThread
pass
def on_window_main_delete_event(self):
#FIXME make this user friendly
text = _("""Cancel the running upgrade?
The system could be in an unusable state if you cancel the upgrade. You are strongly advised to resume the upgrade.""")
text = text.replace("\n", "
")
cancel = QMessageBox.warning(self.window_main, _("Cancel Upgrade?"), text, QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.No)
if cancel == QMessageBox.StandardButton.Yes:
return True
return False
./DistUpgradeViewNonInteractive.py 0000644 0003721 0004705 00000032001 15000447260 016771 0 ustar buildd buildd # DistUpgradeView.py
#
# Copyright (c) 2004,2005 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt
import apt_pkg
import logging
import locale
import time
import sys
import os
import pty
import select
import subprocess
import copy
import apt.progress
from configparser import NoSectionError, NoOptionError
from subprocess import PIPE, Popen
from .DistUpgradeView import DistUpgradeView, InstallProgress, AcquireProgress
from .telemetry import get as get_telemetry
from .DistUpgradeConfigParser import DistUpgradeConfig
class NonInteractiveAcquireProgress(AcquireProgress):
def update_status(self, uri, descr, shortDescr, status):
AcquireProgress.update_status(self, uri, descr, shortDescr, status)
#logging.debug("Fetch: updateStatus %s %s" % (uri, status))
if status == apt_pkg.STAT_DONE:
print("fetched %s (%.2f/100) at %sb/s" % (
uri, self.percent, apt_pkg.size_to_str(int(self.current_cps))))
if sys.stdout.isatty():
sys.stdout.flush()
class NonInteractiveInstallProgress(InstallProgress):
"""
Non-interactive version of the install progress class
This ensures that conffile prompts are handled and that
hanging scripts are killed after a (long) timeout via ctrl-c
"""
def __init__(self, logdir):
InstallProgress.__init__(self)
logging.debug("setting up environ for non-interactive use")
if "DEBIAN_FRONTEND" not in os.environ:
os.environ["DEBIAN_FRONTEND"] = "noninteractive"
os.environ["APT_LISTCHANGES_FRONTEND"] = "none"
os.environ["RELEASE_UPGRADER_NO_APPORT"] = "1"
self.config = DistUpgradeConfig(".")
self.logdir = logdir
self.install_run_number = 0
try:
if self.config.getWithDefault("NonInteractive","ForceOverwrite", False):
apt_pkg.config.set("DPkg::Options::","--force-overwrite")
except (NoSectionError, NoOptionError):
pass
# more debug
#apt_pkg.config.set("Debug::pkgOrderList","true")
#apt_pkg.config.set("Debug::pkgDPkgPM","true")
# default to 2400 sec timeout
self.timeout = 2400
try:
self.timeout = self.config.getint("NonInteractive","TerminalTimeout")
except Exception:
pass
def error(self, pkg, errormsg):
logging.error("got a error from dpkg for pkg: '%s': '%s'" % (pkg, errormsg))
# check if re-run of maintainer script is requested
if not self.config.getWithDefault(
"NonInteractive","DebugBrokenScripts", False):
return
# re-run maintainer script with sh -x/perl debug to get a better
# idea what went wrong
#
# FIXME: this is just a approximation for now, we also need
# to pass:
# - a version after remove (if upgrade to new version)
#
# not everything is a shell or perl script
#
# if the new preinst fails, its not yet in /var/lib/dpkg/info
# so this is inaccurate as well
environ = copy.copy(os.environ)
environ["PYCENTRAL"] = "debug"
cmd = []
# find what maintainer script failed
if "post-installation" in errormsg:
prefix = "/var/lib/dpkg/info/"
name = "postinst"
argument = "configure"
maintainer_script = "%s/%s.%s" % (prefix, pkg, name)
elif "pre-installation" in errormsg:
prefix = "/var/lib/dpkg/tmp.ci/"
#prefix = "/var/lib/dpkg/info/"
name = "preinst"
argument = "install"
maintainer_script = "%s/%s" % (prefix, name)
elif "pre-removal" in errormsg:
prefix = "/var/lib/dpkg/info/"
name = "prerm"
argument = "remove"
maintainer_script = "%s/%s.%s" % (prefix, pkg, name)
elif "post-removal" in errormsg:
prefix = "/var/lib/dpkg/info/"
name = "postrm"
argument = "remove"
maintainer_script = "%s/%s.%s" % (prefix, pkg, name)
else:
print("UNKNOWN (trigger?) dpkg/script failure for %s (%s) " % (pkg, errormsg))
return
# find out about the interpreter
if not os.path.exists(maintainer_script):
logging.error("can not find failed maintainer script '%s' " % maintainer_script)
return
with open(maintainer_script) as f:
interp = f.readline()[2:].strip().split()[0]
if ("bash" in interp) or ("/bin/sh" in interp):
debug_opts = ["-ex"]
elif ("perl" in interp):
debug_opts = ["-d"]
environ["PERLDB_OPTS"] = "AutoTrace NonStop"
else:
logging.warning("unknown interpreter: '%s'" % interp)
# check if debconf is used and fiddle a bit more if it is
with open(maintainer_script) as f:
maintainer_script_text = f.read()
if ". /usr/share/debconf/confmodule" in maintainer_script_text:
environ["DEBCONF_DEBUG"] = "developer"
environ["DEBIAN_HAS_FRONTEND"] = "1"
interp = "/usr/share/debconf/frontend"
debug_opts = ["sh","-ex"]
# build command
cmd.append(interp)
cmd.extend(debug_opts)
cmd.append(maintainer_script)
cmd.append(argument)
# check if we need to pass a version
if name == "postinst":
version = Popen("dpkg-query -s %s|grep ^Config-Version" % pkg,
shell=True, stdout=PIPE,
universal_newlines=True).communicate()[0]
if version:
cmd.append(version.split(":",1)[1].strip())
elif name == "preinst":
pkg = os.path.basename(pkg)
pkg = pkg.split("_")[0]
version = Popen("dpkg-query -s %s|grep ^Version" % pkg,
shell=True, stdout=PIPE,
universal_newlines=True).communicate()[0]
if version:
cmd.append(version.split(":",1)[1].strip())
logging.debug("re-running '%s' (%s)" % (cmd, environ))
ret = subprocess.call(cmd, env=environ)
logging.debug("%s script returned: %s" % (name,ret))
def conffile(self, current, new):
logging.warning("got a conffile-prompt from dpkg for file: '%s'" %
current)
# looks like we have a race here *sometimes*
time.sleep(5)
try:
# don't overwrite
os.write(self.master_fd, b"n\n")
logging.warning("replied no to the conffile-prompt for file: '%s'" %
current)
except Exception as e:
logging.error("error '%s' when trying to write to the conffile"%e)
def start_update(self):
InstallProgress.start_update(self)
self.last_activity = time.time()
progress_log = self.config.getWithDefault("NonInteractive","DpkgProgressLog", False)
if progress_log:
fullpath = os.path.join(self.logdir, "dpkg-progress.%s.log" % self.install_run_number)
logging.debug("writing dpkg progress log to '%s'" % fullpath)
self.dpkg_progress_log = open(fullpath, "w")
else:
self.dpkg_progress_log = open(os.devnull, "w")
self.dpkg_progress_log.write("%s: Start\n" % time.time())
def finish_update(self):
InstallProgress.finish_update(self)
self.dpkg_progress_log.write("%s: Finished\n" % time.time())
self.dpkg_progress_log.close()
self.install_run_number += 1
def status_change(self, pkg, percent, status_str):
self.dpkg_progress_log.write("%s:%s:%s:%s\n" % (time.time(),
percent,
pkg,
status_str))
def update_interface(self):
InstallProgress.update_interface(self)
if self.statusfd == None:
return
if (self.last_activity + self.timeout) < time.time():
logging.warning("no activity %s seconds (%s) - sending ctrl-c" % (
self.timeout, self.status))
# ctrl-c
os.write(self.master_fd,chr(3))
# read master fd and write to stdout so that terminal output
# actualy works
res = select.select([self.master_fd],[],[],0.1)
while len(res[0]) > 0:
self.last_activity = time.time()
try:
s = os.read(self.master_fd, 1)
sys.stdout.write("%s" % s.decode(
locale.getpreferredencoding(), errors='ignore'))
except OSError:
# happens after we are finished because the fd is closed
return
res = select.select([self.master_fd],[],[],0.1)
sys.stdout.flush()
def fork(self):
logging.debug("doing a pty.fork()")
# some maintainer scripts fail without
os.environ["TERM"] = "dumb"
# unset PAGER so that we can do "diff" in the dpkg prompt
os.environ["PAGER"] = "true"
(self.pid, self.master_fd) = pty.fork()
if self.pid != 0:
logging.debug("pid is: %s" % self.pid)
return self.pid
class DistUpgradeViewNonInteractive(DistUpgradeView):
" non-interactive version of the upgrade view "
def __init__(self, datadir=None, logdir=None):
DistUpgradeView.__init__(self)
get_telemetry().set_updater_type('NonInteractive')
self.config = DistUpgradeConfig(".")
self._acquireProgress = NonInteractiveAcquireProgress()
self._installProgress = NonInteractiveInstallProgress(logdir)
self._opProgress = apt.progress.base.OpProgress()
sys.__excepthook__ = self.excepthook
def excepthook(self, type, value, tb):
" on uncaught exceptions -> print error and reboot "
import traceback
logging.exception("got exception '%s': %s " % (type, value))
lines = traceback.format_exception(type, value, tb)
logging.error("not handled exception:\n%s" % "".join(lines))
#sys.excepthook(type, value, tb)
self.confirmRestart()
def getOpCacheProgress(self):
" return a OpProgress() subclass for the given graphic"
return self._opProgress
def getAcquireProgress(self):
" return an acquire progress object "
return self._acquireProgress
def getInstallProgress(self, cache=None):
" return a install progress object "
return self._installProgress
def updateStatus(self, msg):
""" update the current status of the distUpgrade based
on the current view
"""
pass
def setStep(self, step):
""" we have 5 steps current for a upgrade:
1. Analyzing the system
2. Updating repository information
3. Performing the upgrade
4. Post upgrade stuff
5. Complete
"""
super(DistUpgradeViewNonInteractive, self).setStep(step)
pass
def confirmChanges(self, summary, changes, downloadSize,
actions=None, removal_bold=True):
DistUpgradeView.confirmChanges(self, summary, changes,
downloadSize, actions)
logging.debug("toinstall: '%s'" % [p.name for p in self.toInstall])
logging.debug("toupgrade: '%s'" % [p.name for p in self.toUpgrade])
logging.debug("toremove: '%s'" % [p.name for p in self.toRemove])
return True
def askYesNoQuestion(self, summary, msg, default='No'):
" ask a Yes/No question and return True on 'Yes' "
# if this gets enabled upgrades over ssh with the non-interactive
# frontend will no longer work
#if default.lower() == "no":
# return False
return True
def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
return True
def confirmRestart(self):
" generic ask about the restart, can be overridden "
logging.debug("confirmRestart() called")
# rebooting here makes sense if we run e.g. in qemu
return self.config.getWithDefault("NonInteractive","RealReboot", False)
def error(self, summary, msg, extended_msg=None):
" display a error "
logging.error("%s %s (%s)" % (summary, msg, extended_msg))
def abort(self):
logging.error("view.abort called")
./DistUpgradeViewText.py 0000644 0003721 0004705 00000023465 15000447260 015003 0 ustar buildd buildd # DistUpgradeViewText.py
#
# Copyright (c) 2004-2006 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import errno
import sys
import logging
import subprocess
from gettext import dgettext
import apt
import os
from .DistUpgradeApport import run_apport, apport_crash
from .DistUpgradeView import (
AcquireProgress,
DistUpgradeView,
ENCODING,
InstallProgress,
)
from .telemetry import get as get_telemetry
import apt.progress
import gettext
from .DistUpgradeGettext import gettext as _
from .utils import twrap
def readline():
""" py2/py3 compatible readline from stdin """
sys.stdout.flush()
try:
s = input()
except EOFError:
s = ''
if hasattr(s, "decode"):
return s.decode(ENCODING, "backslashreplace")
return s
class TextAcquireProgress(AcquireProgress, apt.progress.text.AcquireProgress):
def __init__(self):
apt.progress.text.AcquireProgress.__init__(self)
AcquireProgress.__init__(self)
def pulse(self, owner):
apt.progress.text.AcquireProgress.pulse(self, owner)
AcquireProgress.pulse(self, owner)
return True
class TextInstallProgress(InstallProgress):
# percent step when progress is reported (to avoid screen spam)
MIN_REPORTING = 5
def __init__(self, *args, **kwargs):
super(TextInstallProgress, self).__init__(*args, **kwargs)
self._prev_percent = 0
def status_change(self, pkg, percent, status):
if self._prev_percent + self.MIN_REPORTING < percent:
# FIXME: move into ubuntu-release-upgrader after trusty
domain = "libapt-pkg4.12"
progress_str = dgettext(domain, "Progress: [%3i%%]") % int(percent)
sys.stdout.write("\r\n%s\r\n" % progress_str)
self._prev_percent = percent
class DistUpgradeViewText(DistUpgradeView):
""" text frontend of the distUpgrade tool """
def __init__(self, datadir=None, logdir=None):
# indicate that we benefit from using gnu screen
self.needs_screen = True
get_telemetry().set_updater_type('Text')
# its important to have a debconf frontend for
# packages like "quagga"
if "DEBIAN_FRONTEND" not in os.environ:
os.environ["DEBIAN_FRONTEND"] = "dialog"
if not datadir or datadir == '.':
localedir=os.path.join(os.getcwd(),"mo")
else:
localedir="/usr/share/locale/ubuntu-release-upgrader"
try:
gettext.bindtextdomain("ubuntu-release-upgrader", localedir)
gettext.textdomain("ubuntu-release-upgrader")
except Exception as e:
logging.warning("Error setting locales (%s)" % e)
self.last_step = None # keep a record of the latest step
self._opCacheProgress = apt.progress.text.OpProgress()
self._acquireProgress = TextAcquireProgress()
self._installProgress = TextInstallProgress()
sys.excepthook = self._handleException
#self._process_events_tick = 0
def _handleException(self, type, value, tb):
# we handle the exception here, hand it to apport and run the
# apport gui manually after it because we kill u-n during the upgrade
# to prevent it from poping up for reboot notifications or FF restart
# notifications or somesuch
import traceback
print()
lines = traceback.format_exception(type, value, tb)
logging.error("not handled exception:\n%s" % "\n".join(lines))
apport_crash(type, value, tb)
if not run_apport():
self.error(_("A fatal error occurred"),
_("Please report this as a bug and include the "
"files /var/log/dist-upgrade/main.log and "
"/var/log/dist-upgrade/apt.log "
"in your report. The upgrade has aborted.\n"
"Your original sources.list was saved in "
"/etc/apt/sources.list.distUpgrade."),
"\n".join(lines))
sys.exit(1)
def getAcquireProgress(self):
return self._acquireProgress
def getInstallProgress(self, cache):
self._installProgress._cache = cache
return self._installProgress
def getOpCacheProgress(self):
return self._opCacheProgress
def updateStatus(self, msg):
print()
print(msg)
sys.stdout.flush()
def abort(self):
print()
print(_("Aborting"))
def setStep(self, step):
super(DistUpgradeViewText, self).setStep(step)
self.last_step = step
def information(self, summary, msg, extended_msg=None):
print()
print(twrap(summary))
print(twrap(msg))
if extended_msg:
print(twrap(extended_msg))
print(_("To continue please press [ENTER]"))
readline()
def error(self, summary, msg, extended_msg=None):
print()
print(twrap(summary))
print(twrap(msg))
if extended_msg:
print(twrap(extended_msg))
return False
def showInPager(self, output):
""" helper to show output in a pager """
# we need to send a encoded str (bytes in py3) to the pipe
# LP: #1068389
if not isinstance(output, bytes):
output = output.encode(ENCODING)
for pager in ["/usr/bin/sensible-pager", "/bin/more"]:
if os.path.exists(pager):
p = subprocess.Popen([pager,"-"],stdin=subprocess.PIPE)
# if lots of data is shown, we need to catch EPIPE
try:
p.stdin.write(output)
p.stdin.close()
p.wait()
except IOError as e:
if e.errno != errno.EPIPE:
raise
return
# if we don't have a pager, just print
print(output)
def confirmChanges(self, summary, changes, downloadSize,
actions=None, removal_bold=True):
DistUpgradeView.confirmChanges(self, summary, changes,
downloadSize, actions)
print()
print(twrap(summary))
print(twrap(self.confirmChangesMessage))
print(" %s %s" % (_("Continue [yN] "), _("Details [d]")), end="")
while True:
res = readline().strip().lower()
# TRANSLATORS: the "y" is "yes"
if res.startswith(_("y")):
return True
# TRANSLATORS: the "n" is "no"
elif not res or res.startswith(_("n")):
return False
# TRANSLATORS: the "d" is "details"
elif res.startswith(_("d")):
output = ""
if len(self.toRemove) > 0:
output += "\n"
output += twrap(
_("Remove: %s\n") % " ".join([p.name for p in self.toRemove]),
subsequent_indent=' ')
if len(self.toRemoveAuto) > 0:
output += twrap(
_("Remove (was auto installed) %s") % " ".join([p.name for p in self.toRemoveAuto]),
subsequent_indent=' ')
output += "\n"
if len(self.toInstall) > 0:
output += "\n"
output += twrap(
_("Install: %s\n") % " ".join([p.name for p in self.toInstall]),
subsequent_indent=' ')
if len(self.toUpgrade) > 0:
output += "\n"
output += twrap(
_("Upgrade: %s\n") % " ".join([p.name for p in self.toUpgrade]),
subsequent_indent=' ')
self.showInPager(output)
print("%s %s" % (_("Continue [yN] "), _("Details [d]")), end="")
def askYesNoQuestion(self, summary, msg, default='No'):
print()
if summary:
print(twrap(summary))
print(twrap(msg))
if default == 'No':
print(_("Continue [yN] "), end="")
res = readline()
# TRANSLATORS: first letter of a positive (yes) answer
if res.strip().lower().startswith(_("y")):
return True
return False
else:
print(_("Continue [Yn] "), end="")
res = readline()
# TRANSLATORS: first letter of a negative (no) answer
if res.strip().lower().startswith(_("n")):
return False
return True
def askCancelContinueQuestion(self, summary, msg, default='Cancel'):
return self.askYesNoQuestion(summary, msg,
default='No' if default == 'Cancel' else 'Yes')
# FIXME: when we need this most the resolver is writing debug logs
# and we redirect stdout/stderr
# def processEvents(self):
# #time.sleep(0.2)
# anim = [".","o","O","o"]
# anim = ["\\","|","/","-","\\","|","/","-"]
# self._process_events_tick += 1
# if self._process_events_tick >= len(anim):
# self._process_events_tick = 0
# sys.stdout.write("[%s]" % anim[self._process_events_tick])
# sys.stdout.flush()
def confirmRestart(self):
return self.askYesNoQuestion(_("Restart required"),
_("To finish the upgrade, a restart is "
"required.\n"
"If you select 'y' the system "
"will be restarted."), default='No')
./EOLReleaseAnnouncement 0000644 0003721 0004705 00000004376 15003755605 014744 0 ustar buildd buildd = Ubuntu 24.10 'Questing Quokka' is no longer supported =
You are about to upgrade to a version of Ubuntu that is no longer
supported.
This release of Ubuntu is '''no longer supported''' by Canonical. The
support timeframe is between 9 months and 5 years after the initial
release. You will not receive security updates or critical
bugfixes. See https://ubuntu.com/releaseendoflife for details.
It is still possible to upgrade this version and eventually you will
be able to upgrade to a supported release of Ubuntu.
Alternatively you may want to consider to reinstall the machine to the
latest version, for more information on this, visit:
https://ubuntu.com/desktop/get-ubuntu
For pre-installed system you may want to contact the manufacturer
for instructions.
== Feedback and Helping ==
If you would like to help shape Ubuntu, take a look at the list of
ways you can participate at
https://ubuntu.com/community/contribute
Your comments, bug reports, patches and suggestions will help ensure
that our next release is the best release of Ubuntu ever. If you feel
that you have found a bug please read:
https://help.ubuntu.com/community/ReportingBugs
Then report bugs using apport in Ubuntu. For example:
ubuntu-bug linux
will open a bug report in Launchpad regarding the linux package.
If you have a question, or if you think you may have found a bug but aren't
sure, first try to reach out on one of the communication channels. Matrix is the
go-to for instant chatting, while Discourse would be more approriate for long
discussions in a more asynchronous way. Otherwise you can still join the #ubuntu
IRC channel on Libera.Chat, send an email to the Ubuntu Users mailing list, or
find some help on the Ubuntu forums:
https://ubuntu.com/community/communications/matrix
https://discourse.ubuntu.com/
https://help.ubuntu.com/community/InternetRelayChat
https://lists.ubuntu.com/mailman/listinfo/ubuntu-users
https://ubuntuforums.org/
== More Information ==
You can find out more about Ubuntu on our website, IRC channel and wiki.
If you're new to Ubuntu, please visit:
https://ubuntu.com/
To sign up for future Ubuntu announcements, please subscribe to Ubuntu's
very low volume announcement list at:
https://lists.ubuntu.com/mailman/listinfo/ubuntu-announce
./EOLReleaseAnnouncement.html 0000644 0003721 0004705 00000006460 15003755605 015703 0 ustar buildd buildd
Ubuntu 24.10 'Questing Quokka' is no longer supported
Ubuntu 24.10 'Questing Quokka' is no longer supported
You are about to upgrade to a version of Ubuntu that is no longer
supported.
This release of Ubuntu is no longer supported by Canonical. The
support timeframe is between 9 months and 5 years after the initial
release. You will not receive security updates or critical
bugfixes. See https://ubuntu.com/releaseendoflife for details.
It is still possible to upgrade this version and eventually you will
be able to upgrade to a supported release of Ubuntu.
Alternatively you may want to consider to reinstall the machine to the
latest version, for more information on this, visit:
https://ubuntu.com/desktop/get-ubuntu
For pre-installed system you may want to contact the manufacturer
for instructions.
Feedback and Helping
If you would like to help shape Ubuntu, take a look at the list of
ways you can participate at
https://ubuntu.com/community/contribute
Your comments, bug reports, patches and suggestions will help ensure
that our next release is the best release of Ubuntu ever. If you feel
that you have found a bug please read:
https://help.ubuntu.com/community/ReportingBugs
Then report bugs using apport in Ubuntu. For example:
ubuntu-bug linux
will open a bug report in Launchpad regarding the linux package.
If you have a question, or if you think you may have found a bug but aren't
sure, first try to reach out on one of the communication channels. Matrix is the
go-to for instant chatting, while Discourse would be more approriate for long
discussions in a more asynchronous way. Otherwise you can still join the #ubuntu
IRC channel on Libera.Chat, send an email to the Ubuntu Users mailing list, or
find some help on the Ubuntu forums:
https://ubuntu.com/community/communications/matrix
https://discourse.ubuntu.com/
https://help.ubuntu.com/community/InternetRelayChat
https://lists.ubuntu.com/mailman/listinfo/ubuntu-users
https://ubuntuforums.org/
More Information
You can find out more about Ubuntu on our website, IRC channel and wiki.
If you're new to Ubuntu, please visit:
https://ubuntu.com/
To sign up for future Ubuntu announcements, please subscribe to Ubuntu's
very low volume announcement list at:
https://lists.ubuntu.com/mailman/listinfo/ubuntu-announce
./GtkProgress.py 0000644 0003721 0004705 00000007664 14635360132 013352 0 ustar buildd buildd # GtkProgress.py
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
#
# Copyright (c) 2004,2005 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
from gi.repository import Gtk, Gdk
import apt
import os
from gettext import gettext as _
from .utils import humanize_size
from .SimpleGtk3builderApp import SimpleGtkbuilderApp
class GtkAcquireProgress(apt.progress.base.AcquireProgress):
def __init__(self, parent, datadir, summary="", descr=""):
uifile = os.path.join(datadir, "gtkbuilder", "AcquireProgress.ui")
self.widgets = SimpleGtkbuilderApp(uifile, "ubuntu-release-upgrader")
# if this is set to false the download will cancel
self._continue = True
# init vars here
# FIXME: find a more elegant way, this sucks
self.summary = self.widgets.label_fetch_summary
self.status = self.widgets.label_fetch_status
# we need to connect the signal manual here, it won't work
# from the main window auto-connect
self.widgets.button_fetch_cancel.connect(
"clicked", self.on_button_fetch_cancel_clicked)
self.progress = self.widgets.progressbar_fetch
self.window_fetch = self.widgets.window_fetch
self.window_fetch.set_transient_for(parent)
self.window_fetch.realize()
self.window_fetch.get_window().set_functions(Gdk.WMFunction.MOVE)
# set summary
if summary != "":
self.summary.set_markup("%s \n\n%s" %
(summary, descr))
def start(self):
self.progress.set_fraction(0)
self.window_fetch.show()
def stop(self):
self.window_fetch.hide()
def on_button_fetch_cancel_clicked(self, widget):
self._continue = False
def pulse(self, owner):
apt.progress.base.AcquireProgress.pulse(self, owner)
current_item = self.current_items + 1
if current_item > self.total_items:
current_item = self.total_items
if self.current_cps > 0:
status_text = (_("Downloading file %(current)li of %(total)li "
"with %(speed)s/s") % {
"current": current_item,
"total": self.total_items,
"speed": humanize_size(self.current_cps)})
else:
status_text = (_("Downloading file %(current)li of %(total)li") %
{"current": current_item,
"total": self.total_items})
self.progress.set_fraction(
(self.current_bytes + self.current_items) /
float(self.total_bytes + self.total_items))
self.status.set_markup("%s" % status_text)
# TRANSLATORS: show the remaining time in a progress bar:
#if self.current_cps > 0:
# eta = ((self.total_bytes + self.current_bytes) /
# float(self.current_cps))
#else:
# eta = 0.0
#self.progress.set_text(_("About %s left" % (apt_pkg.TimeToStr(eta))))
# FIXME: show remaining time
self.progress.set_text("")
while Gtk.events_pending():
Gtk.main_iteration()
return self._continue
./MetaRelease.py 0000644 0000000 0000000 00000041763 14740025451 012467 0 ustar root root # MetaRelease.py
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
#
# Copyright (c) 2004,2005 Canonical
#
# Author: Michael Vogt
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
import apt
import apt_pkg
import distro_info
import configparser
from http.client import BadStatusLine
import logging
import email.utils
import os
import socket
import sys
import time
import threading
from urllib.parse import quote
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from .utils import (
get_lang,
get_dist,
get_dist_version,
get_ubuntu_flavor,
get_ubuntu_flavor_name,
)
class MetaReleaseParseError(Exception):
pass
class Dist(object):
def __init__(self, name, version, date, supported):
self.name = name
self.version = version
self.date = date
self.supported = supported
self.releaseNotesURI = None
self.releaseNotesHtmlUri = None
self.upgradeTool = None
self.upgradeToolSig = None
# the server may report that the upgrade is broken currently
self.upgrade_broken = None
class MetaReleaseCore(object):
"""
A MetaReleaseCore object abstracts the list of released
distributions.
"""
DEBUG = "DEBUG_UPDATE_MANAGER" in os.environ
# some constants
CONF = "/etc/update-manager/release-upgrades"
CONF_METARELEASE = "/etc/update-manager/meta-release"
def __init__(
self,
useDevelopmentRelease=False,
useProposed=False,
debug=False,
forceLTS=False,
forceDownload=False,
cache=None,
):
if debug:
self.DEBUG = True
self._debug(
"MetaRelease.__init__() useDevel=%s useProposed=%s"
% (useDevelopmentRelease, useProposed)
)
# force download instead of sending if-modified-since
self.forceDownload = forceDownload
self.useDevelopmentRelease = useDevelopmentRelease
# information about the available dists
self.downloaded = threading.Event()
self.upgradable_to = None
self.new_dist = None
if cache is None:
cache = apt.Cache()
self.flavor = get_ubuntu_flavor(cache=cache)
self.flavor_name = get_ubuntu_flavor_name(cache=cache)
self.current_dist_name = get_dist()
self.current_dist_version = get_dist_version()
self.no_longer_supported = None
self.prompt = None
# default (if the conf file is missing)
base_uri = "https://changelogs.ubuntu.com/"
self.METARELEASE_URI = base_uri + "meta-release"
self.METARELEASE_URI_LTS = base_uri + "meta-release-lts"
self.METARELEASE_URI_UNSTABLE_POSTFIX = "-development"
self.METARELEASE_URI_PROPOSED_POSTFIX = "-proposed"
# check the meta-release config first
parser = configparser.ConfigParser()
if os.path.exists(self.CONF_METARELEASE):
try:
parser.read(self.CONF_METARELEASE)
except configparser.Error as e:
sys.stderr.write(
"ERROR: failed to read '%s':\n%s"
% (self.CONF_METARELEASE, e)
)
return
# make changing the metarelease file and the location
# for the files easy
if parser.has_section("METARELEASE"):
sec = "METARELEASE"
for k in [
"URI",
"URI_LTS",
"URI_UNSTABLE_POSTFIX",
"URI_PROPOSED_POSTFIX",
]:
if parser.has_option(sec, k):
self._debug(
"%s: %s "
% (self.CONF_METARELEASE, parser.get(sec, k))
)
setattr(self, "%s_%s" % (sec, k), parser.get(sec, k))
# check the config file first to figure if we want lts upgrades only
parser = configparser.ConfigParser()
if os.path.exists(self.CONF):
try:
parser.read(self.CONF)
except configparser.Error as e:
sys.stderr.write(
"ERROR: failed to read '%s':\n%s" % (self.CONF, e)
)
return
# now check which specific url to use
if parser.has_option("DEFAULT", "Prompt"):
prompt = parser.get("DEFAULT", "Prompt").lower()
if prompt == "never" or prompt == "no":
self.prompt = "never"
# nothing to do for this object
# FIXME: what about no longer supported?
self.downloaded.set()
return
elif prompt == "lts":
self.prompt = "lts"
# the Prompt=lts setting only makes sense when running on
# a LTS, otherwise it would result in users not receiving
# any distro upgrades
di = distro_info.UbuntuDistroInfo()
if di.is_lts(self.current_dist_name):
self.METARELEASE_URI = self.METARELEASE_URI_LTS
else:
self._debug("Prompt=lts for non-LTS, ignoring")
else:
self.prompt = "normal"
# needed for the _tryUpgradeSelf() code in DistUpgradeController
if forceLTS:
self.METARELEASE_URI = self.METARELEASE_URI_LTS
# devel and proposed "just" change the postfix
if useDevelopmentRelease:
self.METARELEASE_URI += self.METARELEASE_URI_UNSTABLE_POSTFIX
elif useProposed:
self.METARELEASE_URI += self.METARELEASE_URI_PROPOSED_POSTFIX
self._debug("metarelease-uri: %s" % self.METARELEASE_URI)
self.metarelease_information = None
if not self._buildMetaReleaseFile():
self._debug("_buildMetaReleaseFile failed")
return
# we start the download thread here and we have a timeout
threading.Thread(target=self.download).start()
# threading.Thread(target=self.check).start()
def _buildMetaReleaseFile(self):
# build the metarelease_file name
self.METARELEASE_FILE = os.path.join(
"/var/lib/update-manager/", os.path.basename(self.METARELEASE_URI)
)
# check if we can write to the global location, if not,
# write to homedir
try:
open(self.METARELEASE_FILE, "a").close()
except IOError:
cache_dir = os.getenv(
"XDG_CACHE_HOME", os.path.expanduser("~/.cache")
)
# Take special care when creating this directory; ~/.cache needs
# to be created with mode 0700, but the other directories do
# not.
cache_parent_dir = os.path.split(cache_dir)[0]
if not os.path.exists(cache_parent_dir):
try:
os.makedirs(cache_parent_dir)
except OSError as e:
sys.stderr.write("mkdir() failed: '%s'" % e)
return False
if not os.path.exists(cache_dir):
try:
os.mkdir(cache_dir, 0o700)
except OSError as e:
sys.stderr.write("mkdir() failed: '%s'" % e)
return False
path = os.path.join(cache_dir, "update-manager-core")
if not os.path.exists(path):
try:
os.mkdir(path)
except OSError as e:
sys.stderr.write("mkdir() failed: '%s'" % e)
return False
self.METARELEASE_FILE = os.path.join(
path, os.path.basename(self.METARELEASE_URI)
)
# if it is empty, remove it to avoid I-M-S hits on empty file
try:
if os.path.getsize(self.METARELEASE_FILE) == 0:
os.unlink(self.METARELEASE_FILE)
except Exception:
pass
return True
def dist_no_longer_supported(self, dist):
"""virtual function that is called when the distro is no longer
supported
"""
self.no_longer_supported = dist
def new_dist_available(self, dist):
"""virtual function that is called when a new distro release
is available
"""
self.new_dist = dist
def parse(self):
self._debug("MetaRelease.parse()")
current_dist_name = self.current_dist_name
self._debug("current dist name: '%s'" % current_dist_name)
current_dist = None
dists = []
# parse the metarelease_information file
index_tag = apt_pkg.TagFile(self.metarelease_information)
try:
while index_tag.step():
for required_key in ("Dist", "Version", "Supported", "Date"):
if required_key not in index_tag.section:
raise MetaReleaseParseError(
"Required key '%s' missing" % required_key
)
name = index_tag.section["Dist"]
self._debug("found distro name: '%s'" % name)
rawdate = index_tag.section["Date"]
parseddate = list(email.utils.parsedate(rawdate))
parseddate[8] = 0 # assume no DST
date = time.mktime(tuple(parseddate))
supported = int(index_tag.section["Supported"])
version = index_tag.section["Version"]
# add the information to a new date object
dist = Dist(name, version, date, supported)
if "ReleaseNotes" in index_tag.section:
dist.releaseNotesURI = index_tag.section["ReleaseNotes"]
lang = get_lang()
if lang:
dist.releaseNotesURI += "?lang=%s" % lang
if "ReleaseNotesHtml" in index_tag.section:
dist.releaseNotesHtmlUri = index_tag.section[
"ReleaseNotesHtml"
]
query = self._get_release_notes_uri_query_string(dist)
if query:
dist.releaseNotesHtmlUri += query
if "UpgradeTool" in index_tag.section:
dist.upgradeTool = index_tag.section["UpgradeTool"]
if "UpgradeToolSignature" in index_tag.section:
dist.upgradeToolSig = index_tag.section[
"UpgradeToolSignature"
]
if "UpgradeBroken" in index_tag.section:
dist.upgrade_broken = index_tag.section["UpgradeBroken"]
dists.append(dist)
if name == current_dist_name:
current_dist = dist
except apt_pkg.Error:
raise MetaReleaseParseError(
"Unable to parse %s" % self.METARELEASE_URI
)
self.metarelease_information.close()
self.metarelease_information = None
# first check if the current runing distro is in the meta-release
# information. if not, we assume that we run on something not
# supported and silently return
if current_dist is None:
self._debug("current dist not found in meta-release file\n")
return False
# then see what we can upgrade to
upgradable_to = ""
for dist in dists:
if dist.date > current_dist.date:
# Only offer to upgrade to an unsupported release if running
# with useDevelopmentRelease, this way one can upgrade from an
# LTS release to the next supported non-LTS release e.g. from
# 14.04 to 15.04.
if not dist.supported and not self.useDevelopmentRelease:
continue
upgradable_to = dist
self._debug("new dist: %s" % upgradable_to)
break
# only warn if unsupported and a new dist is available (because
# the development version is also unsupported)
if upgradable_to != "" and not current_dist.supported:
self.upgradable_to = upgradable_to
self.dist_no_longer_supported(current_dist)
if upgradable_to != "":
self.upgradable_to = upgradable_to
self.new_dist_available(upgradable_to)
# parsing done and sucessfully
return True
# the network thread that tries to fetch the meta-index file
# can't touch the gui, runs as a thread
def download(self):
self._debug("MetaRelease.download()")
lastmodified = 0
req = Request(self.METARELEASE_URI)
# make sure that we always get the latest file (#107716)
req.add_header("Cache-Control", "No-Cache")
req.add_header("Pragma", "no-cache")
if os.access(self.METARELEASE_FILE, os.W_OK):
try:
lastmodified = os.stat(self.METARELEASE_FILE).st_mtime
except OSError:
pass
if lastmodified > 0 and not self.forceDownload:
req.add_header(
"If-Modified-Since", time.asctime(time.gmtime(lastmodified))
)
try:
# open
uri = urlopen(req, timeout=20)
# sometime there is a root owned meta-relase file
# there, try to remove it so that we get it
# with proper permissions
if os.path.exists(self.METARELEASE_FILE) and not os.access(
self.METARELEASE_FILE, os.W_OK
):
try:
os.unlink(self.METARELEASE_FILE)
except OSError as e:
print(
"Can't unlink '%s' (%s)" % (self.METARELEASE_FILE, e)
)
# we may get exception here on e.g. disk full
try:
f = open(self.METARELEASE_FILE, "w+")
for line in uri.readlines():
f.write(line.decode("UTF-8"))
f.flush()
f.seek(0, 0)
self.metarelease_information = f
except IOError:
pass
uri.close()
# http error
except HTTPError as e:
# mvo: only reuse local info on "not-modified"
if e.code == 304 and os.path.exists(self.METARELEASE_FILE):
self._debug("reading file '%s'" % self.METARELEASE_FILE)
self.metarelease_information = open(self.METARELEASE_FILE, "r")
else:
self._debug("result of meta-release download: '%s'" % e)
# generic network error
except (URLError, BadStatusLine, socket.timeout) as e:
self._debug("result of meta-release download: '%s'" % e)
print(
"Failed to connect to %s. Check your Internet connection "
"or proxy settings" % self.METARELEASE_URI
)
# now check the information we have
if self.metarelease_information is not None:
self._debug("have self.metarelease_information")
try:
self.parse()
except Exception:
logging.exception(
"parse failed for '%s'" % self.METARELEASE_FILE
)
# no use keeping a broken file around
os.remove(self.METARELEASE_FILE)
# we don't want to keep a meta-release file around when it
# has a "Broken" flag, this ensures we are not bitten by
# I-M-S/cache issues
if self.new_dist and self.new_dist.upgrade_broken:
os.remove(self.METARELEASE_FILE)
else:
self._debug("NO self.metarelease_information")
self.downloaded.set()
@property
def downloading(self):
return not self.downloaded.is_set()
def _get_release_notes_uri_query_string(self, dist):
q = "?"
# get the lang
lang = get_lang()
if lang:
q += "lang=%s&" % lang
# get the os
q += "os=%s&" % self.flavor
# get the version to upgrade to
q += "ver=%s" % dist.version
# the archive didn't respond well to ? being %3F
return quote(q, "/?")
def _debug(self, msg):
if self.DEBUG:
sys.stderr.write(msg + "\n")
if __name__ == "__main__":
meta = MetaReleaseCore(False, False)
./NvidiaDetector/ 0000755 0000000 0000000 00000000000 15004453623 012617 5 ustar root root ./NvidiaDetector/__init__.py 0000644 0000000 0000000 00000000000 14641732414 014722 0 ustar root root ./NvidiaDetector/__pycache__/ 0000755 0000000 0000000 00000000000 15004453623 015027 5 ustar root root ./NvidiaDetector/__pycache__/alternatives.cpython-313.pyc 0000644 0000000 0000000 00000020233 15004453623 022234 0 ustar root root ó
µ‡f¿ ã óT • S SK r S SKrS SKJrJrJr " S S\5 r " S S\5 rg)é N)ÚPopenÚPIPEÚCalledProcessErrorc ó2 • \ rS rSrS rS rS rS rS rSr g) ÚMultiArchUtilsé c ó˜ • SSS.U l U R 5 U l [ U R R 5 5 [ [ U R R 5 5 R
U R 5 ( + 5 U l [ R R S5 ( d S[ R S' g g )NÚi386Úx86_64)r
Úamd64ÚPATHú/sbin:/usr/sbin:/bin:/usr/bin)Ú_supported_architecturesÚ_get_architectureÚ
_main_archÚlistÚvaluesÚintÚindexÚ_other_archÚosÚenvironÚget©Úselfs Ú=/usr/lib/python3/dist-packages/NvidiaDetector/alternatives.pyÚ__init__ÚMultiArchUtils.__init__ sš € à17À(Ñ(KˆÔ%Ø×0Ñ0Ó2ˆŒÜ × =Ñ =× DÑ DÓ FÓGܤ$ t×'DÑ'D×'KÑ'KÓ'MÓ"N×"TÑ"TÐUY×UdÑUdÓ"eÔeÓfñhˆÔô z‰z~‰~˜f×%Ñ%Ø!@ŒBJ‰JvÒð &ó