~ubuntu-branches/ubuntu/raring/sessioninstaller/raring

« back to all changes in this revision

Viewing changes to .pc/05_parent_process_name.patch/sessioninstaller/core.py

  • Committer: Package Import Robot
  • Author(s): POJAR GEORGE
  • Date: 2013-04-08 08:45:12 UTC
  • Revision ID: package-import@ubuntu.com-20130408084512-ve3nt50jq0bdl3ey
Tags: 0.20+bzr134-0ubuntu6
* debian/patches/05_parent_process_name.patch:
  - Cherry pick upstream patch to fix the detection of the client application.
    Patch from Sebastian Heinlein, thanks! (LP: #1049467)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# -*- coding: utf-8 -*-
 
3
"""core - APT based installer using the PackageKit DBus interface"""
 
4
# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>
 
5
# Copyright (C) 2009 Richard Hughes <richard@hughsie.com>
 
6
# Copyright (C) 2008 Sebastian Dröge <sebastian.droege@collabora.co.uk>
 
7
#
 
8
# This program is free software; you can redistribute it and/or modify
 
9
# it under the terms of the GNU General Public License as published by
 
10
# the Free Software Foundation; either version 2 of the License, or
 
11
# any later version.
 
12
#
 
13
# This program is distributed in the hope that it will be useful,
 
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
16
# GNU General Public License for more details.
 
17
#
 
18
# You should have received a copy of the GNU General Public License along
 
19
# with this program; if not, write to the Free Software Foundation, Inc.,
 
20
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
21
 
 
22
__author__  = "Sebastian Heinlein <devel@glatzor.de>"
 
23
__state__   = "experimental"
 
24
 
 
25
from ConfigParser import ConfigParser
 
26
import functools
 
27
import locale
 
28
import logging
 
29
import os
 
30
import re
 
31
import subprocess
 
32
import time
 
33
 
 
34
import apt
 
35
import apt.debfile
 
36
import apt_pkg
 
37
from aptdaemon.policykit1 import get_pid_from_dbus_name
 
38
from defer import Deferred, defer, inline_callbacks, return_value
 
39
from defer.utils import dbus_deferred_method
 
40
import dbus
 
41
import dbus.service
 
42
import dbus.mainloop.glib
 
43
from gettext import gettext as _
 
44
import gettext
 
45
from gi.repository import Gio
 
46
from gi.repository import GObject
 
47
from gi.repository import Gst
 
48
from gi.repository import Gtk
 
49
from gi.repository import Pango
 
50
from xdg.DesktopEntry import DesktopEntry
 
51
 
 
52
import utils
 
53
import errors
 
54
 
 
55
_backend_env = os.getenv("SESSIONINSTALLER_BACKEND", "aptdaemon")
 
56
if _backend_env == "synaptic":
 
57
    from backends.synaptic import SynapticBackend as Backend
 
58
elif _backend_env == "aptdaemon":
 
59
    from backends.aptd import AptDaemonBackend as Backend
 
60
else:
 
61
    from backends.dummy import DummyBackend as Backend
 
62
 
 
63
 
 
64
gettext.textdomain("sessioninstaller")
 
65
gettext.bindtextdomain("sessioninstaller")
 
66
 
 
67
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 
68
 
 
69
PACKAGEKIT_QUERY_DBUS_INTERFACE = "org.freedesktop.PackageKit.Query"
 
70
PACKAGEKIT_MODIFY_DBUS_INTERFACE = "org.freedesktop.PackageKit.Modify"
 
71
PACKAGEKIT_DBUS_PATH = "/org/freedesktop/PackageKit"
 
72
PACKAGEKIT_DBUS_SERVICE = "org.freedesktop.PackageKit"
 
73
 
 
74
INTERACT_NEVER = 0
 
75
INTERACT_CONFIRM_SEARCH = 1
 
76
INTERACT_CONFIRM_DEPS = 2
 
77
INTERACT_CONFIRM_INSTALL = 4
 
78
INTERACT_PROGRESS = 8
 
79
INTERACT_FINISHED = 16
 
80
INTERACT_WARNING = 32
 
81
INTERACT_UNKNOWN = 64
 
82
INTERACT_ALWAYS = 127
 
83
 
 
84
GSTREAMER_RECORD_MAP = {"encoder": "Gstreamer-Encoders",
 
85
                        "decoder": "Gstreamer-Decoders",
 
86
                        "urisource": "Gstreamer-Uri-Sources",
 
87
                        "urisink": "Gstreamer-Uri-Sinks",
 
88
                        "element": "Gstreamer-Elements"}
 
89
 
 
90
RESTRICTED_010_PACKAGES = ["gstreamer0.10-plugins-bad",
 
91
                           "gstreamer0.10-plugins-bad-multiverse",
 
92
                           "gstreamer0.10-ffmpeg",
 
93
                           "gstreamer0.10-plugins-ugly"]
 
94
 
 
95
RESTRICTED_10_PACKAGES = ["gstreamer1.0-plugins-bad",
 
96
                          "gstreamer1.0-libav",
 
97
                          "gstreamer1.0-plugins-ugly"]
 
98
 
 
99
GSTREAMER_010_SCORING = {"gstreamer0.10-plugins-good": 100,
 
100
                         "gstreamer0.10-fluendo-mp3": 90,
 
101
                         "gstreamer0.10-ffmpeg": 79,
 
102
                         "gstreamer0.10-plugins-ugly": 80,
 
103
                         "gstreamer0.10-plugins-bad": 70,
 
104
                         "gstreamer0.10-plugins-bad-multiverse": 60}
 
105
 
 
106
GSTREAMER_10_SCORING = {"gstreamer1.0-plugins-good": 100,
 
107
#                        "gstreamer1.0-fluendo-mp3": 90,
 
108
                        "gstreamer1.0-libav": 79,
 
109
                        "gstreamer1.0-plugins-ugly": 80,
 
110
                        "gstreamer1.0-plugins-bad": 70}
 
111
 
 
112
 
 
113
logging.basicConfig(format="%(levelname)s:%(message)s")
 
114
log = logging.getLogger("sessioninstaller")
 
115
 
 
116
(COLUMN_NAME,
 
117
 COLUMN_DESC,
 
118
 COLUMN_INSTALL,
 
119
 COLUMN_DETAILS,
 
120
 COLUMN_TOOLTIP,
 
121
 COLUMN_SCORE) = range(6)
 
122
 
 
123
DAEMON_IDLE_TIMEOUT = 3 * 60
 
124
DAEMON_IDLE_CHECK_INTERVAL = 30
 
125
 
 
126
# Required to get translated descriptions
 
127
try:
 
128
    locale.setlocale(locale.LC_ALL, "")
 
129
except locale.Error:
 
130
    log.debug("Failed to unset LC_ALL")
 
131
 
 
132
def track_usage(func):
 
133
    """Decorator to keep track of running methods and to update the time
 
134
    stamp of the last action.
 
135
    """
 
136
    @functools.wraps(func)
 
137
    def _track_usage(*args, **kwargs):
 
138
        def _release_track(ret, si):
 
139
            si._tracks -= 1
 
140
            si._last_timestamp = time.time()
 
141
            log.debug("Updating last_timestamp")
 
142
            return ret
 
143
        si = args[0]
 
144
        si._tracks += 1
 
145
        si._last_timestamp = time.time()
 
146
        log.debug("Updating last_timestamp")
 
147
        deferred = defer(func, *args, **kwargs)
 
148
        deferred.add_callbacks(_release_track, _release_track,
 
149
                               callback_args=[si],
 
150
                               errback_args=[si])
 
151
        return deferred
 
152
    return _track_usage
 
153
 
 
154
 
 
155
class GStreamerStructure(object):
 
156
 
 
157
    """Abstraction class of GStramer structure."""
 
158
 
 
159
    def __init__(self, name="", version="", kind="", record="", caps="",
 
160
                 element=""):
 
161
        self.name = name
 
162
        self.version = version
 
163
        self.kind = kind
 
164
        self.record = record
 
165
        self.caps = caps
 
166
        self.element = element
 
167
        self.satisfied = False
 
168
        self.best_provider = None
 
169
        self.best_score = -1
 
170
 
 
171
 
 
172
class GtkOpProgress(apt.progress.base.OpProgress):
 
173
 
 
174
    """A simple helper that keeps the GUI alive."""
 
175
 
 
176
    def __init__(self, progress=None):
 
177
        apt.progress.base.OpProgress.__init__(self)
 
178
        self.progress_dialog = progress
 
179
 
 
180
    def update(self, percent):
 
181
        while Gtk.events_pending():
 
182
            Gtk.main_iteration()
 
183
        if self.progress_dialog:
 
184
            self.progress_dialog.progress.pulse()
 
185
 
 
186
 
 
187
class ErrorDialog(Gtk.MessageDialog):
 
188
 
 
189
    """Allows to show an error message to the user."""
 
190
 
 
191
    def __init__(self, title, message, parent=None):
 
192
        GObject.GObject.__init__(self, message_type=Gtk.MessageType.ERROR,
 
193
                                   buttons=Gtk.ButtonsType.CLOSE)
 
194
        if parent:
 
195
            self.realize()
 
196
            self.set_transient_for(parent)
 
197
        self.set_markup("<b><big>%s</big></b>\n\n%s" % (title, message))
 
198
 
 
199
 
 
200
class ProgressDialog(Gtk.Dialog):
 
201
 
 
202
    """Allows to show the progress of an ongoing action to the user."""
 
203
 
 
204
    def __init__(self, title, message, parent=None):
 
205
        Gtk.Dialog.__init__(self)
 
206
        self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
 
207
        if parent:
 
208
            self.realize()
 
209
            self.set_transient_for(parent)
 
210
        self.set_title(title)
 
211
        self.set_resizable(False)
 
212
        self.set_border_width(12)
 
213
        self.vbox.set_spacing(24)
 
214
        self.label = Gtk.Label()
 
215
        self.label.set_markup("<b><big>%s</big></b>\n\n%s" % (title, message))
 
216
        self.label.set_line_wrap(True)
 
217
        self.vbox.add(self.label)
 
218
        self.progress = Gtk.ProgressBar()
 
219
        self.progress.set_pulse_step(0.01)
 
220
        self.vbox.add(self.progress)
 
221
        self.cancelled = False
 
222
        self.connect("response", self._on_response)
 
223
 
 
224
    def _on_response(self, dialog, response):
 
225
        if response == Gtk.ResponseType.CANCEL:
 
226
            self.cancelled = True
 
227
 
 
228
 
 
229
class ConfirmInstallDialog(Gtk.Dialog):
 
230
 
 
231
    """Allow to confirm an installation."""
 
232
 
 
233
    def __init__(self, title, message, pkgs=[], parent=None, details=None,
 
234
                 package_type=None, selectable=False, action=None):
 
235
        Gtk.Dialog.__init__(self)
 
236
        self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
 
237
        if parent:
 
238
            self.realize()
 
239
            self.set_transient_for(parent)
 
240
        self.set_title(title)
 
241
        self.set_resizable(True)
 
242
        self.set_border_width(12)
 
243
        self.vbox.set_spacing(12)
 
244
        self.icon = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_QUESTION,
 
245
                                             Gtk.IconSize.DIALOG)
 
