~glatzor/aptdaemon/maverik-updates

« back to all changes in this revision

Viewing changes to .pc/05_sources_list_password.patch/aptdaemon/core.py

  • Committer: Bazaar Package Importer
  • Author(s): Michael Vogt
  • Date: 2010-09-08 09:38:31 UTC
  • Revision ID: james.westby@ubuntu.com-20100908093831-xg57uhzyzxuny56a
Tags: 0.31+bzr476-0ubuntu2
* debian/patches/07_i18n_fixes.patch: 
  - use dgettext to not clobber the clients gettext.textdomain
    on import (LP: #631675)
* debian/patches/05_sources_list_password.patch: 
  - when adding a sources.list entry with a password protect
    the file via mode 0640 and root.admin ownership

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# -*- coding: utf-8 -*-
 
3
"""
 
4
Core components of aptdaemon.
 
5
 
 
6
This module provides the following core classes of the aptdaemon:
 
7
AptDaemon - complete daemon for managing software via DBus interface
 
8
Transaction - represents a software management operation
 
9
TransactionQueue - queue for aptdaemon transactions
 
10
 
 
11
The main function allows to run the daemon as a command.
 
12
"""
 
13
# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>
 
14
#
 
15
# This program is free software; you can redistribute it and/or modify
 
16
# it under the terms of the GNU General Public License as published by
 
17
# the Free Software Foundation; either version 2 of the License, or
 
18
# any later version.
 
19
#
 
20
# This program is distributed in the hope that it will be useful,
 
21
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
22
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
23
# GNU General Public License for more details.
 
24
#
 
25
# You should have received a copy of the GNU General Public License along
 
26
# with this program; if not, write to the Free Software Foundation, Inc.,
 
27
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
28
 
 
29
__author__  = "Sebastian Heinlein <devel@glatzor.de>"
 
30
 
 
31
__all__ = ("Transaction", "TransactionQueue", "AptDaemon",
 
32
           "APTDAEMON_TRANSACTION_DBUS_INTERFACE", "APTDAEMON_DBUS_INTERFACE"
 
33
           "APTDAEMON_DBUS_PATH", "APTDAEMON_DBUS_SERVICE",
 
34
           "APTDAEMON_IDLE_CHECK_INTERVAL", "APTDAEMON_IDLE_TIMEOUT",
 
35
           "TRANSACTION_IDLE_TIMEOUT", "TRANSACTION_DEL_TIMEOUT")
 
36
 
 
37
import collections
 
38
from functools import wraps
 
39
import gettext
 
40
from gettext import gettext as _
 
41
import locale
 
42
import logging
 
43
import logging.handlers
 
44
from optparse import OptionParser
 
45
import os
 
46
import Queue
 
47
import signal
 
48
import sys
 
49
import tempfile
 
50
import time
 
51
import threading
 
52
import uuid
 
53
 
 
54
import gobject
 
55
import dbus.exceptions
 
56
import dbus.service
 
57
import dbus.mainloop.glib
 
58
import dbus.glib
 
59
from softwareproperties.AptAuth import AptAuth
 
60
from aptsources.sourceslist import SourcesList
 
61
import apt_pkg
 
62
import aptsources
 
63
import aptsources.distro
 
64
 
 
65
import errors
 
66
import enums
 
67
from defer import dbus_deferred_method, Deferred, inline_callbacks, return_value
 
68
import policykit1
 
69
from worker import AptWorker, DummyWorker
 
70
from loop import mainloop
 
71
 
 
72
# Required for translations from APT
 
73
locale.setlocale(locale.LC_ALL, "")
 
74
 
 
75
# Setup i18n
 
76
gettext.textdomain("aptdaemon")
 
77
 
 
78
APTDAEMON_DBUS_INTERFACE = 'org.debian.apt'
 
79
APTDAEMON_DBUS_PATH = '/org/debian/apt'
 
80
APTDAEMON_DBUS_SERVICE = 'org.debian.apt'
 
81
 
 
82
APTDAEMON_TRANSACTION_DBUS_INTERFACE = 'org.debian.apt.transaction'
 
83
 
 
84
APTDAEMON_IDLE_CHECK_INTERVAL = 60
 
85
APTDAEMON_IDLE_TIMEOUT = 5 * 60
 
86
 
 
87
# Maximum allowed time between the creation of a transaction and its queuing
 
88
TRANSACTION_IDLE_TIMEOUT = 300
 
89
# Keep the transaction for the given time alive on the bus after it has
 
90
# finished
 
91
TRANSACTION_DEL_TIMEOUT = 5
 
92
 
 
93
# Setup the DBus main loop
 
94
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 
95
 
 
96
# Required for daemon mode
 
97
os.putenv("PATH",
 
98
          "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
 
99
 
 
100
# Setup logging to syslog and the console
 
101
log = logging.getLogger("AptDaemon")
 
102
try:
 
103
    _syslog_handler = logging.handlers.SysLogHandler(address="/dev/log",
 
104
                             facility=logging.handlers.SysLogHandler.LOG_DAEMON)
 
105
    _syslog_handler.setLevel(logging.INFO)
 
106
    _syslog_formatter = logging.Formatter("%(name)s: %(levelname)s: "
 
107
                                          "%(message)s")
 
108
    _syslog_handler.setFormatter(_syslog_formatter)
 
109
except:
 
110
    pass
 
111
else:
 
112
    log.addHandler(_syslog_handler)
 
113
_console_handler = logging.StreamHandler()
 
114
_console_formatter = logging.Formatter("%(asctime)s %(name)s [%(levelname)s]: "
 
115
                                       "%(message)s",
 
116
                                       "%T")
 
117
_console_handler.setFormatter(_console_formatter)
 
118
log.addHandler(_console_handler)
 
119
#FIXME: Use LoggerAdapter (requires Python 2.6)
 
120
log_trans = logging.getLogger("AptDaemon.Trans")
 
121
 
 
122
 
 
123
class Transaction(dbus.service.Object):
 
124
 
 
125
    """Represents a transaction on the D-Bus.
 
126
 
 
127
    A transaction represents a single package management task, e.g. installation
 
128
    or removal of packages. This class allows to expose information and to
 
129
    controll the transaction via DBus using policykit for managing
 
130
    privileges.
 
131
    """
 
132
 
 
133
    def __init__(self, role, queue, uid, sender, connect=True, bus=None,
 
134
                 packages=None, kwargs=None):
 
135
        """Initialize a new Transaction instance.
 
136
 
 
137
        Keyword arguments:
 
138
        queue -- TransactionQueue instance of the daemon
 
139
        uid -- the id of the user who created the transaction
 
140
        sender -- the DBus name of the sender who created the transaction
 
141
        connect -- if the Transaction should connect to DBus (default is True)
 
142
        bus -- the DBus connection which should be used (defaults to system bus)
 
143
        """
 
144
        id = uuid.uuid4().get_hex()
 
145
        self.tid = "/org/debian/apt/transaction/%s" % id
 
146
        if connect == True:
 
147
            if bus is None:
 
148
                bus = dbus.SystemBus()
 
149
            bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE, bus)
 
150
            dbus_path = self.tid
 
151
        else:
 
152
            bus = None
 
153
            bus_name = None
 
154
            dbus_path = None
 
155
        dbus.service.Object.__init__(self, bus_name, dbus_path)
 
156
        if not packages:
 
157
            packages = [[], [], [], [], []]
 
158
        if not kwargs:
 
159
            kwargs = {}
 
160
        self.queue = queue
 
161
        self.uid = uid
 
162
        self.locale = ""
 
163
        self.allow_unauthenticated = False
 
164
        self.remove_obsoleted_depends = False
 
165
        self.http_proxy = ""
 
166
        self.terminal = ""
 
167
        self.debconf = ""
 
168
        self.kwargs = kwargs
 
169
        self._role = role
 
170
        self._progress = 0
 
171
        self._progress_details = (0, 0, 0, 0, 0, 0)
 
172
        self._progress_download = ("", "", "", 0, 0, "")
 
173
        self._exit = enums.EXIT_UNFINISHED
 
174
        self._status = enums.STATUS_SETTING_UP
 
175
        self._status_details = ""
 
176
        self._error = (-1, "")
 
177
        self._cancellable = True
 
178
        self._term_attached = False
 
179
        self._required_medium = ("", "")
 
180
        self._config_file_conflict = ("", "")
 
181
        self._config_file_conflict_resolution = ""
 
182
        self.cancelled = False
 
183
        self.paused = False
 
184
        self._meta_data = dbus.Dictionary(signature="ss")
 
185
        self._dpkg_status = None
 
186
        self._download = 0
 
187
        self._space = 0
 
188
        self._depends = [dbus.Array([], signature=dbus.Signature('s')) \
 
189
                         for i in range(7)]
 
190
        self._packages = [pkgs or dbus.Array([], signature=dbus.Signature('s'))\
 
191
                          for pkgs in packages]
 
192
        # Add a timeout which removes the transaction from the bus if it
 
193
        # hasn't been setup and run for the TRANSACTION_IDLE_TIMEOUT period
 
194
        self._idle_watch = gobject.timeout_add_seconds(
 
195
            TRANSACTION_IDLE_TIMEOUT, self._remove_from_connection_no_raise)
 
196
        # Handle a disconnect of the client application
 
197
        self.sender_alive = True
 
198
        if bus:
 
199
            self._sender_watch = bus.watch_name_owner(sender,
 
200
                                                     self._sender_owner_changed)
 
201
        else:
 
202
            self._sender_watch = None
 
203
 
 
204
    def _sender_owner_changed(self, connection):
 
205
        """Callback if the owner of the original sender changed, e.g.
 
206
        disconnected."""
 
207
        if not connection:
 
208
            self.sender_alive = False
 
209
 
 
210
    def _remove_from_connection_no_raise(self):
 
211
        """Version of remove_from_connection that does not raise if the
 
212
        object isn't exported.
 
213
        """
 
214
        log_trans.debug("Removing transaction")
 
215
        try:
 
216
            self.remove_from_connection()
 
217
        except LookupError, error:
 
218
            log_trans.debug("remove_from_connection() raised LookupError: "
 
219
                            "'%s'" % error)
 
220
 
 
221
    def _set_meta_data(self, data):
 
222
        # Perform some checks
 
223
        if self.status != enums.STATUS_SETTING_UP:
 
224
            raise errors.TransactionAlreadyRunning()
 
225
        if not isinstance(data, dbus.Dictionary):
 
226
            raise errors.InvalidMetaDataError("The data value has to be a "
 
227
                                              "dictionary: %s" % data)
 
228
        for key, value in data.iteritems():
 
229
            if key in self._meta_data:
 
230
                raise errors.InvalidMetaDataError("The key %s already "
 
231
                                                  "exists. It is not allowed "
 
232
                                                  "to overwrite existing "
 
233
                                                  "data." % key)
 
234
            if not len(key.split("_")) > 1:
 
235
                raise errors.InvalidMetaDataError("The key %s has to be of the "
 
236
                                                  "format IDENTIFIER-KEYNAME")
 
237
            if not isinstance(value, dbus.String):
 
238
                raise errors.InvalidMetaDataError("The value has to be a "
 
239
                                                  "string: %s" % value)
 
240
        # Merge new data into existing one:
 
241
        self._meta_data.update(data)
 
242
        self.PropertyChanged("MetaData", self._meta_data)
 
243
 
 
244
    def _get_meta_data(self):
 
245
        return self._meta_data
 
246
 
 
247
    meta_data = property(_get_meta_data, _set_meta_data,
 
248
                         doc="Allows client applications to store meta data "
 
249
                             "for the transaction in a dictionary.")
 
250
 
 
251
    def _set_role(self, enum):
 
252
        if self._role != enums.ROLE_UNSET:
 
253
            raise errors.TransactionRoleAlreadySet()
 
254
        self.PropertyChanged("Role", enum)
 
255
        self._role = enum
 
256
 
 
257
    def _get_role(self):
 
258
        return self._role
 
259
 
 
260
    role = property(_get_role, _set_role, doc="Operation type of transaction.")
 
261
 
 
262
    def _set_progress_details(self, details):
 
263
        items_done, items_total, bytes_done, bytes_total, speed, eta = details
 
264
        self._progress_details = details
 
265
        self.PropertyChanged("ProgressDetails", details)
 
266
 
 
267
    def _get_progress_details(self):
 
268
        return self._progress_details
 
269
 
 
270
    progress_details = property(_get_progress_details, _set_progress_details,
 
271
                                doc = "Tuple containing detailed progress "
 
272
                                      "information: items done, total items, "
 
273
                                      "bytes done, total bytes, speed and "
 
274
                                      "remaining time")
 
275
 
 
276
    def _set_error(self, excep):
 
277
        self._error = excep
 
278
        self.PropertyChanged("Error", (excep.code, excep.details))
 
279
 
 
280
    def _get_error(self):
 
281
        return self._error
 
282
 
 
283
    error = property(_get_error, _set_error, doc="Raised exception.")
 
284
 
 
285
    def _set_exit(self, enum):
 
286
        self.status = enums.STATUS_FINISHED
 
287
        self._exit = enum
 
288
        self.PropertyChanged("ExitState", enum)
 
289
        self.Finished(enum)
 
290
        if self._sender_watch:
 
291
            self._sender_watch.cancel()
 
292
        # Remove the transaction from the Bus after it is complete. A short
 
293
        # timeout helps lazy clients
 
294
        gobject.timeout_add_seconds(TRANSACTION_DEL_TIMEOUT,
 
295
                                    self._remove_from_connection_no_raise)
 
296
 
 
297
    def _get_exit(self):
 
298
        return self._exit
 
299
 
 
300
    exit = property(_get_exit, _set_exit,
 
301
                    doc="The exit state of the transaction.")
 
302
 
 
303
    def _get_download(self):
 
304
        return self._download
 
305
 
 
306
    def _set_download(self, size):
 
307
        self.PropertyChanged("Download", size)
 
308
        self._download = size
 
309
 
 
310
    download = property(_get_download, _set_download,
 
311
                        doc="The download size of the transaction.")
 
312
 
 
313
    def _get_space(self):
 
314
        return self._space
 
315
 
 
316
    def _set_space(self, size):
 
317
        self.PropertyChanged("Space", size)
 
318
        self._space = size
 
319
 
 
320
    space = property(_get_space, _set_space,
 
321
                     doc="The required disk space of the transaction.")
 
322
 
 
323
    def _get_packages(self):
 
324
        return self._packages
 
325
 
 
326
    def _set_packages(self, packages):
 
327
        self._packages = [dbus.Array(pkgs, signature=dbus.Signature('s')) \
 
328
                          for pkgs in packages]
 
329
        self.PropertyChanged("Packages", self._packages)
 
330
 
 
331
    packages = property(_get_packages, _set_packages,
 
332
                        doc="Packages which will be explictly install, "
 
333
                            "upgraded, removed, purged or reinstalled.")
 
334
 
 
335
    def _get_depends(self):
 
336
        return self._depends
 
337
 
 
338
    def _set_depends(self, depends):
 
339
        self._depends = [dbus.Array(deps, signature=dbus.Signature('s')) \
 
340
                         for deps in depends]
 
341
        self.PropertyChanged("Dependencies", self._depends)
 
342
 
 
343
    depends = property(_get_depends, _set_depends,
 
344
                       doc="The additional dependencies: installs, removals, "
 
345
                           "upgrades and downgrades.")
 
346
 
 
347
    def _get_status(self):
 
348
        return self._status
 
349
 
 
350
    def _set_status(self, enum):
 
351
        self.PropertyChanged("Status", enum)
 
352
        self._status = enum
 
353
 
 
354
    status = property(_get_status, _set_status,
 
355
                      doc="The status of the transaction.")
 
356
 
 
357
    def _get_status_details(self):
 
358
        return self._status_details
 
359
 
 
360
    def _set_status_details(self, text):
 
361
        self._status_details = text
 
362
        self.PropertyChanged("StatusDetails", text)
 
363
 
 
364
    status_details = property(_get_status_details, _set_status_details,
 
365
                              doc="The status message from apt.")
 
366
 
 
367
    def _get_progress(self):
 
368
        return self._progress
 
369
 
 
370
    def _set_progress(self, percent):
 
371
        self._progress = percent
 
372
        self.PropertyChanged("Progress", percent)
 
373
 
 
374
    progress = property(_get_progress, _set_progress,
 
375
                      doc="The progress of the transaction in percent.")
 
376
 
 
377
    def _get_progress_download(self):
 
378
        return self._progress_download
 
379
 
 
380
    def _set_progress_download(self, progress_download):
 
381
        self._progress_download = progress_download
 
382
        self.PropertyChanged("ProgressDownload", progress_download)
 
383
 
 
384
    progress_download = property(_get_progress_download,
 
385
                                 _set_progress_download,
 
386
                                 doc="The last progress update of a currently"
 
387
                                     "running download. A tuple of URI, "
 
388
                                     "status, short description, full size, "
 
389
                                     "partially downloaded size and a status "
 
390
                                     "message.")
 
391
 
 
392
    def _get_cancellable(self):
 
393
        return self._cancellable
 
394
 
 
395
    def _set_cancellable(self, cancellable):
 
396
        self._cancellable = cancellable
 
397
        self.PropertyChanged("Cancellable", cancellable)
 
398
 
 
399
    cancellable = property(_get_cancellable, _set_cancellable,
 
400
                           doc="If it's currently allowed to cancel the "
 
401
                               "transaction.")
 
402
 
 
403
    def _get_term_attached(self):
 
404
        return self._term_attached
 
405
 
 
406
    def _set_term_attached(self, attached):
 
407
        self._term_attached = attached
 
408
        self.PropertyChanged("TerminalAttached", attached)
 
409
 
 
410
    term_attached = property(_get_term_attached, _set_term_attached,
 
411
                             doc="If the controlling terminal is currently "
 
412
                                 "attached to the dpkg call of the "
 
413
                                 "transaction.")
 
414
 
 
415
    def _get_required_medium(self):
 
416
        return self._required_medium
 
417
 
 
418
    def _set_required_medium(self, medium):
 
419
        label, drive = medium
 
420
        self._required_medium = medium
 
421
        self.PropertyChanged("RequiredMedium", medium)
 
422
        self.MediumRequired(label, drive)
 
423
 
 
424
    required_medium = property(_get_required_medium, _set_required_medium,
 
425
                               doc="Tuple containing the label and the drive "
 
426
                                   "of a required CD/DVD to install packages "
 
427
                                   "from.")
 
428
 
 
429
    def _get_config_file_conflict(self):
 
430
        return self._config_file_conflict
 
431
 
 
432
    def _set_config_file_conflict(self, prompt):
 
433
        if prompt is None:
 
434
            self._config_file_conflict = None
 
435
            return
 
436
        old, new = prompt
 
437
        self._config_file_conflict = prompt
 
438
        self.PropertyChanged("ConfigFileConflict", prompt)
 
439
        self.ConfigFileConflict(old, new)
 
440
 
 
441
    config_file_conflict = property(_get_config_file_conflict,
 
442
                                    _set_config_file_conflict,
 
443
                                    doc="Tuple containing the old and the new "
 
444
                                      "path of the configuration file")
 
445
 
 
446
    # Signals
 
447
 
 
448
    @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
449
                         signature="sv")
 
