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 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]
@@ -82,17 +85,25 @@ class AresError(Exception):
82
85
 
83
86
  # callback helpers
84
87
 
85
- _global_set = set()
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
- self._channel = _ffi.gc(channel, lambda x: _lib.ares_destroy(x[0]))
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 = _ffi.new_handle(callback)
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 = _ffi.new_handle(callback)
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 = _ffi.new_handle(callback)
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 = _ffi.new_handle((callback, query_type))
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 = _ffi.new_handle(callback)
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.8.0'
2
+ __version__ = '4.9.0'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycares
3
- Version: 4.8.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/__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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.3.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-win32
5
5
 
@@ -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,,