1010from __future__ import annotations
1111
1212import abc
13- from dataclasses import dataclass
1413import errno
1514import json
1615import logging
17- import select
16+ import selectors
1817import socket
1918import ssl
2019import threading
2120import time
2221from collections import defaultdict
22+ from dataclasses import dataclass
2323from struct import pack , unpack
2424
2525import zeroconf
2626
27- from .controllers import CallbackType , BaseController
27+ from .const import MESSAGE_TYPE , REQUEST_ID , SESSION_ID
28+ from .controllers import BaseController , CallbackType
2829from .controllers .media import MediaController
2930from .controllers .receiver import CastStatus , CastStatusListener , ReceiverController
30- from .const import MESSAGE_TYPE , REQUEST_ID , SESSION_ID
3131from .dial import get_host_from_service
3232from .error import (
3333 ChromecastConnectionError ,
3434 ControllerNotRegistered ,
35- UnsupportedNamespace ,
3635 NotConnected ,
3736 PyChromecastStopped ,
37+ UnsupportedNamespace ,
3838)
3939
4040# pylint: disable-next=no-name-in-module
6464CONNECTION_STATUS_FAILED_RESOLVE = "FAILED_RESOLVE"
6565# The socket connection was lost and needs to be retried
6666CONNECTION_STATUS_LOST = "LOST"
67- # Check for select poll method
68- SELECT_HAS_POLL = hasattr (select , "poll" )
6967
7068HB_PING_TIME = 10
7169HB_PONG_TIME = 10
72- POLL_TIME_BLOCKING = 5.0
73- POLL_TIME_NON_BLOCKING = 0.01
7470TIMEOUT_TIME = 30.0
7571RETRY_TIME = 5.0
7672
@@ -215,6 +211,11 @@ def __init__(
215211 self .connecting = True
216212 self .first_connection = True
217213 self .socket : socket .socket | ssl .SSLSocket | None = None
214+ self .selector = selectors .DefaultSelector ()
215+ self .wakeup_selector_key = self .selector .register (
216+ self .socketpair [0 ], selectors .EVENT_READ
217+ )
218+ self .remote_selector_key : selectors .SelectorKey | None = None
218219
219220 # dict mapping namespace on Controller objects
220221 self ._handlers : dict [str , set [BaseController ]] = defaultdict (set )
@@ -238,8 +239,10 @@ def initialize_connection( # pylint:disable=too-many-statements, too-many-branc
238239 tries = self .tries
239240
240241 if self .socket is not None :
242+ self .selector .unregister (self .socket )
241243 self .socket .close ()
242244 self .socket = None
245+ self .remote_selector_key = None
243246
244247 # Make sure nobody is blocking.
245248 for callback_function in self ._request_callbacks .values ():
@@ -288,10 +291,15 @@ def mdns_backoff(
288291 try :
289292 if self .socket is not None :
290293 # If we retry connecting, we need to clean up the socket again
291- self .socket .close () # type: ignore[unreachable]
294+ self .selector .unregister (self .socket ) # type: ignore[unreachable]
295+ self .socket .close ()
292296 self .socket = None
297+ self .remote_selector_key = None
293298
294299 self .socket = new_socket ()
300+ self .remote_selector_key = self .selector .register (
301+ self .socket , selectors .EVENT_READ
302+ )
295303 self .socket .settimeout (self .timeout )
296304 self ._report_connection_status (
297305 ConnectionStatus (
@@ -539,7 +547,7 @@ def run(self) -> None:
539547 self .logger .debug ("Thread started..." )
540548 while not self .stop .is_set ():
541549 try :
542- if self ._run_once (timeout = POLL_TIME_BLOCKING ) == 1 :
550+ if self ._run_once () == 1 :
543551 break
544552 except Exception : # pylint: disable=broad-except
545553 self ._force_recon = True
@@ -554,7 +562,7 @@ def run(self) -> None:
554562 # Clean up
555563 self ._cleanup ()
556564
557- def _run_once (self , timeout : float = POLL_TIME_NON_BLOCKING ) -> int :
565+ def _run_once (self ) -> int :
558566 """Receive from the socket and handle data."""
559567 # pylint: disable=too-many-branches, too-many-statements, too-many-return-statements
560568
@@ -568,20 +576,8 @@ def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
568576 assert self .socket is not None
569577
570578 # poll the socket, as well as the socketpair to allow us to be interrupted
571- rlist = [self .socket , self .socketpair [0 ]]
572579 try :
573- if SELECT_HAS_POLL is True :
574- # Map file descriptors to socket objects because select.select does not support fd > 1024
575- # https://stackoverflow.com/questions/14250751/how-to-increase-filedescriptors-range-in-python-select
576- fd_to_socket = {rlist_item .fileno (): rlist_item for rlist_item in rlist }
577-
578- poll_obj = select .poll ()
579- for poll_fd in rlist :
580- poll_obj .register (poll_fd , select .POLLIN )
581- poll_result = poll_obj .poll (timeout * 1000 ) # timeout in milliseconds
582- can_read = [fd_to_socket [fd ] for fd , _status in poll_result ]
583- else :
584- can_read , _ , _ = select .select (rlist , [], [], timeout )
580+ ready = self .selector .select ()
585581 except (ValueError , OSError ) as exc :
586582 self .logger .error (
587583 "[%s(%s):%s] Error in select call: %s" ,
@@ -593,9 +589,10 @@ def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
593589 self ._force_recon = True
594590 return 0
595591
592+ can_read = {key for key , _ in ready }
596593 # read message from chromecast
597594 message = None
598- if self .socket in can_read and not self ._force_recon :
595+ if self .remote_selector_key in can_read and not self ._force_recon :
599596 try :
600597 message = self ._read_message ()
601598 except InterruptLoop as exc :
@@ -631,7 +628,7 @@ def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
631628 else :
632629 data = _dict_from_message_payload (message )
633630
634- if self .socketpair [ 0 ] in can_read :
631+ if self .wakeup_selector_key in can_read :
635632 # Clear the socket's buffer
636633 self .socketpair [0 ].recv (128 )
637634
@@ -776,6 +773,7 @@ def _cleanup(self) -> None:
776773
777774 self .socketpair [0 ].close ()
778775 self .socketpair [1 ].close ()
776+ self .selector .close ()
779777
780778 self .connecting = True
781779
0 commit comments