450
    def PropertyChanged(self, property, value):
 
451
        """Set and emit if a property of the transaction changed.
 
452
 
 
453
        Keyword argument:
 
454
        property -- the name of the property
 
455
        value -- the new value of the property
 
456
        """
 
457
        log_trans.debug("Emitting PropertyChanged: %s, %s" % (property, value))
 
458
 
 
459
    @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
460
                         signature="s")
 
461
    def Finished(self, enum):
 
462
        """Mark and emit the transaction as finished.
 
463
 
 
464
        Keyword argument:
 
465
        enum -- the exit state of the transaction, e.g. EXIT_FAILED
 
466
        """
 
467
        log_trans.debug("Emitting Finished: %s" % \
 
468
                        enums.get_exit_string_from_enum(exit))
 
469
 
 
470
    @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
471
                         signature="ss")
 
472
    def MediumRequired(self, medium, drive):
 
473
        """Set and emit the required medium change.
 
474
 
 
475
        This method/signal should be used to inform the user to
 
476
        insert the installation CD/DVD:
 
477
 
 
478
        Keyword arguments:
 
479
        medium -- the CD/DVD label
 
480
        drive -- mount point of the drive
 
481
        """
 
482
        log_trans.debug("Emitting MediumRequired: %s, %s" % (medium, drive))
 
