[tor-commits] [nyx/master] Rewrite how we handle connection details

atagar at torproject.org atagar at torproject.org
Tue Sep 22 17:08:41 UTC 2015


commit ed2b57107c30e913c31283e1fbcae38f7ce10f3b
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Sep 6 12:09:30 2015 -0700

    Rewrite how we handle connection details
    
    When the user presses 'enter' we show a subdialog with more detailed
    information. Rewriting how we do this, pulling display logic from
    ConnectionLine into a draw submethod.
    
    This also simplifies our connection tracker, merging get_relay_fingerprint()
    and get_all_relay_fingerprints().
---
 nyx/__init__.py         |    1 +
 nyx/connection_panel.py |  243 ++++++++++++++++++-----------------------------
 nyx/log_panel.py        |   10 +-
 nyx/util/tracker.py     |   41 ++------
 4 files changed, 105 insertions(+), 190 deletions(-)

diff --git a/nyx/__init__.py b/nyx/__init__.py
index 65e3c71..078955d 100644
--- a/nyx/__init__.py
+++ b/nyx/__init__.py
@@ -12,6 +12,7 @@ __license__ = 'GPLv3'
 __all__ = [
   'arguments',
   'config_panel',
+  'connection_panel',
   'controller',
   'header_panel',
   'log_panel',
diff --git a/nyx/connection_panel.py b/nyx/connection_panel.py
index f133ff8..92d4fed 100644
--- a/nyx/connection_panel.py
+++ b/nyx/connection_panel.py
@@ -146,7 +146,7 @@ class ConnectionEntry(Entry):
       if self._connection.remote_port == hs_config['HiddenServicePort']:
         return Category.HIDDEN
 
-    fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprint(self._connection.remote_address, self._connection.remote_port)
+    fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address).get(self._connection.remote_port)
 
     if fingerprint:
       for circ in controller.get_circuits([]):
@@ -174,7 +174,7 @@ class ConnectionEntry(Entry):
       controller = tor_controller()
 
       if controller.is_user_traffic_allowed().inbound:
-        return len(nyx.util.tracker.get_consensus_tracker().get_all_relay_fingerprints(self._connection.remote_address)) == 0
+        return len(nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self._connection.remote_address)) == 0
     elif self.get_type() == Category.EXIT:
       # DNS connections exiting us aren't private (since they're hitting our
       # resolvers). Everything else is.
@@ -233,7 +233,7 @@ class ConnectionLine(object):
     """
 
     if self._entry.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT):
-      my_fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprint(self.connection.remote_address, self.connection.remote_port)
+      my_fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(self.connection.remote_address).get(self.connection.remote_port)
       return my_fingerprint if my_fingerprint else default
     else:
       return default  # inbound connections don't have an ORPort we can resolve
@@ -310,19 +310,6 @@ class ConnectionLine(object):
 
     return draw_entry
 
-  @lru_cache()
-  def get_details(self, width):
-    """
-    Provides details on the connection, correlated against available consensus
-    data.
-
-    Arguments:
-      width - available space to display in
-    """
-
-    detail_format = (curses.A_BOLD, CONFIG['attr.connection.category_color'].get(self._entry.get_type(), 'white'))
-    return [(line, detail_format) for line in self._get_detail_content(width)]
-
   def get_etc_content(self, width, listing_type):
     """
     Provides the optional content for the connection.
@@ -488,82 +475,6 @@ class ConnectionLine(object):
 
     return LABEL_FORMAT % (src, dst, etc, padding)
 