246
        self.icon.set_alignment(0 ,0)
 
247
        hbox_base = Gtk.HBox()
 
248
        hbox_base.set_spacing(24)
 
249
        vbox_left = Gtk.VBox()
 
250
        vbox_left.set_spacing(12)
 
251
        hbox_base.pack_start(self.icon, False, True, 0)
 
252
        hbox_base.pack_start(vbox_left, True, True, 0)
 
253
        self.label = Gtk.Label()
 
254
        self.label.set_alignment(0, 0)
 
255
        self.label.set_markup("<b><big>%s</big></b>\n\n%s" % (title, message))
 
256
        self.label.set_line_wrap(True)
 
257
        vbox_left.pack_start(self.label, False, True, 0)
 
258
        self.cancelled = False
 
259
        self.vbox.pack_start(hbox_base, True, True, 0)
 
260
        if not action:
 
261
            action = _("_Install")
 
262
        self.install_button = self.add_button(action, Gtk.ResponseType.OK)
 
263
        self.set_default_response(Gtk.ResponseType.OK)
 
264
        # Show a list of the plugin packages
 
265
        self.pkg_store = Gtk.ListStore(GObject.TYPE_STRING,
 
266
                                       GObject.TYPE_STRING,
 
267
                                       GObject.TYPE_BOOLEAN,
 
268
                                       GObject.TYPE_STRING,
 
269
                                       GObject.TYPE_STRING,
 
270
                                       GObject.TYPE_INT)
 
271
        self.pkg_store.set_sort_column_id(COLUMN_SCORE, Gtk.SortType.DESCENDING)
 
272
        self.pkg_view = Gtk.TreeView(self.pkg_store)
 
273
        self.pkg_view.set_rules_hint(True)
 
274
        self.pkg_view.props.has_tooltip = True
 
275
        self.pkg_view.connect("query-tooltip", self._on_query_tooltip)
 
276
        if selectable:
 
277
            toggle_install = Gtk.CellRendererToggle()
 
278
            toggle_install.connect("toggled", self._on_toggled_install)
 
279
            column_install = Gtk.TreeViewColumn(_("Install"), toggle_install,
 
280
                                                active=COLUMN_INSTALL)
 
281
            self.pkg_view.append_column(column_install)
 
282
        if not package_type:
 
283
            package_type = _("Package")
 
284
        column_desc = Gtk.TreeViewColumn(package_type)
 
285
        renderer_warn = Gtk.CellRendererPixbuf()
 
286
        renderer_warn.props.stock_size = Gtk.IconSize.MENU
 
287
        column_desc.pack_start(renderer_warn, False)
 
288
        column_desc.set_cell_data_func(renderer_warn, self._render_warning)
 
289
        renderer_desc = Gtk.CellRendererText()
 
290
        renderer_desc.props.ellipsize = Pango.EllipsizeMode.END
 
291
        column_desc.pack_start(renderer_desc, True)
 
292
        column_desc.add_attribute(renderer_desc, "markup", COLUMN_DESC)
 
293
        column_desc.props.expand = True
 
294
        column_desc.props.resizable = True
 
295
        column_desc.props.min_width = 50
 
296
        self.pkg_view.append_column(column_desc)
 
297
        if details:
 
298
            renderer_details = Gtk.CellRendererText()
 
299
            renderer_details.props.ellipsize = Pango.EllipsizeMode.END
 
300
            column_details = Gtk.TreeViewColumn(details,
 
301
                                                renderer_details)
 
302
            column_details.add_attribute(renderer_details, "markup",
 
303
                                         COLUMN_DETAILS)
 
304
            column_details.props.resizable = True
 
305
            column_details.props.min_width = 50
 
306
            self.pkg_view.append_column(column_details)
 
307
        if not (selectable or details):
 
308
            self.pkg_view.props.headers_visible = False
 
309
        self.scrolled_window = Gtk.ScrolledWindow()
 
310
        self.scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
 
311
        self.scrolled_window.set_shadow_type(Gtk.ShadowType.IN)
 
312
        self.scrolled_window.add(self.pkg_view)
 
313
        vbox_left.pack_start(self.scrolled_window, True, True, 0)
 
314
        self.install_button.props.sensitive = False
 
315
        for pkg in pkgs:
 
316
            self.add_confirm_package(pkg)
 
317
 
 
318
    def add_confirm(self, name, summary, active=True, details="", score=0,
 
319
                    restricted=False):
 
320
        """Add an entry to the confirmation dialog.
 
321
 
 
322
        Keyword arguments:
 
323
        name -- name of the package or file
 
324
        summary -- short description of the package
 
325
        active -- if the package should be selected by default
 
326
        details -- additional information about the package
 
327
        score -- the ranking of the package which should be used for ordering
 
328
        restricted -- if the use or redistribution is restriceted
 
329
        """
 
330
        if restricted:
 
331
            #TRANSLATORS: %s is the name of a piece of software
 
332
            tooltip = _("The use of %s may be restricted in some "
 
333
                        "countries. You must verify that one of the following "
 
334
                        "is true:\n"
 
335
                        "- These restrictions do not apply in your country "
 
336
                        "of legal residence\n"
 
337
                        "- You have permission to use this software (for "
 
338
                        "example, a patent license)\n"
 
339
                        "- You are using this software for research "
 
340
                        "purposes only") % name
 
341
            # Set the dialog default to cancel if a restricted packages is
 
342
            # selected for installation
 
343
            if active:
 
344
                self.set_default_response(Gtk.ResponseType.CANCEL)
 
345
        else:
 
346
            tooltip = ""
 
347
        desc = utils.get_package_desc(name, summary)
 
348
        self.pkg_store.append((name, desc, active, details, tooltip, score))
 
349
        if active:
 
350
            self.install_button.props.sensitive = True
 
351
 
 
352
    def add_confirm_package(self, pkg, active=True, details="", score=0):
 
353
        """Show an apt.package.Package instance in the confirmation dialog.
 
354
 
 
355
        Keyword arguments:
 
356
        pkg -- the apt.package.Package instance
 
357
        active -- if the package should be selected by default
 
358
        details -- additional information about the package
 
359
        score -- the ranking of the package which should be used for ordering
 
360
        """
 
361
        self.add_confirm(pkg.name, pkg.summary, active, details, score,
 
362
                         _is_package_restricted(pkg))
 
363
 
 
364
    def get_selected_pkgs(self):
 
365
        """Return a list of the package names which are selected."""
 
366
        return [pkg \
 
367
                for pkg, _desc, active, _details, _tooltip, _score \
 
368
                in self.pkg_store \
 
369
                if active]
 
370
 
 
371
    def _on_query_tooltip(self, treeview, x, y, keyboard_tip, tooltip):
 
372
        """Handle tooltips for restrcited packages."""
 
373
        try:
 
374
            result, out_x, out_y, model, path, iter = treeview.get_tooltip_context(x, y, keyboard_tip)
 
