Offload all downloading logic to updatefeed.py.
authorNeal H. Walfield <neal@walfield.org>
Tue, 23 Aug 2011 15:57:16 +0000 (17:57 +0200)
committerNeal H. Walfield <neal@walfield.org>
Tue, 23 Aug 2011 16:10:55 +0000 (18:10 +0200)
 - Change Woodchuck initialization to only process upcalls if started
   from updatefeed.

 - Add org.marcoz.feedingit.UpdateProgress.

 - In rss_sqlite.py, detect if we are running as updatefeed.py:
  - If so:
   - Perform downloads directly, and
   - Make the appropriate progress calls.
  - Otherwise:
   - Don't update feeds directly, instead, make the appropriate dbus
     calls to updatefeed.py (starting it as necessary).

 - updatedbus.py:
  - Remove get_lock and release_lock, which is no longer needed as
    updating is now centralized.
  - Add dbus methods Update and UpdateProgress.
  - Make methods UpdateAll and StopUpdate abstract.
  - Add function update_server_object to retrieve any UpdateServerObject
  - Ensure that there is only a single instance of UpdateServerObject.

 - update_feeds.py:
  - Move system idle detection from FeedingIt.py to here.
  - Add a '--daemon' option, which does not update all feeds on start.
  - Implement Update, UpdateAll and StopUpdate
  - Make update notifications as appropriate.
  - Update locking mechanism: grab the bus name without queueing.

 - FeedingIt.py:
  - Adapt progress logic appropriately.
  - Don't detect when the program is started only for an update (it
    isn't anymore)

 - FeedingIt:
  - Start updatefeeds.py with the --daemon option.

src/FeedingIt
src/FeedingIt-Web.py
src/FeedingIt.py
src/rss_sqlite.py
src/update_feeds.py
src/updatedbus.py
src/wc.py

index ea85e33..93abe74 100644 (file)
@@ -24,7 +24,7 @@ status)
 dbus)
        cd /opt/FeedingIt
        #cp feedingit_status.desktop /usr/share/applications/hildon-status-menu/
-       nice python2.5 update_feeds.py
+       nice python2.5 update_feeds.py --daemon
        ;;
 noqml)
        start_gtk
index 3ea91c5..8fe2c31 100644 (file)
@@ -148,7 +148,6 @@ class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
         if markAllAsRead=="True":
             feed.markAllAsRead()
             listing.updateUnread(key)
-            updateDbusHandler.ArticleCountUpdated()
         xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?><xml>"
         if onlyUnread == "False":
             onlyUnread = False
@@ -213,7 +212,6 @@ class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
             feed = listing.getFeed(key)
             feed.setEntryRead(article)
             listing.updateUnread(key)
-            updateDbusHandler.ArticleCountUpdated()
             self.send_response(200)
             self.send_header("Content-type", "text/html")
             self.end_headers()
@@ -254,7 +252,6 @@ class Handler(BaseHTTPServer.BaseHTTPRequestHandler):
         elif request[1]=="updateAll":
             #app.automaticUpdate()
             self.updateAll()
-            updateDbusHandler.ArticleCountUpdated()
             xml = "<xml>OK</xml>"
         elif request[1] == "addCat":
             catName = request[2]
@@ -287,7 +284,7 @@ import thread
 
 # Initialize the glib mainloop, for dbus purposes
 from feedingitdbus import ServerObject
-from updatedbus import UpdateServerObject, get_lock
+from updatedbus import UpdateServerObject
 
 import gobject
 gobject.threads_init()
@@ -298,10 +295,8 @@ mainthread.init()
 from jobmanager import JobManager
 JobManager(True)
 
-global updateDbusHandler, dbusHandler
 app = App()
 dbusHandler = ServerObject(app)
-updateDbusHandler = UpdateServerObject(app)
 
 # Start the HTTP server in a new thread
 thread.start_new_thread(start_server, ())
index 3fcf18a..a48251f 100644 (file)
@@ -42,10 +42,10 @@ from aboutdialog import HeAboutDialog
 from portrait import FremantleRotation
 from threading import Thread, activeCount
 from feedingitdbus import ServerObject
-from updatedbus import UpdateServerObject, get_lock
 from config import Config
 from cgi import escape
 import weakref
+import dbus
 import debugging
 import logging
 logger = logging.getLogger(__name__)
@@ -54,7 +54,6 @@ from rss_sqlite import Listing
 from opml import GetOpmlData, ExportOpmlData
 
 import mainthread
-from jobmanager import JobManager
 
 from socket import setdefaulttimeout
 timeout = 5
@@ -283,10 +282,6 @@ class DownloadBar(gtk.ProgressBar):
         if hasattr (cls, 'class_init_done'):
             return
 
