Merge branch '2490-connection-info'

This updates the WUI welcome page with more information about each server
connection (and the introducer connection): which handler is being used, how
the connection process is going, and/or why it failed.

Closes ticket:2819
This commit is contained in:
Brian Warner 2016-12-09 11:14:50 -08:00
commit fe1df17d65
15 changed files with 297 additions and 135 deletions

View File

@ -272,7 +272,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
"coverage", "coverage",
"mock", "mock",
"tox", "tox",
"foolscap[tor] >= 0.12.3", "foolscap[tor] >= 0.12.5",
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work "txtorcon >= 0.17.0", # in case pip's resolver doesn't work
"foolscap[i2p]", "foolscap[i2p]",
"txi2p >= 0.3.1", # in case pip's resolver doesn't work "txi2p >= 0.3.1", # in case pip's resolver doesn't work
@ -280,7 +280,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
"pytest-twisted", "pytest-twisted",
], ],
"tor": [ "tor": [
"foolscap[tor] >= 0.12.3", "foolscap[tor] >= 0.12.5",
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work "txtorcon >= 0.17.0", # in case pip's resolver doesn't work
], ],
"i2p": [ "i2p": [

View File

@ -41,7 +41,8 @@ install_requires = [
# with a FIPS build of OpenSSL. # with a FIPS build of OpenSSL.
# * foolscap >= 0.12.3 provides tcp/tor/i2p connection handlers we need, # * foolscap >= 0.12.3 provides tcp/tor/i2p connection handlers we need,
# and allocate_tcp_port # and allocate_tcp_port
"foolscap >= 0.12.3", # * foolscap >= 0.12.5 has ConnectionInfo and ReconnectionInfo
"foolscap >= 0.12.5",
# Needed for SFTP. # Needed for SFTP.
# pycrypto 2.2 doesn't work due to <https://bugs.launchpad.net/pycrypto/+bug/620253> # pycrypto 2.2 doesn't work due to <https://bugs.launchpad.net/pycrypto/+bug/620253>

View File

@ -610,7 +610,7 @@ class Client(node.Node, pollmixin.PollMixin):
return self.encoding_params return self.encoding_params
def introducer_connection_statuses(self): def introducer_connection_statuses(self):
return [ic.connected_to_introducer() for ic in self.introducer_clients] return [ic.connection_status() for ic in self.introducer_clients]
def connected_to_introducer(self): def connected_to_introducer(self):
return any([ic.connected_to_introducer() for ic in self.introducer_clients]) return any([ic.connected_to_introducer() for ic in self.introducer_clients])

View File

@ -2830,3 +2830,60 @@ class InsufficientVersionError(Exception):
class EmptyPathnameComponentError(Exception): class EmptyPathnameComponentError(Exception):
"""The webapi disallows empty pathname components.""" """The webapi disallows empty pathname components."""
class IConnectionStatus(Interface):
"""
I hold information about the 'connectedness' for some reference.
Connections are an illusion, of course: only messages hold any meaning,
and they are fleeting. But for status displays, it is useful to pretend
that 'recently contacted' means a connection is established, and
'recently failed' means it is not.
This object is not 'live': it is created and populated when requested
from the connection manager, and it does not change after that point.
"""
connected = Attribute(
"""
Returns True if we appear to be connected: we've been successful
in communicating with our target at some point in the past, and we
haven't experienced any errors since then.""")
last_connection_time = Attribute(
"""
If is_connected() is True, this returns a number
(seconds-since-epoch) when we last transitioned from 'not connected'
to 'connected', such as when a TCP connect() operation completed and
subsequent negotiation was successful. Otherwise it returns None.
""")
last_connection_summary = Attribute(
"""
Returns a string with a brief summary of the current status, suitable
for display on an informational page. The more complete text from
last_connection_description would be appropriate for a tool-tip
popup.
""")
last_connection_description = Attribute(
"""
Returns a string with a description of the results of the most recent
connection attempt. For Foolscap connections, this indicates the
winning hint and the connection handler which used it, e.g.
'tcp:HOST:PORT via tcp' or 'tor:HOST.onion:PORT via tor':
* 'Connection successful: HINT via HANDLER (other hints: ..)'
* 'Connection failed: HINT->HANDLER->FAILURE, ...'
Note that this describes the last *completed* connection attempt. If
a connection attempt is currently in progress, this method will
describe the results of the previous attempt.
""")
last_received_time = Attribute(
"""
Returns a number (seconds-since-epoch) describing the last time we
heard anything (including low-level keep-alives or inbound requests)
from the other side.
""")

View File

@ -8,7 +8,7 @@ from allmydata.introducer.interfaces import IIntroducerClient, \
RIIntroducerSubscriberClient_v2 RIIntroducerSubscriberClient_v2
from allmydata.introducer.common import sign_to_foolscap, unsign_from_foolscap,\ from allmydata.introducer.common import sign_to_foolscap, unsign_from_foolscap,\
get_tubid_string_from_ann get_tubid_string_from_ann
from allmydata.util import log, yamlutil from allmydata.util import log, yamlutil, connection_status
from allmydata.util.rrefutil import add_version_to_remote_reference from allmydata.util.rrefutil import add_version_to_remote_reference
from allmydata.util.keyutil import BadSignatureError from allmydata.util.keyutil import BadSignatureError
from allmydata.util.assertutil import precondition from allmydata.util.assertutil import precondition
@ -326,14 +326,16 @@ class IntroducerClient(service.Service, Referenceable):
if service_name2 == service_name: if service_name2 == service_name:
eventually(cb, key_s, ann, *args, **kwargs) eventually(cb, key_s, ann, *args, **kwargs)
def connection_status(self):
assert self.running # startService builds _introducer_reconnector
irc = self._introducer_reconnector
last_received = (self._publisher.getDataLastReceivedAt()
if self._publisher
else None)
return connection_status.from_foolscap_reconnector(irc, last_received)
def connected_to_introducer(self): def connected_to_introducer(self):
return bool(self._publisher) return bool(self._publisher)
def get_since(self): def get_since(self):
return self._since return self._since
def get_last_received_data_time(self):
if self._publisher is None:
return None
else:
return self._publisher.getDataLastReceivedAt()

View File

@ -36,7 +36,7 @@ from twisted.application import service
from foolscap.api import eventually from foolscap.api import eventually
from allmydata.interfaces import IStorageBroker, IDisplayableServer, IServer from allmydata.interfaces import IStorageBroker, IDisplayableServer, IServer
from allmydata.util import log, base32 from allmydata.util import log, base32, connection_status
from allmydata.util.assertutil import precondition from allmydata.util.assertutil import precondition
from allmydata.util.observer import ObserverList from allmydata.util.observer import ObserverList
from allmydata.util.rrefutil import add_version_to_remote_reference from allmydata.util.rrefutil import add_version_to_remote_reference
@ -364,17 +364,16 @@ class NativeStorageServer(service.MultiService):
return self.announcement return self.announcement
def get_remote_host(self): def get_remote_host(self):
return self.remote_host return self.remote_host
def get_connection_status(self):
last_received = None
if self.rref:
last_received = self.rref.getDataLastReceivedAt()
return connection_status.from_foolscap_reconnector(self._reconnector,
last_received)
def is_connected(self): def is_connected(self):
return self._is_connected return self._is_connected
def get_last_connect_time(self):
return self.last_connect_time
def get_last_loss_time(self):
return self.last_loss_time
def get_last_received_data_time(self):
if self.rref is None:
return None
else:
return self.rref.getDataLastReceivedAt()
def get_available_space(self): def get_available_space(self):
version = self.get_version() version = self.get_version()

View File

@ -62,7 +62,8 @@ class Tor(unittest.TestCase):
n = FakeNode(config) n = FakeNode(config)
h = n._make_tor_handler() h = n._make_tor_handler()
private_dir = os.path.join(n.basedir, "private") private_dir = os.path.join(n.basedir, "private")
exp = mock.call(n._tor_provider._make_control_endpoint) exp = mock.call(n._tor_provider._make_control_endpoint,
takes_status=True)
self.assertEqual(f.mock_calls, [exp]) self.assertEqual(f.mock_calls, [exp])
self.assertIdentical(h, h1) self.assertIdentical(h, h1)
@ -75,7 +76,9 @@ class Tor(unittest.TestCase):
cfs = mock.Mock(return_value=tcep) cfs = mock.Mock(return_value=tcep)
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor): with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor):
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs): with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
cep = self.successResultOf(tp._make_control_endpoint(reactor)) d = tp._make_control_endpoint(reactor,
update_status=lambda status: None)
cep = self.successResultOf(d)
launch_tor.assert_called_with(reactor, executable, private_dir, launch_tor.assert_called_with(reactor, executable, private_dir,
tp._txtorcon) tp._txtorcon)
cfs.assert_called_with(reactor, "ep_desc") cfs.assert_called_with(reactor, "ep_desc")