375
            if not result:
 
376
                return False
 
377
        except TypeError:
 
378
            return False
 
379
 
 
380
        text = model[path][COLUMN_TOOLTIP]
 
381
        if not text:
 
382
            return False
 
383
        tooltip.set_icon_from_icon_name(Gtk.STOCK_DIALOG_WARNING,
 
384
                                        Gtk.IconSize.DIALOG)
 
385
        tooltip.set_markup(text)
 
386
        treeview.set_tooltip_row(tooltip, path)
 
387
        return True
 
388
 
 
389
    def _on_toggled_install(self, toggle, path):
 
390
        """Handle an activated package."""
 
391
        cur = toggle.get_active()
 
392
        self.pkg_store[path][COLUMN_INSTALL] = not cur
 
393
        for row in self.pkg_store:
 
394
            if row[COLUMN_INSTALL]:
 
395
                self.install_button.props.sensitive = True
 
396
                return
 
397
        self.install_button.props.sensitive = False
 
398
 
 
399
    def _render_warning(self, cell, renderer, model, iter, data):
 
400
        """Show a warning icon for restricted packages."""
 
401
        if model.get_value(iter, COLUMN_TOOLTIP):
 
402
            renderer.props.stock_id = Gtk.STOCK_DIALOG_WARNING
 
403
        else:
 
404
            renderer.props.stock_id = None
 
405
 
 
406
    def run(self):
 
407
        """Run the dialog."""
 
408
        if len(self.pkg_store) > 4:
 
409
            self.scrolled_window.set_policy(Gtk.PolicyType.NEVER,
 
410
                                            Gtk.PolicyType.AUTOMATIC)
 
411
            self.pkg_view.set_size_request(-1, 240)
 
412
        self.show_all()
 
413
        res = Gtk.Dialog.run(self)
 
414
        self.hide()
 
415
        self.destroy()
 
416
        return res
 
417
 
 
418
 
 
419
class SessionInstaller(dbus.service.Object):
 
420
 
 
421
    """Provides the PackageKit session API."""
 
422
 
 
423
    def __init__(self, bus=None):
 
424
        log.info("Starting service")
 
425
        self.loop = GObject.MainLoop()
 
426
        if not bus:
 
427
            bus = dbus.SessionBus()
 
428
        self.bus = bus
 
429
        bus_name = dbus.service.BusName(PACKAGEKIT_DBUS_SERVICE, bus)
 
430
        dbus.service.Object.__init__(self, bus_name, PACKAGEKIT_DBUS_PATH)
 
431
        self._cache = None
 
432
        self.backend = Backend()
 
433
        GObject.timeout_add_seconds(DAEMON_IDLE_CHECK_INTERVAL,
 
434
                                    self._check_for_inactivity)
 
435
        self._tracks = 0
 
436
        self._last_timestamp = time.time()
 
437
 
 
438
    def _check_for_inactivity(self):
 
439
        """Quit after a period of inactivity."""
 
440
        idle_time = time.time() - self._last_timestamp
 
441
        log.debug("Checking for inactivity (%is)", idle_time)
 
442
        if not self._tracks and \
 
443
           not GObject.main_context_default().pending() and \
 
444
           idle_time > DAEMON_IDLE_TIMEOUT:
 
445
            self.loop.quit()
 
446
            log.info("Shutting down because of inactivity")
 
447
        return True
 
448
 
 
449
    def run(self):
 
450
        """Start listening for requests."""
 
451
        self.loop.run()
 
452
 
 
453
    def _init_cache(self, progress=None):
 
454
        """Helper to set up the package cache."""
 
455
        if not self._cache:
 
456
            self._cache = apt.Cache(GtkOpProgress(progress))
 
457
 
 
458
    @inline_callbacks
 
459
    def _get_sender_name(self, sender):
 
460
        """Try to resolve the name of the calling application."""
 
461
        pid = yield get_pid_from_dbus_name(sender, self.bus)
 
462
        try:
 
463
            exe = os.readlink("/proc/%s/exe" % pid)
 
464
        except:
 
465
            return_value(None)
 
466
        # Try to get the name of an interpreted script
 
467
        if exe in ["/usr/bin/python", "/usr/bin/python2.6", "/usr/bin/perl"]:
 
468
            try:
 
469
                fcmd = open("/proc/%s/cmdline" % pid)
 
470
                exe = fcmd.read().split("\0")[1]
 
471
                fcmd.close()
 
472
            except (IndexError, OSError):
 
473
                pass
 
474
        # Special case for the GStreamer codec installation via the helper
 
475
        # gnome-packagekit returns the name of parent window in this case,
 
476
        # But this could be misleading:
 
477
        # return_value(parent.property_get("WM_NAME")[2])
 
478
        if exe in ["/usr/libexec/pk-gstreamer-install",
 
479
                   "/usr/lib/pk-gstreamer-install",
 
480
                   "/usr/bin/gst-install"]:
 
481
            return_value(None)
 
482
        # Return the application name in the case of an installed application
 
483
        for app in Gio.app_info_get_all():
 
484
            if app.get_executable() == exe:
 
485
                return_value(app.get_name())
 
486
        return_value(os.path.basename(exe))
 
487
 
 
488
    def _parse_interaction(self, interaction):
 
489
        mode = 0
 
490
        interact_list = interaction.split(",")
 
491
        if "always" in interact_list:
 
492
            mode = INTERACT_ALWAYS
 
493
        elif "never" in interact_list:
 
494
            mode = INTERACT_NEVER
 
495
        if "show-confirm-progress" in interact_list:
 
496
            mode &= INTERACT_CONFIRM_PROGRESS
 
497
        elif "show-confirm-deps" in interact_list:
 
498
            mode &= INTERACT_CONFIRM_DEPS
 
499
        elif "show-confirm-install" in interact_list:
 
500
            mode &= INTERACT_CONFIRM_INSTALL
 
501
        elif "show-progress" in interact_list:
 
502
            mode &= INTERACT_PROGRESS
 
503
        elif "show-finished" in interact_list:
 
504
            mode &= INTERACT_FINISHED
 
505
        elif "show-warning" in interact_list:
 
506
            mode &= INTERACT_WARNING
 
507
        elif "hide-confirm-progress" in interact_list:
 
508
            mode |= INTERACT_CONFIRM_PROGRESS
 
509
        elif "hide-confirm-deps" in interact_list:
 
510
            mode |= INTERACT_CONFIRM_DEPS
 
511
        elif "hide-confirm-install" in interact_list:
 
512
            mode |= INTERACT_CONFIRM_INSTALL
 
513
        elif "hide-progress" in interact_list:
 
514
            mode |= INTERACT_PROGRESS
 
515
        elif "hide-finished" in interact_list:
 
516
            mode |= INTERACT_FINISHED
 
517
        elif "hide-warning" in interact_list:
 
518
            mode |= INTERACT_WARNING
 
519
        return mode
 
520
 
 
521
    @dbus_deferred_method(PACKAGEKIT_QUERY_DBUS_INTERFACE,
 
522
                          in_signature="ss", out_signature="b",
 
523
                          utf8_strings=True)
 
524
    def IsInstalled(self, package_name, interaction):
 
525
        """Return True if the given package is installed.
 
526
 
 
527
        Keyword arguments:
 
528
        package-name -- the name of the package, e.g. xterm
 
529
        interaction -- the interaction mode, e.g. timeout=10
 
530
        """
 
531
        log.info("IsInstalled() was called: %s, %s", package_name, interaction)
 
532
        return self._is_installed(package_name, interaction)
 
533
 
 
534
    @track_usage
 
535
    def _is_installed(self, package_name, interaction):
 
536
        self._init_cache()
 
537
        try:
 
538
            return self._cache[package_name].is_installed
 
539
        except KeyError:
 
540
            raise errors.QueryNoPackagesFound
 
541
 
 
542
    @dbus_deferred_method(PACKAGEKIT_QUERY_DBUS_INTERFACE,
 
543
                          in_signature="ss", out_signature="bs",
 
544
                          utf8_strings=True)
 
545
    def SearchFile(self, file_name, interaction):
 
546
        """Return the installation state and name of the package which
 
547
        contains the given file.
 
548
 
 
549
        Keyword arguments:
 
550
        file_name -- the to be searched file name
 
551
        interaction -- the interaction mode, e.g. timeout=10
 
552
        """
 
553
        log.info("SearchFile() was called: %s, %s", file_name, interaction)
 
554
        return self._search_file(file_name, interaction)
 
555
 
 
556
    @track_usage
 
557
    def _search_file(self, file_name, interaction):
 
558
        self._init_cache()
 
559
        # Search for installed files
 
560
        if file_name.startswith("/"):
 
561
            pattern = "^%s$" % file_name.replace("/", "\/")
 
562
        else:
 
563
            pattern = ".*\/%s$" % file_name
 
564
        re_file = re.compile(pattern)
 
565
        for pkg in self._cache:
 
566
            # FIXME: Fix python-apt
 
567
            try:
 
568
                for installed_file in pkg.installed_files:
 