-  def _get_detail_content(self, width):
-    """
-    Provides a list with detailed information for this connection.
-
-    Arguments:
-      width - max length of lines
-    """
-
-    lines = [''] * 7
-    lines[0] = 'address: %s' % self.get_destination_label(width - 11)
-    lines[1] = 'locale: %s' % ('??' if self._entry.is_private() else self.get_locale('??'))
-
-    # Remaining data concerns the consensus results, with three possible cases:
-    # - if there's a single match then display its details
-    # - if there's multiple potential relays then list all of the combinations
-    #   of ORPorts / Fingerprints
-    # - if no consensus data is available then say so (probably a client or
-    #   exit connection)
-
-    fingerprint = self.get_fingerprint()
-    controller = tor_controller()
-
-    if fingerprint:
-      lines[1] = '%-13sfingerprint: %s' % (lines[1], fingerprint)  # append fingerprint to second line
-
-      router_status_entry = controller.get_network_status(fingerprint, None)
-      server_descriptor = controller.get_server_descriptor(fingerprint, None)
-
-      if router_status_entry:
-        dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else ''
-        lines[2] = 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label)
-        lines[3] = 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y")
-        lines[4] = 'flags: %s' % ', '.join(router_status_entry.flags)
-
-      if server_descriptor:
-        policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown'
-        lines[5] = 'exit policy: %s' % policy_label
-        lines[3] = '%-35s os: %-14s version: %s' % (lines[3], server_descriptor.operating_system, server_descriptor.tor_version)
-
-        if server_descriptor.contact:
-          lines[6] = 'contact: %s' % server_descriptor.contact
-    else:
-      all_matches = nyx.util.tracker.get_consensus_tracker().get_all_relay_fingerprints(self.connection.remote_address)
-
-      if all_matches:
-        # multiple matches
-        lines[2] = 'Multiple matches, possible fingerprints are:'
-
-        for i in range(len(all_matches)):
-          is_last_line = i == 3
-
-          relay_port, relay_fingerprint = all_matches[i]
-          line_text = '%i. or port: %-5s fingerprint: %s' % (i + 1, relay_port, relay_fingerprint)
-
-          # if there's multiple lines remaining at the end then give a count
-
-          remaining_relays = len(all_matches) - i
-
-          if is_last_line and remaining_relays > 1:
-            line_text = '... %i more' % remaining_relays
-
-          lines[3 + i] = line_text
-
-          if is_last_line:
-            break
-      else:
-        # no consensus entry for this ip address
-        lines[2] = 'No consensus data found'
-
-    # crops any lines that are too long
-
-    for i in range(len(lines)):
-      lines[i] = str_tools.crop(lines[i], width - 2)
-
-    return lines
-
   def get_destination_label(self, max_length, include_locale = False):
     """
     Provides a short description of the destination. This is made up of two
@@ -665,14 +576,6 @@ class CircHeaderLine(ConnectionLine):
 
     return ''
 
-  @lru_cache()
-  def get_details(self, width):
-    if not self.is_built:
-      detail_format = (curses.A_BOLD, CONFIG['attr.connection.category_color'].get(self._entry.get_type(), 'white'))
-      return [('Building Circuit...', detail_format)]
-    else:
-      return ConnectionLine.get_details(self, width)
-
 
 class CircLine(ConnectionLine):
   """
@@ -791,7 +694,6 @@ class ConnectionPanel(panel.Panel, threading.Thread):
 
     self._scroller = ui_tools.Scroller(True)
     self._entries = []            # last fetched display entries
-    self._entry_lines = []        # individual lines rendered from the entries listing
     self._show_details = False    # presents the details panel if true
 
     self._last_update = -1        # time the content was last revised
@@ -915,7 +817,6 @@ class ConnectionPanel(panel.Panel, threading.Thread):
           return ''
 
       self._entries.sort(key = lambda i: [sort_value(i, attr) for attr in CONFIG['features.connection.order']])
