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

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

137

138
def _notify_failure(summary, details=None):
139 140
    """Display a failure notification to the user of the live system.

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

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

155
    action_clicked = _notify(summary, details, _("Show Log"), _("Configure"),
156 157
                             urgent=True)
    if action_clicked == 1:
158
        show_system_log()
159 160
    elif action_clicked == 0:
        show_configuration_window()
161 162


163 164 165 166
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
167 168
             "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus".format(
                  uid=pwd.getpwnam(LIVE_USERNAME).pw_uid),
169 170 171 172 173 174 175 176 177
             "gdbus", "call",
             "--session",
             "--dest", "org.freedesktop.Notifications",
             "--object-path", "/org/freedesktop/Notifications",
             "--method", "org.freedesktop.Notifications.CloseNotification",
             str(notification_id)],
            stdout=subprocess.DEVNULL)


178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
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:
193
        logging.error("fork #1 failed: %d (%s)" % (e.errno, e.strerror))
194 195 196 197 198 199 200 201 202 203 204
        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:
205
        logging.error("fork #2 failed: %d (%s)" % (e.errno, e.strerror))
206 207 208 209 210 211
        sys.exit(1)

    # do stuff
    func()


212 213 214
def _format_iterable(iterable):
    """Return a nice formatted string with the elements of iterable."""
    iterable = sorted(iterable)
215

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


226 227 228
def has_additional_packages_list(search_new_persistence=False):
    """Return true iff a packages list file is found in a persistence.

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


245
def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
246
    """Delete the copy of the old APT lists, if any."""
247 248 249 250
    shutil.rmtree(old_apt_lists_dir)


def save_old_apt_lists(srcdir=APT_LISTS_DIR, destdir=OLD_APT_LISTS_DIR):
251
    """Save a copy of the APT lists"""
252
    if os.path.exists(destdir):
253 254
        logging.warning("Warning: a copy of the APT lists already exists, "
                        "which should never happen. Removing it.")
255 256 257 258 259 260
        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
261
# we want to replace, and then move the content we want to restore.
262
def restore_old_apt_lists(srcdir=OLD_APT_LISTS_DIR, dstdir=APT_LISTS_DIR):
263
    """Restore the copy of the old APT lists."""
264
    # Empty dstdir
Alan's avatar
Alan committed
265 266 267 268 269 270
    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
271
    # Move the content of srcdir to dstdir
Alan's avatar
Alan committed
272 273
    for basename in os.listdir(srcdir):
        path = os.path.join(srcdir, basename)
274
        shutil.move(path, dstdir)
275 276


277 278 279 280 281 282
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.
    """
283
    logging.info("New packages manually installed: %s" % packages)
284
    if has_unlocked_persistence(search_new_persistence=True):
285
        # Translators: Don't translate {packages}, it's a placeholder and will be replaced.
286
        if _notify(_("Add {packages} to your additional software?").format(
287
                    packages=_format_iterable(packages)),
288
                   _("To install it automatically from your persistent "
sajolida's avatar
sajolida committed
289
                     "storage when starting Tails."),
290
                   _("Install Every Time"),
291
                   _("Install Only Once"),
292
                   urgent=True):
293 294 295 296 297 298 299
            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
300
    elif has_persistence():
301 302 303 304 305 306 307 308 309 310
        # 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.
311 312
        logging.warning("Warning: persistence storage is locked, can't add "
                        "additional software.")
Alan's avatar
Alan committed
313
    elif is_tails_media_writable():
314
        # Translators: Don't translate {packages}, it's a placeholder and will be replaced.
315
        if _notify(_("Add {packages} to your additional software?").format(
316
                    packages=_format_iterable(packages)),
317 318
                   _("To install it automatically when starting Tails, you "
                     "can create a persistent storage and activate the "
sajolida's avatar
sajolida committed
319
                     "<b>Additional Software</b> feature."),
320
                   _("Create Persistent Storage"),
321
                   _("Install Only Once"),
322
                   urgent=True):
323 324 325 326 327 328 329 330
            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
331
    else:   # It's impossible to have a persistent storage
332
        logging.warning("Cannot create persistent storage on this media.")
333 334
        if not os.path.isfile(ASP_STATE_INSTALLER_ASKED):
            open(ASP_STATE_INSTALLER_ASKED, 'a').close()
335
            # Translators: Don't translate {packages}, it's a placeholder and will be replaced.
336
            _notify(_("You could install {packages} automatically when "
337
                      "starting Tails").format(
338
                        packages=_format_iterable(packages)),
339
                    _("To do so, you need to run Tails from a USB stick "
sajolida's avatar
sajolida committed
340
                      "installed using <i>Tails Installer</i>."),
341 342
                    documentation_target="install/clone",
                    urgent=True)
343 344 345 346 347 348


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
349
    actually remove them if requested.
350
    """
