Repository: giampaolo/psutil
Pull request #778 (2016-02-27 23:48:39)

777 linux files pos mode


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 296dc85f..3c4dfa7a 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,11 @@ Bug tracker at https://github.com/giampaolo/psutil/issues
 4.1.0 - XXXX-XX-XX
 ==================
 
+**Enhancements**
+
+- #777: [Linux] Process.open_files() on Linux return 3 new fields: position,
+  mode and flags.
+
 **Bug fixes**
 
 - #776: [Linux] Process.cpu_affinity() may erroneously raise NoSuchProcess.
diff --git a/IDEAS b/IDEAS
index 26a9ce25..b8b3a17b 100644
--- a/IDEAS
+++ b/IDEAS
@@ -141,3 +141,9 @@ FEATURES
 - #550: number of threads per core.
 
 - Have psutil.Process().cpu_affinity([]) be an alias for "all CPUs"?
+
+SIMILAR PROJECTS
+================
+
+- https://github.com/hyperic/sigar (Java)
+- zabbix source code: https://zabbix.org/wiki/Get_Zabbix
diff --git a/docs/index.rst b/docs/index.rst
index 286f8a6a..429e76b3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1202,8 +1202,22 @@ Process class
   .. method:: open_files()
 
      Return regular files opened by process as a list of namedtuples including
-     the absolute file name and the file descriptor number (on Windows this is
-     always ``-1``). Example:
+     the following fields:
+
+     - **path**: the absolute file name.
+     - **fd**: the file descriptor number; on Windows this is always ``-1``.
+     - **position** (Linux): the file (offset) position.
+     - **mode** (Linux): a string indicating how the file was opened, similarly
+       `open <https://docs.python.org/3/library/functions.html#open>`__'s
+       ``mode`` argument. Possible values are ``'r'``, ``'w'``, ``'a'``,
+       ``'r+'`` and ``'a+'``. There's no distinction between files opened in
+       bynary or text mode (``"b"`` or ``"t"``).
+     - **flags** (Linux): the flags which were passed to the underlying
+       `os.open <https://docs.python.org/2/library/os.html#os.open>`__ C call
+       when the file was opened (e.g.
+       `os.O_RDONLY <https://docs.python.org/3/library/os.html#os.O_RDONLY>`__,
+       `os.O_TRUNC <https://docs.python.org/3/library/os.html#os.O_TRUNC>`__,
+       etc).
 
      >>> import psutil
      >>> f = open('file.ext', 'w')
@@ -1228,6 +1242,9 @@ Process class
 
      .. versionchanged:: 3.1.0 no longer hangs on Windows.
 
+     .. versionchanged:: 4.1.0 new *position*, *mode* and *flags* fields on
+        Linux.
+
   .. method:: connections(kind="inet")
 
     Return socket connections opened by process as a list of namedtuples.
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index 882cfac8..324ff5cb 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -179,6 +179,16 @@ def readlink(path):
     return path
 
 
+def file_flags_to_mode(flags):
+    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
+    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]
+    if flags & os.O_APPEND:
+        m = m.replace('w', 'a', 1)
+    m = m.replace('w+', 'r+')
+    # possible values: r, w, a, r+, a+
+    return m
+
+
 def get_sector_size():
     try:
         with open(b"/sys/block/sda/queue/hw_sector_size") as f:
@@ -237,6 +247,8 @@ sdiskio = namedtuple('sdiskio', ['read_count', 'write_count',
                                  'read_merged_count', 'write_merged_count',
                                  'busy_time'])
 
+popenfile = namedtuple('popenfile',
+                       ['path', 'fd', 'position', 'mode', 'flags'])
 pmem = namedtuple('pmem', 'rss vms shared text lib data dirty')
 pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', 'pss', 'swap'))
 
@@ -1296,7 +1308,7 @@ class Process(object):
         for fd in files:
             file = "%s/%s/fd/%s" % (self._procfs_path, self.pid, fd)
             try:
-                file = readlink(file)
+                path = readlink(file)
             except OSError as err:
                 # ENOENT == file which is gone in the meantime
                 if err.errno in (errno.ENOENT, errno.ESRCH):
@@ -1308,12 +1320,19 @@ class Process(object):
                 else:
                     raise
             else:
-                # If file is not an absolute path there's no way
-                # to tell whether it's a regular file or not,
-                # so we skip it. A regular file is always supposed
-                # to be absolutized though.
-                if file.startswith('/') and isfile_strict(file):
-                    ntuple = _common.popenfile(file, int(fd))
+                # If path is not an absolute there's no way to tell
+                # whether it's a regular file or not, so we skip it.
+                # A regular file is always supposed to be have an
+                # absolute path though.
+                if path.startswith('/') and isfile_strict(path):
+                    # Get file position and flags.
+                    file = "%s/%s/fdinfo/%s" % (
+                        self._procfs_path, self.pid, fd)
+                    with open_binary(file) as f:
+                        pos = int(f.readline().split()[1])
+                        flags = int(f.readline().split()[1], 8)
+                    mode = file_flags_to_mode(flags)
+                    ntuple = popenfile(path, int(fd), int(pos), mode, flags)
                     retlist.append(ntuple)
         if hit_enoent:
             # raise NSP if the process disappeared on us


