44import socket
55import sys
66from collections .abc import Iterable , Sequence
7+ from types import TracebackType
78from typing import (
89 TYPE_CHECKING ,
910 Any ,
@@ -66,6 +67,7 @@ def __init__(
6667 loop : Optional [asyncio .AbstractEventLoop ] = None ,
6768 ** kwargs : Any ,
6869 ) -> None : # TODO(PY311): Use Unpack for kwargs.
70+ self ._closed = True
6971 self .loop = loop or asyncio .get_event_loop ()
7072 if TYPE_CHECKING :
7173 assert self .loop is not None
@@ -78,6 +80,7 @@ def __init__(
7880 self ._read_fds : set [int ] = set ()
7981 self ._write_fds : set [int ] = set ()
8082 self ._timer : Optional [asyncio .TimerHandle ] = None
83+ self ._closed = False
8184
8285 def _make_channel (self , ** kwargs : Any ) -> tuple [bool , pycares .Channel ]:
8386 if (
@@ -319,3 +322,55 @@ def _start_timer(self) -> None:
319322 timeout = 0.1
320323
321324 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