jsocket 1.9.2__py3-none-any.whl → 1.9.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jsocket/__init__.py +71 -71
- jsocket/jsocket_base.py +211 -191
- jsocket/tserver.py +268 -188
- jsocket-1.9.5.dist-info/LICENSE +201 -0
- {jsocket-1.9.2.dist-info → jsocket-1.9.5.dist-info}/METADATA +5 -2
- jsocket-1.9.5.dist-info/RECORD +8 -0
- {jsocket-1.9.2.dist-info → jsocket-1.9.5.dist-info}/WHEEL +1 -1
- jsocket-1.9.2.dist-info/LICENSE +0 -21
- jsocket-1.9.2.dist-info/RECORD +0 -8
- {jsocket-1.9.2.dist-info → jsocket-1.9.5.dist-info}/top_level.txt +0 -0
jsocket/tserver.py
CHANGED
|
@@ -1,204 +1,284 @@
|
|
|
1
1
|
""" @namespace tserver
|
|
2
|
-
|
|
2
|
+
Contains ThreadedServer, ServerFactoryThread and ServerFactory implementations.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__author__
|
|
6
|
-
__email__
|
|
5
|
+
__author__ = "Christopher Piekarski"
|
|
6
|
+
__email__ = "chris@cpiekarski.com"
|
|
7
7
|
__copyright__= """
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Christopher Piekarski <chris@cpiekarski.com>
|
|
8
|
+
Copyright (C) 2011 by
|
|
9
|
+
Christopher Piekarski <chris@cpiekarski.com>
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(at your option) any later version.
|
|
11
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
you may not use this file except in compliance with the License.
|
|
13
|
+
You may obtain a copy of the License at
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
19
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
20
|
-
GNU General Public License for more details.
|
|
15
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
See the License for the specific language governing permissions and
|
|
21
|
+
limitations under the License.
|
|
22
|
+
"""
|
|
23
|
+
__version__ = "1.0.3"
|
|
25
24
|
|
|
26
|
-
import jsocket.jsocket_base as jsocket_base
|
|
27
25
|
import threading
|
|
28
26
|
import socket
|
|
29
27
|
import time
|
|
30
28
|
import logging
|
|
29
|
+
import abc
|
|
30
|
+
from typing import Optional
|
|
31
|
+
from jsocket import jsocket_base
|
|
31
32
|
|
|
32
33
|
logger = logging.getLogger("jsocket.tserver")
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
35
|
+
|
|
36
|
+
def _response_summary(resp_obj) -> str:
|
|
37
|
+
if isinstance(resp_obj, dict):
|
|
38
|
+
return f"type=dict keys={len(resp_obj)}"
|
|
39
|
+
if isinstance(resp_obj, (list, tuple)):
|
|
40
|
+
return f"type={type(resp_obj).__name__} items={len(resp_obj)}"
|
|
41
|
+
return f"type={type(resp_obj).__name__}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ThreadedServer(threading.Thread, jsocket_base.JsonServer, metaclass=abc.ABCMeta):
|
|
45
|
+
"""Single-threaded server that accepts one connection and processes messages in its thread."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, **kwargs):
|
|
48
|
+
threading.Thread.__init__(self)
|
|
49
|
+
jsocket_base.JsonServer.__init__(self, **kwargs)
|
|
50
|
+
self._is_alive = False
|
|
51
|
+
|
|
52
|
+
@abc.abstractmethod
|
|
53
|
+
def _process_message(self, obj) -> Optional[dict]:
|
|
54
|
+
"""Pure Virtual Method
|
|
55
|
+
|
|
56
|
+
This method is called every time a JSON object is received from a client
|
|
57
|
+
|
|
58
|
+
@param obj JSON "key: value" object received from client
|
|
59
|
+
@retval None or a response object
|
|
60
|
+
"""
|
|
61
|
+
# Return None in the base class to satisfy linters; subclasses should override.
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def _accept_client(self) -> bool:
|
|
65
|
+
"""Accept an incoming connection; return True when a client connects."""
|
|
66
|
+
try:
|
|
67
|
+
self.accept_connection()
|
|
68
|
+
except socket.timeout as e:
|
|
69
|
+
logger.debug("accept timeout on %s:%s: %s", self.address, self.port, e)
|
|
70
|
+
return False
|
|
71
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
72
|
+
# Avoid noisy error logs during normal shutdown/sequencing
|
|
73
|
+
if self._is_alive:
|
|
74
|
+
logger.debug("accept error on %s:%s: %s", self.address, self.port, e)
|
|
75
|
+
return False
|
|
76
|
+
logger.debug("server stopping; accept loop exiting (%s:%s)", self.address, self.port)
|
|
77
|
+
self._is_alive = False
|
|
78
|
+
return False
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def _handle_client_messages(self):
|
|
82
|
+
"""Read, process, and respond to client messages until disconnect."""
|
|
83
|
+
while self._is_alive:
|
|
84
|
+
try:
|
|
85
|
+
obj = self.read_obj()
|
|
86
|
+
resp_obj = self._process_message(obj)
|
|
87
|
+
if resp_obj is not None:
|
|
88
|
+
logger.debug("sending response (%s)", _response_summary(resp_obj))
|
|
89
|
+
self.send_obj(resp_obj)
|
|
90
|
+
except socket.timeout as e:
|
|
91
|
+
logger.debug("read timeout waiting for client data: %s", e)
|
|
92
|
+
continue
|
|
93
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
94
|
+
# Treat client disconnects as normal; keep logs at info/debug
|
|
95
|
+
msg = str(e)
|
|
96
|
+
if isinstance(e, RuntimeError) and 'socket connection broken' in msg:
|
|
97
|
+
logger.info("client connection broken, closing connection")
|
|
98
|
+
else:
|
|
99
|
+
logger.debug("handler error (%s): %s", type(e).__name__, e)
|
|
100
|
+
self._close_connection()
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
def run(self):
|
|
104
|
+
# Ensure the run loop is active even when run() is invoked directly
|
|
105
|
+
# (tests may call run() in a separate thread without invoking start()).
|
|
106
|
+
if not self._is_alive:
|
|
107
|
+
self._is_alive = True
|
|
108
|
+
while self._is_alive:
|
|
109
|
+
if not self._accept_client():
|
|
110
|
+
continue
|
|
111
|
+
self._handle_client_messages()
|
|
112
|
+
# Ensure sockets are cleaned up when the server stops
|
|
113
|
+
try:
|
|
114
|
+
self.close()
|
|
115
|
+
except OSError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def start(self):
|
|
119
|
+
""" Starts the threaded server.
|
|
120
|
+
The newly living know nothing of the dead
|
|
121
|
+
|
|
122
|
+
@retval None
|
|
123
|
+
"""
|
|
124
|
+
self._is_alive = True
|
|
125
|
+
super().start()
|
|
126
|
+
logger.debug("Threaded Server started on %s:%s", self.address, self.port)
|
|
127
|
+
|
|
128
|
+
def stop(self):
|
|
129
|
+
""" Stops the threaded server.
|
|
130
|
+
The life of the dead is in the memory of the living
|
|
131
|
+
|
|
132
|
+
@retval None
|
|
133
|
+
"""
|
|
134
|
+
self._is_alive = False
|
|
135
|
+
logger.debug("Threaded Server stopped on %s:%s", self.address, self.port)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ServerFactoryThread(threading.Thread, jsocket_base.JsonSocket, metaclass=abc.ABCMeta):
|
|
139
|
+
"""Per-connection worker thread used by ServerFactory."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, **kwargs):
|
|
142
|
+
threading.Thread.__init__(self, **kwargs)
|
|
143
|
+
self.socket = None
|
|
144
|
+
self.conn = None
|
|
145
|
+
jsocket_base.JsonSocket.__init__(self, **kwargs)
|
|
146
|
+
self._is_alive = False
|
|
147
|
+
|
|
148
|
+
def swap_socket(self, new_sock):
|
|
149
|
+
""" Swaps the existing socket with a new one. Useful for setting socket after a new connection.
|
|
150
|
+
|
|
151
|
+
@param new_sock socket to replace the existing default jsocket.JsonSocket object
|
|
152
|
+
@retval None
|
|
153
|
+
"""
|
|
154
|
+
self.socket = new_sock
|
|
155
|
+
self.conn = self.socket
|
|
156
|
+
|
|
157
|
+
def run(self):
|
|
158
|
+
""" Should exit when client closes socket conn.
|
|
159
|
+
Can force an exit with force_stop.
|
|
160
|
+
"""
|
|
161
|
+
while self._is_alive:
|
|
162
|
+
try:
|
|
163
|
+
obj = self.read_obj()
|
|
164
|
+
resp_obj = self._process_message(obj)
|
|
165
|
+
if resp_obj is not None:
|
|
166
|
+
logger.debug("sending response (%s)", _response_summary(resp_obj))
|
|
167
|
+
self.send_obj(resp_obj)
|
|
168
|
+
except socket.timeout as e:
|
|
169
|
+
logger.debug("worker read timeout waiting for data: %s", e)
|
|
170
|
+
continue
|
|
171
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
172
|
+
logger.info("client connection broken, closing connection: %s", e)
|
|
173
|
+
self._is_alive = False
|
|
174
|
+
break
|
|
175
|
+
self._close_connection()
|
|
176
|
+
if hasattr(self, "socket"):
|
|
177
|
+
self._close_socket()
|
|
178
|
+
|
|
179
|
+
@abc.abstractmethod
|
|
180
|
+
def _process_message(self, obj) -> Optional[dict]:
|
|
181
|
+
"""Pure Virtual Method - Implementer must define protocol
|
|
182
|
+
|
|
183
|
+
@param obj JSON "key: value" object received from client
|
|
184
|
+
@retval None or a response object
|
|
185
|
+
"""
|
|
186
|
+
# Return None in the base class to satisfy linters; subclasses should override.
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
def start(self):
|
|
190
|
+
""" Starts the factory thread.
|
|
191
|
+
The newly living know nothing of the dead
|
|
192
|
+
|
|
193
|
+
@retval None
|
|
194
|
+
"""
|
|
195
|
+
self._is_alive = True
|
|
196
|
+
super().start()
|
|
197
|
+
logger.debug("ServerFactoryThread started (%s)", self.name)
|
|
198
|
+
|
|
199
|
+
def force_stop(self):
|
|
200
|
+
""" Force stops the factory thread.
|
|
201
|
+
Should exit when client socket is closed under normal conditions.
|
|
202
|
+
The life of the dead is in the memory of the living.
|
|
203
|
+
|
|
204
|
+
@retval None
|
|
205
|
+
"""
|
|
206
|
+
self._is_alive = False
|
|
207
|
+
logger.debug("ServerFactoryThread stopped (%s)", self.name)
|
|
208
|
+
|
|
209
|
+
|
|
153
210
|
class ServerFactory(ThreadedServer):
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
"""Accepts clients and spawns a ServerFactoryThread per connection."""
|
|
212
|
+
def __init__(self, server_thread, **kwargs):
|
|
213
|
+
ThreadedServer.__init__(self, address=kwargs['address'], port=kwargs['port'])
|
|
214
|
+
if not issubclass(server_thread, ServerFactoryThread):
|
|
215
|
+
raise TypeError("serverThread not of type", ServerFactoryThread)
|
|
216
|
+
self._thread_type = server_thread
|
|
217
|
+
self._threads = []
|
|
218
|
+
self._thread_args = kwargs
|
|
219
|
+
self._thread_args.pop('address', None)
|
|
220
|
+
self._thread_args.pop('port', None)
|
|
221
|
+
|
|
222
|
+
def _process_message(self, obj) -> Optional[dict]:
|
|
223
|
+
"""ServerFactory does not process messages itself."""
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def run(self):
|
|
227
|
+
# Ensure the run loop is active even when run() is invoked directly
|
|
228
|
+
# (tests may call run() in a separate thread without invoking start()).
|
|
229
|
+
if not self._is_alive:
|
|
230
|
+
self._is_alive = True
|
|
231
|
+
while self._is_alive:
|
|
232
|
+
tmp = self._thread_type(**self._thread_args)
|
|
233
|
+
self._purge_threads()
|
|
234
|
+
while not self.connected and self._is_alive:
|
|
235
|
+
try:
|
|
236
|
+
self.accept_connection()
|
|
237
|
+
except socket.timeout as e:
|
|
238
|
+
logger.debug("factory accept timeout on %s:%s: %s", self.address, self.port, e)
|
|
239
|
+
continue
|
|
240
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
241
|
+
logger.exception("accept error: %s", e)
|
|
242
|
+
continue
|
|
243
|
+
else:
|
|
244
|
+
# Hand off the accepted connection to the worker
|
|
245
|
+
accepted_conn = self.conn
|
|
246
|
+
# Reset server connection reference so we can accept again
|
|
247
|
+
self._reset_connection_ref()
|
|
248
|
+
tmp.swap_socket(accepted_conn)
|
|
249
|
+
tmp.start()
|
|
250
|
+
self._threads.append(tmp)
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
self._wait_to_exit()
|
|
254
|
+
self.close()
|
|
255
|
+
|
|
256
|
+
def stop_all(self):
|
|
257
|
+
"""Stop and join all active worker threads."""
|
|
258
|
+
for t in self._threads:
|
|
259
|
+
if t.is_alive():
|
|
260
|
+
t.force_stop()
|
|
261
|
+
t.join()
|
|
262
|
+
|
|
263
|
+
def _purge_threads(self):
|
|
264
|
+
# Rebuild list to avoid mutating while iterating
|
|
265
|
+
self._threads = [t for t in self._threads if t.is_alive()]
|
|
266
|
+
|
|
267
|
+
def stop(self):
|
|
268
|
+
# Stop accepting and stop all workers
|
|
269
|
+
self._is_alive = False
|
|
270
|
+
try:
|
|
271
|
+
self.stop_all()
|
|
272
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
273
|
+
pass
|
|
274
|
+
logger.debug("ServerFactory stopped on %s:%s", self.address, self.port)
|
|
275
|
+
|
|
276
|
+
def _wait_to_exit(self):
|
|
277
|
+
"""Block until all worker threads have finished."""
|
|
278
|
+
while self._get_num_of_active_threads():
|
|
279
|
+
time.sleep(0.2)
|
|
280
|
+
|
|
281
|
+
def _get_num_of_active_threads(self):
|
|
282
|
+
return len([True for x in self._threads if x.is_alive()])
|
|
283
|
+
|
|
284
|
+
active = property(_get_num_of_active_threads, doc="number of active threads")
|