351
    logging.info("Additional packages removed: %s" % packages)
352
    # Translators: Don't translate {packages}, it's a placeholder and will be replaced.
Alan's avatar
Alan committed
353
    if _notify(_("Remove {packages} from your additional software?").format(
354
                 packages=_format_iterable(packages)),
355
               # Translators: Don't translate {packages}, it's a placeholder and will be replaced.
356 357
               _("This will stop installing {packages} automatically.").format(
                 packages=_format_iterable(packages)),
Alan's avatar
Alan committed
358 359 360
               _("Remove"),
               _("Cancel"),
               urgent=True):
361 362 363 364 365 366
        try:
            remove_additional_packages(packages, search_new_persistence=True)
        except Exception as e:
            _notify_failure(_("The configuration of your additional "
                              "software failed."))
            raise e
367 368 369 370


def setup_additional_packages():
    """Enable additional software in persistence."""
371 372 373
    launch_persistence_setup("--no-gui",
                             "--no-display-finished-message",
                             "--force-enable-preset", "AdditionalSoftware")
374 375 376 377 378 379 380 381 382 383


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.
    """
384
    logging.info("Creating new persistent volume")
385 386 387
    launch_persistence_setup("--step", "bootstrap",
                             "--no-display-finished-message",
                             "--force-enable-preset", "AdditionalSoftware")
388
    add_additional_packages(packages, search_new_persistence=True)
389
    # show persistence configuration
390
    launch_persistence_setup()
391
    # APT lists and APT archive cache will be synchronized at shutdown by
392
    # tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
393 394


395 396
def show_configuration_window():
    """Show additional packages configuration window."""
397 398
    launch_x_application(LIVE_USERNAME,
                         "/usr/local/bin/tails-additional-software-config")
399 400 401 402


def show_system_log():
    """Show additional packages configuration window."""
403 404 405
    launch_x_application(LIVE_USERNAME,
                         "/usr/bin/gedit",
                         ASP_LOG_FILE)
406 407


408 409
def apt_hook_pre():
    """Subcommand to handle Dpkg::Pre-Install-Pkgs."""
410
    _exit_if_in_live_build()
411
    logging.info("Saving package changes")
Alan's avatar
Alan committed
412

Alan's avatar
Alan committed
413 414
    apt_cache = apt.cache.Cache()

415 416 417 418
    installed_packages = []
    removed_packages = []

    line = sys.stdin.readline()
419 420
    if not line.startswith("VERSION 3"):
        raise ASPDataError("APT data is not version 3")
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
    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
451 452 453
            # Filter packages that will only be upgraded
            if not apt_cache[package_name].is_installed:
                installed_packages.append(package_name)
454 455 456 457
        elif action.endswith("**REMOVE**"):
            removed_packages.append(package_name)

    result = {"installed": installed_packages, "removed": removed_packages}
458
    with open(ASP_STATE_PACKAGES, 'w') as f:
459 460 461 462 463 464 465 466 467
        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.
    """
468
    _exit_if_in_live_build()
469
    logging.info("Examining package changes")
Alan's avatar
Alan committed
470

471
    with open(ASP_STATE_PACKAGES) as f:
472
        packages = json.load(f)
473
    os.remove(ASP_STATE_PACKAGES)
474

475
    additional_packages_names = set(map(
476
        filter_package_details,
477
        get_additional_packages(search_new_persistence=True)))
478

479
    apt_cache = apt.cache.Cache()
480 481 482 483
    # Filter automatically installed packages and packages already configured
    # as additional software
    new_manually_installed_packages = set(filter(
        lambda pkg: not apt_cache[pkg].is_auto_installed
484
                    and pkg not in additional_packages_names,    # NOQA: E131
485
        set(packages["installed"])))
486 487
    if new_manually_installed_packages:
        handle_installed_packages(new_manually_installed_packages)
488 489 490

    # Filter non-additional software packages
    additional_packages_removed = set(packages["removed"]).intersection(
491
        additional_packages_names)
492 493
    if additional_packages_removed:
        handle_removed_packages(additional_packages_removed)
494