View File

@ -271,6 +271,14 @@ class FakeConfig(dict):
raise KeyError raise KeyError
return value return value
class EmptyContext(object):
def __init__(self):
pass
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
pass
class Provider(unittest.TestCase): class Provider(unittest.TestCase):
def test_build(self): def test_build(self):
tor_provider.Provider("basedir", FakeConfig(), "reactor") tor_provider.Provider("basedir", FakeConfig(), "reactor")
@ -298,13 +306,15 @@ class Provider(unittest.TestCase):
txtorcon = mock.Mock() txtorcon = mock.Mock()
handler = object() handler = object()
tor.control_endpoint_maker = mock.Mock(return_value=handler) tor.control_endpoint_maker = mock.Mock(return_value=handler)
tor.add_context = mock.Mock(return_value=EmptyContext())
with mock_tor(tor): with mock_tor(tor):
with mock_txtorcon(txtorcon): with mock_txtorcon(txtorcon):
p = tor_provider.Provider("basedir", FakeConfig(launch=True), p = tor_provider.Provider("basedir", FakeConfig(launch=True),
reactor) reactor)
h = p.get_tor_handler() h = p.get_tor_handler()
self.assertIs(h, handler) self.assertIs(h, handler)
tor.control_endpoint_maker.assert_called_with(p._make_control_endpoint) tor.control_endpoint_maker.assert_called_with(p._make_control_endpoint,
takes_status=True)
# make sure Tor is launched just once, the first time an endpoint is # make sure Tor is launched just once, the first time an endpoint is
# requested, and never again. The clientFromString() function is # requested, and never again. The clientFromString() function is
@ -316,7 +326,8 @@ class Provider(unittest.TestCase):
cfs = mock.Mock(return_value=ep) cfs = mock.Mock(return_value=ep)
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor): with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor):
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs): with mock.patch("allmydata.util.tor_provider.clientFromString", cfs):
d = p._make_control_endpoint(reactor) d = p._make_control_endpoint(reactor,
update_status=lambda status: None)
yield flushEventualQueue() yield flushEventualQueue()
self.assertIs(self.successResultOf(d), ep) self.assertIs(self.successResultOf(d), ep)
launch_tor.assert_called_with(reactor, None, launch_tor.assert_called_with(reactor, None,
@ -328,7 +339,8 @@ class Provider(unittest.TestCase):
cfs2 = mock.Mock(return_value=ep) cfs2 = mock.Mock(return_value=ep)
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor2): with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor2):
with mock.patch("allmydata.util.tor_provider.clientFromString", cfs2): with mock.patch("allmydata.util.tor_provider.clientFromString", cfs2):
d2 = p._make_control_endpoint(reactor) d2 = p._make_control_endpoint(reactor,
update_status=lambda status: None)
yield flushEventualQueue() yield flushEventualQueue()
self.assertIs(self.successResultOf(d2), ep) self.assertIs(self.successResultOf(d2), ep)
self.assertEqual(launch_tor2.mock_calls, []) self.assertEqual(launch_tor2.mock_calls, [])

