~ubuntu-branches/ubuntu/maverick/aptdaemon/maverick-proposed

« back to all changes in this revision

Viewing changes to .pc/07_fix-race-597017.patch/aptdaemon/progress.py

  • Committer: Bazaar Package Importer
  • Author(s): Michael Vogt
  • Date: 2010-10-05 18:04:52 UTC
  • Revision ID: james.westby@ubuntu.com-20101005180452-94rr4nnbppvmhszb
Tags: 0.31+bzr506-0ubuntu2
* 07_fix-race-597017.patch:
  - fix race in locking by backporting the relevant bits from
    lp:aptdaemon/trunk (thanks to Sebastian Heinlein)
    (LP: #597017)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# -*- coding: utf-8 -*-
 
3
"""Progress handlers for APT operations"""
 
4
# Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com>
 
5
#
 
6
# Licensed under the GNU General Public License Version 2
 
7
#
 
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.
 
12
 
 
13
__author__  = "Sebastian Heinlein <devel@glatzor.de>"
 
14
 
 
15
__all__ = ("DaemonAcquireProgress", "DaemonOpenProgress",
 
16
           "DaemonInstallProgress", "DaemonDpkgInstallProgress",
 
17
           "DaemonDpkgRecoverProgress")
 
18
 
 
19
from gettext import gettext as _
 
20
import gettext
 
21
import locale
 
22
import logging
 
23
import os
 
24
import re
 
25
import subprocess
 
26
import sys
 
27
import termios
 
28
import time
 
29
import threading
 
30
import tty
 
31
import warnings
 
32
 
 
33
import apt_pkg
 
34
import apt.progress.base
 
35
import apt.debfile
 
36
import gobject
 
37
 
 
38
import enums
 
39
import lock
 
40
from loop import mainloop
 
41
 
 
42
log = logging.getLogger("AptDaemon.Worker")
 
43
log_terminal = logging.getLogger("AptDaemon.Worker.Terminal")
 
44
 
 
45
INSTALL_TIMEOUT = 10 * 60
 
46
 
 
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")}
 
53
 
 
54
REGEX_ANSI_ESCAPE_CODE = chr(27) + "\[[;?0-9]*[A-Za-z]"
 
55
 
 
56
class DaemonOpenProgress(apt.progress.base.OpProgress):
 
57
 
 
58
    """Handles the progress of the cache opening."""
 
59
 
 
60
    def __init__(self, transaction, begin=0, end=100, quiet=False):
 
61
        """Initialize a new DaemonOpenProgress instance.
 
62
 
 
63
        Keyword arguments:
 
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
 
68
        """
 
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)
 
75
        self.progress = 0
 
76
        self.quiet = quiet
 
77
 
 
78
    def update(self, percent):
 
79
        """Callback for progress updates.
 
80
 
 
81
        Keyword argument:
 
82
        percent - current progress in percent
 
83
        """
 
84
        if percent < 101:
 
85
            progress = int(self.progress_begin + percent / 100 * \
 
86
                           (self.progress_end - self.progress_begin))
 
87
            if self.progress < progress:
 
88
                return
 
89
        else:
 
90
            progress = 101
 
91
        self.progress = progress
 
92
        if not self.quiet:
 
93
            self._transaction.progress = progress
 
94
 
 
95
    def done(self):
 
96
        """Callback after completing a step.
 
97
 
 
98
        Sets the progress range to the next interval."""
 
99
        self.progress_begin = self.progress_end
 
100
        try:
 
101
            self.progress_end = self.steps.pop(0)
 
102
        except:
 
103
            log.warning("An additional step to open the cache is required")
 
104
 
 
105
 
 
106
class DaemonAcquireProgress(apt.progress.base.AcquireProgress):
 
107
    '''
 
108
    Handle the package download process
 
109
    '''
 
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
 
115
        self.progress = 0
 
116
 
 
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
 
130
        else:
 
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
 
139
        else:
 
140
            msg = ""
 
141
        self.transaction.progress_download = item.uri, status, item.shortdesc, \
 
142
                                             total_size | item.owner.filesize, \
 
143
                                             current_size | item.owner.partialsize, \
 
144
                                             msg
 
145
 
 
146
    def done(self, item):
 
147
        """Invoked when an item is successfully and completely fetched."""
 
148
        self._emit_acquire_item(item)
 
149
 
 
150
    def fail(self, item):
 
151
        """Invoked when an item could not be fetched."""
 
152
        self._emit_acquire_item(item)
 
153
 
 
154
    def fetch(self, item):
 
155
        """Invoked when some of the item's data is fetched."""
 