-        jm = JobManager ()
-        jm.stats_hook_register (cls.update_progress,
-                                run_in_main_thread=True)
-
         cls.downloadbars = []
         # Total number of jobs we are monitoring.
         cls.total = 0
@@ -297,6 +292,13 @@ class DownloadBar(gtk.ProgressBar):
 
         cls.class_init_done = True
 
+        bus = dbus.SessionBus()
+        bus.add_signal_receiver(handler_function=cls.update_progress,
+                                bus_name=None,
+                                signal_name='UpdateProgress',
+                                dbus_interface='org.marcoz.feedingit',
+                                path='/org/marcoz/feedingit/update')
+
     def __init__(self, parent):
         self.class_init ()
 
@@ -309,43 +311,43 @@ class DownloadBar(gtk.ProgressBar):
 
     @classmethod
     def downloading(cls):
-        return hasattr (cls, 'jobs_at_start')
+        cls.class_init ()
+        return cls.done != cls.total
 
     @classmethod
-    def update_progress(cls, jm, old_stats, new_stats, updated_feed):
-        if not cls.downloading():
-            cls.jobs_at_start = old_stats['jobs-completed']
-
+    def update_progress(cls, percent_complete,
+                        completed, in_progress, queued,
+                        bytes_downloaded, bytes_updated, bytes_per_second,
+                        feed_updated):
         if not cls.downloadbars:
             return
 
-        if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
-            del cls.jobs_at_start
+        cls.total = completed + in_progress + queued
+        cls.done = completed
+        cls.progress = percent_complete / 100.
+        if cls.progress < 0: cls.progress = 0
+        if cls.progress > 1: cls.progress = 1
+
+        if feed_updated:
             for ref in cls.downloadbars:
                 bar = ref ()
                 if bar is None:
                     # The download bar disappeared.
                     cls.downloadbars.remove (ref)
                 else:
-                    bar.emit("download-done", None)
-            return
+                    bar.emit("download-done", feed_updated)
 
-        # This should never be called if new_stats['jobs'] is 0, but
-        # just in case...
-        cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
-        cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
-        cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
-                            + new_stats['jobs-queued']) / cls.total
-        cls.update_bars()
-
-        if updated_feed:
+        if in_progress == 0 and queued == 0:
             for ref in cls.downloadbars:
                 bar = ref ()
                 if bar is None:
                     # The download bar disappeared.
                     cls.downloadbars.remove (ref)
                 else:
-                    bar.emit("download-done", updated_feed)
+                    bar.emit("download-done", None)
+            return
+
+        cls.update_bars()
 
     @classmethod
     def update_bars(cls):
@@ -716,7 +718,7 @@ class DisplayArticle(hildon.StackableWindow):
             iface.open_new_window(link)
 
 class DisplayFeed(hildon.StackableWindow):
-    def __init__(self, listing, feed, title, key, config, updateDbusHandler):
+    def __init__(self, listing, feed, title, key, config):
         hildon.StackableWindow.__init__(self)
         self.listing = listing
         self.feed = feed
@@ -725,7 +727,6 @@ class DisplayFeed(hildon.StackableWindow):
         self.key=key
         self.current = list()
         self.config = config
-        self.updateDbusHandler = updateDbusHandler
         
         self.downloadDialog = False
         
@@ -956,8 +957,12 @@ class DisplayFeed(hildon.StackableWindow):
         #self.feed.saveFeed(CONFIGDIR)
         self.displayFeed()
 
-    def button_update_clicked(self, button):
+
+    def do_update_feed(self):
         self.listing.updateFeed (self.key, priority=-1)
+
+    def button_update_clicked(self, button):
+        gobject.idle_add(self.do_update_feed)
             
     def show_download_bar(self):
         if not type(self.downloadDialog).__name__=="DownloadBar":
@@ -968,12 +973,13 @@ class DisplayFeed(hildon.StackableWindow):
             self.show_all()
         
     def onDownloadDone(self, widget, feed):
-        if feed == self.feed or feed is None:
-            self.downloadDialog.destroy()
-            self.downloadDialog = False
+        if feed == self.feed:
             self.feed = self.listing.getFeed(self.key)
             self.displayFeed()
-            self.updateDbusHandler.ArticleCountUpdated()
+
+        if feed is None:
+            self.downloadDialog.destroy()
+            self.downloadDialog = False
 
     def buttonReadAllClicked(self, button):
         #self.clear()
@@ -1011,32 +1017,8 @@ class FeedingIt:
         self.config = Config(self.window, CONFIGDIR+"config.ini")
         gobject.idle_add(self.createWindow)
 
