Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\comm\telnet.py: 82%

# Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\comm\telnet.py : 82%

## 79 statements

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET

2#

3"""

4 Communication protocol for telnet. Makes use of the `telnetlib

5<https://docs.python.org/3/library/telnetlib.html>`_ library.

6"""

7

8 import telnetlib

9 from time import sleep

10 from typing import Optional

11

12 from .base import CommunicationProtocol

13 from ..configuration import configdataclass

14 from ..utils.typing import Number

15

16

17 class TelnetError(Exception):

18 """Telnet communication related errors."""

19

20

21@configdataclass

22 class TelnetCommunicationConfig:

23 """

24 Configuration dataclass for :class:`TelnetCommunication`.

25 """

26

27 #: Host to connect to

28 #: can be ``localhost`` or

29 host: Optional[str] = None

30

31 #: Port at which the host is listening

32 port: int = 0

33

34 #: The terminator character. Typically this is ``b'\r\n'`` or ``b'\n'``, but can

35 #: also be ``b'\r'`` or other combinations.

36 terminator: bytes = b"\r\n"

37

38 #: Standard encoding of the connection. Typically this is ``utf-8``, but can also

39 # be ``latin-1`` or something from here:

40 # https://docs.python.org/3/library/codecs.html#standard-encodings

41 encoding: str = "utf-8"

42

43 #: time to wait between attempts of reading a non-empty text

44 wait_sec_read_text_nonempty: Number = 0.5

45

46 #: default number of attempts to read a non-empty text

47 default_n_attempts_read_text_nonempty: int = 10

48

49 def create_telnet(self) -> Optional[telnetlib.Telnet]:

50 """

51 Create a telnet client

52 :return: Closed Telnet object or None if connection is not possible

53 """

54 if self.host is None:

55 return None

56

57 try:

58 tn = telnetlib.Telnet(host=self.host, port=self.port)

59 except (ConnectionRefusedError, TimeoutError, OSError) as exc:

60 raise TelnetError from exc

61 else:

62 # tn.close()

63 return tn

64

65

66 class TelnetCommunication(CommunicationProtocol):

67 """

68 Implements the Communication Protocol for telnet.

69 """

70

71 def __init__(self, configuration):

72 """

73 Constructor for TelnetCommuniation.

74 """

75

76 super().__init__(configuration)

77

78 self._tn: telnetlib.Telnet = self.config.create_telnet()

79 self._is_open: bool = self._tn is not None

80

81 @property

82 def is_open(self) -> bool:

83 """

84 Is the connection open?

85 :return:

86 """

87 return self._is_open

88

89 def open(self):

90 """

91 Open the telnet connection unless it is not yet opened.

92 """

93 if self.is_open:

94 return

95

96 with self.access_lock:

97 try:

98 self._tn.open(self._tn.host, self._tn.port)

99 except (ConnectionRefusedError, TimeoutError, OSError) as exc:

100 raise TelnetError from exc

101 self._is_open = True

102

103 def close(self):

104 """

105 Close the telnet connection unless it is not closed.

106 """

107 if not self.is_open:

108 return

109

110 with self.access_lock:

111 self._tn.close()

112 self._is_open = False

113

114 @staticmethod

115 def config_cls():

116 return TelnetCommunicationConfig

117

118 def write_bytes(self, data: bytes):

119 """

120 Write the data to the telnet connection.

121 :param data: Data to be sent.

122 :raises TelnetError: when connection is not open

123 :raises TelnetError: re-raises an Error during the communication

124 """

125

126 if not self.is_open:

127 raise TelnetError("The Telnet connection is not open.")

128

129 with self.access_lock:

130 self._tn.write(data.rstrip() + self.config.terminator)

131

132 def write_text(self, text: str):

133 """

134 Write text to the telnet connection.

135 :param text: Text to be sent.

136 :raises TelnetError: when connection is not open

137 :raises TelnetError: re-raises an Error during the communication

138 """

139

140 self.write_bytes(text.encode(self.config.encoding))

141

142 def read_bytes(self) -> bytes:

143 """

144 Read data as bytes from the telnet connection.

145 :return: data from telnet connection

146 """

147

148 return self._tn.read_very_eager()

149

150 def read_text(self, encoding: Optional[str] = None) -> str:

151 """

152 Read text as str from the telnet connection.

153

154 :param encoding: Encoding to decode the bytes into a String

155 :return: String read from the telnet connection

156 """

157 if encoding is None:

158 encoding = self.config.encoding

159 try:

160 return self.read_bytes().decode(encoding)

161 except LookupError as exc:

162 raise TelnetError("Wrong encoding? This encoding is not known...") from exc

163 except UnicodeDecodeError as exc:

164 raise TelnetError("Wrong encoding?") from exc

165

166 def read_text_nonempty(

167 self, n_attempts_max: Optional[int] = None, encoding: Optional[str] = None

168 ) -> str:

169 """

170

171 :param encoding:

172 :param n_attempts_max:

173 :return:

174 """

175 answer = self.read_text(encoding=encoding).strip()

176 n_attempts_left = (

177 (n_attempts_max or self.config.default_n_attempts_read_text_nonempty)

178 if self.is_open

179 else 0

180 )

181 attempt_interval_sec = self.config.wait_sec_read_text_nonempty

182 while len(answer) == 0 and n_attempts_left > 0:

183 sleep(attempt_interval_sec)

184 answer = self.read_text(encoding=encoding).strip()

185 n_attempts_left -= 1

186 if answer == "":

187 raise TelnetError("Cannot read a non-empty text.")

188 return answer