483
 
 
484
    @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
485
                         signature="ss")
 
486
    def ConfigFileConflict(self, old, new):
 
487
        """Set and emit the ConfigFileConflict signal.
 
488
 
 
489
        This method/signal should be used to inform the user to
 
490
        answer a config file prompt.
 
491
 
 
492
        Keyword arguments:
 
493
        old -- current version of the configuration prompt
 
494
        new -- new version of the configuration prompt
 
495
        """
 
496
        log_trans.debug("Emitting ConfigFileConflict: %s, %s" % (old, new))
 
497
 
 
498
    # Methods
 
499
 
 
500
    def _set_locale(self, locale_str):
 
501
        """Set the language and encoding.
 
502
 
 
503
        Keyword arguments:
 
504
        locale -- specifies language, territory and encoding according
 
505
                  to RFC 1766,  e.g. "de_DE.UTF-8"
 
506
        """
 
507
 
 
508
        if self.status != enums.STATUS_SETTING_UP:
 
509
            raise errors.TransactionAlreadyRunning()
 
510
        try:
 
511
            (lang, encoding) = locale._parse_localename(locale_str)
 
512
        except ValueError:
 
513
            raise
 
514
        else:
 
515
            self.locale = "%s.%s" % (lang, encoding)
 
516
            self.PropertyChanged("locale", self.locale)
 
517
 
 
518
    @inline_callbacks
 
519
    def _set_http_proxy(self, url, sender):
 
520
        """Set an http network proxy.
 
521
 
 
522
        Keyword arguments:
 
523
        url -- the URL of the proxy server, e.g. http://proxy:8080
 
524
        """
 
525
        if url != "" and (not url.startswith("http://") or not ":" in url):
 
526
            raise errors.InvalidProxyError(proxy)
 
527
        action = policykit1.PK_ACTION_SET_PROXY
 
528
        yield policykit1.check_authorization_by_name(sender, action)
 
529
        self.http_proxy = url
 
530
        self.PropertyChanged("HttpProxy", self.http_proxy)
 
531
 
 
532
    def _set_remove_obsoleted_depends(self, remove_obsoleted_depends):
 
533
        """Set the handling of the removal of automatically installed
 
534
        dependencies which are now obsoleted.
 
535
 
 
536
        Keyword arguments:
 
537
        remove_obsoleted_depends -- If True also remove automatically installed
 
538
            dependencies of to removed packages
 
539
        """
 
540
        self.remove_obsoleted_depends = remove_obsoleted_depends
 
541
        self.PropertyChanged("RemoveObsoletedDepends",
 
542
                             self.remove_obsoleted_depends)
 
543
 
 
544
    def _set_allow_unauthenticated(self, allow_unauthenticated):
 
545
        """Set the handling of unauthenticated packages 
 
546
 
 
547
        Keyword arguments:
 
548
        allow_unauthenticated -- True to allow packages that come from a 
 
549
                           repository without a valid authentication signature
 
550
        """
 
551
        self.allow_unauthenticated = allow_unauthenticated
 
552
        self.PropertyChanged("AllowUnauthenticated", self.allow_unauthenticated)
 
553
 
 
554
    @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
555
                          in_signature="", out_signature="",
 
556
                          sender_keyword="sender")
 
557
    def Run(self, sender):
 
558
        """Queue the transaction for processing."""
 
559
        log_trans.info("Queuing transaction %s", self.tid)
 
560
        return self._run(sender)
 
561
 
 
562
    @inline_callbacks
 
563
    def _run(self, sender):
 
564
        yield self._check_foreign_user(sender)
 
565
        self.queue.put(self)
 
566
 
 
567
    @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
568
                          in_signature="", out_signature="",
 
569
                          sender_keyword="sender")
 
570
    def Cancel(self, sender):
 
571
        """Cancel the transaction.
 
572
 
 
573
        Keyword argument:
 
574
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
575
        """
 