-        # This is set to try when the user interacts with the program.
-        # If, after an update is complete, we discover that the
-        # environment variable DBUS_STARTED_ADDRESS is set and
-        # self.had_interaction is False, we quit.
-        self.had_interaction = False
-        
     def createWindow(self):
         self.category = 0
-        
-        self.app_lock = get_lock("app_lock")
-        if self.app_lock == None:
-            try:
-                self.stopButton.set_sensitive(True)
-            except:
-                self.stopButton = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
-                self.stopButton.set_text("Stop update","")
-                self.stopButton.connect("clicked", self.stop_running_update)
-                self.mainVbox.pack_end(self.stopButton, expand=False, fill=False)
-                self.window.show_all()
-            self.introLabel.set_label("Update in progress, please wait.")
-            gobject.timeout_add_seconds(3, self.createWindow)
-            return False
-        try:
-            self.stopButton.destroy()
-        except:
-            pass
         self.listing = Listing(self.config, CONFIGDIR)
 
         self.downloadDialog = False
@@ -1112,11 +1094,11 @@ class FeedingIt:
         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
         gobject.idle_add(self.late_init)
         
-    def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
-        if (not self.downloadDialog
-            and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
-            self.updateDbusHandler.UpdateStarted()
-
+    def update_progress(self, percent_complete,
+                        completed, in_progress, queued,
+                        bytes_downloaded, bytes_updated, bytes_per_second,
+                        updated_feed):
+        if (in_progress or queued) and not self.downloadDialog:
             self.downloadDialog = DownloadBar(self.window)
             self.downloadDialog.connect("download-done", self.onDownloadDone)
             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
@@ -1130,72 +1112,17 @@ class FeedingIt:
             self.downloadDialog.destroy()
             self.downloadDialog = False
             self.displayListing()
-            self.updateDbusHandler.UpdateFinished()
-            self.updateDbusHandler.ArticleCountUpdated()
-
-        if not self.had_interaction and 'DBUS_STARTER_ADDRESS' in environ:
-            logger.info(
-                "Update complete. No interaction, started by dbus: quitting.")
-            self.quit()
-    def stop_running_update(self, button):
-        self.stopButton.set_sensitive(False)
-        import dbus
-        bus=dbus.SessionBus()
-        remote_object = bus.get_object("org.marcoz.feedingit", # Connection name
-                               "/org/marcoz/feedingit/update" # Object's path
-                              )
-        iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
-        iface.StopUpdate()
-
-    def increase_download_parallelism(self):
-        # The system has been idle for a while.  Enable parallel
-        # downloads.
-        JobManager().num_threads = 4
-        gobject.source_remove (self.increase_download_parallelism_id)
-        del self.increase_download_parallelism_id
-        return False
-
-    def system_inactivity_ind(self, idle):
-        # The system's idle state changed.
-        if (self.am_idle and idle) or (not self.am_idle and not idle):
-            # No change.
-            return
-
-        if not idle:
-            if hasattr (self, 'increase_download_parallelism_id'):
-                gobject.source_remove (self.increase_download_parallelism_id)
-                del self.increase_download_parallelism_id
-        else:
-            self.increase_download_parallelism_id = \
-                gobject.timeout_add_seconds(
-                    60, self.increase_download_parallelism)
-
-        if not idle:
-            JobManager().num_threads = 1
-
-        self.am_idle = idle
 
     def late_init(self):
         self.dbusHandler = ServerObject(self)
-        self.updateDbusHandler = UpdateServerObject(self)
-
-        jm = JobManager()
-        jm.stats_hook_register (self.job_manager_update,
-                                run_in_main_thread=True)
-        jm.num_threads = 1
-        self.am_idle = False
-        JobManager(True)
-
-        import dbus
-        bus = dbus.SystemBus()
-        proxy = bus.get_object('com.nokia.mce',
-                               '/com/nokia/mce/signal')
-        iface = dbus.Interface(proxy, 'com.nokia.mce.signal')
-        iface.connect_to_signal('system_inactivity_ind',
-                                self.system_inactivity_ind)
+        bus = dbus.SessionBus()
+        bus.add_signal_receiver(handler_function=self.update_progress,
+                                bus_name=None,
+                                signal_name='UpdateProgress',
+                                dbus_interface='org.marcoz.feedingit',
+                                path='/org/marcoz/feedingit/update')
 
     def button_markAll(self, button):
-        self.had_interaction = True
         for key in self.listing.getListOfFeeds():
             feed = self.listing.getFeed(key)
             feed.markAllAsRead()
@@ -1205,7 +1132,6 @@ class FeedingIt:
         self.displayListing()
 
     def button_about_clicked(self, button):
-        self.had_interaction = True
         HeAboutDialog.present(self.window, \
                 __appname__, \
                 ABOUT_ICON, \
@@ -1217,11 +1143,9 @@ class FeedingIt:
                 ABOUT_DONATE)
 
     def button_export_clicked(self, button):
-        self.had_interaction = True
         opml = ExportOpmlData(self.window, self.listing)
         
     def button_import_clicked(self, button):
-        self.had_interaction = True
         opml = GetOpmlData(self.window)
         feeds = opml.getData()
         for (title, url) in feeds:
@@ -1229,7 +1153,6 @@ class FeedingIt:
         self.displayListing()
 
     def addFeed(self, urlIn="http://"):
-        self.had_interaction = True
         wizard = AddWidgetWizard(self.window, self.listing, urlIn, self.listing.getListOfCategories())
         ret = wizard.run()
         if ret == 2:
@@ -1240,26 +1163,23 @@ class FeedingIt:
         self.displayListing()
 
     def button_organize_clicked(self, button):
-        self.had_interaction = True
         def after_closing():
             self.displayListing()
         SortList(self.window, self.listing, self, after_closing)
 
-    def button_update_clicked(self, button, key):
-        self.had_interaction = True
+    def do_update_feeds(self):
         for k in self.listing.getListOfFeeds():
             self.listing.updateFeed (k)
-        #self.displayListing()
+
+    def button_update_clicked(self, button, key):
+        gobject.idle_add(self.do_update_feeds)
 
     def onDownloadsDone(self, *widget):
         self.downloadDialog.destroy()
         self.downloadDialog = False
         self.displayListing()
-        self.updateDbusHandler.UpdateFinished()
-        self.updateDbusHandler.ArticleCountUpdated()
 
     def button_preferences_clicked(self, button):
-        self.had_interaction = True
         dialog = self.config.createDialog()
         dialog.connect("destroy", self.prefsClosed)
 
@@ -1353,7 +1273,6 @@ class FeedingIt:
         #    pass
 
     def on_feedList_row_activated(self, treeview, path, column):
-        self.had_interaction = True
         model = treeview.get_model()
         iter = model.get_iter(path)
         key = model.get_value(iter, COLUMN_KEY)
@@ -1377,59 +1296,24 @@ class FeedingIt:
                 self.openFeed(key)
             
     def openFeed(self, key):
-        try:
-            self.feed_lock
-        except:
-            # If feed_lock doesn't exist, we can open the feed, else we do nothing
-            if key != None:
-                self.feed_lock = get_lock(key)
-                self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
-                        self.listing.getFeedTitle(key), key, \
-                        self.config, self.updateDbusHandler)
-                self.disp.connect("feed-closed", self.onFeedClosed)
+        if key != None:
+            self.disp = DisplayFeed(
+                self.listing, self.listing.getFeed(key),
+                self.listing.getFeedTitle(key), key,
+                self.config)
+            self.disp.connect("feed-closed", self.onFeedClosed)
                 
     def openArticle(self, key, id):