569
                    if re_file.match(installed_file):
 
570
                        #FIXME: What about a file in multiple conflicting
 
571
                        # packages?
 
572
                        return pkg.is_installed, pkg.name
 
573
            except:
 
574
                pass
 
575
        # Optionally make use of apt-file's Contents cache to search for not
 
576
        # installed files. But still search for installed files additionally
 
577
        # to make sure that we provide up-to-date results
 
578
        if os.path.exists("/usr/bin/apt-file"):
 
579
            #FIXME: Make use of rapt-file on Debian if the network is available
 
580
            #FIXME: Show a warning to the user if the apt-file cache is several
 
581
            #       weeks old
 
582
            log.debug("Using apt-file")
 
583
            if file_name.startswith("/"):
 
584
                pattern = "^%s$" % file_name[1:].replace("/", "\/")
 
585
            else:
 
586
                pattern = "\/%s$" % file_name
 
587
            cmd = ["/usr/bin/apt-file", "--regexp", "--non-interactive",
 
588
                   "--package-only", "find", pattern]
 
589
            log.debug("Calling: %s" % cmd)
 
590
            apt_file = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
591
                                        stderr=subprocess.PIPE)
 
592
            stdout, stderr = apt_file.communicate()
 
593
            if apt_file.returncode == 0:
 
594
                #FIXME: Actually we should check if the file is part of the
 
595
                #       candidate, e.g. if unstable and experimental are
 
596
                #       enabled and a file would only be part of the
 
597
                #       experimental version
 
598
                #FIXME: Handle multiple packages
 
599
                for pkg_name in stdout.split():
 
600
                    try:
 
601
                        pkg = self._cache[pkg_name]
 
602
                    except:
 
603
                        continue
 
604
                    return pkg.isInstalled, pkg.name
 
605
            else:
 
606
                raise errors.QueryInternatlError("apt-file call failed")
 
607
            return False, ""
 
608
 
 
609
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
610
                          in_signature="uass", out_signature="",
 
611
                          sender_keyword="sender",
 
612
                          utf8_strings=True)
 
613
    def InstallPackageFiles(self, xid, files, interaction, sender):
 
614
        """Install local package files.
 
615
 
 
616
        Keyword arguments:
 
617
        xid -- the window id of the requesting application
 
618
        files -- the list of package file paths
 
619
        interaction -- the interaction mode: which ui elements should be
 
620
                       shown e.g. hide-finished or hide-confirm-search
 
621
        """
 
622
        log.info("InstallPackageFiles was called: %s, %s, %s", xid, files,
 
623
                 interaction)
 
624
        return self._install_package_files(xid, files, interaction, sender)
 
625
 
 
626
    @track_usage
 
627
    @inline_callbacks
 
628
    def _install_package_files(self, xid, files, interaction, sender):
 
629
        parent = None # should get from XID, but removed from Gdk 3
 
630
        header = ""
 
631
        if len(files) != 1:
 
632
            header = _("Failed to install multiple package files")
 
633
            message = _("Installing more than one package file at the same "
 
634
                        "time isn't supported. Please install one after the "
 
635
                        "other.")
 
636
        elif not files[0][0] == "/":
 
637
            header = _("Relative path to package file")
 
638
            message = _("You have to specify the absolute path to the "
 
639
                        "package file.")
 
640
        elif not files[0].endswith(".deb"):
 
641
            header = _("Unsupported package format")
 
642
            message = _("Only Debian packages are supported (*.deb)")
 
643
        try:
 
644
            debfile = apt.debfile.DebPackage(files[0])
 
645
            desc = debfile["Description"].split("\n", 1)[0]
 
646
        except:
 
647
            header = _("Unsupported package format")
 
648
            message = _("Only Debian packages are supported (*.deb)")
 
649
        if header:
 
650
            dia = ErrorDialog(header, message)
 
651
            dia.run()
 
652
            dia.hide()
 
653
            raise errors.ModifyFailed("%s - %s" % (header, message))
 
654
        title = gettext.ngettext("Install package file?",
 
655
                                 "Install package files?",
 
656
                                 len(files))
 
657
        sender_name = yield self._get_sender_name(sender)
 
658
        if sender_name:
 
659
            message = gettext.ngettext("%s requests to install the following "
 
660
                                       "package file.",
 
661
                                       "%s requests to install the following "
 
662
                                       "package files.",
 
663
                                       len(files)) % sender_name
 
664
            message += "\n\n"
 
665
        else:
 
666
            message = ""
 
667
        message += _("Software from foreign sources could be malicious, "
 
668
                     "could contain security risks and or even break your "
 
669
                     "system."
 
670
                     "Install packages from your distribution's "
 
671
                     "repositories as far as possible.")
 
672
        confirm = ConfirmInstallDialog(title, message, parent=parent)
 
673
        confirm.add_confirm(files[0], desc)
 
674
        if confirm.run() == Gtk.ResponseType.CANCEL:
 
675
            raise errors.ModifyCancelled
 
676
        yield self.backend.install_package_files(xid,
 
677
                                                 confirm.get_selected_pkgs(),
 
678
                                                 interaction)
 
679
 
 
680
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
681
                          in_signature="uass", out_signature="",
 
682
                          sender_keyword="sender",
 
683
                          utf8_strings=True)
 
684
    def InstallProvideFiles(self, xid, files, interaction, sender):
 
685
        """Install packages which provide the given files.
 
686
 
 
687
        Keyword arguments:
 
688
        xid -- the window id of the requesting application
 
689
        files -- the list of package file paths
 
690
        interaction -- the interaction mode: which ui elements should be
 
691
                       shown e.g. hide-finished or hide-confirm-search
 
692
        """
 
693
        log.info("InstallProvideFiles() was called: %s, %s, %s", xid, files,
 
694
                 interaction)
 
695
        return self._install_provide_files(xid, files, interaction, sender)
 
696
 
 
697
    @track_usage
 
698
    @inline_callbacks
 
699
    def _install_provide_files(self, xid, files, interaction, sender):
 
700
        #FIXME: Reuse apt-file from the search_file method
 
701
        try:
 
702
            from CommandNotFound import CommandNotFound
 
703
        except ImportError:
 
704
            log.warning("command-not-found not supported")
 
705
        else:
 
706
            cnf = CommandNotFound("/usr/share/command-not-found")
 
707
            to_install = list()
 
708
            for executable in [os.path.basename(f) for f in files]:
 
709
                list_of_packages_and_components =  cnf.getPackages(executable)
 
710
                if list_of_packages_and_components:
 
711
                    (package, component) = list_of_packages_and_components[0]
 
712
                    to_install.append(package)
 
713
            if to_install:
 
714
                yield self._install_package_names(xid, to_install, interaction,
 
715
                                                  sender)
 
716
                raise StopIteration
 
717
            # FIXME: show a message here that the binaries were not
 
718
            #        found instead of falling through to the misleading
 
719
            #        other error message
 
720
 
 
721
        # FIXME: use a different error message
 
722
        header = _("Installing packages by files isn't supported")
 
723
        message = _("This method hasn't yet been implemented.")
 
724
        #FIXME: should provide some information about how to find apps
 
725
        dia = ErrorDialog(header, message)
 
726
        dia.run()
 
727
        dia.hide()
 
728
        #raise errors.ModifyInternalError(message)
 
729
 
 
730
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
731
                          in_signature="uass", out_signature="",
 
732
                          sender_keyword="sender",
 
733
                          utf8_strings=True)
 
734
    def InstallCatalogs(self, xid, files, interaction, sender):
 
735
        """Install packages which provide the given files.
 
736
 
 
737
        Keyword arguments:
 
738
        xid -- the window id of the requesting application
 
739
        files -- the list of catalog file paths
 
740
        interaction -- the interaction mode: which ui elements should be
 
741
                       shown e.g. hide-finished or hide-confirm-search
 
742
        """
 
743
        log.info("InstallCatalogs() was called: %s, %s, %s", xid, files,
 
744
                 interaction)
 
745
        return self._install_catalogs(xid, files, interaction, sender)
 
746
 
 
747
    @track_usage
 
748
    @inline_callbacks
 
749
    def _install_catalogs(self, xid, files, interaction, sender):
 
750
        parent = None # should get from XID, but removed from Gdk 3
 
751
        self._init_cache()
 
752
        arch = os.popen("/usr/bin/dpkg --print-architecture").read().strip()
 
753
        distro, code, release = os.popen("/usr/bin/lsb_release "
 
754
                                         "--id --code --release "
 
755
                                         "--short").read().split()
 
756
        regex = "^(?P<action>[a-z]+)(\(%s(;((%s)|(%s))(;%s)?)?\))?$" % \
 
757
                (distro, code, release, arch)
 
758
        re_action = re.compile(regex, flags=re.IGNORECASE)
 
759
        pkgs = set()
 
760
        missing = set()
 
761
        for catalog_file in files:
 
762
            if not os.path.exists(catalog_file):
 
763
                header = _("Catalog could not be read")
 
764
                #TRANSLATORS: %s is a file path
 