Test patch:
diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py
index 7bf753b2..a9545991 100644
--- a/psutil/tests/test_linux.py
+++ b/psutil/tests/test_linux.py
@@ -37,6 +37,7 @@ from psutil.tests import pyrun
 from psutil.tests import reap_children
 from psutil.tests import retry_before_failing
 from psutil.tests import run_test_module_by_name
+from psutil.tests import safe_remove
 from psutil.tests import sh
 from psutil.tests import skip_on_not_implemented
 from psutil.tests import TESTFN
@@ -719,6 +720,11 @@ class TestMisc(unittest.TestCase):
 @unittest.skipUnless(LINUX, "not a Linux system")
 class TestProcess(unittest.TestCase):
 
+    def setUp(self):
+        safe_remove(TESTFN)
+
+    tearDown = setUp
+
     def test_memory_maps(self):
         src = textwrap.dedent("""
             import time
@@ -765,6 +771,41 @@ class TestProcess(unittest.TestCase):
         self.assertEqual(
             mem.swap, sum([x.swap for x in maps]))
 
+    def test_open_files_mode(self):
+        def get_test_file():
+            p = psutil.Process()
+            giveup_at = time.time() + 2
+            while True:
+                for file in p.open_files():
+                    if file.path == os.path.abspath(TESTFN):
+                        return file
+                    elif time.time() > giveup_at:
+                        break
+            raise RuntimeError("timeout looking for test file")
+
+        #
+        with open(TESTFN, "w"):
+            self.assertEqual(get_test_file().mode, "w")
+        with open(TESTFN, "r"):
+            self.assertEqual(get_test_file().mode, "r")
+        with open(TESTFN, "a"):
+            self.assertEqual(get_test_file().mode, "a")
+        #
+        with open(TESTFN, "r+"):
+            self.assertEqual(get_test_file().mode, "r+")
+        with open(TESTFN, "w+"):
+            self.assertEqual(get_test_file().mode, "r+")
+        with open(TESTFN, "a+"):
+            self.assertEqual(get_test_file().mode, "a+")
+        # note: "x" bit is not supported
+        if PY3:
+            safe_remove(TESTFN)
+            with open(TESTFN, "x"):
+                self.assertEqual(get_test_file().mode, "w")
+            safe_remove(TESTFN)
+            with open(TESTFN, "x+"):
+                self.assertEqual(get_test_file().mode, "r+")
+
     def test_open_files_file_gone(self):
         # simulates a file which gets deleted during open_files()
         # execution
diff --git a/psutil/tests/test_process.py b/psutil/tests/test_process.py
index f1cc1762..c556caf0 100644
--- a/psutil/tests/test_process.py
+++ b/psutil/tests/test_process.py
@@ -876,13 +876,20 @@ class TestProcess(unittest.TestCase):
         p = psutil.Process()
         files = p.open_files()
         self.assertFalse(TESTFN in files)
-        with open(TESTFN, 'w'):
+        with open(TESTFN, 'wb') as f:
+            f.write(b'x' * 1024)
+            f.flush()
             # give the kernel some time to see the new file
-            call_until(p.open_files, "len(ret) != %i" % len(files))
-            filenames = [x.path for x in p.open_files()]
-            self.assertIn(TESTFN, filenames)
-        for file in filenames:
-            assert os.path.isfile(file), file
+            files = call_until(p.open_files, "len(ret) != %i" % len(files))
+            for file in files:
+                if file.path == TESTFN:
+                    if LINUX:
+                        self.assertEqual(file.position, 1024)
+                    break
+            else:
+                self.fail("no file found; files=%s" % repr(files))
+        for file in files:
+            assert os.path.isfile(file.path), file
 
         # another process
         cmdline = "import time; f = open(r'%s', 'r'); time.sleep(60);" % TESTFN
@@ -903,20 +910,20 @@ class TestProcess(unittest.TestCase):
     @unittest.skipIf(BSD, "broken on BSD, see #595")
     @unittest.skipIf(APPVEYOR,
                      "can't find any process file on Appveyor")
-    def test_open_files2(self):
+    def test_open_files_2(self):
         # test fd and path fields
         with open(TESTFN, 'w') as fileobj:
             p = psutil.Process()
-            for path, fd in p.open_files():
-                if path == fileobj.name or fd == fileobj.fileno():
+            for file in p.open_files():
+                if file.path == fileobj.name or file.fd == fileobj.fileno():
                     break
             else:
                 self.fail("no file found; files=%s" % repr(p.open_files()))
-            self.assertEqual(path, fileobj.name)
+            self.assertEqual(file.path, fileobj.name)
             if WINDOWS:
-                self.assertEqual(fd, -1)
+                self.assertEqual(file.fd, -1)
             else:
-                self.assertEqual(fd, fileobj.fileno())
+                self.assertEqual(file.fd, fileobj.fileno())
             # test positions
             ntuple = p.open_files()[0]
             self.assertEqual(ntuple[0], ntuple.path)
@@ -1694,6 +1701,11 @@ class TestFetchAllProcesses(unittest.TestCase):
                 assert f.fd == -1, f
             else:
                 self.assertIsInstance(f.fd, int)
+            if LINUX:
+                self.assertIsInstance(f.position, int)
+                self.assertGreaterEqual(f.position, 0)
+                self.assertIn(f.mode, ('r', 'w', 'a', 'r+', 'a+'))
+                self.assertGreater(f.flags, 0)
             if BSD and not f.path:
                 # XXX see: https://github.com/giampaolo/psutil/issues/595
                 continue