-      self._entry_lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
 
   def get_listing_type(self):
     """
@@ -970,7 +871,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         if self._show_details:
           page_height -= (DETAILS_HEIGHT + 1)
 
-        is_changed = self._scroller.handle_key(key, self._entry_lines, page_height)
+        lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
+        is_changed = self._scroller.handle_key(key, lines, page_height)
 
         if is_changed:
           self.redraw(True)
@@ -1018,7 +920,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         self.redraw(True)
 
         while True:
-          selection = self.get_selection()
+          lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
+          selection = self._scroller.get_cursor_selection(lines)
 
           if not selection:
             break
@@ -1109,68 +1012,40 @@ class ConnectionPanel(panel.Panel, threading.Thread):
 
     return options
 
-  def get_selection(self):
-    """
-    Provides the currently selected connection entry.
-    """
-
-    return self._scroller.get_cursor_selection(self._entry_lines)
-
   def draw(self, width, height):
     with self._vals_lock:
-      # if we don't have any contents then refuse to show details
-
-      if not self._entries:
-        self._show_details = False
-
-      # extra line when showing the detail panel is for the bottom border
-
-      detail_panel_offset = DETAILS_HEIGHT + 1 if self._show_details else 0
-      is_scrollbar_visible = len(self._entry_lines) > height - detail_panel_offset - 1
-
-      scroll_location = self._scroller.get_scroll_location(self._entry_lines, height - detail_panel_offset - 1)
-      cursor_selection = self.get_selection()
-
-      # draws the detail panel if currently displaying it
+      lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
+      selected = self._scroller.get_cursor_selection(lines)
 
-      if self._show_details and cursor_selection:
-        # This is a solid border unless the scrollbar is visible, in which case a
-        # 'T' pipe connects the border to the bar.
-
-        ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
-
-        if is_scrollbar_visible:
-          self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
-
-        draw_entries = cursor_selection.get_details(width)
-
-        for i in range(min(len(draw_entries), DETAILS_HEIGHT)):
-          self.addstr(1 + i, 2, draw_entries[i][0], *draw_entries[i][1])
-
-      # title label with connection counts
+      details_offset = DETAILS_HEIGHT + 1 if self._show_details else 0
+      is_scrollbar_visible = len(lines) > height - details_offset - 1
+      scroll_location = self._scroller.get_scroll_location(lines, height - details_offset - 1)
 
       if self.is_title_visible():
-        self._draw_title(self._entries)
+        self._draw_title(self._entries, self._show_details)
+
+      if self._show_details and selected:
+        self._draw_details(selected, width, is_scrollbar_visible)
 
       scroll_offset = 0
 
       if is_scrollbar_visible:
         scroll_offset = 2
-        self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_offset - 1, len(self._entry_lines), 1 + detail_panel_offset)
+        self.add_scroll_bar(scroll_location, scroll_location + height - details_offset - 1, len(lines), 1 + details_offset)
 
       if self.is_paused() or not self._is_tor_running:
         current_time = self.get_pause_time()
       else:
         current_time = time.time()
 
-      for line_number in range(scroll_location, len(self._entry_lines)):
-        entry_line = self._entry_lines[line_number]
+      for line_number in range(scroll_location, len(lines)):
+        entry_line = lines[line_number]
 
         # hilighting if this is the selected line
 
-        extra_format = curses.A_STANDOUT if entry_line == cursor_selection else curses.A_NORMAL
+        extra_format = curses.A_STANDOUT if entry_line == selected else curses.A_NORMAL
 
-        draw_line = line_number + detail_panel_offset + 1 - scroll_location
+        draw_line = line_number + details_offset + 1 - scroll_location
 
         prefix = entry_line.get_listing_prefix()
 
@@ -1188,17 +1063,79 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         if draw_line >= height:
           break
 
-  def _draw_title(self, entries):
-    if self._show_details:
-      title = 'Connection Details:'
+  def _draw_title(self, entries, showing_details):
+    """
+    Panel title with the number of connections we presently have.
+    """
+
+    if showing_details:
+      self.addstr(0, 0, 'Connection Details:', curses.A_STANDOUT)
     elif not entries:
-      title = 'Connections:'
+      self.addstr(0, 0, 'Connections:', curses.A_STANDOUT)
     else:
       counts = collections.Counter([entry.get_type() for entry in entries])
       count_labels = ['%i %s' % (counts[category], category.lower()) for category in Category if counts[category]]
-      title = 'Connections (%s):' % ', '.join(count_labels)
+      self.addstr(0, 0, 'Connections (%s):' % ', '.join(count_labels), curses.A_STANDOUT)
+
+  def _draw_details(self, selected, width, is_scrollbar_visible):
+    """
+    Shows detailed information about the selected connection.
+    """
+
+    attr = (CONFIG['attr.connection.category_color'].get(selected._entry.get_type(), 'white'), curses.A_BOLD)
+
+    if isinstance(selected, CircHeaderLine) and not selected.is_built:
+      self.addstr(1, 2, 'Building Circuit...', *attr)
+    else:
+      self.addstr(1, 2, 'address: %s' % selected.get_destination_label(width - 11), *attr)
+      self.addstr(2, 2, 'locale: %s' % ('??' if selected._entry.is_private() else selected.get_locale('??')), *attr)
+
+      matches = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprints(selected.connection.remote_address)
+
+      if not matches:
+        self.addstr(3, 2, 'No consensus data found', *attr)
+      elif len(matches) == 1 or selected.connection.remote_port in matches:
+        controller = tor_controller()
+        fingerprint = matches.values()[0] if len(matches) == 1 else matches[selected.connection.remote_port]
+        router_status_entry = controller.get_network_status(fingerprint, None)
+
+        self.addstr(2, 15, 'fingerprint: %s' % fingerprint, *attr)
+
+        if router_status_entry:
+          dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else ''
+          self.addstr(3, 2, 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label), *attr)
+          self.addstr(4, 2, 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y"), *attr)
+          self.addstr(5, 2, 'flags: %s' % ', '.join(router_status_entry.flags), *attr)
+
+          server_descriptor = controller.get_server_descriptor(fingerprint, None)
+
+          if server_descriptor:
+            policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown'
+            self.addstr(6, 2, 'exit policy: %s' % policy_label, *attr)
+            self.addstr(4, 38, 'os: %-14s version: %s' % (server_descriptor.operating_system, server_descriptor.tor_version), *attr)
+
+            if server_descriptor.contact:
+              self.addstr(7, 2, 'contact: %s' % server_descriptor.contact, *attr)
+      else:
+        self.addstr(3, 2, 'Multiple matches, possible fingerprints are:', *attr)
+
+        for i, port in enumerate(sorted(matches.keys())):
+          is_last_line, remaining_relays = i == 3, len(matches) - i
+
+          if not is_last_line or remaining_relays == 1:
+            self.addstr(4 + i, 2, '%i. or port: %-5s fingerprint: %s' % (i + 1, port, matches[port]), *attr)
+          else:
+            self.addstr(4 + i, 2, '... %i more' % remaining_relays, *attr)
+
+          if is_last_line:
+            break
+
+    # draw the border, with a 'T' pipe if connecting with the scrollbar
+
+    ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
 
-    self.addstr(0, 0, title, curses.A_STANDOUT)
+    if is_scrollbar_visible:
+      self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
 
   def stop(self):
     """