576
        log_trans.info("Cancelling transaction %s", self.tid)
 
577
        return self._cancel(sender)
 
578
 
 
579
    @inline_callbacks
 
580
    def _cancel(self, sender):
 
581
        try:
 
582
            yield self._check_foreign_user(sender)
 
583
        except errors.ForeignTransaction:
 
584
            action = policykit1.PK_ACTION_CANCEL_FOREIGN
 
585
            yield policykit1.check_authorization_by_name(sender, action)
 
586
        try:
 
587
            self.queue.remove(self)
 
588
            log_trans.debug("Removed transaction from queue")
 
589
        except ValueError:
 
590
            pass
 
591
        else:
 
592
            self.status = enums.STATUS_CANCELLING
 
593
            self.exit = enums.EXIT_CANCELLED
 
594
            return
 
595
        if self.cancellable:
 
596
            log_trans.debug("Setting cancel event")
 
597
            self.cancelled = True
 
598
            self.status = enums.STATUS_CANCELLING
 
599
            self.paused = False
 
600
            return
 
601
        raise errors.AptDaemonError("Could not cancel transaction")
 
602
 
 
603
    @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
604
                          in_signature="", out_signature="",
 
605
                          sender_keyword="sender")
 
606
    def Simulate(self, sender):
 
607
        """Simulate a transaction to update its dependencies, future status,
 
608
        download size and required disk space.
 
609
 
 
610
        Call this method if you want to show changes before queuing the
 
611
        transaction.
 
612
        """
 
613
        log_trans.info("Simulate was called")
 
614
        return self._simulate(sender)
 
615
 
 
616
    @inline_callbacks
 
617
    def _simulate(self, sender):
 
618
        if self.status != enums.STATUS_SETTING_UP:
 
619
            raise errros.TransactionAlreadyRunning()
 
620
        yield self._check_foreign_user(sender)
 
621
        self.depends, self._dpkg_status, self.download, self.space = \
 
622
                self.queue.worker.simulate(self, self.queue.future_status)
 
623
        if self._idle_watch is not None:
 
624
            gobject.source_remove(self._idle_watch)
 
625
            self._idle_watch = None
 
626
 
 
627
    def _set_terminal(self, ttyname):
 
628
        """Set the controlling terminal.
 
629
 
 
630
        The worker will be attached to the specified slave end of a pty
 
631
        master/slave pair. This allows to interact with the 
 
632
 
 
633
        Can only be changed before the transaction is started.
 
634
 
 
635
        Keyword arguments:
 
636
        ttyname -- file path to the slave file descriptor
 
637
        """
 
638
        if self.status != enums.STATUS_SETTING_UP:
 
639
            raise errros.TransactionAlreadyRunning()
 
640
        if not os.access(ttyname, os.W_OK):
 
641
            raise errors.AptDaemonError("Pty device does not exist: "
 
642
                                        "%s" % ttyname)
 
643
        if not os.stat(ttyname)[4] == self.uid:
 
644
            raise errors.AptDaemonError("Pty device '%s' has to be owned by"
 
645
                                        "the owner of the transaction "
 
646
                                        "(uid %s) " % (ttyname, self.uid))
 
647
        try:
 
648
            slave_fd = os.open(ttyname, os.O_RDWR | os.O_NOCTTY)
 
649
            if os.isatty(slave_fd):
 
650
                self.terminal = ttyname
 
651
                self.PropertyChanged("Terminal", self.terminal)
 
652
            else:
 
653
                raise errors.AptDaemonError("%s isn't a tty" % ttyname)
 
654
        finally:
 
655
            os.close(slave_fd)
 
656
 
 
657
    def _set_debconf(self, debconf_socket):
 
658
        """Set the socket of the debconf proxy.
 
659
 
 
660
        The worker process forwards all debconf commands through this
 
661
        socket by using the passthrough frontend. On the client side
 
662
        debconf-communicate should be connected to the socket.
 
663
 
 
664
        Can only be changed before the transaction is started.
 
665
 
 
666
        Keyword arguments:
 
667
        debconf_socket: absolute path to the socket
 
668
        """
 
669
        if self.status != enums.STATUS_SETTING_UP:
 
670
            raise errors.TransactionAlreadyRunning()
 
671
        if not os.access(debconf_socket, os.W_OK):
 
672
            raise errors.AptDaemonError("socket does not exist: "
 
673
                                        "%s" % debconf_socket)
 
674
        if not os.stat(debconf_socket)[4] == self.uid:
 
675
            raise errors.AptDaemonError("socket '%s' has to be owned by the "
 
676
                                        "owner of the transaction " % ttyname)
 
677
        self.debconf = debconf_socket
 
678
        self.PropertyChanged("DebconfSocket", self.debconf)
 
679
 
 
680
    @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
681
                          in_signature="s", out_signature="",
 
682
                          sender_keyword="sender")
 
683
    def ProvideMedium(self, medium, sender):
 
684
        """Continue paused transaction with the inserted medium.
 
685
 
 
686
        If a media change is required to install packages from CD/DVD
 
687
        the transaction will be paused and could be resumed with this
 
688
        method.
 
689
 
 
690
        Keyword arguments:
 
691
        medium -- the label of the CD/DVD
 
692
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
693
        """
 
694
        log_trans.info("Medium %s was provided", medium)
 
695
        return self._provide_medium(medium, sender)
 
696
 
 
697
    @inline_callbacks
 
698
    def _provide_medium(self, medium, sender):
 
699
        yield self._check_foreign_user(sender)
 
700
        if not self.required_medium:
 
701
            raise errors.AptDaemonError("There isn't any required medium.")
 
702
        if not self.required_medium[0] == medium:
 
703
            raise errros.AptDaemonError("The medium '%s' isn't "
 
704
                                        "requested." % medium)
 
705
        self.paused = False
 
706
 
 
707
    @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
 
708
                          in_signature="ss", out_signature="",
 
709
                          sender_keyword="sender")
 
710
    def ResolveConfigFileConflict(self, config, answer, sender):
 
711
        """Resolve a configuration file conflict and continue the transaction.
 
712
 
 
713
        If a config file prompt is detected the transaction will be
 
714
        paused and could be resumed with this method.
 
715
 
 
716
        Keyword arguments:
 
717
        config -- the path to the original config file
 
718
        answer -- the answer to the configuration file question, can be
 
719
                  "keep" or "replace"
 
720
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
721
        """
 
722
        log_trans.info("Resolved conflict of %s with %s", config, answer)
 
723
        return self._resolve_config_file_conflict(config, answer, sender)
 
724
 
 
725
    @inline_callbacks
 
726
    def _resolve_config_file_conflict(self, config, answer, sender):
 
727
        yield self._check_foreign_user(sender)
 
728
        if not self.config_file_conflict:
 
729
            raise errors.AptDaemonError("There isn't any config file prompt "
 
730
                                        "required")
 
731
        if answer not in ["keep", "replace"]:
 
732
            # FIXME: should we re-send the config file prompt
 
733
            #        message or assume the client is buggy and
 
734
            #        just use a safe default (like keep)?
 
735
            raise errors.AptDaemonError("Invalid value: %s" % answer)
 
736
        if not self.config_file_conflict[0] == config:
 
737
            raise errors.AptDaemonError("Invalid config file: %s" % config)
 
738
        self.config_file_conflict_resolution = answer
 
739
        self.paused = False
 
740
 
 
741
    @dbus_deferred_method(dbus.PROPERTIES_IFACE,
 
742
                          in_signature="ssv", out_signature="",
 
743
                          sender_keyword="sender")
 
744
    def Set(self, iface, property, value, sender):
 
745
        """Set a property.
 
746
 
 
747
        Only the user who intiaited the transaction is
 
748
        allowed to modify it.
 
749
 
 
750
        Keyword arguments:
 
751
        iface -- the interface which provides the property
 
752
        property -- the property which should be modified
 
753
        value -- the new value of the property
 
754
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
755
        """
 
756
        log.debug("Set() was called: %s, %s" % (property, value))
 
757
        return self._set(iface, property, value, sender)
 
758
 
 
759
    @inline_callbacks
 
760
    def _set(self, iface, property, value, sender):
 
