|
12 | 12 |
|
13 | 13 | import sys
|
14 | 14 | import pickle
|
| 15 | +import subprocess |
15 | 16 | import gzip
|
16 | 17 | import hashlib
|
17 | 18 | from hashlib import md5
|
@@ -237,6 +238,54 @@ def hash_timestamp(afile):
|
237 | 238 | return md5hex
|
238 | 239 |
|
239 | 240 |
|
| 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 | + |
240 | 289 | def copyfile(originalfile, newfile, copy=False, create_new=False,
|
241 | 290 | hashmethod=None, use_hardlink=False,
|
242 | 291 | copy_related_files=True):
|
@@ -288,6 +337,10 @@ def copyfile(originalfile, newfile, copy=False, create_new=False,
|
288 | 337 | if hashmethod is None:
|
289 | 338 | hashmethod = config.get('execution', 'hash_method').lower()
|
290 | 339 |
|
| 340 | + # Don't try creating symlinks on CIFS |
| 341 | + if copy is False and on_cifs(newfile): |
| 342 | + copy = True |
| 343 | + |
291 | 344 | # Existing file
|
292 | 345 | # -------------
|
293 | 346 | # Options:
|
|
0 commit comments