mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-06-02 15:50:54 +00:00
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:
commit
fe1df17d65
4
setup.py
4
setup.py
@ -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": [
|
||||||
|
@ -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>
|
||||||
|
@ -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])
|
||||||
|
@ -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.
|
||||||
|
""")
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
@ -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, [])
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
81
src/allmydata/util/connection_status.py
Normal file
81
src/allmydata/util/connection_status.py
Normal 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
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 RX</h3></td>
|
<td><h3>Last 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 RX</h3></td>
|
<td><h3>Last 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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user