765
                message = _("The catalog file '%s' doesn't "
 
766
                            "exist.") % catalog_file
 
767
                self._show_error(header, message)
 
768
                raise errors.ModifyFailed(message)
 
769
            catalog = ConfigParser()
 
770
            try:
 
771
                catalog.read(catalog_file)
 
772
            except:
 
773
                header = _("Catalog could not be read")
 
774
                #TRANSLATORS: %s is a file path
 
775
                message = _("The catalog file '%s' could not be opened  "
 
776
                            "and read.") % catalog_file
 
777
                self._show_error(header, message)
 
778
                raise errors.ModifyFailed(message)
 
779
            if not catalog.sections() == ["PackageKit Catalog"]:
 
780
                header = _("Catalog could not be read")
 
781
                #TRANSLATORS: %s is a file path
 
782
                message = _("The file '%s' isn't a valid software catalog. "
 
783
                            "Please redownload or contact the "
 
784
                            "provider.") % catalog_file
 
785
                self._show_error(header, message)
 
786
                raise errors.ModifyFailed(message)
 
787
            for key, value in catalog.items("PackageKit Catalog"):
 
788
                match = re_action.match(key)
 
789
                if match:
 
790
                    if match.group("action") != "installpackages":
 
791
                        header = _("Catalog is not supported")
 
792
                        message = _("The method '%s' which is used to specify "
 
793
                                    "packages isn't supported.\n"
 
794
                                    "Please contact the provider of the "
 
795
                                    "catalog about this "
 
796
                                    "issue.") % match.group("action")
 
797
                        self._show_error(header, message)
 
798
                        raise errors.ModifyFailed(message)
 
799
                    for pkg_name in value.split(";"):
 
800
                        if pkg_name in self._cache:
 
801
                            pkg = self._cache[pkg_name]
 
802
                            if not pkg.is_installed and not pkg.candidate:
 
803
                                missing.add(pkg_name)
 
804
                            else:
 
805
                                pkgs.add(self._cache[pkg_name])
 
806
                        else:
 
807
                            missing.add(pkg_name)
 
808
                else:
 
809
                    log.debug("Ignoring catalog instruction: %s" % key)
 
810
        # Error out if packages are not available
 
811
        if missing:
 
812
            header = gettext.ngettext("A required package is not installable",
 
813
                                      "Required packages are not installable",
 
814
                                      len(missing))
 
815
            pkgs = " ".join(missing)
 
816
            #TRANSLATORS: %s is the name of the missing packages
 
817
            msg = gettext.ngettext("The catalog requires the installation of "
 
818
                                   "the package %s which is not available.",
 
819
                                   "The catalog requires the installation of "
 
820
                                   "the following packages which are not "
 
821
                                   "available: %s", len(missing)) % pkgs
 
822
            self._show_error(header, msg)
 
823
            raise errors.ModifyNoPackagesFound(msg)
 
824
        parent = None # should get from XID, but removed from Gdk 3
 
825
        # Create nice messages
 
826
        sender_name = yield self._get_sender_name(sender)
 
827
        title = gettext.ngettext("Install the following software package?",
 
828
                                 "Install the following software packages?",
 
829
                                 len(pkgs))
 
830
        if sender_name:
 
831
            #TRANSLATORS: %s is the name of the application which requested
 
832
            #             the installation
 
833
            message = gettext.ngettext("%s requires the installation of an "
 
834
                                       "additional software package.",
 
835
                                       "%s requires the installation of "
 
836
                                       "additional software packages.",
 
837
                                       len(pkgs)) % sender_name
 
838
        else:
 
839
            #TRANSLATORS: %s is an absolute file path, e.g. /usr/bin/xterm
 
840
            message = gettext.ngettext("The package catalog %s requests to "
 
841
                                       "install the following software.",
 
842
            #TRANSLATORS: %s is a list of absoulte file paths
 
843
                                       "The following catalogs request to "
 
844
                                       "install software: %s",
 
845
            #TRANSLATORS: %s is an absolute file path, e.g. /usr/bin/xterm
 
846
                                       len(files)) % " ".join("'%s'")
 
847
        confirm = ConfirmInstallDialog(title, message, pkgs, parent)
 
848
        res = confirm.run()
 
849
        if res == Gtk.ResponseType.OK:
 
850
            yield self.backend.install_packages(xid,
 
851
                                                confirm.get_selected_pkgs(),
 
852
                                                interaction)
 
853
        else:
 
854
            raise errors.ModifyCancelled
 
855
 
 
856
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
857
                          in_signature="uass", out_signature="",
 
858
                          sender_keyword="sender",
 
859
                          utf8_strings=True)
 
860
    def InstallPackageNames(self, xid, packages, interaction, sender):
 
861
        """Install packages from a preconfigured software source.
 
862
 
 
863
        Keyword arguments:
 
864
        xid -- the window id of the requesting application
 
865
        packages -- the list of package names
 
866
        interaction -- the interaction mode: which ui elements should be
 
867
                       shown e.g. hide-finished or hide-confirm-search
 
868
        """
 
869
        log.info("InstallPackageNames() was called: %s, %s, %s", xid, packages,
 
870
                 interaction)
 
871
        return self._install_package_names(xid, packages, interaction, sender)
 
872
 
 
873
    @track_usage
 
874
    @inline_callbacks
 
875
    def _install_package_names(self, xid, packages, interaction, sender):
 
876
        parent = None # should get from XID, but removed from Gdk 3
 
877
        title = gettext.ngettext("Install additional software package?",
 
878
                                 "Install additional software packages?",
 
879
                                 len(packages))
 
880
        sender_name = yield self._get_sender_name(sender)
 
881
        if sender_name:
 
882
            message = gettext.ngettext("%s requests to install the following "
 
883
                                       "software package to provide additional "
 
884
                                       "features.",
 
885
                                       "%s requests to install the following "
 
886
                                       "software packages to provide "
 
887
                                       "additional features.",
 
888
                                       len(packages)) % sender_name
 
889
        else:
 
890
            message = gettext.ngettext("The following software package is "
 
891
                                       "required to provide additional "
 
892
                                       "features.",
 
893
                                       "The following software packages are "
 
894
                                       "required to provide additional "
 
895
                                       "features.",
 
896
                                       len(packages))
 
897
        self._init_cache()
 
898
        confirm = ConfirmInstallDialog(title, message, parent=parent)
 
899
        failed_packages = []
 
900
        for pkg_name in packages:
 
901
            try:
 
902
                pkg = self._cache[pkg_name]
 
903
            except KeyError:
 
904
                failed_packages.append(pkg_name)
 
905
                continue
 
906
            if not pkg.candidate:
 
907
                failed_packages.append(pkg_name)
 
908
                continue
 
909
            if not pkg.is_installed:
 
910
                confirm.add_confirm_package(pkg)
 
911
 
 
912
        if failed_packages:
 
913
            header = gettext.ngettext("Could not find requested package",
 
914
                                      "Could not find requested packages",
 
915
                                      len(failed_packages))
 
916
            if sender_name:
 
917
                message = gettext.ngettext("%s requests to install the "
 
918
                                           "following software package to "
 
919
                                           "provide "
 
920
                                           "additional features:",
 
921
                                           "%s requests to install the "
 
922
                                           "following "
 
923
                                           "software packages to provide "
 
924
                                           "additional features:",
 
925
                                           len(packages)) % sender_name
 
926
            else:
 
927
                message = gettext.ngettext("The following software package "
 
928
                                           "is required to provide "
 
929
                                           "additional features but cannot "
 
930
                                           "be installed:",
 
931
                                           "The following software "
 
932
                                           "packages are required to "
 
933
                                           "provide "
 
934
                                           "additional features but cannot "
 
935
                                           "be installed:",
 
936
                                           len(failed_packages))
 
937
            message += self._get_bullet_list(failed_packages)
 
938
            self._show_error(header, message)
 
939
            raise errors.ModifyNoPackagesFound(header)
 
940
        # If all packages are already installed we return silently
 
941
        if len(confirm.pkg_store) > 0:
 
942
            if confirm.run() == Gtk.ResponseType.CANCEL:
 
943
                raise errors.ModifyCancelled
 
944
            yield self.backend.install_packages(xid,
 
945
                                                confirm.get_selected_pkgs(),
 
946
                                                interaction)
 
947
 
 
948
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
949
                          in_signature="uass", out_signature="",
 
950
                          sender_keyword="sender",
 
951
                          utf8_strings=True)
 
952
    def InstallMimeTypes(self, xid, mime_types, interaction, sender):
 
953
        """Install mime type handler from a preconfigured software source.
 
954
 
 
955
        Keyword arguments:
 
956
        xid -- the window id of the requesting application
 
957
        mime_types -- list of mime types whose handlers should be installed
 
958
        interaction -- the interaction mode: which ui elements should be
 
959
                       shown e.g. hide-finished or hide-confirm-search
 
960
        """
 
961
        log.info("InstallMimeTypes() was called: %s, %s, %s", xid, mime_types,
 
962
                 interaction)
 
963
        return self._install_mime_types(xid, mime_types, interaction, sender)
 
