2
# -*- coding: utf-8 -*-
3
"""Progress handlers for APT operations"""
4
# Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com>
6
# Licensed under the GNU General Public License Version 2
8
# This program is free software; you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation; either version 2 of the License, or
11
# (at your option) any later version.
13
__author__ = "Sebastian Heinlein <devel@glatzor.de>"
15
__all__ = ("DaemonAcquireProgress", "DaemonOpenProgress",
16
"DaemonInstallProgress", "DaemonDpkgInstallProgress",
17
"DaemonDpkgRecoverProgress")
19
from gettext import gettext as _
34
import apt.progress.base
40
from loop import mainloop
42
log = logging.getLogger("AptDaemon.Worker")
43
log_terminal = logging.getLogger("AptDaemon.Worker.Terminal")
45
INSTALL_TIMEOUT = 10 * 60
47
MAP_STAGE = {"install":_("Installing %s"),
48
"configure":_("Configuring %s"),
49
"remove":_("Removing %s"),
50
"trigproc":_("Running post-installation trigger %s"),
51
"purge":_("Purging %s"),
52
"upgrade":_("Upgrading %s")}
54
REGEX_ANSI_ESCAPE_CODE = chr(27) + "\[[;?0-9]*[A-Za-z]"
56
class DaemonOpenProgress(apt.progress.base.OpProgress):
58
"""Handles the progress of the cache opening."""
60
def __init__(self, transaction, begin=0, end=100, quiet=False):
61
"""Initialize a new DaemonOpenProgress instance.
64
transaction -- corresponding transaction D-Bus object
65
begin -- begin of the progress range (defaults to 0)
66
end -- end of the progress range (defaults to 100)
67
quiet -- do not emit any progress information for the transaction
69
apt.progress.base.OpProgress.__init__(self)
70
self._transaction = transaction
71
self.steps = [begin + (end - begin) * modifier
72
for modifier in [0.12, 0.25, 0.50, 0.75, 1.00]]
73
self.progress_begin = float(begin)
74
self.progress_end = self.steps.pop(0)
78
def update(self, percent):
79
"""Callback for progress updates.
82
percent - current progress in percent
85
progress = int(self.progress_begin + percent / 100 * \
86
(self.progress_end - self.progress_begin))
87
if self.progress < progress:
91
self.progress = progress
93
self._transaction.progress = progress
96
"""Callback after completing a step.
98
Sets the progress range to the next interval."""
99
self.progress_begin = self.progress_end
101
self.progress_end = self.steps.pop(0)
103
log.warning("An additional step to open the cache is required")
106
class DaemonAcquireProgress(apt.progress.base.AcquireProgress):
108
Handle the package download process
110
def __init__(self, transaction, begin=0, end=100):
111
apt.progress.base.AcquireProgress.__init__(self)
112
self.transaction = transaction
113
self.progress_end = end
114
self.progress_begin = begin
117
def _emit_acquire_item(self, item, total_size=0, current_size=0):
118
if item.owner.status == apt_pkg.AcquireItem.STAT_DONE:
119
status = enums.DOWNLOAD_DONE
120
# Workaround for a bug in python-apt, see lp: #581886
121
current_size = item.owner.filesize
122
elif item.owner.status == apt_pkg.AcquireItem.STAT_AUTH_ERROR:
123
status = enums.DOWNLOAD_AUTH_ERROR
124
elif item.owner.status == apt_pkg.AcquireItem.STAT_FETCHING:
125
status = enums.DOWNLOAD_FETCHING
126
elif item.owner.status == apt_pkg.AcquireItem.STAT_ERROR:
127
status = enums.DOWNLOAD_ERROR
128
elif item.owner.status == apt_pkg.AcquireItem.STAT_IDLE:
129
status = enums.DOWNLOAD_IDLE
131
# Workaround: The StatTransientNetworkError status isn't mapped
132
# by python-apt, see LP #602578
133
status = enums.DOWNLOAD_NETWORK_ERROR
134
if item.owner.status != apt_pkg.AcquireItem.STAT_DONE and \
135
item.owner.error_text:
136
msg = item.owner.error_text
137
elif item.owner.mode:
138
msg = item.owner.mode
141
self.transaction.progress_download = item.uri, status, item.shortdesc, \
142
total_size | item.owner.filesize, \
143
current_size | item.owner.partialsize, \
146
def done(self, item):
147
"""Invoked when an item is successfully and completely fetched."""
148
self._emit_acquire_item(item)
150
def fail(self, item):
151
"""Invoked when an item could not be fetched."""
152
self._emit_acquire_item(item)
154
def fetch(self, item):
155
"""Invoked when some of the item's data is fetched."""
156
self._emit_acquire_item(item)
158
def ims_hit(self, item):
159
"""Invoked when an item is confirmed to be up-to-date.
161
Invoked when an item is confirmed to be up-to-date. For instance,
162
when an HTTP download is informed that the file on the server was
165
self._emit_acquire_item(item)
167
def pulse(self, owner):
168
"""Callback to update progress information"""
169
if self.transaction.cancelled:
171
self.transaction.progress_details = (self.current_items,
177
percent = (((self.current_bytes + self.current_items) * 100.0) /
178
float(self.total_bytes + self.total_items))
179
progress = int(self.progress_begin + percent/100 * \
180
(self.progress_end - self.progress_begin))
181
# If the progress runs backwards emit an illegal progress value
182
# e.g. during cache updates.
183
if self.progress > progress:
184
self.transaction.progress = 101
186
self.transaction.progress = progress
187
self.progress = progress
188
# Show all currently downloaded files
190
for worker in owner.workers:
191
if not worker.current_item:
193
self._emit_acquire_item(worker.current_item,
196
if worker.current_item.owner.id:
197
items.append(worker.current_item.owner.id)
199
items.append(worker.current_item.shortdesc)
201
msg = gettext.ngettext("Downloading %s", "Downloading %s",
202
len(items)) % " ".join(items)
203
self.transaction.status_details = msg
205
while gobject.main_context_default().pending():
206
gobject.main_context_default().iteration()
210
"""Callback at the beginning of the operation"""
211
self.transaction.status = enums.STATUS_DOWNLOADING
212
self.transaction.cancellable = True
215
"""Callback at the end of the operation"""
216
self.transaction.progress_details = (0, 0, 0, 0, 0, 0)
217
self.transaction.progress = self.progress_end
218
self.transaction.cancellable = False
220
def media_change(self, medium, drive):
221
"""Callback for media changes"""
222
#FIXME: make use of DeviceKit/hal
223
self.transaction.required_medium = medium, drive
224
self.transaction.paused = True
225
self.transaction.status = enums.STATUS_WAITING_MEDIUM
226
while self.transaction.paused:
227
gobject.main_context_default().iteration()
228
self.transaction.status = enums.STATUS_DOWNLOADING
229
if self.transaction.cancelled:
234
class DaemonInstallProgress(object):
236
def __init__(self, transaction, begin=50, end=100):
237
self.transaction = transaction
240
self.progress_begin = begin
241
self.progress_end = end
242
self._child_exit = -1
243
self.last_activity = 0
245
self.status_parent_fd, self.status_child_fd = os.pipe()
247
self._line_buffer = ""
249
def start_update(self):
250
log.debug("Start update")
251
lock.system.release()
252
self.transaction.status = enums.STATUS_COMMITTING
253
self.transaction.term_attached = True
254
self.last_activity = time.time()
255
self.start_time = time.time()
257
def finish_update(self):
258
"""Callback at the end of the operation"""
259
self.transaction.term_attached = False
260
lock.system.acquire()
262
def _child(self, pm):
264
res = pm.do_install(self.status_child_fd)
266
os._exit(apt_pkg.PackageManager.RESULT_FAILED)
270
def run(self, *args, **kwargs):
274
os.close(self.status_parent_fd)
275
self._child(*args, **kwargs)
278
os.close(self.status_child_fd)
279
log.debug("Child pid: %s", pid)
281
flags = gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP
282
if self.transaction.terminal:
283
# Save the settings of the transaction terminal and set to raw mode
284
terminal_fd = os.open(self.transaction.terminal,
285
os.O_RDWR|os.O_NOCTTY|os.O_NONBLOCK)
286
terminal_attr = termios.tcgetattr(terminal_fd)
287
tty.setraw(terminal_fd, termios.TCSANOW)
288
# Setup copying of i/o between the controlling terminals
289
watchers.append(gobject.io_add_watch(terminal_fd, flags,
293
watchers.append(gobject.io_add_watch(self.master_fd, flags,
294
self._copy_io_master, terminal_fd))
295
# Monitor the child process
296
watchers.append(gobject.child_watch_add(pid, self._on_child_exit))
297
# Watch for status updates
298
watchers.append(gobject.io_add_watch(self.status_parent_fd,
300
self._on_status_update))
301
while self._child_exit == -1:
302
gobject.main_context_default().iteration()
304
gobject.source_remove(id)
305
# Restore the settings of the transaction terminal
307
termios.tcsettattr(terminal_fd, termios.TCSADRAIN, terminal_attr)
310
# Make sure all file descriptors are closed
311
for fd in [self.master_fd, self.status_parent_fd, terminal_fd]:
316
return os.WEXITSTATUS(self._child_exit)
318
def _on_child_exit(self, pid, condition):
319
log.debug("Child exited: %s", condition)
320
self._child_exit = condition
323
def _on_status_update(self, source, condition):
324
log.debug("UpdateInterface")
327
while not status_msg.endswith("\n"):
328
self.last_activity = time.time()
329
status_msg += os.read(source, 1)
333
(status, pkg, percent, message_raw) = status_msg.split(":", 3)
335
# silently ignore lines that can't be parsed
337
message = message_raw.strip()
338
#print "percent: %s %s" % (pkg, float(percent)/100.0)
339
if status == "pmerror":
340
self._error(pkg, message)
341
elif status == "pmconffile":
342
# we get a string like this:
343
# 'current-conffile' 'new-conffile' useredited distedited
344
match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", message_raw)
346
new, old = match.group(1), match.group(2)
347
self._conffile(new, old)
348
elif status == "pmstatus":
349
self._status_changed(pkg, float(percent), message)
350
# catch a time out by sending crtl+c
351
if self.last_activity + INSTALL_TIMEOUT < time.time() and \
353
log.critical("Killing child since timeout of %s s", INSTALL_TIMEOUT)
354
os.kill(self.child_pid, 15)
358
"""Fork and create a master/slave pty pair by which the forked process
361
pid, self.master_fd = os.forkpty()
364
# Switch to the language of the user
365
if self.transaction.locale:
366
os.putenv("LANG", self.transaction.locale)
367
# Either connect to the controllong terminal or switch to
368
# non-interactive mode
369
if not self.transaction.terminal:
370
# FIXME: we should check for "mail" or "gnome" here
371
# and not unset in this case
372
os.putenv("APT_LISTCHANGES_FRONTEND", "none")
374
#FIXME: Should this be a setting?
375
os.putenv("TERM", "linux")
376
# Run debconf through a proxy if available
377
if self.transaction.debconf:
378
os.putenv("DEBCONF_PIPE", self.transaction.debconf)
379
os.putenv("DEBIAN_FRONTEND", "passthrough")
380
if log.level == logging.DEBUG:
381
os.putenv("DEBCONF_DEBUG",".")
382
elif not self.transaction.terminal:
383
os.putenv("DEBIAN_FRONTEND", "noninteractive")
384
# Proxy configuration
385
if self.transaction.http_proxy:
386
apt_pkg.config.set("Acquire::http::Proxy",
387
self.transaction.http_proxy)
390
def _copy_io_master(self, source, condition, target):
391
if condition == gobject.IO_IN:
392
self.last_activity = time.time()
394
char = os.read(source, 1)
396
log.debug("Faild to read from master")
398
# Write all the output from dpkg to a log
400
# Skip ANSI characters from the console output
401
line = re.sub(REGEX_ANSI_ESCAPE_CODE, "", self._line_buffer)
403
log_terminal.debug(line)
404
self.output += line + "\n"
405
self._line_buffer = ""
407
self._line_buffer += char
410
os.write(target, char)
412
log.debug("Failed to write to controlling terminal")
417
def _copy_io(self, source, condition):
418
if condition == gobject.IO_IN:
419
char = os.read(source, 1)
420
# Detect config file prompt answers on the console
421
# FIXME: Perhaps should only set the
422
# self.transaction.config_file_prompt_answer and not write
423
if self.transaction.paused and \
424
self.transaction.config_file_conflict:
425
self.transaction.config_file_conflict_resolution = None
426
self.transaction.paused = False
428
os.write(self.master_fd, char)
436
def _status_changed(self, pkg, percent, status):
437
"""Callback to update status information"""
438
log.debug("APT status: %s" % status)
439
progress = self.progress_begin + percent / 100 * \
440
(self.progress_end - self.progress_begin)
441
if self.progress < progress:
442
self.transaction.progress = int(progress)
443
self.progress = progress
444
self.transaction.status_details = status
446
def _conffile(self, current, new):
447
"""Callback for a config file conflict"""
448
log.warning("Config file prompt: '%s' (%s)" % (current, new))
449
self.transaction.config_file_conflict = (current, new)
450
self.transaction.paused = True
451
self.transaction.status = enums.STATUS_WAITING_CONFIG_FILE_PROMPT
452
while self.transaction.paused:
453
gobject.main_context_default().iteration()
454
log.debug("Sending config file answer: %s",
455
self.transaction.config_file_conflict_resolution)
456
if self.transaction.config_file_conflict_resolution == "replace":
457
os.write(self.master_fd, "y\n")
458
elif self.transaction.config_file_conflict_resolution == "keep":
459
os.write(self.master_fd, "n\n")
460
self.transaction.config_file_conflict_resolution = None
461
self.transaction.config_file_conflict = None
462
self.transaction.status = enums.STATUS_COMMITTING
465
def _error(self, pkg, msg):
466
"""Callback for an error"""
467
log.critical("%s: %s" % (pkg, msg))
470
class DaemonDpkgInstallProgress(DaemonInstallProgress):
472
"""Progress handler for a local Debian package installation."""
474
def __init__(self, transaction, begin=101, end=101):
475
DaemonInstallProgress.__init__(self, transaction, begin, end)
477
def _child(self, debfile):
478
args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd)]
479
if not self.transaction.terminal:
480
args.extend(["--force-confdef", "--force-confold"])
481
args.extend(["-i", debfile])
482
os.execlp("/usr/bin/dpkg", *args)
483
# We should never go here
486
def _on_status_update(self, source, condition):
487
log.debug("UpdateInterface")
490
while not status_raw.endswith("\n"):
491
status_raw += os.read(source, 1)
495
status = [s.strip() for s in status_raw.split(":", 3)]
497
# silently ignore lines that can't be parsed
499
# Parse the status message. It can be of the following types:
500
# - "status: PACKAGE: STATUS"
501
# - "status: PACKAGE: error: MESSAGE"
502
# - "status: FILE: conffile: 'OLD' 'NEW' useredited distedited"
503
# - "processing: STAGE: PACKAGE" with STAGE is one of upgrade,
504
# install, configure, trigproc, remove, purge
505
if status[0] == "status":
506
if status[2] == "error":
507
self._error(status[1], status[3])
508
elif status[2] == "conffile":
509
match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status[3])
511
new, old = match.group(1), match.group(2)
512
self._conffile(new, old)
513
elif status == "status":
515
self._status_changed(pkg=status[1], percent=101,
517
elif status[0] == "processing":
519
msg = MAP_STAGE[status[1]] % status[2]
520
except ValueError, IndexError:
522
self._status_changed(pkg=status[2], percent=101, status=msg)
525
class DaemonDpkgRecoverProgress(DaemonDpkgInstallProgress):
527
"""Progress handler for dpkg --confiure -a call."""
530
args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd),
532
if not self.transaction.terminal:
533
args.extend(["--force-confdef", "--force-confold"])
534
os.execlp("/usr/bin/dpkg", *args)
535
# We should never go here