761
        yield self._check_foreign_user(sender)
 
762
        if iface == APTDAEMON_TRANSACTION_DBUS_INTERFACE:
 
763
            if property == "MetaData":
 
764
                self._set_meta_data(value)
 
765
            elif property == "Terminal":
 
766
                self._set_terminal(value)
 
767
            elif property == "DebconfSocket":
 
768
                self._set_debconf(value)
 
769
            elif property == "Locale":
 
770
                self._set_locale(value)
 
771
            elif property == "RemoveObsoletedDepends":
 
772
                self._set_remove_obsoleted_depends(value)
 
773
            elif property == "AllowUnauthenticated":
 
774
                self._set_allow_unauthenticated(value)
 
775
            elif property == "HttpProxy":
 
776
                self._set_http_proxy(value, sender)
 
777
            else:
 
778
                raise dbus.exceptions.DBusException("Unknown or read only "
 
779
                                                    "property: %s" % property)
 
780
        else:
 
781
            raise dbus.exceptions.DBusException("Unknown interface: %s" % iface)
 
782
 
 
783
    @dbus.service.method(dbus.PROPERTIES_IFACE,
 
784
                         in_signature="s", out_signature="a{sv}")
 
785
    def GetAll(self, iface):
 
786
        """Get all available properties of the given interface."""
 
787
        log.debug("GetAll() was called: %s" % iface)
 
788
        return self._get_properties(iface)
 
789
 
 
790
    @dbus.service.method(dbus.PROPERTIES_IFACE,
 
791
                         in_signature="ss", out_signature="v")
 
792
    def Get(self, iface, property):
 
793
        """Return the value of the given property provided by the given 
 
794
        interface.
 
795
        """
 
796
        log.debug("Get() was called: %s, %s" % (iface, property))
 
797
        return self._get_properties(iface)[property]
 
798
 
 
799
    def _get_properties(self, iface):
 
800
        #FIXME: Provide introspection information by overriding the Introspect
 
801
        #       method of the dbus.service.Object
 
802
        if iface == APTDAEMON_TRANSACTION_DBUS_INTERFACE:
 
803
            return {"Role": self.role,
 
804
                    "Progress": self.progress,
 
805
                    "ProgressDetails": self.progress_details,
 
806
                    "ProgressDownload": self.progress_download,
 
807
                    "Status": self.status,
 
808
                    "StatusDetails": self.status_details,
 
809
                    "Cancellable": self.cancellable,
 
810
                    "TerminalAttached": self.term_attached,
 
811
                    "RequiredMedium": self.required_medium,
 
812
                    "ConfigFileConflict": self.config_file_conflict,
 
813
                    "ExitState": self.exit,
 
814
                    "Error": self.error,
 
815
                    "Locale": self.locale,
 
816
                    "Terminal": self.terminal,
 
817
                    "DebconfSocket": self.debconf,
 
818
                    "Paused": self.paused,
 
819
                    "AllowUnauthenticated": self.allow_unauthenticated,
 
820
                    "RemoveObsoletedDepends": self.remove_obsoleted_depends,
 
821
                    "HttpProxy": self.http_proxy,
 
822
                    "Packages": self.packages,
 
823
                    "MetaData": self.meta_data,
 
824
                    "Dependencies": self.depends,
 
825
                    "Download": self.download,
 
826
                    "Space": self.space
 
827
                    }
 
828
        else:
 
829
            return {}
 
830
 
 
831
    @inline_callbacks
 
832
    def _check_foreign_user(self, dbus_name):
 
833
        """Check if the transaction is owned by the given caller."""
 
834
        uid = yield policykit1.get_uid_from_dbus_name(dbus_name)
 
835
        if self.uid != uid:
 
836
            raise errors.ForeignTransaction()
 
837
 
 
838
    def _set_kwargs(self, kwargs):
 
839
        """Set the kwargs which will be send to the AptWorker."""
 
840
        self.kwargs = kwargs
 
841
 
 
842
 
 
843
class TransactionQueue(gobject.GObject):
 
844
 
 
845
    """Queue for transactions."""
 
846
 
 
847
    __gsignals__ = {"queue-changed":(gobject.SIGNAL_RUN_FIRST,
 
848
                                     gobject.TYPE_NONE,
 
849
                                     ()),
 
850
                    "future-status-changed":(gobject.SIGNAL_RUN_FIRST,
 
851
                                     gobject.TYPE_NONE,
 
852
                                     ())}
 
853
 
 
854
    def __init__(self, dummy):
 
855
        """Intialize a new TransactionQueue instance."""
 
856
        gobject.GObject.__init__(self)
 
857
        self._queue = collections.deque()
 
858
        self._proc_count = 0
 
859
        if dummy:
 
860
            self.worker = DummyWorker()
 
861
        else:
 
862
            self.worker = AptWorker()
 
863
        self.future_status = None
 
864
        self.future_status_fd = None
 
865
        self.worker.connect("transaction-done", self._on_transaction_done)
 
866
 
 
867
    def __len__(self):
 
868
        return len(self._queue)
 
869
 
 
870
    def _emit_queue_changed(self):
 
871
        """Emit the queued-changed signal."""
 
872
        log.debug("emitting queue changed")
 
873
        self.emit("queue-changed")
 
874
 
 
875
    def _emit_future_status_changed(self):
 
876
        """Emit the future-status-changed signal."""
 
877
        log.debug("emitting future-status changed")
 
878
        self.emit("future-status-changed")
 
879
        #FIXME: All not yet queued transactions should listen to this signal
 
880
        #       and update be re-simulated if already done so
 
881
 
 
882
    def put(self, trans):
 
883
        """Add an item to the queue."""
 
884
        #FIXME: Add a timestamp to check if the future status of the trans
 
885
        #       is really the later one
 
886
        # Simulate the new transaction if this has not been done before:
 
887
        if trans._dpkg_status is None:
 
888
            trans.depends, trans._dpkg_status, trans.download, trans.space = \
 
889
                self.worker.simulate(trans, self.future_status)
 
890
        # Replace the old future status with the new one
 
891
        if self.future_status_fd is not None:
 
892
            os.close(self.future_status_fd)
 
893
            os.remove(self.future_status)
 
894
        self.future_status_fd, self.future_status = \
 
895
            tempfile.mkstemp(prefix="future-status-")
 
896
        os.write(self.future_status_fd, trans._dpkg_status)
 
897
        self._emit_future_status_changed()
 
898
 
 
899
        if trans._idle_watch is not None:
 
900
            gobject.source_remove(trans._idle_watch)
 
901
        if self.worker.trans:
 
902
            self._queue.append(trans)
 
903
        else:
 
904
            self.worker.run(trans)
 
905
        self._emit_queue_changed()
 
906
 
 
907
    def _on_transaction_done(self, worker, tid):
 
908
        """Mark the last item as done and request a new item."""
 
909
        #FIXME: Check if the transaction failed because of a broken system or
 
910
        #       if dpkg journal is dirty. If so allready queued transactions
 
911
        #       except the repair transactions should be removed from the queue
 
912
        try:
 
913
            next = self._queue.popleft()
 
914
        except IndexError:
 
915
            log.debug("There isn't any queued transaction")
 
916
            # Reset the future status to the system one
 
917
            if self.future_status_fd is not None:
 
918
                os.close(self.future_status_fd)
 
919
                os.remove(self.future_status)
 
920
            self.future_status_fd = None
 
921
            self.future_status = None
 
922
            self._emit_future_status_changed()
 
923
        else:
 
924
            self.worker.run(next)
 
925
        self._emit_queue_changed()
 
926
 
 
927
    def remove(self, transaction):
 
928
        """Remove the specified item from the queue."""
 
929
        # FIXME: handle future status
 
930
        self._queue.remove(transaction)
 
931
        self._emit_queue_changed()
 
932
 
 
933
    def clear(self):
 
934
        """Remove all items from the queue."""
 
935
        # FIXME: handle future status
 
936
        for transaction in self._queue:
 
937
            transaction._remove_from_connection_no_raise()
 
938
        self._queue.clear()
 
939
 
 
940
    @property
 
941
    def items(self):
 
942
        """Return a list containing all queued items."""
 
943
        return list(self._queue)
 
944
 
 
945
 
 
946
class AptDaemon(dbus.service.Object):
 
