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 CHANGED
@@ -11,10 +11,13 @@ from ._version import __version__
11
11
 
12
12
  import socket
13
13
  import math
14
- import functools
15
- import sys
14
+ import threading
15
+ import time
16
+ import weakref
16
17
  from collections.abc import Callable, Iterable
17
- from typing import Any, Optional, Union
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
- _global_set = set()
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
- self._channel = _ffi.gc(channel, lambda x: _lib.ares_destroy(x[0]))
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 = _ffi.new_handle(callback)
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 = _ffi.new_handle(callback)
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 = _ffi.new_handle(callback)
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 = _ffi.new_handle((callback, query_type))
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 = _ffi.new_handle(callback)
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.7.0'
2
+ __version__ = '4.9.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycares
3
- Version: 4.7.0
3
+ Version: 4.9.0
4
4
  Summary: Python interface for c-ares
5
5
  Home-page: http://github.com/saghul/pycares
6
6
  Author: Saúl Ibarra Corretgé
@@ -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
@@ -1,5 +1,6 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp39-cp39-macosx_11_0_arm64
5
+ Generator: delocate 0.13.0
5
6
 
@@ -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