Issue36422
This issue tracker has been migrated to GitHub,
and is currently read-only.
For more information,
see the GitHub FAQs in the Python's Developer Guide.
Created on 2019-03-25 11:16 by riccardomurri, last changed 2022-04-11 14:59 by admin. This issue is now closed.
Files | ||||
---|---|---|---|---|
File name | Uploaded | Description | Edit | |
test-mounted-image.py | Jeffrey.Kintscher, 2019-06-20 08:04 | onerror test program for mount points (macOS) |
Pull Requests | |||
---|---|---|---|
URL | Status | Linked | Edit |
PR 14292 | open | Jeffrey.Kintscher, 2019-06-21 18:28 |
Messages (11) | |||
---|---|---|---|
msg338795 - (view) | Author: Riccardo Murri (riccardomurri) | Date: 2019-03-25 11:16 | |
The behavior of `tempfile.TemporaryDirectory()` is to delete the temporary directory when done; this behavior cannot be turned off (there's no `delete=False`like `NamedTemporaryFile` has instead). However, in case a filesystem has been mounted on the temporary directory, this can lead to the entire filesystem being removed. While I agree that it would be responsibility of the programmer to ensure that anything that has been mounted on the temp dir is unmounted, the current behavior makes it quite easy to shoot oneself in the foot. Consider the following code:: @contextmanager def mount_sshfs(localdir, remote): subprocess.run(f"sshfs {remote} {localdir}") yield subprocess.run(f"fusermount -u {localdir}", check=True) with TemporaryDirectory() as tmpdir: with mount_sshfs(tmpdir, remote): # ... do stuff ... Now, even if the `fusermount` call fails, cleanup of `TemporaryDirectory()` will be performed and the remote filesystem will be erased! Is there a way this pattern can be prevented or at least mitigated? Two options that come to mind: * add a `delete=True/False` option to `TemporaryDirectory` like `NamedTemporaryFile` already has * add a `delete_on_error` option to avoid performing cleanup during error exit from a `with:` block I have seen this happen with Py 3.6 but it's likely there in the entire 3.x series since `TemporaryDirectory` was added to stdlib. Thanks, Riccardo |
|||
msg338804 - (view) | Author: Josh Rosenberg (josh.r) * | Date: 2019-03-25 15:43 | |
Allowing delete_on_error=False is kind of defeating the purpose here; the whole point of the with statement is guaranteed, consistent cleanup in both normal and error cases. If you knew enough to know you needed to pass delete_on_error, you'd also know enough to know you should be handling errors properly in the first place, e.g. by changing your mount_sshfs manager to: @contextmanager def mount_sshfs(localdir, remote): subprocess.run(f"sshfs {remote} {localdir}") try: yield finally: subprocess.run(f"fusermount -u {localdir}", check=True) so it actually performed the guaranteed cleanup you expected from it. I don't see anything wrong with adding a delete=True argument to TemporaryDirectory, though I'm not seeing it as being as useful as it is with TemporaryFile (since the "rewrite file to tempfile, atomic replace old file with new file" pattern for updating a file safely doesn't transfer directly to directories, where atomic renames aren't an option). It just seems like your fundamental problem is code that doesn't properly handle failure, and I don't think the solution is to make TemporaryDirectory handle failure badly as well. |
|||
msg338806 - (view) | Author: Riccardo Murri (riccardomurri) | Date: 2019-03-25 15:53 | |
> you should be handling errors properly in the first place, > e.g. by changing your mount_sshfs manager to: > > @contextmanager > def mount_sshfs(localdir, remote): > subprocess.run(f"sshfs {remote} {localdir}") > try: > yield > finally: > subprocess.run(f"fusermount -u {localdir}", check=True) > > so it actually performed the guaranteed cleanup you expected from it. This would fix the case where errors occur in the "yield" part of the `mount_sshfs` context manager, but would not protect from errors *in the `fusermount -u` call itself*: if `fusermount -u` fails and throws an exception, the entire mounted filesystem will be erased. I would contend that, in general, `TemporaryDirectory.cleanup()` should stop at filesystem boundaries and not descend filesystems mounted in the temporary directory tree (whether the mount has been done via a context manager as in the example above or by any other means is irrelevant). |
|||
msg344775 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-05 22:56 | |
Only deleting from the local filesystem seems reasonable to me. In the context of a temporary directory tree, I don't see a semantic difference between "unmounting a mount point" and "removing a subdirectory entry" -- i.e. they both remove the offending subdirectory tree from the temporary directory tree. Internally, tempfile.TemporaryDirectory() calls shutil.rmtree() with a custom error handler. The handler tries to reset the permissions on the offending entry and calls os.unlink(). If this throws an error, it recursively calls shutil.rmtree() on the offending entry (ignoring the temp directory entry itself). This seems like where a mounted directory tree would get deleted. shutil.rmtree() follows the "rm -rf" semantics and refuses to delete a mount point. It seems reasonable to me to add special handling for mount points to the error handler in tempfile.TemporaryDirectory(). The high-level logic would be something like: if os.path.ismount(path): try: unmount_func(path) _shutil.rmtree(path) except FileNotFoundError: pass The tricky part is implementing unmount_func() in a portable manner since there is no such functionality currently implemented in the Python library. Also, what would be the appropriate behavior when unmount_func() fails (e.g. for a permission error)? Right now, any exceptions other than IsADirectoryError, IsADirectoryError, and FileNotFoundError are passed on to the caller. |
|||
msg344776 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-05 22:59 | |
(correcting typos) What would be the appropriate behavior when unmount_func() fails (e.g. for a permission error)? Right now, any exceptions other than IsADirectoryError, PermissionError, and FileNotFoundError are passed on to the caller. |
|||
msg344785 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-06 03:26 | |
Since tempfile.TemporaryDirectory() already returns some exceptions to the caller when it can't figure out how to delete a directory item, I don't see a problem with throwing PermissionError when encountering a mount point. This would preserve the 'rm -rf' semantics of the underlying shutil.rmtree() calls. |
|||
msg344851 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-06 18:37 | |
Since having tempfile.TemporaryDirectory() automatically unmount mount points is difficult to implement in a portable way (Windows, BSD/macOS, and Linux are all different), a shorter-term solution could be to add an 'onerror' callback function as a class initialization parameter. This would allow the caller to inspect and try to handle any directory entries that the cleanup() function can't handle (like unmounting a mount point). |
|||
msg345022 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-08 05:32 | |
Another data point: On macOS 10.14.4, unlink() returns EBUSY when it tries to delete the mount point. tempfile.TemporaryDirectory() doesn't handle EBUSY and ends up skipping over the mount point and propagates the OSError exception up to the caller. It leaves the mounted directory tree untouched. |
|||
msg345173 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-11 01:53 | |
tempfile.TemporaryDirectory() relies upon shutil.rmtree() to do the actual cleanup. Up through 3.7, it simply calls shutil.rmtree(). 3.8 adds some more logic using the onerror callback parameter of shutil.rmtree() to try changing the permissions on otherwise undeletable directory entries. In order to make tempfile.TemporaryDirectory() handle mount points, shutil.rmtree() requires some enhancements so that mount points can be detected before their contents are deleted. I am working on some generic enhancements to tempfile.TemporaryDirectory() and shutil.rmtree() that add (hopefully) useful functionality that covers mount points as well as other corner cases: 1. Add an "onitem" callback paramter to shutil.rmtree() that, if provided, gets called for each directory entry as it is encountered. This allows the caller to perform any required special handling of individual directory entries (e.g. unmounting a mount point, closing a file, shutting down a named pipe, etc.). 2. Add an "onerror" callback parameter to the tempfile.TemporaryDirectory member functions so that the caller can perform special handling for directory items that it can't automatically delete. The caller created the undeletable directory entries, so it is reasonable to believe the caller may know how to unmake what they made. 3. Add an "onitem" callback parameter to the tempfile.TemporaryDirectory member functions that they pass on to shutil.rmtree(). I debated implementing "ondelete" instead of "onitem". "ondelete" would be called just prior to deleting an item, while "onitem" is called when the item is encountered. The difference in semantics is that a subdirectory entry triggers a call to "onitem" immediately, while "ondelete" would get called after the subdirectory tree had been traversed and all of its items deleted. "onitem" seems more useful because it allows the caller to implement special handling per item. I will have a pull request ready soon. In the mean time, I am open to any other suggestions for handling non-portable corner cases. |
|||
msg346107 - (view) | Author: Jeffrey Kintscher (Jeffrey.Kintscher) * | Date: 2019-06-20 08:04 | |
I implemented an onerror callback for tempfile.TemporaryDirectory() and confirmed that it cannot be used to unmount a mount point. Attempting to unmount the mount point from within the callback results in a resource busy error. It may be related to shutil.rmtree() holding an open file descriptor to the parent directory of the mount point. I uploaded the program I used for testing on macOS (test-mounted-image.py). The subprocess.run() calls can be modified to run it on other platforms. |
|||
msg346878 - (view) | Author: Giampaolo Rodola' (giampaolo.rodola) * | Date: 2019-06-29 11:57 | |
> in case a filesystem has been mounted on the temporary directory, this can lead to the entire filesystem being removed -1 That is expected behavior and the use case looks pretty unusual. Such a new parameter wouldn't even be supported by other "batteries" since there's no portable/standard way to either mount or unmount a directory (in fact you had to use a subprocess in your unit-tests). A `delete=bool` parameter would be a more reasonable proposal in principle but: 1) if you want to keep the directory around then you can just use tempfile.mkdtemp() (see "there should preferably be only one way to do it") 2) it would conflict with the context manager usage which is expected to delete the dir on ctx manager exit In summary, I think this would over-complicate the API for no good reason. I'm closing this out as rejected. |
History | |||
---|---|---|---|
Date | User | Action | Args |
2022-04-11 14:59:12 | admin | set | github: 80603 |
2019-06-29 11:57:21 | giampaolo.rodola | set | status: open -> closed versions: + Python 3.9, - Python 3.6 messages: + msg346878 resolution: rejected stage: patch review -> resolved |
2019-06-21 18:28:24 | Jeffrey.Kintscher | set | keywords:
+ patch stage: patch review pull_requests: + pull_request14117 |
2019-06-20 08:04:55 | Jeffrey.Kintscher | set | files:
+ test-mounted-image.py messages: + msg346107 |
2019-06-13 00:02:53 | Jeffrey.Kintscher | set | nosy:
+ giampaolo.rodola, tarek |
2019-06-11 01:53:32 | Jeffrey.Kintscher | set | messages: + msg345173 |
2019-06-08 05:32:38 | Jeffrey.Kintscher | set | messages: + msg345022 |
2019-06-06 18:37:02 | Jeffrey.Kintscher | set | messages: + msg344851 |
2019-06-06 03:26:25 | Jeffrey.Kintscher | set | messages: + msg344785 |
2019-06-05 22:59:10 | Jeffrey.Kintscher | set | messages: + msg344776 |
2019-06-05 22:56:16 | Jeffrey.Kintscher | set | messages: + msg344775 |
2019-05-22 09:37:47 | Jeffrey.Kintscher | set | nosy:
+ Jeffrey.Kintscher |
2019-03-25 15:53:23 | riccardomurri | set | messages: + msg338806 |
2019-03-25 15:43:06 | josh.r | set | nosy:
+ josh.r messages: + msg338804 |
2019-03-25 11:16:45 | riccardomurri | create |