[tor-commits] [stem/master] Supporting TAKEOWNERSHIP when spawing a tor process

atagar at torproject.org atagar at torproject.org
Sat Dec 8 22:20:05 UTC 2012


commit 7fba50ff561578f746ceb3383b1bb16d709a14e3
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Dec 8 14:13:43 2012 -0800

    Supporting TAKEOWNERSHIP when spawing a tor process
    
    Adding a 'take_ownership' option to the stem.process module's launch_tor*()
    functions. When it's set we'll do the following...
    
    * Assert ownership over the tor process that we spawn by setting its
      __OwningControllerProcess to be our pid.
    
    * If a Controller connects to it then we replace our ownership via the pid with
      ownership via the socket connection (issuing a TAKEOWNERSHIP request and
      dropping __OwningControllerProcess).
    
    Thanks to lunar for the initial TAKEOWNERSHIP patch on #7666, and rransom for
    his advice on #7667.
---
 stem/control.py       |   20 +++++++++
 stem/process.py       |   43 ++++++++++++++++---
 stem/version.py       |    4 +-
 test/integ/process.py |  112 +++++++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 160 insertions(+), 19 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index 79ad70f..db5b9c5 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -110,6 +110,7 @@ providing its own for interacting at a higher level.
 
 from __future__ import with_statement
 
+import os
 import time
 import Queue
 import StringIO
@@ -1495,6 +1496,25 @@ class Controller(BaseController):
       self._attach_listeners()
     except stem.ProtocolError, exc:
       log.warn("We were unable to re-attach our event listeners to the new tor instance (%s)" % exc)
+    
+    # issue TAKEOWNERSHIP if we're the owning process for this tor instance
+    
+    owning_pid = self.get_conf("__OwningControllerProcess", None)
+    
+    if owning_pid == str(os.getpid()):
+      response = self.msg("TAKEOWNERSHIP")
+      stem.response.convert("SINGLELINE", response)
+      
+      if response.is_ok():
+        # Now that tor is tracking our ownership of the process via the control
+        # connection, we can stop having it check for us via our pid.
+        
+        try:
+          self.reset_conf("__OwningControllerProcess")
+        except stem.ControllerError, exc:
+          log.warn("We were unable to reset tor's __OwningControllerProcess configuration. It will continue to periodically check if our pid exists. (%s)" % response)
+      else:
+        log.warn("We were unable assert ownership of tor through TAKEOWNERSHIP, despite being configured to be the owning process thrugh __OwningControllerProcess. (%s)" % response)
   
   def _handle_event(self, event_message):
     stem.response.convert("EVENT", event_message, arrived_at = time.time())
diff --git a/stem/process.py b/stem/process.py
index 25ff88a..d0523bc 100644
--- a/stem/process.py
+++ b/stem/process.py
@@ -29,7 +29,7 @@ import stem.util.system
 NO_TORRC = "<no torrc>"
 DEFAULT_INIT_TIMEOUT = 90
 
-def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT):
+def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT, take_ownership = False):
   """
   Initializes a tor process. This blocks until initialization completes or we
   error out.
@@ -51,6 +51,9 @@ def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_perce
     tor's initialization stdout as we get it
   :param int timeout: time after which the attempt to start tor is aborted, no
     timeouts are applied if **None**
+  :param bool take_ownership: asserts ownership over the tor process so it
+    aborts if this python process terminates or a :class:`~stem.control.Controller`
+    we establish to it disconnects
   
   :returns: **subprocess.Popen** instance for the tor subprocess
   
@@ -76,6 +79,9 @@ def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_perce
     else:
       runtime_args += ["-f", torrc_path]
   
+  if take_ownership:
+    runtime_args += ["--__OwningControllerProcess", _get_pid()]
+  
   tor_process = subprocess.Popen(runtime_args, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
   
   if timeout:
@@ -140,13 +146,28 @@ def launch_tor(tor_cmd = "tor", args = None, torrc_path = None, completion_perce
         if ": " in msg: msg = msg.split(": ")[-1].strip()
         last_problem = msg
 
-def launch_tor_with_config(config, tor_cmd = "tor", completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT):
+def launch_tor_with_config(config, tor_cmd = "tor", completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT, take_ownership = False):
   """
   Initializes a tor process, like :func:`~stem.process.launch_tor`, but with a
   customized configuration. This writes a temporary torrc to disk, launches
   tor, then deletes the torrc.
   