View File

@ -2,6 +2,7 @@ from twisted.trial import unittest
from ...storage_client import NativeStorageServer from ...storage_client import NativeStorageServer
from ...web.root import Root from ...web.root import Root
from ...util.connection_status import ConnectionStatus
class FakeRoot(Root): class FakeRoot(Root):
def __init__(self): def __init__(self):
@ -26,6 +27,8 @@ class RenderServiceRow(unittest.TestCase):
"permutation-seed-base32": "w2hqnbaa25yw4qgcvghl5psa3srpfgw3", "permutation-seed-base32": "w2hqnbaa25yw4qgcvghl5psa3srpfgw3",
} }
s = NativeStorageServer("server_id", ann, None, {}) s = NativeStorageServer("server_id", ann, None, {})
cs = ConnectionStatus(False, "summary", "description", 0, 0)
s.get_connection_status = lambda: cs
r = FakeRoot() r = FakeRoot()
ctx = FakeContext() ctx = FakeContext()

View File

@ -20,6 +20,7 @@ from allmydata.web import status
from allmydata.util import fileutil, base32, hashutil from allmydata.util import fileutil, base32, hashutil
from allmydata.util.consumer import download_to_data from allmydata.util.consumer import download_to_data
from allmydata.util.encodingutil import to_str from allmydata.util.encodingutil import to_str
from ...util.connection_status import ConnectionStatus
from ..common import FakeCHKFileNode, FakeMutableFileNode, \ from ..common import FakeCHKFileNode, FakeMutableFileNode, \
create_chk_filenode, WebErrorMixin, \ create_chk_filenode, WebErrorMixin, \
make_mutable_file_uri, create_mutable_filenode make_mutable_file_uri, create_mutable_filenode
@ -164,26 +165,21 @@ class FakeDisplayableServer(StubServer):
self.last_loss_time = last_loss_time self.last_loss_time = last_loss_time
self.last_rx_time = last_rx_time self.last_rx_time = last_rx_time
self.last_connect_time = last_connect_time self.last_connect_time = last_connect_time
def on_status_changed(self, cb): def on_status_changed(self, cb): # TODO: try to remove me
cb(self) cb(self)
def is_connected(self): def is_connected(self): # TODO: remove me
return self.connected return self.connected
def get_permutation_seed(self): def get_permutation_seed(self):
return "" return ""
def get_remote_host(self):
return ""
def get_last_loss_time(self):
return self.last_loss_time
def get_last_received_data_time(self):
return self.last_rx_time
def get_last_connect_time(self):
return self.last_connect_time
def get_announcement(self): def get_announcement(self):
return self.announcement return self.announcement
def get_nickname(self): def get_nickname(self):
return self.announcement["nickname"] return self.announcement["nickname"]
def get_available_space(self): def get_available_space(self):
return 123456 return 123456
def get_connection_status(self):
return ConnectionStatus(self.connected, "summary", "description",
self.last_connect_time, self.last_rx_time)
class FakeBucketCounter(object): class FakeBucketCounter(object):
def get_state(self): def get_state(self):
@ -243,7 +239,7 @@ class FakeClient(Client):
self.storage_broker.test_add_server("disconnected_nodeid", self.storage_broker.test_add_server("disconnected_nodeid",
FakeDisplayableServer( FakeDisplayableServer(
serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False, serverid="other_nodeid", nickname=u"disconnected_nickname \u263B", connected = False,
last_connect_time = 15, last_loss_time = 25, last_rx_time = 35)) last_connect_time = None, last_loss_time = 25, last_rx_time = 35))
self.introducer_client = None self.introducer_client = None
self.history = FakeHistory() self.history = FakeHistory()
self.uploader = FakeUploader() self.uploader = FakeUploader()
@ -611,6 +607,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
def test_welcome(self): def test_welcome(self):
d = self.GET("/") d = self.GET("/")
def _check(res): def _check(res):
# TODO: replace this with a parser
self.failUnlessIn('<title>Tahoe-LAFS - Welcome</title>', res) self.failUnlessIn('<title>Tahoe-LAFS - Welcome</title>', res)
self.failUnlessIn(FAVICON_MARKUP, res) self.failUnlessIn(FAVICON_MARKUP, res)
self.failUnlessIn('<a href="status">Recent and Active Operations</a>', res) self.failUnlessIn('<a href="status">Recent and Active Operations</a>', res)
@ -624,14 +621,27 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
self.failUnlessIn(u'Connected to <span>1</span>\n of <span>2</span> known storage servers', res_u) self.failUnlessIn(u'Connected to <span>1</span>\n of <span>2</span> known storage servers', res_u)
def timestamp(t): def timestamp(t):
return (u'"%s"' % (t,)) if self.have_working_tzset() else u'"[^"]*"' return (u'"%s"' % (t,)) if self.have_working_tzset() else u'"[^"]*"'
# TODO: use a real parser to make sure these two nodes are siblings
self.failUnless(re.search( self.failUnless(re.search(
u'<div class="status-indicator"><img (src="img/connected-yes.png" |alt="Connected" ){2}/>' u'<div class="status-indicator"><img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>'
u'</div>\n <a( class="timestamp"| title=%s){2}>1d\u00A00h\u00A00m\u00A050s</a>' u'\s+'
u'<div class="nickname">other_nickname \u263B</div>',
res_u), repr(res_u))
self.failUnless(re.search(
u'<a( class="timestamp"| title=%s){2}>\s+1d\u00A00h\u00A00m\u00A050s\s+</a>'
% timestamp(u'1970-01-01 13:00:10'), res_u), repr(res_u)) % timestamp(u'1970-01-01 13:00:10'), res_u), repr(res_u))
# same for these two nodes
self.failUnless(re.search( self.failUnless(re.search(
u'<div class="status-indicator"><img (src="img/connected-no.png" |alt="Disconnected" ){2}/>' u'<div class="status-indicator"><img (src="img/connected-no.png" |alt="Disconnected" ){2}/></div>'
u'</div>\n <a( class="timestamp"| title=%s){2}>1d\u00A00h\u00A00m\u00A035s</a>' u'\s+'
% timestamp(u'1970-01-01 13:00:25'), res_u), repr(res_u)) u'<div class="nickname">disconnected_nickname \u263B</div>',
res_u), repr(res_u))
self.failUnless(re.search(
u'<a( class="timestamp"| title="N/A"){2}>\s+N/A\s+</a>',
res_u), repr(res_u))
self.failUnless(re.search( self.failUnless(re.search(
u'<td class="service-last-received-data"><a( class="timestamp"| title=%s){2}>' u'<td class="service-last-received-data"><a( class="timestamp"| title=%s){2}>'
u'1d\u00A00h\u00A00m\u00A030s</a></td>' u'1d\u00A00h\u00A00m\u00A030s</a></td>'
@ -656,12 +666,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
class MockIntroducerClient(object): class MockIntroducerClient(object):
def __init__(self, connected): def __init__(self, connected):
self.connected = connected self.connected = connected
def connected_to_introducer(self): def connection_status(self):
return self.connected return ConnectionStatus(self.connected,
def get_since(self): "summary", "description", 0, 0)
return 0
def get_last_received_data_time(self):
return 0
d = defer.succeed(None) d = defer.succeed(None)
@ -673,7 +680,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_set_introducer_not_connected_unguessable) d.addCallback(_set_introducer_not_connected_unguessable)
def _check_introducer_not_connected_unguessable(res): def _check_introducer_not_connected_unguessable(res):
html = res.replace('\n', ' ') html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someIntroducer/[censored]</div>', html) self.failUnlessIn('<div class="connection-status" title="description">summary</div>', html)
self.failIfIn('pb://someIntroducer/secret', html) self.failIfIn('pb://someIntroducer/secret', html)
self.failUnless(re.search('<img (alt="Disconnected" |src="img/connected-no.png" ){2}/></div>[ ]*<div>No introducers connected</div>', html), res) self.failUnless(re.search('<img (alt="Disconnected" |src="img/connected-no.png" ){2}/></div>[ ]*<div>No introducers connected</div>', html), res)
@ -687,7 +694,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_set_introducer_connected_unguessable) d.addCallback(_set_introducer_connected_unguessable)
def _check_introducer_connected_unguessable(res): def _check_introducer_connected_unguessable(res):
html = res.replace('\n', ' ') html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someIntroducer/[censored]</div>', html) self.failUnlessIn('<div class="connection-status" title="description">summary</div>', html)
self.failIfIn('pb://someIntroducer/secret', html) self.failIfIn('pb://someIntroducer/secret', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res) self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
d.addCallback(_check_introducer_connected_unguessable) d.addCallback(_check_introducer_connected_unguessable)
@ -700,7 +707,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_set_introducer_connected_guessable) d.addCallback(_set_introducer_connected_guessable)
def _check_introducer_connected_guessable(res): def _check_introducer_connected_guessable(res):
html = res.replace('\n', ' ') html = res.replace('\n', ' ')
self.failUnlessIn('<div class="furl">pb://someIntroducer/introducer</div>', html) self.failUnlessIn('<div class="connection-status" title="description">summary</div>', html)
self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res) self.failUnless(re.search('<img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>[ ]*<div>1 introducer connected</div>', html), res)
d.addCallback(_check_introducer_connected_guessable) d.addCallback(_check_introducer_connected_guessable)
return d return d

