tails-additional-software 24.9 KB
Newer Older
1
#!/usr/bin/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.config import (
    add_additional_packages,
Alan's avatar
Alan committed
20
    filter_package_details,
21
22
    get_additional_packages,
    get_packages_list_path,
23
    remove_additional_packages)
24

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

32
33
from tailslib.utils import launch_x_application

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

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

Alan's avatar
Alan committed
44

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


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

54
    Launch apt-get with given arguments list, log its standard and error output
55
    and return its returncode."""
56
    apt_get_env = os.environ.copy()
57
58
    # 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
59
60
61
62
    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
63
    apt_get_env['LANG'] = "C"
64
    apt_get_env['DEBIAN_PRIORITY'] = "critical"
65
66
    args = ["apt-get", "--quiet", "--yes"]
    args.extend(specific_args)
67
68
    apt_get = subprocess.Popen(args,
                               env=apt_get_env,
bertagaz's avatar
bertagaz committed
69
                               universal_newlines=True,
70
71
                               stderr=subprocess.STDOUT,
                               stdout=subprocess.PIPE)
72
73
    for line in iter(apt_get.stdout.readline, ''):
        if not line.startswith('('):
74
            logging.info(line.rstrip())
75
    apt_get.wait()
Tails developers's avatar
Tails developers committed
76
    if apt_get.returncode:
77
        logging.warn("apt-get exited with returncode %i" % apt_get.returncode)
78
79
    return apt_get.returncode

Alan's avatar
Alan committed
80

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

85
86
87
    The notification will show title and body.

    If accept_label or deny_label are set, they will be shown on action buttons
88
89
    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
90
91
92
93
    clicked.

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

    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
100

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

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

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

135

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

    The user has the option to edit the configuration of to view the system
    log.
    """
142
143
    if details:
        details = _("{details} Please check your list of additional "
144
                    "software or read the system log to "
145
146
147
148
                    "understand the problem.").format(details=details)

    else:
        details = _("Please check your list of additional "
149
                    "software or read the system log to "
150
151
                    "understand the problem.").format(details=details)

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


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


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

    # do stuff
    func()


209
210
211
def _format_iterable(iterable):
    """Return a nice formatted string with the elements of iterable."""
    iterable = sorted(iterable)
212

213
    if len(iterable) == 1:
Alan's avatar
Alan committed
214
        return iterable[0]
215
216
217
    elif len(iterable) > 1:
        return _("{beginning} and {last}").format(
            beginning=_(", ").join(iterable[:-1]), last=iterable[-1])
218
    else:
Alan's avatar
Alan committed
219
        return str(iterable)
220
221


222
223
224
def has_additional_packages_list(search_new_persistence=False):
    """Return true iff a packages list file is found in a persistence.

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


241
def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
242
    """Delete the copy of the old APT lists, if any."""
243
244
245
246
    shutil.rmtree(old_apt_lists_dir)


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


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


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
342
    actually remove them if requested.
343
    """
344
    logging.info("Additional packages removed: %s" % packages)
Alan's avatar
Alan committed
345
    if _notify(_("Remove {packages} from your additional software?").format(
346
                 packages=_format_iterable(packages)),
347
348
               _("This will stop installing {packages} automatically.").format(
                 packages=_format_iterable(packages)),
Alan's avatar
Alan committed
349
350
351
               _("Remove"),
               _("Cancel"),
               urgent=True):
352
353
354
355
356
357
        try:
            remove_additional_packages(packages, search_new_persistence=True)
        except Exception as e:
            _notify_failure(_("The configuration of your additional "
                              "software failed."))
            raise e
358
359
360
361


def setup_additional_packages():
    """Enable additional software in persistence."""
362
363
364
    launch_persistence_setup("--no-gui",
                             "--no-display-finished-message",
                             "--force-enable-preset", "AdditionalSoftware")
365
366
367
368
369
370
371
372
373
374


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.
    """
375
    logging.info("Creating new persistent volume")
376
377
378
    launch_persistence_setup("--step", "bootstrap",
                             "--no-display-finished-message",
                             "--force-enable-preset", "AdditionalSoftware")
379
    add_additional_packages(packages, search_new_persistence=True)
380
    # show persistence configuration
381
    launch_persistence_setup()
382
    # APT lists and APT archive cache will be synchronized at shutdown by
383
    # tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
384
385


386
387
def show_configuration_window():
    """Show additional packages configuration window."""
388
389
    launch_x_application(LIVE_USERNAME,
                         "/usr/local/bin/tails-additional-software-config")
390
391
392
393


def show_system_log():
    """Show additional packages configuration window."""
394
395
396
    launch_x_application(LIVE_USERNAME,
                         "/usr/bin/gedit",
                         ASP_LOG_FILE)
397
398


399
400
def apt_hook_pre():
    """Subcommand to handle Dpkg::Pre-Install-Pkgs."""
401
    _exit_if_in_live_build()
402
    logging.info("Saving package changes")
Alan's avatar
Alan committed
403

Alan's avatar
Alan committed
404
405
    apt_cache = apt.cache.Cache()

406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
    installed_packages = []
    removed_packages = []

    line = sys.stdin.readline()
    assert line.startswith("VERSION 3")
    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
441
442
443
            # Filter packages that will only be upgraded
            if not apt_cache[package_name].is_installed:
                installed_packages.append(package_name)
444
445
446
447
        elif action.endswith("**REMOVE**"):
            removed_packages.append(package_name)

    result = {"installed": installed_packages, "removed": removed_packages}
448
    with open(ASP_STATE_PACKAGES, 'w') as f:
449
450
451
452
453
454
455
456
457
        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.
    """
458
    _exit_if_in_live_build()
