infocalypse
 
(Steve Dougherty)
2013-08-14: Add initial new protocol implementation.

Add initial new protocol implementation. Replies to VoidQuery with VoidResult. Also nicely disconnects on SIGINT.

diff --git a/infocalypse/plugin_connect.py b/infocalypse/plugin_connect.py
--- a/infocalypse/plugin_connect.py
+++ b/infocalypse/plugin_connect.py
@@ -1,11 +1,12 @@
+from signal import signal, SIGINT
+from time import sleep
 import fcp
 import threading
 from mercurial import util
-from config import Config
+import sys
 
 PLUGIN_NAME = "org.freenetproject.plugin.dvcs_webui.main.Plugin"
 
-
 def connect(ui, repo):
     node = fcp.FCPNode()
 
@@ -15,28 +16,47 @@ def connect(ui, repo):
     # TODO: Where to document the spec? devnotes.txt? How to format?
     hi_there = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
                                      plugin_params={'Message': 'Hello',
-                                                    'GetRepoList': 'true'})[0]
+                                                    'VoidQuery': 'true'})[0]
 
     if hi_there['header'] == 'Error':
         raise util.Abort("The DVCS web UI plugin is not loaded.")
 
     if hi_there['Replies.Message'] == 'Error':
+        # TODO: Debugging
+        print hi_there
         raise util.Abort("Another VCS instance is already connected.")
 
-    print "Connected."
-    import sys
-    sys.exit()
+    session_token = hi_there['Replies.SessionToken']
+
+    ui.status("Connected.\n")
+
+    def disconnect(signum, frame):
+        ui.status("Disconnecting.\n")
+        node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                              plugin_params=
+                              {'Message': 'Disconnect',
+                               'SessionToken': session_token})
+        sys.exit()
+
+    # Send Disconnect on interrupt instead of waiting on timeout.
+    signal(SIGINT, disconnect)
 
     def ping():
-        pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
-                                     plugin_params={'Message': 'Ping'})[0]
-        if pong['Replies.Message'] == 'Error':
-            raise util.Abort(pong['Replies.Description'])
-        elif pong['Replies.Message'] != 'Pong':
-            ui.warn("Got unrecognized Ping reply '{0}'.\n".format(pong[
-                    'Replies.Message']))
-        # Must be faster than the timeout threshold. (5 seconds)
-        threading.Timer(4.0, ping).start()
+        # Loop with delay.
+        while True:
+            pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                                         plugin_params=
+                                         {'Message': 'Ping',
+                                          'SessionToken': session_token})[0]
+            if pong['Replies.Message'] == 'Error':
+                raise util.Abort(pong['Replies.Description'])
+            elif pong['Replies.Message'] != 'Pong':
+                ui.warn("Got unrecognized Ping reply '{0}'.\n".format(pong[
+                        'Replies.Message']))
+
+            # Wait for less than timeout threshold. In testing responses take
+            # a little over a second.
+            sleep(3.5)
 
     # Start self-perpetuating pinging in the background.
     t = threading.Timer(0.0, ping)
@@ -46,35 +66,47 @@ def connect(ui, repo):
     t.start()
 
     while True:
-        sequenceID = node._getUniqueId()
+        query_identifier = node._getUniqueId()
         # The event-querying is single-threaded, which makes things slow as
         # everything waits on the completion of the current operation.
         # Asynchronous code would require changes on the plugin side but
         # potentially have much lower latency.
+        # TODO: Can wrap away PLUGIN_NAME, SessionToken, and QueryIdentifier?
         command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
                                         plugin_params=
-                                        {'Message': 'ClearToSend',
-                                         'SequenceID': sequenceID})[0]
-        # TODO: Look up handlers in a dictionary.
-        print command
-
-        # Reload the config each time - it may have changed between messages.
-        cfg = Config.from_ui(ui)
+                                        {'Message': 'Ready',
+                                         'SessionToken': session_token,
+                                         'QueryIdentifier': query_identifier})[0]
 
         response = command['Replies.Message']
         if response == 'Error':
             raise util.Abort(command['Replies.Description'])
-        elif response == 'ListLocalRepos':
-            params = {'Message': 'RepoList',
-                      'SequenceID': sequenceID}
 
-            # Request USKs are keyed by repo path.
-            repo_index = 0
-            for path in cfg.request_usks.iterkeys():
-                params['Repo%s' % repo_index] = path
-                repo_index += 1
+        if response not in handlers:
+            raise util.Abort("Unsupported query '{0}'\n")
 
-            ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
-                                        plugin_params=params)[0]
-            print ack
+        # Handlers are indexed by the query message name, take the query
+        # message, and return (result_name, plugin_params).
+        result_name, plugin_params = handlers[response](command)
 
+        plugin_params['Message'] = result_name
+        plugin_params['QueryIdentifier'] = query_identifier
+        plugin_params['SessionToken'] = session_token
+
+        ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+                                    plugin_params=plugin_params)[0]
+
+        if ack['Replies.Message'] != "Ack":
+            raise util.Abort("Received unexpected message instead of result "
+                             "acknowledgement:\n{0}\n".format(ack))
+
+
+# Handlers return two items: result message name, message-specific parameters.
+# The sending code handles the plugin name, required parameters and plugin name.
+
+
+def VoidQuery(query):
+    return "VoidResult", {}
+
+# TODO: Perhaps look up method by name directly?
+handlers = {'VoidQuery': VoidQuery}