View File

@ -0,0 +1,81 @@
import time
from zope.interface import implementer
from ..interfaces import IConnectionStatus
@implementer(IConnectionStatus)
class ConnectionStatus:
def __init__(self, connected, summary,
last_connection_description, last_connection_time,
last_received_time):
self.connected = connected
self.last_connection_summary = summary
self.last_connection_description = last_connection_description
self.last_connection_time = last_connection_time
self.last_received_time = last_received_time
def _describe_statuses(hints, handlers, statuses):
descriptions = []
for hint in sorted(hints):
handler = handlers.get(hint)
handler_dsc = " via %s" % handler if handler else ""
status = statuses[hint]
descriptions.append(" %s%s: %s\n" % (hint, handler_dsc, status))
return "".join(descriptions)
def from_foolscap_reconnector(rc, last_received):
ri = rc.getReconnectionInfo()
state = ri.state
# the Reconnector shouldn't even be exposed until it is started, so we
# should never see "unstarted"
assert state in ("connected", "connecting", "waiting"), state
ci = ri.connectionInfo
if state == "connected":
connected = True
# build a description that shows the winning hint, and the outcomes
# of the losing ones
statuses = ci.connectorStatuses
handlers = ci.connectionHandlers
others = set(statuses.keys())
winner = ci.winningHint
if winner:
others.remove(winner)
winning_handler = ci.connectionHandlers[winner]
winning_dsc = "to %s via %s" % (winner, winning_handler)
else:
winning_dsc = "via listener (%s)" % ci.listenerStatus[0]
if others:
other_dsc = "\nother hints:\n%s" % \
_describe_statuses(others, handlers, statuses)
else:
other_dsc = ""
details = "Connection successful " + winning_dsc + other_dsc
summary = "Connected %s" % winning_dsc
last_connected = ci.establishedAt
elif state == "connecting":
connected = False
# ci describes the current in-progress attempt
statuses = ci.connectorStatuses
current = _describe_statuses(sorted(statuses.keys()),
ci.connectionHandlers, statuses)
details = "Trying to connect:\n%s" % current
summary = "Trying to connect"
last_connected = None
elif state == "waiting":
connected = False
now = time.time()
elapsed = now - ri.lastAttempt
delay = ri.nextAttempt - now
# ci describes the previous (failed) attempt
statuses = ci.connectorStatuses
last = _describe_statuses(sorted(statuses.keys()),
ci.connectionHandlers, statuses)
details = "Reconnecting in %d seconds\nLast attempt %ds ago:\n%s" \
% (delay, elapsed, last)
summary = "Reconnecting in %d seconds" % delay
last_connected = None
cs = ConnectionStatus(connected, summary, details,
last_connected, last_received)
return cs

