Merge lp:~aptdaemon-developers/aptdaemon/future-status into lp:aptdaemon

Proposed by Sebastian Heinlein
Status: Merged
Merged at revision: not available
Proposed branch: lp:~aptdaemon-developers/aptdaemon/future-status
Merge into: lp:aptdaemon
Diff against target: 1090 lines (+677/-94)
7 files modified
aptdaemon/client.py (+46/-0)
aptdaemon/console.py (+165/-10)
aptdaemon/core.py (+123/-14)
aptdaemon/enums.py (+9/-0)
aptdaemon/progress.py (+0/-7)
aptdaemon/test/test_future_status.py (+89/-0)
aptdaemon/worker.py (+245/-63)
To merge this branch: bzr merge lp:~aptdaemon-developers/aptdaemon/future-status
Reviewer Review Type Date Requested Status
Aptdaemon Developers Pending
Review via email: mp+23623@code.launchpad.net

Description of the change

Add support for simulating a transaction and to preview changes which take into account all currently running and queued transactions.

To post a comment you must log in.
378. By Sebastian Heinlein

Fix showing the download size

379. By Sebastian Heinlein

CONSOLE: make use of ngettext for the package listing

380. By Sebastian Heinlein

CONSOLE: take APT::Get::Assume-Yes into account

381. By Sebastian Heinlein

CONSOLE: Add full-upgrade and safe-upgrade options (see aptitude)

382. By Sebastian Heinlein

CORE: Use seconds for the timeout

383. By Sebastian Heinlein

CORE: set the idle timeout of a transaction to 5 minutes

384. By Sebastian Heinlein

CORE: Allow to modify the packages attribute of a transaction after creation

385. By Sebastian Heinlein

Worker: Add Support for simulating file installs

386. By Sebastian Heinlein

Console: Allow to install package files by --install PATH

387. By Sebastian Heinlein

Console: Fix transaction confirmation

388. By Sebastian Heinlein

Core: Fix packages signal (copy paste error)

389. By Sebastian Heinlein

Console: Fix packages order

390. By Sebastian Heinlein

Worker: Show the package file as install if not installed before and as reinstall in the other case

391. By Sebastian Heinlein

WORKER: Fix local install by unlocking the cache

392. By Sebastian Heinlein

Improve status messages in the console client

393. By Sebastian Heinlein

Worker: Check for skipped updates in the simulation

394. By Sebastian Heinlein

Add an enum for the package group index in the Transaction.depends and Transaction.packages lists

395. By Sebastian Heinlein

Use the same order in Transaction.packages and Transaction.depends and extend depends accordingly. Use the new enums.

396. By Sebastian Heinlein

Do not ierate on the main loop during sensible actions like opening the cache or forking. This could lead to a cache race with the transaction simulation

397. By Sebastian Heinlein

