|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the JSON based client base class. |
|
8 """ |
|
9 |
|
10 import io |
|
11 import sys |
|
12 import socket |
|
13 import select |
|
14 import traceback |
|
15 import json |
|
16 import contextlib |
|
17 |
|
18 |
|
19 class EricJsonClient: |
|
20 """ |
|
21 Class implementing a JSON based client base class. |
|
22 """ |
|
23 def __init__(self, host, port, idString=""): |
|
24 """ |
|
25 Constructor |
|
26 |
|
27 @param host IP address the background service is listening |
|
28 @type str |
|
29 @param port port of the background service |
|
30 @type int |
|
31 @param idString assigned client id to be sent back to the server in |
|
32 order to identify the connection |
|
33 @type str |
|
34 """ |
|
35 self.__connection = socket.create_connection((host, port)) |
|
36 if idString: |
|
37 reply = idString + '\n' |
|
38 self.__connection.sendall(reply.encode('utf8', 'backslashreplace')) |
|
39 |
|
40 def sendJson(self, command, params): |
|
41 """ |
|
42 Public method to send a single refactoring command to the server. |
|
43 |
|
44 @param command command name to be sent |
|
45 @type str |
|
46 @param params dictionary of named parameters for the command |
|
47 @type dict |
|
48 """ |
|
49 commandDict = { |
|
50 "jsonrpc": "2.0", |
|
51 "method": command, |
|
52 "params": params, |
|
53 } |
|
54 cmd = json.dumps(commandDict) + '\n' |
|
55 self.__connection.sendall(cmd.encode('utf8', 'backslashreplace')) |
|
56 |
|
57 def __receiveJson(self): |
|
58 """ |
|
59 Private method to receive a JSON encode command and data from the |
|
60 server. |
|
61 |
|
62 @return tuple containing the received command and a dictionary |
|
63 containing the associated data |
|
64 @rtype tuple of (str, dict) |
|
65 """ |
|
66 # step 1: receive the data |
|
67 # The JSON RPC string is prefixed by a 9 character long length field. |
|
68 length = self.__connection.recv(9) |
|
69 if len(length) < 9: |
|
70 # invalid length string received |
|
71 return None, None |
|
72 |
|
73 length = int(length) |
|
74 data = b'' |
|
75 while len(data) < length: |
|
76 newData = self.__connection.recv(length - len(data)) |
|
77 if not newData: |
|
78 return None, None |
|
79 |
|
80 data += newData |
|
81 |
|
82 # step 2: decode and convert the data |
|
83 line = data.decode( |
|
84 'utf8', 'backslashreplace') |
|
85 try: |
|
86 commandDict = json.loads(line.strip()) |
|
87 except (TypeError, ValueError) as err: |
|
88 self.sendJson("ClientException", { |
|
89 "ExceptionType": "ProtocolError", |
|
90 "ExceptionValue": str(err), |
|
91 "ProtocolData": line.strip(), |
|
92 }) |
|
93 return None, None |
|
94 |
|
95 method = commandDict["method"] |
|
96 params = commandDict["params"] |
|
97 |
|
98 return method, params |
|
99 |
|
100 def handleCall(self, method, params): |
|
101 """ |
|
102 Public method to handle a method call from the server. |
|
103 |
|
104 Note: This is an empty implementation that must be overridden in |
|
105 derived classes. |
|
106 |
|
107 @param method requested method name |
|
108 @type str |
|
109 @param params dictionary with method specific parameters |
|
110 @type dict |
|
111 """ |
|
112 pass |
|
113 |
|
114 def run(self): |
|
115 """ |
|
116 Public method implementing the main loop of the client. |
|
117 """ |
|
118 try: |
|
119 selectErrors = 0 |
|
120 while selectErrors <= 10: # selected arbitrarily |
|
121 try: |
|
122 rrdy, wrdy, xrdy = select.select( |
|
123 [self.__connection], [], []) |
|
124 |
|
125 # Just waiting for self.__connection. Therefor no check |
|
126 # needed. |
|
127 method, params = self.__receiveJson() |
|
128 if method is None: |
|
129 selectErrors += 1 |
|
130 elif method == "Exit": |
|
131 break |
|
132 else: |
|
133 self.handleCall(method, params) |
|
134 |
|
135 # reset select errors |
|
136 selectErrors = 0 |
|
137 |
|
138 except (select.error, KeyboardInterrupt, socket.error): |
|
139 selectErrors += 1 |
|
140 |
|
141 except Exception: |
|
142 exctype, excval, exctb = sys.exc_info() |
|
143 tbinfofile = io.StringIO() |
|
144 traceback.print_tb(exctb, None, tbinfofile) |
|
145 tbinfofile.seek(0) |
|
146 tbinfo = tbinfofile.read() |
|
147 del exctb |
|
148 self.sendJson("ClientException", { |
|
149 "ExceptionType": str(exctype), |
|
150 "ExceptionValue": str(excval), |
|
151 "Traceback": tbinfo, |
|
152 }) |
|
153 |
|
154 # Give time to process latest response on server side |
|
155 with contextlib.suppress(socket.error, OSError): |
|
156 self.__connection.shutdown(socket.SHUT_RDWR) |
|
157 self.__connection.close() |
|
158 |
|
159 def poll(self, waitMethod=""): |
|
160 """ |
|
161 Public method to check and receive one message (if available). |
|
162 |
|
163 @param waitMethod name of a method to wait for |
|
164 @type str |
|
165 @return dictionary containing the data of the waited for method |
|
166 @rtype dict |
|
167 """ |
|
168 try: |
|
169 if waitMethod: |
|
170 rrdy, wrdy, xrdy = select.select( |
|
171 [self.__connection], [], []) |
|
172 else: |
|
173 rrdy, wrdy, xrdy = select.select( |
|
174 [self.__connection], [], [], 0) |
|
175 |
|
176 if self.__connection in rrdy: |
|
177 method, params = self.__receiveJson() |
|
178 if method is not None: |
|
179 if method == "Exit": |
|
180 self.__exitClient = True |
|
181 elif method == waitMethod: |
|
182 return params |
|
183 else: |
|
184 self.handleCall(method, params) |
|
185 |
|
186 except (select.error, KeyboardInterrupt, socket.error): |
|
187 # just ignore these |
|
188 pass |
|
189 |
|
190 except Exception: |
|
191 exctype, excval, exctb = sys.exc_info() |
|
192 tbinfofile = io.StringIO() |
|
193 traceback.print_tb(exctb, None, tbinfofile) |
|
194 tbinfofile.seek(0) |
|
195 tbinfo = tbinfofile.read() |
|
196 del exctb |
|
197 self.sendJson("ClientException", { |
|
198 "ExceptionType": str(exctype), |
|
199 "ExceptionValue": str(excval), |
|
200 "Traceback": tbinfo, |
|
201 }) |
|
202 |
|
203 return None |