[tor-commits] [stem/master] Controller method to query tor's pid

atagar at torproject.org atagar at torproject.org
Tue May 28 04:24:14 UTC 2013


commit 0f7d5d118d83db000dd57b646ba91d4152c44e91
Author: Damian Johnson <atagar at 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):



More information about the tor-commits mailing list