-  :param dict config: configuration options, such as '{"ControlPort": "9051"}'
+  For example...
+  
+  ::
+  
+    tor_process = stem.process.launch_tor_with_config(
+      config = {
+        'ControlPort': '2778',
+        'Log': [
+          'NOTICE stdout',
+          'ERR file /tmp/tor_error_log',
+        ],
+      },
+    )
+  
+  :param dict config: configuration options, such as '{"ControlPort": "9051"}',
+    values can either be a **str** or **list of str** if for multiple values
   :param str tor_cmd: command for starting tor
   :param int completion_percent: percent of bootstrap completion at which
     this'll return
@@ -154,6 +175,9 @@ def launch_tor_with_config(config, tor_cmd = "tor", completion_percent = 100, in
     tor's initialization stdout as we get it
   :param int timeout: time after which the attempt to start tor is aborted, no
     timeouts are applied if **None**
+  :param bool take_ownership: asserts ownership over the tor process so it
+    aborts if this python process terminates or a :class:`~stem.control.Controller`
+    we establish to it disconnects
   
   :returns: **subprocess.Popen** instance for the tor subprocess
   
@@ -165,11 +189,18 @@ def launch_tor_with_config(config, tor_cmd = "tor", completion_percent = 100, in
   
   try:
     with open(torrc_path, "w") as torrc_file:
-      for key, value in config.items():
-        torrc_file.write("%s %s\n" % (key, value))
+      for key, values in config.items():
+        if isinstance(values, str):
+          torrc_file.write("%s %s\n" % (key, values))
+        else:
+          for value in values:
+            torrc_file.write("%s %s\n" % (key, value))
     
-    return launch_tor(tor_cmd, None, torrc_path, completion_percent, init_msg_handler, timeout)
+    return launch_tor(tor_cmd, None, torrc_path, completion_percent, init_msg_handler, timeout, take_ownership)
   finally:
     try: os.remove(torrc_path)
     except: pass
 
+def _get_pid():
+  return str(os.getpid())
+
diff --git a/stem/version.py b/stem/version.py
index a7f31c3..243fc50 100644
--- a/stem/version.py
+++ b/stem/version.py
@@ -37,7 +37,8 @@ easily parsed and compared, for instance...
   **AUTH_SAFECOOKIE**                   'SAFECOOKIE' authentication method
   **GETINFO_CONFIG_TEXT**               'GETINFO config-text' query
   **EXTENDCIRCUIT_PATH_OPTIONAL**       'EXTENDCIRCUIT' queries can omit the path if the circuit is zero
-  **LOADCONF**                          'LOADCONF' query
+  **LOADCONF**                          'LOADCONF' requests
+  **TAKEOWNERSHIP**                     'TAKEOWNERSHIP' requests
   **TORRC_CONTROL_SOCKET**              'ControlSocket <path>' config option
   **TORRC_DISABLE_DEBUGGER_ATTACHMENT** 'DisableDebuggerAttachment' config option
   **FEATURE_VERBOSE_NAMES**             'VERBOSE_NAMES' optional feature
@@ -257,6 +258,7 @@ Requirement = stem.util.enum.Enum(
   ("GETINFO_CONFIG_TEXT", Version("0.2.2.7")),
   ("EXTENDCIRCUIT_PATH_OPTIONAL", Version("0.2.2.9")),
   ("LOADCONF", Version("0.2.1.1")),
+  ("TAKEOWNERSHIP", Version("0.2.2.28-beta")),
   ("TORRC_CONTROL_SOCKET", Version("0.2.0.30")),
   ("TORRC_DISABLE_DEBUGGER_ATTACHMENT", Version("0.2.3.9")),
   ("FEATURE_VERBOSE_NAMES", Version("0.2.2.1-alpha")),
diff --git a/test/integ/process.py b/test/integ/process.py
index fc2825a..1c99f88 100644
--- a/test/integ/process.py
+++ b/test/integ/process.py
@@ -7,20 +7,35 @@ import time
 import shutil
 import signal
 import unittest
+import subprocess
 
 import stem.prereq
 import stem.socket
 import stem.process
-import test.runner
+import stem.version
 import stem.util.system
 
+import test.runner
+from test import mocking
+
 DATA_DIRECTORY = '/tmp/stem_integ'
 
+def _kill_process(process):
+  if stem.prereq.is_python_26():
+    process.kill()
+  elif not stem.util.system.is_windows():
+    os.kill(process.pid, signal.SIGTERM)
+  
+  process.communicate() # block until its definitely gone
+
 class TestProcess(unittest.TestCase):
   def setUp(self):
     if not os.path.exists(DATA_DIRECTORY):
       os.makedirs(DATA_DIRECTORY)
   
+  def tearDown(self):
+    mocking.revert_mocking()
+  
   def test_launch_tor_with_config(self):
     """
     Exercises launch_tor_with_config.
@@ -57,17 +72,7 @@ class TestProcess(unittest.TestCase):
       self.assertEquals("ControlPort=2778", str(getconf_response))
     finally:
       if control_socket: control_socket.close()
-      
-      if stem.prereq.is_python_26():
-        tor_process.kill()
-      elif not stem.util.system.is_windows():
-        os.kill(tor_process.pid, signal.SIGTERM)
-        
-        # On OSX, python 2.5 this kill call doesn't seem to block, causing our
-        # tor instance to linger and cause a port conflict with the following
-        # test. Giving it a moment to kill for realz.
-        
-        time.sleep(0.5)
+      _kill_process(tor_process)
   
   def test_launch_tor_with_timeout(self):
     """
@@ -88,4 +93,87 @@ class TestProcess(unittest.TestCase):
     
     if not (runtime > 2 and runtime < 3):
       self.fail("Test should have taken 2-3 seconds, took %i instead" % runtime)
+  
+  def test_take_ownership_via_pid(self):
+    """
+    Checks that the tor process quits after we do if we set take_ownership. To
+    test this we spawn a process and trick tor into thinking that it is us.
+    """
+    
+    if not stem.prereq.is_python_26() and stem.util.system.is_windows():
+      test.runner.skip(self, "(unable to kill subprocesses)")
+      return
+    elif not stem.util.system.is_available("sleep"):
+      test.runner.skip(self, "('sleep' command is unavailable)")
+      return
+    elif test.runner.only_run_once(self, "test_take_ownership_via_pid"): return
+    elif test.runner.require_version(self, stem.version.Requirement.TAKEOWNERSHIP): return
+    
+    # Have os.getpid provide the pid of a process we can safely kill. I hate
+    # needing to a _get_pid() helper but after much head scratching I haven't
+    # been able to mock os.getpid() or posix.getpid().
+    
+    sleep_process = subprocess.Popen(['sleep', '10'])
+    mocking.mock(stem.process._get_pid, mocking.return_value(str(sleep_process.pid)))
+    
+    tor_process = stem.process.launch_tor_with_config(
+      tor_cmd = test.runner.get_runner().get_tor_command(),
+      config = {
+        'SocksPort': '2777',
+        'ControlPort': '2778',
+        'DataDirectory': DATA_DIRECTORY,
+      },
+      completion_percent = 5,
+      take_ownership = True,
+    )
+    
+    # Kill the sleep command. Tor should quit shortly after.
+    
+    _kill_process(sleep_process)
+    
+    # tor polls for the process every fifteen seconds so this may take a
+    # while...
+    
+    for seconds_waited in xrange(30):
+      if tor_process.poll() == 0:
+        return # tor exited
+      
+      time.sleep(1)
+    
+    self.fail("tor didn't quit after the process that owned it terminated")
+  
+  def test_take_ownership_via_controller(self):
+    """
+    Checks that the tor process quits after the controller that owns it
+    connects, then disconnects..
+    """
+    
+    if test.runner.only_run_once(self, "test_take_ownership_via_controller"): return
+    elif test.runner.require_version(self, stem.version.Requirement.TAKEOWNERSHIP): return
+    
+    tor_process = stem.process.launch_tor_with_config(
+      tor_cmd = test.runner.get_runner().get_tor_command(),
+      config = {
+        'SocksPort': '2777',
+        'ControlPort': '2778',
+        'DataDirectory': DATA_DIRECTORY,
+      },
+      completion_percent = 5,
+      take_ownership = True,
+    )
+    
+    # We're the controlling process. Just need to connect then disconnect.
+    
+    controller = stem.control.Controller.from_port(control_port = 2778)
+    controller.authenticate()
+    controller.close()
+    
+    # give tor a few seconds to quit
+    for seconds_waited in xrange(5):
+      if tor_process.poll() == 0:
+        return # tor exited
+      
+      time.sleep(1)
+    
+    self.fail("tor didn't quit after the controller that owned it disconnected")
 



More information about the tor-commits mailing list