-        try:
-            self.feed_lock
-        except:
-            # If feed_lock doesn't exist, we can open the feed, else we do nothing
-            if key != None:
-                self.feed_lock = get_lock(key)
-                self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), \
-                        self.listing.getFeedTitle(key), key, \
-                        self.config, self.updateDbusHandler)
-                self.disp.button_clicked(None, id)
-                self.disp.connect("feed-closed", self.onFeedClosed)
-        
+        if key != None:
+            self.openFeed(key)
+            self.disp.button_clicked(None, id)
 
     def onFeedClosed(self, object, key):
-        #self.listing.saveConfig()
-        #del self.feed_lock
-        gobject.idle_add(self.onFeedClosedTimeout)
         self.displayListing()
-        #self.updateDbusHandler.ArticleCountUpdated()
         
-    def onFeedClosedTimeout(self):
-        del self.feed_lock
-        self.updateDbusHandler.ArticleCountUpdated()
-
     def quit(self, *args):
         self.window.hide()
-
-        if hasattr (self, 'app_lock'):
-            del self.app_lock
-
-        # Wait until all slave threads have properly exited before
-        # terminating the mainloop.
-        jm = JobManager()
-        jm.quit ()
-        stats = jm.stats()
-        if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
-            gtk.main_quit ()
-        else:
-            gobject.timeout_add(500, self.quit)
-
-        return False
+        gtk.main_quit ()
 
     def run(self):
         self.window.connect("destroy", self.quit)
@@ -1470,13 +1354,6 @@ class FeedingIt:
         self.button_update_clicked(None, None)
         return True
     
