commit 0f7d5d118d83db000dd57b646ba91d4152c44e91 Author: Damian Johnson atagar@torproject.org Date: Mon May 27 21:20:42 2013 -0700
Controller method to query tor's pid
Adding a get_pid() method to the controller to make it simpler to figure out its pid. This attempts resolution via the PidFile, process name, control port, and control socket file. --- docs/change_log.rst | 1 + stem/control.py | 55 ++++++++++++++++++++++++ test/unit/control/controller.py | 89 ++++++++++++++++++++++++++++++++------- 3 files changed, 130 insertions(+), 15 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index d485071..cb0134e 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -40,6 +40,7 @@ The following are only available within stem's `git repository
* **Controller**
+ * Added a :class:`~stem.control.Controller` method for querying tor's pid (:func:`~stem.control.Controller.get_pid`) * :class:`~stem.response.events.AddrMapEvent` support for the new CACHED argument (:trac:`8596`, :spec:`25b0d43`) * :func:`~stem.control.Controller.attach_stream` could encounter an undocumented 555 response (:trac:`8701`, :spec:`7286576`) * :class:`~stem.descriptor.server_descriptor.RelayDescriptor` digest validation was broken when dealing with non-unicode content with python 3 (:trac:`8755`) diff --git a/stem/control.py b/stem/control.py index 6ec0006..a1a7ae4 100644 --- a/stem/control.py +++ b/stem/control.py @@ -24,6 +24,7 @@ providing its own for interacting at a higher level. |- get_exit_policy - provides our exit policy |- get_socks_listeners - provides where tor is listening for SOCKS connections |- get_protocolinfo - information about the controller interface + |- get_pid - provides the pid of our tor process | |- get_microdescriptor - querying the microdescriptor for a relay |- get_microdescriptors - provides all presently available microdescriptors @@ -152,6 +153,7 @@ import stem.socket import stem.util.connection import stem.util.enum import stem.util.str_tools +import stem.util.system import stem.util.tor_tools import stem.version
@@ -992,6 +994,59 @@ class Controller(BaseController): else: return default
+ def get_pid(self, default = UNDEFINED): + """ + Provides the process id of tor. This only works if tor is running locally. + Also, most of its checks are platform dependent, and hence are not entirely + reliable. + + :param object default: response if the query fails + + :returns: int with our process' pid + + :raises: **ValueError** if unable to determine the pid and no default was + provided + """ + + if not self.get_socket().is_localhost(): + if default == UNDEFINED: + raise ValueError("Tor isn't running locally") + else: + return default + + pid = self._get_cache("pid") + + if not pid: + pid_file_path = self.get_conf("PidFile", None) + + if pid_file_path is not None: + with open(pid_file_path) as pid_file: + pid_file_contents = pid_file.read().strip() + + if pid_file_contents.isdigit(): + pid = int(pid_file_contents) + + if not pid: + pid = stem.util.system.get_pid_by_name('tor') + + if not pid: + control_socket = self.get_socket() + + if isinstance(control_socket, stem.socket.ControlPort): + pid = stem.util.system.get_pid_by_port(control_socket.get_port()) + elif isinstance(control_socket, stem.socket.ControlSocketFile): + pid = stem.util.system.get_pid_by_open_file(control_socket.get_socket_path()) + + if pid and self.is_caching_enabled(): + self._set_cache({"pid": pid}) + + if pid: + return pid + elif default == UNDEFINED: + raise ValueError("Unable to resolve tor's pid") + else: + return default + def get_microdescriptor(self, relay, default = UNDEFINED): """ Provides the microdescriptor for the relay with the given fingerprint or diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py index 9efe4bf..ab89ac0 100644 --- a/test/unit/control/controller.py +++ b/test/unit/control/controller.py @@ -3,16 +3,20 @@ Unit tests for the stem.control module. The module's primarily exercised via integ tests, but a few bits lend themselves to unit testing. """
+import os +import tempfile import unittest
import stem.descriptor.router_status_entry import stem.response import stem.socket +import stem.util.system import stem.version
from stem import InvalidArguments, InvalidRequest, ProtocolError, UnsatisfiableRequest from stem.control import _parse_circ_path, Controller, EventType from stem.exit_policy import ExitPolicy +from stem.socket import ControlSocket from test import mocking
@@ -215,63 +219,118 @@ class TestControl(unittest.TestCase): Exercises the get_protocolinfo() method. """
- # Use the handy mocked protocolinfo response. + # use the handy mocked protocolinfo response + mocking.mock(stem.connection.get_protocolinfo, mocking.return_value( mocking.get_protocolinfo_response() )) - # Compare the str representation of these object, because the class - # does not have, nor need, a direct comparison operator. - self.assertEqual(str(mocking.get_protocolinfo_response()), str(self.controller.get_protocolinfo()))
- # Raise an exception in the stem.connection.get_protocolinfo() call. + # compare the str representation of these object, because the class + # does not have, nor need, a direct comparison operator + + self.assertEqual( + str(mocking.get_protocolinfo_response()), + str(self.controller.get_protocolinfo()) + ) + + # raise an exception in the stem.connection.get_protocolinfo() call + mocking.mock(stem.connection.get_protocolinfo, mocking.raise_exception(ProtocolError))
- # Get a default value when the call fails. + # get a default value when the call fails
self.assertEqual( "default returned", self.controller.get_protocolinfo(default = "default returned") )
- # No default value, accept the error. + # no default value, accept the error + self.assertRaises(ProtocolError, self.controller.get_protocolinfo)
+ def test_get_pid_remote(self): + """ + Exercise the get_pid() method for a non-local socket. + """ + + mocking.mock_method(ControlSocket, "is_localhost", mocking.return_false()) + + self.assertRaises(ValueError, self.controller.get_pid) + self.assertEqual(123, self.controller.get_pid(123)) + + def test_get_pid_by_pid_file(self): + """ + Exercise the get_pid() resolution via a PidFile. + """ + + # It's a little inappropriate for us to be using tempfile in unit tests, + # but this is more reliable than trying to mock open(). + + mocking.mock_method(ControlSocket, "is_localhost", mocking.return_true()) + + pid_file_path = tempfile.mkstemp()[1] + + try: + with open(pid_file_path, 'w') as pid_file: + pid_file.write('321') + + mocking.mock_method(Controller, "get_conf", mocking.return_value(pid_file_path)) + self.assertEqual(321, self.controller.get_pid()) + finally: + os.remove(pid_file_path) + + def test_get_pid_by_name(self): + """ + Exercise the get_pid() resolution via the process name. + """ + + mocking.mock_method(ControlSocket, "is_localhost", mocking.return_true()) + mocking.mock(stem.util.system.get_pid_by_name, mocking.return_value(432)) + self.assertEqual(432, self.controller.get_pid()) + def test_get_network_status(self): """ Exercises the get_network_status() method. """
- # Build a single router status entry. + # build a single router status entry + nickname = "Beaver" fingerprint = "/96bKo4soysolMgKn5Hex2nyFSY" desc = "r %s %s u5lTXJKGsLKufRLnSyVqT7TdGYw 2012-12-30 22:02:49 77.223.43.54 9001 0\ns Fast Named Running Stable Valid\nw Bandwidth=75" % (nickname, fingerprint) router = stem.descriptor.router_status_entry.RouterStatusEntryV2(desc)
- # Always return the same router status entry. + # always return the same router status entry + mocking.mock_method(Controller, "get_info", mocking.return_value(desc))
- # Pretend to get the router status entry with its name. + # pretend to get the router status entry with its name + self.assertEqual(router, self.controller.get_network_status(nickname))
- # Pretend to get the router status entry with its fingerprint. + # pretend to get the router status entry with its fingerprint + hex_fingerprint = stem.descriptor.router_status_entry._base64_to_hex(fingerprint, False) self.assertEqual(router, self.controller.get_network_status(hex_fingerprint))
- # Mangle hex fingerprint and try again. + # mangle hex fingerprint and try again + hex_fingerprint = hex_fingerprint[2:] self.assertRaises(ValueError, self.controller.get_network_status, hex_fingerprint)
- # Raise an exception in the get_info() call. + # raise an exception in the get_info() call + mocking.mock_method(Controller, "get_info", mocking.raise_exception(InvalidArguments))
- # Get a default value when the call fails. + # get a default value when the call fails
self.assertEqual( "default returned", self.controller.get_network_status(nickname, default = "default returned") )
- # No default value, accept the error. + # no default value, accept the error + self.assertRaises(InvalidArguments, self.controller.get_network_status, nickname)
def test_event_listening(self):
tor-commits@lists.torproject.org