View File

@ -226,7 +226,8 @@ class Provider(service.MultiService):
if self._get_tor_config("launch", False, boolean=True): if self._get_tor_config("launch", False, boolean=True):
if not self._txtorcon: if not self._txtorcon:
return None return None
return self._tor.control_endpoint_maker(self._make_control_endpoint) return self._tor.control_endpoint_maker(self._make_control_endpoint,
takes_status=True)
socks_endpoint_desc = self._get_tor_config("socks.port", None) socks_endpoint_desc = self._get_tor_config("socks.port", None)
if socks_endpoint_desc: if socks_endpoint_desc:
@ -241,9 +242,11 @@ class Provider(service.MultiService):
return self._tor.default_socks() return self._tor.default_socks()
@inlineCallbacks @inlineCallbacks
def _make_control_endpoint(self, reactor): def _make_control_endpoint(self, reactor, update_status):
# this will only be called when tahoe.cfg has "[tor] launch = true" # this will only be called when tahoe.cfg has "[tor] launch = true"
(endpoint_desc, _) = yield self._get_launched_tor(reactor) update_status("launching Tor")
with self._tor.add_context(update_status, "launching Tor"):
(endpoint_desc, _) = yield self._get_launched_tor(reactor)
tor_control_endpoint = clientFromString(reactor, endpoint_desc) tor_control_endpoint = clientFromString(reactor, endpoint_desc)
returnValue(tor_control_endpoint) returnValue(tor_control_endpoint)

