commit 97404e029a7140a857bd713eea3d6fa5f95e8d8b Author: Damian Johnson atagar@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@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])