./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 15017640702 015352  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 15035756004 016316  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 00000144564 15035756004 014242  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 = _(
            'The package {} is marked for removal '
            'but it is in the removal deny list.'
        ).format(self.package)
        # 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 '
                'no longer need {}, you can manually remove it '
                'and try the upgrade again.'
            ).format(self.package)
        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, _index) 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 00000263304 15035756004 015354  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.
                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 00000022763 15035756004 015424  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
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 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 00000140017 15035756004 014502  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 _
from .RiscvProfile import (RiscvProfile, RiscvProfileException)
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_rva23()
        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_rva23(self):
        """
        With release Ubuntu 25.10 we have raised the ISA profile requirement
        on RISC-V to RVA23U64. Prevent systems that don't implement RVA23U64
        from upgrading beyond Ubuntu 24.04 LTS.
        """
        di = distro_info.UbuntuDistroInfo()
        version = di.version(self.controller.toDist) or 'next release'
        try:
            cpuinfo = RiscvProfile.read_cpuinfo()
        except RiscvProfileException as ex:
            # Either /proc/cpuinfo cannot be read or this not a RISC-V system
            logging.debug(f'RVA23 check: {ex}')
            return
        try:
            cpuinfo.assert_rva23_ready()
            return
        except RiscvProfileException as ex:
            logging.error(f'RVA23 check: {ex}')
            self._view.error(
                _(
                    f'Sorry, cannot upgrade this system to {version}'
                ),
                _(
                    'On Ubuntu 25.10 and later, RISC-V systems are required '
                    'to support the RVA23U64 profile, but your system does '
                    'not. Please, see https://discourse.ubuntu.com/t/'
                    'questing-quokka-release-notes for more information.'
                ),
            )
            self.controller.abort()
    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 15035756004 014642  0                                                                                                    ustar   buildd                          buildd                                                                                                                                                                                                                 VERSION = '25.10.6'
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            ./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 15017640702 014740  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 15035756004 015702  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 15002703666 012472  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 15036255613 012623  5                                                                                                    ustar   root                            root                                                                                                                                                                                                                   ./NvidiaDetector/alternatives.py                                                                    0000644 0000000 0000000 00000013677 14641732414 015714  0                                                                                                    ustar   root                            root                                                                                                                                                                                                                   #
#       alternatives.py
#
#       Copyright 2010 Canonical Services Ltd
#       Author: Alberto Milone 
#
#       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., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.
import os
import subprocess
from subprocess import Popen, PIPE, CalledProcessError
class MultiArchUtils(object):
    def __init__(self):
        # We have 2 alternatives, one for each architecture
        self._supported_architectures = {'i386': 'i386', 'amd64': 'x86_64'}
        self._main_arch = self._get_architecture()
        self._other_arch = list(self._supported_architectures.values())[
                          int(not list(self._supported_architectures.values()).index(self._main_arch))]
        # Make sure that the PATH environment variable is set
        if not os.environ.get('PATH'):
            os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
    def _get_architecture(self):
        dev_null = open('/dev/null', 'w')
        p1 = Popen(['dpkg', '--print-architecture'], stdout=PIPE,
                   stderr=dev_null, universal_newlines=True)
        p = p1.communicate()[0]
        dev_null.close()
        architecture = p.strip()
        return self._supported_architectures.get(architecture)
    def _get_alternative_name_from_arch(self, architecture):
        alternative = '%s-linux-gnu_gl_conf' % architecture
        return alternative
    def get_main_alternative_name(self):
        return self._get_alternative_name_from_arch(self._main_arch)
    def get_other_alternative_name(self):
        return self._get_alternative_name_from_arch(self._other_arch)