-    def stopUpdate(self):
-        # Not implemented in the app (see update_feeds.py)
-        try:
-            JobManager().cancel ()
-        except:
-            pass
-    
     def getStatus(self):
         status = ""
         for key in self.listing.getListOfFeeds():
index e989a1a..8ad30da 100644 (file)
@@ -36,10 +36,12 @@ import urllib2
 from BeautifulSoup import BeautifulSoup
 from urlparse import urljoin
 from calendar import timegm
-from updatedbus import get_lock, release_lock
 import threading
 import traceback
 from wc import wc, wc_init, woodchuck
+import subprocess
+import dbus
+from updatedbus import update_server_object
 
 from jobmanager import JobManager
 import mainthread
@@ -69,6 +71,14 @@ def downloader(progress_handler=None, proxy=None):
 
     return urllib2.build_opener (*openers)
 
+# If not None, a subprocess.Popen object corresponding to a
+# update_feeds.py process.
+update_feed_process = None
+
+update_feeds_iface = None
+
+jobs_at_start = 0
+
 class Feed:
     serial_execution_lock = threading.Lock()
 
@@ -130,22 +140,61 @@ class Feed:
         return filename
 
     def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False, priority=0, postFeedUpdateFunc=None, *postFeedUpdateFuncArgs):
-        def doit():
-            def it():
-                self._updateFeed(configdir, url, etag, modified, expiryTime, proxy, imageCache, postFeedUpdateFunc, *postFeedUpdateFuncArgs)
-            return it
-        JobManager().execute(doit(), self.key, priority=priority)
+        if (os.path.basename(sys.argv[0]) == 'update_feeds.py'):
+            def doit():
+                def it():
+                    self._updateFeed(configdir, url, etag, modified, expiryTime, proxy, imageCache, postFeedUpdateFunc, *postFeedUpdateFuncArgs)
+                return it
+            JobManager().execute(doit(), self.key, priority=priority)
+        else:
+            def send_update_request():
+                global update_feeds_iface
+                if update_feeds_iface is None:
+                    bus=dbus.SessionBus()
+                    remote_object = bus.get_object(
+                        "org.marcoz.feedingit", # Connection name
+                        "/org/marcoz/feedingit/update" # Object's path
+                        )
+                    update_feeds_iface = dbus.Interface(
+                        remote_object, 'org.marcoz.feedingit')
+
+                try:
+                    update_feeds_iface.Update(self.key)
+                except Exception, e:
+                    logger.error("Invoking org.marcoz.feedingit.Update: %s"
+                                 % str(e))
+                    update_feeds_iface = None
+                else:
+                    return True
+
+            if send_update_request():
+                # Success!  It seems we were able to start the update
+                # daemon via dbus (or, it was already running).
+                return
+
+            global update_feed_process
+            if (update_feed_process is None
+                or update_feed_process.poll() is not None):
+                # The update_feeds process is not running.  Start it.
+                update_feeds = os.path.join(os.path.dirname(__file__),
+                                            'update_feeds.py')
+                argv = ['/usr/bin/env', 'python', update_feeds, '--daemon' ]
+                logger.debug("Starting update_feeds: running %s"
+                             % (str(argv),))
+                update_feed_process = subprocess.Popen(argv)
+                # Make sure the dbus calls go to the right process:
+                # rebind.
+                update_feeds_iface = None
+
+            for _ in xrange(5):
+                if send_update_request():
+                    break
+                time.sleep(1)
 
     def _updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False, postFeedUpdateFunc=None, *postFeedUpdateFuncArgs):
         success = False
         have_serial_execution_lock = False
         try:
-            update_lock = None
-            update_lock = get_lock("key")
-            if not update_lock:
-                # Someone else is doing an update.
-                return
-
             download_start = time.time ()
 
             progress_handler = HTTPProgressHandler(download_callback)
@@ -431,9 +480,6 @@ class Feed:
             if have_serial_execution_lock:
                 self.serial_execution_lock.release ()
 
-            if update_lock is not None:
-                release_lock (update_lock)
-
             updateTime = 0
             try:
                 rows = self.db.execute("SELECT MAX(date) FROM feed;")
@@ -722,8 +768,9 @@ class Listing:
 
         # Check that Woodchuck's state is up to date with respect our
         # state.
-        wc_init (self)
-        if wc().available():
+        updater = os.path.basename(sys.argv[0]) == 'update_feeds.py'
+        wc_init (self, True if updater else False)
+        if wc().available() and updater:
             # The list of known streams.
             streams = wc().streams_list ()
             stream_ids = [s.identifier for s in streams]
@@ -849,6 +896,23 @@ class Listing:
                             (title, key))
         self.db.commit()
         self.updateUnread(key)
