2
# -*- coding: utf-8 -*-
4
Core components of aptdaemon.
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
11
The main function allows to run the daemon as a command.
13
# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>
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
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.
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.
29
__author__ = "Sebastian Heinlein <devel@glatzor.de>"
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")
38
from functools import wraps
40
from gettext import gettext as _
43
import logging.handlers
44
from optparse import OptionParser
55
import dbus.exceptions
57
import dbus.mainloop.glib
59
from softwareproperties.AptAuth import AptAuth
60
from aptsources.sourceslist import SourcesList
63
import aptsources.distro
67
from defer import dbus_deferred_method, Deferred, inline_callbacks, return_value
69
from worker import AptWorker, DummyWorker
70
from loop import mainloop
72
# Required for translations from APT
73
locale.setlocale(locale.LC_ALL, "")
76
gettext.textdomain("aptdaemon")
78
APTDAEMON_DBUS_INTERFACE = 'org.debian.apt'
79
APTDAEMON_DBUS_PATH = '/org/debian/apt'
80
APTDAEMON_DBUS_SERVICE = 'org.debian.apt'
82
APTDAEMON_TRANSACTION_DBUS_INTERFACE = 'org.debian.apt.transaction'
84
APTDAEMON_IDLE_CHECK_INTERVAL = 60
85
APTDAEMON_IDLE_TIMEOUT = 5 * 60
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
91
TRANSACTION_DEL_TIMEOUT = 5
93
# Setup the DBus main loop
94
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
96
# Required for daemon mode
98
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
100
# Setup logging to syslog and the console
101
log = logging.getLogger("AptDaemon")
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: "
108
_syslog_handler.setFormatter(_syslog_formatter)
112
log.addHandler(_syslog_handler)
113
_console_handler = logging.StreamHandler()
114
_console_formatter = logging.Formatter("%(asctime)s %(name)s [%(levelname)s]: "
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")
123
class Transaction(dbus.service.Object):
125
"""Represents a transaction on the D-Bus.
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
133
def __init__(self, role, queue, uid, sender, connect=True, bus=None,
134
packages=None, kwargs=None):
135
"""Initialize a new Transaction instance.
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)
144
id = uuid.uuid4().get_hex()
145
self.tid = "/org/debian/apt/transaction/%s" % id
148
bus = dbus.SystemBus()
149
bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE, bus)
155
dbus.service.Object.__init__(self, bus_name, dbus_path)
157
packages = [[], [], [], [], []]
163
self.allow_unauthenticated = False
164
self.remove_obsoleted_depends = False
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
184
self._meta_data = dbus.Dictionary(signature="ss")
185
self._dpkg_status = None
188
self._depends = [dbus.Array([], signature=dbus.Signature('s')) \
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
199
self._sender_watch = bus.watch_name_owner(sender,
200
self._sender_owner_changed)
202
self._sender_watch = None
204
def _sender_owner_changed(self, connection):
205
"""Callback if the owner of the original sender changed, e.g.
208
self.sender_alive = False
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.
214
log_trans.debug("Removing transaction")
216
self.remove_from_connection()
217
except LookupError, error:
218
log_trans.debug("remove_from_connection() raised LookupError: "
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 "
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)
244
def _get_meta_data(self):
245
return self._meta_data
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.")
251
def _set_role(self, enum):
252
if self._role != enums.ROLE_UNSET:
253
raise errors.TransactionRoleAlreadySet()
254
self.PropertyChanged("Role", enum)
260
role = property(_get_role, _set_role, doc="Operation type of transaction.")
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)
267
def _get_progress_details(self):
268
return self._progress_details
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 "
276
def _set_error(self, excep):
278
self.PropertyChanged("Error", (excep.code, excep.details))
280
def _get_error(self):
283
error = property(_get_error, _set_error, doc="Raised exception.")
285
def _set_exit(self, enum):
286
self.status = enums.STATUS_FINISHED
288
self.PropertyChanged("ExitState", 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)
300
exit = property(_get_exit, _set_exit,
301
doc="The exit state of the transaction.")
303
def _get_download(self):
304
return self._download
306
def _set_download(self, size):
307
self.PropertyChanged("Download", size)
308
self._download = size
310
download = property(_get_download, _set_download,
311
doc="The download size of the transaction.")
313
def _get_space(self):
316
def _set_space(self, size):
317
self.PropertyChanged("Space", size)
320
space = property(_get_space, _set_space,
321
doc="The required disk space of the transaction.")
323
def _get_packages(self):
324
return self._packages
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)
331
packages = property(_get_packages, _set_packages,
332
doc="Packages which will be explictly install, "
333
"upgraded, removed, purged or reinstalled.")
335
def _get_depends(self):
338
def _set_depends(self, depends):
339
self._depends = [dbus.Array(deps, signature=dbus.Signature('s')) \
341
self.PropertyChanged("Dependencies", self._depends)
343
depends = property(_get_depends, _set_depends,
344
doc="The additional dependencies: installs, removals, "
345
"upgrades and downgrades.")
347
def _get_status(self):
350
def _set_status(self, enum):
351
self.PropertyChanged("Status", enum)
354
status = property(_get_status, _set_status,
355
doc="The status of the transaction.")
357
def _get_status_details(self):
358
return self._status_details
360
def _set_status_details(self, text):
361
self._status_details = text
362
self.PropertyChanged("StatusDetails", text)
364
status_details = property(_get_status_details, _set_status_details,
365
doc="The status message from apt.")
367
def _get_progress(self):
368
return self._progress
370
def _set_progress(self, percent):
371
self._progress = percent
372
self.PropertyChanged("Progress", percent)
374
progress = property(_get_progress, _set_progress,
375
doc="The progress of the transaction in percent.")
377
def _get_progress_download(self):
378
return self._progress_download
380
def _set_progress_download(self, progress_download):
381
self._progress_download = progress_download
382
self.PropertyChanged("ProgressDownload", progress_download)
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 "
392
def _get_cancellable(self):
393
return self._cancellable
395
def _set_cancellable(self, cancellable):
396
self._cancellable = cancellable
397
self.PropertyChanged("Cancellable", cancellable)
399
cancellable = property(_get_cancellable, _set_cancellable,
400
doc="If it's currently allowed to cancel the "
403
def _get_term_attached(self):
404
return self._term_attached
406
def _set_term_attached(self, attached):
407
self._term_attached = attached
408
self.PropertyChanged("TerminalAttached", attached)
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 "
415
def _get_required_medium(self):
416
return self._required_medium
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)
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 "
429
def _get_config_file_conflict(self):
430
return self._config_file_conflict
432
def _set_config_file_conflict(self, prompt):
434
self._config_file_conflict = None
437
self._config_file_conflict = prompt
438
self.PropertyChanged("ConfigFileConflict", prompt)
439
self.ConfigFileConflict(old, new)
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")
448
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
450
def PropertyChanged(self, property, value):
451
"""Set and emit if a property of the transaction changed.
454
property -- the name of the property
455
value -- the new value of the property
457
log_trans.debug("Emitting PropertyChanged: %s, %s" % (property, value))
459
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
461
def Finished(self, enum):
462
"""Mark and emit the transaction as finished.
465
enum -- the exit state of the transaction, e.g. EXIT_FAILED
467
log_trans.debug("Emitting Finished: %s" % \
468
enums.get_exit_string_from_enum(exit))
470
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
472
def MediumRequired(self, medium, drive):
473
"""Set and emit the required medium change.
475
This method/signal should be used to inform the user to
476
insert the installation CD/DVD:
479
medium -- the CD/DVD label
480
drive -- mount point of the drive
482
log_trans.debug("Emitting MediumRequired: %s, %s" % (medium, drive))
484
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
486
def ConfigFileConflict(self, old, new):
487
"""Set and emit the ConfigFileConflict signal.
489
This method/signal should be used to inform the user to
490
answer a config file prompt.
493
old -- current version of the configuration prompt
494
new -- new version of the configuration prompt
496
log_trans.debug("Emitting ConfigFileConflict: %s, %s" % (old, new))
500
def _set_locale(self, locale_str):
501
"""Set the language and encoding.
504
locale -- specifies language, territory and encoding according
505
to RFC 1766, e.g. "de_DE.UTF-8"
508
if self.status != enums.STATUS_SETTING_UP:
509
raise errors.TransactionAlreadyRunning()
511
(lang, encoding) = locale._parse_localename(locale_str)
515
self.locale = "%s.%s" % (lang, encoding)
516
self.PropertyChanged("locale", self.locale)
519
def _set_http_proxy(self, url, sender):
520
"""Set an http network proxy.
523
url -- the URL of the proxy server, e.g. http://proxy:8080
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)
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.
537
remove_obsoleted_depends -- If True also remove automatically installed
538
dependencies of to removed packages
540
self.remove_obsoleted_depends = remove_obsoleted_depends
541
self.PropertyChanged("RemoveObsoletedDepends",
542
self.remove_obsoleted_depends)
544
def _set_allow_unauthenticated(self, allow_unauthenticated):
545
"""Set the handling of unauthenticated packages
548
allow_unauthenticated -- True to allow packages that come from a
549
repository without a valid authentication signature
551
self.allow_unauthenticated = allow_unauthenticated
552
self.PropertyChanged("AllowUnauthenticated", self.allow_unauthenticated)
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)
563
def _run(self, sender):
564
yield self._check_foreign_user(sender)
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.
574
sender -- the unique D-Bus name of the sender (provided by D-Bus)
576
log_trans.info("Cancelling transaction %s", self.tid)
577
return self._cancel(sender)
580
def _cancel(self, sender):
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)
587
self.queue.remove(self)
588
log_trans.debug("Removed transaction from queue")
592
self.status = enums.STATUS_CANCELLING
593
self.exit = enums.EXIT_CANCELLED
596
log_trans.debug("Setting cancel event")
597
self.cancelled = True
598
self.status = enums.STATUS_CANCELLING
601
raise errors.AptDaemonError("Could not cancel transaction")
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.
610
Call this method if you want to show changes before queuing the
613
log_trans.info("Simulate was called")
614
return self._simulate(sender)
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
627
def _set_terminal(self, ttyname):
628
"""Set the controlling terminal.
630
The worker will be attached to the specified slave end of a pty
631
master/slave pair. This allows to interact with the
633
Can only be changed before the transaction is started.
636
ttyname -- file path to the slave file descriptor
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: "
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))
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)
653
raise errors.AptDaemonError("%s isn't a tty" % ttyname)
657
def _set_debconf(self, debconf_socket):
658
"""Set the socket of the debconf proxy.
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.
664
Can only be changed before the transaction is started.
667
debconf_socket: absolute path to the socket
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)
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.
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
691
medium -- the label of the CD/DVD
692
sender -- the unique D-Bus name of the sender (provided by D-Bus)
694
log_trans.info("Medium %s was provided", medium)
695
return self._provide_medium(medium, sender)
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)
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.
713
If a config file prompt is detected the transaction will be
714
paused and could be resumed with this method.
717
config -- the path to the original config file
718
answer -- the answer to the configuration file question, can be
720
sender -- the unique D-Bus name of the sender (provided by D-Bus)
722
log_trans.info("Resolved conflict of %s with %s", config, answer)
723
return self._resolve_config_file_conflict(config, answer, sender)
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 "
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
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):
747
Only the user who intiaited the transaction is
748
allowed to modify it.
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)
756
log.debug("Set() was called: %s, %s" % (property, value))
757
return self._set(iface, property, value, sender)
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)
778
raise dbus.exceptions.DBusException("Unknown or read only "
779
"property: %s" % property)
781
raise dbus.exceptions.DBusException("Unknown interface: %s" % iface)
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)
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
796
log.debug("Get() was called: %s, %s" % (iface, property))
797
return self._get_properties(iface)[property]
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,
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,
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)
836
raise errors.ForeignTransaction()
838
def _set_kwargs(self, kwargs):
839
"""Set the kwargs which will be send to the AptWorker."""
843
class TransactionQueue(gobject.GObject):
845
"""Queue for transactions."""
847
__gsignals__ = {"queue-changed":(gobject.SIGNAL_RUN_FIRST,
850
"future-status-changed":(gobject.SIGNAL_RUN_FIRST,
854
def __init__(self, dummy):
855
"""Intialize a new TransactionQueue instance."""
856
gobject.GObject.__init__(self)
857
self._queue = collections.deque()
860
self.worker = DummyWorker()
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)
868
return len(self._queue)
870
def _emit_queue_changed(self):
871
"""Emit the queued-changed signal."""
872
log.debug("emitting queue changed")
873
self.emit("queue-changed")
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
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()
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)
904
self.worker.run(trans)
905
self._emit_queue_changed()
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
913
next = self._queue.popleft()
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()
924
self.worker.run(next)
925
self._emit_queue_changed()
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()
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()
942
"""Return a list containing all queued items."""
943
return list(self._queue)
946
class AptDaemon(dbus.service.Object):
948
"""Provides a system daemon to process package management tasks.
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.
955
def __init__(self, options, connect=True, bus=None):
956
"""Initialize a new AptDaemon instance.
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)
963
log.info("Initializing daemon")
964
signal.signal(signal.SIGQUIT, self._sigquit)
965
signal.signal(signal.SIGTERM, self._sigquit)
966
self.options = options
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
973
bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE,
976
except dbus.exceptions.NameExistsException:
977
if self.options.replace == False:
978
log.critical("Another daemon is already running")
980
log.warn("Replacing already running daemon")
981
the_other_guy = bus.get_object(APTDAEMON_DBUS_SERVICE,
983
the_other_guy.Quit(dbus_interface=APTDAEMON_DBUS_INTERFACE,
986
bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE,
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")
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
1003
queued = [trans.tid for trans in self.queue.items]
1004
self.ActiveTransactionsChanged(current, queued)
1006
@dbus.service.signal(dbus_interface=APTDAEMON_DBUS_INTERFACE,
1008
def ActiveTransactionsChanged(self, current, queued):
1009
"""Emit the ActiveTransactionsChanged signal.
1012
current -- the tid of the currently running transaction or an empty
1014
queued -- list of the ids of the queued transactions
1016
log.debug("Emitting ActiveTransactionsChanged signal: %s, %s",
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")
1028
except KeyboardInterrupt:
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
1044
yield policykit1.check_authorization_by_name(sender, alt_action,
1046
except errors.NotAuthorizedError:
1047
yield policykit1.check_authorization_by_name(sender, action)
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,
1053
return_value(trans.tid)
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.
1063
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1065
log.info("FixIncompleteInstall() called")
1066
action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
1067
return self._create_trans(enums.ROLE_FIX_INCOMPLETE_INSTALL,
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.
1078
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1080
log.info("FixBrokenDepends() called")
1081
action = policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES
1082
return self._create_trans(enums.ROLE_FIX_BROKEN_DEPENDS,
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
1092
Download the latest information about available packages from the
1096
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1098
log.info("UpdateCache() was called")
1099
return self._create_trans(enums.ROLE_UPDATE_CACHE,
1100
policykit1.PK_ACTION_UPDATE_CACHE,
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
1111
package_names -- list of package names to remove
1112
sender -- the unique D-Bus name of the sender (provided by D-Bus)
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,
1118
packages=([], [], package_names, [], []))
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.
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.
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)
1134
log.info("UpgradeSystem() was called with safe mode: "
1136
return self._create_trans(enums.ROLE_UPGRADE_SYSTEM,
1137
policykit1.PK_ACTION_UPGRADE_PACKAGES,
1139
kwargs={"safe_mode": safe_mode})
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,
1146
"""Perform a complex package operation.
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)
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,
1164
def _commit_packages(self, install, reinstall, remove, purge, upgrade,
1166
def check_empty_list(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)
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.
1192
package_names -- list of package names to install
1193
sender -- the unique D-Bus name of the sender (provided by D-Bus)
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,
1199
packages=(package_names, [], [], [], []))
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.
1208
package_names -- list of package names to upgrade
1209
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1211
log.info("UpgradePackages() was called: %s" % package_names)
1212
return self._create_trans(enums.ROLE_UPGRADE_PACKAGES,
1213
policykit1.PK_ACTION_UPGRADE_PACKAGES,
1215
packages=([], [], [], [], package_names))
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
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)
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,
1233
kwargs={"keyid": keyid,
1234
"keyserver": keyserver})
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.
1243
path -- the absolute path to the key file
1244
sender -- the unique D-Bus name of the sender (provided by D-Bus)
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,
1251
kwargs={"path": path})
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.
1260
fingerprint -- the fingerprint of the key to remove
1261
sender -- the unique D-Bus name of the sender (provided by D-Bus)
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,
1268
kwargs={"fingerprint": fingerprint})
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.
1277
path -- the absolute path to the package file
1278
sender -- the unique D-Bus name of the sender (provided by D-Bus)
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,
1286
kwargs={"path": path})
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,
1293
"""Add given repository to the sources list.
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)
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)
1311
def _add_repository(self, type, uri, dist, comps, comment, 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))
1320
action = policykit1.PK_ACTION_CHANGE_REPOSITORY
1321
yield policykit1.check_authorization_by_name(sender, action)
1322
old_umask = os.umask(0022)
1324
sources = SourcesList()
1325
entry = sources.add(type, uri, dist, comps, comment,
1328
raise errors.RepositoryInvalidError()
1329
#FIXME: Should be removed after requiring Python 2.6
1330
except Exception, error:
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.
1344
component -- a components, e.g. main or universe
1345
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1347
log.info("EnableComponent() was called: component='%s' ", component)
1348
return self._enable_distro_component(component, sender)
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)
1356
sourceslist = SourcesList()
1357
distro = aptsources.distro.get_distro()
1358
distro.get_sources(sourceslist)
1359
distro.enable_component(component)
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)
1373
def _get_trusted_vendor_keys(self, sender):
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()])
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
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
1391
return current, queued
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.
1400
caller_name -- the D-Bus name of the caller (provided by D-Bus)
1402
log.info("Shutdown was requested")
1403
log.debug("Quitting main loop...")
1407
def _sigquit(self, signum, frame):
1408
"""Internal callback for the quit signal."""
1411
def _check_for_inactivity(self):
1412
"""Shutdown the daemon if it has been inactive for time specified
1413
in APTDAEMON_IDLE_TIMEOUT.
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 \
1421
log.info("Quiting due to inactivity")
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",
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",
1441
action="store_true", dest="debug",
1442
help=_("Show internal processing "
1443
"information").decode(enc))
1444
parser.add_option("-r", "--replace",
1446
action="store_true", dest="replace",
1447
help=_("Quit and replace an already running "
1448
"daemon").decode(enc))
1449
parser.add_option("-p", "--profile",
1451
action="store", type="string", dest="profile",
1452
help=_("Store profile stats in the specified "
1453
"file").decode(enc))
1454
parser.add_option("--dummy",
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)
1463
log.setLevel(logging.INFO)
1464
_console_handler.setLevel(logging.INFO)
1465
daemon = AptDaemon(options)
1468
profiler = profile.Profile()
1469
profiler.runcall(daemon.run)
1470
profiler.dump_stats(options.profile)
1471
profiler.print_stats()