@@ -1248,7 +1185,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
             exit_port = entry_line.connection.remote_port
             self._exit_port_usage[exit_port] = self._exit_port_usage.get(exit_port, 0) + 1
 
-      self._entries, self._entry_lines = new_entries, list(itertools.chain.from_iterable([entry.get_lines() for entry in new_entries]))
+      self._entries = new_entries
 
       self.set_sort_order()
       self._last_resource_fetch = current_resolution_count
diff --git a/nyx/log_panel.py b/nyx/log_panel.py
index 4912cd7..dedfc99 100644
--- a/nyx/log_panel.py
+++ b/nyx/log_panel.py
@@ -281,12 +281,12 @@ class LogPanel(panel.Panel, threading.Thread):
     last_content_height = int(self._last_content_height)
     show_duplicates = self._show_duplicates
 
-    is_scroll_bar_visible = last_content_height > height - 1
+    is_scrollbar_visible = last_content_height > height - 1
 
-    if is_scroll_bar_visible:
+    if is_scrollbar_visible:
       self.add_scroll_bar(scroll, scroll + height - 1, last_content_height, 1)
 
-    x, y = 3 if is_scroll_bar_visible else 1, 1 - scroll
+    x, y = 3 if is_scrollbar_visible else 1, 1 - scroll
 
     # group entries by date, filtering out those that aren't visible
 