+
+        update_server_object().ArticleCountUpdated()
+
+        stats = JobManager().stats()
+        global jobs_at_start
+        completed = stats['jobs-completed'] - jobs_at_start
+        in_progress = stats['jobs-in-progress']
+        queued = stats['jobs-queued']
+
+        percent = (100 * ((completed + in_progress / 2.))
+                   / (completed + in_progress + queued))
+
+        update_server_object().UpdateProgress(
+            percent, completed, in_progress, queued, 0, 0, 0, key)
+
+        if in_progress == 0 and queued == 0:
+            jobs_at_start = stats['jobs-completed']
         
     def getFeed(self, key):
         if key == "ArchivedArticles":
index 8159498..4a7ffad 100644 (file)
 
 from rss_sqlite import Listing
 from config import Config
-from updatedbus import get_lock, UpdateServerObject
+from updatedbus import UpdateServerObject
 
 import os
-import gobject
 import traceback
+import sys
+import dbus
 
 from jobmanager import JobManager
 import mainthread
 
+import gobject
+gobject.threads_init()
+
 import logging
 logger = logging.getLogger(__name__)
 import debugging
@@ -42,58 +46,205 @@ debugging.init(dot_directory=".feedingit", program_name="update_feeds")
 
 CONFIGDIR="/home/user/.feedingit/"
 #DESKTOP_FILE = "/usr/share/applications/hildon-status-menu/feedingit_status.desktop"
-dbug = False
 
 from socket import setdefaulttimeout
 timeout = 5
 setdefaulttimeout(timeout)
 del timeout
 
-from updatedbus import UpdateServerObject
+class FeedUpdate(UpdateServerObject):
+    def __init__(self, bus_name):
+        UpdateServerObject.__init__(self, bus_name)
 
-class FeedUpdate():
-    def __init__(self):
         self.config = Config(self, CONFIGDIR+"config.ini")
-        self.dbusHandler = UpdateServerObject(self)
         self.listing = Listing(self.config, CONFIGDIR)
-        self.done = False
 
         jm = JobManager(True)
         jm.stats_hook_register (self.job_manager_update,
                                 run_in_main_thread=True)
 
-        self.dbusHandler.UpdateStarted()
-        for k in self.listing.getListOfFeeds():
-            self.listing.updateFeed (k)
-        
+        # Whether or no an update is in progress.
+        self.am_updating = False
+
+        # After an update an finished, we start the inactivity timer.
+        # If this fires before a new job arrives, we quit.
+        self.inactivity_timer = 0
+
+        # Whether we started in daemon mode, or not.
+        self.daemon = '--daemon' in sys.argv
+
+        if self.daemon:
+            logger.debug("Running in daemon mode: waiting for commands.")
+            self.inactivity_timer = gobject.timeout_add(
+                5 * 60 * 1000, self.inactivity_cb)
+        else:
+            # Update all feeds.
+            logger.debug("Not running in daemon mode: updating all feeds.")
+            gobject.idle_add(self.UpdateAll)
+
+        # If the system becomes idle
+        bus = dbus.SystemBus()
+
+        mce_request_proxy = bus.get_object(
+            'com.nokia.mce', '/com/nokia/mce/request')
+        mce_request_iface = dbus.Interface(
+            mce_request_proxy, 'com.nokia.mce.request')
+        system_idle = mce_request_iface.get_inactivity_status()
+        # Force self.system_inactivity_ind to run: ensure that a state
+        # change occurs.
+        self.system_idle = not system_idle
+        self.system_inactivity_ind(system_idle)
+
+        mce_signal_proxy = bus.get_object(
+            'com.nokia.mce', '/com/nokia/mce/signal')
+        mce_signal_iface = dbus.Interface(
+            mce_signal_proxy, 'com.nokia.mce.signal')
+        mce_signal_iface.connect_to_signal(
+            'system_inactivity_ind', self.system_inactivity_ind)
+
+    def increase_download_parallelism(self):
+        # The system has been idle for a while.  Enable parallel
+        # downloads.
+        logger.debug("Increasing parallelism to 4 workers.")
+        JobManager().num_threads = 4
+        gobject.source_remove (self.increase_download_parallelism_id)
+        del self.increase_download_parallelism_id
+        return False
+
+    def system_inactivity_ind(self, idle):
+        # The system's idle state changed.
+        if (self.system_idle and idle) or (not self.system_idle and not idle):
+            # No change.
+            return
+
+        if not idle:
+            if hasattr (self, 'increase_download_parallelism_id'):
+                gobject.source_remove (self.increase_download_parallelism_id)
+                del self.increase_download_parallelism_id
+        else:
+            self.increase_download_parallelism_id = \
+                gobject.timeout_add_seconds(
+                    60, self.increase_download_parallelism)
+
+        if not idle:
+            logger.debug("Reducing parallelism to 1 worker.")
+            JobManager().num_threads = 1
+
+        self.system_idle = idle
+
     def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
