Skip to content

Commit 0bc7c1a

Browse files
committed
Remove "Antivirus-compatible" delete method
Because the FILE_SHARE_DELETE approach only marks the file for deletion and does't necessarily delete before returning (the file is deleted when it is closed by the other process). That causes further cleanup (such as deleting the parent directory) to fail, and only makes things more complicated. Since we need to wait for the file to actually go away, we can just keep the retry loop around os.unlink().
1 parent c8f2d06 commit 0bc7c1a

1 file changed

Lines changed: 25 additions & 64 deletions

File tree

src/cjdk/_utils.py

Lines changed: 25 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import os
66
import shutil
7-
import sys
87
import time
98

109
from . import _progress
@@ -48,30 +47,12 @@ def backoff_seconds(initial_interval, max_interval, max_total, factor=1.5):
4847

4948

5049
def rmtree(path, timeout=2.5):
51-
# Try extra hard to clean up a temporary directory.
52-
53-
if sys.version_info >= (3, 12):
54-
55-
def retry_unlink(func, path, exc):
56-
if func is os.unlink:
57-
unlink_file(path, timeout=0)
58-
else:
59-
raise exc
60-
61-
rmtree_kwargs = {"onexc": retry_unlink}
62-
else:
63-
64-
def retry_unlink(func, path, exc_info):
65-
if func is os.unlink:
66-
unlink_file(path, timeout=0)
67-
else:
68-
raise exc_info[1].with_traceback(exc_info[2])
69-
70-
rmtree_kwargs = {"onerror": retry_unlink}
50+
# Try extra hard to clean up a temporary directory. See comment in
51+
# unlink_file() for why.
7152

7253
for wait_seconds in backoff_seconds(0.001, 0.5, timeout):
7354
try:
74-
shutil.rmtree(path, **rmtree_kwargs)
55+
shutil.rmtree(path)
7556
except OSError as e:
7657
if (
7758
hasattr(e, "winerror")
@@ -86,51 +67,31 @@ def retry_unlink(func, path, exc_info):
8667

8768

8869
def unlink_file(path, timeout=2.5):
89-
if sys.platform != "win32":
90-
return os.unlink(path)
91-
92-
# On Windows, we sometimes encounter errors when trying to delete a file
93-
# that we just closed after writing. This is due to Antivirus opening the
94-
# file to scan it. Microsoft Defender Antivirus is said to use
95-
# FILE_SHARE_DELETE, but os.unlink() calls DeleteFileW(), which does not
96-
# use FILE_SHARE_DELETE; since the check is bidirectional, it fails.
97-
# So use Win32 API calls that will not have this problem.
98-
99-
import ctypes
100-
from ctypes import wintypes
101-
102-
kernel32 = ctypes.windll.kernel32
103-
kernel32.CreateFileW.restype = wintypes.HANDLE
104-
105-
DELETE = 0x00010000
106-
FILE_SHARE_READ = 0x01
107-
FILE_SHARE_WRITE = 0x02
108-
FILE_SHARE_DELETE = 0x04
109-
OPEN_EXISTING = 3
110-
FILE_FLAG_DELETE_ON_CLOSE = 0x04000000
111-
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
112-
113-
def do_unlink(path):
114-
handle = INVALID_HANDLE_VALUE
115-
try:
116-
handle = kernel32.CreateFileW(
117-
str(path),
118-
DELETE,
119-
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
120-
None,
121-
OPEN_EXISTING,
122-
FILE_FLAG_DELETE_ON_CLOSE,
123-
None,
124-
)
125-
if handle == INVALID_HANDLE_VALUE:
126-
os.unlink(path) # Let it raise an appropriate error.
127-
finally:
128-
if handle != INVALID_HANDLE_VALUE:
129-
kernel32.CloseHandle(handle)
70+
# On Windows, we may encounter errors when trying to delete a file that we
71+
# just closed after writing, due to Antivirus opening the file to scan it.
72+
# Microsoft Defender Antivirus is said to use FILE_SHARE_DELETE, but
73+
# os.unlink() calls DeleteFileW(), which does not use FILE_SHARE_DELETE;
74+
# since the check is bidirectional, it fails.
75+
#
76+
# (To be clear, this is probably rare and the exact conditions are unknown;
77+
# Also I have not actually observed it happening.)
78+
#
79+
# So we could use different Win32 API calls, namely CreateFile(path,
80+
# DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
81+
# OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL) and then CloseHandle().
82+
# This should succeed on files being scanned.
83+
#
84+
# However, that method only marks the file for deleteion; actual deletion
85+
# happens once all processes close the file. This is a problem, because we
86+
# need to do the next thing, which is often to delete the parent directory
87+
# of the file. So we need to wait for the file to go away.
88+
#
89+
# And as long as we're waiting anyway, we can wait for regular deletion to
90+
# succeed, so we don't need to use Win32 API calls directly.
13091

13192
for wait_seconds in backoff_seconds(0.001, 0.5, timeout):
13293
try:
133-
do_unlink(path)
94+
os.unlink(path)
13495
except OSError as e:
13596
if (
13697
hasattr(e, "winerror")

0 commit comments

Comments
 (0)