pycares 4.8.0__cp312-cp312-win32.whl → 4.9.0__cp312-cp312-win32.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycares/__init__.py +165 -20
- pycares/_cares.pyd +0 -0
- pycares/_version.py +1 -1
- {pycares-4.8.0.dist-info → pycares-4.9.0.dist-info}/METADATA +1 -1
- pycares-4.9.0.dist-info/RECORD +12 -0
- {pycares-4.8.0.dist-info → pycares-4.9.0.dist-info}/WHEEL +1 -1
- pycares-4.8.0.dist-info/RECORD +0 -12
- {pycares-4.8.0.dist-info → pycares-4.9.0.dist-info}/licenses/LICENSE +0 -0
- {pycares-4.8.0.dist-info → pycares-4.9.0.dist-info}/top_level.txt +0 -0
pycares/__init__.py
CHANGED
@@ -11,10 +11,13 @@ from ._version import __version__
|
|
11
11
|
|
12
12
|
import socket
|
13
13
|
import math
|
14
|
-
import
|
15
|
-
import
|
14
|
+
import threading
|
15
|
+
import time
|
16
|
+
import weakref
|
16
17
|
from collections.abc import Callable, Iterable
|
17
|
-
from
|
18
|
+
from contextlib import suppress
|
19
|
+
from typing import Any, Callable, Optional, Dict, Union
|
20
|
+
from queue import SimpleQueue
|
18
21
|
|
19
22
|
IP4 = tuple[str, int]
|
20
23
|
IP6 = tuple[str, int, int, int]
|
@@ -82,17 +85,25 @@ class AresError(Exception):
|
|
82
85
|
|
83
86
|
# callback helpers
|
84
87
|
|
85
|
-
|
88
|
+
_handle_to_channel: Dict[Any, "Channel"] = {} # Maps handle to channel to prevent use-after-free
|
89
|
+
|
86
90
|
|
87
91
|
@_ffi.def_extern()
|
88
92
|
def _sock_state_cb(data, socket_fd, readable, writable):
|
93
|
+
# Note: sock_state_cb handle is not tracked in _handle_to_channel
|
94
|
+
# because it has a different lifecycle (tied to the channel, not individual queries)
|
95
|
+
if _ffi is None:
|
96
|
+
return
|
89
97
|
sock_state_cb = _ffi.from_handle(data)
|
90
98
|
sock_state_cb(socket_fd, readable, writable)
|
91
99
|
|
92
100
|
@_ffi.def_extern()
|
93
101
|
def _host_cb(arg, status, timeouts, hostent):
|
102
|
+
# Get callback data without removing the reference yet
|
103
|
+
if _ffi is None or arg not in _handle_to_channel:
|
104
|
+
return
|
105
|
+
|
94
106
|
callback = _ffi.from_handle(arg)
|
95
|
-
_global_set.discard(arg)
|
96
107
|
|
97
108
|
if status != _lib.ARES_SUCCESS:
|
98
109
|
result = None
|
@@ -101,11 +112,15 @@ def _host_cb(arg, status, timeouts, hostent):
|
|
101
112
|
status = None
|
102
113
|
|
103
114
|
callback(result, status)
|
115
|
+
_handle_to_channel.pop(arg, None)
|
104
116
|
|
105
117
|
@_ffi.def_extern()
|
106
118
|
def _nameinfo_cb(arg, status, timeouts, node, service):
|
119
|
+
# Get callback data without removing the reference yet
|
120
|
+
if _ffi is None or arg not in _handle_to_channel:
|
121
|
+
return
|
122
|
+
|
107
123
|
callback = _ffi.from_handle(arg)
|
108
|
-
_global_set.discard(arg)
|
109
124
|
|
110
125
|
if status != _lib.ARES_SUCCESS:
|
111
126
|
result = None
|
@@ -114,11 +129,15 @@ def _nameinfo_cb(arg, status, timeouts, node, service):
|
|
114
129
|
status = None
|
115
130
|
|
116
131
|
callback(result, status)
|
132
|
+
_handle_to_channel.pop(arg, None)
|
117
133
|
|
118
134
|
@_ffi.def_extern()
|
119
135
|
def _query_cb(arg, status, timeouts, abuf, alen):
|
136
|
+
# Get callback data without removing the reference yet
|
137
|
+
if _ffi is None or arg not in _handle_to_channel:
|
138
|
+
return
|
139
|
+
|
120
140
|
callback, query_type = _ffi.from_handle(arg)
|
121
|
-
_global_set.discard(arg)
|
122
141
|
|
123
142
|
if status == _lib.ARES_SUCCESS:
|
124
143
|
if query_type == _lib.T_ANY:
|
@@ -141,11 +160,15 @@ def _query_cb(arg, status, timeouts, abuf, alen):
|
|
141
160
|
result = None
|
142
161
|
|
143
162
|
callback(result, status)
|
163
|
+
_handle_to_channel.pop(arg, None)
|
144
164
|
|
145
165
|
@_ffi.def_extern()
|
146
166
|
def _addrinfo_cb(arg, status, timeouts, res):
|
167
|
+
# Get callback data without removing the reference yet
|
168
|
+
if _ffi is None or arg not in _handle_to_channel:
|
169
|
+
return
|
170
|
+
|
147
171
|
callback = _ffi.from_handle(arg)
|
148
|
-
_global_set.discard(arg)
|
149
172
|
|
150
173
|
if status != _lib.ARES_SUCCESS:
|
151
174
|
result = None
|
@@ -154,6 +177,7 @@ def _addrinfo_cb(arg, status, timeouts, res):
|
|
154
177
|
status = None
|
155
178
|
|
156
179
|
callback(result, status)
|
180
|
+
_handle_to_channel.pop(arg, None)
|
157
181
|
|
158
182
|
def parse_result(query_type, abuf, alen):
|
159
183
|
if query_type == _lib.T_A:
|
@@ -314,6 +338,53 @@ def parse_result(query_type, abuf, alen):
|
|
314
338
|
return result, status
|
315
339
|
|
316
340
|
|
341
|
+
class _ChannelShutdownManager:
|
342
|
+
"""Manages channel destruction in a single background thread using SimpleQueue."""
|
343
|
+
|
344
|
+
def __init__(self) -> None:
|
345
|
+
self._queue: SimpleQueue = SimpleQueue()
|
346
|
+
self._thread: Optional[threading.Thread] = None
|
347
|
+
self._thread_started = False
|
348
|
+
|
349
|
+
def _run_safe_shutdown_loop(self) -> None:
|
350
|
+
"""Process channel destruction requests from the queue."""
|
351
|
+
while True:
|
352
|
+
# Block forever until we get a channel to destroy
|
353
|
+
channel = self._queue.get()
|
354
|
+
|
355
|
+
# Sleep for 1 second to ensure c-ares has finished processing
|
356
|
+
# Its important that c-ares is past this critcial section
|
357
|
+
# so we use a delay to ensure it has time to finish processing
|
358
|
+
# https://github.com/c-ares/c-ares/blob/4f42928848e8b73d322b15ecbe3e8d753bf8734e/src/lib/ares_process.c#L1422
|
359
|
+
time.sleep(1.0)
|
360
|
+
|
361
|
+
# Destroy the channel
|
362
|
+
if _lib is not None and channel is not None:
|
363
|
+
_lib.ares_destroy(channel[0])
|
364
|
+
|
365
|
+
def destroy_channel(self, channel) -> None:
|
366
|
+
"""
|
367
|
+
Schedule channel destruction on the background thread with a safety delay.
|
368
|
+
|
369
|
+
Thread Safety and Synchronization:
|
370
|
+
This method uses SimpleQueue which is thread-safe for putting items
|
371
|
+
from multiple threads. The background thread processes channels
|
372
|
+
sequentially with a 1-second delay before each destruction.
|
373
|
+
"""
|
374
|
+
# Put the channel in the queue
|
375
|
+
self._queue.put(channel)
|
376
|
+
|
377
|
+
# Start the background thread if not already started
|
378
|
+
if not self._thread_started:
|
379
|
+
self._thread_started = True
|
380
|
+
self._thread = threading.Thread(target=self._run_safe_shutdown_loop, daemon=True)
|
381
|
+
self._thread.start()
|
382
|
+
|
383
|
+
|
384
|
+
# Global shutdown manager instance
|
385
|
+
_shutdown_manager = _ChannelShutdownManager()
|
386
|
+
|
387
|
+
|
317
388
|
class Channel:
|
318
389
|
__qtypes__ = (_lib.T_A, _lib.T_AAAA, _lib.T_ANY, _lib.T_CAA, _lib.T_CNAME, _lib.T_MX, _lib.T_NAPTR, _lib.T_NS, _lib.T_PTR, _lib.T_SOA, _lib.T_SRV, _lib.T_TXT)
|
319
390
|
__qclasses__ = (_lib.C_IN, _lib.C_CHAOS, _lib.C_HS, _lib.C_NONE, _lib.C_ANY)
|
@@ -337,6 +408,9 @@ class Channel:
|
|
337
408
|
resolvconf_path: Union[str, bytes, None] = None,
|
338
409
|
event_thread: bool = False) -> None:
|
339
410
|
|
411
|
+
# Initialize _channel to None first to ensure __del__ doesn't fail
|
412
|
+
self._channel = None
|
413
|
+
|
340
414
|
channel = _ffi.new("ares_channel *")
|
341
415
|
options = _ffi.new("struct ares_options *")
|
342
416
|
optmask = 0
|
@@ -421,8 +495,9 @@ class Channel:
|
|
421
495
|
if r != _lib.ARES_SUCCESS:
|
422
496
|
raise AresError('Failed to initialize c-ares channel')
|
423
497
|
|
424
|
-
|
425
|
-
|
498
|
+
# Initialize all attributes for consistency
|
499
|
+
self._event_thread = event_thread
|
500
|
+
self._channel = channel
|
426
501
|
if servers:
|
427
502
|
self.servers = servers
|
428
503
|
|
@@ -432,6 +507,46 @@ class Channel:
|
|
432
507
|
if local_dev:
|
433
508
|
self.set_local_dev(local_dev)
|
434
509
|
|
510
|
+
def __enter__(self):
|
511
|
+
"""Enter the context manager."""
|
512
|
+
return self
|
513
|
+
|
514
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
515
|
+
"""Exit the context manager and close the channel."""
|
516
|
+
self.close()
|
517
|
+
return False
|
518
|
+
|
519
|
+
def __del__(self) -> None:
|
520
|
+
"""Ensure the channel is destroyed when the object is deleted."""
|
521
|
+
if self._channel is not None:
|
522
|
+
# Schedule channel destruction using the global shutdown manager
|
523
|
+
self._schedule_destruction()
|
524
|
+
|
525
|
+
def _create_callback_handle(self, callback_data):
|
526
|
+
"""
|
527
|
+
Create a callback handle and register it for tracking.
|
528
|
+
|
529
|
+
This ensures that:
|
530
|
+
1. The callback data is wrapped in a CFFI handle
|
531
|
+
2. The handle is mapped to this channel to keep it alive
|
532
|
+
|
533
|
+
Args:
|
534
|
+
callback_data: The data to pass to the callback (usually a callable or tuple)
|
535
|
+
|
536
|
+
Returns:
|
537
|
+
The CFFI handle that can be passed to C functions
|
538
|
+
|
539
|
+
Raises:
|
540
|
+
RuntimeError: If the channel is destroyed
|
541
|
+
|
542
|
+
"""
|
543
|
+
if self._channel is None:
|
544
|
+
raise RuntimeError("Channel is destroyed, no new queries allowed")
|
545
|
+
|
546
|
+
userdata = _ffi.new_handle(callback_data)
|
547
|
+
_handle_to_channel[userdata] = self
|
548
|
+
return userdata
|
549
|
+
|
435
550
|
def cancel(self) -> None:
|
436
551
|
_lib.ares_cancel(self._channel[0])
|
437
552
|
|
@@ -531,16 +646,14 @@ class Channel:
|
|
531
646
|
else:
|
532
647
|
raise ValueError("invalid IP address")
|
533
648
|
|
534
|
-
userdata =
|
535
|
-
_global_set.add(userdata)
|
649
|
+
userdata = self._create_callback_handle(callback)
|
536
650
|
_lib.ares_gethostbyaddr(self._channel[0], address, _ffi.sizeof(address[0]), family, _lib._host_cb, userdata)
|
537
651
|
|
538
652
|
def gethostbyname(self, name: str, family: socket.AddressFamily, callback: Callable[[Any, int], None]) -> None:
|
539
653
|
if not callable(callback):
|
540
654
|
raise TypeError("a callable is required")
|
541
655
|
|
542
|
-
userdata =
|
543
|
-
_global_set.add(userdata)
|
656
|
+
userdata = self._create_callback_handle(callback)
|
544
657
|
_lib.ares_gethostbyname(self._channel[0], parse_name(name), family, _lib._host_cb, userdata)
|
545
658
|
|
546
659
|
def getaddrinfo(
|
@@ -563,8 +676,7 @@ class Channel:
|
|
563
676
|
else:
|
564
677
|
service = ascii_bytes(port)
|
565
678
|
|
566
|
-
userdata =
|
567
|
-
_global_set.add(userdata)
|
679
|
+
userdata = self._create_callback_handle(callback)
|
568
680
|
|
569
681
|
hints = _ffi.new('struct ares_addrinfo_hints*')
|
570
682
|
hints.ai_flags = flags
|
@@ -592,8 +704,7 @@ class Channel:
|
|
592
704
|
if query_class not in self.__qclasses__:
|
593
705
|
raise ValueError('invalid query class specified')
|
594
706
|
|
595
|
-
userdata =
|
596
|
-
_global_set.add(userdata)
|
707
|
+
userdata = self._create_callback_handle((callback, query_type))
|
597
708
|
func(self._channel[0], parse_name(name), query_class, query_type, _lib._query_cb, userdata)
|
598
709
|
|
599
710
|
def set_local_ip(self, ip):
|
@@ -631,13 +742,47 @@ class Channel:
|
|
631
742
|
else:
|
632
743
|
raise ValueError("Invalid address argument")
|
633
744
|
|
634
|
-
userdata =
|
635
|
-
_global_set.add(userdata)
|
745
|
+
userdata = self._create_callback_handle(callback)
|
636
746
|
_lib.ares_getnameinfo(self._channel[0], _ffi.cast("struct sockaddr*", sa), _ffi.sizeof(sa[0]), flags, _lib._nameinfo_cb, userdata)
|
637
747
|
|
638
748
|
def set_local_dev(self, dev):
|
639
749
|
_lib.ares_set_local_dev(self._channel[0], dev)
|
640
750
|
|
751
|
+
def close(self) -> None:
|
752
|
+
"""
|
753
|
+
Close the channel as soon as it's safe to do so.
|
754
|
+
|
755
|
+
This method can be called from any thread. The channel will be destroyed
|
756
|
+
safely using a background thread with a 1-second delay to ensure c-ares
|
757
|
+
has completed its cleanup.
|
758
|
+
|
759
|
+
Note: Once close() is called, no new queries can be started. Any pending
|
760
|
+
queries will be cancelled and their callbacks will receive ARES_ECANCELLED.
|
761
|
+
|
762
|
+
"""
|
763
|
+
if self._channel is None:
|
764
|
+
# Already destroyed
|
765
|
+
return
|
766
|
+
|
767
|
+
# Cancel all pending queries - this will trigger callbacks with ARES_ECANCELLED
|
768
|
+
self.cancel()
|
769
|
+
|
770
|
+
# Schedule channel destruction
|
771
|
+
self._schedule_destruction()
|
772
|
+
|
773
|
+
def _schedule_destruction(self) -> None:
|
774
|
+
"""Schedule channel destruction using the global shutdown manager."""
|
775
|
+
if self._channel is None:
|
776
|
+
return
|
777
|
+
channel = self._channel
|
778
|
+
self._channel = None
|
779
|
+
# Can't start threads during interpreter shutdown
|
780
|
+
# The channel will be cleaned up by the OS
|
781
|
+
# TODO: Change to PythonFinalizationError when Python 3.12 support is dropped
|
782
|
+
with suppress(RuntimeError):
|
783
|
+
_shutdown_manager.destroy_channel(channel)
|
784
|
+
|
785
|
+
|
641
786
|
|
642
787
|
class AresResult:
|
643
788
|
__slots__ = ()
|
pycares/_cares.pyd
CHANGED
Binary file
|
pycares/_version.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
|
2
|
-
__version__ = '4.
|
2
|
+
__version__ = '4.9.0'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
pycares/__init__.py,sha256=ArtBS_p6Q3mzSQLymfl2Ai5l04bwZfPnVJoEKDmVDXQ,39309
|
2
|
+
pycares/__main__.py,sha256=-WwwGX4NQ8hpOqrNuCy59quCQJt7IAwQXdQjga5s4WA,2880
|
3
|
+
pycares/_cares.pyd,sha256=_td49TOrEb51ACVXOgFk4QkQGcjFd6byYEK-PB0Ehfg,218624
|
4
|
+
pycares/_version.py,sha256=cwSQF2Mr2Air7lTld14ORbGvBY9YVvupK2KA0SGv9wo,25
|
5
|
+
pycares/errno.py,sha256=32f2SnSjYACq7peW9Iqb7cvDvwup6LDpNMWGHWhLnWI,2340
|
6
|
+
pycares/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
pycares/utils.py,sha256=ZzjEdkygbU3_B1g4SVwlDS949PhttJG2gK735_6G5Ps,1344
|
8
|
+
pycares-4.9.0.dist-info/licenses/LICENSE,sha256=ZzIVbIpf5QFzaiLCDSjxhvH5EViAWLVO-W4ZgBzWvb8,1090
|
9
|
+
pycares-4.9.0.dist-info/METADATA,sha256=o2LlsAJHZbyF8M9vohunxfn1fl6nPArn9mL4rpT-hdg,4622
|
10
|
+
pycares-4.9.0.dist-info/WHEEL,sha256=LwxTQZ0gyDP_uaeNCLm-ZIktY9hv6x0e22Q-hgFd-po,97
|
11
|
+
pycares-4.9.0.dist-info/top_level.txt,sha256=nIeo7L2XUVBQZO2YE6pH7tlKaBWTfmmRcXbqe_NWYCw,15
|
12
|
+
pycares-4.9.0.dist-info/RECORD,,
|
pycares-4.8.0.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
pycares/__init__.py,sha256=HW5ZP40mcZS4OacgdYM3yMEwCmg-Mf62rlsslb7pnjo,33758
|
2
|
-
pycares/__main__.py,sha256=-WwwGX4NQ8hpOqrNuCy59quCQJt7IAwQXdQjga5s4WA,2880
|
3
|
-
pycares/_cares.pyd,sha256=uDOjluhp4PgmZqN1shovcxCDoCglqG5qul111rWrqZE,218624
|
4
|
-
pycares/_version.py,sha256=PDP4J6LUC78yYPTz7-gw425AL_1QMvSLs8NWrNcxbbM,25
|
5
|
-
pycares/errno.py,sha256=32f2SnSjYACq7peW9Iqb7cvDvwup6LDpNMWGHWhLnWI,2340
|
6
|
-
pycares/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
pycares/utils.py,sha256=ZzjEdkygbU3_B1g4SVwlDS949PhttJG2gK735_6G5Ps,1344
|
8
|
-
pycares-4.8.0.dist-info/licenses/LICENSE,sha256=ZzIVbIpf5QFzaiLCDSjxhvH5EViAWLVO-W4ZgBzWvb8,1090
|
9
|
-
pycares-4.8.0.dist-info/METADATA,sha256=e--e95_lvcw89okQpbeHect6WCMTcIwKIluteJxlq7A,4622
|
10
|
-
pycares-4.8.0.dist-info/WHEEL,sha256=5BZ7yYYQqS-uotryveO5ueDnJWJnUbna4XsxS0lHQXk,97
|
11
|
-
pycares-4.8.0.dist-info/top_level.txt,sha256=nIeo7L2XUVBQZO2YE6pH7tlKaBWTfmmRcXbqe_NWYCw,15
|
12
|
-
pycares-4.8.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|