-        if not new_stats['jobs-queued'] and not new_stats['jobs-in-progress']:
-            self.dbusHandler.UpdateFinished()
-            self.dbusHandler.ArticleCountUpdated()
-            self.done = True
+        queued = new_stats['jobs-queued']
+        in_progress = new_stats['jobs-in-progress']
+
+        if (queued or in_progress) and not self.am_updating:
+            logger.debug("new update started")
+            self.am_updating = True
+            self.UpdateStarted()
+            self.UpdateProgress(0, 0, in_progress, queued, 0, 0, 0, "")
+
+        if not queued and not in_progress:
+            logger.debug("update finished!")
+            self.am_updating = False
+            self.UpdateFinished()
+            self.ArticleCountUpdated()
+
+            if self.daemon:
+                self.inactivity_timer = gobject.timeout_add(
+                    60 * 1000, self.inactivity_cb)
+            else:
+                logger.debug("update finished, not running in daemon mode: "
+                             "quitting")
+                mainloop.quit()
+
+        if (queued or in_progress) and self.inactivity_timer:
+            gobject.source_remove(self.inactivity_timer)
+            self.inactivity_timer = 0
+
+    def inactivity_cb(self):
+        """
+        The updater has been inactive for a while.  Quit.
+        """
+        assert self.inactivity_timer
+        self.inactivity_timer = 0
+
+        if not self.am_updating:
+            logger.info("Nothing to do for a while.  Quitting.")
             mainloop.quit()
 
-    def stopUpdate(self):
-        logger.info("Stop update called.")
+    def StopUpdate(self):
+        """
+        Stop updating.
+        """
+        super(FeedUpdate, self).stopUpdate()
+
         JobManager().quit()
 
+    def UpdateAll(self):
+        """
+        Update all feeds.
+        """
+        super(FeedUpdate, self).UpdateAll()
+
+        feeds = self.listing.getListOfFeeds()
+        for k in feeds:
+            self.listing.updateFeed(k)
+        logger.debug("Queued all feeds (%d) for update." % len(feeds))
+
+    def Update(self, feed):
+        """
+        Update a particular feed.
+        """
+        super(FeedUpdate, self).Update(feed)
+
+        # We got a request via dbus.  If we weren't in daemon mode
+        # before, enter it now.
+        self.daemon = True
+
+        self.listing.updateFeed(feed)
+
+
 import dbus.mainloop.glib
 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
 
-gobject.threads_init()
-mainthread.init()
 mainloop = gobject.MainLoop()
+mainthread.init()
+
+# Acquire our name on the session bus.  If this doesn't work, most
+# likely another update_feeds instance is already running.  In this
+# case, just quit.
+try:
+    bus_name = dbus.service.BusName('org.marcoz.feedingit',
+                                    bus=dbus.SessionBus(),
+                                    do_not_queue=True)
+except Exception:
+    # We failed to acquire our bus name.  Die.
+    try:
+        dbus_proxy = dbus.SessionBus().get_object(
+            'org.freedesktop.DBus', '/org/freedesktop/DBus')
+        dbus_iface = dbus.Interface(dbus_proxy, 'org.freedesktop.DBus')
+        pid = dbus_iface.GetConnectionUnixProcessID('org.marcoz.feedingit')
+        logger.error("update_feeds already running: pid %d." % pid)
+    except Exception, e:
+        logger.error("Getting pid associated with org.marcoz.feedingit: %s"
+                     % str(e))
+        logger.error("update_feeds already running.")
+
+    sys.exit(1)
+
+# Run the updater.  Note: we run this until feed.am_updating is false.
+# Only is this case have all worker threads exited.  If the main
+# thread exits before all threads have exited and the process gets a
+# signal, the Python interpreter is unable to handle the signal and it
+# runs really slow (rescheduling after ever single instruction instead
+# of every few thousand).
+feed = FeedUpdate(bus_name)
+while True:
+    try:
+        mainloop.run()
+    except KeyboardInterrupt:
+        logger.error("Interrupted.  Quitting.")
+        JobManager().quit()
+
+    if not feed.am_updating:
+        break
 
-app_lock = get_lock("app_lock")
-
-if app_lock != None:
-    feed = FeedUpdate()
-    while not feed.done:
-        try:
-            mainloop.run()
-        except KeyboardInterrupt:
-            logger.error("Interrupted.  Quitting.")
-            JobManager().quit()
-    del app_lock
-else:
-    logger.error("Update in progress")
index 8e110ae..caf8754 100644 (file)
@@ -2,6 +2,7 @@
 
 # 
 # Copyright (c) 2007-2008 INdT.
