wslink 1.12.4__py3-none-any.whl → 2.0.0__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.
- wslink/chunking.py +214 -0
- wslink/protocol.py +71 -130
- wslink/publish.py +5 -34
- {wslink-1.12.4.dist-info → wslink-2.0.0.dist-info}/METADATA +2 -1
- {wslink-1.12.4.dist-info → wslink-2.0.0.dist-info}/RECORD +7 -6
- {wslink-1.12.4.dist-info → wslink-2.0.0.dist-info}/WHEEL +1 -1
- {wslink-1.12.4.dist-info → wslink-2.0.0.dist-info}/top_level.txt +0 -0
wslink/chunking.py
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
import sys
|
2
|
+
import secrets
|
3
|
+
import msgpack
|
4
|
+
from typing import Dict, Tuple, Union
|
5
|
+
if sys.version_info >= (3, 8):
|
6
|
+
from typing import TypedDict # pylint: disable=no-name-in-module
|
7
|
+
else:
|
8
|
+
from typing_extensions import TypedDict
|
9
|
+
|
10
|
+
UINT32_LENGTH = 4
|
11
|
+
ID_LOCATION = 0
|
12
|
+
ID_LENGTH = UINT32_LENGTH
|
13
|
+
MESSAGE_OFFSET_LOCATION = ID_LOCATION + ID_LENGTH
|
14
|
+
MESSAGE_OFFSET_LENGTH = UINT32_LENGTH
|
15
|
+
MESSAGE_SIZE_LOCATION = MESSAGE_OFFSET_LOCATION + MESSAGE_OFFSET_LENGTH
|
16
|
+
MESSAGE_SIZE_LENGTH = UINT32_LENGTH
|
17
|
+
|
18
|
+
HEADER_LENGTH = ID_LENGTH + MESSAGE_OFFSET_LENGTH + MESSAGE_SIZE_LENGTH
|
19
|
+
|
20
|
+
|
21
|
+
def _encode_header(id: bytes, offset: int, size: int) -> bytes:
|
22
|
+
return (
|
23
|
+
id
|
24
|
+
+ offset.to_bytes(MESSAGE_OFFSET_LENGTH, "little", signed=False)
|
25
|
+
+ size.to_bytes(MESSAGE_SIZE_LENGTH, "little", signed=False)
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
def _decode_header(header: bytes) -> Tuple[bytes, int, int]:
|
30
|
+
id = header[ID_LOCATION:ID_LENGTH]
|
31
|
+
offset = int.from_bytes(
|
32
|
+
header[
|
33
|
+
MESSAGE_OFFSET_LOCATION : MESSAGE_OFFSET_LOCATION + MESSAGE_OFFSET_LENGTH
|
34
|
+
],
|
35
|
+
"little",
|
36
|
+
signed=False,
|
37
|
+
)
|
38
|
+
size = int.from_bytes(
|
39
|
+
header[MESSAGE_SIZE_LOCATION : MESSAGE_SIZE_LOCATION + MESSAGE_SIZE_LENGTH],
|
40
|
+
"little",
|
41
|
+
signed=False,
|
42
|
+
)
|
43
|
+
return id, offset, size
|
44
|
+
|
45
|
+
|
46
|
+
def generate_chunks(message: bytes, max_size: int):
|
47
|
+
total_size = len(message)
|
48
|
+
|
49
|
+
if max_size == 0:
|
50
|
+
max_content_size = total_size
|
51
|
+
else:
|
52
|
+
max_content_size = max(max_size - HEADER_LENGTH, 1)
|
53
|
+
|
54
|
+
id = secrets.token_bytes(ID_LENGTH)
|
55
|
+
|
56
|
+
offset = 0
|
57
|
+
|
58
|
+
while offset < total_size:
|
59
|
+
header = _encode_header(id, offset, total_size)
|
60
|
+
chunk_content = message[offset : offset + max_content_size]
|
61
|
+
|
62
|
+
yield header + chunk_content
|
63
|
+
|
64
|
+
offset += max_content_size
|
65
|
+
|
66
|
+
return
|
67
|
+
|
68
|
+
|
69
|
+
class PendingMessage(TypedDict):
|
70
|
+
received_size: int
|
71
|
+
content: bytearray
|
72
|
+
|
73
|
+
|
74
|
+
# This un-chunker is vulnerable to DOS.
|
75
|
+
# If it receives a message with a header claiming a large incoming message
|
76
|
+
# it will allocate the memory blindly even without actually receiving the content
|
77
|
+
# Chunks for a given message can come in any order
|
78
|
+
# Chunks across messages can be interleaved.
|
79
|
+
class UnChunker:
|
80
|
+
pending_messages: Dict[bytes, PendingMessage]
|
81
|
+
max_message_size: int
|
82
|
+
|
83
|
+
def __init__(self):
|
84
|
+
self.pending_messages = {}
|
85
|
+
self.max_message_size = 512
|
86
|
+
|
87
|
+
def set_max_message_size(self, size):
|
88
|
+
self.max_message_size = size
|
89
|
+
|
90
|
+
def release_pending_messages(self):
|
91
|
+
self.pending_messages = {}
|
92
|
+
|
93
|
+
def process_chunk(self, chunk: bytes) -> Union[bytes, None]:
|
94
|
+
header, chunk_content = chunk[:HEADER_LENGTH], chunk[HEADER_LENGTH:]
|
95
|
+
id, offset, total_size = _decode_header(header)
|
96
|
+
|
97
|
+
pending_message = self.pending_messages.get(id, None)
|
98
|
+
|
99
|
+
if pending_message is None:
|
100
|
+
if total_size > self.max_message_size:
|
101
|
+
raise ValueError(
|
102
|
+
f"""Total size for message {id} exceeds the allocation limit allowed.
|
103
|
+
Maximum size = {self.max_message_size},
|
104
|
+
Received size = {total_size}."""
|
105
|
+
)
|
106
|
+
|
107
|
+
pending_message = PendingMessage(
|
108
|
+
received_size=0, content=bytearray(total_size)
|
109
|
+
)
|
110
|
+
self.pending_messages[id] = pending_message
|
111
|
+
|
112
|
+
# This should never happen, but still check it
|
113
|
+
if total_size != len(pending_message["content"]):
|
114
|
+
del self.pending_messages[id]
|
115
|
+
raise ValueError(
|
116
|
+
f"Total size in chunk header for message {id} does not match total size declared by previous chunk."
|
117
|
+
)
|
118
|
+
|
119
|
+
content_size = len(chunk_content)
|
120
|
+
content_view = memoryview(pending_message["content"])
|
121
|
+
content_view[offset : offset + content_size] = chunk_content
|
122
|
+
pending_message["received_size"] += content_size
|
123
|
+
|
124
|
+
if pending_message["received_size"] >= total_size:
|
125
|
+
full_message = pending_message["content"]
|
126
|
+
del self.pending_messages[id]
|
127
|
+
return msgpack.unpackb(bytes(full_message))
|
128
|
+
|
129
|
+
return None
|
130
|
+
|
131
|
+
|
132
|
+
class StreamPendingMessage(TypedDict):
|
133
|
+
received_size: int
|
134
|
+
total_size: int
|
135
|
+
unpacker: msgpack.Unpacker
|
136
|
+
|
137
|
+
|
138
|
+
# This un-chunker is more memory efficient
|
139
|
+
# (each chunk is passed immediately to msgpack)
|
140
|
+
# and it will only allocate memory when it receives content.
|
141
|
+
# Chunks for a given message are expected to come sequentially
|
142
|
+
# Chunks across messages can be interleaved.
|
143
|
+
class StreamUnChunker:
|
144
|
+
pending_messages: Dict[bytes, StreamPendingMessage]
|
145
|
+
|
146
|
+
def __init__(self):
|
147
|
+
self.pending_messages = {}
|
148
|
+
|
149
|
+
def set_max_message_size(self, _size):
|
150
|
+
pass
|
151
|
+
|
152
|
+
def release_pending_messages(self):
|
153
|
+
self.pending_messages = {}
|
154
|
+
|
155
|
+
def process_chunk(self, chunk: bytes) -> Union[bytes, None]:
|
156
|
+
header, chunk_content = chunk[:HEADER_LENGTH], chunk[HEADER_LENGTH:]
|
157
|
+
id, offset, total_size = _decode_header(header)
|
158
|
+
|
159
|
+
pending_message = self.pending_messages.get(id, None)
|
160
|
+
|
161
|
+
if pending_message is None:
|
162
|
+
pending_message = StreamPendingMessage(
|
163
|
+
received_size=0,
|
164
|
+
total_size=total_size,
|
165
|
+
unpacker=msgpack.Unpacker(max_buffer_size=total_size),
|
166
|
+
)
|
167
|
+
self.pending_messages[id] = pending_message
|
168
|
+
|
169
|
+
# This should never happen, but still check it
|
170
|
+
if offset != pending_message["received_size"]:
|
171
|
+
del self.pending_messages[id]
|
172
|
+
raise ValueError(
|
173
|
+
f"""Received an unexpected chunk for message {id}.
|
174
|
+
Expected offset = {pending_message['received_size']},
|
175
|
+
Received offset = {offset}."""
|
176
|
+
)
|
177
|
+
|
178
|
+
# This should never happen, but still check it
|
179
|
+
if total_size != pending_message["total_size"]:
|
180
|
+
del self.pending_messages[id]
|
181
|
+
raise ValueError(
|
182
|
+
f"""Received an unexpected total size in chunk header for message {id}.
|
183
|
+
Expected size = {pending_message['total_size']},
|
184
|
+
Received size = {total_size}."""
|
185
|
+
)
|
186
|
+
|
187
|
+
content_size = len(chunk_content)
|
188
|
+
pending_message["received_size"] += content_size
|
189
|
+
|
190
|
+
unpacker = pending_message["unpacker"]
|
191
|
+
unpacker.feed(chunk_content)
|
192
|
+
|
193
|
+
full_message = None
|
194
|
+
|
195
|
+
try:
|
196
|
+
full_message = unpacker.unpack()
|
197
|
+
except msgpack.OutOfData:
|
198
|
+
pass # message is incomplete, keep ingesting chunks
|
199
|
+
|
200
|
+
if full_message is not None:
|
201
|
+
del self.pending_messages[id]
|
202
|
+
|
203
|
+
if pending_message["received_size"] < total_size:
|
204
|
+
# In principle feeding a stream to the unpacker could yield multiple outputs
|
205
|
+
# for example unpacker.feed(b'0123') would yield b'0', b'1', ect
|
206
|
+
# or concatenated packed payloads would yield two or more unpacked objects
|
207
|
+
# but in our use case we expect a full message to be mapped to a single object
|
208
|
+
raise ValueError(
|
209
|
+
f"""Received a parsable payload shorter than expected for message {id}.
|
210
|
+
Expected size = {total_size},
|
211
|
+
Received size = {pending_message['received_size']}."""
|
212
|
+
)
|
213
|
+
|
214
|
+
return full_message
|
wslink/protocol.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
import asyncio
|
2
2
|
import copy
|
3
3
|
import inspect
|
4
|
-
import json
|
5
4
|
import logging
|
6
|
-
import
|
5
|
+
import msgpack
|
6
|
+
import os
|
7
7
|
import traceback
|
8
8
|
|
9
9
|
from wslink import schedule_coroutine
|
10
10
|
from wslink.publish import PublishManager
|
11
|
+
from wslink.chunking import generate_chunks, UnChunker
|
11
12
|
|
12
13
|
# from http://www.jsonrpc.org/specification, section 5.1
|
13
14
|
METHOD_NOT_FOUND = -32601
|
@@ -17,6 +18,9 @@ RESULT_SERIALIZE_ERROR = -32002
|
|
17
18
|
# used in client JS code:
|
18
19
|
CLIENT_ERROR = -32099
|
19
20
|
|
21
|
+
# 4MB is the default inside aiohttp
|
22
|
+
MAX_MSG_SIZE = int(os.environ.get("WSLINK_MAX_MSG_SIZE", 4194304))
|
23
|
+
|
20
24
|
logger = logging.getLogger(__name__)
|
21
25
|
|
22
26
|
|
@@ -147,6 +151,7 @@ class WslinkHandler(object):
|
|
147
151
|
self.authentified_client_ids = set()
|
148
152
|
self.attachment_atomic = asyncio.Lock()
|
149
153
|
self.pub_manager = PublishManager()
|
154
|
+
self.unchunkers = {}
|
150
155
|
|
151
156
|
# Build the rpc method dictionary, assuming we were given a serverprotocol
|
152
157
|
if self.getServerProtocol():
|
@@ -183,6 +188,8 @@ class WslinkHandler(object):
|
|
183
188
|
return "reverse_connection_client_id"
|
184
189
|
|
185
190
|
async def onConnect(self, request, client_id):
|
191
|
+
self.unchunkers[client_id] = UnChunker()
|
192
|
+
|
186
193
|
if not self.serverProtocol:
|
187
194
|
return
|
188
195
|
if hasattr(self.serverProtocol, "onConnect"):
|
@@ -192,6 +199,8 @@ class WslinkHandler(object):
|
|
192
199
|
linkProtocol.onConnect(request, client_id)
|
193
200
|
|
194
201
|
async def onClose(self, client_id):
|
202
|
+
del self.unchunkers[client_id]
|
203
|
+
|
195
204
|
if not self.serverProtocol:
|
196
205
|
return
|
197
206
|
if hasattr(self.serverProtocol, "onClose"):
|
@@ -212,9 +221,16 @@ class WslinkHandler(object):
|
|
212
221
|
and await self.validateToken(args[0]["secret"], client_id)
|
213
222
|
):
|
214
223
|
self.authentified_client_ids.add(client_id)
|
224
|
+
# Once a client is authenticated let the unchunker allocate memory unrestricted
|
225
|
+
self.unchunkers[client_id].set_max_message_size(
|
226
|
+
4 * 1024 * 1024 * 1024
|
227
|
+
) # 4GB
|
215
228
|
await self.sendWrappedMessage(
|
216
229
|
rpcid,
|
217
|
-
{
|
230
|
+
{
|
231
|
+
"clientID": "c{0}".format(client_id),
|
232
|
+
"maxMsgSize": MAX_MSG_SIZE,
|
233
|
+
},
|
218
234
|
client_id=client_id,
|
219
235
|
)
|
220
236
|
else:
|
@@ -235,34 +251,16 @@ class WslinkHandler(object):
|
|
235
251
|
return False
|
236
252
|
|
237
253
|
async def onMessage(self, is_binary, msg, client_id):
|
238
|
-
|
239
|
-
|
240
|
-
if is_binary:
|
241
|
-
if self.isClientAuthenticated(client_id):
|
242
|
-
# assume all binary messages are attachments
|
243
|
-
try:
|
244
|
-
key = self.attachmentsRecvQueue.pop(0)
|
245
|
-
self.attachmentsReceived[key] = payload
|
246
|
-
except:
|
247
|
-
pass
|
248
|
-
return
|
254
|
+
if not is_binary:
|
255
|
+
return
|
249
256
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
if type(payload) is bytes:
|
254
|
-
payload = payload.decode("utf-8")
|
257
|
+
full_message = self.unchunkers[client_id].process_chunk(msg.data)
|
258
|
+
if full_message is not None:
|
259
|
+
await self.onCompleteMessage(full_message, client_id)
|
255
260
|
|
256
|
-
|
261
|
+
async def onCompleteMessage(self, rpc, client_id):
|
257
262
|
logger.debug("wslink incoming msg %s", self.payloadWithSecretStripped(rpc))
|
258
263
|
if "id" not in rpc:
|
259
|
-
# should be a binary attachment header
|
260
|
-
if rpc.get("method") == "wslink.binary.attachment":
|
261
|
-
keys = rpc.get("args", [])
|
262
|
-
if isinstance(keys, list):
|
263
|
-
for k in keys:
|
264
|
-
# wait for an attachment by it's order
|
265
|
-
self.attachmentsRecvQueue.append(k)
|
266
264
|
return
|
267
265
|
|
268
266
|
# TODO validate
|
@@ -303,73 +301,37 @@ class WslinkHandler(object):
|
|
303
301
|
return
|
304
302
|
|
305
303
|
obj, func = self.functionMap[methodName]
|
304
|
+
args.insert(0, obj)
|
305
|
+
|
306
306
|
try:
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
and re.match(r"^wslink_bin\d+$", o)
|
312
|
-
and o in self.attachmentsReceived
|
313
|
-
):
|
314
|
-
attachment = self.attachmentsReceived[o]
|
315
|
-
del self.attachmentsReceived[o]
|
316
|
-
return attachment
|
317
|
-
elif isinstance(o, list):
|
318
|
-
for i, v in enumerate(o):
|
319
|
-
o[i] = findAttachments(v)
|
320
|
-
elif isinstance(o, dict):
|
321
|
-
for k in o:
|
322
|
-
o[k] = findAttachments(o[k])
|
323
|
-
return o
|
324
|
-
|
325
|
-
args = findAttachments(args)
|
326
|
-
kwargs = findAttachments(kwargs)
|
327
|
-
|
328
|
-
args.insert(0, obj)
|
329
|
-
|
330
|
-
try:
|
331
|
-
self.web_app.last_active_client_id = client_id
|
332
|
-
results = func(*args, **kwargs)
|
333
|
-
if inspect.isawaitable(results):
|
334
|
-
results = await results
|
335
|
-
|
336
|
-
if self.connections[client_id].closed:
|
337
|
-
# Connection was closed during RPC call.
|
338
|
-
return
|
339
|
-
|
340
|
-
await self.sendWrappedMessage(
|
341
|
-
rpcid, results, method=methodName, client_id=client_id
|
342
|
-
)
|
343
|
-
except Exception as e_inst:
|
344
|
-
captured_trace = traceback.format_exc()
|
345
|
-
logger.error("Exception raised")
|
346
|
-
logger.error(repr(e_inst))
|
347
|
-
logger.error(captured_trace)
|
348
|
-
await self.sendWrappedError(
|
349
|
-
rpcid,
|
350
|
-
EXCEPTION_ERROR,
|
351
|
-
"Exception raised",
|
352
|
-
{
|
353
|
-
"method": methodName,
|
354
|
-
"exception": repr(e_inst),
|
355
|
-
"trace": captured_trace,
|
356
|
-
},
|
357
|
-
client_id=client_id,
|
358
|
-
)
|
307
|
+
self.web_app.last_active_client_id = client_id
|
308
|
+
results = func(*args, **kwargs)
|
309
|
+
if inspect.isawaitable(results):
|
310
|
+
results = await results
|
359
311
|
|
360
|
-
|
312
|
+
if self.connections[client_id].closed:
|
313
|
+
# Connection was closed during RPC call.
|
314
|
+
return
|
315
|
+
|
316
|
+
await self.sendWrappedMessage(
|
317
|
+
rpcid, results, method=methodName, client_id=client_id
|
318
|
+
)
|
319
|
+
except Exception as e_inst:
|
320
|
+
captured_trace = traceback.format_exc()
|
321
|
+
logger.error("Exception raised")
|
322
|
+
logger.error(repr(e_inst))
|
323
|
+
logger.error(captured_trace)
|
361
324
|
await self.sendWrappedError(
|
362
325
|
rpcid,
|
363
326
|
EXCEPTION_ERROR,
|
364
327
|
"Exception raised",
|
365
328
|
{
|
366
329
|
"method": methodName,
|
367
|
-
"exception": repr(
|
368
|
-
"trace":
|
330
|
+
"exception": repr(e_inst),
|
331
|
+
"trace": captured_trace,
|
369
332
|
},
|
370
333
|
client_id=client_id,
|
371
334
|
)
|
372
|
-
return
|
373
335
|
|
374
336
|
def payloadWithSecretStripped(self, payload):
|
375
337
|
payload = copy.deepcopy(payload)
|
@@ -428,9 +390,10 @@ class WslinkHandler(object):
|
|
428
390
|
"id": rpcid,
|
429
391
|
"result": content,
|
430
392
|
}
|
393
|
+
|
431
394
|
try:
|
432
|
-
|
433
|
-
except
|
395
|
+
packed_wrapper = msgpack.packb(wrapper)
|
396
|
+
except Exception:
|
434
397
|
# the content which is not serializable might be arbitrarily large, don't include.
|
435
398
|
# repr(content) would do that...
|
436
399
|
await self.sendWrappedError(
|
@@ -444,47 +407,14 @@ class WslinkHandler(object):
|
|
444
407
|
|
445
408
|
websockets = self.getAuthenticatedWebsockets(client_id, skip_last_active_client)
|
446
409
|
|
447
|
-
#
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
for
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
found_keys.append(key)
|
456
|
-
# increment for key
|
457
|
-
self.pub_manager.registerAttachment(key)
|
458
|
-
|
459
|
-
for key in found_keys:
|
460
|
-
# send header
|
461
|
-
header = {
|
462
|
-
"wslink": "1.0",
|
463
|
-
"method": "wslink.binary.attachment",
|
464
|
-
"args": [key],
|
465
|
-
}
|
466
|
-
json_header = json.dumps(header, ensure_ascii=False)
|
467
|
-
|
468
|
-
# aiohttp can not handle pending ws.send_bytes()
|
469
|
-
# tried with semaphore but got exception with >1
|
470
|
-
# https://github.com/aio-libs/aiohttp/issues/2934
|
471
|
-
async with self.attachment_atomic:
|
472
|
-
for ws in websockets:
|
473
|
-
if ws is not None:
|
474
|
-
# Send binary header
|
475
|
-
await ws.send_str(json_header)
|
476
|
-
# Send binary message
|
477
|
-
await ws.send_bytes(attachments[key])
|
478
|
-
|
479
|
-
# decrement for key
|
480
|
-
self.pub_manager.unregisterAttachment(key)
|
481
|
-
|
482
|
-
for ws in websockets:
|
483
|
-
if ws is not None:
|
484
|
-
await ws.send_str(encMsg)
|
485
|
-
|
486
|
-
loop = asyncio.get_event_loop()
|
487
|
-
loop.call_soon(self.pub_manager.freeAttachments, found_keys)
|
410
|
+
# aiohttp can not handle pending ws.send_bytes()
|
411
|
+
# tried with semaphore but got exception with >1
|
412
|
+
# https://github.com/aio-libs/aiohttp/issues/2934
|
413
|
+
async with self.attachment_atomic:
|
414
|
+
for chunk in generate_chunks(packed_wrapper, MAX_MSG_SIZE):
|
415
|
+
for ws in websockets:
|
416
|
+
if ws is not None:
|
417
|
+
await ws.send_bytes(chunk)
|
488
418
|
|
489
419
|
async def sendWrappedError(self, rpcid, code, message, data=None, client_id=None):
|
490
420
|
wrapper = {
|
@@ -497,15 +427,26 @@ class WslinkHandler(object):
|
|
497
427
|
}
|
498
428
|
if data:
|
499
429
|
wrapper["error"]["data"] = data
|
500
|
-
|
430
|
+
|
431
|
+
try:
|
432
|
+
packed_wrapper = msgpack.packb(wrapper)
|
433
|
+
except Exception:
|
434
|
+
del wrapper["error"]["data"]
|
435
|
+
packed_wrapper = msgpack.packb(wrapper)
|
436
|
+
|
501
437
|
websockets = (
|
502
438
|
[self.connections[client_id]]
|
503
439
|
if client_id
|
504
440
|
else [self.connections[c] for c in self.connections]
|
505
441
|
)
|
506
|
-
|
507
|
-
|
508
|
-
|
442
|
+
# aiohttp can not handle pending ws.send_bytes()
|
443
|
+
# tried with semaphore but got exception with >1
|
444
|
+
# https://github.com/aio-libs/aiohttp/issues/2934
|
445
|
+
async with self.attachment_atomic:
|
446
|
+
for chunk in generate_chunks(packed_wrapper, MAX_MSG_SIZE):
|
447
|
+
for ws in websockets:
|
448
|
+
if ws is not None:
|
449
|
+
await ws.send_bytes(chunk)
|
509
450
|
|
510
451
|
def publish(self, topic, data, client_id=None, skip_last_active_client=False):
|
511
452
|
client_list = [client_id] if client_id else [c_id for c_id in self.connections]
|
wslink/publish.py
CHANGED
@@ -7,9 +7,6 @@ from . import schedule_coroutine
|
|
7
7
|
class PublishManager(object):
|
8
8
|
def __init__(self):
|
9
9
|
self.protocols = []
|
10
|
-
self.attachmentMap = {}
|
11
|
-
self.attachmentRefCounts = {} # keyed same as attachment map
|
12
|
-
self.attachmentId = 0
|
13
10
|
self.publishCount = 0
|
14
11
|
|
15
12
|
def registerProtocol(self, protocol):
|
@@ -19,38 +16,12 @@ class PublishManager(object):
|
|
19
16
|
if protocol in self.protocols:
|
20
17
|
self.protocols.remove(protocol)
|
21
18
|
|
22
|
-
def getAttachmentMap(self):
|
23
|
-
return self.attachmentMap
|
24
|
-
|
25
|
-
def clearAttachmentMap(self):
|
26
|
-
self.attachmentMap.clear()
|
27
|
-
|
28
|
-
def registerAttachment(self, attachKey):
|
29
|
-
self.attachmentRefCounts[attachKey] += 1
|
30
|
-
|
31
|
-
def unregisterAttachment(self, attachKey):
|
32
|
-
self.attachmentRefCounts[attachKey] -= 1
|
33
|
-
|
34
|
-
def freeAttachments(self, keys=None):
|
35
|
-
keys_to_delete = []
|
36
|
-
keys_to_check = keys if keys is not None else [k for k in self.attachmentMap]
|
37
|
-
|
38
|
-
for key in keys_to_check:
|
39
|
-
if self.attachmentRefCounts.get(key) == 0:
|
40
|
-
keys_to_delete.append(key)
|
41
|
-
|
42
|
-
for key in keys_to_delete:
|
43
|
-
self.attachmentMap.pop(key)
|
44
|
-
self.attachmentRefCounts.pop(key)
|
45
|
-
|
46
19
|
def addAttachment(self, payload):
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
self.attachmentId += 1
|
53
|
-
return binaryId
|
20
|
+
"""Deprecated method, keeping it to avoid breaking compatibility
|
21
|
+
Now that we use msgpack to pack/unpack messages,
|
22
|
+
We can have binary data directly in the object itself,
|
23
|
+
without needing to transfer it separately from the rest."""
|
24
|
+
return payload
|
54
25
|
|
55
26
|
def publish(self, topic, data, client_id=None, skip_last_active_client=False):
|
56
27
|
for protocol in self.protocols:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: wslink
|
3
|
-
Version:
|
3
|
+
Version: 2.0.0
|
4
4
|
Summary: Python/JavaScript library for communicating over WebSocket
|
5
5
|
Home-page: https://github.com/kitware/wslink
|
6
6
|
Author: Kitware, Inc.
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.9
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
24
24
|
Requires-Dist: aiohttp <4
|
25
|
+
Requires-Dist: msgpack <2,>=1
|
25
26
|
Provides-Extra: ssl
|
26
27
|
Requires-Dist: cryptography ; extra == 'ssl'
|
27
28
|
|
@@ -1,8 +1,9 @@
|
|
1
1
|
wslink/LICENSE,sha256=I44UH7kDVqxDLnnlOWw_hFL2Fz7RjQ_4vPzZv9NYgTU,1483
|
2
2
|
wslink/__init__.py,sha256=AbEm-sUSoGL-uLpnbK1rSSjHSvyW-bMsGHWie7FgMHw,2708
|
3
|
+
wslink/chunking.py,sha256=BZZ0YAlh6PNI8rQe80NfdxU8pAvn_Klxew47AkvUJow,7130
|
3
4
|
wslink/launcher.py,sha256=8VMs3juObLkyGYQFNLjMoo4qFpKIcxWz0kS-af-DKO4,21170
|
4
|
-
wslink/protocol.py,sha256=
|
5
|
-
wslink/publish.py,sha256=
|
5
|
+
wslink/protocol.py,sha256=zdf4QthFHpAgEw3hTUyyaOuN76jzHeOJBpvekPbk7aY,15886
|
6
|
+
wslink/publish.py,sha256=9G5TXqyGr-LCo_LwHYhzif6lhG2iXDvEBmEgwR8fh1M,1437
|
6
7
|
wslink/relay.py,sha256=E8Lzu2Ay7KbOheN1-ArAZawo8lLqdDgJXOZSBuMknYs,86
|
7
8
|
wslink/server.py,sha256=FKSJAKHDyfkNVM45-M-y1Zn8hh2TTYto1hTCIJx1pp8,9440
|
8
9
|
wslink/ssl_context.py,sha256=hNOJJCdrStws1Qf6vPvY4vTk9Bf8J5d90W3fS0cRv8o,2290
|
@@ -18,7 +19,7 @@ wslink/backends/jupyter/__init__.py,sha256=Qu65gWsd2xCSsxybnDtEDI5vMjHN-F5jgPZOy
|
|
18
19
|
wslink/backends/jupyter/core.py,sha256=H73IEEHyom3TsbhkyI5O88bFBbUIDzHVuvqbIF6PAIM,3858
|
19
20
|
wslink/backends/tornado/__init__.py,sha256=Qu65gWsd2xCSsxybnDtEDI5vMjHN-F5jgPZOyNIxnGs,112
|
20
21
|
wslink/backends/tornado/core.py,sha256=tPMkkhWuO_ovkisVim0zcegwZKEAG4IRUdd_O_0a_R0,2157
|
21
|
-
wslink-
|
22
|
-
wslink-
|
23
|
-
wslink-
|
24
|
-
wslink-
|
22
|
+
wslink-2.0.0.dist-info/METADATA,sha256=XWnXD2kxZEtIuOy4Y-_8HZ8anq9hYFL7axDQhv96GoE,3045
|
23
|
+
wslink-2.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
24
|
+
wslink-2.0.0.dist-info/top_level.txt,sha256=N0d8eqvhwhfW1p1yPTmvxlbzhjz7ZyhBfysNvaFqpQY,7
|
25
|
+
wslink-2.0.0.dist-info/RECORD,,
|
File without changes
|