tails-additional-software 25.9 KB
Newer Older
Alan's avatar
Alan committed
1
#!/usr/bin/env python3
2 3

import gettext
4
import json
5 6
import logging
import logging.handlers
intrigeri's avatar
intrigeri committed
7
import os
8
import os.path
Alan's avatar
Alan committed
9
import pwd
10
import shutil
11
import subprocess
12
import sys
13

14
import apt.cache
15

Alan's avatar
Alan committed
16 17
from tailslib import LIVE_USERNAME

18 19
from tailslib.additionalsoftware import (
    ASPDataError,
20
    add_additional_packages,
Alan's avatar
Alan committed
21
    filter_package_details,
22 23
    get_additional_packages,
    get_packages_list_path,
24
    remove_additional_packages)
25

26 27 28
from tailslib.persistence import (
    has_unlocked_persistence,
    has_persistence,
29
    is_tails_media_writable,
30
    launch_persistence_setup,
31
    PERSISTENCE_DIR)
32

33 34
from tailslib.utils import launch_x_application

Alan's avatar
Alan committed
35 36
_ = gettext.gettext

37
ASP_STATE_DIR = "/run/live-additional-software"
38
ASP_STATE_PACKAGES = os.path.join(ASP_STATE_DIR, "packages")
39
ASP_STATE_INSTALLER_ASKED = os.path.join(ASP_STATE_DIR, "installer-asked")
40
ASP_LOG_FILE = os.path.join(ASP_STATE_DIR, "log")
41
OLD_APT_LISTS_DIR = os.path.join(PERSISTENCE_DIR, 'apt', 'lists.old')
Alan's avatar
Alan committed
42 43
APT_ARCHIVES_DIR = "/var/cache/apt/archives"
APT_LISTS_DIR = "/var/lib/apt/lists"
44

Alan's avatar
Alan committed
45

46 47 48 49 50 51
def _exit_if_in_live_build():
    """Exits with success if running inside live-build."""
    if "SOURCE_DATE_EPOCH" in os.environ:
        sys.exit(0)


52
def _launch_apt_get(specific_args):
53
    """Launch apt-get with given arguments.
Alan's avatar
Alan committed
54

55
    Launch apt-get with given arguments list, log its standard and error output
56
    and return its returncode."""
57
    apt_get_env = os.environ.copy()
58 59
    # The environnment provided in GDM PostLogin hooks doesn't contain /sbin/
    # which is required by dpkg. Let's use the default path for root in Tails.
Alan's avatar
Alan committed
60 61 62 63
    apt_get_env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:" \
                          "/usr/bin:/sbin:/bin"
    # We will log the output and want it in English when included in bug
    # reports
64
    apt_get_env['LANG'] = "C"
65
    apt_get_env['DEBIAN_PRIORITY'] = "critical"
66 67
    args = ["apt-get", "--quiet", "--yes"]
    args.extend(specific_args)
68 69
    apt_get = subprocess.Popen(args,
                               env=apt_get_env,
bertagaz's avatar
bertagaz committed
70
                               universal_newlines=True,
71 72
                               stderr=subprocess.STDOUT,
                               stdout=subprocess.PIPE)
73 74
    for line in iter(apt_get.stdout.readline, ''):
        if not line.startswith('('):
75
            logging.info(line.rstrip())
76
    apt_get.wait()
Tails developers's avatar
Tails developers committed
77
    if apt_get.returncode:
78 79
        logging.warning("apt-get exited with returncode %i"
                        % apt_get.returncode)
80 81
    return apt_get.returncode

Alan's avatar
Alan committed
82

83 84
def _notify(title, body="", accept_label="", deny_label="",
            documentation_target="", urgent=False, return_id=False):
85 86
    """Display a notification to the user of the live system.

87 88 89
    The notification will show title and body.

    If accept_label or deny_label are set, they will be shown on action buttons
90 91
    and the method will wait for user input and return 1 if the button with
    accept_label was clicked or 0 if the button with deny_label was
92 93 94 95
    clicked.

    If documentation_target is set, a "Documentation" action button will open
    corresponding tails documentation when clicked.
96 97 98 99 100 101

    If return_id is true, returns the notification ID, which may be used to
    close the notification.

    Else, return None.
    """
