Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5ac5b83

Browse files
authored
Merge pull request ikalchev#180 from ikalchev/v2.4.2
Release v2.4.2
2 parents 31d4962 + b4d2f47 commit 5ac5b83

File tree

5 files changed

+89
-10
lines changed

5 files changed

+89
-10
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ Sections
1616
### Developers
1717
-->
1818

19+
## [2.4.2] - 2019-01-04
20+
21+
### Fixed
22+
- Fixed an issue where stopping the `AccessoryDriver` can fail with `RuntimeError('dictionary changed size during iteration')`.
23+
- Fixed an issue where the `HAPServer` can crash when sending events to clients.
24+
25+
### Added
26+
- Tests for `hap_server`.
27+
1928
## [2.4.1] - 2018-11-11
2029

2130
### Fixed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![PyPI version](https://badge.fury.io/py/HAP-python.svg)](https://badge.fury.io/py/HAP-python) [![Build Status](https://travis-ci.org/ikalchev/HAP-python.svg?branch=master)](https://travis-ci.org/ikalchev/HAP-python) [![codecov](https://codecov.io/gh/ikalchev/HAP-python/branch/master/graph/badge.svg)](https://codecov.io/gh/ikalchev/HAP-python) [![Documentation Status](https://readthedocs.org/projects/hap-python/badge/?version=latest)](http://hap-python.readthedocs.io/en/latest/?badge=latest)
1+
[![PyPI version](https://badge.fury.io/py/HAP-python.svg)](https://badge.fury.io/py/HAP-python) [![Build Status](https://travis-ci.org/ikalchev/HAP-python.svg?branch=master)](https://travis-ci.org/ikalchev/HAP-python) [![codecov](https://codecov.io/gh/ikalchev/HAP-python/branch/master/graph/badge.svg)](https://codecov.io/gh/ikalchev/HAP-python) [![Documentation Status](https://readthedocs.org/projects/hap-python/badge/?version=latest)](http://hap-python.readthedocs.io/en/latest/?badge=latest) [![Downloads](https://pepy.tech/badge/hap-python)](https://pepy.tech/project/hap-python)
22
# HAP-python
33

44
HomeKit Accessory Protocol implementation in python 3.

pyhap/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""This module contains constants used by other modules."""
22
MAJOR_VERSION = 2
33
MINOR_VERSION = 4
4-
PATCH_VERSION = 1
4+
PATCH_VERSION = 2
55
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
66
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
77
REQUIRED_PYTHON_VER = (3, 5)

pyhap/hap_server.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/test_hap_server.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests for the HAPServer."""
2+
from socket import timeout
3+
from unittest.mock import Mock, MagicMock, patch
4+
5+
import pytest
6+
7+
from pyhap import hap_server
8+
9+
10+
@patch('pyhap.hap_server.HAPServer.server_bind', new=MagicMock())
11+
@patch('pyhap.hap_server.HAPServer.server_activate', new=MagicMock())
12+
def test_finish_request_pops_socket():
13+
"""Test that ``finish_request`` always clears the connection after a request."""
14+
amock = Mock()
15+
client_addr = ('192.168.1.1', 55555)
16+
server_addr = ('', 51826)
17+
18+
# Positive case: The request is handled
19+
server = hap_server.HAPServer(server_addr, amock,
20+
handler_type=lambda *args: MagicMock())
21+
22+
server.connections[client_addr] = amock
23+
server.finish_request(amock, client_addr)
24+
25+
assert len(server.connections) == 0
26+
27+
# Negative case: The request fails with a timeout
28+
def raises(*args):
29+
raise timeout()
30+
server = hap_server.HAPServer(server_addr, amock,
31+
handler_type=raises)
32+
server.connections[client_addr] = amock
33+
server.finish_request(amock, client_addr)
34+
35+
assert len(server.connections) == 0
36+
37+
# Negative case: The request raises some other exception
38+
server = hap_server.HAPServer(server_addr, amock,
39+
handler_type=lambda *args: 1 / 0)
40+
server.connections[client_addr] = amock
41+
42+
with pytest.raises(Exception):
43+
server.finish_request(amock, client_addr)
44+
45+
assert len(server.connections) == 0

0 commit comments

Comments
 (0)