Skip to content

Commit c15df99

Browse files
authored
Merge pull request #1941 from effigies/disable_symlinks
ENH: Disable symlinks on CIFS filesystems
2 parents 0e7e170 + ae5c6e1 commit c15df99

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

nipype/utils/filemanip.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import sys
1414
import pickle
15+
import subprocess
1516
import gzip
1617
import hashlib
1718
from hashlib import md5
@@ -237,6 +238,54 @@ def hash_timestamp(afile):
237238
return md5hex
238239

239240

241+
def _generate_cifs_table():
242+
"""Construct a reverse-length-ordered list of mount points that
243+
fall under a CIFS mount.
244+
245+
This precomputation allows efficient checking for whether a given path
246+
would be on a CIFS filesystem.
247+
248+
On systems without a ``mount`` command, or with no CIFS mounts, returns an
249+
empty list.
250+
"""
251+
exit_code, output = subprocess.getstatusoutput("mount")
252+
# Not POSIX
253+
if exit_code != 0:
254+
return []
255+
256+
# (path, fstype) tuples, sorted by path length (longest first)
257+
mount_info = sorted((line.split()[2:5:2] for line in output.splitlines()),
258+
key=lambda x: len(x[0]),
259+
reverse=True)
260+
cifs_paths = [path for path, fstype in mount_info if fstype == 'cifs']
261+
262+
return [mount for mount in mount_info
263+
if any(mount[0].startswith(path) for path in cifs_paths)]
264+
265+
266+
_cifs_table = _generate_cifs_table()
267+
268+
269+
def on_cifs(fname):
270+
""" Checks whether a file path is on a CIFS filesystem mounted in a POSIX
271+
host (i.e., has the ``mount`` command).
272+
273+
On Windows, Docker mounts host directories into containers through CIFS
274+
shares, which has support for Minshall+French symlinks, or text files that
275+
the CIFS driver exposes to the OS as symlinks.
276+
We have found that under concurrent access to the filesystem, this feature
277+
can result in failures to create or read recently-created symlinks,
278+
leading to inconsistent behavior and ``FileNotFoundError``s.
279+
280+
This check is written to support disabling symlinks on CIFS shares.
281+
"""
282+
# Only the first match (most recent parent) counts
283+
for fspath, fstype in _cifs_table:
284+
if fname.startswith(fspath):
285+
return fstype == 'cifs'
286+
return False
287+
288+
240289
def copyfile(originalfile, newfile, copy=False, create_new=False,
241290
hashmethod=None, use_hardlink=False,
242291
copy_related_files=True):
@@ -288,6 +337,10 @@ def copyfile(originalfile, newfile, copy=False, create_new=False,
288337
if hashmethod is None:
289338
hashmethod = config.get('execution', 'hash_method').lower()
290339

340+
# Don't try creating symlinks on CIFS
341+
if copy is False and on_cifs(newfile):
342+
copy = True
343+
291344
# Existing file
292345
# -------------
293346
# Options:

nipype/utils/tests/test_filemanip.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ...utils.filemanip import (save_json, load_json,
1616
fname_presuffix, fnames_presuffix,
1717
hash_rename, check_forhash,
18+
_cifs_table, on_cifs,
1819
copyfile, copyfiles,
1920
filename_to_list, list_to_filename,
2021
check_depends,
@@ -334,3 +335,28 @@ def test_related_files(file, length, expected_files):
334335
for ef in expected_files:
335336
assert ef in related_files
336337

338+
339+
def test_cifs_check():
340+
assert isinstance(_cifs_table, list)
341+
assert isinstance(on_cifs('/'), bool)
342+
fake_table = [('/scratch/tmp', 'ext4'), ('/scratch', 'cifs')]
343+
cifs_targets = [('/scratch/tmp/x/y', False),
344+
('/scratch/tmp/x', False),
345+
('/scratch/x/y', True),
346+
('/scratch/x', True),
347+
('/x/y', False),
348+
('/x', False),
349+
('/', False)]
350+
351+
orig_table = _cifs_table[:]
352+
_cifs_table[:] = []
353+
354+
for target, _ in cifs_targets:
355+
assert on_cifs(target) is False
356+
357+
_cifs_table.extend(fake_table)
358+
for target, expected in cifs_targets:
359+
assert on_cifs(target) is expected
360+
361+
_cifs_table[:] = []
362+
_cifs_table.extend(orig_table)

0 commit comments

Comments
 (0)