diff --git a/HISTORY.rst b/HISTORY.rst index 296dc85fe..3c4dfa7a1 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 26a9ce257..b8b3a17bc 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 286f8a6ab..429e76b3e 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 `__'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 `__ C call + when the file was opened (e.g. + `os.O_RDONLY `__, + `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 882cfac88..324ff5cb9 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 @@ def set_scputimes_ntuple(procfs_path): '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 @@ def open_files(self): 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 @@ def open_files(self): 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 diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py index 7bf753b27..a95459915 100644 --- a/psutil/tests/test_linux.py +++ b/psutil/tests/test_linux.py @@ -37,6 +37,7 @@ 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 @@ def open_mock(name, *args, **kwargs): @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 @@ def test_memory_full_info(self): 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 f1cc17623..c556caf04 100644 --- a/psutil/tests/test_process.py +++ b/psutil/tests/test_process.py @@ -876,13 +876,20 @@ def test_open_files(self): 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 @@ def test_open_files(self): @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 @@ def open_files(self, ret, proc): 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