Repository navigation
test.support.os_helper.rmdir(): PermissionError: [WinError 32] The process cannot access the file because it is being used by another process #98219
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Oct 12, 2022 The current wait works around an asynchronous unlink of one or more child files or directories. In this case, deleting a file or directory succeeds synchronously, but it might remain linked in the parent directory until all handles to it have been closed1.
A permission error would be because a file or directory is opened without sharing delete access, or because some file or directory in the tree is open. Ideally a worker process and its child processes should already be terminated by the time we try to delete the temporary directory. However,
libregrtest.runtest_mp.TestWorkerProcessis pretty basic on Windows. It has no support for console process groups or kernel job objects. It only supports terminating the immediate worker process viaTerminateProcess().Ideally, each worker should be created in a new process group and added to a job object. Begin by calling
CreateProcessW(executable, commandLine, ...)with the creation flagsCREATE_NEW_PROCESS_GROUPandCREATE_SUSPENDED. Create a job viaCreateJobObjectW(), and configure it to allow breakaway (but not silent breakaway) and to kill on close viaQueryInformationJobObject(hJob, ...)andSetInformationJobObject(hJob, ...). Add the process to the job viaAssignProcessToJobObject(hJob, hProcess). Finally, start the process viaResumeThread(hThread). To stop the worker, if the process ID is in our console session, as determined viaGetConsoleProcessList(), send its process group a break event viaGenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, processId), and wait a little while to give the processes in the group a chance to exit on their own2. Then callTerminateJobObject(hJob, exitStatus), which forcefully terminates any remaining processes in the job.This functionality could be implemented in
libregrtestvia ctypes or by wrapping the required API functions in the_winapimodule. We'd maybe need one small change to the subprocess module, to make it retain the all-access thread handle thatCreateProcessW()returns. This would be to support creating the process suspended, which avoids a potential race in which the child spawns another process before it gets assigned to the job3. That's not likely to matter in our case, so usingCREATE_SUSPENDEDandResumeThread()could be omitted.Footnotes
-
On Windows 10+, the NTFS filesystem supports a POSIX-like delete that unlinks a deleted file as soon as the handle that was used to delete the file is closed. Only NTFS implements this new capability. In the API,
DeleteFileW()was updated to try a POSIX delete, butRemoveDirectoryW()still only tries a classic delete. ↩ -
The default handler for the break event calls
ExitProcess(), but a Python process can set a handler for the CSIGBREAKsignal, or set a lower-level console control handler via WinAPISetConsoleCtrlHandler(). ↩ -
On Windows 10+, there's also a
PROC_THREAD_ATTRIBUTE_JOB_LISTcreation attribute that can assign a process to one or more jobs in race-free way without having to create the process suspended. ↩
Reacted by Oleg Iarygin-
@eryksun
os.kill(pid, _signal.CTRL_BREAK_EVENT)would callGenerateConsoleCtrlEvent()for us:Lines 7985 to 7992 in 05e4886
if (sig == CTRL_C_EVENT || sig == CTRL_BREAK_EVENT) { if (GenerateConsoleCtrlEvent(sig, (DWORD)pid) == 0) { err = GetLastError(); PyErr_SetFromWindowsErr(err); } else Py_RETURN_NONE; } pid, tid, _, _ = _winapi.CreateProcess(creation_flags=_winapi.CREATE_NEW_PROCESS_GROUP); _winapi.CloseHandle(tid)should do its work too.However, I don't know job objects. Why are they necessary if we already can terminate the whole process group subtree?
os.kill(pid, _signal.CTRL_BREAK_EVENT) would call GenerateConsoleCtrlEvent()
I don't want to touch that mess, or perpetuate any further dependence on it. On Windows,
os.kill()misusesGenerateConsoleCtrlEvent(), which is only for a process group ID (i.e. POSIXkillpg(), orkill()with a negative pid value), and it also has bugs with exception handling that can raiseSystemError. The conceptual mistakes there have in turn led tosubprocess.Popenmistakenly usingos.kill()on Windows to implementsend_signal(), which is not actually supposed to target a process group.What makes matters worse is that WinAPI
GenerateConsoleCtrlEvent()has a serious bug if it's not used in the documented and supported way.However, I don't know job objects. Why are they necessary if we already can terminate the whole process group subtree?
Any process in the group can choose to handle
CTRL_BREAK_EVENTby ignoring it, e.g. with aSIGBREAKhandler in Python that does nothing. Also, some processes in the tree might be spawned in another process group (i.e.CREATE_NEW_PROCESS_GROUP), another console session (i.e.CREATE_NEW_CONSOLEorCREATE_NO_WINDOW), or without a console session (i.e.DETACHED_PROCESSor a GUI app). In these cases we can't send the process a break event.Sending the break event is an attempt to be polite for processes that support it. But after the timeout, we really need to ensure that all remaining processes in the job itself are forcefully terminated via
TerminateJobObject(). This is closest that we can reasonably get to the behavior of POSIXkillpg(pgrp, SIGTERM), followed bykillpg(pgrp, SIGKILL)after a timeout.The only processes that escape this net will be those spawned with the flags
CREATE_NEW_PROCESS_GROUP(or a new console session or no console session) andCREATE_BREAKAWAY_FROM_JOB.- added a commit that references this issue
on Nov 14, 2022 - added a commit that references this issue
on Nov 16, 2022
On Windows, it's common that a test fails at exit when trying to remove a temporary directory created by a test. Usually, it's because the test spawns a child process in this directory and the test tries to remove the temporary directory before the child process completes.
For example, temp_dir() of test.support.os_helper creates a temporary directory and then deletes it with rmtree(path) of test.support.os_helper: this function is different than shutil.rmtree(), it tries again on error with a timeout of 1.0 second.
But sometimes, the function fails with the error "PermissionError: [WinError 32] The process cannot access the file because it is being used by another process" on the os.rmdir() step.
If os.rmdir() fails with PermissionError, it would be nice to try again later.
Notes:
Linked PRs
asynciosubprocess test #99464asynciosubprocess test (GH-99464) #99504