commit 7fba50ff561578f746ceb3383b1bb16d709a14e3
Author: Damian Johnson <atagar(a)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")