pycares 4.7.0__cp39-cp39-macosx_11_0_arm64.whl → 4.9.0__cp39-cp39-macosx_11_0_arm64.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 +169 -20
- pycares/_cares.abi3.so +0 -0
- pycares/_version.py +1 -1
- {pycares-4.7.0.dist-info → pycares-4.9.0.dist-info}/METADATA +1 -1
- pycares-4.9.0.dist-info/RECORD +12 -0
- {pycares-4.7.0.dist-info → pycares-4.9.0.dist-info}/WHEEL +2 -1
- pycares-4.7.0.dist-info/RECORD +0 -12
- {pycares-4.7.0.dist-info → pycares-4.9.0.dist-info}/licenses/LICENSE +0 -0
- {pycares-4.7.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]
|
@@ -28,6 +31,8 @@ ARES_FLAG_STAYOPEN = _lib.ARES_FLAG_STAYOPEN
|
|
28
31
|
ARES_FLAG_NOSEARCH = _lib.ARES_FLAG_NOSEARCH
|
29
32
|
ARES_FLAG_NOALIASES = _lib.ARES_FLAG_NOALIASES
|
30
33
|
ARES_FLAG_NOCHECKRESP = _lib.ARES_FLAG_NOCHECKRESP
|
34
|
+
ARES_FLAG_EDNS = _lib.ARES_FLAG_EDNS
|
35
|
+
ARES_FLAG_NO_DFLT_SVR = _lib.ARES_FLAG_NO_DFLT_SVR
|
31
36
|
|
32
37
|
# Nameinfo flag values
|
33
38
|
ARES_NI_NOFQDN = _lib.ARES_NI_NOFQDN
|
@@ -80,17 +85,25 @@ class AresError(Exception):
|
|
80
85
|
|
81
86
|
# callback helpers
|
82
87
|
|
83
|
-
|
88
|
+
_handle_to_channel: Dict[Any, "Channel"] = {} # Maps handle to channel to prevent use-after-free
|
89
|
+
|
84
90
|
|
85
91
|
@_ffi.def_extern()
|
86
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
|
87
97
|
sock_state_cb = _ffi.from_handle(data)
|
88
98
|
sock_state_cb(socket_fd, readable, writable)
|
89
99
|
|
90
100
|
@_ffi.def_extern()
|
91
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
|
+
|
92
106
|
callback = _ffi.from_handle(arg)
|
93
|
-
_global_set.discard(arg)
|
94
107
|
|
95
108
|
if status != _lib.ARES_SUCCESS:
|
96
109
|
result = None
|
@@ -99,11 +112,15 @@ def _host_cb(arg, status, timeouts, hostent):
|
|
99
112
|
status = None
|
100
113
|
|
101
114
|
callback(result, status)
|
115
|
+
_handle_to_channel.pop(arg, None)
|
102
116
|
|
103
117
|
@_ffi.def_extern()
|
104
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
|
+
|
105
123
|
callback = _ffi.from_handle(arg)
|
106
|
-
_global_set.discard(arg)
|
107
124
|
|
108
125
|
if status != _lib.ARES_SUCCESS:
|
109
126
|
result = None
|
@@ -112,11 +129,15 @@ def _nameinfo_cb(arg, status, timeouts, node, service):
|
|
112
129
|
status = None
|
113
130
|
|
114
131
|
callback(result, status)
|
132
|
+
_handle_to_channel.pop(arg, None)
|
115
133
|
|
116
134
|
@_ffi.def_extern()
|
117
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
|
+
|
118
140
|
callback, query_type = _ffi.from_handle(arg)
|
119
|
-
_global_set.discard(arg)
|
120
141
|
|
121
142
|
if status == _lib.ARES_SUCCESS:
|
122
143
|
if query_type == _lib.T_ANY:
|
@@ -139,11 +160,15 @@ def _query_cb(arg, status, timeouts, abuf, alen):
|
|
139
160
|
result = None
|
140
161
|
|
141
162
|
callback(result, status)
|
163
|
+
_handle_to_channel.pop(arg, None)
|
142
164
|
|
143
165
|
@_ffi.def_extern()
|
144
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
|
+
|
145
171
|
callback = _ffi.from_handle(arg)
|
146
|
-
_global_set.discard(arg)
|
147
172
|
|
148
173
|
if status != _lib.ARES_SUCCESS:
|
149
174
|
result = None
|
@@ -152,6 +177,7 @@ def _addrinfo_cb(arg, status, timeouts, res):
|
|
152
177
|
status = None
|
153
178
|
|
154
179
|
callback(result, status)
|
180
|
+
_handle_to_channel.pop(arg, None)
|
155
181
|
|
156
182
|
def parse_result(query_type, abuf, alen):
|
157
183
|
if query_type == _lib.T_A:
|
@@ -312,6 +338,53 @@ def parse_result(query_type, abuf, alen):
|
|
312
338
|
return result, status
|
313
339
|
|
314
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
|
+
|
315
388
|
class Channel:
|
316
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)
|
317
390
|
__qclasses__ = (_lib.C_IN, _lib.C_CHAOS, _lib.C_HS, _lib.C_NONE, _lib.C_ANY)
|
@@ -335,6 +408,9 @@ class Channel:
|
|
335
408
|
resolvconf_path: Union[str, bytes, None] = None,
|
336
409
|
event_thread: bool = False) -> None:
|
337
410
|
|
411
|
+
# Initialize _channel to None first to ensure __del__ doesn't fail
|
412
|
+
self._channel = None
|
413
|
+
|
338
414
|
channel = _ffi.new("ares_channel *")
|
339
415
|
options = _ffi.new("struct ares_options *")
|
340
416
|
optmask = 0
|
@@ -419,8 +495,9 @@ class Channel:
|
|
419
495
|
if r != _lib.ARES_SUCCESS:
|
420
496
|
raise AresError('Failed to initialize c-ares channel')
|
421
497
|
|
422
|
-
|
423
|
-
|
498
|
+
# Initialize all attributes for consistency
|
499
|
+
self._event_thread = event_thread
|
500
|
+
self._channel = channel
|
424
501
|
if servers:
|
425
502
|
self.servers = servers
|
426
503
|
|
@@ -430,6 +507,46 @@ class Channel:
|
|
430
507
|
if local_dev:
|
431
508
|
self.set_local_dev(local_dev)
|
432
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
|
+
|
433
550
|
def cancel(self) -> None:
|
434
551
|
_lib.ares_cancel(self._channel[0])
|
435
552
|
|
@@ -529,16 +646,14 @@ class Channel:
|
|
529
646
|
else:
|
530
647
|
raise ValueError("invalid IP address")
|
531
648
|
|
532
|
-
userdata =
|
533
|
-
_global_set.add(userdata)
|
649
|
+
userdata = self._create_callback_handle(callback)
|
534
650
|
_lib.ares_gethostbyaddr(self._channel[0], address, _ffi.sizeof(address[0]), family, _lib._host_cb, userdata)
|
535
651
|
|
536
652
|
def gethostbyname(self, name: str, family: socket.AddressFamily, callback: Callable[[Any, int], None]) -> None:
|
537
653
|
if not callable(callback):
|
538
654
|
raise TypeError("a callable is required")
|
539
655
|
|
540
|
-
userdata =
|
541
|
-
_global_set.add(userdata)
|
656
|
+
userdata = self._create_callback_handle(callback)
|
542
657
|
_lib.ares_gethostbyname(self._channel[0], parse_name(name), family, _lib._host_cb, userdata)
|
543
658
|
|
544
659
|
def getaddrinfo(
|
@@ -561,8 +676,7 @@ class Channel:
|
|
561
676
|
else:
|
562
677
|
service = ascii_bytes(port)
|
563
678
|
|
564
|
-
userdata =
|
565
|
-
_global_set.add(userdata)
|
679
|
+
userdata = self._create_callback_handle(callback)
|
566
680
|
|
567
681
|
hints = _ffi.new('struct ares_addrinfo_hints*')
|
568
682
|
hints.ai_flags = flags
|
@@ -590,8 +704,7 @@ class Channel:
|
|
590
704
|
if query_class not in self.__qclasses__:
|
591
705
|
raise ValueError('invalid query class specified')
|
592
706
|
|
593
|
-
userdata =
|
594
|
-
_global_set.add(userdata)
|
707
|
+
userdata = self._create_callback_handle((callback, query_type))
|
595
708
|
func(self._channel[0], parse_name(name), query_class, query_type, _lib._query_cb, userdata)
|
596
709
|
|
597
710
|
def set_local_ip(self, ip):
|
@@ -629,13 +742,47 @@ class Channel:
|
|
629
742
|
else:
|
630
743
|
raise ValueError("Invalid address argument")
|
631
744
|
|
632
|
-
userdata =
|
633
|
-
_global_set.add(userdata)
|
745
|
+
userdata = self._create_callback_handle(callback)
|
634
746
|
_lib.ares_getnameinfo(self._channel[0], _ffi.cast("struct sockaddr*", sa), _ffi.sizeof(sa[0]), flags, _lib._nameinfo_cb, userdata)
|
635
747
|
|
636
748
|
def set_local_dev(self, dev):
|
637
749
|
_lib.ares_set_local_dev(self._channel[0], dev)
|
638
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
|
+
|
639
786
|
|
640
787
|
class AresResult:
|
641
788
|
__slots__ = ()
|
@@ -881,6 +1028,8 @@ __all__ = (
|
|
881
1028
|
"ARES_FLAG_NOSEARCH",
|
882
1029
|
"ARES_FLAG_NOALIASES",
|
883
1030
|
"ARES_FLAG_NOCHECKRESP",
|
1031
|
+
"ARES_FLAG_EDNS",
|
1032
|
+
"ARES_FLAG_NO_DFLT_SVR",
|
884
1033
|
|
885
1034
|
# Nameinfo flag values
|
886
1035
|
"ARES_NI_NOFQDN",
|
pycares/_cares.abi3.so
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/errno.py,sha256=XlxDLFvCXrGBnqsNgBwYz9_q5lB9wnZ-JTfCtBbWlj0,2271
|
2
|
+
pycares/_version.py,sha256=6uX66RgEKRjXLLJ5qbAbkzb9Feahl_ZO976QMimw8ew,23
|
3
|
+
pycares/_cares.abi3.so,sha256=ECi4M9MuQnIdheN7NEk14XEbveGw39bNhSnF1ciPJDc,383696
|
4
|
+
pycares/__init__.py,sha256=jKcpW3r0j8EI4_tTd46REO0Epz07gBYplDOdw8HdTCs,38226
|
5
|
+
pycares/utils.py,sha256=BxfZ13HfupyVOz2pTYsdr9QNGCAINjD7XNHgdRFhwlY,1291
|
6
|
+
pycares/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
pycares/__main__.py,sha256=zagpuBSUv3MqoMvzby_5xzSW9ky76DAxWM8b5SMiJrw,2796
|
8
|
+
pycares-4.9.0.dist-info/RECORD,,
|
9
|
+
pycares-4.9.0.dist-info/WHEEL,sha256=bDWaFWigpG5bEpqw9IoRiyYs8MvmSFh0OhUAOoi_-KA,134
|
10
|
+
pycares-4.9.0.dist-info/top_level.txt,sha256=nIeo7L2XUVBQZO2YE6pH7tlKaBWTfmmRcXbqe_NWYCw,15
|
11
|
+
pycares-4.9.0.dist-info/METADATA,sha256=FDeVl5WkJhWG_cILK5IG8hAlvpS4tSrqVohRKdUpMwc,4338
|
12
|
+
pycares-4.9.0.dist-info/licenses/LICENSE,sha256=QnuzpTcgJKJuLCkLJDglErzHyhI0McTdF43WE77qfu4,1070
|
pycares-4.7.0.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
pycares/errno.py,sha256=XlxDLFvCXrGBnqsNgBwYz9_q5lB9wnZ-JTfCtBbWlj0,2271
|
2
|
-
pycares/_version.py,sha256=kdfRZje8_zWML_WhF-jokwyqus2C8NoX_hYP4nI4I-M,23
|
3
|
-
pycares/_cares.abi3.so,sha256=o2FFY8T3VLJwJjcd2Vb4Ut8r4mkK4N1QFbb5N0a71p4,327888
|
4
|
-
pycares/__init__.py,sha256=jmzqkXU8PreF41RtzXG52wVprx9vLCzj34xgE1iQ7h8,32681
|
5
|
-
pycares/utils.py,sha256=BxfZ13HfupyVOz2pTYsdr9QNGCAINjD7XNHgdRFhwlY,1291
|
6
|
-
pycares/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
pycares/__main__.py,sha256=zagpuBSUv3MqoMvzby_5xzSW9ky76DAxWM8b5SMiJrw,2796
|
8
|
-
pycares-4.7.0.dist-info/RECORD,,
|
9
|
-
pycares-4.7.0.dist-info/WHEEL,sha256=25c7jxmzya5ORDpqz9NT-OlZSbKgFRmwRgILUSJcHB0,107
|
10
|
-
pycares-4.7.0.dist-info/top_level.txt,sha256=nIeo7L2XUVBQZO2YE6pH7tlKaBWTfmmRcXbqe_NWYCw,15
|
11
|
-
pycares-4.7.0.dist-info/METADATA,sha256=vI1wVMngaY0dM1D3ktihnPys7qXZtYhQq0glOwMbF2o,4338
|
12
|
-
pycares-4.7.0.dist-info/licenses/LICENSE,sha256=QnuzpTcgJKJuLCkLJDglErzHyhI0McTdF43WE77qfu4,1070
|
File without changes
|
File without changes
|