|
13 | 13 | import os |
14 | 14 | import socket |
15 | 15 | import errno |
| 16 | +import struct |
16 | 17 | import threading |
17 | 18 | import time |
18 | 19 | from threading import Lock, RLock |
|
49 | 50 | DEFAULT_HOST = "localhost" |
50 | 51 | DEFAULT_PORT = 8125 |
51 | 52 |
|
| 53 | +# Socket prefixes |
| 54 | +UNIX_ADDRESS_SCHEME = "unix://" |
| 55 | +UNIX_ADDRESS_DATAGRAM_SCHEME = "unixgram://" |
| 56 | +UNIX_ADDRESS_STREAM_SCHEME = "unixstream://" |
| 57 | + |
52 | 58 | # Buffering-related values (in seconds) |
53 | 59 | DEFAULT_BUFFERING_FLUSH_INTERVAL = 0.3 |
54 | 60 | MIN_FLUSH_INTERVAL = 0.0001 |
@@ -488,12 +494,47 @@ def socket_path(self): |
488 | 494 | def socket_path(self, path): |
489 | 495 | with self._socket_lock: |
490 | 496 | self._socket_path = path |
491 | | - if path is None: |
492 | | - self._transport = "udp" |
493 | | - self._max_payload_size = self._max_buffer_len or UDP_OPTIMAL_PAYLOAD_LENGTH |
494 | | - else: |
495 | | - self._transport = "uds" |
496 | | - self._max_payload_size = self._max_buffer_len or UDS_OPTIMAL_PAYLOAD_LENGTH |
| 497 | + |
| 498 | + @property |
| 499 | + def socket(self): |
| 500 | + return self._socket |
| 501 | + |
| 502 | + @socket.setter |
| 503 | + def socket(self, new_socket): |
| 504 | + self._socket = new_socket |
| 505 | + if new_socket: |
| 506 | + try: |
| 507 | + self._socket_kind = new_socket.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) |
| 508 | + if new_socket.family == socket.AF_UNIX: |
| 509 | + if self._socket_kind == socket.SOCK_STREAM: |
| 510 | + self._transport = "uds-stream" |
| 511 | + else: |
| 512 | + self._transport = "uds" |
| 513 | + self._max_payload_size = self._max_buffer_len or UDS_OPTIMAL_PAYLOAD_LENGTH |
| 514 | + else: |
| 515 | + self._transport = "udp" |
| 516 | + self._max_payload_size = self._max_buffer_len or UDP_OPTIMAL_PAYLOAD_LENGTH |
| 517 | + return |
| 518 | + except AttributeError: # _socket can't have a type if it doesn't have sockopts |
| 519 | + log.info("Unexpected socket provided with no support for getsockopt") |
| 520 | + self._socket_kind = None |
| 521 | + # When the socket is None, we use the UDP optimal payload length |
| 522 | + self._max_payload_size = UDP_OPTIMAL_PAYLOAD_LENGTH |
| 523 | + |
| 524 | + @property |
| 525 | + def telemetry_socket(self): |
| 526 | + return self._telemetry_socket |
| 527 | + |
| 528 | + @telemetry_socket.setter |
| 529 | + def telemetry_socket(self, t_socket): |
| 530 | + self._telemetry_socket = t_socket |
| 531 | + if t_socket: |
| 532 | + try: |
| 533 | + self._telemetry_socket_kind = t_socket.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) |
| 534 | + return |
| 535 | + except AttributeError: # _telemetry_socket can't have a kind if it doesn't have sockopts |
| 536 | + log.info("Unexpected telemetry socket provided with no support for getsockopt") |
| 537 | + self._telemetry_socket_kind = None |
497 | 538 |
|
498 | 539 | def enable_background_sender(self, sender_queue_size=0, sender_queue_timeout=0): |
499 | 540 | """ |
@@ -738,11 +779,37 @@ def _ensure_min_send_buffer_size(cls, sock, min_size=MIN_SEND_BUFFER_SIZE): |
738 | 779 |
|
739 | 780 | @classmethod |
740 | 781 | def _get_uds_socket(cls, socket_path, timeout): |
741 | | - sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) |
742 | | - sock.settimeout(timeout) |
743 | | - cls._ensure_min_send_buffer_size(sock) |
744 | | - sock.connect(socket_path) |
745 | | - return sock |
| 782 | + valid_socket_kinds = [socket.SOCK_DGRAM, socket.SOCK_STREAM] |
| 783 | + if socket_path.startswith(UNIX_ADDRESS_DATAGRAM_SCHEME): |
| 784 | + valid_socket_kinds = [socket.SOCK_DGRAM] |
| 785 | + socket_path = socket_path[len(UNIX_ADDRESS_DATAGRAM_SCHEME):] |
| 786 | + elif socket_path.startswith(UNIX_ADDRESS_STREAM_SCHEME): |
| 787 | + valid_socket_kinds = [socket.SOCK_STREAM] |
| 788 | + socket_path = socket_path[len(UNIX_ADDRESS_STREAM_SCHEME):] |
| 789 | + elif socket_path.startswith(UNIX_ADDRESS_SCHEME): |
| 790 | + socket_path = socket_path[len(UNIX_ADDRESS_SCHEME):] |
| 791 | + |
| 792 | + last_error = ValueError("Invalid socket path") |
| 793 | + for socket_kind in valid_socket_kinds: |
| 794 | + # py2 stores socket kinds differently than py3, determine the name independently from version |
| 795 | + sk_name = {socket.SOCK_STREAM: "stream", socket.SOCK_DGRAM: "datagram"}[socket_kind] |
| 796 | + |
| 797 | + try: |
| 798 | + sock = socket.socket(socket.AF_UNIX, socket_kind) |
| 799 | + sock.settimeout(timeout) |
| 800 | + cls._ensure_min_send_buffer_size(sock) |
| 801 | + sock.connect(socket_path) |
| 802 | + log.debug("Connected to socket %s with kind %s", socket_path, sk_name) |
| 803 | + return sock |
| 804 | + except Exception as e: |
| 805 | + if sock is not None: |
| 806 | + sock.close() |
| 807 | + log.debug("Failed to connect to %s with kind %s: %s", socket_path, sk_name, e) |
| 808 | + if e.errno == errno.EPROTOTYPE: |
| 809 | + last_error = e |
| 810 | + continue |
| 811 | + raise e |
| 812 | + raise last_error |
746 | 813 |
|
747 | 814 | @classmethod |
748 | 815 | def _get_udp_socket(cls, host, port, timeout): |
@@ -1243,14 +1310,23 @@ def _xmit_packet_with_telemetry(self, packet): |
1243 | 1310 | self.packets_dropped_writer += 1 |
1244 | 1311 |
|
1245 | 1312 | def _xmit_packet(self, packet, is_telemetry): |
| 1313 | + socket_kind = None |
1246 | 1314 | try: |
1247 | 1315 | if is_telemetry and self._dedicated_telemetry_destination(): |
1248 | 1316 | mysocket = self.telemetry_socket or self.get_socket(telemetry=True) |
| 1317 | + socket_kind = self._telemetry_socket_kind |
1249 | 1318 | else: |
1250 | 1319 | # If set, use socket directly |
1251 | 1320 | mysocket = self.socket or self.get_socket() |
| 1321 | + socket_kind = self._socket_kind |
1252 | 1322 |
|
1253 | | - mysocket.send(packet.encode(self.encoding)) |
| 1323 | + encoded_packet = packet.encode(self.encoding) |
| 1324 | + if socket_kind == socket.SOCK_STREAM: |
| 1325 | + with self._socket_lock: |
| 1326 | + mysocket.sendall(struct.pack('<I', len(encoded_packet))) |
| 1327 | + mysocket.sendall(encoded_packet) |
| 1328 | + else: |
| 1329 | + mysocket.send(encoded_packet) |
1254 | 1330 |
|
1255 | 1331 | if not is_telemetry and self._telemetry: |
1256 | 1332 | self.packets_sent += 1 |
@@ -1283,13 +1359,19 @@ def _xmit_packet(self, packet, is_telemetry): |
1283 | 1359 | ) |
1284 | 1360 | self.close_socket() |
1285 | 1361 | except Exception as exc: |
1286 | | - print("Unexpected error: %s", exc) |
| 1362 | + print("Unexpected error: ", exc) |
1287 | 1363 | log.error("Unexpected error: %s", str(exc)) |
1288 | 1364 |
|
1289 | 1365 | if not is_telemetry and self._telemetry: |
1290 | 1366 | self.bytes_dropped_writer += len(packet) |
1291 | 1367 | self.packets_dropped_writer += 1 |
1292 | 1368 |
|
| 1369 | + # if in stream mode we need to shut down the socket; we can't recover from a |
| 1370 | + # partial send |
| 1371 | + if socket_kind == socket.SOCK_STREAM: |
| 1372 | + log.debug("Confirming socket closure after error streaming") |
| 1373 | + self.close_socket() |
| 1374 | + |
1293 | 1375 | return False |
1294 | 1376 |
|
1295 | 1377 | def _send_to_buffer(self, packet): |
|
0 commit comments