package.py 5.47 KB
Newer Older
1
#
2
# This file is part of FreedomBox.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
"""
Framework for installing and updating distribution packages
"""

import logging
22
import subprocess
23
import threading
24

25
from django.utils.translation import ugettext as _
26

27
from plinth import actions
28 29 30 31

logger = logging.getLogger(__name__)


32 33 34 35
class PackageException(Exception):
    """A package operation has failed."""

    def __init__(self, error_string=None, error_details=None, *args, **kwargs):
36
        """Store apt-get error string and details."""
37 38 39 40 41
        super(PackageException, self).__init__(*args, **kwargs)

        self.error_string = error_string
        self.error_details = error_details

42 43 44 45 46
    def __str__(self):
        """Return the strin representation of the exception."""
        return 'PackageException(error_string="{0}", error_details="{1}")' \
            .format(self.error_string, self.error_details)

47

48 49 50
class Transaction(object):
    """Information about an ongoing transaction."""

51
    def __init__(self, module_name, package_names):
52 53 54 55
        """Initialize transaction object.

        Set most values to None until they are sent as progress update.
        """
56
        self.module_name = module_name
57 58
        self.package_names = package_names

59
        self._reset_status()
60 61 62 63 64

    def get_id(self):
        """Return a identifier to use as a key in a map of transactions."""
        return frozenset(self.package_names)

65 66 67 68 69
    def _reset_status(self):
        """Reset the current status progress."""
        self.status_string = ''
        self.percentage = 0
        self.stderr = None
70

71
    def install(self, skip_recommends=False):
72
        """Run an apt-get transaction to install given packages.
73

74 75 76 77
        FreedomBox Service (Plinth) needs to be running as root when calling
        this. Currently, this is meant to be only during first time setup when
        --setup is argument is passed.

78 79
        """
        try:
80
            self._run_apt_command(['update'])
81 82 83 84 85 86
            if skip_recommends:
                self._run_apt_command(
                    ['install', '--skip-recommends', self.module_name] +
                    self.package_names)
            else:
                self._run_apt_command(['install', self.module_name] +
87
                                  self.package_names)
88 89 90
        except subprocess.CalledProcessError as exception:
            logger.exception('Error installing package: %s', exception)
            raise
91

92 93 94 95 96 97 98 99
    def refresh_package_lists(self):
        """Refresh apt package lists."""
        try:
            self._run_apt_command(['update'])
        except subprocess.CalledProcessError as exception:
            logger.exception('Error updating package lists: %s', exception)
            raise

100 101 102 103
    def _run_apt_command(self, arguments):
        """Run apt-get and update progress."""
        self._reset_status()

104 105
        process = actions.superuser_run('packages', arguments,
                                        run_in_background=True)
106 107 108
        process.stdin.close()

        stdout_thread = threading.Thread(target=self._read_stdout,
109
                                         args=(process, ))
110
        stderr_thread = threading.Thread(target=self._read_stderr,
111
                                         args=(process, ))
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
        stdout_thread.start()
        stderr_thread.start()
        stdout_thread.join()
        stderr_thread.join()

        return_code = process.wait()
        if return_code != 0:
            raise PackageException(_('Error during installation'), self.stderr)

    def _read_stdout(self, process):
        """Read the stdout of the process and update progress."""
        for line in process.stdout:
            self._parse_progress(line.decode())

    def _read_stderr(self, process):
        """Read the stderr of the process and store in buffer."""
        self.stderr = process.stderr.read().decode()

    def _parse_progress(self, line):
        """Parse the apt-get process output line.

        See README.progress-reporting in apt source code.
        """
        parts = line.split(':')
        if len(parts) < 4:
            return

        status_map = {
140 141 142 143 144 145 146 147
            'pmstatus':
                _('installing'),
            'dlstatus':
                _('downloading'),
            'media-change':
                _('media change'),
            'pmconffile':
                _('configuration file: {file}').format(file=parts[1]),
148 149 150
        }
        self.status_string = status_map.get(parts[0], '')
        self.percentage = int(float(parts[2]))
151 152 153 154 155


def is_package_manager_busy():
    """Return whether a package manager is running."""
    try:
156 157
        actions.superuser_run('packages', ['is-package-manager-busy'],
                              log_error=False)
158 159 160
        return True
    except actions.ActionError:
        return False
161 162 163 164 165 166


def refresh_package_lists():
    """To be run in case apt package lists are outdated."""
    transaction = Transaction(None, None)
    transaction.refresh_package_lists()