View File

@ -1,6 +1,5 @@
import time, os import time, os
from twisted.internet import address
from twisted.web import http from twisted.web import http
from nevow import rend, url, tags as T from nevow import rend, url, tags as T
from nevow.inevow import IRequest from nevow.inevow import IRequest
@ -240,18 +239,14 @@ class Root(rend.Page):
return "%s introducers connected" % (connected_count,) return "%s introducers connected" % (connected_count,)
def data_total_introducers(self, ctx, data): def data_total_introducers(self, ctx, data):
return len(self.client.introducer_furls) return len(self.client.introducer_connection_statuses())
def data_connected_introducers(self, ctx, data): def data_connected_introducers(self, ctx, data):
return self.client.introducer_connection_statuses().count(True) return len([1 for cs in self.client.introducer_connection_statuses()
if cs.connected])
def data_connected_to_introducer(self, ctx, data):
if self.client.connected_to_introducer():
return "yes"
return "no"
def data_connected_to_at_least_one_introducer(self, ctx, data): def data_connected_to_at_least_one_introducer(self, ctx, data):
if True in self.client.introducer_connection_statuses(): if self.data_connected_introducers(ctx, data):
return "yes" return "yes"
return "no" return "no"
@ -260,42 +255,35 @@ class Root(rend.Page):
# In case we configure multiple introducers # In case we configure multiple introducers
def data_introducers(self, ctx, data): def data_introducers(self, ctx, data):
connection_statuses = self.client.introducer_connection_statuses() return self.client.introducer_connection_statuses()
s = []
furls = self.client.introducer_furls
for furl in furls:
if connection_statuses:
display_furl = furl
# trim off the secret swissnum
(prefix, _, swissnum) = furl.rpartition("/")
if swissnum != "introducer":
display_furl = "%s/[censored]" % (prefix,)
i = furls.index(furl)
ic = self.client.introducer_clients[i]
s.append((display_furl, bool(connection_statuses[i]), ic))
s.sort()
return s
def render_introducers_row(self, ctx, s): def render_introducers_row(self, ctx, cs):
(furl, connected, ic) = s connected = "yes" if cs.connected else "no"
service_connection_status = "yes" if connected else "no" ctx.fillSlots("service_connection_status", connected)
since = ic.get_since()
service_connection_status_rel_time = render_time_delta(since, self.now_fn())
service_connection_status_abs_time = render_time_attr(since)
last_received_data_time = ic.get_last_received_data_time()
last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn())
last_received_data_abs_time = render_time_attr(last_received_data_time)
ctx.fillSlots("introducer_furl", "%s" % (furl))
ctx.fillSlots("service_connection_status", "%s" % (service_connection_status,))
ctx.fillSlots("service_connection_status_alt", ctx.fillSlots("service_connection_status_alt",
self._connectedalts[service_connection_status]) self._connectedalts[connected])
ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time) since = cs.last_connection_time
ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time) ctx.fillSlots("service_connection_status_rel_time",
ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time) render_time_delta(since, self.now_fn())
if since is not None
else "N/A")
ctx.fillSlots("service_connection_status_abs_time",
render_time_attr(since)
if since is not None
else "N/A")
last_received_data_time = cs.last_received_time
ctx.fillSlots("last_received_data_abs_time",
render_time_attr(last_received_data_time)
if last_received_data_time is not None
else "N/A")
ctx.fillSlots("last_received_data_rel_time",
render_time_delta(last_received_data_time, self.now_fn())
if last_received_data_time is not None
else "N/A")
ctx.fillSlots("summary", "%s" % cs.last_connection_summary)
ctx.fillSlots("details", "%s" % cs.last_connection_description)
return ctx.tag return ctx.tag
def data_helper_furl_prefix(self, ctx, data): def data_helper_furl_prefix(self, ctx, data):
@ -344,33 +332,38 @@ class Root(rend.Page):
return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid()) return sorted(sb.get_known_servers(), key=lambda s: s.get_serverid())
def render_service_row(self, ctx, server): def render_service_row(self, ctx, server):
server_id = server.get_serverid() cs = server.get_connection_status()
ctx.fillSlots("peerid", server.get_longname()) ctx.fillSlots("peerid", server.get_longname())
ctx.fillSlots("nickname", server.get_nickname()) ctx.fillSlots("nickname", server.get_nickname())
rhost = server.get_remote_host()
if server.is_connected():
if server_id == self.client.get_long_nodeid():
rhost_s = "(loopback)"
elif isinstance(rhost, address.IPv4Address):
rhost_s = "%s:%d" % (rhost.host, rhost.port)
else:
rhost_s = str(rhost)
addr = rhost_s
service_connection_status = "yes"
last_connect_time = server.get_last_connect_time()
service_connection_status_rel_time = render_time_delta(last_connect_time, self.now_fn())
service_connection_status_abs_time = render_time_attr(last_connect_time)
else:
addr = "N/A"
service_connection_status = "no"
last_loss_time = server.get_last_loss_time()
service_connection_status_rel_time = render_time_delta(last_loss_time, self.now_fn())
service_connection_status_abs_time = render_time_attr(last_loss_time)
last_received_data_time = server.get_last_received_data_time() connected = "yes" if cs.connected else "no"
last_received_data_rel_time = render_time_delta(last_received_data_time, self.now_fn()) ctx.fillSlots("service_connection_status", connected)
last_received_data_abs_time = render_time_attr(last_received_data_time) ctx.fillSlots("service_connection_status_alt",
self._connectedalts[connected])
since = cs.last_connection_time
ctx.fillSlots("service_connection_status_rel_time",
render_time_delta(since, self.now_fn())
if since is not None
else "N/A")
ctx.fillSlots("service_connection_status_abs_time",
render_time_attr(since)
if since is not None
else "N/A")
last_received_data_time = cs.last_received_time
ctx.fillSlots("last_received_data_abs_time",
render_time_attr(last_received_data_time)
if last_received_data_time is not None
else "N/A")
ctx.fillSlots("last_received_data_rel_time",
render_time_delta(last_received_data_time, self.now_fn())
if last_received_data_time is not None
else "N/A")
ctx.fillSlots("summary", "%s" % cs.last_connection_summary)
ctx.fillSlots("details", "%s" % cs.last_connection_description)
announcement = server.get_announcement() announcement = server.get_announcement()
version = announcement.get("my-version", "") version = announcement.get("my-version", "")
@ -379,14 +372,6 @@ class Root(rend.Page):
available_space = "N/A" available_space = "N/A"
else: else:
available_space = abbreviate_size(available_space) available_space = abbreviate_size(available_space)
ctx.fillSlots("address", addr)
ctx.fillSlots("service_connection_status", service_connection_status)
ctx.fillSlots("service_connection_status_alt",
self._connectedalts[service_connection_status])
ctx.fillSlots("service_connection_status_abs_time", service_connection_status_abs_time)
ctx.fillSlots("service_connection_status_rel_time", service_connection_status_rel_time)
ctx.fillSlots("last_received_data_abs_time", last_received_data_abs_time)
ctx.fillSlots("last_received_data_rel_time", last_received_data_rel_time)
ctx.fillSlots("version", version) ctx.fillSlots("version", version)
ctx.fillSlots("available_space", available_space) ctx.fillSlots("available_space", available_space)

