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 "
153
154
                    "understand the problem.").format(details=details)

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 = 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"
641
    syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
642
    file_handler = logging.FileHandler(ASP_LOG_FILE)
643
644
645
    logging.basicConfig(format=log_format,
                        handlers=[syslog_handler, file_handler],
                        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)