964
 
 
965
    @track_usage
 
966
    @inline_callbacks
 
967
    def _install_mime_types(self, xid, mime_types_list, interaction, sender):
 
968
        parent = None # should get from XID, but removed from Gdk 3
 
969
        if not os.path.exists(utils.APP_INSTALL_DATA):
 
970
            #FIXME: should provide some information about how to find apps
 
971
            header = _("Installing mime type handlers isn't supported")
 
972
            message = _("To search and install software which can open "
 
973
                        "certain file types you have to install "
 
974
                        "app-install-data.")
 
975
            dia = ErrorDialog(header, message)
 
976
            dia.run()
 
977
            dia.hide()
 
978
            raise errors.ModifyInternalError(message)
 
979
        sender_name = yield self._get_sender_name(sender)
 
980
        title = _("Searching for suitable software to open files")
 
981
        mime_types = set(mime_types_list)
 
982
        mime_names = [Gio.content_type_get_description(mt) for mt in mime_types]
 
983
        if sender_name:
 
984
            #TRANSLATORS: %s is an application
 
985
            message = gettext.ngettext("%s requires to install software to "
 
986
                                       "open files of the following file type:",
 
987
                                       "%s requires to install software to "
 
988
                                       "open files of the following file "
 
989
                                       "types:",
 
990
                                       len(mime_types)) % sender_name
 
991
        else:
 
992
            message = gettext.ngettext("Software to open files of the "
 
993
                                       "following file type is required "
 
994
                                       "but is not installed:",
 
995
                                       "Software to open files of the "
 
996
                                       "following file types is required "
 
997
                                       "but is not installed:",
 
998
                                       len(mime_types))
 
999
        mime_types_desc = [Gio.content_type_get_description(mime_type) \
 
1000
                           for mime_type in mime_types]
 
1001
        message += self._get_bullet_list(mime_types_desc)
 
1002
        progress = ProgressDialog(title, message, parent)
 
1003
        progress.show_all()
 
1004
        while Gtk.events_pending():
 
1005
            Gtk.main_iteration()
 
1006
        # Search the app-install-data desktop files for mime type handlers
 
1007
        pkgs = []
 
1008
        partial_providers = False
 
1009
        self._init_cache(progress)
 
1010
        package_map = {}
 
1011
        unsatisfied = mime_types.copy()
 
1012
        mixed = False
 
1013
        for count, path in enumerate(os.listdir(utils.APP_INSTALL_DATA)):
 
1014
            if path[0] == "." or not path.endswith(".desktop"):
 
1015
                continue
 
1016
            if not count % 20:
 
1017
                while Gtk.events_pending():
 
1018
                    Gtk.main_iteration()
 
1019
                progress.progress.pulse()
 
1020
                if progress.cancelled:
 
1021
                    progress.hide()
 
1022
                    progress.destroy()
 
1023
                    raise errors.ModifyCancelled
 
1024
            desktop_entry = DesktopEntry(os.path.join(utils.APP_INSTALL_DATA,
 
1025
                                                      path))
 
1026
            pkg_name = desktop_entry.get("X-AppInstall-Package")
 
1027
            try:
 
1028
                if self._cache[pkg_name].is_installed:
 
1029
                    continue
 
1030
            except KeyError:
 
1031
                continue
 
1032
            supported_mime_types = set(desktop_entry.getMimeTypes())
 
1033
            for mime_type in supported_mime_types:
 
1034
                if not mime_type in mime_types:
 
1035
                    continue
 
1036
                package_map.setdefault(pkg_name, [[], set(), 0])
 
1037
                #FIXME: Don't add desktop entries twice
 
1038
                package_map[pkg_name][0].append(desktop_entry)
 
1039
                desc = Gio.content_type_get_description(mime_type)
 
1040
                package_map[pkg_name][1].add(desc)
 
1041
                popcon = desktop_entry.get("X-AppInstall-Popcon",
 
1042
                                           type="integer")
 
1043
                if package_map[pkg_name][2] < popcon:
 
1044
                    package_map[pkg_name][2] = popcon
 
1045
                unsatisfied.discard(mime_type)
 
1046
                if not mixed and \
 
1047
                   not supported_mime_types.issuperset(mime_types):
 
1048
                    mixed = True
 
1049
        progress.hide()
 
1050
        progress.destroy()
 
1051
        if mixed or unsatisfied:
 
1052
            details = _("Supported file types")
 
1053
        else:
 
1054
            details = None
 
1055
        title = _("Install software to open files?")
 
1056
        if unsatisfied:
 
1057
            unsatisfied_desc = [Gio.content_type_get_description(mime_type) \
 
1058
                                for mime_type in unsatisfied]
 
1059
            unsatisfied_str = self._get_bullet_list(unsatisfied_desc)
 
1060
            message += "\n\n"
 
1061
            #TRANSLATORS: %s is either a single file type or a bullet list of
 
1062
            #             file types
 
1063
            message += gettext.ngettext("%s is not supported.",
 
1064
                                        "Unsupported file types: %s",
 
1065
                                        len(unsatisfied)) % unsatisfied_str
 
1066
        confirm = ConfirmInstallDialog(title, message, parent=parent,
 
1067
                                       details=details,
 
1068
                                       selectable=len(package_map) > 1,
 
1069
                                       package_type=_("Application"))
 
1070
        for pkg, (entries, provides, score) in package_map.iteritems():
 
1071
            if len(provides) == len(mime_types):
 
1072
                details = _("All")
 
1073
            else:
 
1074
                #TRANSLATORS: Separator for a list of plugins
 
1075
                details = _(",\n").join(provides)
 
1076
            confirm.add_confirm_package(self._cache[pkg], len(package_map) == 1,
 
1077
                                        details, score)
 
1078
        res = confirm.run()
 
1079
        if res == Gtk.ResponseType.OK:
 
1080
            yield self.backend.install_packages(xid,
 
1081
                                                confirm.get_selected_pkgs(),
 
1082
                                                interaction)
 
1083
        else:
 
1084
            raise errors.ModifyCancelled
 
1085
 
 
1086
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
1087
                          in_signature="uass", out_signature="",
 
1088
                          utf8_strings=True)
 
1089
    def InstallPrinterDrivers(self, xid, resources, interaction):
 
1090
        """Install printer drivers from from a preconfigured software source.
 
1091
 
 
1092
        Keyword arguments:
 
1093
        xid -- the window id of the requesting application
 
1094
        resources -- a list of printer model descriptors in IEEE 1284
 
1095
               Device ID format e.g. "MFG:Hewlett-Packard" "MDL:HP LaserJet6MP"
 
1096
        interaction -- the interaction mode: which ui elements should be
 
1097
               shown e.g. hide-finished or hide-confirm-search
 
1098
        """
 
1099
        log.info("InstallPrinterDrivers() was called: %s, %s, %s", xid,
 
1100
                 resources, interaction)
 
1101
        return self._install_printer_drivers(xid, resources, interaction)
 
1102
 
 
1103
    @track_usage
 
1104
    def _install_printer_drivers(self, xid, resources, interaction):
 
1105
        return
 
1106
        header = _("Installing printer drivers on request isn't supported")
 
1107
        message = _("Currently autodetection and installation of "
 
1108
                    "missing printer drivers is not supported.")
 
1109
        #FIXME: should provide some information about how to get printers
 
1110
        dia = ErrorDialog(header, message)
 
1111
        dia.run()
 
1112
        dia.hide()
 
1113
        raise errors.ModifyInternalError(message)
 
1114
 
 
1115
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
1116
                          in_signature="uass", out_signature="",
 
1117
                          utf8_strings=True)
 
1118
    def InstallFontconfigResources(self, xid, resources, interaction,):
 
1119
        """Install fontconfig resources from from a
 
1120
        preconfigured software source.
 
1121
 
 
1122
        Keyword arguments:
 
1123
        xid -- the window id of the requesting application
 
1124
        resources -- list of fontconfig resources (usually fonts)
 
1125
        interaction -- the interaction mode: which ui elements should be
 
1126
                       shown e.g. hide-finished or hide-confirm-search
 
1127
        """
 
1128
        log.info("InstallFontconfigResources() was called: %s, %s, %s", xid,
 
1129
                 resources, interaction)
 
1130
        return self._install_fontconfig_resources(xid, resources, interaction)
 
1131
 
 
1132
    @track_usage
 
1133
    def _install_fontconfig_resources(self, xid, resources, interaction):
 
1134
        header = _("Installing fonts on request isn't supported")
 
1135
        message = _("Currently autodetection and installation of "
 
1136
                    "missing fonts is not supported.")
 
1137
        #FIXME: should provide some information about how to get fonts
 
1138
        dia = ErrorDialog(header, message)
 
1139
        dia.run()
 
1140
        dia.hide()
 
1141
        raise errors.ModifyInternalError(message)
 
1142
 
 
1143
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
1144
                          in_signature="uass", out_signature="",
 
1145
                          sender_keyword="sender",
 
1146
                          utf8_strings=True)
 
1147
    def InstallGStreamerResources(self, xid, resources, interaction, sender):
 