+# Copyright (c) 2011 Neal H. Walfield
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
 # Description : Simple RSS Reader
 # ============================================================================
 
-import dbus
 import dbus.service
-import mainthread
+import logging
+logger = logging.getLogger(__name__)
 
-@mainthread.mainthread
-def get_lock(key):
-    try:
-        bus_name = dbus.service.BusName('org.marcoz.feedingit.lock_%s' %key,bus=dbus.SessionBus(), do_not_queue=True)
-    except:
-        bus_name = None
-    return bus_name
-
-def release_lock(lock):
-    assert lock is not None
-    mainthread.execute(lock.__del__, async=True)
+_update_server_object = None
+def update_server_object():
+    global _update_server_object
+    assert _update_server_object is not None, \
+        "No UpdateServerObject instantiated!"
+    return _update_server_object
 
 class UpdateServerObject(dbus.service.Object):
-    def __init__(self, app):
-        # Here the service name
-        bus_name = dbus.service.BusName('org.marcoz.feedingit',bus=dbus.SessionBus())
-        # Here the object path
-        dbus.service.Object.__init__(self, bus_name, '/org/marcoz/feedingit/update')
-        self.app = app
+    def __init__(self, bus_name):
+        """
+        Start listening for requests.
+        """
+        global _update_server_object
+        assert _update_server_object is None, \
+            "Attempt to instantiate multiple UpdateServerObject objects."
+        _update_server_object = self
+
+        dbus.service.Object.__init__(self, bus_name,
+                                     '/org/marcoz/feedingit/update')
 
-    @dbus.service.method('org.marcoz.feedingit')
-    def UpdateAll(self):
-        self.app.automaticUpdate()
-        return "Done"
-    
     @dbus.service.method('org.marcoz.feedingit')
     def StopUpdate(self):
-        self.app.stopUpdate()
-        return "Done"
+        logger.debug("Stop update called.")
+
+    @dbus.service.method('org.marcoz.feedingit')
+    def UpdateAll(self):
+        logger.debug("UpdateAll called.")
+
+    @dbus.service.method('org.marcoz.feedingit', in_signature='s')
+    def Update(self, feed):
+        logger.debug("Update(%s) called." % feed)
     
     # A signal that will be exported to dbus
     @dbus.service.signal('org.marcoz.feedingit', signature='')
@@ -63,6 +66,14 @@ class UpdateServerObject(dbus.service.Object):
         pass
 
     # A signal that will be exported to dbus
+    @dbus.service.signal('org.marcoz.feedingit', signature='uuuuttus')
+    def UpdateProgress(self, percent_complete,
+                       feeds_downloaded, feeds_downloading, feeds_pending,
+                       bytes_downloaded, bytes_uploaded, bytes_per_second,
+                       updated_feed):
+        pass
+
+    # A signal that will be exported to dbus
     @dbus.service.signal('org.marcoz.feedingit', signature='')
     def UpdateStarted(self):
         pass
@@ -70,4 +81,6 @@ class UpdateServerObject(dbus.service.Object):
     # A signal that will be exported to dbus
     @dbus.service.signal('org.marcoz.feedingit', signature='')
     def UpdateFinished(self):
-        pass
\ No newline at end of file
+        pass
+
+
index d99eae6..cb73c47 100644 (file)
--- a/src/wc.py
+++ b/src/wc.py
@@ -40,8 +40,10 @@ except ImportError, exception:
 refresh_interval = 6 * 60 * 60
 
 class mywoodchuck (PyWoodchuck):
-    def __init__(self, listing, *args):
-        PyWoodchuck.__init__ (self, *args)
+    def __init__(self, listing, human_readable_name, identifier,
+                 request_feedback):
+        PyWoodchuck.__init__ (self, human_readable_name, identifier,
+                              request_feedback)
 
         self.listing = listing
 
@@ -65,11 +67,12 @@ class mywoodchuck (PyWoodchuck):
                 stream.human_readable_name, stream.identifier))
 
 _w = None
-def wc_init(listing):
+def wc_init(listing, request_feedback=False):
     global _w
     assert _w is None
     
-    _w = mywoodchuck (listing, "FeedingIt", "org.maemo.feedingit")
+    _w = mywoodchuck (listing, "FeedingIt", "org.marcoz.feedingit",
+                      request_feedback)
 
     if not woodchuck_imported or not _w.available ():
         logger.info("Unable to contact Woodchuck server.")