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/tserver.py CHANGED
@@ -1,204 +1,284 @@
1
1
  """ @namespace tserver
2
- Contains ThreadedServer, ServerFactoryThread and ServerFactory implementations.
2
+ Contains ThreadedServer, ServerFactoryThread and ServerFactory implementations.
3
3
  """
4
4
 
5
- __author__ = "Christopher Piekarski"
6
- __email__ = "chris@cpiekarski.com"
5
+ __author__ = "Christopher Piekarski"
6
+ __email__ = "chris@cpiekarski.com"
7
7
  __copyright__= """
8
- This file is part of the jsocket package.
9
- Copyright (C) 2011 by
10
- Christopher Piekarski <chris@cpiekarski.com>
8
+ Copyright (C) 2011 by
9
+ Christopher Piekarski <chris@cpiekarski.com>
11
10
 
12
- The tserver module is free software: you can redistribute it and/or modify
13
- it under the terms of the GNU General Public License as published by
14
- the Free Software Foundation, either version 3 of the License, or
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
- The jsocket package is distributed in the hope that it will be useful,
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
- You should have received a copy of the GNU General Public License
23
- along with tserver module. If not, see <http://www.gnu.org/licenses/>."""
24
- __version__ = "1.0.2"
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
- class ThreadedServer(threading.Thread, jsocket_base.JsonServer):
35
- def __init__(self, **kwargs):
36
- threading.Thread.__init__(self)
37
- jsocket_base.JsonServer.__init__(self, **kwargs)
38
- self._isAlive = False
39
-
40
- def _process_message(self, obj):
41
- """ Pure Virtual Method
42
-
43
- This method is called every time a JSON object is received from a client
44
-
45
- @param obj JSON "key: value" object received from client
46
- @retval None or a response object
47
- """
48
- pass
49
-
50
- def run(self):
51
- while self._isAlive:
52
- try:
53
- self.accept_connection()
54
- except socket.timeout as e:
55
- logger.debug("socket.timeout: %s" % e)
56
- continue
57
- except Exception as e:
58
- logger.exception(e)
59
- continue
60
-
61
- while self._isAlive:
62
- try:
63
- obj = self.read_obj()
64
- resp_obj = self._process_message(obj)
65
- if resp_obj is not None:
66
- logger.debug("message has a response")
67
- self.send_obj(resp_obj)
68
- except socket.timeout as e:
69
- logger.debug("socket.timeout: %s" % e)
70
- continue
71
- except Exception as e:
72
- logger.exception(e)
73
- self._close_connection()
74
- break
75
- self._close_socket()
76
-
77
- def start(self):
78
- """ Starts the threaded server.
79
- The newly living know nothing of the dead
80
-
81
- @retval None
82
- """
83
- self._isAlive = True
84
- super(ThreadedServer, self).start()
85
- logger.debug("Threaded Server has been started.")
86
-
87
- def stop(self):
88
- """ Stops the threaded server.
89
- The life of the dead is in the memory of the living
90
-
91
- @retval None
92
- """
93
- self._isAlive = False
94
- logger.debug("Threaded Server has been stopped.")
95
-
96
- class ServerFactoryThread(threading.Thread, jsocket_base.JsonSocket):
97
- def __init__(self, **kwargs):
98
- threading.Thread.__init__(self, **kwargs)
99
- jsocket_base.JsonSocket.__init__(self, **kwargs)
100
- self._isAlive = False
101
-
102
- def swap_socket(self, new_sock):
103
- """ Swaps the existing socket with a new one. Useful for setting socket after a new connection.
104
-
105
- @param new_sock socket to replace the existing default jsocket.JsonSocket object
106
- @retval None
107
- """
108
- del self.socket
109
- self.socket = new_sock
110
- self.conn = self.socket
111
-
112
- def run(self):
113
- """ Should exit when client closes socket conn.
114
- Can force an exit with force_stop.
115
- """
116
- while self._isAlive:
117
- try:
118
- obj = self.read_obj()
119
- resp_obj = self._process_message(obj)
120
- if resp_obj is not None:
121
- logger.debug("message has a response")
122
- self.send_obj(resp_obj)
123
- except socket.timeout as e:
124
- logger.debug("socket.timeout: %s" % e)
125
- continue
126
- except Exception as e:
127
- logger.info("client connection broken, exit and close connection socket")
128
- self._isAlive = False
129
- break
130
- self._close_connection()
131
-
132
- def start(self):
133
- """ Starts the factory thread.
134
- The newly living know nothing of the dead
135
-
136
- @retval None
137
- """
138
- self._isAlive = True
139
- super(ServerFactoryThread, self).start()
140
- logger.debug("ServerFactoryThread has been started.")
141
-
142
- def force_stop(self):
143
- """ Force stops the factory thread.
144
- Should exit when client socket is closed under normal conditions.
145
- The life of the dead is in the memory of the living.
146
-
147
- @retval None
148
- """
149
- self._isAlive = False
150
- logger.debug("ServerFactoryThread has been stopped.")
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
- def __init__(self, server_thread, **kwargs):
155
- ThreadedServer.__init__(self, address=kwargs['address'], port=kwargs['port'])
156
- if not issubclass(server_thread, ServerFactoryThread):
157
- raise TypeError("serverThread not of type", ServerFactoryThread)
158
- self._thread_type = server_thread
159
- self._threads = []
160
- self._thread_args = kwargs
161
- self._thread_args.pop('address', None)
162
- self._thread_args.pop('port', None)
163
-
164
- def run(self):
165
- while self._isAlive:
166
- tmp = self._thread_type(**self._thread_args)
167
- self._purge_threads()
168
- while not self.connected and self._isAlive:
169
- try:
170
- self.accept_connection()
171
- except socket.timeout as e:
172
- logger.debug("socket.timeout: %s" % e)
173
- continue
174
- except Exception as e:
175
- logger.exception(e)
176
- continue
177
- else:
178
- tmp.swap_socket(self.conn)
179
- tmp.start()
180
- self._threads.append(tmp)
181
- break
182
-
183
- self._wait_to_exit()
184
- self.close()
185
-
186
- def stop_all(self):
187
- for t in self._threads:
188
- if t.is_alive():
189
- t.force_stop()
190
- t.join()
191
-
192
- def _purge_threads(self):
193
- for t in self._threads:
194
- if not t.is_alive():
195
- self._threads.remove(t)
196
-
197
- def _wait_to_exit(self):
198
- while self._get_num_of_active_threads():
199
- time.sleep(0.2)
200
-
201
- def _get_num_of_active_threads(self):
202
- return len([True for x in self._threads if x.is_alive()])
203
-
204
- active = property(_get_num_of_active_threads, doc="number of active threads")
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")