1148
        """Install GStreamer resources from from a preconfigured
 
1149
        software source.
 
1150
 
 
1151
        Keyword arguments:
 
1152
        xid -- the window id of the requesting application
 
1153
        resources -- list of GStreamer structures, e.g.
 
1154
                     "gstreamer0.10(decoder-video/x-wmv)(wmvversion=3)"
 
1155
        interaction -- the interaction mode: which ui elements should be
 
1156
                       shown e.g. hide-finished or hide-confirm-search
 
1157
        """
 
1158
        log.info("InstallGstreamerResources() was called: %s, %s, %s", xid,
 
1159
                 resources, interaction)
 
1160
        Gst.init(None)
 
1161
        return self._install_gstreamer_resources(xid, resources, interaction,
 
1162
                                                 sender)
 
1163
 
 
1164
    @track_usage
 
1165
    @inline_callbacks
 
1166
    def _install_gstreamer_resources(self, xid, resources, interaction, sender):
 
1167
        def parse_gstreamer_structure(resource):
 
1168
            # E.g. "MS Video|gstreamer0.10(decoder-video/x-wmv)(wmvversion=3)"
 
1169
            match = re.match("^(?P<name>.*)\|gstreamer(?P<version>[0-9\.]+)"
 
1170
                             "\((?P<kind>.+?)-(?P<structname>.+?)\)"
 
1171
                             "(?P<fields>\(.+\))?$", resource)
 
1172
            caps = None
 
1173
            element = None
 
1174
            if not match:
 
1175
                title = _("Invalid search term")
 
1176
                message = _("The following term doesn't describe a "
 
1177
                           "GStreamer resource: %s") % resource
 
1178
                self._show_error(title, message)
 
1179
                raise errors.ModifyFailed(message)
 
1180
            if match.group("kind") in ["encoder", "decoder"]:
 
1181
                caps_str = "%s" % match.group("structname")
 
1182
                if match.group("fields"):
 
1183
                    for field in re.findall("\((.+?=(\(.+?\))?.+?)\)",
 
1184
                                            match.group("fields")):
 
1185
                        caps_str += ", %s" % field[0]
 
1186
                # gst.Caps.__init__ cannot handle unicode instances
 
1187
                caps = Gst.Caps.from_string(str(caps_str))
 
1188
            else:
 
1189
                element = match.group("structname")
 
1190
            record = GSTREAMER_RECORD_MAP[match.group("kind")]
 
1191
            return GStreamerStructure(match.group("name"),
 
1192
                                      match.group("version"),
 
1193
                                      match.group("kind"),
 
1194
                                      record, caps, element)
 
1195
 
 
1196
        structures = [parse_gstreamer_structure(res) for res in resources]
 
1197
        kinds = set([struct.kind for struct in structures])
 
1198
        # Show a progress dialog
 
1199
        parent = None # should get from XID, but removed from Gdk 3
 
1200
        sender_name = yield self._get_sender_name(sender)
 
1201
        title = _("Searching for multimedia plugins")
 
1202
        # Get a nice dialog message
 
1203
        if kinds.issubset(set(["encoder"])):
 
1204
            if sender_name:
 
1205
                #TRANSLATORS: %s is the application requesting the plugins
 
1206
                message = gettext.ngettext("%s requires to install plugins to "
 
1207
                                           "create media files of the "
 
1208
                                           "following type:",
 
1209
                                           "%s requires to install plugins to "
 
1210
                                           "create files of the following "
 
1211
                                           "types:",
 
1212
                                           len(structures)) % sender_name
 
1213
            else:
 
1214
                message = gettext.ngettext("The plugin to create media files "
 
1215
                                           "of the following type is not "
 
1216
                                           "installed:",
 
1217
                                           "The plugin to create media files "
 
1218
                                           "of the following types is not "
 
1219
                                           "installed:",
 
1220
                                           len(structures))
 
1221
        elif kinds.issubset(set(["decoder"])):
 
1222
            if sender_name:
 
1223
                #TRANSLATORS: %s is the application requesting the plugins
 
1224
                message = gettext.ngettext("%s requires to install plugins to "
 
1225
                                           "play media files of the "
 
1226
                                           "following type:",
 
1227
                                           "%s requires to install plugins to "
 
1228
                                           "play files of the following "
 
1229
                                           "types:",
 
1230
                                           len(structures)) % sender_name
 
1231
            else:
 
1232
                message = gettext.ngettext("The plugin to play media files "
 
1233
                                           "of the following type is not "
 
1234
                                           "installed:",
 
1235
                                           "The plugin to play media files "
 
1236
                                           "of the following types is not "
 
1237
                                           "installed:",
 
1238
                                           len(structures))
 
1239
        elif kinds.issubset(set(["encoder", "decoder"])):
 
1240
            if sender_name:
 
1241
                #TRANSLATORS: %s is the application requesting the plugins
 
1242
                message = gettext.ngettext("%s requires to install plugins to "
 
1243
                                           "create and play media files of the "
 
1244
                                           "following type:",
 
1245
                                           "%s requires to install plugins to "
 
1246
                                           "create and play media files of the "
 
1247
                                           "following types:",
 
1248
                                           len(structures)) % sender_name
 
1249
            else:
 
1250
                message = gettext.ngettext("The plugins to create and play "
 
1251
                                           "media files of the following type "
 
1252
                                           "are not installed:",
 
1253
                                           "The plugins to create and play "
 
1254
                                           "media files of the following types "
 
1255
                                           "are not installed:",
 
1256
                                           len(structures))
 
1257
        else:
 
1258
            if sender_name:
 
1259
                #TRANSLATORS: %s is the application requesting the plugins
 
1260
                message = gettext.ngettext("%s requires to install plugins to "
 
1261
                                           "support the following "
 
1262
                                           "multimedia feature:",
 
1263
                                           "%s requires to install plugins to "
 
1264
                                           "support the following multimedia "
 
1265
                                           "features:",
 
1266
                                           len(structures)) % sender_name
 
1267
            else:
 
1268
                message = gettext.ngettext("Extra plugins to provide the "
 
1269
                                           "following multimedia feature are "
 
1270
                                           "not installed:",
 
1271
                                           "Extra plugins to provide the "
 
1272
                                           "following multimedia features are "
 
1273
                                           "not installed:",
 
1274
                                           len(structures))
 
1275
        message += self._get_bullet_list([struct.name or struct.record \
 
1276
                                         for struct in structures])
 
1277
        progress = ProgressDialog(title, message, parent)
 
1278
        progress.show_all()
 
1279
        while Gtk.events_pending():
 
1280
            Gtk.main_iteration()
 
1281
        # Search the package cache for packages providing the plugins
 
1282
        pkgs = []
 
1283
        partial_providers = False
 
1284
        self._init_cache(progress)
 
1285
 
 
1286
        # Get the architectures with an installed gstreamer library
 
1287
        # Unfortunately the architecture isn't part of the request. So we
 
1288
        # have to detect for which architectuers gstreamer has been installed
 
1289
        # on the system, to avoid showing codecs for not used  but enabeled
 
1290
        # architecures, see LP #899001
 
1291
        architectures = apt_pkg.get_architectures()
 
1292
        supported_archs = set()
 
1293
        if len(architectures) > 1:
 
1294
            for gst_version in set([struct.version for struct in structures]):
 
1295
                for arch in architectures:
 
1296
                    try:
 
1297
                        pkg = self._cache["libgstreamer%s-0:%s" % (gst_version,
 
1298
                                                                   arch)]
 
1299
                    except KeyError:
 
1300
                        continue
 
1301
                    if pkg.is_installed:
 
1302
                        supported_archs.add(arch)
 
1303
        else:
 
1304
            supported_archs = architectures
 
1305
 
 
1306
        for count, pkg in enumerate(self._cache):
 
1307
            if not count % 100:
 
1308
                while Gtk.events_pending():
 
1309
                    Gtk.main_iteration()
 
1310
                progress.progress.pulse()
 
1311
                if progress.cancelled:
 
1312
                    progress.hide()
 
1313
                    progress.destroy()
 
1314
                    raise errors.ModifyCancelled
 
1315
            if (pkg.is_installed or
 
1316
                not pkg.candidate or 
 
1317
                not "Gstreamer-Version" in pkg.candidate.record or
 
1318
                not pkg.candidate.architecture in supported_archs):
 
1319
                continue
 
1320
            # Check if the package could not be free in usage or distribution
 
1321
            # Allow to prefer special packages
 
1322
            try:
 
1323
                pkg_name = pkg.name.split(":")[0]
 
1324
                if pkg_name in GSTREAMER_010_SCORING.keys():
 
1325
                    score = GSTREAMER_010_SCORING[pkg_name]
 
1326
                elif pkg_name in GSTREAMER_10_SCORING.keys():
 
1327
                    score = GSTREAMER_10_SCORING[pkg_name]
 
1328
                else:
 
1329
                    raise KeyError
 
1330
            except KeyError:
 
1331
                score = 0
 
1332
            if _is_package_restricted(pkg):
 
