[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