Skip to content

Commit f91745a

Browse files
committed
gh-154137: Check for Windows handle leaks in regrtest
Add handle_count() to test.support.os_helper.
1 parent 84897d2 commit f91745a

5 files changed

Lines changed: 96 additions & 2 deletions

File tree

Lib/test/libregrtest/refleak.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,15 @@ def get_pooled_int(value):
118118
rc_deltas = [0] * repcount
119119
alloc_deltas = [0] * repcount
120120
fd_deltas = [0] * repcount
121+
handle_deltas = [0] * repcount
121122
getallocatedblocks = sys.getallocatedblocks
122123
gettotalrefcount = sys.gettotalrefcount
123124
getunicodeinternedsize = sys.getunicodeinternedsize
124125
fd_count = os_helper.fd_count
126+
handle_count = os_helper.handle_count
125127
# initialize variables to make pyflakes quiet
126128
rc_before = alloc_before = fd_before = interned_immortal_before = 0
129+
handle_before = 0
127130

128131
if not quiet:
129132
print("beginning", repcount, "repetitions. Showing number of leaks "
@@ -160,14 +163,17 @@ def get_pooled_int(value):
160163
alloc_after = getallocatedblocks() - interned_immortal_after
161164
rc_after = gettotalrefcount()
162165
fd_after = fd_count()
166+
handle_after = handle_count()
163167

164168
rc_deltas[i] = get_pooled_int(rc_after - rc_before)
165169
alloc_deltas[i] = get_pooled_int(alloc_after - alloc_before)
166170
fd_deltas[i] = get_pooled_int(fd_after - fd_before)
171+
handle_deltas[i] = get_pooled_int(handle_after - handle_before)
167172

168173
if not quiet:
169174
# use max, not sum, so total_leaks is one of the pooled ints
170-
total_leaks = max(rc_deltas[i], alloc_deltas[i], fd_deltas[i])
175+
total_leaks = max(rc_deltas[i], alloc_deltas[i],
176+
fd_deltas[i], handle_deltas[i])
171177
if total_leaks <= 0:
172178
symbol = '.'
173179
elif total_leaks < 10:
@@ -185,6 +191,7 @@ def get_pooled_int(value):
185191
alloc_before = alloc_after
186192
rc_before = rc_after
187193
fd_before = fd_after
194+
handle_before = handle_after
188195
interned_immortal_before = interned_immortal_after
189196

190197
restore_support_xml(xml_filename)
@@ -215,7 +222,8 @@ def check_fd_deltas(deltas):
215222
for deltas, item_name, checker in [
216223
(rc_deltas, 'references', check_rc_deltas),
217224
(alloc_deltas, 'memory blocks', check_rc_deltas),
218-
(fd_deltas, 'file descriptors', check_fd_deltas)
225+
(fd_deltas, 'file descriptors', check_fd_deltas),
226+
(handle_deltas, 'handles', check_rc_deltas),
219227
]:
220228
# ignore warmup runs
221229
deltas = deltas[warmups:]

Lib/test/support/os_helper.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,3 +830,35 @@ def subst_drive(path):
830830
DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE,
831831
drive, path):
832832
raise ctypes.WinError(ctypes.get_last_error())
833+
834+
835+
try:
836+
if support.MS_WINDOWS:
837+
import ctypes.util
838+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
839+
else:
840+
raise AttributeError
841+
except (ImportError, AttributeError):
842+
def handle_count():
843+
return 0
844+
else:
845+
@ctypes.util.wrap_dll_function(kernel32)
846+
def GetCurrentProcess() -> ctypes.wintypes.HANDLE:
847+
pass
848+
849+
@ctypes.util.wrap_dll_function(kernel32)
850+
def GetProcessHandleCount(khProcess: ctypes.wintypes.HANDLE,
851+
pdwHandleCount: ctypes.wintypes.LPDWORD) -> ctypes.wintypes.BOOL:
852+
pass
853+
854+
del kernel32
855+
856+
def handle_count():
857+
# Pseudo-handle that doesn't need to be closed
858+
hproc = GetCurrentProcess()
859+
860+
handle_count = ctypes.wintypes.DWORD()
861+
if not GetProcessHandleCount(hproc, ctypes.byref(handle_count)):
862+
raise ctypes.WinError()
863+
864+
return handle_count.value

Lib/test/test_regrtest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,34 @@ def test_leak(self):
13821382
""")
13831383
self.check_leak(code, 'file descriptors')
13841384

1385+
@unittest.skipUnless(support.Py_DEBUG, 'need a debug build')
1386+
def test_huntrleaks_handle_leak(self):
1387+
# test --huntrleaks for Windows handle leak
1388+
code = textwrap.dedent("""
1389+
import unittest
1390+
import _winapi
1391+
1392+
handle = None
1393+
1394+
class HandleLeakTest(unittest.TestCase):
1395+
def test_leak(self):
1396+
global handle
1397+
if handle is None:
1398+
handle = _winapi.CreateFile(
1399+
__file__, _winapi.GENERIC_READ,
1400+
0, _winapi.NULL,
1401+
_winapi.OPEN_EXISTING,
1402+
0, _winapi.NULL)
1403+
else:
1404+
hproc = _winapi.GetCurrentProcess()
1405+
copy = _winapi.DuplicateHandle(
1406+
hproc, handle,
1407+
hproc, 0, False,
1408+
_winapi.DUPLICATE_SAME_ACCESS)
1409+
# bug! the new handle is never closed
1410+
""")
1411+
self.check_leak(code, 'handles')
1412+
13851413
def test_list_tests(self):
13861414
# test --list-tests
13871415
tests = [self.create_test() for i in range(5)]

Lib/test/test_support.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
from test.support import socket_helper
2727
from test.support import warnings_helper
2828

29+
if support.MS_WINDOWS:
30+
import _winapi
31+
32+
2933
TESTFN = os_helper.TESTFN
3034

3135

@@ -623,6 +627,27 @@ def test_fd_count(self):
623627
os.close(fd)
624628
self.assertEqual(more - start, 1)
625629

630+
@unittest.skipUnless(support.MS_WINDOWS, "test specific to Windows")
631+
def test_handle_count(self):
632+
handle = _winapi.CreateFile(
633+
__file__, _winapi.GENERIC_READ,
634+
0, _winapi.NULL,
635+
_winapi.OPEN_EXISTING,
636+
0, _winapi.NULL)
637+
self.addCleanup(_winapi.CloseHandle, handle)
638+
639+
start = os_helper.handle_count()
640+
hproc = _winapi.GetCurrentProcess()
641+
copy = _winapi.DuplicateHandle(
642+
hproc, handle,
643+
hproc, 0, False,
644+
_winapi.DUPLICATE_SAME_ACCESS)
645+
try:
646+
more = os_helper.handle_count()
647+
finally:
648+
_winapi.CloseHandle(copy)
649+
self.assertEqual(more - start, 1)
650+
626651
def check_print_warning(self, msg, expected):
627652
stderr = io.StringIO()
628653
with support.swap_attr(support.print_warning, 'orig_stderr', stderr):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Check for Windows handle leaks in regrtest. Patch by Victor Stinner.

0 commit comments

Comments
 (0)