156
        self._emit_acquire_item(item)
 
157
 
 
158
    def ims_hit(self, item):
 
159
        """Invoked when an item is confirmed to be up-to-date.
 
160
 
 
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
 
163
        not modified.
 
164
        """
 
165
        self._emit_acquire_item(item)
 
166
 
 
167
    def pulse(self, owner):
 
168
        """Callback to update progress information"""
 
169
        if self.transaction.cancelled:
 
170
            return False
 
171
        self.transaction.progress_details = (self.current_items,
 
172
                                             self.total_items,
 
173
                                             self.current_bytes,
 
174
                                             self.total_bytes,
 
175
                                             self.current_cps,
 
176
                                             self.elapsed_time)
 
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
 
185
        else:
 
186
            self.transaction.progress = progress
 
187
            self.progress = progress
 
188
        # Show all currently downloaded files
 
189
        items = []
 
190
        for worker in owner.workers:
 
191
            if not worker.current_item:
 
192
                continue
 
193
            self._emit_acquire_item(worker.current_item,
 
194
                                    worker.total_size,
 
195
                                    worker.current_size)
 
196
            if worker.current_item.owner.id:
 
197
                items.append(worker.current_item.owner.id)
 
198
            else:
 
199
                items.append(worker.current_item.shortdesc)
 
200
        if items:
 
201
            msg = gettext.ngettext("Downloading %s", "Downloading %s",
 
202
                                   len(items)) % " ".join(items)
 
203
            self.transaction.status_details = msg
 
204
 
 
205
        while gobject.main_context_default().pending():
 
206
            gobject.main_context_default().iteration()
 
207
        return True
 
208
 
 
209
    def start(self):
 
210
        """Callback at the beginning of the operation"""
 
211
        self.transaction.status = enums.STATUS_DOWNLOADING
 
212
        self.transaction.cancellable = True
 
213
 
 
214
    def stop(self):
 
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
 
219
 
 
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:
 
230
            return False
 
231
        return True
 
232
 
 
233
 
 
234
class DaemonInstallProgress(object):
 
235
 
 
236
    def __init__(self, transaction, begin=50, end=100):
 
237
        self.transaction = transaction
 
238
        self.status = ""
 
239
        self.progress = 0
 
240
        self.progress_begin = begin
 
241
        self.progress_end = end
 
242
        self._child_exit = -1
 
243
        self.last_activity = 0
 
244
        self.child_pid = 0
 
245
        self.status_parent_fd, self.status_child_fd = os.pipe()
 
246
        self.output = ""
 
247
        self._line_buffer = ""
 
248
 
 
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()
 
256
 
 
257
    def finish_update(self):
 
258
        """Callback at the end of the operation"""
 
259
        self.transaction.term_attached = False
 
260
        lock.system.acquire()
 
261
 
 
262
    def _child(self, pm):
 
263
        try:
 
264
            res = pm.do_install(self.status_child_fd)
 
265
        except:
 
266
            os._exit(apt_pkg.PackageManager.RESULT_FAILED)
 
267
        else:
 
268
            os._exit(res)
 
269
 
 
270
    def run(self, *args, **kwargs):
 
271
        log.debug("Run")
 
272
        pid = self._fork()
 
273
        if pid == 0:
 
274
            os.close(self.status_parent_fd)
 
275
            self._child(*args, **kwargs)
 
276
        else:
 
277
            self.child_pid = pid
 
278
            os.close(self.status_child_fd)
 
279
        log.debug("Child pid: %s", pid)
 
280
        watchers = []
 
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,
 
290
                                                 self._copy_io))
 
291
        else:
 
292
            terminal_fd = None
 
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,
 
299
                                             gobject.IO_IN,
 
300
                                             self._on_status_update))
 
301
        while self._child_exit == -1:
 
302
            gobject.main_context_default().iteration()
 
303
        for id in watchers:
 
304
            gobject.source_remove(id)
 
305
        # Restore the settings of the transaction terminal
 
306
        try:
 
307
            termios.tcsettattr(terminal_fd, termios.TCSADRAIN, terminal_attr)
 
308
        except:
 
309
            pass
 
310
        # Make sure all file descriptors are closed
 
311
        for fd in [self.master_fd, self.status_parent_fd, terminal_fd]:
 
312
            try:
 
313
                os.close(fd)
 
314
            except:
 
315
                pass
 
316
        return os.WEXITSTATUS(self._child_exit)
 
317
 
 
318
    def _on_child_exit(self, pid, condition):
 
319
        log.debug("Child exited: %s", condition)
 
320
        self._child_exit = condition
 
