From 4d915a80512aa503b53a986ef140aaa406fb9b66 Mon Sep 17 00:00:00 2001 From: Lea Waller Date: Wed, 13 Oct 2021 20:20:05 +0200 Subject: [PATCH] [FIX] Also allow `errno.EBUSY` during `emptydirs` on NFS (#3357) * Also allow `errno.EBUSY` during `emptydirs` on NFS - Can occur if a file is still open somewhere, so NFS will rename it to a hidden file in the same directory - When `shutil` tries to delete that hidden file, we get an `OSError` with `errno.EBUSY` * Ignore `.nfs` placeholder files when catching the error - I forgot that `os.listdir` also lists hidden files in the previous commit * Add unit test for `emptydirs` on NFS - With mock for NFS silly-rename (yes, it's really called that) behavior * Run `black` * Update nipype/utils/tests/test_filemanip.py Handle mock test case when no `dir_fd` is passed --- nipype/utils/filemanip.py | 11 ++++++++--- nipype/utils/tests/test_filemanip.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/nipype/utils/filemanip.py b/nipype/utils/filemanip.py index f02efa163f..03786ec935 100644 --- a/nipype/utils/filemanip.py +++ b/nipype/utils/filemanip.py @@ -780,8 +780,13 @@ def emptydirs(path, noexist_ok=False): try: shutil.rmtree(path) except OSError as ex: - elcont = os.listdir(path) - if ex.errno == errno.ENOTEMPTY and not elcont: + elcont = [ + Path(root) / file + for root, _, files in os.walk(path) + for file in files + if not file.startswith(".nfs") + ] + if ex.errno in [errno.ENOTEMPTY, errno.EBUSY] and not elcont: fmlogger.warning( "An exception was raised trying to remove old %s, but the path" " seems empty. Is it an NFS mount?. Passing the exception.", @@ -793,7 +798,7 @@ def emptydirs(path, noexist_ok=False): else: raise ex - os.makedirs(path) + os.makedirs(path, exist_ok=True) def silentrm(filename): diff --git a/nipype/utils/tests/test_filemanip.py b/nipype/utils/tests/test_filemanip.py index 299029a8d2..f02ad4164e 100644 --- a/nipype/utils/tests/test_filemanip.py +++ b/nipype/utils/tests/test_filemanip.py @@ -31,6 +31,7 @@ savepkl, path_resolve, write_rst_list, + emptydirs, ) @@ -670,3 +671,26 @@ def test_write_rst_list(tmp_path, items, expected): else: with pytest.raises(expected): write_rst_list(items) + + +def nfs_unlink(pathlike, *, dir_fd=None): + if dir_fd is None: + path = Path(pathlike) + deleted = path.with_name(".nfs00000000") + path.rename(deleted) + else: + os.rename(pathlike, ".nfs1111111111", src_dir_fd=dir_fd, dst_dir_fd=dir_fd) + + +def test_emptydirs_dangling_nfs(tmp_path): + busyfile = tmp_path / "base" / "subdir" / "busyfile" + busyfile.parent.mkdir(parents=True) + busyfile.touch() + + with mock.patch("os.unlink") as mocked: + mocked.side_effect = nfs_unlink + emptydirs(tmp_path / "base") + + assert Path.exists(tmp_path / "base") + assert not busyfile.exists() + assert busyfile.parent.exists() # Couldn't remove