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 _
41
from locale import _parse_localename as parse_localename
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
73
gettext.textdomain("aptdaemon")
75
APTDAEMON_DBUS_INTERFACE = 'org.debian.apt'
76
APTDAEMON_DBUS_PATH = '/org/debian/apt'
77
APTDAEMON_DBUS_SERVICE = 'org.debian.apt'
79
APTDAEMON_TRANSACTION_DBUS_INTERFACE = 'org.debian.apt.transaction'
81
APTDAEMON_IDLE_CHECK_INTERVAL = 60
82
APTDAEMON_IDLE_TIMEOUT = 5 * 60
84
# Maximum allowed time between the creation of a transaction and its queuing
85
TRANSACTION_IDLE_TIMEOUT = 300
86
# Keep the transaction for the given time alive on the bus after it has
88
TRANSACTION_DEL_TIMEOUT = 5
90
# Setup the DBus main loop
91
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
93
# Required for daemon mode
95
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
97
# Setup logging to syslog and the console
98
log = logging.getLogger("AptDaemon")
100
_syslog_handler = logging.handlers.SysLogHandler(address="/dev/log",
101
facility=logging.handlers.SysLogHandler.LOG_DAEMON)
102
_syslog_handler.setLevel(logging.INFO)
103
_syslog_formatter = logging.Formatter("%(name)s: %(levelname)s: "
105
_syslog_handler.setFormatter(_syslog_formatter)
109
log.addHandler(_syslog_handler)
110
_console_handler = logging.StreamHandler()
111
_console_formatter = logging.Formatter("%(asctime)s %(name)s [%(levelname)s]: "
114
_console_handler.setFormatter(_console_formatter)
115
log.addHandler(_console_handler)
116
#FIXME: Use LoggerAdapter (requires Python 2.6)
117
log_trans = logging.getLogger("AptDaemon.Trans")
120
class Transaction(dbus.service.Object):
122
"""Represents a transaction on the D-Bus.
124
A transaction represents a single package management task, e.g. installation
125
or removal of packages. This class allows to expose information and to
126
controll the transaction via DBus using policykit for managing
130
def __init__(self, role, queue, uid, connect=True, bus=None,
131
packages=None, kwargs=None):
132
"""Initialize a new Transaction instance.
135
queue -- TransactionQueue instance of the daemon
136
uid -- the id of the user who created the transaction
137
connect -- if the Transaction should connect to DBus (default is True)
138
bus -- the DBus connection which should be used (defaults to system bus)
140
id = uuid.uuid4().get_hex()
141
self.tid = "/org/debian/apt/transaction/%s" % id
144
bus = dbus.SystemBus()
145
bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE, bus)
150
dbus.service.Object.__init__(self, bus_name, dbus_path)
152
packages = [[], [], [], [], []]
158
self.allow_unauthenticated = False
159
self.remove_obsoleted_depends = False
166
self._progress_details = (0, 0, 0, 0, 0, 0)
167
self._progress_download = ("", "", "", 0, 0, "")
168
self._exit = EXIT_UNFINISHED
169
self._status = STATUS_SETTING_UP
170
self._status_details = ""
171
self._error = (-1, "")
172
self._cancellable = True
173
self._term_attached = False
174
self._required_medium = ("", "")
175
self._config_file_conflict = ("", "")
176
self._config_file_conflict_resolution = ""
177
self.cancelled = False
179
self._meta_data = dbus.Dictionary(signature="ss")
180
self._dpkg_status = None
183
self._depends = [dbus.Array([], signature=dbus.Signature('s')) \
185
self._packages = [pkgs or dbus.Array([], signature=dbus.Signature('s'))\
186
for pkgs in packages]
187
# Add a timeout which removes the transaction from the bus if it
188
# hasn't been setup and run for the TRANSACTION_IDLE_TIMEOUT period
189
self._idle_watch = gobject.timeout_add_seconds(
190
TRANSACTION_IDLE_TIMEOUT, self._remove_from_connection_no_raise)
192
def _remove_from_connection_no_raise(self):
193
"""Version of remove_from_connection that does not raise if the
194
object isn't exported.
196
log_trans.debug("Removing transaction")
198
self.remove_from_connection()
199
except LookupError, error:
200
log_trans.debug("remove_from_connection() raised LookupError: "
203
def _set_meta_data(self, data):
204
# Perform some checks
205
if self.status != STATUS_SETTING_UP:
206
raise errors.TransactionAlreadyRunning()
207
if not isinstance(data, dbus.Dictionary):
208
raise errors.InvalidMetaDataError("The data value has to be a "
209
"dictionary: %s" % data)
210
for key, value in data.iteritems():
211
if key in self._meta_data:
212
raise errors.InvalidMetaDataError("The key %s already "
213
"exists. It is not allowed "
214
"to overwrite existing "
216
if not len(key.split("_")) > 1:
217
raise errors.InvalidMetaDataError("The key %s has to be of the "
218
"format IDENTIFIER-KEYNAME")
219
if not isinstance(value, dbus.String):
220
raise errors.InvalidMetaDataError("The value has to be a "
221
"string: %s" % value)
222
# Merge new data into existing one:
223
self._meta_data.update(data)
224
self.PropertyChanged("MetaData", self._meta_data)
226
def _get_meta_data(self):
227
return self._meta_data
229
meta_data = property(_get_meta_data, _set_meta_data,
230
doc="Allows client applications to store meta data "
231
"for the transaction in a dictionary.")
233
def _set_role(self, enum):
234
if self._role != ROLE_UNSET:
235
raise errors.TransactionRoleAlreadySet()
236
self.PropertyChanged("Role", enum)
242
role = property(_get_role, _set_role, doc="Operation type of transaction.")
244
def _set_progress_details(self, details):
245
items_done, items_total, bytes_done, bytes_total, speed, eta = details
246
self._progress_details = details
247
self.PropertyChanged("ProgressDetails", details)
249
def _get_progress_details(self):
250
return self._progress_details
252
progress_details = property(_get_progress_details, _set_progress_details,
253
doc = "Tuple containing detailed progress "
254
"information: items done, total items, "
255
"bytes done, total bytes, speed and "
258
def _set_error(self, excep):
260
self.PropertyChanged("Error", (excep.code, excep.details))
262
def _get_error(self):
265
error = property(_get_error, _set_error, doc="Raised exception.")
267
def _set_exit(self, enum):
268
self.status = STATUS_FINISHED
270
self.PropertyChanged("ExitState", enum)
272
# Remove the transaction from the Bus after it is complete. A short
273
# timeout helps lazy clients
274
gobject.timeout_add_seconds(TRANSACTION_DEL_TIMEOUT,
275
self._remove_from_connection_no_raise)
280
exit = property(_get_exit, _set_exit,
281
doc="The exit state of the transaction.")
283
def _get_download(self):
284
return self._download
286
def _set_download(self, size):
287
self.PropertyChanged("Download", size)
288
self._download = size
290
download = property(_get_download, _set_download,
291
doc="The download size of the transaction.")
293
def _get_space(self):
296
def _set_space(self, size):
297
self.PropertyChanged("Space", size)
300
space = property(_get_space, _set_space,
301
doc="The required disk space of the transaction.")
303
def _get_packages(self):
304
return self._packages
306
def _set_packages(self, packages):
307
self._packages = [dbus.Array(pkgs, signature=dbus.Signature('s')) \
308
for pkgs in packages]
309
self.PropertyChanged("Packages", self._packages)
311
packages = property(_get_packages, _set_packages,
312
doc="Packages which will be explictly install, "
313
"upgraded, removed, purged or reinstalled.")
315
def _get_depends(self):
318
def _set_depends(self, depends):
319
self._depends = [dbus.Array(deps, signature=dbus.Signature('s')) \
321
self.PropertyChanged("Dependencies", self._depends)
323
depends = property(_get_depends, _set_depends,
324
doc="The additional dependencies: installs, removals, "
325
"upgrades and downgrades.")
327
def _get_status(self):
330
def _set_status(self, enum):
331
self.PropertyChanged("Status", enum)
334
status = property(_get_status, _set_status,
335
doc="The status of the transaction.")
337
def _get_status_details(self):
338
return self._status_details
340
def _set_status_details(self, text):
341
self._status_details = text
342
self.PropertyChanged("StatusDetails", text)
344
status_details = property(_get_status_details, _set_status_details,
345
doc="The status message from apt.")
347
def _get_progress(self):
348
return self._progress
350
def _set_progress(self, percent):
351
self._progress = percent
352
self.PropertyChanged("Progress", percent)
354
progress = property(_get_progress, _set_progress,
355
doc="The progress of the transaction in percent.")
357
def _get_progress_download(self):
358
return self._progress_download
360
def _set_progress_download(self, progress_download):
361
self._progress_download = progress_download
362
self.PropertyChanged("ProgressDownload", progress_download)
364
progress_download = property(_get_progress_download,
365
_set_progress_download,
366
doc="The last progress update of a currently"
367
"running download. A tuple of URI, "
368
"status, short description, full size, "
369
"partially downloaded size and a status "
372
def _get_cancellable(self):
373
return self._cancellable
375
def _set_cancellable(self, cancellable):
376
self._cancellable = cancellable
377
self.PropertyChanged("Cancellable", cancellable)
379
cancellable = property(_get_cancellable, _set_cancellable,
380
doc="If it's currently allowed to cancel the "
383
def _get_term_attached(self):
384
return self._term_attached
386
def _set_term_attached(self, attached):
387
self._term_attached = attached
388
self.PropertyChanged("TerminalAttached", attached)
390
term_attached = property(_get_term_attached, _set_term_attached,
391
doc="If the controlling terminal is currently "
392
"attached to the dpkg call of the "
395
def _get_required_medium(self):
396
return self._required_medium
398
def _set_required_medium(self, medium):
399
label, drive = medium
400
self._required_medium = medium
401
self.PropertyChanged("RequiredMedium", medium)
402
self.MediumRequired(label, drive)
404
required_medium = property(_get_required_medium, _set_required_medium,
405
doc="Tuple containing the label and the drive "
406
"of a required CD/DVD to install packages "
409
def _get_config_file_conflict(self):
410
return self._config_file_conflict
412
def _set_config_file_conflict(self, prompt):
414
self._config_file_conflict = None
417
self._config_file_conflict = prompt
418
self.PropertyChanged("ConfigFileConflict", prompt)
419
self.ConfigFileConflict(old, new)
421
config_file_conflict = property(_get_config_file_conflict,
422
_set_config_file_conflict,
423
doc="Tuple containing the old and the new "
424
"path of the configuration file")
428
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
430
def PropertyChanged(self, property, value):
431
"""Set and emit if a property of the transaction changed.
434
property -- the name of the property
435
value -- the new value of the property
437
log_trans.debug("Emitting PropertyChanged: %s, %s" % (property, value))
439
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
441
def Finished(self, enum):
442
"""Mark and emit the transaction as finished.
445
enum -- the exit state of the transaction, e.g. EXIT_FAILED
447
log_trans.debug("Emitting Finished: %s" % \
448
get_exit_string_from_enum(exit))
450
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
452
def MediumRequired(self, medium, drive):
453
"""Set and emit the required medium change.
455
This method/signal should be used to inform the user to
456
insert the installation CD/DVD:
459
medium -- the CD/DVD label
460
drive -- mount point of the drive
462
log_trans.debug("Emitting MediumRequierd: %s, %s" % (medium, drive))
464
@dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE,
466
def ConfigFileConflict(self, old, new):
467
"""Set and emit the ConfigFileConflict signal.
469
This method/signal should be used to inform the user to
470
answer a config file prompt.
473
old -- current version of the configuration prompt
474
new -- new version of the configuration prompt
476
log_trans.debug("Emitting ConfigFileConflict: %s, %s" % (old, new))
480
def _set_locale(self, locale):
481
"""Set the language and encoding.
484
locale -- specifies language, territory and encoding according
485
to RFC 1766, e.g. "de_DE.UTF-8"
488
if self.status != STATUS_SETTING_UP:
489
raise errors.TransactionAlreadyRunning()
491
(lang, encoding) = parse_localename(locale)
495
self.locale = "%s.%s" % (lang, encoding)
496
self.PropertyChanged("locale", self.locale)
498
def _set_http_proxy(self, url):
499
"""Set an http network proxy.
502
url -- the URL of the proxy server, e.g. http://proxy:8080
504
if url != "" and (not url.startswith("http://") or not ":" in url):
505
raise errors.InvalidProxyError(proxy)
506
self.http_proxy = url
507
self.PropertyChanged("HttpProxy", self.http_proxy)
509
def _set_remove_obsoleted_depends(self, remove_obsoleted_depends):
510
"""Set the handling of the removal of automatically installed
511
dependencies which are now obsoleted.
514
remove_obsoleted_depends -- If True also remove automatically installed
515
dependencies of to removed packages
517
self.remove_obsoleted_depends = remove_obsoleted_depends
518
self.PropertyChanged("RemoveObsoletedDepends",
519
self.remove_obsoleted_depends)
521
def _set_allow_unauthenticated(self, allow_unauthenticated):
522
"""Set the handling of unauthenticated packages
525
allow_unauthenticated -- True to allow packages that come from a
526
repository without a valid authentication signature
528
self.allow_unauthenticated = allow_unauthenticated
529
self.PropertyChanged("AllowUnauthenticated", self.allow_unauthenticated)
531
@dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
532
in_signature="", out_signature="",
533
sender_keyword="sender")
534
def Run(self, sender):
535
"""Queue the transaction for processing."""
536
log_trans.info("Queuing transaction %s", self.tid)
537
return self._run(sender)
540
def _run(self, sender):
541
yield self._check_foreign_user(sender)
544
@dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
545
in_signature="", out_signature="",
546
sender_keyword="sender")
547
def Cancel(self, sender):
548
"""Cancel the transaction.
551
sender -- the unique D-Bus name of the sender (provided by D-Bus)
553
log_trans.info("Cancelling transaction %s", self.tid)
554
return self._cancel(sender)
557
def _cancel(self, sender):
559
yield self._check_foreign_user(sender)
560
except errors.ForeignTransaction:
561
action = policykit1.PK_ACTION_CANCEL_FOREIGN
562
yield policykit1.check_authorization_by_name(sender, action)
564
self.queue.remove(self)
565
log_trans.debug("Removed transaction from queue")
569
self.status = STATUS_CANCELLING
570
self.exit = EXIT_CANCELLED
573
log_trans.debug("Setting cancel event")
574
self.cancelled = True
575
self.status = STATUS_CANCELLING
578
raise errors.AptDaemonError("Could not cancel transaction")
580
@dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
581
in_signature="", out_signature="",
582
sender_keyword="sender")
583
def Simulate(self, sender):
584
"""Simulate a transaction to update its dependencies, future status,
585
download size and required disk space.
587
Call this method if you want to show changes before queuing the
590
log_trans.info("Simulate was called")
591
return self._simulate(sender)
594
def _simulate(self, sender):
595
if self.status != STATUS_SETTING_UP:
596
raise errros.TransactionAlreadyRunning()
597
yield self._check_foreign_user(sender)
598
self.depends, self._dpkg_status, self.download, self.space = \
599
self.queue.worker.simulate(self, self.queue.future_status)
601
def _set_terminal(self, ttyname):
602
"""Set the controlling terminal.
604
The worker will be attached to the specified slave end of a pty
605
master/slave pair. This allows to interact with the
607
Can only be changed before the transaction is started.
610
ttyname -- file path to the slave file descriptor
612
if self.status != STATUS_SETTING_UP:
613
raise errros.TransactionAlreadyRunning()
614
if not os.access(ttyname, os.W_OK):
615
raise errors.AptDaemonError("Pty device does not exist: "
617
if not os.stat(ttyname)[4] == self.uid:
618
raise errors.AptDaemonError("Pty device '%s' has to be owned by"
619
"the owner of the transaction "
620
"(uid %s) " % (ttyname, self.uid))
622
slave_fd = os.open(ttyname, os.O_RDWR | os.O_NOCTTY)
623
if os.isatty(slave_fd):
624
self.terminal = ttyname
625
self.PropertyChanged("Terminal", self.terminal)
627
raise errors.AptDaemonError("%s isn't a tty" % ttyname)
631
def _set_debconf(self, debconf_socket):
632
"""Set the socket of the debconf proxy.
634
The worker process forwards all debconf commands through this
635
socket by using the passthrough frontend. On the client side
636
debconf-communicate should be connected to the socket.
638
Can only be changed before the transaction is started.
641
debconf_socket: absolute path to the socket
643
if self.status != STATUS_SETTING_UP:
644
raise errors.TransactionAlreadyRunning()
645
if not os.access(debconf_socket, os.W_OK):
646
raise errors.AptDaemonError("socket does not exist: "
647
"%s" % debconf_socket)
648
if not os.stat(debconf_socket)[4] == self.uid:
649
raise errors.AptDaemonError("socket '%s' has to be owned by the "
650
"owner of the transaction " % ttyname)
651
self.debconf = debconf_socket
652
self.PropertyChanged("DebconfSocket", self.debconf)
654
@dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
655
in_signature="s", out_signature="",
656
sender_keyword="sender")
657
def ProvideMedium(self, medium, sender):
658
"""Continue paused transaction with the inserted medium.
660
If a media change is required to install packages from CD/DVD
661
the transaction will be paused and could be resumed with this
665
medium -- the label of the CD/DVD
666
sender -- the unique D-Bus name of the sender (provided by D-Bus)
668
log_trans.info("Medium %s was provided", medium)
669
return self._provide_medium(medium, sender)
672
def _provide_medium(self, medium, sender):
673
yield self._check_foreign_user(sender)
674
if not self.required_medium:
675
raise errors.AptDaemonError("There isn't any required medium.")
676
if not self.required_medium[0] == medium:
677
raise errros.AptDaemonError("The medium '%s' isn't "
678
"requested." % medium)
681
@dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
682
in_signature="ss", out_signature="",
683
sender_keyword="sender")
684
def ResolveConfigFileConflict(self, config, answer, sender):
685
"""Resolve a configuration file conflict and continue the transaction.
687
If a config file prompt is detected the transaction will be
688
paused and could be resumed with this method.
691
config -- the path to the original config file
692
answer -- the answer to the configuration file question, can be
694
sender -- the unique D-Bus name of the sender (provided by D-Bus)
696
log_trans.info("Resolved conflict of %s with %s", config, answer)
697
return self._resolve_config_file_conflict(config, answer, sender)
700
def _resolve_config_file_conflict(self, config, answer, sender):
701
yield self._check_foreign_user(sender)
702
if not self.config_file_conflict:
703
raise errors.AptDaemonError("There isn't any config file prompt "
705
if answer not in ["keep", "replace"]:
706
# FIXME: should we re-send the config file prompt
707
# message or assume the client is buggy and
708
# just use a safe default (like keep)?
709
raise errors.AptDaemonError("Invalid value: %s" % answer)
710
if not self.config_file_conflict[0] == config:
711
raise errors.AptDaemonError("Invalid config file: %s" % config)
712
self.config_file_conflict_resolution = answer
715
@dbus_deferred_method(dbus.PROPERTIES_IFACE,
716
in_signature="ssv", out_signature="",
717
sender_keyword="sender")
718
def Set(self, iface, property, value, sender):
721
Only the user who intiaited the transaction is
722
allowed to modify it.
725
iface -- the interface which provides the property
726
property -- the property which should be modified
727
value -- the new value of the property
728
sender -- the unique D-Bus name of the sender (provided by D-Bus)
730
log.debug("Set() was called: %s, %s" % (property, value))
731
return self._set(iface, property, value, sender)
734
def _set(self, iface, property, value, sender):
735
yield self._check_foreign_user(sender)
736
if iface == APTDAEMON_TRANSACTION_DBUS_INTERFACE:
737
if property == "MetaData":
738
self._set_meta_data(value)
739
elif property == "Terminal":
740
self._set_terminal(value)
741
elif property == "DebconfSocket":
742
self._set_debconf(value)
743
elif property == "Locale":
744
self._set_locale(value)
745
elif property == "RemoveObsoletedDepends":
746
self._set_remove_obsoleted_depends(value)
747
elif property == "AllowUnauthenticated":
748
self._set_allow_unauthenticated(value)
749
elif property == "HttpProxy":
750
self._set_http_proxy(value)
752
raise dbus.exceptions.DBusException("Unknown or read only "
753
"property: %s" % property)
755
raise dbus.exceptions.DBusException("Unknown interface: %s" % iface)
757
@dbus.service.method(dbus.PROPERTIES_IFACE,
758
in_signature="s", out_signature="a{sv}")
759
def GetAll(self, iface):
760
"""Get all available properties of the given interface."""
761
log.debug("GetAll() was called: %s" % iface)
762
return self._get_properties(iface)
764
@dbus.service.method(dbus.PROPERTIES_IFACE,
765
in_signature="ss", out_signature="v")
766
def Get(self, iface, property):
767
"""Return the value of the given property provided by the given
770
log.debug("Get() was called: %s, %s" % (iface, property))
771
return self._get_properties(iface)[property]
773
def _get_properties(self, iface):
774
#FIXME: Provide introspection information by overriding the Introspect
775
# method of the dbus.service.Object
776
if iface == APTDAEMON_TRANSACTION_DBUS_INTERFACE:
777
return {"Role": self.role,
778
"Progress": self.progress,
779
"ProgressDetails": self.progress_details,
780
"ProgressDownload": self.progress_download,
781
"Status": self.status,
782
"StatusDetails": self.status_details,
783
"Cancellable": self.cancellable,
784
"TerminalAttached": self.term_attached,
785
"RequiredMedium": self.required_medium,
786
"ConfigFileConflict": self.config_file_conflict,
787
"ExitState": self.exit,
789
"Locale": self.locale,
790
"Terminal": self.terminal,
791
"DebconfSocket": self.debconf,
792
"Paused": self.paused,
793
"AllowUnauthenticated": self.allow_unauthenticated,
794
"RemoveObsoletedDepends": self.remove_obsoleted_depends,
795
"HttpProxy": self.http_proxy,
796
"Packages": self.packages,
797
"MetaData": self.meta_data,
798
"Dependencies": self.depends,
799
"Download": self.download,
806
def _check_foreign_user(self, dbus_name):
807
"""Check if the transaction is owned by the given caller."""
808
uid = yield policykit1.get_uid_from_dbus_name(dbus_name)
810
raise errors.ForeignTransaction()
812
def _set_kwargs(self, kwargs):
813
"""Set the kwargs which will be send to the AptWorker."""
817
class TransactionQueue(gobject.GObject):
819
"""Queue for transactions."""
821
__gsignals__ = {"queue-changed":(gobject.SIGNAL_RUN_FIRST,
824
"future-status-changed":(gobject.SIGNAL_RUN_FIRST,
828
def __init__(self, dummy):
829
"""Intialize a new TransactionQueue instance."""
830
gobject.GObject.__init__(self)
831
self._queue = collections.deque()
834
self.worker = DummyWorker()
836
self.worker = AptWorker()
837
self.future_status = None
838
self.future_status_fd = None
839
self.worker.connect("transaction-done", self._on_transaction_done)
842
return len(self._queue)
844
def _emit_queue_changed(self):
845
"""Emit the queued-changed signal."""
846
log.debug("emitting queue changed")
847
self.emit("queue-changed")
849
def _emit_future_status_changed(self):
850
"""Emit the future-status-changed signal."""
851
log.debug("emitting future-status changed")
852
self.emit("future-status-changed")
853
#FIXME: All not yet queued transactions should listen to this signal
854
# and update be re-simulated if already done so
856
def put(self, trans):
857
"""Add an item to the queue."""
858
#FIXME: Add a timestamp to check if the future status of the trans
859
# is really the later one
860
# Simulate the new transaction if this has not been done before:
861
if trans._dpkg_status is None:
862
trans.depends, trans._dpkg_status, trans.download, trans.space = \
863
self.worker.simulate(trans, self.future_status)
864
# Replace the old future status with the new one
865
if self.future_status_fd is not None:
866
os.close(self.future_status_fd)
867
os.remove(self.future_status)
868
self.future_status_fd, self.future_status = \
869
tempfile.mkstemp(prefix="future-status-")
870
os.write(self.future_status_fd, trans._dpkg_status)
871
self._emit_future_status_changed()
873
if trans._idle_watch is not None:
874
gobject.source_remove(trans._idle_watch)
875
if self.worker.trans:
876
self._queue.append(trans)
878
self.worker.run(trans)
879
self._emit_queue_changed()
881
def _on_transaction_done(self, worker, tid):
882
"""Mark the last item as done and request a new item."""
883
#FIXME: Check if the transaction failed because of a broken system or
884
# if dpkg journal is dirty. If so allready queued transactions
885
# except the repair transactions should be removed from the queue
887
next = self._queue.popleft()
889
log.debug("There isn't any queued transaction")
890
# Reset the future status to the system 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 = None
895
self.future_status = None
896
self._emit_future_status_changed()
898
self.worker.run(next)
899
self._emit_queue_changed()
901
def remove(self, transaction):
902
"""Remove the specified item from the queue."""
903
# FIXME: handle future status
904
self._queue.remove(transaction)
905
self._emit_queue_changed()
908
"""Remove all items from the queue."""
909
# FIXME: handle future status
910
for transaction in self._queue:
911
transaction._remove_from_connection_no_raise()
916
"""Return a list containing all queued items."""
917
return list(self._queue)
920
class AptDaemon(dbus.service.Object):
922
"""Provides a system daemon to process package management tasks.
924
The daemon is transaction based. Each package management tasks runs
925
in a separate transaction. The transactions can be created,
926
monitored and managed via the D-Bus interface.
929
def __init__(self, options, connect=True, bus=None):
930
"""Initialize a new AptDaemon instance.
933
options -- command line options of the type optparse.Values
934
connect -- if the daemon should connect to the D-Bus (default is True)
935
bus -- the D-Bus to connect to (defaults to the system bus)
937
log.info("Initializing daemon")
938
signal.signal(signal.SIGQUIT, self._sigquit)
939
signal.signal(signal.SIGTERM, self._sigquit)
940
self.options = options
943
bus = dbus.SystemBus()
944
# Check if another object has already registered the name on
945
# the bus. Quit the other daemon if replace would be set
947
bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE,
950
except dbus.exceptions.NameExistsException:
951
if self.options.replace == False:
952
log.critical("Another daemon is already running")
954
log.warn("Replacing already running daemon")
955
the_other_guy = bus.get_object(APTDAEMON_DBUS_SERVICE,
957
the_other_guy.Quit(dbus_interface=APTDAEMON_DBUS_INTERFACE,
960
bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE,
965
dbus.service.Object.__init__(self, bus_name, APTDAEMON_DBUS_PATH)
966
self.queue = TransactionQueue(options.dummy)
967
self.queue.connect("queue-changed", self._on_queue_changed)
968
log.debug("Setting up worker thread")
969
log.debug("Daemon was initialized")
971
def _on_queue_changed(self, queue):
972
"""Callback for a changed transaction queue."""
973
if self.queue.worker.trans:
974
current = self.queue.worker.trans.tid
977
queued = [trans.tid for trans in self.queue.items]
978
self.ActiveTransactionsChanged(current, queued)
980
@dbus.service.signal(dbus_interface=APTDAEMON_DBUS_INTERFACE,
982
def ActiveTransactionsChanged(self, current, queued):
983
"""Emit the ActiveTransactionsChanged signal.
986
current -- the tid of the currently running transaction or an empty
988
queued -- list of the ids of the queued transactions
990
log.debug("Emitting ActiveTransactionsChanged signal: %s, %s",
994
"""Start the daemon and listen for calls."""
995
if self.options.disable_timeout == False:
996
log.debug("Using inactivity check")
997
gobject.timeout_add_seconds(APTDAEMON_IDLE_CHECK_INTERVAL,
998
self._check_for_inactivity)
999
log.debug("Waiting for calls")
1002
except KeyboardInterrupt:
1006
def _create_trans(self, role, action, sender, packages=None, kwargs=None):
1007
"""Helper method which returns the tid of a new transaction."""
1008
yield policykit1.check_authorization_by_name(sender, action)
1009
uid = yield policykit1.get_uid_from_dbus_name(sender)
1010
trans = Transaction(role, self.queue, uid, packages=packages,
1012
return_value(trans.tid)
1014
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1015
in_signature="", out_signature="s",
1016
sender_keyword="sender")
1017
def FixIncompleteInstall(self, sender):
1018
"""Return the id of a newly create transaction which will try to
1019
fix incomplete installations by running dpkg --configure -a.
1022
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1024
log.info("FixIncompleteInstall() called")
1025
return self._create_trans(ROLE_FIX_INCOMPLETE_INSTALL,
1026
policykit1.PK_ACTION_FIX,
1029
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1030
in_signature="", out_signature="s",
1031
sender_keyword="sender")
1032
def FixBrokenDepends(self, sender):
1033
"""Return the id of a newly create transaction which will try to
1034
fix broken dependencies.
1037
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1039
log.info("FixBrokenDepends() called")
1040
return self._create_trans(ROLE_FIX_BROKEN_DEPENDS,
1041
policykit1.PK_ACTION_FIX,
1044
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1045
in_signature="", out_signature="s",
1046
sender_keyword="sender")
1047
def UpdateCache(self, sender):
1048
"""Return the id of a newly create transaction which will update
1051
Download the latest information about available packages from the
1055
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1057
log.info("UpdateCache() was called")
1058
return self._create_trans(ROLE_UPDATE_CACHE,
1059
policykit1.PK_ACTION_UPDATE_CACHE,
1062
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1063
in_signature="as", out_signature="s",
1064
sender_keyword="sender")
1065
def RemovePackages(self, package_names, sender):
1066
"""Return the id of a newly create transaction which will remove
1070
package_names -- list of package names to remove
1071
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1073
log.info("RemovePackages() was called: '%s'", package_names)
1074
return self._create_trans(ROLE_REMOVE_PACKAGES,
1075
policykit1.PK_ACTION_REMOVE_PACKAGES,
1077
packages=([], [], package_names, [], []))
1079
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1080
in_signature="b", out_signature="s",
1081
sender_keyword="sender")
1082
def UpgradeSystem(self, safe_mode, sender):
1083
"""Upgrade the whole system.
1085
If in safe mode only already installed packages will be updated.
1086
Updates which require to remove installed packages or to install
1087
additional packages will be skipped.
1090
safe_mode -- boolean value if the safe mode should be used
1091
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1093
log.info("UpgradeSystem() was called with safe mode: "
1095
return self._create_trans(ROLE_UPGRADE_SYSTEM,
1096
policykit1.PK_ACTION_UPGRADE_SYSTEM,
1098
kwargs={"safe_mode": safe_mode})
1100
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1101
in_signature="asasasasas", out_signature="s",
1102
sender_keyword="sender")
1103
def CommitPackages(self, install, reinstall, remove, purge, upgrade,
1105
"""Perform a complex package operation.
1108
install -- package names to be installed
1109
reinstall -- packages names to be reinstalled
1110
remove -- package names to be removed
1111
purge -- package names to be removed including configuration
1112
upgrade -- package names to be upgraded
1113
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1115
#FIXME: take sha1 or md5 cash into accout to allow selecting a version
1116
# or an origin different from the candidate
1117
log.info("CommitPackages() was called: %s, %s, %s, %s, %s",
1118
install, reinstall, remove, purge, upgrade)
1119
return self._commit_packages(install, reinstall, remove, purge, upgrade,
1123
def _commit_packages(self, install, reinstall, remove, purge, upgrade,
1125
def check_empty_list(lst):
1130
packages = [check_empty_list(lst) for lst in [install, reinstall,
1131
remove, purge, upgrade]]
1132
if install != [""] or reinstall != [""]:
1133
action = policykit1.PK_ACTION_INSTALL_PACKAGES
1134
yield policykit1.check_authorization_by_name(sender, action)
1135
elif upgrade != [""]:
1136
action = policykit1.PK_ACTION_UPGRADE_PACKAGES
1137
yield policykit1.check_authorization_by_name(sender, action)
1138
if remove != [""] or purge != [""]:
1139
action = policykit1.PK_ACTION_REMOVE_PACKAGES
1140
yield policykit1.check_authorization_by_name(sender, action)
1141
uid = yield policykit1.get_uid_from_dbus_name(sender)
1142
trans = Transaction(ROLE_COMMIT_PACKAGES, self.queue, uid,
1144
return_value(trans.tid)
1146
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1147
in_signature="as", out_signature="s",
1148
sender_keyword="sender")
1149
def InstallPackages(self, package_names, sender):
1150
"""Install the given packages.
1153
package_names -- list of package names to install
1154
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1156
log.info("InstallPackages() was called: %s" % package_names)
1157
return self._create_trans(ROLE_INSTALL_PACKAGES,
1158
policykit1.PK_ACTION_INSTALL_PACKAGES,
1160
packages=(package_names, [], [], [], []))
1162
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1163
in_signature="as", out_signature="s",
1164
sender_keyword="sender")
1165
def UpgradePackages(self, package_names, sender):
1166
"""Upgrade the given packages to their latest version.
1169
package_names -- list of package names to upgrade
1170
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1172
log.info("UpgradePackages() was called: %s" % package_names)
1173
return self._create_trans(ROLE_UPGRADE_PACKAGES,
1174
policykit1.PK_ACTION_UPGRADE_PACKAGES,
1176
packages=([], [], [], [], package_names))
1178
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1179
in_signature="s", out_signature="s",
1180
sender_keyword="sender")
1181
def AddVendorKeyFromFile(self, path, sender):
1182
"""Install the given key file.
1185
path -- the absolute path to the key file
1186
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1188
#FIXME: Should not be a transaction
1189
log.info("InstallVendorKeyFile() was called: %s" % path)
1190
return self._create_trans(ROLE_ADD_VENDOR_KEY,
1191
policykit1.PK_ACTION_ADD_VENDOR_KEY,
1193
kwargs={"path": path})
1195
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1196
in_signature="s", out_signature="s",
1197
sender_keyword="sender")
1198
def RemoveVendorKey(self, fingerprint, sender):
1199
"""Remove the given key.
1202
fingerprint -- the fingerprint of the key to remove
1203
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1205
#FIXME: Should not be a transaction
1206
log.info("RemoveVendorKey() was called: %s" % fingerprint)
1207
return self._create_trans(ROLE_REMOVE_VENDOR_KEY,
1208
policykit1.PK_ACTION_REMOVE_VENDOR_KEY,
1210
kwargs={"fingerprint": fingerprint})
1212
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1213
in_signature="s", out_signature="s",
1214
sender_keyword="sender")
1215
def InstallFile(self, path, sender):
1216
"""Install the given package file.
1219
path -- the absolute path to the package file
1220
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1222
log.info("InstallFile() was called: %s" % path)
1223
#FIXME: Perform some checks
1224
#FIXME: Should we already extract the package name here?
1225
return self._create_trans(ROLE_INSTALL_FILE,
1226
policykit1.PK_ACTION_INSTALL_FILE,
1228
kwargs={"path": path})
1230
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1231
in_signature="sssasss", out_signature="",
1232
sender_keyword="sender")
1233
def AddRepository(self, type, uri, dist, comps, comment, sourcesfile,
1235
"""Add given repository to the sources list.
1238
type -- the type of the entry (deb, deb-src)
1239
uri -- the main repository uri (e.g. http://archive.ubuntu.com/ubuntu)
1240
dist -- the distribution to use (e.g. karmic, "/")
1241
comps -- a (possible empty) list of components (main, restricted)
1242
comment -- an (optional) comment
1243
sourcesfile -- an (optinal) filename in sources.list.d
1244
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1246
log.info("AddRepository() was called: type='%s' uri='%s' "
1247
"dist='%s' comps='%s' comment='%s' sourcefile='%s'",
1248
type, uri, dist, comps, comment, sourcesfile)
1249
return self._add_repository(self, type, uri, dist, comps, comment,
1250
sourcesfile, sender)
1253
def _add_repository(self, type, uri, dist, comps, comment, sourcesfile,
1256
if not sourcesfile.endswith(".list"):
1257
sourcesfile += ".list"
1258
d = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
1259
sourcesfile = os.path.join(d, os.path.basename(sourcesfile))
1262
action = policykit1.PK_ACTION_CHANGE_REPOSITORY
1263
yield policykit1.check_authorization_by_name(sender, action)
1264
old_umask = os.umask(0022)
1266
sources = SourcesList()
1267
entry = sources.add(type, uri, dist, comps, comment,
1270
raise errors.RepositoryInvalidError()
1271
#FIXME: Should be removed after requiring Python 2.6
1272
except Exception, error:
1279
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1280
in_signature="s", out_signature="b",
1281
sender_keyword="sender")
1282
def EnableDistroComponent(self, component, sender):
1283
"""Enable given component in the sources list.
1286
component -- a components, e.g. main or universe
1287
sender -- the unique D-Bus name of the sender (provided by D-Bus)
1289
log.info("EnableComponent() was called: component='%s' ", component)
1290
return self._enable_distro_component(component, sender)
1293
def _enable_distro_component(self, component, sender):
1294
action = policykit1.PK_ACTION_CHANGE_REPOSITORY
1295
yield policykit1.check_authorization_by_name(sender, action)
1296
old_umask = os.umask(0022)
1298
sourceslist = SourcesList()
1299
distro = aptsources.distro.get_distro()
1300
distro.get_sources(sourceslist)
1301
distro.enable_component(component)
1306
@dbus_deferred_method(APTDAEMON_DBUS_INTERFACE,
1307
in_signature="", out_signature="as",
1308
sender_keyword="sender")
1309
def GetTrustedVendorKeys(self, sender):
1310
"""Return a list of installed repository keys."""
1311
log.info("GetTrustedVendorKeys() was called")
1312
return self._get_trusted_vendor_keys(sender)
1315
def _get_trusted_vendor_keys(self, sender):
1317
action = policykit1.PK_ACTION_GET_TRUSTED_VENDOR_KEYS
1318
yield policykit1.check_authorization_by_name(sender, action)
1319
return_value([key.decode("utf-8", "ignore") for key in aptauth.list()])
1321
@dbus.service.method(APTDAEMON_DBUS_INTERFACE,
1322
in_signature="", out_signature="sas")
1323
def GetActiveTransactions(self):
1324
"""Return the currently running transaction and the list of queued
1327
log.debug("GetActiveTransactions() was called")
1328
queued = [trans.tid for trans in self.queue.items]
1329
if self.queue.worker.trans:
1330
current = self.queue.worker.trans.tid
1333
return current, queued
1335
@dbus.service.method(APTDAEMON_DBUS_INTERFACE,
1336
in_signature="", out_signature="",
1337
sender_keyword="caller_name")
1338
def Quit(self, caller_name):
1339
"""Request a shutdown of the daemon.
1342
caller_name -- the D-Bus name of the caller (provided by D-Bus)
1344
log.info("Shutdown was requested")
1345
log.debug("Quitting main loop...")
1349
def _sigquit(self, signum, frame):
1350
"""Internal callback for the quit signal."""
1353
def _check_for_inactivity(self):
1354
"""Shutdown the daemon if it has been inactive for time specified
1355
in APTDAEMON_IDLE_TIMEOUT.
1357
log.debug("Checking for inactivity")
1358
timestamp = self.queue.worker.last_action_timestamp
1359
if not self.queue.worker.trans and \
1360
not gobject.main_context_default().pending() and \
1361
time.time() - timestamp > APTDAEMON_IDLE_TIMEOUT and \
1363
log.info("Quiting due to inactivity")
1370
"""Allow to run the daemon from the command line."""
1371
parser = OptionParser()
1372
parser.add_option("-t", "--disable-timeout",
1374
action="store_true", dest="disable_timeout",
1375
help=_("Do not shutdown the daemon because of "
1377
parser.add_option("-d", "--debug",
1379
action="store_true", dest="debug",
1380
help=_("Show internal processing information"))
1381
parser.add_option("-r", "--replace",
1383
action="store_true", dest="replace",
1384
help=_("Quit and replace an already running daemon"))
1385
parser.add_option("-p", "--profile",
1387
action="store", type="string", dest="profile",
1388
help=_("Store profile stats in the specified file"))
1389
parser.add_option("--dummy",
1391
action="store_true", dest="dummy",
1392
help=_("Do not make any changes to the system (Only "
1393
"of use to developers)"))
1394
options, args = parser.parse_args()
1395
if options.debug == True:
1396
log.setLevel(logging.DEBUG)
1398
log.setLevel(logging.INFO)
1399
_console_handler.setLevel(logging.INFO)
1400
daemon = AptDaemon(options)
1403
profiler = profile.Profile()
1404
profiler.runcall(daemon.run)
1405
profiler.dump_stats(options.profile)
1406
profiler.print_stats()