[tor-commits] [arm/master] Function to get process using a port

atagar at torproject.org atagar at torproject.org
Tue Jan 7 17:20:50 UTC 2014


commit 97404e029a7140a857bd713eea3d6fa5f95e8d8b
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Jan 6 16:46:22 2014 -0800

    Function to get process using a port
    
    Helper function that does the main functionality behind the AppResolver class.
    The class had a little additional logic to remove an extra 'sh' entry caused by
    running lsof itself. I'm a little puzzled where that came from so I'm leaving
    it out for now - we'll likely need to add some similar suppression later.
---
 arm/util/tracker.py                     |   75 ++++++++++++++++++++++++++++
 test/util/tracker/port_usage_tracker.py |   81 +++++++++++++++++++++++++++++++
 2 files changed, 156 insertions(+)

diff --git a/arm/util/tracker.py b/arm/util/tracker.py
index 6c5b1f4..fe6b822 100644
--- a/arm/util/tracker.py
+++ b/arm/util/tracker.py
@@ -187,6 +187,81 @@ def _resources_via_proc(pid):
   return (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
 
 
+def _process_for_ports(local_ports, remote_ports):
+  """
+  Provides the name of the process using the given ports.
+
+  :param list local_ports: local port numbers to look up
+  :param list remote_ports: remote port numbers to look up
+
+  :returns: **dict** mapping the ports to the associated process names
+
+  :raises: **IOError** if unsuccessful
+  """
+
+  def _parse_lsof_line(line):
+    line_comp = line.split()
+
+    if not line:
+      return None, None, None  # blank line
+    elif len(line_comp) != 10:
+      raise ValueError('lines are expected to have ten fields')
+    elif line_comp[9] != '(ESTABLISHED)':
+      return None, None, None  # connection isn't established
+
+    cmd = line_comp[0]
+    port_map = line_comp[8]
+
+    if '->' not in port_map:
+      raise ValueError("'%s' is expected to be a '->' separated mapping" % port_map)
+
+    local, remote = port_map.split('->', 1)
+
+    if ':' not in local or ':' not in remote:
+      raise ValueError("'%s' is expected to be 'address:port' entries" % port_map)
+
+    local_port = local.split(':', 1)[1]
+    remote_port = remote.split(':', 1)[1]
+
+    if not connection.is_valid_port(local_port):
+      raise ValueError("'%s' isn't a valid port" % local_port)
+    elif not connection.is_valid_port(remote_port):
+      raise ValueError("'%s' isn't a valid port" % remote_port)
+
+    return int(local_port), int(remote_port), cmd
+
+  # atagar at fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277
+  # COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+  # tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
+  # tor     2001 atagar   15u  IPv4  22024      0t0  TCP localhost:9051->localhost:51849 (ESTABLISHED)
+  # python  2462 atagar    3u  IPv4  14047      0t0  TCP localhost:37277->localhost:9051 (ESTABLISHED)
+  # python  3444 atagar    3u  IPv4  22023      0t0  TCP localhost:51849->localhost:9051 (ESTABLISHED)
+
+  lsof_cmd = 'lsof -nP ' + ' '.join(['-i tcp:%s' % port for port in (local_ports + remote_ports)])
+  lsof_call = system.call(lsof_cmd)
+
+  if lsof_call:
+    results = {}
+
+    if lsof_call[0].startswith('COMMAND  '):
+      lsof_call = lsof_call[1:]  # strip the title line
+
+    for line in lsof_call:
+      try:
+        local_port, remote_port, cmd = _parse_lsof_line(line)
+
+        if local_port in local_ports:
+          results[local_port] = cmd
+        elif remote_port in remote_ports:
+          results[remote_port] = cmd
+      except ValueError as exc:
+        raise IOError("unrecognized output from lsof (%s): %s" % (exc, line))
+
+    return results
+
+  raise IOError("no results from lsof")
+
+
 class Daemon(threading.Thread):
   """
   Daemon that can perform a given action at a set rate. Subclasses are expected
diff --git a/test/util/tracker/port_usage_tracker.py b/test/util/tracker/port_usage_tracker.py
new file mode 100644
index 0000000..a0fecdb
--- /dev/null
+++ b/test/util/tracker/port_usage_tracker.py
@@ -0,0 +1,81 @@
+import unittest
+
+from arm.util.tracker import _process_for_ports
+
+from mock import Mock, patch
+
+LSOF_OUTPUT = """\
+COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
+tor     2001 atagar   15u  IPv4  22024      0t0  TCP localhost:9051->localhost:51849 (ESTABLISHED)
+python  2462 atagar    3u  IPv4  14047      0t0  TCP localhost:37277->localhost:9051 (ESTABLISHED)
+python  3444 atagar    3u  IPv4  22023      0t0  TCP localhost:51849->localhost:9051 (ESTABLISHED)
+"""
+
+BAD_LSOF_OUTPUT_NO_ENTRY = """\
+COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+"""
+
+BAD_LSOF_OUTPUT_NOT_ESTABLISHED = """\
+COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (CLOSE_WAIT)
+"""
+
+BAD_LSOF_OUTPUT_MISSING_FIELD = """\
+COMMAND  PID   USER   TYPE DEVICE SIZE/OFF NODE NAME
+tor     2001 atagar   IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
+"""
+
+BAD_LSOF_OUTPUT_UNRECOGNIZED_MAPPING = """\
+COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051=>localhost:37277 (ESTABLISHED)
+"""
+
+BAD_LSOF_OUTPUT_NO_ADDRESS = """\
+COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+tor     2001 atagar   14u  IPv4  14048      0t0  TCP 9051->localhost:37277 (ESTABLISHED)
+"""
+
+BAD_LSOF_OUTPUT_INVALID_PORT = """\
+COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9037351->localhost:37277 (ESTABLISHED)
+"""
+
+
+class TestPortUsageTracker(unittest.TestCase):
+  @patch('arm.util.tracker.system.call', Mock(return_value = LSOF_OUTPUT.split('\n')))
+  def test_process_for_ports(self):
+    self.assertEqual({}, _process_for_ports([], []))
+    self.assertEqual({}, _process_for_ports([80, 443], []))
+    self.assertEqual({}, _process_for_ports([], [80, 443]))
+
+    self.assertEqual({37277: 'python', 51849: 'tor'}, _process_for_ports([37277], [51849]))
+
+  @patch('arm.util.tracker.system.call')
+  def test_process_for_ports_malformed(self, call_mock):
+    # Issues that are valid, but should result in us not having any content.
+
+    test_inputs = (
+      BAD_LSOF_OUTPUT_NO_ENTRY,
+      BAD_LSOF_OUTPUT_NOT_ESTABLISHED,
+    )
+
+    for test_input in test_inputs:
+      call_mock.return_value = test_input.split('\n')
+      self.assertEqual({}, _process_for_ports([80], [443]))
+
+    # Isuses that are reported as errors.
+
+    call_mock.return_value = []
+    self.assertRaises(IOError, _process_for_ports, [80], [443])
+
+    test_inputs = (
+      BAD_LSOF_OUTPUT_MISSING_FIELD,
+      BAD_LSOF_OUTPUT_UNRECOGNIZED_MAPPING,
+      BAD_LSOF_OUTPUT_NO_ADDRESS,
+      BAD_LSOF_OUTPUT_INVALID_PORT,
+    )
+
+    for test_input in test_inputs:
+      call_mock.return_value = test_input.split('\n')
+      self.assertRaises(IOError, _process_for_ports, [80], [443])





More information about the tor-commits mailing list