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",
"mock",
"tox",
"foolscap[tor] >= 0.12.3",
"foolscap[tor] >= 0.12.5",
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work
"foolscap[i2p]",
"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",
],
"tor": [
"foolscap[tor] >= 0.12.3",
"foolscap[tor] >= 0.12.5",
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work
],
"i2p": [

View File

@ -41,7 +41,8 @@ install_requires = [
# with a FIPS build of OpenSSL.
# * foolscap >= 0.12.3 provides tcp/tor/i2p connection handlers we need,
# and allocate_tcp_port
"foolscap >= 0.12.3",
# * foolscap >= 0.12.5 has ConnectionInfo and ReconnectionInfo
"foolscap >= 0.12.5",
# Needed for SFTP.
# 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
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):
return any([ic.connected_to_introducer() for ic in self.introducer_clients])

View File

@ -2830,3 +2830,60 @@ class InsufficientVersionError(Exception):
class EmptyPathnameComponentError(Exception):
"""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
from allmydata.introducer.common import sign_to_foolscap, unsign_from_foolscap,\
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.keyutil import BadSignatureError
from allmydata.util.assertutil import precondition
@ -326,14 +326,16 @@ class IntroducerClient(service.Service, Referenceable):
if service_name2 == service_name:
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):
return bool(self._publisher)
def get_since(self):
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 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.observer import ObserverList
from allmydata.util.rrefutil import add_version_to_remote_reference
@ -364,17 +364,16 @@ class NativeStorageServer(service.MultiService):
return self.announcement
def get_remote_host(self):
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):
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):
version = self.get_version()

View File

@ -62,7 +62,8 @@ class Tor(unittest.TestCase):
n = FakeNode(config)
h = n._make_tor_handler()
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.assertIdentical(h, h1)
@ -75,7 +76,9 @@ class Tor(unittest.TestCase):
cfs = mock.Mock(return_value=tcep)
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor):
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,
tp._txtorcon)
cfs.assert_called_with(reactor, "ep_desc")

View File

@ -271,6 +271,14 @@ class FakeConfig(dict):
raise KeyError
return value
class EmptyContext(object):
def __init__(self):
pass
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
pass
class Provider(unittest.TestCase):
def test_build(self):
tor_provider.Provider("basedir", FakeConfig(), "reactor")
@ -298,13 +306,15 @@ class Provider(unittest.TestCase):
txtorcon = mock.Mock()
handler = object()
tor.control_endpoint_maker = mock.Mock(return_value=handler)
tor.add_context = mock.Mock(return_value=EmptyContext())
with mock_tor(tor):
with mock_txtorcon(txtorcon):
p = tor_provider.Provider("basedir", FakeConfig(launch=True),
reactor)
h = p.get_tor_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
# requested, and never again. The clientFromString() function is
@ -316,7 +326,8 @@ class Provider(unittest.TestCase):
cfs = mock.Mock(return_value=ep)
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor):
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()
self.assertIs(self.successResultOf(d), ep)
launch_tor.assert_called_with(reactor, None,
@ -328,7 +339,8 @@ class Provider(unittest.TestCase):
cfs2 = mock.Mock(return_value=ep)
with mock.patch("allmydata.util.tor_provider._launch_tor", launch_tor2):
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()
self.assertIs(self.successResultOf(d2), ep)
self.assertEqual(launch_tor2.mock_calls, [])

View File

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

View File

@ -20,6 +20,7 @@ from allmydata.web import status
from allmydata.util import fileutil, base32, hashutil
from allmydata.util.consumer import download_to_data
from allmydata.util.encodingutil import to_str
from ...util.connection_status import ConnectionStatus
from ..common import FakeCHKFileNode, FakeMutableFileNode, \
create_chk_filenode, WebErrorMixin, \
make_mutable_file_uri, create_mutable_filenode
@ -164,26 +165,21 @@ class FakeDisplayableServer(StubServer):
self.last_loss_time = last_loss_time
self.last_rx_time = last_rx_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)
def is_connected(self):
def is_connected(self): # TODO: remove me
return self.connected
def get_permutation_seed(self):
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):
return self.announcement
def get_nickname(self):
return self.announcement["nickname"]
def get_available_space(self):
return 123456
def get_connection_status(self):
return ConnectionStatus(self.connected, "summary", "description",
self.last_connect_time, self.last_rx_time)
class FakeBucketCounter(object):
def get_state(self):
@ -243,7 +239,7 @@ class FakeClient(Client):
self.storage_broker.test_add_server("disconnected_nodeid",
FakeDisplayableServer(
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.history = FakeHistory()
self.uploader = FakeUploader()
@ -611,6 +607,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
def test_welcome(self):
d = self.GET("/")
def _check(res):
# TODO: replace this with a parser
self.failUnlessIn('<title>Tahoe-LAFS - Welcome</title>', res)
self.failUnlessIn(FAVICON_MARKUP, 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)
def timestamp(t):
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(
u'<div class="status-indicator"><img (src="img/connected-yes.png" |alt="Connected" ){2}/>'
u'</div>\n <a( class="timestamp"| title=%s){2}>1d\u00A00h\u00A00m\u00A050s</a>'
u'<div class="status-indicator"><img (src="img/connected-yes.png" |alt="Connected" ){2}/></div>'
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))
# same for these two nodes
self.failUnless(re.search(
u'<div class="status-indicator"><img (src="img/connected-no.png" |alt="Disconnected" ){2}/>'
u'</div>\n <a( class="timestamp"| title=%s){2}>1d\u00A00h\u00A00m\u00A035s</a>'
% timestamp(u'1970-01-01 13:00:25'), res_u), repr(res_u))
u'<div class="status-indicator"><img (src="img/connected-no.png" |alt="Disconnected" ){2}/></div>'
u'\s+'
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(
u'<td class="service-last-received-data"><a( class="timestamp"| title=%s){2}>'
u'1d\u00A00h\u00A00m\u00A030s</a></td>'
@ -656,12 +666,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
class MockIntroducerClient(object):
def __init__(self, connected):
self.connected = connected
def connected_to_introducer(self):
return self.connected
def get_since(self):
return 0
def get_last_received_data_time(self):
return 0
def connection_status(self):
return ConnectionStatus(self.connected,
"summary", "description", 0, 0)
d = defer.succeed(None)
@ -673,7 +680,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_set_introducer_not_connected_unguessable)
def _check_introducer_not_connected_unguessable(res):
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.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)
def _check_introducer_connected_unguessable(res):
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.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)
@ -700,7 +707,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi
d.addCallback(_set_introducer_connected_guessable)
def _check_introducer_connected_guessable(res):
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)
d.addCallback(_check_introducer_connected_guessable)
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 not self._txtorcon:
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)
if socks_endpoint_desc:
@ -241,8 +242,10 @@ class Provider(service.MultiService):
return self._tor.default_socks()
@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"
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)
returnValue(tor_control_endpoint)

View File

@ -1,6 +1,5 @@
import time, os
from twisted.internet import address
from twisted.web import http
from nevow import rend, url, tags as T
from nevow.inevow import IRequest
@ -240,18 +239,14 @@ class Root(rend.Page):
return "%s introducers connected" % (connected_count,)
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):
return self.client.introducer_connection_statuses().count(True)
def data_connected_to_introducer(self, ctx, data):
if self.client.connected_to_introducer():
return "yes"
return "no"
return len([1 for cs in self.client.introducer_connection_statuses()
if cs.connected])
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 "no"
@ -260,42 +255,35 @@ class Root(rend.Page):
# In case we configure multiple introducers
def data_introducers(self, ctx, data):
connection_statuses = 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
return self.client.introducer_connection_statuses()
def render_introducers_row(self, ctx, s):
(furl, connected, ic) = s
service_connection_status = "yes" if connected else "no"
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,))
def render_introducers_row(self, ctx, cs):
connected = "yes" if cs.connected else "no"
ctx.fillSlots("service_connection_status", connected)
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)
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)
return ctx.tag
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())
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("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()
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)
connected = "yes" if cs.connected else "no"
ctx.fillSlots("service_connection_status", connected)
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()
version = announcement.get("my-version", "")
@ -379,14 +372,6 @@ class Root(rend.Page):
available_space = "N/A"
else:
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("available_space", available_space)

View File

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

View File

@ -176,7 +176,7 @@
<thead>
<tr n:pattern="header">
<td><h3>Nickname</h3></td>
<td><h3>Address</h3></td>
<td><h3>Connection</h3></td>
<td><h3>Last&nbsp;RX</h3></td>
<td><h3>Version</h3></td>
<td><h3>Available</h3></td>
@ -185,16 +185,22 @@
<tr n:pattern="item" n:render="service_row">
<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>
<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="nodeid"><n:slot name="peerid"/></div>
</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-version"><n:slot name="version"/></td>
<td class="service-available-space"><n:slot name="available_space"/></td>
</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>
<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>
@ -202,7 +208,7 @@
<table class="table table-striped table-bordered peer-status" n:render="sequence" n:data="introducers">
<thead>
<tr n:pattern="header">
<td><h3>Address</h3></td>
<td><h3>Connection</h3></td>
<td><h3>Last&nbsp;RX</h3></td>
</tr>
</thead>
@ -210,7 +216,7 @@
<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>
<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 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>