947
 
 
948
    """Provides a system daemon to process package management tasks.
 
949
 
 
950
    The daemon is transaction based. Each package management tasks runs
 
951
    in a separate transaction. The transactions can be created,
 
952
    monitored and managed via the D-Bus interface.
 
953
    """
 
954
 
 
955
    def __init__(self, options, connect=True, bus=None):
 
956
        """Initialize a new AptDaemon instance.
 
957
 
 
958
        Keyword arguments:
 
959
        options -- command line options of the type optparse.Values
 
960
        connect -- if the daemon should connect to the D-Bus (default is True)
 
961
        bus -- the D-Bus to connect to (defaults to the system bus)
 
962
        """
 
963
        log.info("Initializing daemon")
 
964
        signal.signal(signal.SIGQUIT, self._sigquit)
 
965
        signal.signal(signal.SIGTERM, self._sigquit)
 
966
        self.options = options
 
967
        if connect == True:
 
968
            if bus is None:
 
969
                bus = dbus.SystemBus()
 
970
            # Check if another object has already registered the name on
 
971
            # the bus. Quit the other daemon if replace would be set
 
972
            try:
 
973
                bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE,
 
974
                                                bus,
 
975
                                                do_not_queue=True)
 
976
            except dbus.exceptions.NameExistsException:
 
977
                if self.options.replace == False:
 
978
                    log.critical("Another daemon is already running")
 
979
                    sys.exit(1)
 
980
                log.warn("Replacing already running daemon")
 
981
                the_other_guy = bus.get_object(APTDAEMON_DBUS_SERVICE,
 
982
                                               APTDAEMON_DBUS_PATH)
 
983
                the_other_guy.Quit(dbus_interface=APTDAEMON_DBUS_INTERFACE,
 
984
                                   timeout=300)
 
985
                time.sleep(1)
 
986
                bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE,
 
987
                                                bus,
 
988
                                                do_not_queue=True)
 
989
        else:
 
990
            bus_name = None
 
991
        dbus.service.Object.__init__(self, bus_name, APTDAEMON_DBUS_PATH)
 
992
        self.queue = TransactionQueue(options.dummy)
 
993
        self.queue.connect("queue-changed", self._on_queue_changed)
 
994
        log.debug("Setting up worker thread")
 
995
        log.debug("Daemon was initialized")
 
996
 
 
997
    def _on_queue_changed(self, queue):
 
998
        """Callback for a changed transaction queue."""
 
999
        if self.queue.worker.trans:
 
1000
            current = self.queue.worker.trans.tid
 
1001
        else:
 
1002
            current = ""
 
1003
        queued = [trans.tid for trans in self.queue.items]
 
1004
        self.ActiveTransactionsChanged(current, queued)
 
1005
 
 
1006
    @dbus.service.signal(dbus_interface=APTDAEMON_DBUS_INTERFACE,
 
1007
                         signature="sas")
 
1008
    def ActiveTransactionsChanged(self, current, queued):
 
1009
        """Emit the ActiveTransactionsChanged signal.
 
1010
 
 
1011
        Keyword arguments:
 
1012
        current -- the tid of the currently running transaction or an empty
 
1013
                   string
 
1014
        queued -- list of the ids of the queued transactions
 
1015
        """
 
1016
        log.debug("Emitting ActiveTransactionsChanged signal: %s, %s",
 
1017
                  current, queued)
 
1018
 
 
1019
    def run(self):
 
1020
        """Start the daemon and listen for calls."""
 
1021
        if self.options.disable_timeout == False:
 
1022
            log.debug("Using inactivity check")
 
1023
            gobject.timeout_add_seconds(APTDAEMON_IDLE_CHECK_INTERVAL,
 
1024
                                        self._check_for_inactivity)
 
1025
        log.debug("Waiting for calls")
 
1026
        try:
 
1027
            mainloop.run()
 
1028
        except KeyboardInterrupt:
 
1029
            self.Quit(None)
 
1030
 
 
1031
    @inline_callbacks
 
1032
    def _create_trans(self, role, action, sender, packages=None, kwargs=None):
 
1033
        """Helper method which returns the tid of a new transaction."""
 
1034
        # UBUNTU-HACK: Check silently if changing repositories has been granted
 
1035
        #              before to reduce clicks to install packages from
 
1036
        #              partner repository.
 
1037
        #              AddRepository->(AddKey)->UpdateCache->InstallPackages
 
1038
        if role in [enums.ROLE_UPDATE_CACHE, 
 
1039
                    enums.ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER,
 
1040
                    enums.ROLE_INSTALL_PACKAGES]:
 
1041
            alt_action = policykit1.PK_ACTION_CHANGE_REPOSITORY
 
1042
            flags = policykit1.CHECK_AUTH_NONE
 
1043
            try:
 
1044
                yield policykit1.check_authorization_by_name(sender, alt_action,
 
1045
                                                             flags=flags)
 
1046
            except errors.NotAuthorizedError:
 
1047
                yield policykit1.check_authorization_by_name(sender, action)
 
1048
        else:
 
1049
            yield policykit1.check_authorization_by_name(sender, action)
 
1050
        uid = yield policykit1.get_uid_from_dbus_name(sender)
 
1051
        trans = Transaction(role, self.queue, uid, sender, packages=packages,
 
1052
                            kwargs=kwargs)
 
1053
        return_value(trans.tid)
 
1054
 
 
1055
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1056
                          in_signature="", out_signature="s",
 
1057
                          sender_keyword="sender")
 
1058
    def FixIncompleteInstall(self, sender):
 
1059
        """Return the id of a newly create transaction which will try to
 
1060
        fix incomplete installations by running dpkg --configure -a.
 
1061
 
 
1062
        Keyword argument:
 
1063
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1064
        """
 
1065
        log.info("FixIncompleteInstall() called")
 
1066
        action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
 
1067
        return self._create_trans(enums.ROLE_FIX_INCOMPLETE_INSTALL,
 
1068
                                  action, sender)
 
1069
 
 
1070
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1071
                          in_signature="", out_signature="s",
 
1072
                          sender_keyword="sender")
 
1073
    def FixBrokenDepends(self, sender):
 
1074
        """Return the id of a newly create transaction which will try to
 
1075
        fix broken dependencies.
 
1076
 
 
1077
        Keyword argument:
 
1078
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1079
        """
 
1080
        log.info("FixBrokenDepends() called")
 
1081
        action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
 
1082
        return self._create_trans(enums.ROLE_FIX_BROKEN_DEPENDS,
 
1083
                                  action, sender)
 
1084
 
 
1085
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1086
                          in_signature="", out_signature="s",
 
1087
                          sender_keyword="sender")
 
1088
    def UpdateCache(self, sender):
 
1089
        """Return the id of a newly create transaction which will update
 
1090
        the package cache.
 
1091
 
 
1092
        Download the latest information about available packages from the
 
1093
        repositories.
 
1094
 
 
1095
        Keyword argument:
 
1096
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1097
        """
 
1098
        log.info("UpdateCache() was called")
 
1099
        return self._create_trans(enums.ROLE_UPDATE_CACHE,
 
1100
                                  policykit1.PK_ACTION_UPDATE_CACHE,
 
1101
                                  sender)
 
1102
 
 
1103
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1104
                          in_signature="as", out_signature="s",
 
1105
                          sender_keyword="sender")
 
1106
    def RemovePackages(self, package_names, sender):
 
1107
        """Return the id of a newly create transaction which will remove
 
1108
        the given packages.
 
1109
 
 
1110
        Keyword arguments:
 
1111
        package_names -- list of package names to remove
 
1112
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1113
        """
 
1114
        log.info("RemovePackages() was called: '%s'", package_names)
 
1115
        action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
 
1116
        return self._create_trans(enums.ROLE_REMOVE_PACKAGES,
 
1117
                                  action, sender,
 
1118
                                  packages=([], [], package_names, [], []))
 
1119
 
 
1120
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1121
                          in_signature="b", out_signature="s",
 
1122
                          sender_keyword="sender")
 
1123
    def UpgradeSystem(self, safe_mode, sender):
 