View File

@ -50,6 +50,9 @@ body {
margin-top: 5px; margin-top: 5px;
} }
.connection-status {
}
.furl { .furl {
font-size: 0.8em; font-size: 0.8em;
word-wrap: break-word; word-wrap: break-word;
@ -78,7 +81,7 @@ body {
margin: 5px; margin: 5px;
} }
.nickname-and-peerid .timestamp { .timestamp {
float: right; float: right;
} }

View File

@ -176,7 +176,7 @@
<thead> <thead>
<tr n:pattern="header"> <tr n:pattern="header">
<td><h3>Nickname</h3></td> <td><h3>Nickname</h3></td>
<td><h3>Address</h3></td> <td><h3>Connection</h3></td>
<td><h3>Last&nbsp;RX</h3></td> <td><h3>Last&nbsp;RX</h3></td>
<td><h3>Version</h3></td> <td><h3>Version</h3></td>
<td><h3>Available</h3></td> <td><h3>Available</h3></td>
@ -185,16 +185,22 @@
<tr n:pattern="item" n:render="service_row"> <tr n:pattern="item" n:render="service_row">
<td class="nickname-and-peerid"> <td class="nickname-and-peerid">
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div> <div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div>
<a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a>
<div class="nickname"><n:slot name="nickname"/></div> <div class="nickname"><n:slot name="nickname"/></div>
<div class="nodeid"><n:slot name="peerid"/></div> <div class="nodeid"><n:slot name="peerid"/></div>
</td> </td>
<td class="address"><n:slot name="address"/></td> <td class="connection-status">
<n:attr name="title"><n:slot name="details"/></n:attr>
<n:slot name="summary"/>
<a class="timestamp">
<n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr>
<n:slot name="service_connection_status_rel_time"/>
</a>
</td>
<td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td> <td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
<td class="service-version"><n:slot name="version"/></td> <td class="service-version"><n:slot name="version"/></td>
<td class="service-available-space"><n:slot name="available_space"/></td> <td class="service-available-space"><n:slot name="available_space"/></td>
</tr> </tr>
<tr n:pattern="empty"><td colspan="5">You are not presently connected to any peers</td></tr> <tr n:pattern="empty"><td colspan="5">You are not presently connected to any servers.</td></tr>
</table> </table>
<div class="row-fluid"> <div class="row-fluid">
<h2>Connected to <span n:render="string" n:data="connected_introducers" /> of <span n:render="string" n:data="total_introducers" /> introducers</h2> <h2>Connected to <span n:render="string" n:data="connected_introducers" /> of <span n:render="string" n:data="total_introducers" /> introducers</h2>
@ -202,7 +208,7 @@
<table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="introducers"> <table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="introducers">
<thead> <thead>
<tr n:pattern="header"> <tr n:pattern="header">
<td><h3>Address</h3></td> <td><h3>Connection</h3></td>
<td><h3>Last&nbsp;RX</h3></td> <td><h3>Last&nbsp;RX</h3></td>
</tr> </tr>
</thead> </thead>
@ -210,7 +216,7 @@
<td class="nickname-and-peerid"> <td class="nickname-and-peerid">
<div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div> <div class="status-indicator"><img><n:attr name="src">img/connected-<n:slot name="service_connection_status" />.png</n:attr><n:attr name="alt"><n:slot name="service_connection_status_alt" /></n:attr></img></div>
<a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a> <a class="timestamp"><n:attr name="title"><n:slot name="service_connection_status_abs_time"/></n:attr><n:slot name="service_connection_status_rel_time"/></a>
<div class="furl"><n:slot name="introducer_furl"/></div> <div class="connection-status"><n:attr name="title"><n:slot name="details"/></n:attr><n:slot name="summary"/></div>
</td> </td>
<td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td> <td class="service-last-received-data"><a class="timestamp"><n:attr name="title"><n:slot name="last_received_data_abs_time"/></n:attr><n:slot name="last_received_data_rel_time"/></a></td>
</tr> </tr>