class Alternatives(object):
    def __init__(self, master_link):
        self._open_drivers_alternative = 'mesa/ld.so.conf'
        self._open_egl_drivers_alternative = 'mesa-egl/ld.so.conf'
        self._command = 'update-alternatives'
        self._master_link = master_link
        # Make sure that the PATH environment variable is set
        if not os.environ.get('PATH'):
            os.environ['PATH'] = '/sbin:/usr/sbin:/bin:/usr/bin'
    def list_alternatives(self):
        '''Get the list of alternatives for the master link'''
        dev_null = open('/dev/null', 'w')
        alternatives = []
        p1 = Popen([self._command, '--list', self._master_link],
                   stdout=PIPE, stderr=dev_null, universal_newlines=True)
        p = p1.communicate()[0]
        dev_null.close()
        c = p.split('\n')
        for line in c:
            line.strip() and alternatives.append(line.strip())
        return alternatives
    def get_current_alternative(self):
        '''Get the alternative in use'''
        dev_null = open('/dev/null', 'w')
        p1 = Popen([self._command, '--query', self._master_link],
                   stdout=PIPE, stderr=dev_null, universal_newlines=True)
        p = p1.communicate()[0]
        dev_null.close()
        c = p.split('\n')
        for line in c:
            if line.strip().startswith('Value:'):
                return line.replace('Value:', '').strip()
        return None
    def get_alternative_by_name(self, name, ignore_pattern=None):
        '''Get the alternative link by providing the driver name
        ignore_pattern allows ignoring a substring in the name'''
        if ignore_pattern:
            name = name.replace(ignore_pattern, '')
        alternatives = self.list_alternatives()
        for alternative in alternatives:
            if alternative.split('/')[-2] == name:
                return alternative
        return None
    def get_open_drivers_alternative(self):
        '''Get the alternative link for open drivers'''
        return self.get_alternative_by_name(self._open_drivers_alternative)
    def get_open_egl_drivers_alternative(self):
        '''Get the alternative link for open EGL/GLES drivers'''
        return self.get_alternative_by_name(self._open_egl_drivers_alternative)
    def update_gmenu(self):
        '''Trigger gmenu so that the icons will show up in the menu'''
        try:
            subprocess.check_call(['dpkg-trigger', '--by-package=fakepackage',
                                   'gmenucache'])
            subprocess.check_call(['dpkg', '--configure', '-a'])
        except (OSError, CalledProcessError):
            pass
    def set_alternative(self, path):
        '''Tries to set an alternative and returns the boolean exit status'''
        try:
            subprocess.check_call([self._command, '--set',
                                   self._master_link, path])
            self.ldconfig()
        except CalledProcessError:
            return False
        self.update_gmenu()
        return True
    def ldconfig(self):
        '''Call ldconfig'''
        try:
            subprocess.check_call(['ldconfig'])
        except CalledProcessError:
            return False
        return True
    def resolve_module_alias(self, alias):
        '''Get the 1st kernel module name matching an alias'''
        dev_null = open('/dev/null', 'w')
        p1 = Popen(['modprobe', '--resolve-alias', alias], stdout=PIPE,
                   stderr=dev_null, universal_newlines=True)
        p = p1.communicate()[0]
        dev_null.close()
        c = p.split('\n')
        for line in c:
            if line.strip().startswith('Usage:'):
                return None
            return line.strip()
        return None
                                                                 ./NvidiaDetector/__pycache__/                                                                       0000755 0000000 0000000 00000000000 15036255613 015033  5                                                                                                    ustar   root                            root                                                                                                                                                                                                                   ./NvidiaDetector/__pycache__/nvidiadetector.cpython-313.pyc                                         0000644 0000000 0000000 00000035576 15036255613 022563  0                                                                                                    ustar   root                            root                                                                                                                                                                                                                   ó
    µ‡fD  ã                   óh   • S r SSKrSSKJrJr  SSKrSSKrSSKrSr " S S\	5      r
 " S S\5      rg)	z)Detection tool for nVidia graphics cards é    N)ÚPopenÚPIPEz)/usr/share/ubuntu-drivers-common/obsoletec                   ó   • \ rS rSrSrSrg)ÚNoDatadirErroré"   z4Exception thrown when no modaliases dir can be found© N)Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú__static_attributes__r   ó    Ú?/usr/lib/python3/dist-packages/NvidiaDetector/nvidiadetector.pyr   r   "   s   † Ü:r   r   c                   ój   • \ rS rSrSrSS\4S jrS rS rS r	S r
S	 rS
 rS r
S rS
 rS rS rSrg)ÚNvidiaDetectioné&