1124
        """Upgrade the whole system.
 
1125
 
 
1126
        If in safe mode only already installed packages will be updated.
 
1127
        Updates which require to remove installed packages or to install
 
1128
        additional packages will be skipped.
 
1129
 
 
1130
        Keyword arguments:
 
1131
        safe_mode -- boolean value if the safe mode should be used
 
1132
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1133
        """
 
1134
        log.info("UpgradeSystem() was called with safe mode: "
 
1135
                 "%s" % safe_mode)
 
1136
        return self._create_trans(enums.ROLE_UPGRADE_SYSTEM,
 
1137
                                  policykit1.PK_ACTION_UPGRADE_PACKAGES,
 
1138
                                  sender,
 
1139
                                  kwargs={"safe_mode": safe_mode})
 
1140
 
 
1141
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1142
                          in_signature="asasasasas", out_signature="s",
 
1143
                          sender_keyword="sender")
 
1144
    def CommitPackages(self, install, reinstall, remove, purge, upgrade,
 
1145
                       sender):
 
1146
        """Perform a complex package operation.
 
1147
 
 
1148
        Keyword arguments:
 
1149
        install -- package names to be installed
 
1150
        reinstall -- packages names to be reinstalled
 
1151
        remove -- package names to be removed
 
1152
        purge -- package names to be removed including configuration
 
1153
        upgrade -- package names to be upgraded
 
1154
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1155
        """
 
1156
        #FIXME: take sha1 or md5 cash into accout to allow selecting a version
 
1157
        #       or an origin different from the candidate
 
1158
        log.info("CommitPackages() was called: %s, %s, %s, %s, %s",
 
1159
                 install, reinstall, remove, purge, upgrade)
 
1160
        return self._commit_packages(install, reinstall, remove, purge, upgrade,
 
1161
                                     sender)
 
1162
 
 
1163
    @inline_callbacks
 
1164
    def _commit_packages(self, install, reinstall, remove, purge, upgrade,
 
1165
                         sender):
 
1166
        def check_empty_list(lst):
 
1167
            if lst == [""]:
 
1168
                return []
 
1169
            else:
 
1170
                return lst
 
1171
        packages = [check_empty_list(lst) for lst in [install, reinstall,
 
1172
                                                      remove, purge, upgrade]]
 
1173
        if install != [""] or reinstall != [""] or \
 
1174
           remove !=  [""] or purge != [""]:
 
1175
            action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
 
1176
            yield policykit1.check_authorization_by_name(sender, action)
 
1177
        elif upgrade != [""]:
 
1178
            action = policykit1.PK_ACTION_UPGRADE_PACKAGES
 
1179
            yield policykit1.check_authorization_by_name(sender, action)
 
1180
        uid = yield policykit1.get_uid_from_dbus_name(sender)
 
1181
        trans = Transaction(enums.ROLE_COMMIT_PACKAGES, self.queue, uid,
 
1182
                            sender, packages=packages)
 
1183
        return_value(trans.tid)
 
1184
 
 
1185
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1186
                          in_signature="as", out_signature="s",
 
1187
                          sender_keyword="sender")
 
1188
    def InstallPackages(self, package_names, sender):
 
1189
        """Install the given packages.
 
1190
 
 
1191
        Keyword arguments:
 
1192
        package_names -- list of package names to install
 
1193
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1194
        """
 
1195
        log.info("InstallPackages() was called: %s" % package_names)
 
1196
        action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
 
1197
        return self._create_trans(enums.ROLE_INSTALL_PACKAGES,
 
1198
                                  action, sender,
 
1199
                                  packages=(package_names, [], [], [], []))
 
1200
 
 
1201
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1202
                         in_signature="as", out_signature="s",
 
1203
                         sender_keyword="sender")
 
1204
    def UpgradePackages(self, package_names, sender):
 
1205
        """Upgrade the given packages to their latest version.
 
1206
 
 
1207
        Keyword arguments:
 
1208
        package_names -- list of package names to upgrade
 
1209
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1210
        """
 
1211
        log.info("UpgradePackages() was called: %s" % package_names)
 
1212
        return self._create_trans(enums.ROLE_UPGRADE_PACKAGES,
 
1213
                                  policykit1.PK_ACTION_UPGRADE_PACKAGES,
 
1214
                                  sender,
 
1215
                                  packages=([], [], [], [], package_names))
 
1216
 
 
1217
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1218
                          in_signature="ss", out_signature="s",
 
1219
                          sender_keyword="sender")
 
1220
    def AddVendorKeyFromKeyserver(self, keyid, keyserver, sender):
 
1221
        """Download and install the given keyid via keyserver
 
1222
 
 
1223
        Keyword arguments:
 
1224
        keyid - the keyid of the key (e.g. 0x0EB12F05)
 
1225
        keyserver - the keyserver (e.g. keyserver.ubuntu.com)
 
1226
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1227
        """
 
1228
        #FIXME: Should not be a transaction
 
1229
        log.info("InstallVendorKeyFromKeyserver() was called: %s %s" % (keyid, keyserver))
 
1230
        return self._create_trans(enums.ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER,
 
1231
                                  policykit1.PK_ACTION_CHANGE_REPOSITORY,
 
1232
                                  sender,
 
1233
                                  kwargs={"keyid": keyid,
 
1234
                                          "keyserver": keyserver})
 
1235
 
 
1236
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1237
                          in_signature="s", out_signature="s",
 
1238
                          sender_keyword="sender")
 
1239
    def AddVendorKeyFromFile(self, path, sender):
 
1240
        """Install the given key file.
 
1241
 
 
1242
        Keyword arguments:
 
1243
        path -- the absolute path to the key file
 
1244
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1245
        """
 
1246
        #FIXME: Should not be a transaction
 
1247
        log.info("InstallVendorKeyFile() was called: %s" % path)
 
1248
        return self._create_trans(enums.ROLE_ADD_VENDOR_KEY_FILE,
 
1249
                                  policykit1.PK_ACTION_CHANGE_REPOSITORY,
 
1250
                                  sender,
 
1251
                                  kwargs={"path": path})
 
1252
 
 
1253
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1254
                          in_signature="s", out_signature="s",
 
1255
                          sender_keyword="sender")
 
1256
    def RemoveVendorKey(self, fingerprint, sender):
 
1257
        """Remove the given key.
 
1258
 
 
1259
        Keyword arguments:
 
1260
        fingerprint -- the fingerprint of the key to remove
 
1261
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1262
        """
 
1263
        #FIXME: Should not be a transaction
 
1264
        log.info("RemoveVendorKey() was called: %s" % fingerprint)
 
1265
        return self._create_trans(enums.ROLE_REMOVE_VENDOR_KEY,
 
1266
                                  policykit1.PK_ACTION_CHANGE_REPOSITORY,
 
1267
                                  sender,
 
1268
                                  kwargs={"fingerprint": fingerprint})
 
1269
 
 
1270
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1271
                          in_signature="s", out_signature="s",
 
1272
                          sender_keyword="sender")
 
1273
    def InstallFile(self, path, sender):
 
1274
        """Install the given package file.
 
1275
 
 
1276
        Keyword arguments:
 
1277
        path -- the absolute path to the package file
 
1278
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1279
        """
 
1280
        log.info("InstallFile() was called: %s" % path)
 
1281
        #FIXME: Perform some checks
 
1282
        #FIXME: Should we already extract the package name here?
 
1283
        return self._create_trans(enums.ROLE_INSTALL_FILE,
 
1284
                                  policykit1.PK_ACTION_INSTALL_FILE,
 
1285
                                  sender,
 
1286
                                  kwargs={"path": path})
 
1287
 
 
1288
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1289
                          in_signature="sssasss", out_signature="",
 
1290
                          sender_keyword="sender")
 
1291
    def AddRepository(self, type, uri, dist, comps, comment, sourcesfile,
 
1292
                      sender):
 
1293
        """Add given repository to the sources list.
 
1294
 
 
1295
        Keyword arguments:
 
1296
        type -- the type of the entry (deb, deb-src)
 
1297
        uri -- the main repository uri (e.g. http://archive.ubuntu.com/ubuntu)
 
1298
        dist -- the distribution to use (e.g. karmic, "/")
 
1299
        comps -- a (possible empty) list of components (main, restricted)
 
1300
        comment -- an (optional) comment
 
1301
        sourcesfile -- an (optinal) filename in sources.list.d 
 
1302
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1303
        """
 