Alan's avatar
Alan committed
102

103
    cmd = "/usr/local/lib/tails-additional-software-notify"
Alan's avatar
Alan committed
104 105 106 107 108
    if urgent:
        urgent = "urgent"
    else:
        urgent = ""

109
    try:
anonym's avatar
anonym committed
110 111 112 113 114 115 116 117 118 119
        completed_process = subprocess.run(
            [
                "sudo", "-u", LIVE_USERNAME,
                cmd, title, body, accept_label, deny_label,
                documentation_target, urgent
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=True
        )
120 121 122
        if completed_process.returncode == 1:
            # sudo failed to execute the command
            raise OSError(completed_process.stderr)
Alan's avatar
Alan committed
123
    except OSError as e:
124 125
        logging.warning("Warning: unable to notify the user. %s" % e)
        logging.warning("The notification was: %s %s" % (title, body))
126
        return None
Alan's avatar
Alan committed
127

128
    if return_id:
129
        for line in completed_process.stdout.splitlines():
130 131
            if line.startswith("id="):
                return line[3:]
Alan's avatar
Alan committed
132
    else:
133
        if completed_process.returncode == 0:
134
            return 1
135
        elif completed_process.returncode == 3:
136
            return 0
137
        else:
138
            return None
Alan's avatar
Alan committed
139

140

141
def _notify_failure(summary, details=None):
142 143
    """Display a failure notification to the user of the live system.

Alan's avatar
Alan committed
144
    The user has the option to edit the configuration or to view the system
145 146
    log.
    """
147
    if details:
Alan's avatar
Alan committed
148 149
        # Translators: Don't translate {details}, it's a placeholder and will
        # be replaced.
150
        details = _("{details} Please check your list of additional "
151
                    "software or read the system log to "
152 153 154 155
                    "understand the problem.").format(details=details)

    else:
        details = _("Please check your list of additional "
156
                    "software or read the system log to "
Alan's avatar
Alan committed
157
                    "understand the problem.")
158

159
    action_clicked = _notify(summary, details, _("Show Log"), _("Configure"),
160 161
                             urgent=True)
    if action_clicked == 1:
162
        show_system_log()
163 164
    elif action_clicked == 0:
        show_configuration_window()
165 166


167 168 169 170
def _close_notification(notification_id):
    """Close a notification shown to the user of the live system."""
    subprocess.run(
            ["sudo", "-u", LIVE_USERNAME,
Alan's avatar
Alan committed
171 172
             "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus".format(
                  uid=pwd.getpwnam(LIVE_USERNAME).pw_uid),
173 174 175 176 177 178 179 180 181
             "gdbus", "call",
             "--session",
             "--dest", "org.freedesktop.Notifications",
             "--object-path", "/org/freedesktop/Notifications",
             "--method", "org.freedesktop.Notifications.CloseNotification",
             str(notification_id)],
            stdout=subprocess.DEVNULL)


182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
def _spawn_daemon(func):
    """Spawn func after double-forking.

    Do the UNIX double-fork magic, see Stevens' "Advanced
    Programming in the UNIX Environment" for details (ISBN 0201563177).

    From https://stackoverflow.com/questions/6011235/run-a-program-from-
    python-and-have-it-continue-to-run-after-the-script-is-kille
    """
    try:
        pid = os.fork()
        if pid > 0:
            # parent process, return and keep running
            return
    except OSError as e:
197
        logging.error("fork #1 failed: %d (%s)" % (e.errno, e.strerror))
198 199 200 201 202 203 204 205 206 207 208
        sys.exit(1)

    os.setsid()

    # do second fork
    try:
        pid = os.fork()
        if pid > 0:
            # exit from second parent
            sys.exit(0)
    except OSError as e:
209
        logging.error("fork #2 failed: %d (%s)" % (e.errno, e.strerror))
210 211 212 213 214 215
        sys.exit(1)

    # do stuff
    func()


216 217 218
def _format_iterable(iterable):
    """Return a nice formatted string with the elements of iterable."""
    iterable = sorted(iterable)
219

220
    if len(iterable) == 1:
Alan's avatar
Alan committed
221
        return iterable[0]
222
    elif len(iterable) > 1:
Alan's avatar
Alan committed
223 224
        # Translators: Don't translate {beginning} or {last}, they are
        # placeholders and will be replaced.
225 226
        return _("{beginning} and {last}").format(
            beginning=_(", ").join(iterable[:-1]), last=iterable[-1])
227
    else:
Alan's avatar
Alan committed
228
        return str(iterable)
229 230


231 232 233
def has_additional_packages_list(search_new_persistence=False):
    """Return true iff a packages list file is found in a persistence.

234
    Log warnings in syslog.
235 236 237
    The search_new_persistence argument is passed to get_persistence_path.
    """
    try:
238 239
        packages_list_path = get_packages_list_path(search_new_persistence)
    except FileNotFoundError as e:
240
        logging.warning("Warning: {}".format(e))
241 242
        return False
    if os.path.isfile(packages_list_path):
243
        logging.info("Found additional packages list.")
244 245
        return True
    else:
246
        logging.warning("Warning: no configuration file found.")
247
        return False
248 249


250
def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
251
    """Delete the copy of the old APT lists, if any."""
252 253 254 255
    shutil.rmtree(old_apt_lists_dir)


def save_old_apt_lists(srcdir=APT_LISTS_DIR, destdir=OLD_APT_LISTS_DIR):
256
    """Save a copy of the APT lists"""
257
    if os.path.exists(destdir):
258 259
        logging.warning("Warning: a copy of the APT lists already exists, "
                        "which should never happen. Removing it.")
260 261 262 263 264 265
        delete_old_apt_lists(destdir)
    shutil.copytree(srcdir, destdir, symlinks=True)


# Note: we can't do nicer delete + move operations because the directory
# we want to replace is bind-mounted. So we have to delete the content
266
# we want to replace, and then move the content we want to restore.
267
def restore_old_apt_lists(srcdir=OLD_APT_LISTS_DIR, dstdir=APT_LISTS_DIR):
268
    """Restore the copy of the old APT lists."""
269
    # Empty dstdir
Alan's avatar
Alan committed
270 271 272 273 274 275
    for basename in os.listdir(dstdir):
        path = os.path.join(dstdir, basename)
        if os.path.isfile(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path)
anonym's avatar
anonym committed
276
    # Move the content of srcdir to dstdir
Alan's avatar
Alan committed
277 278
    for basename in os.listdir(srcdir):
        path = os.path.join(srcdir, basename)
279
        shutil.move(path, dstdir)
280 281


282 283 284 285 286 287
def handle_installed_packages(packages):
    """Configure packages as additional software packages if the user wants to.

    Ask the user if packages should be added to additional software, and
    actually add them if requested.
    """
288
    logging.info("New packages manually installed: %s" % packages)
289
    if has_unlocked_persistence(search_new_persistence=True):
Alan's avatar
Alan committed
290 291
        # Translators: Don't translate {packages}, it's a placeholder and will
        # be replaced.
292
        if _notify(_("Add {packages} to your additional software?").format(
293
                    packages=_format_iterable(packages)),
294
                   _("To install it automatically from your persistent "
sajolida's avatar
sajolida committed
295
                     "storage when starting Tails."),
296
                   _("Install Every Time"),
297
                   _("Install Only Once"),
298
                   urgent=True):
299 300 301 302 303 304 305
            try:
                setup_additional_packages()
                add_additional_packages(packages, search_new_persistence=True)
            except Exception as e:
                _notify_failure(_("The configuration of your additional "
                                  "software failed."))
                raise e
306
    elif has_persistence():
307 308 309 310 311 312 313 314 315 316
        # When a package is installed with a persistent storage locked, don't
        # show any notification.
        #
        # People who have a persistent storage but don't unlock it, probably do
        # this only sometimes and for a reason. They probably otherwise unlock
        # their persistent storage most of the time.
        #
        # If they install packages with their persistent storage locked, they
        # probably do it with their persistent storage unlock as well and would
        # learn about this feature when it's most relevant for them.
317 318
        logging.warning("Warning: persistence storage is locked, can't add "
                        "additional software.")
Alan's avatar
Alan committed
319
    elif is_tails_media_writable():
Alan's avatar
Alan committed
320 321
        # Translators: Don't translate {packages}, it's a placeholder and will
        # be replaced.
322
        if _notify(_("Add {packages} to your additional software?").format(
323
                    packages=_format_iterable(packages)),
324 325
                   _("To install it automatically when starting Tails, you "
                     "can create a persistent storage and activate the "
sajolida's avatar
sajolida committed
326
                     "<b>Additional Software</b> feature."),
327
                   _("Create Persistent Storage"),
328
                   _("Install Only Once"),
329
                   urgent=True):
330 331 332 333 334 335 336 337
            try:
                create_persistence_and_setup_additional_packages(packages)
            except Exception as e:
                _notify_failure(_("The configuration of your additional "
                                  "software failed."),
                                _("Creating your persistent storage "
                                  "failed."))
                raise e
338
    else:   # It's impossible to have a persistent storage
339
        logging.warning("Cannot create persistent storage on this media.")
340 341
        if not os.path.isfile(ASP_STATE_INSTALLER_ASKED):
            open(ASP_STATE_INSTALLER_ASKED, 'a').close()
Alan's avatar
Alan committed
342 343
            # Translators: Don't translate {packages}, it's a placeholder and
            # will be replaced.
344
            _notify(_("You could install {packages} automatically when "
345
                      "starting Tails").format(
346
                        packages=_format_iterable(packages)),
347
                    _("To do so, you need to run Tails from a USB stick "
sajolida's avatar
sajolida committed
348
                      "installed using <i>Tails Installer</i>."),
349 350
                    documentation_target="install/clone",
                    urgent=True)
351 352 353 354 355 356


def handle_removed_packages(packages):
    """Removes packages from additional software packages if the user wants to.

    Ask the user if packages should be removed from additional software, and
357
    actually remove them if requested.
358
    """
359
    logging.info("Additional packages removed: %s" % packages)
Alan's avatar
Alan committed
360 361
    # Translators: Don't translate {packages}, it's a placeholder and will be
    # replaced.
Alan's avatar
Alan committed
362
    if _notify(_("Remove {packages} from your additional software?").format(
363
                 packages=_format_iterable(packages)),
Alan's avatar
Alan committed
364 365
               # Translators: Don't translate {packages}, it's a placeholder
               # and will be replaced.
366 367
               _("This will stop installing {packages} automatically.").format(
                 packages=_format_iterable(packages)),
Alan's avatar
Alan committed
368 369 370
               _("Remove"),
               _("Cancel"),
               urgent=True):
371 372 373 374 375 376
        try:
            remove_additional_packages(packages, search_new_persistence=True)
        except Exception as e:
            _notify_failure(_("The configuration of your additional "
                              "software failed."))
            raise e
377 378 379 380


def setup_additional_packages():
    """Enable additional software in persistence."""
381
    launch_persistence_setup("--no-gui",
382 383
                             "--no-display_finished_message",
                             "--force_enable_preset", "AdditionalSoftware")
384 385 386 387 388 389 390 391 392 393


def create_persistence_and_setup_additional_packages(packages):
    """Create persistence and add packages to its configuration.

    Create a new persistence with additional packages enabled.
    Then add the packages to additional packages configuration.

    packages should be a list of packages names.
    """
394
    logging.info("Creating new persistent volume")
395
    launch_persistence_setup("--step", "bootstrap",
396 397
                             "--no-display_finished_message",
                             "--force_enable_preset", "AdditionalSoftware")
398
    add_additional_packages(packages, search_new_persistence=True)
399
    # show persistence configuration
400
    launch_persistence_setup()
401
    # APT lists and APT archive cache will be synchronized at shutdown by
402
    # tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
403 404


405 406
def show_configuration_window():
    """Show additional packages configuration window."""
407 408
    launch_x_application(LIVE_USERNAME,
                         "/usr/local/bin/tails-additional-software-config")
409 410 411 412


def show_system_log():
    """Show additional packages configuration window."""
413 414 415
    launch_x_application(LIVE_USERNAME,
                         "/usr/bin/gedit",
                         ASP_LOG_FILE)
416 417


418 419
def apt_hook_pre():
    """Subcommand to handle Dpkg::Pre-Install-Pkgs."""
420
    _exit_if_in_live_build()
421
    logging.info("Saving package changes")
Alan's avatar
Alan committed
422

Alan's avatar
Alan committed
423 424
    apt_cache = apt.cache.Cache()

425 426 427 428
    installed_packages = []
    removed_packages = []

    line = sys.stdin.readline()
429 430
    if not line.startswith("VERSION 3"):
        raise ASPDataError("APT data is not version 3")
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
    line = sys.stdin.readline()
    # Ignore configuration space, which ends with an empty line
    while line != "\n":
        line = sys.stdin.readline()
    # Package action lines
    for line in sys.stdin:
        # Package action lines consist of five fields in Version 2: package
        # name (without architecture qualification even if foreign), old
        # version, direction of version change (< for upgrades, > for
        # downgrades, = for no change), new version, action. The version
        # fields are "-" for no version at all (for example when installing
        # a package for the first time; no version is treated as earlier
        # than any real version, so that is an upgrade, indicated as - <
        # 1.23.4). The action field is "**CONFIGURE**" if the package is
        # being configured, "**REMOVE**" if it is being removed, or the
        # filename of a .deb file if it is being unpacked.
        #
        # In Version 3 after each version field follows the architecture of
        # this version, which is "-" if there is no version, and a field
        # showing the MultiArch type "same", "foreign", "allowed" or "none".
        # Note that "none" is an incorrect typename which is just kept to
        # remain compatible, it should be read as "no" and users are
        # encouraged to support both.
        #
        # Example:
        #
        # colordif - - none < 1.0.16-1 all none **CONFIGURE**
        package_name, old_version, old_arch, old_multiarch, direction, \
                new_version, new_arch, new_multiarch, action = line.split()
        if action.endswith(".deb"):
Alan's avatar
Alan committed
461 462 463
            # Filter packages that will only be upgraded
            if not apt_cache[package_name].is_installed:
                installed_packages.append(package_name)
464 465 466 467
        elif action.endswith("**REMOVE**"):
            removed_packages.append(package_name)

    result = {"installed": installed_packages, "removed": removed_packages}
468
    with open(ASP_STATE_PACKAGES, 'w') as f:
469 470 471 472 473 474 475 476 477
        json.dump(result, f)


def apt_hook_post():
    """Subcommand to handle Dpkg::Post-Invoke.

    Retrieve the list of packages saved by apt_hook_pre, filter packages not
    interesting and pass the resulting list to the appropriate method.
    """
478
    _exit_if_in_live_build()
479
    logging.info("Examining package changes")
Alan's avatar
Alan committed
480

481
    with open(ASP_STATE_PACKAGES) as f:
482
        packages = json.load(f)
483
    os.remove(ASP_STATE_PACKAGES)
484

485
    additional_packages_names = {
Alan's avatar
Alan committed
486 487
        filter_package_details(pkg) for pkg in
        get_additional_packages(search_new_persistence=True)
488
    }
489

490
    apt_cache = apt.cache.Cache()
491 492
    # Filter automatically installed packages and packages already configured
    # as additional software
493
    new_manually_installed_packages = {
Alan's avatar
Alan committed
494 495 496
        pkg for pkg in packages["installed"] if (
            not apt_cache[pkg].is_auto_installed and
            pkg not in additional_packages_names)
497 498
    }

499 500
    if new_manually_installed_packages:
        handle_installed_packages(new_manually_installed_packages)
501 502 503

    # Filter non-additional software packages
    additional_packages_removed = set(packages["removed"]).intersection(
504
        additional_packages_names)
505 506
    if additional_packages_removed:
        handle_removed_packages(additional_packages_removed)
507

Alan's avatar
Alan committed
508

509 510 511 512 513
def install_additional_packages(upgrade_mode=False):
    """Subcommand which activates and installs all additional packages.

    If upgrade_mode is True, don't attempt to restore old apt lists and don't
    notify the user using desktop notifications."""
514
    logging.info("Starting to install additional software...")
intrigeri's avatar
intrigeri committed
515

516
    if not has_additional_packages_list():
517
        return True
intrigeri's avatar
intrigeri committed
518

519 520 521 522 523 524 525 526
    # If a copy of old APT lists is found, then the previous upgrade
    # attempt has not completed successfully (it may have failed e.g.
    # due to network problems, or it may have been interrupted).
    # In many of these cases, the APT package cache lacks some
    # packages the new APT lists reference, so the (offline)
    # installation step below in this function will fail. To avoid
    # that, we restore the old APT lists: there are greater chances
    # that the APT packages cache still has the corresponding packages.
527
    if os.path.isdir(OLD_APT_LISTS_DIR) and not upgrade_mode:
528
        logging.warning("Found a copy of old APT lists, restoring it.")
529 530
        try:
            restore_old_apt_lists()
531
        except Exception as e:
532 533
            logging.warning("Restoring old APT lists failed with %r, "
                            "deleting them and proceeding anyway." % e)
534 535 536 537 538 539 540
        # In all cases, delete the old APT lists: if they could be
        # restored we don't need them anymore (and we don't want to
        # restore them again next time); if they could not be
        # restored, chances are restoration will fail next time
        # as well.
        delete_old_apt_lists()

541 542
    packages = get_additional_packages()
    if not packages:
543
        logging.warning("Warning: no packages to install, exiting")
544
        return True
545 546
    if not upgrade_mode:
        installing_notification_id = _notify(
Alan's avatar
Alan committed
547 548
            _("Installing your additional software from persistent "
              "storage..."),
549
            _("This can take several minutes."),
550
            return_id=True)
551 552
    logging.info("Will install the following packages: %s"
                 % " ".join(packages))
Alan's avatar
Alan committed
553 554 555
    apt_get_returncode = _launch_apt_get(
        ["--no-remove",
         "--option", "DPkg::Options::=--force-confold",
556
         "install"] + list(packages))
557
    if apt_get_returncode:
558 559
        logging.warning("Warning: installation of %s failed"
                        % " ".join(packages))
560 561 562 563
        if not upgrade_mode:
            _close_notification(installing_notification_id)
            _notify_failure(_("The installation of your additional software "
                              "failed"))
564 565
        return False
    else:
566
        logging.info("Installation completed successfully.")
567 568 569 570 571 572 573 574 575 576 577
        if not upgrade_mode:
            _close_notification(installing_notification_id)
            # XXX: there should be a "Configure" button in this notification.
            # However, the easy way to implement it makes this process not
            # return until the notification is clicked. The notification
            # process could be detached, and handle the "configure" action
            # itself.
            #  if _notify(_("Additional software installed successfully"),
            #             accept_label=_("Configure")):
            #      show_configuration_window()
            _notify(_("Additional software installed successfully"))
578 579
        return True

Alan's avatar
Alan committed
580

581
def upgrade_additional_packages():
582
    """Subcommand which upgrades all additional packages."""
583
    logging.info("Starting to upgrade additional software...")
584 585 586 587

    if not has_additional_packages_list():
        return True

588 589 590
    # Save a copy of APT lists that we'll delete only once the upgrade
    # has succeeded, to ensure that the APT packages cache is up-to-date
    # wrt. the APT lists.
591
    logging.info("Saving old APT lists...")
592 593
    save_old_apt_lists()

594
    apt_get_returncode = _launch_apt_get(["update"])
595
    if apt_get_returncode:
596
        logging.warning("Warning: the update failed.")
597 598
        _notify_failure(_("The check for upgrades of your additional software "
                          "failed"),
599 600 601
                        _("Please check your network connection, "
                          "restart Tails, or read the system log to "
                          "understand the problem."))
602
        return False
603
    if install_additional_packages(upgrade_mode=True):
604
        logging.info("The upgrade was successful.")
605
    else:
606
        _notify_failure(_("The upgrade of your additional software failed"),
607 608 609
                        _("Please check your network connection, "
                          "restart Tails, or read the system log to "
                          "understand the problem."))
610 611
        return False

612 613 614
    # We now know that the APT packages cache is up-to-date wrt. the APT lists,
    # so we can delete the copy of the old lists
    delete_old_apt_lists()
Alan's avatar
Alan committed
615

616 617 618 619 620 621 622 623 624 625
    # Remove outdated packages from the local package cache. This is needed as
    # we disable apt-daily.timer, which would else take care of this cleanup.
    # We do this after the upgrade has succeeded so that the old packages
    # remain available in the cache in case we have to restore the old lists.
    # In the past we did this before upgrading in order to remove the
    # i386 packages from the cache before downloading amd64 ones, but
    # this does not matter anymore now that all persistent volumes
    # must have been upgraded already.
    apt_get_returncode = _launch_apt_get(["autoclean"])
    if apt_get_returncode:
626
        logging.warning("Warning: autoclean failed.")
627
    return True
Alan's avatar
Alan committed
628

629

630
def print_help():
631
    """Subcommand which displays help."""
632 633
    sys.stderr.write("Usage: %s <subcommand>\n" % program_name)
    sys.stderr.write("""Subcommands:
634 635
    install: install additional software
    upgrade: upgrade additional software\n""")
636

Alan's avatar
Alan committed
637

638 639 640
if __name__ == "__main__":
    program_name = os.path.basename(sys.argv[0])

641 642 643 644 645
    # Exits with success if running inside live-build.
    if "SOURCE_DATE_EPOCH" in os.environ:
        sys.exit(0)

    # Set loglevel if debug is found in kernel command line.
646 647 648 649 650 651
    with open('/proc/cmdline') as cmdline_fd:
        cmdline = cmdline_fd.read()
    if "DEBUG" in os.environ or "debug" in cmdline.split():
        log_level = logging.DEBUG
        log_format = "[%(levelname)s] %(filename)s:%(lineno)d " \
                     "%(funcName)s: %(message)s"
652
    else:
653 654
        log_level = logging.INFO
        log_format = "[%(levelname)s] %(message)s"
Alan's avatar
Alan committed
655
    stderr_handler = logging.StreamHandler()
656
    file_handler = logging.FileHandler(ASP_LOG_FILE)
657
    logging.basicConfig(format=log_format,
Alan's avatar
Alan committed
658
                        handlers=[stderr_handler, file_handler],
659
                        level=log_level)
660

661
    gettext.install("tails")
662 663

    if len(sys.argv) < 2:
Alan's avatar
Alan committed
664
        print_help()
Alan's avatar
Alan committed
665
        sys.exit(2)
666 667 668

    if sys.argv[1] == "install":
        if not install_additional_packages():
Alan's avatar
Alan committed
669
            sys.exit(150)
670 671
    elif sys.argv[1] == "upgrade":
        if not upgrade_additional_packages():
Alan's avatar
Alan committed
672
            sys.exit(151)
673 674 675
    elif sys.argv[1] == "apt-pre":
        apt_hook_pre()
    elif sys.argv[1] == "apt-post":
676
        _spawn_daemon(apt_hook_post)
677 678
    else:
        print_help()
Alan's avatar
Alan committed
679
        sys.exit(2)