giampaolo-psutil-2515
Results
The prompt the agent sees
On Linux, calling `psutil.Process().cwd()` can raise `FileNotFoundError` when the process’s current-working-directory link disappears or cannot be read due to a race condition. This should be handled gracefully: the call should return an empty string instead of propagating the exception.
Hidden tests · 1 fail-to-pass, 80 pass-to-passrun after the agent submits, in a clean verifier
Test patch · 20 lines
diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py
index 7d25fc841c..836311e220 100755
--- a/psutil/tests/test_linux.py
+++ b/psutil/tests/test_linux.py
@@ -2064,6 +2064,15 @@ def test_exe_mocked(self):
assert m.called
assert ret == ""
+ def test_cwd_mocked(self):
+ # https://github.com/giampaolo/psutil/issues/2514
+ with mock.patch(
+ 'psutil._pslinux.readlink', side_effect=FileNotFoundError
+ ) as m:
+ ret = psutil.Process().cwd()
+ assert m.called
+ assert ret == ""
+
def test_issue_1014(self):
# Emulates a case where smaps file does not exist. In this case
# wrap_exception decorator should not raise NoSuchProcess.
Reference fix · 2 files, +33 −11the upstream merge, used only for grading calibration
The agent could not see this: the repository holds one commit and the sandbox has no network. Leak audit.
HISTORY.rst, psutil/_pslinux.py
diff --git a/HISTORY.rst b/HISTORY.rst
index f84aab61cc..c05e4114f8 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,5 +1,15 @@
*Bug tracker at https://github.com/giampaolo/psutil/issues*
+7.1.0
+=====
+
+XXXX-XX-XX
+
+**Bug fixes**
+
+- 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due
+ to a race condition.
+
7.0.0
=====
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index 8cc64e9a10..73e76297dd 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -83,6 +83,7 @@
PAGESIZE = cext_posix.getpagesize()
BOOT_TIME = None # set later
LITTLE_ENDIAN = sys.byteorder == 'little'
+UNSET = object()
# "man iostat" states that sectors are equivalent with blocks and have
# a size of 512 bytes. Despite this value can be queried at runtime
@@ -1698,6 +1699,22 @@ def _raise_if_not_alive(self):
# incorrect or incomplete result.
os.stat(f"{self._procfs_path}/{self.pid}")
+ def _readlink(self, path, fallback=UNSET):
+ # * https://github.com/giampaolo/psutil/issues/503
+ # os.readlink('/proc/pid/exe') may raise ESRCH (ProcessLookupError)
+ # instead of ENOENT (FileNotFoundError) when it races.
+ # * ENOENT may occur also if the path actually exists if PID is
+ # a low PID (~0-20 range).
+ # * https://github.com/giampaolo/psutil/issues/2514
+ try:
+ return readlink(path)
+ except (FileNotFoundError, ProcessLookupError):
+ if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
+ self._raise_if_zombie()
+ if fallback is not UNSET:
+ return fallback
+ raise
+
@wrap_exceptions
@memoize_when_activated
def _parse_stat_file(self):
@@ -1770,16 +1787,9 @@ def name(self):
@wrap_exceptions
def exe(self):
- try:
- return readlink(f"{self._procfs_path}/{self.pid}/exe")
- except (FileNotFoundError, ProcessLookupError):
- self._raise_if_zombie()
- # no such file error; might be raised also if the
- # path actually exists for system processes with
- # low pids (about 0-20)
- if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
- return ""
- raise
+ return self._readlink(
+ f"{self._procfs_path}/{self.pid}/exe", fallback=""
+ )
@wrap_exceptions
def cmdline(self):
@@ -2054,7 +2064,9 @@ def get_blocks(lines, current_block):
@wrap_exceptions
def cwd(self):
- return readlink(f"{self._procfs_path}/{self.pid}/cwd")
+ return self._readlink(
+ f"{self._procfs_path}/{self.pid}/cwd", fallback=""
+ )
@wrap_exceptions
def num_ctx_switches(
GLM-5.3 Flash · attempt 2 · solved · 7 steps · $0.001
Submitted diff · 19 lines
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index 8cc64e9..91b4cee 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -2054,7 +2054,13 @@ class Process:
@wrap_exceptions
def cwd(self):
- return readlink(f"{self._procfs_path}/{self.pid}/cwd")
+ try:
+ return readlink(f"{self._procfs_path}/{self.pid}/cwd")
+ except FileNotFoundError:
+ # No such file or directory error means that either the
+ # process went away or the cwd was deleted (race condition).
+ # In both cases return an empty string instead of crashing.
+ return ""
@wrap_exceptions
def num_ctx_switches(step 1
$ grep -rn "cwd" /repo/psutil/_pslinux.py | head -30; ls /repo
output · 18 lines
2056: def cwd(self):
2057: return readlink(f"{self._procfs_path}/{self.pid}/cwd")
CONTRIBUTING.md
CREDITS
HISTORY.rst
INSTALL.rst
LICENSE
MANIFEST.in
Makefile
README.rst
SECURITY.md
docs6 more lines
make.bat psutil psutil.egg-info pyproject.toml scripts setup.py
step 2
$ sed -n 2040,2070p /repo/psutil/_pslinux.py; grep -n "def readlink" /repo/psutil/_pslinux.py
output · 32 lines
path,
data.get(b'Rss:', 0),
data.get(b'Size:', 0),
data.get(b'Pss:', 0),
data.get(b'Shared_Clean:', 0),
data.get(b'Shared_Dirty:', 0),
data.get(b'Private_Clean:', 0),
data.get(b'Private_Dirty:', 0),
data.get(b'Referenced:', 0),
data.get(b'Anonymous:', 0),
data.get(b'Swap:', 0),
)20 more lines
ls.append(item)
return ls
@wrap_exceptions
def cwd(self):
return readlink(f"{self._procfs_path}/{self.pid}/cwd")
@wrap_exceptions
def num_ctx_switches(
self, _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)')
):
data = self._read_status_file()
ctxsw = _ctxsw_re.findall(data)
if not ctxsw:
msg = (
"'voluntary_ctxt_switches' and"
" 'nonvoluntary_ctxt_switches'lines were not found in"
f" {self._procfs_path}/{self.pid}/status; the kernel is"
" probably older than 2.6.23"
199:def readlink(path):step 3
$ sed -n 190,215p /repo/psutil/_pslinux.py
output · 26 lines
'iowait'])
# fmt: on
# =====================================================================
# --- utils
# =====================================================================
def readlink(path):
"""Wrapper around os.readlink()."""
assert isinstance(path, str), path14 more lines
path = os.readlink(path)
# readlink() might return paths containing null bytes ('\x00')
# resulting in "TypeError: must be encoded string without NULL
# bytes, not str" errors when the string is passed to other
# fs-related functions (os.*, open(), ...).
# Apparently everything after '\x00' is garbage (we can have
# ' (deleted)', 'new' and possibly others), see:
# https://github.com/giampaolo/psutil/issues/717
path = path.split('\x00')[0]
# Certain paths have ' (deleted)' appended. Usually this is
# bogus as the file actually exists. Even if it doesn't we
# don't care.
if path.endswith(' (deleted)') and not path_exists_strict(path):
path = path[:-10]step 4
$ cd /repo && python - <<'PY'
output · 1 lines
'/repo'
step 5
$ cd /repo && python -m pytest psutil/tests/test_linux.py -x -q 2>&1 | tail -3
output · 3 lines
FAILED psutil/tests/test_linux.py::TestSystemVirtualMemoryAgainstFree::test_available !!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!! 1 failed in 0.68s
step 6
$ cd /repo && python -m pytest psutil/tests/test_linux.py -q -k "cwd or Process" 2>&1 | tail -3; git stash -q; python -m pytest psutil/tests/test_linux.py::TestSystemVirtualMemoryAgainstFree -q 2>&1 | tail -2; git stash pop -q
output · 4 lines
............................ [100%] 28 passed, 83 deselected in 0.33s FAILED psutil/tests/test_linux.py::TestSystemVirtualMemoryAgainstFree::test_used 5 failed in 0.81s
step 7
The memory-vs-free test failures are pre-existing (they fail on the pristine tree too). My change passes all cwd-related tests.
$ done: {"notes": "psutil/_pslinux.py: Process.cwd() now catches FileNotFoundError from readlink (race where cwd link disappears) and returns \"\" instead of raising."}output · 1 lines
submitted.