/core/script/cli/stream.py

https://github.com/nocproject/noc · Python · 151 lines · 98 code · 13 blank · 40 comment · 20 complexity · 4f10f0a73bbc6979955852b16cec622b MD5 · raw file

  1. # ----------------------------------------------------------------------
  2. # <describe module here>
  3. # ----------------------------------------------------------------------
  4. # Copyright (C) 2007-2020 The NOC Project
  5. # See LICENSE for details
  6. # ----------------------------------------------------------------------
  7. # Python modules
  8. import socket
  9. import asyncio
  10. import contextlib
  11. from typing import Optional
  12. from .base import BaseCLI
  13. class BaseStream(object):
  14. default_port = 23
  15. # compiled capabilities
  16. HAS_TCP_KEEPALIVE = hasattr(socket, "SO_KEEPALIVE")
  17. HAS_TCP_KEEPIDLE = hasattr(socket, "TCP_KEEPIDLE")
  18. HAS_TCP_KEEPINTVL = hasattr(socket, "TCP_KEEPINTVL")
  19. HAS_TCP_KEEPCNT = hasattr(socket, "TCP_KEEPCNT")
  20. HAS_TCP_NODELAY = hasattr(socket, "TCP_NODELAY")
  21. # Time until sending first keepalive probe
  22. KEEP_IDLE = 10
  23. # Keepalive packets interval
  24. KEEP_INTVL = 10
  25. # Terminate connection after N keepalive failures
  26. KEEP_CNT = 3
  27. def __init__(self, cli: BaseCLI):
  28. self._timeout: Optional[float] = None
  29. self.logger = cli.logger
  30. self.tos = cli.tos
  31. self.socket: Optional[socket.socket] = None
  32. async def connect(self, address: str, port: Optional[int] = None):
  33. """
  34. Process connection sequence
  35. :param address:
  36. :param port:
  37. :return:
  38. """
  39. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  40. if self.tos:
  41. self.logger.debug("Setting IP ToS to %d", self.tos)
  42. self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, self.tos)
  43. if self.HAS_TCP_NODELAY:
  44. self.logger.info("Setting TCP NODELAY")
  45. self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  46. if self.HAS_TCP_KEEPALIVE:
  47. self.logger.info("Settings KEEPALIVE")
  48. self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
  49. if self.HAS_TCP_KEEPIDLE:
  50. self.logger.info("Setting TCP KEEPIDLE to %d", self.KEEP_IDLE)
  51. self.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, self.KEEP_IDLE)
  52. if self.HAS_TCP_KEEPINTVL:
  53. self.logger.info("Setting TCP KEEPINTVL to %d", self.KEEP_INTVL)
  54. self.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, self.KEEP_INTVL)
  55. if self.HAS_TCP_KEEPCNT:
  56. self.logger.info("Setting TCP KEEPCNT to %d", self.KEEP_CNT)
  57. self.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, self.KEEP_CNT)
  58. self.socket.setblocking(False)
  59. loop = asyncio.get_running_loop()
  60. try:
  61. await asyncio.wait_for(
  62. loop.sock_connect(self.socket, (address, port or self.default_port)), self._timeout
  63. )
  64. except OSError:
  65. raise ConnectionRefusedError
  66. async def startup(self):
  67. """
  68. Setup connection after startup
  69. :return:
  70. """
  71. async def wait_for_read(self):
  72. """
  73. Wait until data available for read
  74. :return:
  75. """
  76. if not self.socket:
  77. return
  78. loop = asyncio.get_running_loop()
  79. read_ev = asyncio.Event()
  80. fileno = self.socket.fileno()
  81. loop.add_reader(fileno, read_ev.set)
  82. try:
  83. await asyncio.wait_for(read_ev.wait(), self._timeout)
  84. finally:
  85. loop.remove_reader(fileno)
  86. async def wait_for_write(self):
  87. """
  88. Wait until socket will be available for write
  89. :return:
  90. """
  91. if not self.socket:
  92. return
  93. loop = asyncio.get_running_loop()
  94. write_ev = asyncio.Event()
  95. fileno = self.socket.fileno()
  96. loop.add_writer(fileno, write_ev.set)
  97. try:
  98. await asyncio.wait_for(write_ev.wait(), self._timeout)
  99. finally:
  100. loop.remove_writer(fileno)
  101. async def read(self, n: int) -> bytes:
  102. """
  103. Read up to n bytes from socket.
  104. Return empty bytes on EOF
  105. :param n:
  106. :return:
  107. """
  108. await self.wait_for_read()
  109. try:
  110. return self.socket.recv(n)
  111. except ConnectionResetError:
  112. self.logger.debug("Connection reset")
  113. raise asyncio.TimeoutError
  114. async def write(self, data: bytes):
  115. """
  116. Write data to socket
  117. :param data:
  118. :return:
  119. """
  120. while data:
  121. await self.wait_for_write()
  122. try:
  123. sent = self.socket.send(data)
  124. except OSError as e:
  125. self.logger.debug("Failed to write: %s", e)
  126. raise asyncio.TimeoutError()
  127. data = data[sent:]
  128. def close(self):
  129. self.socket.close()
  130. self.socket = None
  131. def set_timeout(self, timeout: Optional[float] = None):
  132. self._timeout = timeout
  133. @contextlib.contextmanager
  134. def timeout(self, timeout: Optional[float] = None):
  135. old_timeout = self.timeout
  136. self.set_timeout(timeout)
  137. yield
  138. self.set_timeout(old_timeout)