From 77fd41b66edeea4f9fa31fcb28c82a7774b790b4 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 8 Dec 2016 15:15:49 -0800 Subject: [PATCH] update WUI welcome page with new connection-status info This shows current-connection info, and provides per-hint status details in a tooltip. The "Connection" section no longer shows seconds-since-loss when the server was not connected (previously it showed seconds-since-connect when connected, and flipped to seconds-since-loss when disconnected). We already have the "Last RX" column, which is arguably more meaningful (and I can't think of a good case when these would differ), so we don't really need seconds-since-loss, and the new ConnectionStatus doesn't track it anyways. So now the "Connection" timestamp for non-connected servers is just "N/A" (both the main text and the tooltip). The "Introducers" section was changed the same way. This moves the per-server connection timestamp out of the nickname/serverid box and over into the Connection box. It also right-floats all timestamps, regardless of which box they're in, which makes them share the box with connection_status more politely. Internally, this adds code to create ConnectionStatus objects when necessary. --- src/allmydata/client.py | 2 +- src/allmydata/introducer/client.py | 10 +- src/allmydata/storage_client.py | 10 +- src/allmydata/test/web/test_root.py | 3 + src/allmydata/test/web/test_web.py | 39 ++++-- src/allmydata/web/root.py | 133 +++++++++------------ src/allmydata/web/static/css/new-tahoe.css | 5 +- src/allmydata/web/welcome.xhtml | 18 ++- 8 files changed, 127 insertions(+), 93 deletions(-) diff --git a/src/allmydata/client.py b/src/allmydata/client.py index d9c77b7e3..669d962f5 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -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]) diff --git a/src/allmydata/introducer/client.py b/src/allmydata/introducer/client.py index e1643e8ac..83ce2975c 100644 --- a/src/allmydata/introducer/client.py +++ b/src/allmydata/introducer/client.py @@ -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,6 +326,14 @@ 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) diff --git a/src/allmydata/storage_client.py b/src/allmydata/storage_client.py index 64a6985f1..73736f2ad 100644 --- a/src/allmydata/storage_client.py +++ b/src/allmydata/storage_client.py @@ -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,6 +364,14 @@ 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): diff --git a/src/allmydata/test/web/test_root.py b/src/allmydata/test/web/test_root.py index e0bfa779b..8d8232f5a 100644 --- a/src/allmydata/test/web/test_root.py +++ b/src/allmydata/test/web/test_root.py @@ -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() diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index 3914d02be..e607c8a6e 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -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 @@ -184,6 +185,9 @@ class FakeDisplayableServer(StubServer): 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 +247,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 +615,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('Tahoe-LAFS - Welcome', res) self.failUnlessIn(FAVICON_MARKUP, res) self.failUnlessIn('Recent and Active Operations', res) @@ -624,14 +629,27 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi self.failUnlessIn(u'Connected to 1\n of 2 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'
' - u'
\n 1d\u00A00h\u00A00m\u00A050s' + u'
' + u'\s+' + u'
other_nickname \u263B
', + res_u), repr(res_u)) + self.failUnless(re.search( + u'\s+1d\u00A00h\u00A00m\u00A050s\s+' % timestamp(u'1970-01-01 13:00:10'), res_u), repr(res_u)) + + # same for these two nodes self.failUnless(re.search( - u'
' - u'
\n 1d\u00A00h\u00A00m\u00A035s' - % timestamp(u'1970-01-01 13:00:25'), res_u), repr(res_u)) + u'
' + u'\s+' + u'
disconnected_nickname \u263B
', + res_u), repr(res_u)) + self.failUnless(re.search( + u'\s+N/A\s+', + res_u), repr(res_u)) + self.failUnless(re.search( u'' u'1d\u00A00h\u00A00m\u00A030s' @@ -662,6 +680,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi 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 +694,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('
pb://someIntroducer/[censored]
', html) + self.failUnlessIn('
summary
', html) self.failIfIn('pb://someIntroducer/secret', html) self.failUnless(re.search('[ ]*
No introducers connected
', html), res) @@ -687,7 +708,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('
pb://someIntroducer/[censored]
', html) + self.failUnlessIn('
summary
', html) self.failIfIn('pb://someIntroducer/secret', html) self.failUnless(re.search('[ ]*
1 introducer connected
', html), res) d.addCallback(_check_introducer_connected_unguessable) @@ -700,7 +721,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('
pb://someIntroducer/introducer
', html) + self.failUnlessIn('
summary
', html) self.failUnless(re.search('[ ]*
1 introducer connected
', html), res) d.addCallback(_check_introducer_connected_guessable) return d diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index 8e4f7c1e8..468275a0e 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -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) diff --git a/src/allmydata/web/static/css/new-tahoe.css b/src/allmydata/web/static/css/new-tahoe.css index 8ab7f47d6..38bd662e1 100644 --- a/src/allmydata/web/static/css/new-tahoe.css +++ b/src/allmydata/web/static/css/new-tahoe.css @@ -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; } diff --git a/src/allmydata/web/welcome.xhtml b/src/allmydata/web/welcome.xhtml index 2626374b3..302f5ce0e 100644 --- a/src/allmydata/web/welcome.xhtml +++ b/src/allmydata/web/welcome.xhtml @@ -176,7 +176,7 @@

Nickname

-

Address

+

Connection

Last RX

Version

Available

@@ -185,16 +185,22 @@
img/connected-.png
-
- + + + + + + + + - You are not presently connected to any peers + You are not presently connected to any servers.

Connected to of introducers

@@ -202,7 +208,7 @@ - + @@ -210,7 +216,7 @@

Address

Connection

Last RX

img/connected-.png
-
+