| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | # Copyright 2019 miruka | 
					
						
							|  |  |  | # This file is part of harmonyqml, licensed under GPLv3. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import socket | 
					
						
							|  |  |  | import ssl | 
					
						
							|  |  |  | import time | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  | from threading import Lock | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | from typing import Callable, Optional, Tuple | 
					
						
							|  |  |  | from uuid import UUID | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  | import nio | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | OptSock        = Optional[ssl.SSLSocket] | 
					
						
							|  |  |  | NioRequestFunc = Callable[..., Tuple[UUID, bytes]] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class NioErrorResponse(Exception): | 
					
						
							| 
									
										
										
										
											2019-04-28 01:00:59 -04:00
										 |  |  |     def __init__(self, response: nio.ErrorResponse) -> None: | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  |         self.response = response | 
					
						
							|  |  |  |         super().__init__(str(response)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 16:52:12 -04:00
										 |  |  | class RetrySleeper: | 
					
						
							|  |  |  |     def __init__(self) -> None: | 
					
						
							|  |  |  |         self.current_time: float = 0 | 
					
						
							|  |  |  |         self.tries:        int = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sleep(self, max_time: float) -> None: | 
					
						
							|  |  |  |         self.current_time = max( | 
					
						
							|  |  |  |             0, min((max_time / 10) * (2 ^ (self.tries - 1)), max_time) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         time.sleep(self.current_time) | 
					
						
							|  |  |  |         self.tries += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | class NetworkManager: | 
					
						
							| 
									
										
										
										
											2019-04-12 04:48:00 -04:00
										 |  |  |     http_retry_codes = {408, 429, 500, 502, 503, 504, 507} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |     def __init__(self, host: str, port: int, nio_client: nio.client.HttpClient | 
					
						
							|  |  |  |                 ) -> None: | 
					
						
							|  |  |  |         self.host = host | 
					
						
							|  |  |  |         self.port = port | 
					
						
							|  |  |  |         self.nio  = nio_client | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  |         self._ssl_context: ssl.SSLContext = ssl.create_default_context() | 
					
						
							|  |  |  |         self._ssl_session: Optional[ssl.SSLSession] = None | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |         self._lock: Lock = Lock() | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _get_socket(self) -> ssl.SSLSocket: | 
					
						
							|  |  |  |         sock = self._ssl_context.wrap_socket(  # type: ignore | 
					
						
							| 
									
										
										
										
											2019-04-19 17:04:53 -04:00
										 |  |  |             socket.create_connection((self.host, self.port), timeout=16), | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |             server_hostname = self.host, | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  |             session         = self._ssl_session, | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         self._ssl_session = self._ssl_session or sock.session | 
					
						
							|  |  |  |         return sock | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |     def _close_socket(sock: Optional[socket.socket]) -> None: | 
					
						
							|  |  |  |         if not sock: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 23:01:26 -04:00
										 |  |  |         try: | 
					
						
							|  |  |  |             sock.shutdown(how=socket.SHUT_RDWR) | 
					
						
							|  |  |  |         except OSError:  # Already closer by server | 
					
						
							|  |  |  |             pass | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  |         sock.close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |     def http_disconnect(self) -> None: | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2019-04-19 17:04:53 -04:00
										 |  |  |             self.write(self.nio.disconnect()) | 
					
						
							| 
									
										
										
										
											2019-04-28 01:00:59 -04:00
										 |  |  |         except (OSError, nio.ProtocolError): | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |             pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-28 01:00:59 -04:00
										 |  |  |     def read(self, with_sock: OptSock = None) -> nio.Response: | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  |         sock = with_sock or self._get_socket() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         response = None | 
					
						
							|  |  |  |         while not response: | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |             left_to_send = self.nio.data_to_send() | 
					
						
							|  |  |  |             if left_to_send: | 
					
						
							|  |  |  |                 self.write(left_to_send, sock) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.nio.receive(sock.recv(4096)) | 
					
						
							|  |  |  |             response = self.nio.next_response() | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-28 01:00:59 -04:00
										 |  |  |         if isinstance(response, nio.ErrorResponse): | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  |             raise NioErrorResponse(response) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not with_sock: | 
					
						
							|  |  |  |             self._close_socket(sock) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return response | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def write(self, data: bytes, with_sock: OptSock = None) -> None: | 
					
						
							|  |  |  |         sock = with_sock or self._get_socket() | 
					
						
							|  |  |  |         sock.sendall(data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not with_sock: | 
					
						
							|  |  |  |             self._close_socket(sock) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |     def talk(self, | 
					
						
							|  |  |  |              nio_func: NioRequestFunc, | 
					
						
							|  |  |  |              *args, | 
					
						
							| 
									
										
										
										
											2019-04-28 01:00:59 -04:00
										 |  |  |              **kwargs) -> nio.Response: | 
					
						
							| 
									
										
										
										
											2019-04-26 16:02:20 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |         with self._lock: | 
					
						
							| 
									
										
										
										
											2019-04-19 16:52:12 -04:00
										 |  |  |             retry = RetrySleeper() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |             while True: | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |                 sock = None | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |                 try: | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |                     sock = self._get_socket() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     if not self.nio.connection: | 
					
						
							|  |  |  |                         # Establish HTTP protocol connection: | 
					
						
							|  |  |  |                         self.write(self.nio.connect(), sock) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     to_send = nio_func(*args, **kwargs)[1] | 
					
						
							| 
									
										
										
										
											2019-04-17 21:08:32 -04:00
										 |  |  |                     self.write(to_send, sock) | 
					
						
							|  |  |  |                     response = self.read(sock) | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-28 01:00:59 -04:00
										 |  |  |                 except (OSError, nio.RemoteTransportError) as err: | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |                     self._close_socket(sock) | 
					
						
							|  |  |  |                     self.http_disconnect() | 
					
						
							| 
									
										
										
										
											2019-04-19 16:52:12 -04:00
										 |  |  |                     retry.sleep(max_time=2) | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |                 except NioErrorResponse as err: | 
					
						
							|  |  |  |                     logging.error("Nio response error for %s: %s", | 
					
						
							|  |  |  |                                   nio_func.__name__, err) | 
					
						
							|  |  |  |                     self._close_socket(sock) | 
					
						
							| 
									
										
										
										
											2019-04-11 13:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-21 07:04:42 -04:00
										 |  |  |                     if err.response.status_code not in self.http_retry_codes: | 
					
						
							|  |  |  |                         raise | 
					
						
							| 
									
										
										
										
											2019-04-12 04:48:00 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 16:52:12 -04:00
										 |  |  |                     retry.sleep(max_time=10) | 
					
						
							| 
									
										
										
										
											2019-04-12 04:48:00 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-19 16:15:21 -04:00
										 |  |  |                 else: | 
					
						
							|  |  |  |                     return response |