Woodchuck: 0d0d87742f5a8ccada5a0afbfa5c4082cca33c9e

     1: #! /usr/bin/env python
     2: 
     3: # Copyright 2011 Neal H. Walfield <neal@walfield.org>
     4: #
     5: # This file is part of Woodchuck.
     6: #
     7: # Woodchuck is free software; you can redistribute it and/or modify it
     8: # under the terms of the GNU General Public License as published by
     9: # the Free Software Foundation; either version 3 of the License, or
    10: # (at your option) any later version.
    11: #
    12: # Woodchuck is distributed in the hope that it will be useful, but WITHOUT
    13: # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    14: # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
    15: # License for more details.
    16: #
    17: # You should have received a copy of the GNU General Public License
    18: # along with this program.  If not, see
    19: # <http://www.gnu.org/licenses/>.
    20: 
    21: from __future__ import with_statement
    22: 
    23: import sys
    24: import os
    25: import errno
    26: import glob
    27: import time
    28: import traceback
    29: import gconf
    30: import dbus
    31: import gobject
    32: import subprocess
    33: import apt
    34: import threading
    35: import curses.ascii
    36: from optparse import OptionParser
    37: from pywoodchuck import PyWoodchuck
    38: import woodchuck
    39: 
    40: from dbus.mainloop.glib import DBusGMainLoop
    41: DBusGMainLoop(set_as_default=True)
    42: 
    43: # Load and configure the logging facilities.
    44: import logging
    45: logger = logging.getLogger(__name__)
    46: 
    47: # Save the logging output to a file.  If the file exceeds
    48: # logfile_max_size, rotate it (and remove the old file).
    49: logfile = os.path.expanduser("~/.apt-woodchuck.log")
    50: logfile_max_size = 1024 * 1024
    51: try:
    52:     logfile_size = os.stat(logfile).st_size
    53: except OSError:
    54:     # Most likely the file does not exist.
    55:     logfile_size = 0
    56: 
    57: if logfile_size > logfile_max_size:
    58:     try:
    59:         old_logfile = logfile + ".old"
    60:         os.rename(logfile, old_logfile)
    61:     except OSError, e:
    62:         print "Renaming %s to %s: %s" % (logfile, old_logfile, str(e))
    63: 
    64: # The desired logging level.
    65: logging_level = logging.DEBUG
    66: #logging_level = logging.INFO
    67: logging.basicConfig(
    68:     level=logging_level,
    69:     format=('%(asctime)s (pid: ' + str(os.getpid()) + '; %(threadName)s) '
    70:             + '%(levelname)-8s %(message)s'),
    71:     filename=logfile,
    72:     filemode='a')
    73: 
    74: def print_and_log(*args):
    75:     """
    76:     Print the arguments to stdout and send them to the log file.
    77:     """
    78:     logger.warn(*args)
    79:     sys.stdout.write(' '.join(args) + "\n")
    80: 
    81: logger.info("APT Woodchuck started (%s)." % (repr(sys.argv)))
    82: 
    83: # Log uncaught exceptions.
    84: original_excepthook = sys.excepthook
    85: 
    86: def my_excepthook(exctype, value, tb):
    87:     """Log uncaught exceptions."""
    88:     print(
    89:         "Uncaught exception: %s"
    90:         % (''.join(traceback.format_exception(exctype, value, tb)),))
    91:     original_excepthook(exctype, value, tb)
    92: sys.excepthook = my_excepthook
    93: 
    94: def redirect(thing):
    95:     filename = os.path.join(os.environ['HOME'], '.apt-woodchuck.' + thing)
    96:     try:
    97:         with open(filename, "r") as fhandle:
    98:             contents = fhandle.read()
    99:     except IOError, e:
   100:         if e.errno in (errno.ENOENT,):
   101:             fhandle = None
   102:             contents = ""
   103:         else:
   104:             logging.error("Reading %s: %s" % (filename, str(e)))
   105:             raise
   106: 
   107:     logging.error("std%s of last run: %s" % (thing, contents))
   108: 
   109:     if fhandle is not None:
   110:         os.remove(filename)
   111: 
   112:     print "Redirecting std%s to %s" % (thing, filename)
   113:     return open(filename, "w", 0)
   114: 
   115: sys.stderr = redirect('err')
   116: sys.stdout = redirect('out')
   117: 
   118: def maemo():
   119:     """Return whether we are running on Maemo."""
   120:     # On Mameo, this file is included in the dpkg package.
   121:     return os.path.isfile('/etc/dpkg/origins/maemo')
   122: 
   123: def ham_check_interval_update(verbose=False):
   124:     """Configure Maemo to never do an update on its own."""
   125:     client = gconf.client_get_default()
   126: 
   127:     check_interval = client.get_int(
   128:         '/apps/hildon/update-notifier/check_interval')
   129:     check_interval_never = (1 << 31) - 1
   130:     if check_interval != check_interval_never:
   131:         print_and_log("Updating check interval from %d to %d seconds"
   132:                       % (check_interval, check_interval_never))
   133:         client.set_int('/apps/hildon/update-notifier/check_interval',
   134:                        check_interval_never)
   135:     elif verbose:
   136:         print_and_log("HAM update check interval set to %d seconds."
   137:                       % check_interval)
   138: 
   139: class Job(threading.Thread):
   140:     """This class implements a simple job manager: Jobs can be queued
   141:     from the main thread and they are executed serially, FIFO."""
   142: 
   143:     # The job that is currently executing, or, None, if none.
   144:     running = None
   145:     # List of queued jobs.
   146:     jobs = []
   147: 
   148:     # A lock to protect the above two variables.
   149:     lock = threading.Lock()
   150: 
   151:     def __init__(self, key, func, func_args=None, **kwargs):
   152:         """Create and queue a job.  kwargs can include the special
   153:         arguments on_start and on_finish, which are methods (which are
   154:         passed no arguments) that will be invoked before the job is
   155:         started or after it is finished."""
   156:         threading.Thread.__init__(self)
   157: 
   158:         self.key = key
   159: 
   160:         if 'on_start' in kwargs:
   161:             self.on_start = kwargs['on_start']
   162:             del kwargs['on_start']
   163:         else:
   164:             self.on_start = None
   165:         if 'on_finish' in kwargs:
   166:             self.on_finish = kwargs['on_finish']
   167:             del kwargs['on_finish']
   168:         else:
   169:             self.on_finish = None
   170:             
   171:         self.func = func
   172:         self.args = func_args if func_args is not None else []
   173:         self.kwargs = kwargs
   174: 
   175:         with self.lock:
   176:             jobs = self.jobs[:]
   177:             if self.__class__.running is not None:
   178:                 jobs.append(self.__class__.running)
   179:             for j in jobs:
   180:                 if j.key == key:
   181:                     logger.debug("Job %s already queued, ignoring." % key)
   182:                     break
   183:             else:
   184:                 logger.debug("Job %s enqueued." % key)
   185:                 self.jobs.append(self)
   186: 
   187:         Job.scheduler()
   188: 
   189:     def run(self):
   190:         with self.lock:
   191:             assert self.__class__.running == self
   192: 
   193:         try:
   194:             logger.debug("Running job %s" % self.key)
   195:             if self.on_start is not None:
   196:                 self.on_start()
   197: 
   198:             self.func(*self.args, **self.kwargs)
   199: 
   200:             if self.on_finish is not None:
   201:                 self.on_finish()
   202:         except Exception, e:
   203:             logger.exception("Executing %s: %s" % (self.key, str(e)))
   204:         finally:
   205:             with self.lock:
   206:                 assert self.__class__.running is self
   207:                 self.__class__.running = None
   208: 
   209:             # Run the next job.
   210:             Job.scheduler()
   211: 
   212:     @classmethod
   213:     def scheduler(cls):
   214:         """Try and execute a job."""
   215:         with cls.lock:
   216:             if cls.running is not None:
   217:                 return
   218: 
   219:             try:
   220:                 job = cls.jobs.pop(0)
   221:             except IndexError:
   222:                 return
   223: 
   224:             cls.running = job
   225:         job.start()
   226: 
   227: APT_CACHE_DIRECTORY = "/var/cache/apt/archives"
   228: 
   229: dbus_service_name = "org.woodchuck.apt-woodchuck"
   230: 
   231: # Class that receives and processes Woodchuck upcalls.
   232: class AptWoodchuck(PyWoodchuck):
   233:     packages_stream_identifier = 'packages'
   234: 
   235:     def __init__(self, daemon):
   236:         # We need to do three things:
   237:         # - Claim our DBus name.
   238:         # - Connect to Woodchuck
   239:         # - Make sure that Woodchuck knows about all of our streams
   240:         #   and objects.  This really only needs to be done the
   241:         #   first time we are run.  If daemon is False, then after we
   242:         #   register, we quit.
   243: 
   244:         # Miscellaneous configuration.
   245: 
   246:         # Whether an update is in progress.
   247:         self.ham_update_check_progress_id = 0
   248: 
   249: 
   250:         if daemon:
   251:             # Claim our DBus service name.
   252:             #
   253:             # We do this as soon as possible to avoid missing messges.
   254:             # (DBus queues messages for 25 seconds.  Sometimes, that is
   255:             # just not enough.)
   256:             try:
   257:                 self.bus_name = dbus.service.BusName(dbus_service_name,
   258:                                                      bus=dbus.SessionBus(),
   259:                                                      do_not_queue=True)
   260:             except dbus.exceptions.NameExistsException, e:
   261:                 print_and_log("Already running (Unable to claim %s: %s)."
   262:                               % (dbus_service_name, str(e)))
   263:                 sys.exit(1)
   264: 
   265: 
   266:         # Connect to Woodchuck.
   267:         #
   268:         # The human readable name will be shown to the user.  Be
   269:         # careful what you choose: you can't easily change it;
   270:         # PyWoodchuck uses both the human readable name AND the dbus
   271:         # service name to identify itself to Woodchuck.
   272:         PyWoodchuck.__init__(
   273:             self, human_readable_name="Application Update Manager",
   274:             dbus_service_name=dbus_service_name,
   275:             request_feedback=daemon)
   276: 
   277:         # Check if Woodchuck is really available.  If not, bail.
   278:         if not self.available():
   279:             print_and_log("Unable to contact Woodchuck server.")
   280:             sys.exit(1)
   281: 
   282: 
   283:         # Register our streams and objects with Woodchuck.
   284:         #
   285:         # This program uses one stream: the 'package' stream.
   286:         # Updating the stream means doing an 'apt-get update'.  We
   287:         # only register packages for which an update is available:
   288:         # these are bits that the user is very likely interested in;
   289:         # the expected utility of prefetching packages that are not
   290:         # installed is low, and we don't have enough disk space to
   291:         # mirror the whole archive anyway.
   292: 
   293:         # Be default, we want to do an apt-get update approximately
   294:         # every day.
   295:         freshness=24 * 60 * 60
   296:         try:
   297:             self.stream_register(
   298:                 stream_identifier=self.packages_stream_identifier,
   299:                 human_readable_name='Packages',
   300:                 freshness=freshness)
   301:         except woodchuck.ObjectExistsError:
   302:             self[self.packages_stream_identifier].freshness = freshness
   303: 
   304:         if not daemon:
   305:             print_and_log("Registered Woodchuck callbacks.")
   306:             sys.exit(0)
   307: 
   308:         # Register packages with Woodchuck for which an update is
   309:         # available.
   310:         self.register_available_updates()
   311: 
   312:     def stream_update_cb(self, stream):
   313:         logger.debug("stream update called on %s" % (str(stream.identifier),))
   314: 
   315:         if stream.identifier != self.packages_stream_identifier:
   316:             logger.info("Unknown stream: %s (%s)"
   317:                         % (stream.human_readable_name, stream.identifier))
   318:             try:
   319:                 del self[stream.identifier]
   320:             except woodchuck.Error, e:
   321:                 logger.info("Removing unknown stream %s: %s"
   322:                             % (stream.identifier, str(e)))
   323:             return
   324: 
   325:         # Start an update.
   326:         Job(key='stream:%s' % (str(stream.identifier),),
   327:             on_start=self.inactive_timeout_remove,
   328:             on_finish=self.inactive_timeout_start,
   329:             func=self.apt_get_update)
   330: 
   331:     def object_transfer_cb(self, stream, object,
   332:                            version, filename, quality):
   333:         logger.debug("object transfer called on stream %s, object %s"
   334:                      % (stream.identifier, object.identifier))
   335: 
   336:         if stream.identifier != self.packages_stream_identifier:
   337:             logger.info("Unknown stream %s" % (stream.identifier))
   338:             try:
   339:                 del self[stream.identifier]
   340:             except woodchuck.Error, e:
   341:                 logger.info("Removing unknown stream %s: %s"
   342:                             % (stream.identifier, str(e)))
   343:             return
   344: 
   345:         # Prefetch the specified object.
   346:         try:
   347:             Job(key='object:%s' % (str(object.identifier),),
   348:                 on_start=self.inactive_timeout_remove,
   349:                 on_finish=self.inactive_timeout_start,
   350:                 func=self.apt_get_download, func_args=[object.identifier])
   351:         except Exception, e:
   352:             logger.exception("Creating Job for object transfer %s: %s"
   353:                              % (object.identifier, str(e),))
   354: 
   355:     # APT package cache management.
   356:     def apt_cache_check_validity(self, force_reload=False):
   357:         """Reload the apt package cache if necessary."""
   358:         if not hasattr(self, '_apt_cache'):
   359:             self._apt_cache = apt.Cache()
   360:         elif (self._apt_cache_last_refreshed + 30 > time.time()
   361:             or force_reload):
   362:             self._apt_cache.open(None)
   363:         else:
   364:             return
   365: 
   366:         self._apt_cache_last_refreshed = time.time()
   367: 
   368:     def apt_cache_invalidate(self):
   369:         """Invalidate the apt package cache forcing a reload the next
   370:         time it is needed."""
   371:         self._apt_cache_last_refreshed = 0
   372: 
   373:     @property
   374:     def apt_cache(self):
   375:         """Return an apt package cache object."""
   376:         self.apt_cache_check_validity()
   377:         return self._apt_cache
   378: 
   379:     def apt_cache_filename(self, package_version,
   380:                            version_string=None, arch_string=None):
   381:         """Return the filename that would be used to save a version of
   382:         a package."""
   383: 
   384:         # Look at
   385:         # apt/apt-pkg/acquire-item.cc:pkgAcqArchive::pkgAcqArchive and
   386:         # apt/apt-pkg/contrib/strutl.cc:QuoteString to see how the
   387:         # filename is created.
   388:         def escape(s, more_bad=''):
   389:             escaped = []
   390:             have_one = False
   391:             for c in s:
   392:                 if (c in ('_:%' + more_bad)
   393:                     or not curses.ascii.isprint(c)
   394:                     or ord(c) <= 0x20 or ord(c) >= 0x7F):
   395:                     have_one = True
   396:                     escaped.append("%%%02x" % ord(c))
   397:                 else:
   398:                     escaped.append(c)
   399: 
   400:             if not have_one:
   401:                 return s
   402: 
   403:             return ''.join(escaped)
   404: 
   405:         try:
   406:             package_name = package_version.package.name
   407: 
   408:             if version_string is None:
   409:                 version_string = package_version.version
   410: 
   411:             if arch_string is None:
   412:                 arch_string = package_version.architecture
   413:         except AttributeError:
   414:             # Assume package_version is a string and that
   415:             # version_string and arch_string are provided.
   416:             package_name = package_version
   417: 
   418:         s = (escape(package_name)
   419:              + '_' + escape(version_string)
   420:              + '_' + escape(arch_string, more_bad='.')
   421:              + '.deb')
   422: 
   423:         logger.debug(
   424:             "Cache file for (%s, %s, %s): %s"
   425:             % (package_name, str(version_string), str(arch_string), s))
   426: 
   427:         return os.path.join(APT_CACHE_DIRECTORY, s)
   428: 
   429:     def apt_get_update(self):
   430:         """Perform an update."""
   431:         logger.info("Running apt-get update")
   432: 
   433:         if self.ham_update_check_progress_id:
   434:             logger.info("apt-get update already in progress.")
   435:             return
   436: 
   437:         transfer_start = time.time()
   438: 
   439:         success = False
   440:         if maemo():
   441:             # On Maemo, we don't directly call apt-get update as the
   442:             # hildon package manager does some other magic, such as
   443:             # updating status files in
   444:             # /home/user/.hildon-application-manager.
   445:             process = subprocess.Popen(
   446:                 args=["/usr/libexec/apt-worker", "check-for-updates"],
   447:                 close_fds=True,
   448:                 stdout=subprocess.PIPE,
   449:                 stderr=subprocess.STDOUT)
   450: 
   451:             # Read stdout (and stderr).
   452:             output = process.stdout.read()
   453:             logger.info("apt-worker(%d): %s" % (process.pid, output))
   454: 
   455:             # Process closed stdout.  Wait for it to finish executing
   456:             # and get the return code.
   457:             exit_code = process.wait()
   458:             logger.info("apt-worker: exit code: %d" % (exit_code,))
   459: 
   460:             if exit_code == 0:
   461:                 success = True
   462:         else:
   463:             # On "generic" Debian, we use apt-get update.
   464:             try:
   465:                 success = self.apt_cache.update()
   466:             except apt.cache.LockFailedException, e:
   467:                 logger.info("apt_get_update: Failed to obtain lock: %s"
   468:                             % str(e))
   469:             except Exception, e:
   470:                 logger.exception("apt_get_update: %s" % repr(e))
   471: 
   472:             logger.info("apt-get update returned: %s" % str(success))
   473: 
   474:         transfer_duration = time.time() - transfer_start
   475:         logger.debug("apt-get update took %d seconds" % (transfer_duration,))
   476: 
   477:         # Force the APT cache database to be reloaded in this thread
   478:         # (rather than blocking the main thread).
   479:         self.apt_cache_check_validity(force_reload=True)
   480: 
   481:         # We can only execute Woodchuck calls in the main thread as
   482:         # DBus is not thread safe.  gobject.idle_add is thread safe.
   483:         # We use it to register a callback in the main thread.
   484:         def report_to_woodchuck():
   485:             if success:
   486:                 new_updates = self.register_available_updates()
   487: 
   488:                 self[self.packages_stream_identifier].updated(
   489:                     # Would be nice to get these values:
   490:                     # transferred_up=..., transferred_down=...,
   491:                     transfer_time=transfer_start,
   492:                     transfer_duration=transfer_duration,
   493:                     new_objects=new_updates,
   494:                     objects_inline=0)
   495:             else:
   496:                 self[self.packages_stream_identifier].update_failed(
   497:                     reason=woodchuck.TransferStatus.TransientOther
   498:                     #transferred_up=0, transferred_down=0
   499:                     )
   500: 
   501:         gobject.idle_add(report_to_woodchuck)
   502: 
   503:     def register_available_updates(self):
   504:         """Determine the set of installed packages that can be updated
   505:         and register them with Woodchuck."""
   506:         logger.info("Synchronizing available updates with Woodchuck.")
   507: 
   508:         new_updates = 0
   509: 
   510:         # The packages registered with Woodchuck.
   511:         registered_packages = self[self.packages_stream_identifier].keys()
   512:         logger.debug("Packages registered with Woodchuck: %s"
   513:                      % (str(registered_packages),))
   514: 
   515:         for pkg in self.apt_cache:
   516:             version = pkg.candidate
   517: 
   518:             if pkg.isUpgradable:
   519:                 # The package is upgradable.  Either: we haven't yet
   520:                 # registered the package with Woodchuck, or we have.
   521:                 # In the latter case, we still might have to do
   522:                 # something if there is a newer version available.
   523:                 if pkg.name not in registered_packages:
   524:                     # The package is not yet registered with
   525:                     # Woodchuck.  Do so.
   526:                     logger.info("Registering %s package update" % (pkg.name,))
   527:                     try:
   528:                         self[self.packages_stream_identifier].object_register(
   529:                             object_identifier=pkg.name,
   530:                             human_readable_name=pkg.name,
   531:                             expected_size=version.size)
   532:                         new_updates += 1
   533:                     except woodchuck.Error, e:
   534:                         logger.error("Registering package %s: %s"
   535:                                      % (pkg.name, str(e)))
   536:                 else:
   537:                     # The package is already registered.
   538:                     #
   539:                     # There are a few scenarios:
   540:                     #
   541:                     # - The package has been downloaded but not yet
   542:                     #   updated.  There is nothing to do, but we
   543:                     #   should be careful to not trigger it to be
   544:                     #   downloaded again (e.g., by claiming there is
   545:                     #   an update available by setting NeedUpdate to
   546:                     #   true.)
   547:                     #
   548:                     # - An old version of the package has been
   549:                     #   downloaded.  We need to tell Woodchuck to
   550:                     #   download the object again so that we have the
   551:                     #   latest version.
   552:                     #
   553:                     # - The package has not been downloaded yet.
   554:                     #   There is nothing to do (setting NeedUpdate in
   555:                     #   this case doesn't hurt).  Eventually,
   556:                     #   Woodchuck will tell us to download the object.
   557: 
   558:                     # Check if the version we want is downloaded.
   559:                     filename = self.apt_cache_filename(version)
   560:                     if os.path.isfile(filename):
   561:                         # The latest version is available.  There is
   562:                         # nothing to do.
   563:                         pass
   564:                     else:
   565:                         logger.debug("Setting '%s'.NeedUpdate" % (pkg.name,))
   566:                         try:
   567:                             self[self.packages_stream_identifier][pkg.name] \
   568:                                 .need_update = True
   569:                         except (KeyError, woodchuck.Error), e:
   570:                             logger.error("Setting %s.NeedUpdate: %s"
   571:                                          % (pkg.name, str(e)))
   572: 
   573:                     registered_packages.remove(pkg.name)
   574: 
   575:         logger.debug("Unregistering objects %s" % (registered_packages,))
   576:         for package_name in registered_packages:
   577:             # The package is registered, but is no longer upgradable.
   578:             # This is likely because the user installed the updated
   579:             # (although it may be just have become uninstallable).
   580:             # Remove any cache files and unregister the object.
   581:             logger.debug("Unregistering object %s" % (package_name,))
   582: 
   583:             for filename in glob.glob(
   584:                 self.apt_cache_filename(str(package_name), '*', '*')):
   585:                 try:
   586:                     logger.debug("Removing %s" % (filename,))
   587:                     os.remove(filename)
   588:                 except OSError, e:
   589:                     logger.error("Removing %s: %s" % (filename, str(e)))
   590: 
   591:             try:
   592:                 del self[self.packages_stream_identifier][package_name]
   593:             except (KeyError, woodchuck.Error), e:
   594:                 logger.info("Unregistering object %s: %s"
   595:                             % (package_name, str(e)))
   596: 
   597:         return new_updates
   598: 
   599:     def apt_get_download(self, package_name):
   600:         """Download the specified package."""
   601:         try:
   602:             pkg = self.apt_cache[package_name]
   603:         except KeyError:
   604:             logger.error("Package %s unknown." % (package_name,))
   605:             return
   606: 
   607:         version = pkg.candidate
   608: 
   609:         transfer_time = time.time()
   610: 
   611:         logger.info("Downloading package %s" % (package_name,))
   612:         try:
   613:             filename = version.fetch_binary(APT_CACHE_DIRECTORY)
   614:             logger.debug("Downloaded %s -> %s" % (package_name, filename))
   615:             if filename is None:
   616:                 # If the binary is already downloaded, filename is set
   617:                 # to None.  If this really looks like the case,
   618:                 # succeed.
   619:                 filenames \
   620:                     = glob.glob(self.apt_cache_filename(pkg.name, '*', '*'))
   621:                 if filenames:
   622:                     filename = filenames[0]
   623:                     ok = True
   624:                 else:
   625:                     logger.error("Downloading or saving package %s failed."
   626:                                  % package_name)
   627:                     ok = False
   628:             else:
   629:                 ok = True
   630:         except Exception, e:
   631:             logger.exception("Downloading %s: %s" % (package_name, str(e)))
   632:             ok = False
   633: 
   634:         transfer_duration = time.time() - transfer_time
   635: 
   636:         def register_result():
   637:             try:
   638:                  if ok:
   639:                      self[self.packages_stream_identifier].object_transferred(
   640:                          object_identifier=package_name,
   641:                          # Would be nice to get these values:
   642:                          # transferred_up=0, transferred_down=0,
   643:                          transfer_time=transfer_time,
   644:                          transfer_duration=transfer_duration,
   645:                          object_size=version.size,
   646:                          files=[(filename,
   647:                                  True, # The file is not shared.
   648:                                  woodchuck.DeletionPolicy.DeleteWithoutConsultation),
   649:                                 ])
   650:                  else:
   651:                      self[self.packages_stream_identifier].object_transfer_failed(
   652:                          object_identifier=package_name,
   653:                          # Assume the failure is transient.
   654:                          reason=woodchuck.TransferStatus.TransientOther,
   655:                          transferred_up=0, transferred_down=0)
   656:             except Exception, e:
   657:                 logger.exception("Reporting transfer of %s: %s"
   658:                                  % (package_name, str(e)))
   659:             except woodchuck.Error, e:
   660:                 logger.exception("Reporting transfer of %s: %s"
   661:                                  % (package_name, str(e)))
   662:         gobject.idle_add(register_result)
   663: 
   664:     # Inactivity manager: if there is no activity for a while (in
   665:     # milliseconds), quit.
   666:     inactive_duration = 5 * 60 * 1000
   667: 
   668:     inactive_timeout_id = None
   669:     def inactive_timeout(self):
   670:         """The timer fired.  Quit."""
   671:         print_and_log("Inactive, quitting.")
   672:         mainloop.quit()
   673:         self.inactive_timeout_remove()
   674: 
   675:     def inactive_timeout_remove(self):
   676:         """Remove any timer, e.g., because there was some activity."""
   677:         if self.inactive_timeout_id:
   678:             gobject.source_remove(self.inactive_timeout_id)
   679:             self.inactive_timeout_id = None
   680:     
   681:     def inactive_timeout_start(self):
   682:         """Start (or, it if is already running, reset) the timer."""
   683:         self.inactive_timeout_remove()
   684:         self.inactive_timeout_id \
   685:             = gobject.timeout_add(self.inactive_duration, self.inactive_timeout)
   686: 
   687: def main():
   688:     # Command line options.
   689:     description="""\
   690: APT Woodchuck, a package updater for Woodchuck.
   691: 
   692: APT Woodchuck updates APT's list of packages and downloads package
   693: updates when a good Internet connection is available as determined by
   694: Woodchuck.
   695: 
   696: When run as a daemon (--daemon), waits for instructions from the
   697: Woodchuck server.  APT Woodchuck automatically quits if it has nothing
   698: to do a few minutes.
   699: 
   700: When run without any options, APT Woodchuck registers with the
   701: Woodchuck server and disables the Hildon Application Manager's
   702: automatic updates.  NOTE: APT Woodchuck must be run once in order to
   703: make Woodchuck aware of APT Woodchuck.
   704: 
   705: Messages are logged to $HOME/.apt-woodchuck.log.
   706: """
   707:     parser = OptionParser(description=description)
   708:     # By default, the description is refilled without respecting
   709:     # paragraph boundaries.  Override this.
   710:     parser.format_description = lambda self: description
   711:     parser.add_option("-d", "--daemon", action="store_true", default=False,
   712:                       help="Run in daemon mode.")
   713: 
   714:     (options, args) = parser.parse_args()
   715:     if args:
   716:         print "Unknown arguments: %s" % (str(args))
   717:         parser.print_help()
   718:         sys.exit(1)
   719: 
   720:     if maemo():
   721:         ham_check_interval_update(verbose=not options.daemon)
   722: 
   723:     wc = AptWoodchuck(options.daemon)
   724: 
   725: gobject.threads_init()
   726: gobject.idle_add(main)
   727: mainloop = gobject.MainLoop()
   728: mainloop.run()

Generated by git2html.