Repository navigation
TemporaryDirectory.__exit__ sometimes raises bad PermissionError #113009
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Dec 12, 2023 - changed the title
[-]TemporaryDirectory.__exit__ raises PermissionError if other process keeping handle to a file in the tmp directory has just finished its execution[/-][+]`TemporaryDirectory.__exit__` raises PermissionError if other process keeping handle to a file in the tmp directory has just finished its execution[/+]on Dec 12, 2023 - changed the title
[-]`TemporaryDirectory.__exit__` raises PermissionError if other process keeping handle to a file in the tmp directory has just finished its execution[/-][+]`TemporaryDirectory.__exit__` sometimes raises bad PermissionError[/+]on Dec 12, 2023 - addedstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
on Dec 12, 2023 I was also able to reproduce this on Windows 11 Pro (22H2) with 3.11.7 and 3.12.1. It reproduces reliably with the code snippet above.
I also tested this on Linux (Debian) with 3.11, 3.12.0 and 3.12.1 and was unable to repro.
- added3.11only security fixesonly security fixes3.12only security fixesonly security fixes3.13only security fixesonly security fixes
on Dec 13, 2023 I have spent some time debugging another issue in the same piece of code that revealed this bug and probably I have found the cause of this as well. So consider the following block of code:
import multiprocessing import os import tempfile import time import psutil def _open_func(file_path): with open(file_path, "w"): time.sleep(1000) def test(i): with tempfile.TemporaryDirectory(suffix="used_by_another_process") as dir_path: file_path = os.path.join(dir_path, "file_being_used") proc = multiprocessing.Process(target=_open_func, args=(file_path,)) proc.start() while not os.path.exists(file_path): time.sleep(0.1) proc.terminate() proc.join() try: process = psutil.Process(proc.pid) print(f'{process=} {i=}') assert False except psutil.Error: pass if __name__ == "__main__": for i in range(1000): test(i)
Running
psutil.Process, since it takes some time, prevents from falling intoPermissionErrorissue but sometimes the above code prints the child process information and finally fails on theassert False.So the real problem here is probably in fact that invoking
proc.terminate(); proc.join()finishes its execution before the process is stopped.It looks like the regression was introduced by this change: a2074911ba Please note that it is not present in 3.12.0 but in 3.12.1 it is. Previously, the
terminatemethod was not settingself.returncodeso thewaitmethod was indeed waiting. But now, sincereturncodeis always set byterminate,waitdoes nothing. Reverting the change fixes both issues.
CC: @vstinner as the author of the change.Previously, the terminate method was not setting self.returncode so the wait method was indeed waiting. But now, since returncode is always set by terminate, wait does nothing.
Do you mean that in terminate(), when TerminateProcess() raises PermissionError and GetExitCodeProcess() returns an exit code different than STILL_ACTIVE, the process is in fact still running?
Please log the returncode. Example:
def test(): with tempfile.TemporaryDirectory(suffix="used_by_another_process") as dir_path: file_path = os.path.join(dir_path, "file_being_used") proc = multiprocessing.Process(target=_open_func, args=(file_path,)) print(repr(proc)) proc.start() while not os.path.exists(file_path): time.sleep(0.1) proc.terminate() print(f"terminate: {proc._popen.returncode=}") proc.join() print(f"join: {proc._popen.returncode=}")
No, I mean that we now set
returncodein theelseclause:
self.returncode = -signal.SIGTERM Thus
waitskips waiting:
and we may end up with running process for some time afterif self.returncode is None: Process.joinfinishes.The output of the program with additional debug logs:
terminate: proc._popen.returncode=-15 join: proc._popen.returncode=-153 remaining items
So the difference with my recent change is that WaitForSingleObject() is no longer called. Sometimes, the child process is no longer running, and TemporaryDirectory removal is fine. Sometimes, the child process is still running, and TemporaryDirectory removal fails.
I wrote PR #113128 to restore the old behavior: always call WaitForSingleObject(), even if TerminateProcess() succeeded and GetExitCodeProcess() is not STILL_ACTIVE. As an Unix programmer, the Windows API always surprise me. I expected TerminateProcess() to... terminate the process, not to schedule an asynchonous termination.
It reminds me a similar process in asyncio with asynchronous cancellation: https://vstinner.github.io/asyncio-proactor-cancellation-from-hell.html
Thank you for the fix! It now works fine with the change from your PR. Do you think it will be included in 3.12.2?
Thanks for the bug report, analysis and testing my fix. It should now be fixed. Oops, I was surprised by async Windows API, different than Unix way of doing things. Backports will follow.
Reacted by Kamil Frydel
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Bug report
TemporaryDirectory.__exit__usually raises PermissionError if another process keeping a handle to a file in the tmp directory has just finished its execution.Bug description:
Please consider the following block of code:
Despite the child process being terminated and joined, the
__exit__method ofTemporaryDirectorysometimes raisesPermissionError:It does not reproduce in 100% but most executions fail. I can reproduce it only using Python 3.12.1. It does not happen to me on 3.12.0 or 3.11. It seems to be a regression in the last release.
With some small sleep after the
proc.join()it stops reproducing so it looks like a kind of race condition.The Windows version I use is:
Edition: Windows 10 Enterprise
Version: 21H2
OS build: 19044.3693
CPython versions tested on:
3.12
Operating systems tested on:
Windows
Linked PRs