@@ -780,7 +780,7 @@ class HAPServer(socketserver.ThreadingMixIn,
780780 b"Content-Length: "
781781
782782 TIMEOUT_ERRNO_CODES = (errno .ECONNRESET , errno .EPIPE , errno .EHOSTUNREACH ,
783- errno .ETIMEDOUT , errno .EHOSTDOWN )
783+ errno .ETIMEDOUT , errno .EHOSTDOWN , errno . EBADF )
784784
785785 @classmethod
786786 def create_hap_event (cls , bytesdata ):
@@ -820,6 +820,8 @@ def _handle_sock_timeout(self, client_addr, exception):
820820 # NOTE: In python <3.3 socket.timeout is not OSError, hence the above.
821821 # Also, when it is actually an OSError, it MAY not have an errno equal to
822822 # ETIMEDOUT.
823+ logger .debug ("Connection timeout for %s with exception %s" , client_addr , exception )
824+ logger .debug ("Current connections %s" , self .connections )
823825 sock = self .connections .pop (client_addr , None )
824826 if sock is not None :
825827 self ._close_socket (sock )
@@ -834,26 +836,47 @@ def get_request(self):
834836 self .connections [client_addr ] = client_socket
835837 return (client_socket , client_addr )
836838
837- def finish_request (self , sock , client_addr ):
839+ def finish_request (self , request , client_address ):
840+ """Handle the client request.
841+
842+ HAP connections are not closed. Once the client negotiates a session,
843+ the connection is kept open for both incoming and outgoing traffic, including
844+ for sending events.
845+
846+ The client can gracefully close the connection, but in other cases it can just
847+ leave, which will result in a timeout. In either case, we need to remove the
848+ connection from ``self.connections``, because it could also be used for
849+ pushing events to the server.
850+ """
838851 try :
839- self .RequestHandlerClass (sock , client_addr , self , self .accessory_handler )
852+ self .RequestHandlerClass (request , client_address ,
853+ self , self .accessory_handler )
840854 except (OSError , socket .timeout ) as e :
841- self ._handle_sock_timeout (client_addr , e )
842- logger .debug ("Connection timeout" )
855+ self ._handle_sock_timeout (client_address , e )
856+ logger .debug ('Connection timeout' )
857+ finally :
858+ logger .debug ('Cleaning connection to %s' , client_address )
859+ conn_sock = self .connections .pop (client_address , None )
860+ if conn_sock is not None :
861+ self ._close_socket (conn_sock )
843862
844863 def server_close (self ):
845864 """Close all connections."""
846865 logger .info ('Stopping HAP server' )
847- for sock in self .connections .values ():
866+
867+ # When the AccessoryDriver is shutting down, it will stop advertising the
868+ # Accessory on the network before stopping the server. At that point, clients
869+ # can see the Accessory disappearing and could close the connection. This can
870+ # happen while we deal with all connections here so we will get a "changed while
871+ # iterating" exception. To avoid that, make a copy and iterate over it instead.
872+ for sock in list (self .connections .values ()):
848873 self ._close_socket (sock )
849874 self .connections .clear ()
850875 super ().server_close ()
851876
852877 def push_event (self , bytesdata , client_addr ):
853878 """Send an event to the current connection with the provided data.
854879
855- .. note: Sets a timeout of PUSH_EVENT_TIMEOUT for the duration of socket.sendall.
856-
857880 :param bytesdata: The data to send.
858881 :type bytesdata: bytes
859882
@@ -865,12 +888,14 @@ def push_event(self, bytesdata, client_addr):
865888 """
866889 client_socket = self .connections .get (client_addr )
867890 if client_socket is None :
891+ logger .debug ('No socket for %s' , client_addr )
868892 return False
869893 data = self .create_hap_event (bytesdata )
870894 try :
871895 client_socket .sendall (data )
872896 return True
873897 except (OSError , socket .timeout ) as e :
898+ logger .debug ('exception %s for %s in push_event()' , e , client_addr )
874899 self ._handle_sock_timeout (client_addr , e )
875900 return False
876901
0 commit comments