1304
        log.info("AddRepository() was called: type='%s' uri='%s' "
 
1305
                 "dist='%s' comps='%s' comment='%s' sourcefile='%s'",
 
1306
                 type, uri, dist, comps, comment, sourcesfile)
 
1307
        return self._add_repository(type, uri, dist, comps, comment,
 
1308
                                    sourcesfile, sender)
 
1309
 
 
1310
    @inline_callbacks
 
1311
    def _add_repository(self, type, uri, dist, comps, comment, sourcesfile,
 
1312
                        sender):
 
1313
        if sourcesfile:
 
1314
            if not sourcesfile.endswith(".list"):
 
1315
                sourcesfile += ".list"
 
1316
            d = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
 
1317
            sourcesfile = os.path.join(d, os.path.basename(sourcesfile))
 
1318
        else:
 
1319
            sourcesfile = None
 
1320
        action = policykit1.PK_ACTION_CHANGE_REPOSITORY
 
1321
        yield policykit1.check_authorization_by_name(sender, action)
 
1322
        old_umask = os.umask(0022)
 
1323
        try:
 
1324
            sources = SourcesList()
 
1325
            entry = sources.add(type, uri, dist, comps, comment,
 
1326
                                file=sourcesfile)
 
1327
            if entry.invalid:
 
1328
                raise errors.RepositoryInvalidError()
 
1329
        #FIXME: Should be removed after requiring Python 2.6
 
1330
        except Exception, error:
 
1331
            raise error
 
1332
        else:
 
1333
            sources.save()
 
1334
        finally:
 
1335
            os.umask(old_umask)
 
1336
 
 
1337
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1338
                          in_signature="s", out_signature="",
 
1339
                          sender_keyword="sender")
 
1340
    def EnableDistroComponent(self, component, sender):
 
1341
        """Enable given component in the sources list.
 
1342
 
 
1343
        Keyword arguments:
 
1344
        component -- a components, e.g. main or universe
 
1345
        sender -- the unique D-Bus name of the sender (provided by D-Bus)
 
1346
        """
 
1347
        log.info("EnableComponent() was called: component='%s' ", component)
 
1348
        return self._enable_distro_component(component, sender)
 
1349
 
 
1350
    @inline_callbacks
 
1351
    def _enable_distro_component(self, component, sender):
 
1352
        action = policykit1.PK_ACTION_CHANGE_REPOSITORY
 
1353
        yield policykit1.check_authorization_by_name(sender, action)
 
1354
        old_umask = os.umask(0022)
 
1355
        try:
 
1356
            sourceslist = SourcesList()
 
1357
            distro = aptsources.distro.get_distro()
 
1358
            distro.get_sources(sourceslist)
 
1359
            distro.enable_component(component)
 
1360
            sourceslist.save()
 
1361
        finally:
 
1362
            os.umask(old_umask)
 
1363
 
 
1364
    @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
 
1365
                          in_signature="", out_signature="as",
 
1366
                          sender_keyword="sender")
 
1367
    def GetTrustedVendorKeys(self, sender):
 
1368
        """Return a list of installed repository keys."""
 
1369
        log.info("GetTrustedVendorKeys() was called")
 
1370
        return self._get_trusted_vendor_keys(sender)
 
1371
 
 
1372
    @inline_callbacks
 
1373
    def _get_trusted_vendor_keys(self, sender):
 
1374
        aptauth = AptAuth()
 
1375
        action = policykit1.PK_ACTION_GET_TRUSTED_VENDOR_KEYS
 
1376
        yield policykit1.check_authorization_by_name(sender, action)
 
1377
        return_value([key.decode("utf-8", "ignore") for key in aptauth.list()])
 
1378
 
 
1379
    @dbus.service.method(APTDAEMON_DBUS_INTERFACE,
 
1380
                         in_signature="", out_signature="sas")
 
1381
    def GetActiveTransactions(self):
 
1382
        """Return the currently running transaction and the list of queued
 
1383
        transactions.
 
1384
        """
 
1385
        log.debug("GetActiveTransactions() was called")
 
1386
        queued = [trans.tid for trans in self.queue.items]
 
1387
        if self.queue.worker.trans:
 
1388
            current = self.queue.worker.trans.tid
 
1389
        else:
 
1390
            current = ""
 
1391
        return current, queued
 
1392
 
 
1393
    @dbus.service.method(APTDAEMON_DBUS_INTERFACE,
 
1394
                         in_signature="", out_signature="",
 
1395
                         sender_keyword="caller_name")
 
1396
    def Quit(self, caller_name):
 
1397
        """Request a shutdown of the daemon.
 
1398
 
 
1399
        Keyword argument:
 
1400
        caller_name -- the D-Bus name of the caller (provided by D-Bus)
 
1401
        """
 
1402
        log.info("Shutdown was requested")
 
1403
        log.debug("Quitting main loop...")
 
1404
        mainloop.quit()
 
1405
        log.debug("Exit")
 
1406
 
 
1407
    def _sigquit(self, signum, frame):
 
1408
        """Internal callback for the quit signal."""
 
1409
        self.Quit(None)
 
1410
 
 
1411
    def _check_for_inactivity(self):
 
1412
        """Shutdown the daemon if it has been inactive for time specified
 
1413
        in APTDAEMON_IDLE_TIMEOUT.
 
1414
        """
 
1415
        log.debug("Checking for inactivity")
 
1416
        timestamp = self.queue.worker.last_action_timestamp
 
1417
        if not self.queue.worker.trans and \
 
1418
           not gobject.main_context_default().pending() and \
 
1419
           time.time() - timestamp > APTDAEMON_IDLE_TIMEOUT and \
 
1420
           not self.queue:
 
1421
            log.info("Quiting due to inactivity")
 
1422
            self.Quit(None)
 
1423
            return False
 
1424
        return True
 
1425
 
 
1426
 
 
1427
def main():
 
1428
    """Allow to run the daemon from the command line."""
 
1429
    parser = OptionParser()
 
1430
    #FIXME: Workaround a bug in optparser which doesn't handle unicode/str
 
1431
    #       correctly, see http://bugs.python.org/issue4391
 
1432
    #       Shoudl be resolved by Python3
 
1433
    enc = locale.getpreferredencoding()
 
1434
    parser.add_option("-t", "--disable-timeout",
 
1435
                      default=False,
 
1436
                      action="store_true", dest="disable_timeout",
 
1437
                      help=_("Do not shutdown the daemon because of "
 
1438
                             "inactivity").decode(enc))
 
1439
    parser.add_option("-d", "--debug",
 
1440
                      default=False,
 
1441
                      action="store_true", dest="debug",
 
1442
                      help=_("Show internal processing "
 
1443
                             "information").decode(enc))
 
1444
    parser.add_option("-r", "--replace",
 
1445
                      default=False,
 
1446
                      action="store_true", dest="replace",
 
1447
                      help=_("Quit and replace an already running "
 
1448
                             "daemon").decode(enc))
 
1449
    parser.add_option("-p", "--profile",
 
1450
                      default=False,
 
1451
                      action="store", type="string", dest="profile",
 
1452
                      help=_("Store profile stats in the specified "
 
1453
                             "file").decode(enc))
 
1454
    parser.add_option("--dummy",
 
1455
                      default=False,
 
1456
                      action="store_true", dest="dummy",
 
1457
                      help=_("Do not make any changes to the system (Only "
 
1458
                             "of use to developers)").decode(enc))
 
1459
    options, args = parser.parse_args()
 
1460
    if options.debug == True:
 
1461
        log.setLevel(logging.DEBUG)
 
1462
    else:
 
1463
        log.setLevel(logging.INFO)
 
1464
        _console_handler.setLevel(logging.INFO)
 
1465
    daemon = AptDaemon(options)
 
1466
    if options.profile:
 
1467
        import profile
 
1468
        profiler = profile.Profile()
 
1469
        profiler.runcall(daemon.run)
 
1470
        profiler.dump_stats(options.profile)
 
1471
        profiler.print_stats()
 
1472
    else:
 
1473
        daemon.run()
 
1474
 
 
1475
# vim:ts=4:sw=4:et