(drak)
2013-08-16: merge merge
diff --git a/.bugs/bugs b/.bugs/bugs
--- a/.bugs/bugs
+++ b/.bugs/bugs
@@ -13,5 +13,5 @@ Add i18n support for messages.
Fix implied clone destination when cloning from freenet:. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:9bd3ef617ba8871d28fbdae2500542e93302c791, time:1375736289.27
Unit tests involving the node are hard. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:b01a53e59a2096254ecacdcee7673df5323d786e, time:1375737309.25
Consider reworking repo list XML for conciseness. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:b511106032c58ffe7b33d0eb65bac7ec5555575e, time:1375734587.66
-simpler-wot-uris: name/repo | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:d4f2df3ca2c441e4be389be846634f5a4a08906e, time:1372232568.9
+simpler-wot-uris: name/repo | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:d4f2df3ca2c441e4be389be846634f5a4a08906e, time:1372232568.9
Add a warning that comments and unrecognized entries in ~/.infocalypse are removed. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:e9236fdd23d44bfbf8565bb1a59317289f324fd6, time:1375735601.35
diff --git a/infocalypse/commands.py b/infocalypse/commands.py
--- a/infocalypse/commands.py
+++ b/infocalypse/commands.py
@@ -63,8 +63,8 @@ def infocalypse_create(ui_, repo, **opts
nick_prefix, repo_name, repo_edition = opts['wot'].split('/', 2)
if not repo_name.endswith('.R1') and not repo_name.endswith('.R0'):
- ui_.warning("Warning: Creating repository without redundancy. (R0"
- " or R1)")
+ ui_.warn("Warning: Creating repository without redundancy. (R0 or"
+ " R1)\n")
from wot_id import Local_WoT_ID
@@ -238,8 +238,8 @@ def infocalypse_check_notifications(ui,
def infocalypse_connect(ui, repo, **opts):
- import wot
- wot.connect(ui, repo)
+ import plugin_connect
+ plugin_connect.connect(ui, repo)
def infocalypse_push(ui_, repo, **opts):
diff --git a/infocalypse/config.py b/infocalypse/config.py
--- a/infocalypse/config.py
+++ b/infocalypse/config.py
@@ -128,8 +128,13 @@ class Config:
# repo id -> publisher WoT identity
self.wot_identities = {}
# TODO: Should this be keyed by str(WoT_ID) ?
- # WoT public key hash -> Freemail password
+ # WoT identity ID -> Freemail password
self.freemail_passwords = {}
+ # WoT identity ID -> last known repo list edition.
+ # TODO: Once WoT allows setting a property without triggering an
+ # immediate insert, this can move to a WoT property. (Can then query
+ # remote identities! Easier bootstrapping than from edition 0.)
+ self.repo_list_editions = {}
# fms_id -> (usk_hash, ...) map
self.fmsread_trust_map = DEFAULT_TRUST.copy()
self.fmsread_groups = DEFAULT_GROUPS
@@ -269,6 +274,23 @@ class Config:
"Run hg fn-setupfreemail --truster {0}\n"
.format(wot_identity))
+ def set_repo_list_edition(self, wot_identity, edition):
+ """
+ Set the repository list edition for the given WoT identity.
+ :type wot_identity: WoT_ID
+ """
+ self.repo_list_editions[wot_identity.identity_id] = edition
+
+ def get_repo_list_edition(self, wot_identity):
+ """
+ Return the repository list edition associated with the given WoT
+ identity. Return 0 if one is not set.
+ """
+ if wot_identity.identity_id in self.repo_list_editions:
+ return self.repo_list_editions[wot_identity.identity_id]
+ else:
+ return 0
+
# Hmmm... really nescessary?
def get_dir_insert_uri(self, repo_dir):
""" Return the insert USK for repo_dir or None. """
@@ -383,6 +405,11 @@ class Config:
cfg.freemail_passwords[wot_id] = parser.get(
'freemail_passwords', wot_id)
+ if parser.has_section('repo_list_editions'):
+ for wot_id in parser.options('repo_list_editions'):
+ cfg.repo_list_editions[wot_id] = int(parser.get(
+ 'repo_list_editions', wot_id))
+
# ignored = fms_id|usk_hash|usk_hash|...
if parser.has_section('fmsread_trust_map'):
cfg.fmsread_trust_map.clear() # Wipe defaults.
@@ -467,6 +494,10 @@ class Config:
for wot_id in cfg.freemail_passwords:
parser.set('freemail_passwords', wot_id, cfg.freemail_passwords[
wot_id])
+ parser.add_section('repo_list_editions')
+ for wot_id in cfg.repo_list_editions:
+ parser.set('repo_list_editions', wot_id, cfg.repo_list_editions[
+ wot_id])
parser.add_section('fmsread_trust_map')
for index, fms_id in enumerate(cfg.fmsread_trust_map):
entry = cfg.fmsread_trust_map[fms_id]
diff --git a/infocalypse/keys.py b/infocalypse/keys.py
--- a/infocalypse/keys.py
+++ b/infocalypse/keys.py
@@ -18,6 +18,28 @@ class USK:
elif self.key.startswith('freenet://'):
self.key = self.key[len('freenet://'):]
+ def get_repo_name(self):
+ """
+ Return name with the redundancy level, if any, removed.
+
+ # TODO: tests. Use in detecting duplicate names. (Also
+ # determining repo names from URI.)
+
+ >>> USK('USK@.../name/5').get_repo_name()
+ 'name'
+ >>> USK('USK@.../name.R1/5').get_repo_name()
+ 'name'
+ >>> USK('USK@.../name.R0/5').get_repo_name()
+ 'name'
+ >>> USK('USK@.../name.something/5').get_repo_name()
+ 'name.something'
+ >>> USK('USK@.../name.R2/5').get_repo_name()
+ 'name.R2'
+ """
+ if self.name.endswith('.R1') or self.name.endswith('.R0'):
+ return self.name[:-3]
+ return self.name
+
def clone(self):
return USK(str(self))
diff --git a/infocalypse/plugin_connect.py b/infocalypse/plugin_connect.py
new file mode 100644
--- /dev/null
+++ b/infocalypse/plugin_connect.py
@@ -0,0 +1,112 @@
+from signal import signal, SIGINT
+from time import sleep
+import fcp
+import threading
+from mercurial import util
+import sys
+
+PLUGIN_NAME = "org.freenetproject.plugin.dvcs_webui.main.Plugin"
+
+def connect(ui, repo):
+ node = fcp.FCPNode()
+
+ ui.status("Connecting.\n")
+
+ # TODO: Would it be worthwhile to have a wrapper that includes PLUGIN_NAME?
+ # TODO: Where to document the spec? devnotes.txt? How to format?
+ hi_there = node.fcpPluginMessage(plugin_name=PLUGIN_NAME,
+ plugin_params={'Message': 'Hello',
+ '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.")
+
+ 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():
+ # 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)
+ # Daemon threads do not hold up the process exiting. Allows prompt
+ # response to - for instance - SIGTERM.
+ t.daemon = True
+ t.start()
+
+ while True:
+ 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': 'Ready',
+ 'SessionToken': session_token,
+ 'QueryIdentifier': query_identifier})[0]
+
+ response = command['Replies.Message']
+ if response == 'Error':
+ raise util.Abort(command['Replies.Description'])
+
+ if response not in handlers:
+ raise util.Abort("Unsupported query '{0}'\n")
+
+ # 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}
diff --git a/infocalypse/wot.py b/infocalypse/wot.py
--- a/infocalypse/wot.py
+++ b/infocalypse/wot.py
@@ -8,76 +8,16 @@ from keys import USK
import yaml
from email.mime.text import MIMEText
import imaplib
-import threading
from wot_id import Local_WoT_ID, WoT_ID
FREEMAIL_SMTP_PORT = 4025
FREEMAIL_IMAP_PORT = 4143
VCS_TOKEN = "[vcs]"
-PLUGIN_NAME = "org.freenetproject.plugin.dvcs_webui.main.Plugin"
# "infocalypse" is lower case in case it is used somewhere mixed case can
# cause problems like a filesystem path. Used for machine-readable VCS name.
VCS_NAME = "infocalypse"
-def connect(ui, repo):
- node = fcp.FCPNode()
-
- # TODO: Should I be using this? Looks internal. The identifier needs to
- # be consistent though.
- fcp_id = node._getUniqueId()
-
- ui.status("Connecting as '%s'.\n" % fcp_id)
-
- def ping():
- pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
- plugin_params={'Message': 'Ping'})[0]
- if pong['Replies.Message'] == 'Error':
- raise util.Abort(pong['Replies.Description'])
- # Must be faster than the timeout threshold. (5 seconds)
- threading.Timer(4.0, ping).start()
-
- # Start self-perpetuating pinging in the background.
- t = threading.Timer(0.0, ping)
- # Daemon threads do not hold up the process exiting. Allows prompt
- # response to - for instance - SIGTERM.
- t.daemon = True
- t.start()
-
- while True:
- sequenceID = 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.
- command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
- 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)
-
- 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
-
- ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id,
- plugin_params=params)[0]
- print ack
-
-
def send_pull_request(ui, repo, from_identity, to_identity, to_repo_name):
"""
Prompt for a pull request message, and send a pull request from
@@ -311,8 +251,10 @@ def update_repo_listing(ui, for_identity
# TODO: Somehow store the edition, perhaps in ~/.infocalypse. WoT
# properties are apparently not appropriate.
+ cfg = Config.from_ui(ui)
+
insert_uri.name = 'vcs'
- insert_uri.edition = '0'
+ insert_uri.edition = cfg.get_repo_list_edition(for_identity)
ui.status("Inserting with URI:\n{0}\n".format(insert_uri))
uri = node.put(uri=str(insert_uri), mimetype='application/xml',
@@ -322,6 +264,8 @@ def update_repo_listing(ui, for_identity
ui.warn("Failed to update repository listing.")
else:
ui.status("Updated repository listing:\n{0}\n".format(uri))
+ cfg.set_repo_list_edition(for_identity, USK(uri).edition)
+ Config.to_file(cfg)
def build_repo_list(ui, for_identity):
@@ -369,32 +313,85 @@ def read_repo_listing(ui, identity):
:type identity: WoT_ID
"""
+ cfg = Config.from_ui(ui)
uri = identity.request_uri.clone()
uri.name = 'vcs'
- uri.edition = 0
+ uri.edition = cfg.get_repo_list_edition(identity)
# TODO: Set and read vcs edition property.
- node = fcp.FCPNode()
- ui.status("Fetching {0}\n".format(uri))
- # TODO: What exception can this throw on failure? Catch it,
- # print its description, and return None.
- mime_type, repo_xml, msg = node.get(str(uri), priority=1,
- followRedirect=True)
+ ui.status("Fetching.\n")
+ mime_type, repo_xml, msg = fetch_edition(uri)
+ ui.status("Fetched {0}.\n".format(uri))
- ui.status("Parsing.\n")
+ cfg.set_repo_list_edition(identity, uri.edition)
+ Config.to_file(cfg)
+
repositories = {}
+ ambiguous = []
root = fromstring(repo_xml)
for repository in root.iterfind('repository'):
if repository.get('vcs') == VCS_NAME:
- uri = repository.text
- # Expecting key/reponame.R<num>/edition
- name = uri.split('/')[1].split('.')[0]
- ui.status("Found repository \"{0}\" at {1}\n".format(name, uri))
- repositories[name] = uri
+ uri = USK(repository.text)
+ name = uri.get_repo_name()
+ if name not in repositories:
+ repositories[name] = uri
+ else:
+ existing = repositories[name]
+ if uri.key == existing.key and uri.name == existing.name:
+ # Different edition of same key and complete name.
+ # Use the latest edition.
+ if uri.edition > existing.edition:
+ repositories[name] = uri
+ else:
+ # Different key or complete name. Later remove and give
+ # warning.
+ ambiguous.append(name)
+
+ for name in ambiguous:
+ # Same repo name but different key or exact name.
+ ui.warn("\"{0}\" refers ambiguously to multiple paths. Ignoring.\n"
+ .format(name))
+ del repositories[name]
+
+ # TODO: Would it make sense to mention those for which multiple editions
+ # are specified? It has no practical impact from this perspective,
+ # and these problems should be pointed out (or prevented) for local repo
+ # lists.
+
+ for name in repositories.iterkeys():
+ ui.status("Found repository \"{0}\".\n".format(name))
+
+ # Convert values from USKs to strings - USKs are not expected elsewhere.
+ for key in repositories.keys():
+ repositories[key] = str(repositories[key])
return repositories
+def fetch_edition(uri):
+ """
+ Fetch a USK uri, following redirects. Change the uri edition to the one
+ fetched.
+ :type uri: USK
+ """
+ node = fcp.FCPNode()
+ # Following a redirect automatically does not provide the edition used,
+ # so manually following redirects is required.
+ # TODO: Is there ever legitimately more than one redirect?
+ try:
+ return node.get(str(uri), priority=1)
+ except fcp.FCPGetFailed, e:
+ # Error code 27 is permanent redirect: there's a newer edition of
+ # the USK.
+ # https://wiki.freenetproject.org/FCPv2/GetFailed#Fetch_Error_Codes
+ if not e.info['Code'] == 27:
+ raise
+
+ uri.edition = USK(e.info['RedirectURI']).edition
+
+ return node.get(str(uri), priority=1)
+
+
def resolve_pull_uri(ui, path, truster):
"""
Return a pull URI for the given path.