4
4
import socket
5
5
import sys
6
6
from collections .abc import Iterable , Sequence
7
+ from types import TracebackType
7
8
from typing import (
8
9
TYPE_CHECKING ,
9
10
Any ,
@@ -66,6 +67,7 @@ def __init__(
66
67
loop : Optional [asyncio .AbstractEventLoop ] = None ,
67
68
** kwargs : Any ,
68
69
) -> None : # TODO(PY311): Use Unpack for kwargs.
70
+ self ._closed = True
69
71
self .loop = loop or asyncio .get_event_loop ()
70
72
if TYPE_CHECKING :
71
73
assert self .loop is not None
@@ -78,6 +80,7 @@ def __init__(
78
80
self ._read_fds : set [int ] = set ()
79
81
self ._write_fds : set [int ] = set ()
80
82
self ._timer : Optional [asyncio .TimerHandle ] = None
83
+ self ._closed = False
81
84
82
85
def _make_channel (self , ** kwargs : Any ) -> tuple [bool , pycares .Channel ]:
83
86
if (
@@ -319,3 +322,55 @@ def _start_timer(self) -> None:
319
322
timeout = 0.1
320
323
321
324
self ._timer = self .loop .call_later (timeout , self ._timer_cb )
325
+
326
+ def _cleanup (self ) -> None :
327
+ """Cleanup timers and file descriptors when closing resolver."""
328
+ if self ._closed :
329
+ return
330
+ # Mark as closed first to prevent double cleanup
331
+ self ._closed = True
332
+ # Cancel timer if running
333
+ if self ._timer is not None :
334
+ self ._timer .cancel ()
335
+ self ._timer = None
336
+
337
+ # Remove all file descriptors
338
+ for fd in list (self ._read_fds ):
339
+ self .loop .remove_reader (fd )
340
+ for fd in list (self ._write_fds ):
341
+ self .loop .remove_writer (fd )
342
+
343
+ self ._read_fds .clear ()
344
+ self ._write_fds .clear ()
345
+ self ._channel .close ()
346
+
347
+ async def close (self ) -> None :
348
+ """
349
+ Cleanly close the DNS resolver.
350
+
351
+ This should be called to ensure all resources are properly released.
352
+ After calling close(), the resolver should not be used again.
353
+ """
354
+ self ._cleanup ()
355
+
356
+ async def __aenter__ (self ) -> 'DNSResolver' :
357
+ """Enter the async context manager."""
358
+ return self
359
+
360
+ async def __aexit__ (
361
+ self ,
362
+ exc_type : Optional [type [BaseException ]],
363
+ exc_val : Optional [BaseException ],
364
+ exc_tb : Optional [TracebackType ],
365
+ ) -> None :
366
+ """Exit the async context manager."""
367
+ await self .close ()
368
+
369
+ def __del__ (self ) -> None :
370
+ """Handle cleanup when the resolver is garbage collected."""
371
+ # Check if we have a channel to clean up
372
+ # This can happen if an exception occurs during __init__ before
373
+ # _channel is created (e.g., RuntimeError on Windows
374
+ # without proper loop)
375
+ if hasattr (self , '_channel' ):
376
+ self ._cleanup ()
0 commit comments