495 496 497 498 499
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."""
500
    logging.info("Starting to install additional software...")
intrigeri's avatar
intrigeri committed
501

502
    if not has_additional_packages_list():
503
        return True
intrigeri's avatar
intrigeri committed
504

505 506 507 508 509 510 511 512
    # 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.
513
    if os.path.isdir(OLD_APT_LISTS_DIR) and not upgrade_mode:
514
        logging.warning("Found a copy of old APT lists, restoring it.")
515 516
        try:
            restore_old_apt_lists()
517
        except Exception as e:
518 519
            logging.warning("Restoring old APT lists failed with %r, "
                            "deleting them and proceeding anyway." % e)
520 521 522 523 524 525 526
        # 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()

527 528
    packages = get_additional_packages()
    if not packages:
529
        logging.warning("Warning: no packages to install, exiting")
530
        return True
531 532
    if not upgrade_mode:
        installing_notification_id = _notify(
Alan's avatar
Alan committed
533 534
            _("Installing your additional software from persistent "
              "storage..."),
535
            _("This can take several minutes."),
536
            return_id=True)
537 538
    logging.info("Will install the following packages: %s"
                 % " ".join(packages))
Alan's avatar
Alan committed
539 540 541
    apt_get_returncode = _launch_apt_get(
        ["--no-remove",
         "--option", "DPkg::Options::=--force-confold",
542
         "install"] + list(packages))
543
    if apt_get_returncode:
544 545
        logging.warning("Warning: installation of %s failed"
                        % " ".join(packages))
546 547 548 549
        if not upgrade_mode:
            _close_notification(installing_notification_id)
            _notify_failure(_("The installation of your additional software "
                              "failed"))
550 551
        return False
    else:
552
        logging.info("Installation completed successfully.")
553 554 555 556 557 558 559 560 561 562 563
        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"))
564 565
        return True

Alan's avatar
Alan committed
566

567
def upgrade_additional_packages():
568
    """Subcommand which upgrades all additional packages."""
569
    logging.info("Starting to upgrade additional software...")
570 571 572 573

    if not has_additional_packages_list():
        return True

574 575 576
    # 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.
577
    logging.info("Saving old APT lists...")
578 579
    save_old_apt_lists()

580
    apt_get_returncode = _launch_apt_get(["update"])
581
    if apt_get_returncode:
582
        logging.warning("Warning: the update failed.")
583 584
        _notify_failure(_("The check for upgrades of your additional software "
                          "failed"),
585 586 587
                        _("Please check your network connection, "
                          "restart Tails, or read the system log to "
                          "understand the problem."))
588
        return False
589
    if install_additional_packages(upgrade_mode=True):
590
        logging.info("The upgrade was successful.")
591
    else:
592
        _notify_failure(_("The upgrade of your additional software failed"),
593 594 595
                        _("Please check your network connection, "
                          "restart Tails, or read the system log to "
                          "understand the problem."))
596 597
        return False

598 599 600
    # 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
601

602 603 604 605 606 607 608 609 610 611
    # 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:
612
        logging.warning("Warning: autoclean failed.")
613
    return True
Alan's avatar
Alan committed
614

615

616
def print_help():
617
    """Subcommand which displays help."""
618 619
    sys.stderr.write("Usage: %s <subcommand>\n" % program_name)
    sys.stderr.write("""Subcommands:
620 621
    install: install additional software
    upgrade: upgrade additional software\n""")
622

Alan's avatar
Alan committed
623

624 625 626
if __name__ == "__main__":
    program_name = os.path.basename(sys.argv[0])

627 628 629 630 631
    # 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.
632 633 634 635 636 637
    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"
638
    else:
639 640
        log_level = logging.INFO
        log_format = "[%(levelname)s] %(message)s"
Alan's avatar
Alan committed
641
    stderr_handler = logging.StreamHandler()
642
    file_handler = logging.FileHandler(ASP_LOG_FILE)
643
    logging.basicConfig(format=log_format,
Alan's avatar
Alan committed
644
                        handlers=[stderr_handler, file_handler],
645
                        level=log_level)
646

647
    gettext.install("tails")
648 649

    if len(sys.argv) < 2:
Alan's avatar
Alan committed
650
        print_help()
Alan's avatar
Alan committed
651
        sys.exit(2)
652 653 654

    if sys.argv[1] == "install":
        if not install_additional_packages():
Alan's avatar
Alan committed
655
            sys.exit(150)
656 657
    elif sys.argv[1] == "upgrade":
        if not upgrade_additional_packages():
Alan's avatar
Alan committed
658
            sys.exit(151)
659 660 661
    elif sys.argv[1] == "apt-pre":
        apt_hook_pre()
    elif sys.argv[1] == "apt-post":
662
        _spawn_daemon(apt_hook_post)
663 664
    else:
        print_help()
Alan's avatar
Alan committed
665
        sys.exit(2)