Repository: nipy/nipype

Issue:
copy files to avoid concurrency issues in mris_expand



Discussion:

Resolving patch:
diff --git a/nipype/utils/filemanip.py b/nipype/utils/filemanip.py
--- a/nipype/utils/filemanip.py
+++ b/nipype/utils/filemanip.py
@@ -12,6 +12,7 @@
 
 import sys
 import pickle
+import subprocess
 import gzip
 import hashlib
 from hashlib import md5
@@ -237,6 +238,54 @@ def hash_timestamp(afile):
     return md5hex
 
 
+def _generate_cifs_table():
+    """Construct a reverse-length-ordered list of mount points that
+    fall under a CIFS mount.
+
+    This precomputation allows efficient checking for whether a given path
+    would be on a CIFS filesystem.
+
+    On systems without a ``mount`` command, or with no CIFS mounts, returns an
+    empty list.
+    """
+    exit_code, output = subprocess.getstatusoutput("mount")
+    # Not POSIX
+    if exit_code != 0:
+        return []
+
+    # (path, fstype) tuples, sorted by path length (longest first)
+    mount_info = sorted((line.split()[2:5:2] for line in output.splitlines()),
+                        key=lambda x: len(x[0]),
+                        reverse=True)
+    cifs_paths = [path for path, fstype in mount_info if fstype == 'cifs']
+
+    return [mount for mount in mount_info
+            if any(mount[0].startswith(path) for path in cifs_paths)]
+
+
+_cifs_table = _generate_cifs_table()
+
+
+def on_cifs(fname):
+    """ Checks whether a file path is on a CIFS filesystem mounted in a POSIX
+    host (i.e., has the ``mount`` command).
+
+    On Windows, Docker mounts host directories into containers through CIFS
+    shares, which has support for Minshall+French symlinks, or text files that
+    the CIFS driver exposes to the OS as symlinks.
+    We have found that under concurrent access to the filesystem, this feature
+    can result in failures to create or read recently-created symlinks,
+    leading to inconsistent behavior and ``FileNotFoundError``s.
+
+    This check is written to support disabling symlinks on CIFS shares.
+    """
+    # Only the first match (most recent parent) counts
+    for fspath, fstype in _cifs_table:
+        if fname.startswith(fspath):
+            return fstype == 'cifs'
+    return False
+
+
 def copyfile(originalfile, newfile, copy=False, create_new=False,
              hashmethod=None, use_hardlink=False,
              copy_related_files=True):
@@ -288,6 +337,10 @@ def copyfile(originalfile, newfile, copy=False, create_new=False,
     if hashmethod is None:
         hashmethod = config.get('execution', 'hash_method').lower()
 
+    # Don't try creating symlinks on CIFS
+    if copy is False and on_cifs(newfile):
+        copy = True
+
     # Existing file
     # -------------
     # Options:


Test patch:
diff --git a/nipype/utils/tests/test_filemanip.py b/nipype/utils/tests/test_filemanip.py
--- a/nipype/utils/tests/test_filemanip.py
+++ b/nipype/utils/tests/test_filemanip.py
@@ -15,6 +15,7 @@
 from ...utils.filemanip import (save_json, load_json,
                                 fname_presuffix, fnames_presuffix,
                                 hash_rename, check_forhash,
+                                _cifs_table, on_cifs,
                                 copyfile, copyfiles,
                                 filename_to_list, list_to_filename,
                                 check_depends,
@@ -334,3 +335,28 @@ def test_related_files(file, length, expected_files):
     for ef in expected_files:
         assert ef in related_files
 
+
+def test_cifs_check():
+    assert isinstance(_cifs_table, list)
+    assert isinstance(on_cifs('/'), bool)
+    fake_table = [('/scratch/tmp', 'ext4'), ('/scratch', 'cifs')]
+    cifs_targets = [('/scratch/tmp/x/y', False),
+                    ('/scratch/tmp/x', False),
+                    ('/scratch/x/y', True),
+                    ('/scratch/x', True),
+                    ('/x/y', False),
+                    ('/x', False),
+                    ('/', False)]
+
+    orig_table = _cifs_table[:]
+    _cifs_table[:] = []
+
+    for target, _ in cifs_targets:
+        assert on_cifs(target) is False
+
+    _cifs_table.extend(fake_table)
+    for target, expected in cifs_targets:
+        assert on_cifs(target) is expected
+
+    _cifs_table[:] = []
+    _cifs_table.extend(orig_table)