459
    logging.info("Examining package changes")
Alan's avatar
Alan committed
460

461
    with open(ASP_STATE_PACKAGES) as f:
462
        packages = json.load(f)
463
    os.remove(ASP_STATE_PACKAGES)
464

465
466
467
    additional_packages_names = map(
        filter_package_details,
        get_additional_packages(search_new_persistence=True))
468

469
    apt_cache = apt.cache.Cache()
470
471
472
473
    # 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
474
                    and pkg not in additional_packages_names,    # NOQA: E131
475
        set(packages["installed"])))
476
477
    if new_manually_installed_packages:
        handle_installed_packages(new_manually_installed_packages)
478
479
480

    # Filter non-additional software packages
    additional_packages_removed = set(packages["removed"]).intersection(
481
        additional_packages_names)
482
483
    if additional_packages_removed:
        handle_removed_packages(additional_packages_removed)
484
485


486
def install_additional_packages(ignore_old_apt_lists=False):
487
    """Subcommand which activates and installs all additional packages."""
488
    logging.info("Starting to install additional software...")
intrigeri's avatar
intrigeri committed
489

490
    if not has_additional_packages_list():
491
        return True
intrigeri's avatar
intrigeri committed
492

493
494
495
496
497
498
499
500
501
    # 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.
    if os.path.isdir(OLD_APT_LISTS_DIR) and not ignore_old_apt_lists:
502
        logging.warn("Found a copy of old APT lists, restoring it.")
503
504
        try:
            restore_old_apt_lists()
505
        except Exception as e:
506
507
            logging.warn("Restoring old APT lists failed with %r, "
                         "deleting them and proceeding anyway." % e)
508
509
510
511
512
513
514
        # 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()

515
516
    packages = get_additional_packages()
    if not packages:
517
        logging.warn("Warning: no packages to install, exiting")
518
        return True
519
    installing_notification_id = _notify(
Alan's avatar
Alan committed
520
521
            _("Installing your additional software from persistent "
              "storage..."),
522
            _("This can take several minutes."),
523
            return_id=True)
524
525
    logging.info("Will install the following packages: %s"
                 % " ".join(packages))
Alan's avatar
Alan committed
526
527
528
    apt_get_returncode = _launch_apt_get(
        ["--no-remove",
         "--option", "DPkg::Options::=--force-confold",
529
         "install"] + list(packages))
530
    if apt_get_returncode:
531
        _close_notification(installing_notification_id)
532
        logging.warn("Warning: installation of %s failed" % " ".join(packages))
533
        _notify_failure(_("The installation of your additional software "
534
                          "failed"))
535
536
        return False
    else:
537
        logging.info("Installation completed successfully.")
538
        _close_notification(installing_notification_id)
539
540
541
542
543
544
545
546
        # 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"))
547
548
        return True

Alan's avatar
Alan committed
549

550
def upgrade_additional_packages():
551
    """Subcommand which upgrades all additional packages."""
552
    logging.info("Starting to upgrade additional software...")
553
554
555
556

    if not has_additional_packages_list():
        return True

557
558
559
    # 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.
560
    logging.info("Saving old APT lists...")
561
562
    save_old_apt_lists()

563
    apt_get_returncode = _launch_apt_get(["update"])
564
    if apt_get_returncode:
565
        logging.warn("Warning: the update failed.")
566
567
        _notify_failure(_("The check for upgrades of your additional software "
                          "failed"),
568
569
570
                        _("Please check your network connection, "
                          "restart Tails, or read the system log to "
                          "understand the problem."))
571
        return False
572
    if install_additional_packages(ignore_old_apt_lists=True):
573
        logging.info("The upgrade was successful.")
574
    else:
575
        _notify_failure(_("The upgrade of your additional software failed"),
576
577
578
                        _("Please check your network connection, "
                          "restart Tails, or read the system log to "
                          "understand the problem."))
579
580
        return False

581
582
583
    # 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
584

585
586
587
588
589
590
591
592
593
594
    # 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:
595
        logging.warn("Warning: autoclean failed.")
596
    return True
Alan's avatar
Alan committed
597

598

599
def print_help():
600
    """Subcommand which displays help."""
601
602
    sys.stderr.write("Usage: %s <subcommand>\n" % program_name)
    sys.stderr.write("""Subcommands:
603
604
    install: install additional software
    upgrade: upgrade additional software\n""")
605

Alan's avatar
Alan committed
606

607
608
609
if __name__ == "__main__":
    program_name = os.path.basename(sys.argv[0])

610
611
612
613
614
    # 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.
615
616
617
618
619
620
    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"
621
    else:
622
623
        log_level = logging.INFO
        log_format = "[%(levelname)s] %(message)s"
624
    syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
625
    file_handler = logging.FileHandler(ASP_LOG_FILE)
626
627
628
    logging.basicConfig(format=log_format,
                        handlers=[syslog_handler, file_handler],
                        level=log_level)
629

630
    gettext.install("tails")
631
632

    if len(sys.argv) < 2:
Alan's avatar
Alan committed
633
        print_help()
Alan's avatar
Alan committed
634
        sys.exit(2)
635
636
637

    if sys.argv[1] == "install":
        if not install_additional_packages():
Alan's avatar
Alan committed
638
            sys.exit(150)
639
640
    elif sys.argv[1] == "upgrade":
        if not upgrade_additional_packages():
Alan's avatar
Alan committed
641
            sys.exit(151)
642
643
644
    elif sys.argv[1] == "apt-pre":
        apt_hook_pre()
    elif sys.argv[1] == "apt-post":
645
        _spawn_daemon(apt_hook_post)
646
647
    else:
        print_help()
Alan's avatar
Alan committed
648
        sys.exit(2)