Hide keyboard shortcuts

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""" 

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

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

6""" 

7 

8import telnetlib 

9from time import sleep 

10from typing import Optional 

11 

12from .base import CommunicationProtocol 

13from ..configuration import configdataclass 

14from ..utils.typing import Number 

15 

16 

17class TelnetError(Exception): 

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

19 

20 

21@configdataclass 

22class 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 

66class 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