Reuse the fixed DaemonOpenProgress again in the worker.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'aptdaemon/client.py'
2--- aptdaemon/client.py 2010-04-01 05:22:25 +0000
3+++ aptdaemon/client.py 2010-04-20 17:40:54 +0000
4@@ -82,6 +82,21 @@
5 __gsignals__ = {"finished": (gobject.SIGNAL_RUN_FIRST,
6 gobject.TYPE_NONE,
7 (gobject.TYPE_INT,)),
8+ "dependencies-changed": (gobject.SIGNAL_RUN_FIRST,
9+ gobject.TYPE_NONE,
10+ (gobject.TYPE_PYOBJECT,
11+ gobject.TYPE_PYOBJECT,
12+ gobject.TYPE_PYOBJECT,
13+ gobject.TYPE_PYOBJECT,
14+ gobject.TYPE_PYOBJECT,
15+ gobject.TYPE_PYOBJECT,
16+ gobject.TYPE_PYOBJECT)),
17+ "download-changed": (gobject.SIGNAL_RUN_FIRST,
18+ gobject.TYPE_NONE,
19+ (gobject.TYPE_INT,)),
20+ "space-changed": (gobject.SIGNAL_RUN_FIRST,
21+ gobject.TYPE_NONE,
22+ (gobject.TYPE_INT,)),
23 "error": (gobject.SIGNAL_RUN_FIRST,
24 gobject.TYPE_NONE,
25 (gobject.TYPE_INT, gobject.TYPE_STRING)),
26@@ -168,9 +183,12 @@
27 self.progress = 0
28 self.paused = False
29 self.http_proxy = None
30+ self.dependencies = [[], [], [], [], [], [], []]
31 self.packages = [[], [], [], [], []]
32 self.meta_data = {}
33 self.remove_obsoleted_depends = False
34+ self.download = 0
35+ self.space = 0
36 self._locale = ""
37 self._exit_handler = None
38 self._method = None
39@@ -217,6 +235,9 @@
40 self.emit("allow-unauthenticated-changed", value)
41 elif property_name == "Terminal":
42 self.emit("terminal-changed", value)
43+ elif property_name == "Dependencies":
44+ self.dependencies = value
45+ self.emit("dependencies-changed", *value)
46 elif property_name == "Packages":
47 self.packages = value
48 self.emit("packages-changed", *value)
49@@ -255,6 +276,12 @@
50 elif property_name == "ProgressDetails":
51 self.progress_details = value
52 self.emit("progress-details-changed", *value)
53+ elif property_name == "Download":
54+ self.download = value
55+ self.emit("download-changed", value)
56+ elif property_name == "Space":
57+ self.space = value
58+ self.emit("space-changed", value)
59 elif property_name == "HttpProxy":
60 self.http_proxy = value
61 self.emit("http-proxy-changed", value)
62@@ -315,6 +342,25 @@
63 else:
64 raise
65
66+ def simulate(self, reply_handler=None, error_handler=None):
67+ """Simulate the transaction to calculate the dependencies, the
68+ required download size and the required disk space.
69+
70+ The corresponding properties of the transaction will be updated.
71+
72+ Also TransactionFailed exceptions could be raised, if e.g. a
73+ requested package could not be installed or the cache is currently
74+ broken.
75+
76+ Keyword arguments:
77+ reply_handler - callback function. If specified in combination with
78+ error_handler the method will be called asynchrounsouly.
79+ error_handler - in case of an error the given callback gets the
80+ corresponding DBus exception instance
81+ """
82+ self._iface.Simulate(reply_handler=reply_handler,
83+ error_handler=error_handler)
84+
85 def cancel(self, reply_handler=None, error_handler=None):
86 """Cancel the running transaction.
87
88
89=== modified file 'aptdaemon/console.py'
90--- aptdaemon/console.py 2010-03-30 19:48:19 +0000
91+++ aptdaemon/console.py 2010-04-20 17:40:54 +0000
92@@ -24,9 +24,12 @@
93 import array
94 import fcntl
95 from gettext import gettext as _
96+from gettext import ngettext
97+import locale
98 from optparse import OptionParser
99 import os
100 import pty
101+import re
102 import termios
103 import time
104 import tty
105@@ -47,6 +50,7 @@
106 ANSI_BOLD = chr(27) + "[1m"
107 ANSI_RESET = chr(27) + "[0m"
108
109+locale.setlocale(locale.LC_ALL, "")
110
111 class ConsoleClient:
112 """
113@@ -132,9 +136,10 @@
114 self._client.update_cache(reply_handler=self._run_transaction,
115 error_handler=self._on_exception)
116
117- def upgrade_system(self):
118+ def upgrade_system(self, safe_mode):
119 """Upgrade system"""
120- self._client.upgrade_system(reply_handler=self._run_transaction,
121+ self._client.upgrade_system(safe_mode,
122+ reply_handler=self._run_transaction,
123 error_handler=self._on_exception)
124
125 def run(self):
126@@ -249,6 +254,30 @@
127 "%3.3s%% " % percent +
128 "%-*.*s" % (text_width, text_width, text) + "\r")
129
130+ def _update_custom_progress(self, msg, percent=None, spin=True):
131+ """Update the progress bar with a custom status message."""
132+ text = ANSI_BOLD + msg + ANSI_RESET
133+ text_width = self._terminal_width - 9
134+ # Spin the progress line (maximum 5 times a second)
135+ if spin:
136+ self._spin_cur = (self._spin_cur + 1) % len(self._spin_elements)
137+ self._spin_stamp = time.time()
138+ spinner = self._spin_elements[self._spin_cur]
139+ else:
140+ spinner = "+"
141+ # Show progress information if available
142+ if percent is None:
143+ percent = "---"
144+ sys.stderr.write("[%s] " % spinner +
145+ "%3.3s%% " % percent +
146+ "%-*.*s" % (text_width, text_width, text) + "\r")
147+ return True
148+
149+ def _stop_custom_progress(self):
150+ """Stop the spinner which shows non trans status messages."""
151+ if self._progress_id is not None:
152+ gobject.source_remove(self._progress_id)
153+
154 def _clear_progress(self):
155 """Clear progress information on stderr."""
156 sys.stderr.write("%-*.*s\r" % (self._terminal_width,
157@@ -257,7 +286,8 @@
158
159 def _on_cancel_signal(self, signum, frame):
160 """Callback for a cancel signal."""
161- if self._transaction:
162+ if self._transaction and \
163+ self._transaction.status != enums.STATUS_SETTING_UP:
164 self._transaction.cancel()
165 else:
166 self._loop.quit()
167@@ -330,8 +360,119 @@
168 def _run_transaction(self, trans):
169 """Callback which runs a requested transaction."""
170 self._set_transaction(trans)
171- trans.run(reply_handler=lambda: True,
172- error_handler=self._on_exception)
173+ self._stop_custom_progress()
174+ if self._transaction.role in [enums.ROLE_UPDATE_CACHE,
175+ enums.ROLE_ADD_VENDOR_KEY_FILE,
176+ enums.ROLE_REMOVE_VENDOR_KEY,
177+ enums.ROLE_FIX_INCOMPLETE_INSTALL]:
178+ #TRANSLATORS: status message
179+ self._progress_id = \
180+ gobject.timeout_add(250, self._update_custom_progress,
181+ _("Queuing"))
182+ self._transaction.run(error_handler=self._on_exception,
183+ reply_handler=lambda: self._stop_custom_progress())
184+ else:
185+ #TRANSLATORS: status message
186+ self._progress_id = \
187+ gobject.timeout_add(250, self._update_custom_progress,
188+ _("Resolving dependencies"))
189+ self._transaction.simulate(reply_handler=self._show_changes,
190+ error_handler=self._on_exception)
191+
192+ def _show_changes(self):
193+ def show_packages(pkgs):
194+ """Format the pkgs in a nice way."""
195+ line = " "
196+ pkgs.sort()
197+ for pkg in pkgs:
198+ if len(line) + 1 + len(pkg) > self._terminal_width and \
199+ line != " ":
200+ print line
201+ line = " "
202+ line += " %s" % pkg
203+ if line != " ":
204+ print line
205+ self._stop_custom_progress()
206+ self._clear_progress()
207+ installs, reinstalls, removals, purges, upgrades = \
208+ self._transaction.packages
209+ dep_installs, dep_reinstalls, dep_removals, dep_purges, dep_upgrades, \
210+ dep_downgrades, dep_kepts = self._transaction.dependencies
211+ installs.extend(dep_installs)
212+ upgrades.extend(dep_upgrades)
213+ removals.extend(purges)
214+ removals.extend(dep_removals)
215+ removals.extend(dep_purges)
216+ reinstalls.extend(dep_reinstalls)
217+ #FIXME: should be change when supported by CommitPackages
218+ downgrades = dep_downgrades
219+ kepts = dep_kepts
220+ if installs:
221+ #TRANSLATORS: %s is the number of packages
222+ print ngettext("The following NEW package will be installed (%s):",
223+ "The following NEW packages will be installed (%s):",
224+ len(installs)) % len(installs)
225+ show_packages(installs)
226+ if upgrades:
227+ #TRANSLATORS: %s is the number of packages
228+ print ngettext("The following package will be upgraded (%s):",
229+ "The following packages will be upgraded (%s):",
230+ len(upgrades)) % len(upgrades)
231+ show_packages(upgrades)
232+ if removals:
233+ #TRANSLATORS: %s is the number of packages
234+ print ngettext("The following package will be REMOVED (%s):",
235+ "The following packages will be REMOVED (%s):",
236+ len(removals)) % len(removals)
237+ #FIXME: mark purges
238+ show_packages(removals)
239+ if downgrades:
240+ #TRANSLATORS: %s is the number of packages
241+ print ngettext("The following package will be DOWNGRADED (%s):",
242+ "The following packages will be DOWNGRADED (%s):",
243+ len(downgrades)) % len(downgrades)
244+ show_packages(downgrades)
245+ if reinstalls:
246+ #TRANSLATORS: %s is the number of packages
247+ print ngettext("The following package will be reinstalled (%s):",
248+ "The following packages will be reinstalled (%s):",
249+ len(reinstalls)) % len(reinstalls)
250+ show_packages(reinstalls)
251+ if kepts:
252+ print ngettext("The following package has been kept back (%s):",
253+ "The following packages have been kept back (%s):",
254+ len(kepts)) % len(kepts)
255+ show_packages(kepts)
256+
257+ if self._transaction.download:
258+ print _("Need to get %sB of archives.") % \
259+ apt_pkg.size_to_str(self._transaction.download)
260+ if self._transaction.space > 0:
261+ print _("After this operation, %sB of additional disk space "
262+ "will be used.") % \
263+ apt_pkg.size_to_str(self._transaction.space)
264+ elif self._transaction.space < 0:
265+ print _("After this operation, %sB of additional disk space "
266+ "will be freed.") % \
267+ apt_pkg.size_to_str(self._transaction.space)
268+ if not apt_pkg.config.find_b("APT::Get::Assume-Yes"):
269+ try:
270+ cont = raw_input(_("Do you want to continue [Y/n]?"))
271+ except EOFError:
272+ cont = "n"
273+ #FIXME: Listen to changed dependencies!
274+ if not re.match(locale.nl_langinfo(locale.YESEXPR), cont) and \
275+ cont != "":
276+ msg = enums.get_exit_string_from_enum(enums.EXIT_CANCELLED)
277+ self._update_custom_progress(msg, None, False)
278+ self._loop.quit()
279+ sys.exit(1)
280+ #TRANSLATORS: status message
281+ self._progress_id = gobject.timeout_add(250,
282+ self._update_custom_progress,
283+ _("Queuing"))
284+ self._transaction.run(error_handler=self._on_exception,
285+ reply_handler=lambda: self._stop_custom_progress())
286
287
288 def main():
289@@ -367,9 +508,16 @@
290 parser.add_option("-u", "--upgrade", default="",
291 action="store", type="string", dest="upgrade",
292 help=_("Install the given packages"))
293- parser.add_option("", "--upgrade-system", default="",
294- action="store_true", dest="dist_upgrade",
295- help=_("Upgrade the system"))
296+ parser.add_option("", "--upgrade-system",
297+ action="store_true", dest="safe_upgrade",
298+ help=_("Deprecated: Please use --safe-upgrade"))
299+ parser.add_option("", "--safe-upgrade",
300+ action="store_true", dest="safe_upgrade",
301+ help=_("Upgrade the system in a safe way"))
302+ parser.add_option("", "--full-upgrade",
303+ action="store_true", dest="full_upgrade",
304+ help=_("Upgrade the system, possibly installing and "
305+ "removing packages"))
306 parser.add_option("", "--add-vendor-key", default="",
307 action="store", type="string", dest="add_vendor_key",
308 help=_("Add the vendor to the trusted ones"))
309@@ -396,14 +544,21 @@
310 (options, args) = parser.parse_args()
311 con = ConsoleClient(show_terminal=not options.hide_terminal,
312 allow_unauthenticated=options.allow_unauthenticated)
313- if options.dist_upgrade:
314- con.upgrade_system()
315+ #TRANSLATORS: status message
316+ con._progress_id = gobject.timeout_add(250, con._update_custom_progress,
317+ _("Waiting for authentication"))
318+ if options.safe_upgrade:
319+ con.upgrade_system(True)
320+ elif options.full_upgrade:
321+ con.upgrade_system(False)
322 elif options.refresh:
323 con.update_cache()
324 elif options.fix_install:
325 con.fix_incomplete_install()
326 elif options.fix_depends:
327 con.fix_broken_depends()
328+ elif options.install and options.install.endswith(".deb"):
329+ con.install_file(options.install)
330 elif options.install or options.reinstall or options.remove or \
331 options.purge or options.upgrade:
332 con.commit_packages(options.install.split(),
333
334=== modified file 'aptdaemon/core.py'
335--- aptdaemon/core.py 2010-03-31 08:52:50 +0000
336+++ aptdaemon/core.py 2010-04-20 17:40:54 +0000
337@@ -40,6 +40,7 @@
338 import Queue
339 import signal
340 import sys
341+import tempfile
342 import time
343 import threading
344 import uuid
345@@ -73,10 +74,10 @@
346 APTDAEMON_IDLE_TIMEOUT = 5 * 60
347
348 # Maximum allowed time between the creation of a transaction and its queuing
349-TRANSACTION_IDLE_TIMEOUT = 30 * 1000
350+TRANSACTION_IDLE_TIMEOUT = 300
351 # Keep the transaction for the given time alive on the bus after it has
352 # finished
353-TRANSACTION_DEL_TIMEOUT = 5 * 1000
354+TRANSACTION_DEL_TIMEOUT = 5
355
356 # Setup the DBus main loop
357 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
358@@ -163,22 +164,28 @@
359 self.cancelled = False
360 self.paused = False
361 self._meta_data = dbus.Dictionary(signature="ss")
362- self.packages = [pkgs or dbus.Array([], signature=dbus.Signature('s')) \
363- for pkgs in packages]
364+ self._dpkg_status = None
365+ self._download = 0
366+ self._space = 0
367+ self._depends = [dbus.Array([], signature=dbus.Signature('s')) \
368+ for i in range(7)]
369+ self._packages = [pkgs or dbus.Array([], signature=dbus.Signature('s'))\
370+ for pkgs in packages]
371 # Add a timeout which removes the transaction from the bus if it
372 # hasn't been setup and run for the TRANSACTION_IDLE_TIMEOUT period
373- self._idle_watch = gobject.timeout_add(
374+ self._idle_watch = gobject.timeout_add_seconds(
375 TRANSACTION_IDLE_TIMEOUT, self._remove_from_connection_no_raise)
376
377 def _remove_from_connection_no_raise(self):
378 """Version of remove_from_connection that does not raise if the
379 object isn't exported.
380 """
381+ log_trans.debug("Removing transaction")
382 try:
383 self.remove_from_connection()
384 except LookupError, error:
385- logging.debug("remove_from_connection() raised LookupError: "
386- "'%s'" % error)
387+ log_trans.debug("remove_from_connection() raised LookupError: "
388+ "'%s'" % error)
389
390 def _set_meta_data(self, data):
391 # Perform some checks
392@@ -251,8 +258,8 @@
393 self.status = STATUS_FINISHED
394 # Remove the transaction from the Bus after it is complete. A short
395 # timeout helps lazy clients
396- gobject.timeout_add(TRANSACTION_DEL_TIMEOUT,
397- self._remove_from_connection_no_raise)
398+ gobject.timeout_add_seconds(TRANSACTION_DEL_TIMEOUT,
399+ self._remove_from_connection_no_raise)
400
401 def _get_exit(self):
402 return self._exit
403@@ -260,6 +267,50 @@
404 exit = property(_get_exit, _set_exit,
405 doc="The exit state of the transaction.")
406
407+ def _get_download(self):
408+ return self._download
409+
410+ def _set_download(self, size):
411+ self.PropertyChanged("Download", size)
412+ self._download = size
413+
414+ download = property(_get_download, _set_download,
415+ doc="The download size of the transaction.")
416+
417+ def _get_space(self):
418+ return self._space
419+
420+ def _set_space(self, size):
421+ self.PropertyChanged("Space", size)
422+ self._space = size
423+
424+ space = property(_get_space, _set_space,
425+ doc="The required disk space of the transaction.")
426+
427+ def _get_packages(self):
428+ return self._packages
429+
430+ def _set_packages(self, packages):
431+ self._packages = [dbus.Array(pkgs, signature=dbus.Signature('s')) \
432+ for pkgs in packages]
433+ self.PropertyChanged("Packages", self._packages)
434+
435+ packages = property(_get_packages, _set_packages,
436+ doc="Packages which will be explictly install, "
437+ "upgraded, removed, purged or reinstalled.")
438+
439+ def _get_depends(self):
440+ return self._depends
441+
442+ def _set_depends(self, depends):
443+ self._depends = [dbus.Array(deps, signature=dbus.Signature('s')) \
444+ for deps in depends]
445+ self.PropertyChanged("Dependencies", self._depends)
446+
447+ depends = property(_get_depends, _set_depends,
448+ doc="The additional dependencies: installs, removals, "
449+ "upgrades and downgrades.")
450+
451 def _get_status(self):
452 return self._status
453
454@@ -493,6 +544,24 @@
455 return
456 raise errors.APTDError("Could not cancel transaction")
457
458+ @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE,
459+ in_signature="", out_signature="",
460+ sender_keyword="sender")
461+ def Simulate(self, sender):
462+ """Simulate a transaction to update its dependencies, future status,
463+ download size and required disk space.
464+
465+ Call this method if you want to show changes before queuing the
466+ transaction.
467+ """
468+ def helper():
469+ self.depends, self._dpkg_status, self.download, self.space = \
470+ self.queue.worker.simulate(self, self.queue.future_status)
471+ log_trans.info("Simulate was called")
472+ deferred = self._check_foreign_user(sender)
473+ deferred.add_callback(lambda x: helper())
474+ return deferred
475+
476 def _set_terminal(self, ttyname):
477 """Set the controlling terminal.
478
479@@ -691,6 +760,9 @@
480 "HttpProxy": self.http_proxy,
481 "Packages": self.packages,
482 "MetaData": self.meta_data,
483+ "Dependencies": self.depends,
484+ "Download": self.download,
485+ "Space": self.space
486 }
487 else:
488 return {}
489@@ -715,6 +787,9 @@
490
491 __gsignals__ = {"queue-changed":(gobject.SIGNAL_RUN_FIRST,
492 gobject.TYPE_NONE,
493+ ()),
494+ "future-status-changed":(gobject.SIGNAL_RUN_FIRST,
495+ gobject.TYPE_NONE,
496 ())}
497
498 def __init__(self, dummy):
499@@ -726,6 +801,8 @@
500 self.worker = DummyWorker()
501 else:
502 self.worker = AptWorker()
503+ self.future_status = None
504+ self.future_status_fd = None
505 self.worker.connect("transaction-done", self._on_transaction_done)
506
507 def __len__(self):
508@@ -736,33 +813,65 @@
509 log.debug("emitting queue changed")
510 self.emit("queue-changed")
511
512- def put(self, transaction):
513+ def _emit_future_status_changed(self):
514+ """Emit the future-status-changed signal."""
515+ log.debug("emitting future-status changed")
516+ self.emit("future-status-changed")
517+ #FIXME: All not yet queued transactions should listen to this signal
518+ # and update be re-simulated if already done so
519+
520+ def put(self, trans):
521 """Add an item to the queue."""
522- if transaction._idle_watch is not None:
523- gobject.source_remove(transaction._idle_watch)
524+ #FIXME: Add a timestamp to check if the future status of the trans
525+ # is really the later one
526+ # Simulate the new transaction if this has not been done before:
527+ if trans._dpkg_status is None:
528+ trans.depends, trans._dpkg_status, trans.download, trans.space = \
529+ self.worker.simulate(trans, self.future_status)
530+ # Replace the old future status with the new one
531+ if self.future_status_fd is not None:
532+ os.close(self.future_status_fd)
533+ self.future_status_fd, self.future_status = \
534+ tempfile.mkstemp(prefix="future-status-")
535+ os.write(self.future_status_fd, trans._dpkg_status)
536+ self._emit_future_status_changed()
537+
538+ if trans._idle_watch is not None:
539+ gobject.source_remove(trans._idle_watch)
540 if self.worker.trans:
541- self._queue.append(transaction)
542+ self._queue.append(trans)
543 else:
544- self.worker.run(transaction)
545+ self.worker.run(trans)
546 self._emit_queue_changed()
547
548 def _on_transaction_done(self, worker, tid):
549 """Mark the last item as done and request a new item."""
550+ #FIXME: Check if the transaction failed because of a broken system or
551+ # if dpkg journal is dirty. If so allready queued transactions
552+ # except the repair transactions should be removed from the queue
553 try:
554 next = self._queue.popleft()
555 except IndexError:
556 log.debug("There isn't any queued transaction")
557+ # Reset the future status to the system one
558+ if self.future_status_fd is not None:
559+ os.close(self.future_status_fd)
560+ self.future_status_fd = None
561+ self.future_status = None
562+ self._emit_future_status_changed()
563 else:
564 self.worker.run(next)
565 self._emit_queue_changed()
566
567 def remove(self, transaction):
568 """Remove the specified item from the queue."""
569+ # FIXME: handle future status
570 self._queue.remove(transaction)
571 self._emit_queue_changed()
572
573 def clear(self):
574 """Remove all items from the queue."""
575+ # FIXME: handle future status
576 for transaction in self._queue:
577 transaction._remove_from_connection_no_raise()
578 self._queue.clear()
579
580=== modified file 'aptdaemon/enums.py'
581--- aptdaemon/enums.py 2010-04-01 05:52:48 +0000
582+++ aptdaemon/enums.py 2010-04-20 17:40:54 +0000
583@@ -23,6 +23,15 @@
584 def _(msg):
585 return gettext.dgettext("aptdaemon", msg)
586
587+# Index of package groups in the depends and packages property
588+(PKGS_INSTALL,
589+ PKGS_REINSTALL,
590+ PKGS_REMOVE,
591+ PKGS_PURGE,
592+ PKGS_UPGRADE,
593+ PKGS_DOWNGRADE,
594+ PKGS_KEEP) = range(7)
595+
596 # Finish states
597 (EXIT_SUCCESS,
598 EXIT_CANCELLED,
599
600=== modified file 'aptdaemon/progress.py'
601--- aptdaemon/progress.py 2010-02-10 10:03:26 +0000
602+++ aptdaemon/progress.py 2010-04-20 17:40:54 +0000
603@@ -82,8 +82,6 @@
604 if not self.quiet:
605 self._transaction.progress = progress
606 self.progress = progress
607- while gobject.main_context_default().pending():
608- gobject.main_context_default().iteration()
609
610 def done(self):
611 """Callback after completing a step.
612@@ -299,11 +297,6 @@
613 """Fork and create a master/slave pty pair by which the forked process
614 can be controlled.
615 """
616- # process all pending events in the main loop, since we will quit
617- # the loop in the child process
618- context = gobject.main_context_default()
619- while context.pending():
620- context.iteration()
621 pid, self.master_fd = os.forkpty()
622 if pid == 0:
623 mainloop.quit()
624
625=== added file 'aptdaemon/test/test_future_status.py'
626--- aptdaemon/test/test_future_status.py 1970-01-01 00:00:00 +0000
627+++ aptdaemon/test/test_future_status.py 2010-04-20 17:40:54 +0000
628@@ -0,0 +1,89 @@
629+#!/usr/bin/env python
630+# -*- coding: utf-8 -*-
631+"""Tests the debconf forwarding"""
632+
633+import logging
634+import os
635+import subprocess
636+import sys
637+import tempfile
638+import unittest
639+
640+import apt
641+import apt_pkg
642+
643+sys.path.insert(0, "../..")
644+import aptdaemon.worker
645+import aptdaemon.enums
646+
647+DEBUG=False
648+
649+class MockTrans(object):
650+ def __init__(self, packages):
651+ self.packages = packages
652+ self.role = aptdaemon.enums.ROLE_COMMIT_PACKAGES
653+ self.tid = "12123"
654+
655+class FutureStatusTest(unittest.TestCase):
656+
657+ def setUp(self):
658+ self.worker = aptdaemon.worker.AptWorker()
659+ self.worker._cache = apt.Cache()
660+ self.status_orig = apt_pkg.config.get("Dir::State::status")
661+
662+ def testInstall(self):
663+ # Create a transaction which installs a package which has got
664+ # uninstalled dependencies
665+ for pkg in self.worker._cache:
666+ if not pkg.is_installed and pkg.candidate:
667+ deps = self._get_uninstalled_deps(pkg)
668+ if deps:
669+ break
670+ try:
671+ pkg.mark_install()
672+ except SystemError:
673+ self.worker._cache.clear()
674+ continue
675+ trans = MockTrans([[pkg.name], [], [], [], []])
676+ # The test
677+ deps, status, download, space = self.worker.simulate(trans)
678+ # Check if the package is installed in the new status file
679+ status_file, status_path = tempfile.mkstemp(prefix="future-status-")
680+ os.write(status_file, status)
681+ apt_pkg.config.set("Dir::State::status", status_path)
682+ apt_pkg.init_system()
683+ self.worker._cache.open()
684+ self.assertTrue(self.worker._cache[trans.packages[0][0]].is_installed,
685+ "The package is not installed in the future")
686+ if self.worker._cache.broken_count:
687+ broken = [pkg for pkg in self.worker._cache if pkg.is_now_broken]
688+ self.fail("The following packages are broken: " % " ".join(broken))
689+
690+ def _get_uninstalled_deps(self, pkg):
691+ deps = []
692+ for dep in pkg.candidate.dependencies:
693+ if len(dep.or_dependencies) > 1:
694+ return None
695+ for base_dep in dep.or_dependencies:
696+ try:
697+ dep = self.worker._cache[base_dep.name]
698+ except KeyError:
699+ return None
700+ if not dep.is_installed and dep.candidate and \
701+ apt_pkg.CheckDep(dep.candidate.version,
702+ base_dep.relation,
703+ base_dep.version):
704+ deps.append(dep)
705+ else:
706+ return None
707+ return deps
708+
709+ def tearDown(self):
710+ pass
711+
712+if __name__ == "__main__":
713+ if DEBUG:
714+ logging.basicConfig(level=logging.DEBUG)
715+ unittest.main()
716+
717+# vim: ts=4 et sts=4
718
719=== modified file 'aptdaemon/worker.py'
720--- aptdaemon/worker.py 2010-04-17 07:39:07 +0000
721+++ aptdaemon/worker.py 2010-04-20 17:40:54 +0000
722@@ -73,6 +73,7 @@
723 self.trans = None
724 self.last_action_timestamp = time.time()
725 self._cache = None
726+ self._status_orig = apt_pkg.config.find_file("Dir::State::status")
727 self._lock_fd = -1
728
729 def run(self, transaction):
730@@ -106,7 +107,7 @@
731 self._lock_cache()
732 if self.trans.role == ROLE_FIX_INCOMPLETE_INSTALL:
733 self.fix_incomplete_install()
734- elif not is_dpkg_journal_clean():
735+ elif not self.is_dpkg_journal_clean():
736 raise TransactionFailed(ERROR_INCOMPLETE_INSTALL)
737 self._open_cache()
738 # Process transaction which can handle a broken dep cache
739@@ -302,7 +303,7 @@
740 dpkg_range = (64, 99)
741 self._commit_changes(fetch_range=(5, 33),
742 install_range=(34, 63))
743- self._lock_cache()
744+ self._unlock_cache()
745 # Install the dpkg file
746 if deb.install(DaemonDpkgInstallProgress(self.trans,
747 begin=64, end=95)):
748@@ -486,6 +487,8 @@
749 quiet -- if True do no report any progress
750 """
751 self.trans.status = STATUS_LOADING_CACHE
752+ apt_pkg.config.set("Dir::State::status", self._status_orig)
753+ apt_pkg.init_system()
754 try:
755 progress = DaemonOpenProgress(self.trans, begin=begin, end=end,
756 quiet=quiet)
757@@ -499,7 +502,7 @@
758 def _lock_cache(self):
759 """Lock the APT cache."""
760 try:
761- self._lock_fd = lock_pkg_system()
762+ self._lock_fd = self.lock_pkg_system()
763 except LockFailedError, error:
764 logging.error("Failed to lock the cache")
765 self.trans.paused = True
766@@ -518,7 +521,7 @@
767 def _watch_lock(self):
768 """Unpause the transaction if the lock can be obtained."""
769 try:
770- lock_pkg_system()
771+ self.lock_pkg_system()
772 except LockFailedError:
773 return True
774 self.trans.paused = False
775@@ -530,6 +533,68 @@
776 os.close(self._lock_fd)
777 self._lock_fd = -1
778
779+ def lock_pkg_system(self):
780+ """Lock the package system and provide information if this cannot be
781+ done.
782+
783+ This is a reemplemenataion of apt_pkg.PkgSystemLock(), since we want to
784+ handle an incomplete dpkg run separately.
785+ """
786+ def get_lock_fd(lock_path):
787+ """Return the file descriptor of the lock file or raise
788+ LockFailedError if the lock cannot be obtained.
789+ """
790+ fd_lock = apt_pkg.get_lock(lock_path)
791+ if fd_lock < 0:
792+ process = None
793+ try:
794+ # Get the pid of the locking application
795+ fd_lock_read = open(lock_path, "r")
796+ flk = struct.pack('hhQQi', fcntl.F_WRLCK, os.SEEK_SET, 0,
797+ 0, 0)
798+ flk_ret = fcntl.fcntl(fd_lock_read, fcntl.F_GETLK, flk)
799+ pid = struct.unpack("hhQQi", flk_ret)[4]
800+ # Get the command of the pid
801+ fd_status = open("/proc/%s/status" % pid, "r")
802+ try:
803+ for key, value in (line.split(":") for line in \
804+ fd_status.readlines()):
805+ if key == "Name":
806+ process = value.strip()
807+ break
808+ finally:
809+ fd_status.close()
810+ except:
811+ pass
812+ finally:
813+ fd_lock_read.close()
814+ raise LockFailedError(lock_path, process)
815+ else:
816+ return fd_lock
817+
818+ # Try the lock in /var/cache/apt/archive/lock first
819+ # this is because apt-get install will hold it all the time
820+ # while the dpkg lock is briefly given up before dpkg is
821+ # forked off. this can cause a race (LP: #437709)
822+ lock_archive = os.path.join(apt_pkg.config.find_dir("Dir::Cache::Archives"),
823+ "lock")
824+ lock_fd_archive = get_lock_fd(lock_archive)
825+ os.close(lock_fd_archive)
826+ # Then the status lock
827+ lock_sys = os.path.join(os.path.dirname(self._status_orig), "lock")
828+ return get_lock_fd(lock_sys)
829+
830+ def is_dpkg_journal_clean(self):
831+ """Return False if there are traces of incomplete dpkg status
832+ updates."""
833+ status_updates = os.path.join(os.path.dirname(self._status_orig),
834+ "updates/")
835+ for dentry in os.listdir(status_updates):
836+ if dentry.isdigit():
837+ return False
838+ return True
839+
840+
841 def _commit_changes(self, fetch_range=(5, 50), install_range=(50, 90)):
842 """Commit previously marked changes to the cache.
843
844@@ -581,6 +646,182 @@
845 raise TransactionFailed(ERROR_PACKAGE_MANAGER_FAILED,
846 "%s: %s" % (excep, output))
847
848+ def simulate(self, trans, status_path=None):
849+ """Return the dependencies which will be installed by the transaction,
850+ the content of the dpkg status file after the transaction would have
851+ been applied, the download size and the required disk space.
852+
853+ Keyword arguments:
854+ trans -- the transaction which should be simulated
855+ status_path -- the path to a dpkg status file on which the transaction
856+ should be applied
857+ """
858+ log.info("Simulating trans: %s" % trans.tid)
859+ try:
860+ return self._simulate_helper(trans, status_path)
861+ except TransactionFailed, excep:
862+ trans.error = excep
863+ except Exception, excep:
864+ trans.error = TransactionFailed(ERROR_UNKNOWN,
865+ traceback.format_exc())
866+ trans.exit = EXIT_FAILED
867+ trans.progress = 100
868+ self.last_action_timestamp = time.time()
869+
870+ def _simulate_helper(self, trans, status_path):
871+ #FIXME: A lot of redundancy
872+ #FIXME: Add checks for obsolete dependencies and unauthenticated
873+ def get_base_records(sec, additional=[]):
874+ records = ["Priority", "Installed-Size", "Architecture",
875+ "Version", "Replaces", "Depends", "Conflicts",
876+ "Breaks", "Recommends", "Suggests", "Provides",
877+ "Pre-Depends", "Essential"]
878+ records.extend(additional)
879+ ret = ""
880+ for record in records:
881+ try:
882+ ret += "%s: %s\n" % (record, sec[record])
883+ except KeyError:
884+ pass
885+ return ret
886+
887+ status = ""
888+ depends = [[], [], [], [], [], [], []]
889+ skip_pkgs = []
890+ size = 0
891+ installs = reinstalls = removals = purges = upgrades = downgrades = \
892+ kepts = upgradables = []
893+
894+ # Only handle transaction which change packages
895+ #FIXME: Add support for ROLE_FIX_INCOMPLETE_INSTALL,
896+ # ROLE_FIX_BROKEN_DEPENDS
897+ if trans.role not in [ROLE_INSTALL_PACKAGES, ROLE_UPGRADE_PACKAGES,
898+ ROLE_UPGRADE_SYSTEM, ROLE_REMOVE_PACKAGES,
899+ ROLE_COMMIT_PACKAGES, ROLE_INSTALL_FILE]:
900+ return depends, status, 0, 0
901+
902+ if not self.trans and not self.is_dpkg_journal_clean():
903+ raise TransactionFailed(ERROR_INCOMPLETE_INSTALL)
904+
905+ # Fast forward the cache
906+ if not status_path:
907+ status_path = self._status_orig
908+ apt_pkg.config.set("Dir::State::status", status_path)
909+ apt_pkg.init_system()
910+ #FIXME: open cache in background after startup
911+ if not self._cache:
912+ self._cache = apt.cache.Cache()
913+ else:
914+ self._cache.open()
915+
916+ if self._cache.broken_count:
917+ broken = [pkg.name for pkg in self._cache if pkg.is_now_broken]
918+ raise TransactionFailed(ERROR_CACHE_BROKEN, " ".join(broken))
919+
920+ # Mark the changes and apply
921+ if trans.role == ROLE_UPGRADE_SYSTEM:
922+ #FIXME: Should be part of python-apt to avoid using private API
923+ upgradables = [self._cache[pkgname] \
924+ for pkgname in self._cache._set \
925+ if self._cache._depcache.is_upgradable(\
926+ self._cache._cache[pkgname])]
927+ upgradables = [pkg for pkg in self._cache if pkg.is_upgradable]
928+ self._cache.upgrade(not trans.kwargs["safe_mode"])
929+ elif trans.role == ROLE_INSTALL_FILE:
930+ deb = apt.debfile.DebPackage(trans.kwargs["path"], self._cache)
931+ if not deb.check():
932+ raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED,
933+ deb._failure_string)
934+ status += "Package: %s\n" % deb.pkgname
935+ status += "Status: install ok installed\n"
936+ status += get_base_records(deb)
937+ status += "\n"
938+ skip_pkgs.append(deb.pkgname)
939+ try:
940+ size = int(deb["Installed-Size"]) * 1024
941+ except (KeyError, AttributeError):
942+ pass
943+ try:
944+ pkg = self._cache[deb.pkgname]
945+ except KeyError:
946+ trans.packages[PKGS_INSTALL] = [deb.pkgname]
947+ else:
948+ if pkg.is_installed:
949+ # if we failed to get the size from the deb file do nor
950+ # try to get the delta
951+ if size != 0:
952+ size -= pkg.installed.installed_size
953+ trans.packages[PKGS_REINSTALL] = [deb.pkgname]
954+ else:
955+ trans.packages[PKGS_INSTALL] = [deb.pkgname]
956+ installs, reinstalls, removal, purges, upgrades = trans.packages
957+ else:
958+ ac = self._cache.actiongroup()
959+ installs, reinstalls, removals, purges, upgrades = trans.packages
960+ resolver = apt.cache.ProblemResolver(self._cache)
961+ self._mark_packages_for_installation(installs, resolver)
962+ self._mark_packages_for_installation(reinstalls, resolver,
963+ reinstall=True)
964+ self._mark_packages_for_removal(removals, resolver)
965+ self._mark_packages_for_removal(purges, resolver, purge=True)
966+ self._mark_packages_for_upgrade(upgrades, resolver)
967+ self._resolve_depends(resolver)
968+ ac.release()
969+ changes = self._cache.get_changes()
970+ changes_names = []
971+
972+ # get the additional dependencies
973+ for pkg in changes:
974+ if pkg.marked_upgrade and pkg.is_installed and \
975+ not pkg.name in upgrades:
976+ depends[PKGS_UPGRADE].append(pkg.name)
977+ elif pkg.marked_reinstall and not pkg.name in reinstalls:
978+ depends[PKGS_REINSTALL].append(pkg.name)
979+ elif pkg.marked_downgrade and not pkg.name in downgrades:
980+ depends[PKGS_DOWNGRADE].append(pkg.name)
981+ elif pkg.marked_install and not pkg.name in installs:
982+ depends[PKGS_INSTALL].append(pkg.name)
983+ elif pkg.marked_delete and not pkg.name in removals:
984+ depends[PKGS_REMOVE].append(pkg.name)
985+ #FIXME: add support for purges
986+ changes_names.append(pkg.name)
987+ # Check for skipped upgrades
988+ for pkg in upgradables:
989+ if not pkg in changes or not pkg.marked_upgrade:
990+ depends[PKGS_KEEP].append(pkg.name)
991+
992+ # merge the changes into the dpkg status
993+ for sec in apt_pkg.TagFile(open(status_path)):
994+ pkg_name = sec["Package"]
995+ if pkg_name in skip_pkgs:
996+ continue
997+ status += "Package: %s\n" % pkg_name
998+ if pkg_name in changes_names:
999+ pkg = self._cache[sec["Package"]]
1000+ if pkg.marked_delete:
1001+ status += "Status: deinstall ok config-files\n"
1002+ version = pkg.installed
1003+ else:
1004+ # Install, Upgrade, downgrade and reinstall all use the
1005+ # candidate version
1006+ version = pkg.candidate
1007+ status += "Status: install ok installed\n"
1008+ status += get_base_records(version.record)
1009+ changes.remove(pkg)
1010+ else:
1011+ status += get_base_records(sec, ["Status"])
1012+ status += "\n"
1013+ # Add changed and not yet known (installed) packages to the status
1014+ for pkg in changes:
1015+ version = pkg.candidate
1016+ status += "Package: %s\n" % pkg.name
1017+ status += "Status: install ok installed\n"
1018+ status += get_base_records(pkg.candidate.record)
1019+ status += "\n"
1020+
1021+ return depends, status, self._cache.required_download, \
1022+ size + self._cache.required_space
1023+
1024
1025 class DummyWorker(AptWorker):
1026
1027@@ -663,63 +904,4 @@
1028 return False
1029
1030
1031-def lock_pkg_system():
1032- """Lock the package system and provide information if this cannot be done.
1033-
1034- This is a reemplemenataion of apt_pkg.PkgSystemLock(), since we want to
1035- handle an incomplete dpkg run separately.
1036- """
1037- def get_lock_fd(lock_path):
1038- """Return the file descriptor of the lock file or raise
1039- LockFailedError if the lock cannot be obtained.
1040- """
1041- fd_lock = apt_pkg.get_lock(lock_path)
1042- if fd_lock < 0:
1043- process = None
1044- try:
1045- # Get the pid of the locking application
1046- fd_lock_read = open(lock_path, "r")
1047- flk = struct.pack('hhQQi', fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0)
1048- flk_ret = fcntl.fcntl(fd_lock_read, fcntl.F_GETLK, flk)
1049- pid = struct.unpack("hhQQi", flk_ret)[4]
1050- # Get the command of the pid
1051- fd_status = open("/proc/%s/status" % pid, "r")
1052- try:
1053- for key, value in (line.split(":") for line in \
1054- fd_status.readlines()):
1055- if key == "Name":
1056- process = value.strip()
1057- break
1058- finally:
1059- fd_status.close()
1060- except:
1061- pass
1062- finally:
1063- fd_lock_read.close()
1064- raise LockFailedError(lock_path, process)
1065- else:
1066- return fd_lock
1067-
1068- # Try the lock in /var/cache/apt/archive/lock first
1069- # this is because apt-get install will hold it all the time
1070- # while the dpkg lock is briefly given up before dpkg is
1071- # forked off. this can cause a race (LP: #437709)
1072- lock_archive = os.path.join(apt_pkg.config.find_dir("Dir::Cache::Archives"),
1073- "lock")
1074- lock_fd_archive = get_lock_fd(lock_archive)
1075- os.close(lock_fd_archive)
1076- # Then the status lock
1077- status_file = apt_pkg.config.find_file("Dir::State::status")
1078- lock_sys = os.path.join(os.path.dirname(status_file), "lock")
1079- return get_lock_fd(lock_sys)
1080-
1081-def is_dpkg_journal_clean():
1082- """Return False if there are traces of incomplete dpkg status updates."""
1083- status_file = apt_pkg.config.find_file("Dir::State::status")
1084- status_updates = os.path.join(os.path.dirname(status_file), "updates/")
1085- for dentry in os.listdir(status_updates):
1086- if dentry.isdigit():
1087- return False
1088- return True
1089-
1090 # vim:ts=4:sw=4:et

Subscribers

People subscribed via source and target branches

to status/vote changes: