commit d7d14a82e7d28ff863c71b4b027c9922e17a6661
Author: Damian Johnson <atagar(a)torproject.org>
Date: Wed May 29 20:44:23 2013 -0700
Methods to query user tor is running as
Couple additions to make it easier to determine the user tor is running as:
* stem.util.system.get_user(pid) => determines the user a process is running as
* Controller.get_user() => provides the user that tor is running as
---
docs/change_log.rst | 2 ++
stem/control.py | 36 +++++++++++++++++++++++++++--
stem/util/system.py | 36 +++++++++++++++++++++++++++++
test/integ/util/system.py | 48 +++++++++++++++++++++++++++++++++++++++
test/unit/control/controller.py | 29 +++++++++++++++++++++++
5 files changed, 149 insertions(+), 2 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst
index 6e969fa..a2c92ea 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -44,6 +44,7 @@ The following are only available within stem's `git repository
* :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`)
* The :class:`~stem.control.Controller` use of cached content wasn't thread safe (:trac:`8607`)
+ * Added :func:`~stem.control.Controller.get_user` method to the:class:`~stem.control.Controller`
* Added :func:`~stem.control.Controller.get_pid` method to the:class:`~stem.control.Controller`
* **Descriptors**
@@ -55,6 +56,7 @@ The following are only available within stem's `git repository
* :func:`~stem.util.system.set_process_name` inserted spaces between characters (:trac:`8631`)
* :func:`~stem.util.system.get_pid_by_name` can now pull for all processes with a given name
* :func:`~stem.util.system.call` ignored the subprocess' exit status
+ * Added :func:`stem.util.system.get_user`
* Added :func:`stem.util.system.get_start_time`
* Added :func:`stem.util.system.get_bsd_jail_path`
diff --git a/stem/control.py b/stem/control.py
index d92acc9..53d49b9 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_user - provides the user tor is running as
|- get_pid - provides the pid of our tor process
|
|- get_microdescriptor - querying the microdescriptor for a relay
@@ -991,6 +992,39 @@ class Controller(BaseController):
else:
return default
+ def get_user(self, default = UNDEFINED):
+ """
+ Provides the user tor is running as. This often 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: str with the username tor is running as
+ """
+
+ user = self._get_cache("user")
+
+ if not user:
+ user = self.get_info("process/user", None)
+
+ if not user and self.get_socket().is_localhost():
+ pid = self.get_pid(None)
+
+ if pid:
+ user = stem.util.system.get_user(pid)
+
+ if user:
+ self._set_cache({"user": user})
+ return user
+ elif default == UNDEFINED:
+ if self.get_socket().is_localhost():
+ raise ValueError("Unable to resolve tor's user")
+ else:
+ raise ValueError("Tor isn't running locally")
+ else:
+ return default
+
def get_pid(self, default = UNDEFINED):
"""
Provides the process id of tor. This often only works if tor is running
@@ -1008,8 +1042,6 @@ class Controller(BaseController):
pid = self._get_cache("pid")
if not pid:
- # this is the only pid resolution method that works remotely
-
getinfo_pid = self.get_info("process/pid", None)
if getinfo_pid and getinfo_pid.isdigit():
diff --git a/stem/util/system.py b/stem/util/system.py
index 3e72d83..5887fb7 100644
--- a/stem/util/system.py
+++ b/stem/util/system.py
@@ -20,6 +20,7 @@ best-effort, providing **None** if the lookup fails.
get_pid_by_port - gets the pid for a process listening to a given port
get_pid_by_open_file - gets the pid for the process with an open file
get_cwd - provides the current working directory for a given process
+ get_user - provides the user a process is running under
get_start_time - provides the unix timestamp when the process started
get_bsd_jail_id - provides the BSD jail id a given process is running within
get_bsd_jail_path - provides the path of the given BSD jail
@@ -34,6 +35,7 @@ import ctypes
import ctypes.util
import os
import platform
+import pwd
import subprocess
import time
@@ -598,6 +600,37 @@ def get_cwd(pid):
return None # all queries failed
+def get_user(pid):
+ """
+ Provides the user a process is running under.
+
+ :param int pid: process id of the process to be queried
+
+ :returns: **str** with the username a process is running under, **None** if
+ it can't be determined
+ """
+
+ if not isinstance(pid, int) or pid < 0:
+ return None
+
+ if stem.util.proc.is_available():
+ try:
+ uid = stem.util.proc.get_uid(pid)
+
+ if uid and uid.isdigit():
+ return pwd.getpwuid(int(uid)).pw_name
+ except:
+ pass
+
+ if is_available("ps"):
+ results = call("ps -o user %s" % pid, [])
+
+ if len(results) >= 2:
+ return results[1].strip()
+
+ return None
+
+
def get_start_time(pid):
"""
Provides the unix timestamp when the given process started.
@@ -608,6 +641,9 @@ def get_start_time(pid):
if it can't be determined
"""
+ if not isinstance(pid, int) or pid < 0:
+ return None
+
if stem.util.proc.is_available():
try:
return float(stem.util.proc.get_stats(pid, stem.util.proc.Stat.START_TIME)[0])
diff --git a/test/integ/util/system.py b/test/integ/util/system.py
index 0bf77ac..4bfda1e 100644
--- a/test/integ/util/system.py
+++ b/test/integ/util/system.py
@@ -383,6 +383,54 @@ class TestSystem(unittest.TestCase):
runner_pid, tor_cwd = runner.get_pid(), runner.get_tor_cwd()
self.assertEquals(tor_cwd, stem.util.system.get_cwd(runner_pid))
+ def test_get_user_none(self):
+ """
+ Tests the get_user function when the process doesn't exist.
+ """
+
+ self.assertEqual(None, stem.util.system.get_user(None))
+ self.assertEqual(None, stem.util.system.get_user(-5))
+ self.assertEqual(None, stem.util.system.get_start_time(98765))
+
+ def test_get_user_proc(self):
+ """
+ Tests the get_user function with a proc response.
+ """
+
+ if not stem.util.proc.is_available():
+ test.runner.skip(self, "(proc unavailable)")
+ return
+
+ mocking.mock(stem.util.system.call, filter_system_call(['ps ']))
+
+ # we started our tor process so it should be running with the same user
+
+ pid = test.runner.get_runner().get_pid()
+ self.assertTrue(getpass.getuser(), stem.util.system.get_user(pid))
+
+ def test_get_user_ps(self):
+ """
+ Tests the get_user function with a ps response.
+ """
+
+ if not stem.util.system.is_available("ps"):
+ test.runner.skip(self, "(ps unavailable)")
+ return
+
+ mocking.mock(stem.util.proc.is_available, mocking.return_false())
+
+ pid = test.runner.get_runner().get_pid()
+ self.assertTrue(getpass.getuser(), stem.util.system.get_user(pid))
+
+ def test_get_start_time_none(self):
+ """
+ Tests the get_start_time function when the process doesn't exist.
+ """
+
+ self.assertEqual(None, stem.util.system.get_start_time(None))
+ self.assertEqual(None, stem.util.system.get_start_time(-5))
+ self.assertEqual(None, stem.util.system.get_start_time(98765))
+
def test_get_start_time_proc(self):
"""
Tests the get_start_time function with a proc response.
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
index 449bc6d..a0deaf1 100644
--- a/test/unit/control/controller.py
+++ b/test/unit/control/controller.py
@@ -248,6 +248,35 @@ class TestControl(unittest.TestCase):
self.assertRaises(ProtocolError, self.controller.get_protocolinfo)
+ def test_get_user_remote(self):
+ """
+ Exercise the get_user() method for a non-local socket.
+ """
+
+ mocking.mock_method(ControlSocket, "is_localhost", mocking.return_false())
+
+ self.assertRaises(ValueError, self.controller.get_user)
+ self.assertEqual(123, self.controller.get_user(123))
+
+ def test_get_user_by_getinfo(self):
+ """
+ Exercise the get_user() resolution via its getinfo option.
+ """
+
+ mocking.mock_method(ControlSocket, "is_localhost", mocking.return_true())
+ mocking.mock_method(Controller, "get_info", mocking.return_value('atagar'))
+ self.assertEqual('atagar', self.controller.get_user())
+
+ def test_get_user_by_system(self):
+ """
+ Exercise the get_user() resolution via the system module.
+ """
+
+ mocking.mock_method(ControlSocket, "is_localhost", mocking.return_true())
+ mocking.mock(stem.util.system.get_pid_by_name, mocking.return_value(432))
+ mocking.mock(stem.util.system.get_user, mocking.return_value('atagar'))
+ self.assertEqual('atagar', self.controller.get_user())
+
def test_get_pid_remote(self):
"""
Exercise the get_pid() method for a non-local socket.