@@ -333,9 +333,9 @@ class LogPanel(panel.Panel, threading.Thread):
       force_redraw_reason = 'estimate was off by %i' % content_height_delta
     elif new_content_height > height and self._scroll + height - 1 > new_content_height:
       force_redraw_reason = 'scrolled off the bottom of the page'
-    elif not is_scroll_bar_visible and new_content_height > height - 1:
+    elif not is_scrollbar_visible and new_content_height > height - 1:
       force_redraw_reason = "scroll bar wasn't previously visible"
-    elif is_scroll_bar_visible and new_content_height <= height - 1:
+    elif is_scrollbar_visible and new_content_height <= height - 1:
       force_redraw_reason = "scroll bar shouldn't be visible"
     else:
       force_redraw = False
diff --git a/nyx/util/tracker.py b/nyx/util/tracker.py
index 88a53c3..237a01c 100644
--- a/nyx/util/tracker.py
+++ b/nyx/util/tracker.py
@@ -31,8 +31,7 @@ Background tasks for gathering information about the tor process.
   ConsensusTracker - performant lookups for consensus related information
     |- update - updates the consensus information we're based on
     |- get_relay_nickname - provides the nickname for a given relay
-    |- get_relay_fingerprint - provides the relay running at a location
-    |- get_all_relay_fingerprints - provides all relays running at a location
+    |- get_relay_fingerprints - provides relays running at a location
     +- get_relay_address - provides the address a relay is running at
 
 .. data:: Resources
@@ -821,47 +820,25 @@ class ConsensusTracker(object):
 
     return self._nickname_cache.get(fingerprint)
 
-  def get_relay_fingerprint(self, address, port = None):
+  def get_relay_fingerprints(self, address):
     """
-    Provides the relay running at a given location. If there's multiple relays
-    and no port is provided to disambiguate then this returns **None**.
+    Provides the relays running at a given location.
 
     :param str address: address to be checked
-    :param int port: optional ORPort to match against
 
-    :returns: **str** with the fingerprint of the relay running there
+    :returns: **dict** of ORPorts to their fingerprint
     """
 
     controller = tor_controller()
 
     if address == controller.get_info('address', None):
-      if not port or port in controller.get_ports(stem.control.Listener.OR, []):
-        return controller.get_info('fingerprint', None)
+      fingerprint = controller.get_info('fingerprint', None)
+      ports = controller.get_ports(stem.control.Listener.OR, None)
 
-    matches = self._fingerprint_cache.get(address, [])
+      if fingerprint and ports:
+        return dict([(port, fingerprint) for port in ports])
 
-    if len(matches) == 1:
-      match_port, match_fingerprint = matches[0]
-      return match_fingerprint if (not port or port == match_port) else None
-    elif len(matches) > 1 and port:
-      # there's multiple matches and we have a port to disambiguate with
-
-      for match_port, match_fingerprint in matches:
-        if port == match_port:
-          return match_fingerprint
-
-    return None
-
-  def get_all_relay_fingerprints(self, address):
-    """
-    Provides a [(port, fingerprint)...] tuple of all relays on a given address.
-
-    :param str address: address to be checked
-
-    :returns: **list** of port/fingerprint tuples running on it
-    """
-
-    return self._fingerprint_cache.get(address, [])
+    return dict([(port, fp) for (port, fp) in self._fingerprint_cache.get(address, [])])
 
   def get_relay_address(self, fingerprint, default):
     """





More information about the tor-commits mailing list