321
        return False
 
322
 
 
323
    def _on_status_update(self, source, condition):
 
324
        log.debug("UpdateInterface")
 
325
        status_msg = ""
 
326
        try:
 
327
            while not status_msg.endswith("\n"):
 
328
                self.last_activity = time.time()
 
329
                status_msg += os.read(source, 1)
 
330
        except:
 
331
            return False
 
332
        try:
 
333
            (status, pkg, percent, message_raw) = status_msg.split(":", 3)
 
334
        except ValueError:
 
335
            # silently ignore lines that can't be parsed
 
336
            return True
 
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)
 
345
            if match:
 
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 \
 
352
           self.child_pid:
 
353
            log.critical("Killing child since timeout of %s s", INSTALL_TIMEOUT)
 
354
            os.kill(self.child_pid, 15)
 
355
        return True
 
356
 
 
357
    def _fork(self):
 
358
        """Fork and create a master/slave pty pair by which the forked process
 
359
        can be controlled.
 
360
        """
 
361
        pid, self.master_fd = os.forkpty()
 
362
        if pid == 0:
 
363
            mainloop.quit()
 
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")
 
373
            else:
 
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)
 
388
        return pid
 
389
 
 
390
    def _copy_io_master(self, source, condition, target):
 
391
        if condition == gobject.IO_IN:
 
392
            self.last_activity = time.time()
 
393
            try:
 
394
                char = os.read(source, 1)
 
395
            except OSError:
 
396
                log.debug("Faild to read from master")
 
397
                return True
 
398
            # Write all the output from dpkg to a log
 
399
            if char == "\n":
 
400
                # Skip ANSI characters from the console output
 
401
                line = re.sub(REGEX_ANSI_ESCAPE_CODE, "", self._line_buffer)
 
402
                if line:
 
403
                    log_terminal.debug(line)
 
404
                    self.output += line + "\n"
 
405
                self._line_buffer = ""
 
406
            else:
 
407
                self._line_buffer += char
 
408
            if target:
 
409
                try:
 
410
                    os.write(target, char)
 
411
                except OSError:
 
412
                    log.debug("Failed to write to controlling terminal")
 
413
            return True
 
414
        os.close(source)
 
415
        return False
 
416
 
 
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
 
427
            try:
 
428
                os.write(self.master_fd, char)
 
429
            except:
 
430
                pass
 
431
            else:
 
432
                return True
 
433
        os.close(source)
 
434
        return False
 
435
 
 
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
 
445
 
 
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
 
463
        return True
 
464
 
 
465
    def _error(self, pkg, msg):
 
466
        """Callback for an error"""
 
467
        log.critical("%s: %s" % (pkg, msg))
 
468
 
 
469
 
 
470
class DaemonDpkgInstallProgress(DaemonInstallProgress):
 
471
 
 
472
    """Progress handler for a local Debian package installation."""
 
473
 
 
474
    def __init__(self, transaction, begin=101, end=101):
 
475
        DaemonInstallProgress.__init__(self, transaction, begin, end)
 
476
 
 
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
 
484
         os._exit()
 
485
 
 
486
    def _on_status_update(self, source, condition):
 
487
        log.debug("UpdateInterface")
 
488
        status_raw = ""
 
489
        try:
 
490
            while not status_raw.endswith("\n"):
 
491
                status_raw += os.read(source, 1)
 
492
        except:
 
493
            return False
 
494
        try:
 
495
            status = [s.strip() for s in status_raw.split(":", 3)]
 
496
        except ValueError:
 
497
            # silently ignore lines that can't be parsed
 
498
            return True
 
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])
 
510
                if match:
 
511
                    new, old = match.group(1), match.group(2)
 
512
                    self._conffile(new, old)
 
513
            elif status == "status":
 
514
 
 
515
                self._status_changed(pkg=status[1], percent=101,
 
516
                                     status=status[2])
 
517
        elif status[0] == "processing":
 
518
            try:
 
519
                msg = MAP_STAGE[status[1]] % status[2]
 
520
            except ValueError, IndexError:
 
521
                msg = status[1]
 
522
            self._status_changed(pkg=status[2], percent=101, status=msg)
 
523
 
 
524
 
 
525
class DaemonDpkgRecoverProgress(DaemonDpkgInstallProgress):
 
526
 
 
527
    """Progress handler for dpkg --confiure -a call."""
 
528
 
 
529
    def _child(self):
 
530
        args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd),
 
531
                "--configure", "-a"]
 
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
 
536
        os._exit()
 
537
 
 
538
 
 
539
# vim:ts=4:sw=4:et