1333
                score -= 10
 
1334
            provides = []
 
1335
            for struct in structures:
 
1336
                if pkg.candidate.record["Gstreamer-Version"] != struct.version:
 
1337
                    continue
 
1338
                if struct.caps:
 
1339
                    try:
 
1340
                        pkg_caps = Gst.Caps.from_string(pkg.candidate.record[struct.record])
 
1341
                    except KeyError:
 
1342
                        continue
 
1343
                    if not pkg_caps.intersect(struct.caps):
 
1344
                        continue
 
1345
                else:
 
1346
                    try:
 
1347
                        elements = pkg.candidate.record[struct.record]
 
1348
                    except KeyError:
 
1349
                        continue
 
1350
                    if not struct.element in elements:
 
1351
                        continue
 
1352
                provides.append(struct.name)
 
1353
                struct.satisfied = True
 
1354
                if score > struct.best_score:
 
1355
                    struct.best_provider = pkg.name.split(":")[0]
 
1356
                    struct.best_score = score
 
1357
            if provides:
 
1358
                provides_all = len(structures) == len(provides)
 
1359
                if not provides_all:
 
1360
                    partial_providers = True
 
1361
                pkgs.append((pkg, provides_all, provides, score))
 
1362
        progress.hide()
 
1363
        progress.destroy()
 
1364
        # Error out if there isn't any package available
 
1365
        if not pkgs:
 
1366
            #FIXME: Add more info and possible solutions for the user
 
1367
            dia = ErrorDialog(_("Required plugin could not be found"),
 
1368
                              message)
 
1369
            dia.run()
 
1370
            dia.hide()
 
1371
            raise errors.ModifyNoPackagesFound
 
1372
        # Show a confirmation dialog
 
1373
        title = gettext.ngettext("Install extra multimedia plugin?",
 
1374
                                 "Install extra multimedia plugins?",
 
1375
                                 len(pkgs))
 
1376
        unsatisfied = [stru.name for stru in structures if not stru.satisfied]
 
1377
        if unsatisfied:
 
1378
            message += "\n\n"
 
1379
            message += gettext.ngettext("The following plugin is not "
 
1380
                                        "available:",
 
1381
                                        "The following plugins are not "
 
1382
                                        "available:",
 
1383
                                        len(unsatisfied))
 
1384
            message += " "
 
1385
            #TRANSLATORS: list separator
 
1386
            message += self._get_bullet_list(unsatisfied)
 
1387
 
 
1388
        # We only show the details if there are packages which would only
 
1389
        # provide a subset of the requests
 
1390
        if partial_providers:
 
1391
            details = _("Provides")
 
1392
        else:
 
1393
            details = None
 
1394
        best_providers = set([struct.best_provider for struct in structures])
 
1395
        confirm = ConfirmInstallDialog(title, message, parent=parent,
 
1396
                                       details=details,
 
1397
                                       selectable=len(pkgs) > 1,
 
1398
                                       package_type=_("Plugin Package"))
 
1399
        for pkg, provides_all, provides, score in pkgs:
 
1400
            if provides_all:
 
1401
                details = _("All")
 
1402
            else:
 
1403
                #TRANSLATORS: Separator for a list of plugins
 
1404
                details = _(",\n").join(provides)
 
1405
            # Skip the architecture from the name
 
1406
            install = pkg.name.split(":")[0] in best_providers
 
1407
            confirm.add_confirm_package(pkg, install, details, score)
 
1408
        res = confirm.run()
 
1409
        if res == Gtk.ResponseType.OK:
 
1410
            yield self.backend.install_packages(xid,
 
1411
                                                confirm.get_selected_pkgs(),
 
1412
                                                interaction)
 
1413
        else:
 
1414
            raise errors.ModifyCancelled
 
1415
 
 
1416
    @dbus_deferred_method(PACKAGEKIT_MODIFY_DBUS_INTERFACE,
 
1417
                          in_signature="uass", out_signature="",
 
1418
                          sender_keyword="sender",
 
1419
                          utf8_strings=True)
 
1420
    def RemovePackageByFiles(self, xid, files, interaction, sender):
 
1421
        """Remove packages which provide the given files.
 
1422
 
 
1423
        Keyword arguments:
 
1424
        xid -- the window id of the requesting application
 
1425
        files -- the list of file paths
 
1426
        interaction -- the interaction mode: which ui elements should be
 
1427
                       shown e.g. hide-finished or hide-confirm-search
 
1428
        """
 
1429
        log.info("RemovePackageByFiles() was called: %s, %s, %s", xid, files,
 
1430
                 interaction)
 
1431
        return self._remove_package_by_files(xid, files, interaction, sender)
 
1432
 
 
1433
    @track_usage
 
1434
    @inline_callbacks
 
1435
    def _remove_package_by_files(self, xid, files, interaction, sender):
 
1436
        parent = None # should get from XID, but removed from Gdk 3
 
1437
        sender_name = yield self._get_sender_name(sender)
 
1438
        if [filename for filename in files if not filename.startswith("/")]:
 
1439
            raise errors.ModifyFailed("Only absolute file names")
 
1440
        pkgs = []
 
1441
        title = _("Searching software to be removed")
 
1442
        if sender_name:
 
1443
            message = gettext.ngettext("%s wants to remove the software "
 
1444
                                       "which provides the following file:",
 
1445
                                       "%s wants to remove the software which "
 
1446
                                       "provides the following files:",
 
1447
                                       len(files)) % sender_name
 
1448
        else:
 
1449
            message = gettext.ngettext("The software which provides the "
 
1450
                                       "following file is requested to be "
 
1451
                                       "removed:"
 
1452
                                       "The software which provides the "
 
1453
                                       "following files is requested to be "
 
1454
                                       "removed:",
 
1455
                                       len(files))
 
1456
        message += self._get_bullet_list(files)
 
1457
        progress = ProgressDialog(title, message, parent)
 
1458
        progress.show_all()
 
1459
        self._init_cache(progress)
 
1460
        for pkg in self._cache:
 
1461
            try:
 
1462
                for installed_file in pkg.installed_files:
 
1463
                    if installed_file in files:
 
1464
                        pkgs.append(pkg)
 
1465
            except:
 
1466
                pass
 
1467
        progress.hide()
 
1468
        if not pkgs:
 
1469
            self._show_error(_("Files are not installed"),
 
1470
                             _("The files which should be removed are not "
 
1471
                               "part of any installed software."))
 
1472
            raise errors.ModifyNoPackagesFound
 
1473
        title = gettext.ngettext("Remove software package?",
 
1474
                                 "Remove software packages?", len(pkgs))
 
1475
        if sender_name:
 
1476
            #TRANSLATORS: %s is the name of an application
 
1477
            message = gettext.ngettext("%s wants to remove the following "
 
1478
                                       "software package from your computer.",
 
1479
                                       "%s wants to remove the following "
 
1480
                                       "software packages from your computer.",
 
1481
                                       len(pkgs)) % sender_name
 
1482
        else:
 
1483
            message = gettext.ngettext("The following software package "
 
1484
                                       "will be removed from your computer.",
 
1485
                                       "The following software packages "
 
1486
                                       "will be removed from your computer.",
 
1487
                                       len(pkgs))
 
1488
        confirm = ConfirmInstallDialog(title, message, parent=parent,
 
1489
                                       selectable=len(pkgs) > 1, pkgs=pkgs,
 
1490
                                       action=_("_Remove"))
 
1491
        res = confirm.run()
 
1492
        if res == Gtk.ResponseType.OK:
 
1493
            yield self.backend.remove_packages(xid,
 
1494
                                               confirm.get_selected_pkgs(),
 
1495
                                               interaction)
 
1496
        else:
 
1497
            raise errors.ModifyCancelled
 
1498
 
 
1499
    def _show_error(self, header, message):
 
1500
        """Show an error dialog."""
 
1501
        dialog = ErrorDialog(header, message)
 
1502
        dialog.run()
 
1503
        dialog.hide()
 
1504
 
 
1505
    def _get_bullet_list(self, lst):
 
1506
        """Return a string with a bullet list for the given list."""
 
1507
        text = ""
 
1508
        if len(lst) == 1:
 
1509
            text += " "
 
1510
            text += lst[0]
 
1511
        else:
 
1512
            for element in lst:
 
1513
                text += "\n• %s" % element
 
1514
        return text
 
1515
 
 
1516
 
 
1517
def _is_package_restricted(pkg):
 
1518
    """If a package is possibly restricted in use."""
 
1519
    return (pkg.name.split(":")[0] in RESTRICTED_010_PACKAGES + RESTRICTED_10_PACKAGES \
 
1520
            or pkg.candidate.origins[0].component in ("non-free",
 
1521
                                                   "restricted",
 
1522
                                                   "multiverse"))
 
1523
 
 
1524
def main():
 
1525
    log.setLevel(logging.DEBUG)
 
1526
    si = SessionInstaller()
 
1527
    si.run()
 
1528
 
 
1529
if __name__ == "__main__":
 
1530
    main()
 
1531
 
 
1532
# vim:ts=4:sw=4:et