@scrypted/server 0.138.4 → 0.138.6
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.
- package/dist/plugin/plugin-host-api.js +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +0 -4
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/runtime/child-process-worker.d.ts +1 -0
- package/dist/plugin/runtime/child-process-worker.js +4 -1
- package/dist/plugin/runtime/child-process-worker.js.map +1 -1
- package/dist/plugin/runtime/node-thread-worker.d.ts +1 -0
- package/dist/plugin/runtime/node-thread-worker.js +2 -0
- package/dist/plugin/runtime/node-thread-worker.js.map +1 -1
- package/dist/plugin/runtime/runtime-worker.d.ts +3 -5
- package/dist/promise-utils.d.ts +13 -0
- package/dist/promise-utils.js +92 -0
- package/dist/promise-utils.js.map +1 -0
- package/dist/runtime.d.ts +3 -3
- package/dist/runtime.js +13 -5
- package/dist/runtime.js.map +1 -1
- package/dist/services/plugin.js +1 -1
- package/dist/services/plugin.js.map +1 -1
- package/package.json +2 -2
- package/python/plugin_remote.py +3 -9
- package/python/plugin_repl.py +103 -299
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +0 -4
- package/src/plugin/runtime/child-process-worker.ts +6 -1
- package/src/plugin/runtime/node-thread-worker.ts +4 -1
- package/src/plugin/runtime/runtime-worker.ts +3 -5
- package/src/promise-utils.ts +96 -0
- package/src/runtime.ts +15 -6
- package/src/services/plugin.ts +1 -1
package/python/plugin_repl.py
CHANGED
@@ -1,32 +1,28 @@
|
|
1
|
+
# Copy this here before it gets populated with the rest of this file's code
|
2
|
+
base_globals = globals().copy()
|
3
|
+
|
1
4
|
import asyncio
|
2
|
-
import concurrent.futures
|
3
|
-
from functools import partial
|
4
5
|
import inspect
|
5
|
-
import os
|
6
6
|
import prompt_toolkit
|
7
7
|
from prompt_toolkit import print_formatted_text
|
8
8
|
from prompt_toolkit.application import Application
|
9
9
|
import prompt_toolkit.application.current
|
10
|
+
from prompt_toolkit.application.current import create_app_session
|
11
|
+
from prompt_toolkit.data_structures import Size
|
10
12
|
import prompt_toolkit.key_binding.key_processor
|
11
|
-
|
12
|
-
from prompt_toolkit.
|
13
|
+
from prompt_toolkit.input import create_pipe_input
|
14
|
+
from prompt_toolkit.output.vt100 import Vt100_Output
|
13
15
|
from prompt_toolkit.output.color_depth import ColorDepth
|
14
|
-
from
|
15
|
-
from ptpython.repl import embed, PythonRepl, _has_coroutine_flag
|
16
|
+
from ptpython.repl import embed, PythonRepl
|
16
17
|
import ptpython.key_bindings
|
17
18
|
import ptpython.python_input
|
18
19
|
import ptpython.history_browser
|
19
20
|
import ptpython.layout
|
20
|
-
import socket
|
21
|
-
import sys
|
22
|
-
import telnetlib
|
23
|
-
import threading
|
24
|
-
import traceback
|
25
|
-
import types
|
26
21
|
from typing import List, Dict, Any
|
27
22
|
|
28
23
|
from scrypted_python.scrypted_sdk import ScryptedStatic, ScryptedDevice
|
29
24
|
|
25
|
+
from cluster_setup import cluster_listen_zero
|
30
26
|
from rpc import maybe_await
|
31
27
|
|
32
28
|
|
@@ -38,175 +34,72 @@ ColorDepth.default = lambda *args, **kwargs: ColorDepth.DEPTH_4_BIT
|
|
38
34
|
# that there is only one global Application, so multiple REPLs will confuse
|
39
35
|
# the library. The patches here allow us to scope a particular call stack
|
40
36
|
# to a particular REPL, and to get the current Application from the stack.
|
41
|
-
|
37
|
+
def patch_prompt_toolkit():
|
38
|
+
default_get_app = prompt_toolkit.application.current.get_app
|
39
|
+
|
40
|
+
def get_app_patched() -> Application[Any]:
|
41
|
+
stack = inspect.stack()
|
42
|
+
for frame in stack:
|
43
|
+
self_var = frame.frame.f_locals.get("self")
|
44
|
+
if self_var is not None and isinstance(self_var, Application):
|
45
|
+
return self_var
|
46
|
+
return default_get_app()
|
47
|
+
|
48
|
+
prompt_toolkit.application.current.get_app = get_app_patched
|
49
|
+
prompt_toolkit.key_binding.key_processor.get_app = get_app_patched
|
50
|
+
ptpython.python_input.get_app = get_app_patched
|
51
|
+
ptpython.key_bindings.get_app = get_app_patched
|
52
|
+
ptpython.history_browser.get_app = get_app_patched
|
53
|
+
ptpython.layout.get_app = get_app_patched
|
54
|
+
patch_prompt_toolkit()
|
55
|
+
|
56
|
+
|
57
|
+
def configure(repl: PythonRepl) -> None:
|
58
|
+
repl.confirm_exit = False
|
59
|
+
repl.enable_open_in_editor = False
|
60
|
+
repl.enable_system_bindings = False
|
42
61
|
|
43
62
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
return self_var
|
50
|
-
return default_get_app()
|
63
|
+
class AsyncStreamStdout:
|
64
|
+
"""
|
65
|
+
Wrapper around StreamReader and StreamWriter to provide `write` and `flush`
|
66
|
+
methods for Vt100_Output.
|
67
|
+
"""
|
51
68
|
|
69
|
+
def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
70
|
+
self.reader = reader
|
71
|
+
self.writer = writer
|
72
|
+
self.loop = asyncio.get_event_loop()
|
52
73
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
ptpython.key_bindings.get_app = get_app_patched
|
58
|
-
ptpython.history_browser.get_app = get_app_patched
|
59
|
-
ptpython.layout.get_app = get_app_patched
|
74
|
+
def write(self, data: bytes) -> None:
|
75
|
+
if isinstance(data, str):
|
76
|
+
data = data.encode()
|
77
|
+
self.writer.write(data)
|
60
78
|
|
79
|
+
def flush(self) -> None:
|
80
|
+
self.loop.create_task(self.writer.drain())
|
81
|
+
|
82
|
+
def isatty(self) -> bool:
|
83
|
+
return True
|
61
84
|
|
62
|
-
|
63
|
-
#
|
64
|
-
|
65
|
-
async def run_async_patched(self: PythonRepl) -> None:
|
66
|
-
"""
|
67
|
-
Run the REPL loop, but run the blocking parts in an executor, so that
|
68
|
-
we don't block the event loop. Both the input and output (which can
|
69
|
-
display a pager) will run in a separate thread with their own event
|
70
|
-
loop, this way ptpython's own event loop won't interfere with the
|
71
|
-
asyncio event loop from where this is called.
|
72
|
-
|
73
|
-
The "eval" however happens in the current thread, which is important.
|
74
|
-
(Both for control-C to work, as well as for the code to see the right
|
75
|
-
thread in which it was embedded).
|
76
|
-
"""
|
77
|
-
loop = asyncio.get_running_loop()
|
78
|
-
|
79
|
-
if self.terminal_title:
|
80
|
-
set_title(self.terminal_title)
|
81
|
-
|
82
|
-
self._add_to_namespace()
|
83
|
-
|
84
|
-
try:
|
85
|
-
while True:
|
86
|
-
try:
|
87
|
-
# Read.
|
88
|
-
try:
|
89
|
-
text = await loop.run_in_executor(None, self.read)
|
90
|
-
except EOFError:
|
91
|
-
return
|
92
|
-
except asyncio.CancelledError:
|
93
|
-
return
|
94
|
-
except AssertionError:
|
95
|
-
return
|
96
|
-
except BaseException:
|
97
|
-
# Something went wrong while reading input.
|
98
|
-
# (E.g., a bug in the completer that propagates. Don't
|
99
|
-
# crash the REPL.)
|
100
|
-
traceback.print_exc()
|
101
|
-
continue
|
102
|
-
|
103
|
-
# Eval.
|
104
|
-
await self.run_and_show_expression_async(text)
|
105
|
-
|
106
|
-
except KeyboardInterrupt as e:
|
107
|
-
# XXX: This does not yet work properly. In some situations,
|
108
|
-
# `KeyboardInterrupt` exceptions can end up in the event
|
109
|
-
# loop selector.
|
110
|
-
self._handle_keyboard_interrupt(e)
|
111
|
-
except SystemExit:
|
112
|
-
return
|
113
|
-
finally:
|
114
|
-
if self.terminal_title:
|
115
|
-
clear_title()
|
116
|
-
self._remove_from_namespace()
|
117
|
-
|
118
|
-
|
119
|
-
# This is a patched version of PythonRepl.eval_async to cross thread
|
120
|
-
# boundaries when running user REPL instructions.
|
121
|
-
# Original: https://github.com/prompt-toolkit/ptpython/blob/3.0.26/ptpython/repl.py#L304
|
122
|
-
async def eval_async_patched(self: PythonRepl, line: str) -> object:
|
123
|
-
"""
|
124
|
-
Evaluate the line and print the result.
|
125
|
-
"""
|
126
|
-
scrypted_loop: asyncio.AbstractEventLoop = self.scrypted_loop
|
127
|
-
|
128
|
-
def task_done_cb(future: concurrent.futures.Future, task: asyncio.Task):
|
129
|
-
try:
|
130
|
-
result = task.result()
|
131
|
-
future.set_result(result)
|
132
|
-
except BaseException as e:
|
133
|
-
future.set_exception(e)
|
134
|
-
|
135
|
-
def eval_in_scrypted(future: concurrent.futures.Future, code, *args, **kwargs):
|
136
|
-
try:
|
137
|
-
result = eval(code, *args, **kwargs)
|
138
|
-
if _has_coroutine_flag(code):
|
139
|
-
task = scrypted_loop.create_task(result)
|
140
|
-
task.add_done_callback(partial(task_done_cb, future))
|
141
|
-
else:
|
142
|
-
future.set_result(result)
|
143
|
-
except BaseException as e:
|
144
|
-
future.set_exception(e)
|
145
|
-
|
146
|
-
def eval_across_loops(code, *args, **kwargs):
|
147
|
-
future = concurrent.futures.Future()
|
148
|
-
scrypted_loop.call_soon_threadsafe(
|
149
|
-
partial(eval_in_scrypted, future), code, *args, **kwargs
|
150
|
-
)
|
151
|
-
return future.result()
|
152
|
-
|
153
|
-
# WORKAROUND: Due to a bug in Jedi, the current directory is removed
|
154
|
-
# from sys.path. See: https://github.com/davidhalter/jedi/issues/1148
|
155
|
-
if "" not in sys.path:
|
156
|
-
sys.path.insert(0, "")
|
157
|
-
|
158
|
-
if line.lstrip().startswith("!"):
|
159
|
-
# Run as shell command
|
160
|
-
os.system(line[1:])
|
161
|
-
else:
|
162
|
-
# Try eval first
|
163
|
-
try:
|
164
|
-
code = self._compile_with_flags(line, "eval")
|
165
|
-
except SyntaxError:
|
166
|
-
pass
|
167
|
-
else:
|
168
|
-
# No syntax errors for eval. Do eval.
|
169
|
-
|
170
|
-
result = eval_across_loops(code, self.get_globals(), self.get_locals())
|
171
|
-
self._store_eval_result(result)
|
172
|
-
return result
|
173
|
-
|
174
|
-
# If not a valid `eval` expression, compile as `exec` expression
|
175
|
-
# but still run with eval to get an awaitable in case of a
|
176
|
-
# awaitable expression.
|
177
|
-
code = self._compile_with_flags(line, "exec")
|
178
|
-
result = eval_across_loops(code, self.get_globals(), self.get_locals())
|
179
|
-
|
180
|
-
return None
|
181
|
-
|
182
|
-
|
183
|
-
def configure(scrypted_loop: asyncio.AbstractEventLoop, repl: PythonRepl) -> None:
|
184
|
-
repl.confirm_exit = False
|
185
|
-
repl.enable_system_bindings = False
|
186
|
-
repl.enable_mouse_support = False
|
187
|
-
repl.run_async = types.MethodType(run_async_patched, repl)
|
188
|
-
repl.eval_async = types.MethodType(eval_async_patched, repl)
|
189
|
-
repl.scrypted_loop = scrypted_loop
|
85
|
+
|
86
|
+
# keep a reference to the server alive so it doesn't get garbage collected
|
87
|
+
repl_server = None
|
190
88
|
|
191
89
|
|
192
90
|
async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
91
|
+
global repl_server
|
92
|
+
|
93
|
+
if repl_server is not None:
|
94
|
+
return repl_server["port"]
|
95
|
+
|
193
96
|
deviceManager = sdk.deviceManager
|
194
97
|
systemManager = sdk.systemManager
|
195
98
|
mediaManager = sdk.mediaManager
|
196
99
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
sock.settimeout(None)
|
201
|
-
# TODO: this should use an equivalent to cluster_listen_zero
|
202
|
-
sock.bind(("0.0.0.0" if os.getenv("SCRYPTED_CLUSTER_ADDRESS") else "127.0.0.1", 0))
|
203
|
-
sock.listen()
|
204
|
-
|
205
|
-
scrypted_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
206
|
-
|
207
|
-
async def start_telnet_repl(future: concurrent.futures.Future, filter: str) -> None:
|
208
|
-
repl_loop = asyncio.get_event_loop()
|
209
|
-
|
100
|
+
async def on_repl_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
101
|
+
filter = await reader.read(1024)
|
102
|
+
filter = filter.decode()
|
210
103
|
if filter == "undefined":
|
211
104
|
filter = None
|
212
105
|
|
@@ -224,141 +117,52 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
224
117
|
device = plugin
|
225
118
|
for c in chain:
|
226
119
|
device = await maybe_await(device.getDevice(c))
|
227
|
-
|
228
120
|
realDevice = systemManager.getDeviceById(device.id)
|
229
121
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
async def interact(connection: TelnetConnection) -> None:
|
237
|
-
# repl_loop owns the print capabilities, but the prints will
|
238
|
-
# be executed in scrypted_loop. We need to bridge the two here
|
239
|
-
repl_print = partial(print_formatted_text, output=connection.vt100_output)
|
240
|
-
|
241
|
-
def print_across_loops(*args, **kwargs):
|
242
|
-
repl_loop.call_soon_threadsafe(repl_print, *args, **kwargs)
|
243
|
-
|
244
|
-
global_dict = {
|
245
|
-
**globals(),
|
246
|
-
"print": print_across_loops,
|
247
|
-
"help": lambda *args, **kwargs: print_across_loops(
|
248
|
-
"Help is not available in this environment"
|
249
|
-
),
|
250
|
-
"input": lambda *args, **kwargs: print_across_loops(
|
251
|
-
"Input is not available in this environment"
|
252
|
-
),
|
253
|
-
}
|
254
|
-
locals_dict = {
|
255
|
-
"device": device,
|
256
|
-
"systemManager": systemManager,
|
257
|
-
"deviceManager": deviceManager,
|
258
|
-
"mediaManager": mediaManager,
|
259
|
-
"sdk": sdk,
|
260
|
-
"realDevice": realDevice,
|
261
|
-
}
|
262
|
-
vars_prompt = "\n".join([f" {k}" for k in locals_dict.keys()])
|
263
|
-
banner = f"Python REPL variables:\n{vars_prompt}"
|
264
|
-
print_formatted_text(banner)
|
265
|
-
await embed(
|
266
|
-
return_asyncio_coroutine=True,
|
267
|
-
globals=global_dict,
|
268
|
-
locals=locals_dict,
|
269
|
-
configure=partial(configure, scrypted_loop),
|
270
|
-
)
|
271
|
-
|
272
|
-
server_task: asyncio.Task = None
|
273
|
-
|
274
|
-
def ready_cb():
|
275
|
-
future.set_result(
|
276
|
-
(
|
277
|
-
telnet_port,
|
278
|
-
lambda: repl_loop.call_soon_threadsafe(server_task.cancel),
|
279
|
-
)
|
280
|
-
)
|
281
|
-
|
282
|
-
# Start the REPL server
|
283
|
-
telnet_server = TelnetServer(
|
284
|
-
interact=interact, port=telnet_port, enable_cpr=False
|
285
|
-
)
|
286
|
-
server_task = asyncio.create_task(telnet_server.run(ready_cb=ready_cb))
|
287
|
-
try:
|
288
|
-
await server_task
|
289
|
-
except:
|
290
|
-
pass
|
291
|
-
|
292
|
-
def handle_connection(conn: socket.socket):
|
293
|
-
conn.settimeout(None)
|
294
|
-
filter = conn.recv(1024).decode()
|
295
|
-
server_started_future = concurrent.futures.Future()
|
296
|
-
repl_loop = asyncio.SelectorEventLoop()
|
297
|
-
|
298
|
-
# we're not in the main loop, so can't handle any signals anyways
|
299
|
-
repl_loop.add_signal_handler = lambda sig, cb: None
|
300
|
-
repl_loop.remove_signal_handler = lambda sig: True
|
301
|
-
|
302
|
-
def finish_setup():
|
303
|
-
telnet_port, exit_server = server_started_future.result()
|
304
|
-
|
305
|
-
telnet_client = telnetlib.Telnet("localhost", telnet_port, timeout=None)
|
306
|
-
|
307
|
-
def telnet_negotiation_cb(telnet_socket, command, option):
|
308
|
-
pass # ignore telnet negotiation
|
309
|
-
|
310
|
-
telnet_client.set_option_negotiation_callback(telnet_negotiation_cb)
|
311
|
-
|
312
|
-
# initialize telnet terminal
|
313
|
-
# this tells the telnet server we are a vt100 terminal
|
314
|
-
telnet_client.get_socket().sendall(
|
315
|
-
b"\xff\xfb\x18\xff\xfa\x18\x00\x61\x6e\x73\x69\xff\xf0"
|
122
|
+
with create_pipe_input() as vt100_input:
|
123
|
+
vt100_output = Vt100_Output(
|
124
|
+
AsyncStreamStdout(reader, writer),
|
125
|
+
lambda: Size(rows=24, columns=80),
|
126
|
+
term=None,
|
316
127
|
)
|
317
|
-
telnet_client.get_socket().sendall(b"\r\n")
|
318
128
|
|
319
|
-
|
320
|
-
def forward_to_telnet():
|
129
|
+
async def vt100_input_coro():
|
321
130
|
while True:
|
322
|
-
data =
|
131
|
+
data = await reader.read(1024)
|
323
132
|
if not data:
|
324
133
|
break
|
325
|
-
|
326
|
-
|
327
|
-
|
134
|
+
vt100_input.send_bytes(data)
|
135
|
+
|
136
|
+
asyncio.create_task(vt100_input_coro())
|
137
|
+
|
138
|
+
with create_app_session(input=vt100_input, output=vt100_output):
|
139
|
+
global_dict = {
|
140
|
+
**base_globals.copy(),
|
141
|
+
"print": print_formatted_text,
|
142
|
+
"help": lambda *args, **kwargs: print_formatted_text(
|
143
|
+
"Help is not available in this environment"
|
144
|
+
),
|
145
|
+
"input": lambda *args, **kwargs: print_formatted_text(
|
146
|
+
"Input is not available in this environment"
|
147
|
+
),
|
148
|
+
}
|
149
|
+
locals_dict = {
|
150
|
+
"device": device,
|
151
|
+
"systemManager": systemManager,
|
152
|
+
"deviceManager": deviceManager,
|
153
|
+
"mediaManager": mediaManager,
|
154
|
+
"sdk": sdk,
|
155
|
+
"realDevice": realDevice,
|
156
|
+
}
|
157
|
+
vars_prompt = "\n".join([f" {k}" for k in locals_dict.keys()])
|
158
|
+
banner = f"Python REPL variables:\n{vars_prompt}"
|
159
|
+
print_formatted_text(banner)
|
160
|
+
await embed(
|
161
|
+
return_asyncio_coroutine=True,
|
162
|
+
globals=global_dict,
|
163
|
+
locals=locals_dict,
|
164
|
+
configure=configure,
|
165
|
+
)
|
328
166
|
|
329
|
-
|
330
|
-
|
331
|
-
while True:
|
332
|
-
data = telnet_client.read_some()
|
333
|
-
if not data:
|
334
|
-
conn.sendall("REPL exited".encode())
|
335
|
-
break
|
336
|
-
if b">>>" in data:
|
337
|
-
# This is an ugly hack - somewhere in ptpython, the
|
338
|
-
# initial prompt is being printed many times. Normal
|
339
|
-
# telnet clients handle it properly, but xtermjs doesn't
|
340
|
-
# like it. We just replace the first few with spaces
|
341
|
-
# so it's not too ugly.
|
342
|
-
prompt_count += 1
|
343
|
-
if prompt_count < 5:
|
344
|
-
data = data.replace(b">>>", b" ")
|
345
|
-
conn.sendall(data)
|
346
|
-
conn.close()
|
347
|
-
exit_server()
|
348
|
-
|
349
|
-
threading.Thread(target=forward_to_telnet).start()
|
350
|
-
threading.Thread(target=forward_to_socket).start()
|
351
|
-
|
352
|
-
threading.Thread(target=finish_setup).start()
|
353
|
-
|
354
|
-
repl_loop.run_until_complete(start_telnet_repl(server_started_future, filter))
|
355
|
-
|
356
|
-
def accept_connection():
|
357
|
-
while True:
|
358
|
-
conn, addr = sock.accept()
|
359
|
-
threading.Thread(target=handle_connection, args=(conn,)).start()
|
360
|
-
|
361
|
-
threading.Thread(target=accept_connection).start()
|
362
|
-
|
363
|
-
proxy_port = sock.getsockname()[1]
|
364
|
-
return proxy_port
|
167
|
+
repl_server = await cluster_listen_zero(on_repl_client)
|
168
|
+
return repl_server["port"]
|
@@ -38,7 +38,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
38
38
|
logger.log('w', 'plugin restart was requested, but a different instance was found. restart cancelled.');
|
39
39
|
return;
|
40
40
|
}
|
41
|
-
this.scrypted.runPlugin(plugin);
|
41
|
+
await this.scrypted.runPlugin(plugin);
|
42
42
|
}, 15000);
|
43
43
|
|
44
44
|
constructor(public scrypted: ScryptedRuntime, pluginId: string, public pluginHost: PluginHost, public mediaManager: MediaManager) {
|
@@ -450,10 +450,6 @@ export class PluginHost {
|
|
450
450
|
this.peer.kill('plugin disconnected');
|
451
451
|
};
|
452
452
|
|
453
|
-
this.worker.on('close', () => {
|
454
|
-
logger.log('e', `${this.pluginName} close`);
|
455
|
-
disconnect();
|
456
|
-
});
|
457
453
|
this.worker.on('exit', async (code, signal) => {
|
458
454
|
logger.log('e', `${this.pluginName} exited ${code} ${signal}`);
|
459
455
|
disconnect();
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import child_process from 'child_process';
|
2
|
+
import { once } from 'events';
|
2
3
|
import { EventEmitter } from "ws";
|
3
4
|
import { RpcMessage, RpcPeer } from "../../rpc";
|
4
5
|
import { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
|
@@ -6,6 +7,7 @@ import { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
|
|
6
7
|
export abstract class ChildProcessWorker extends EventEmitter implements RuntimeWorker {
|
7
8
|
public pluginId: string;
|
8
9
|
protected worker: child_process.ChildProcess;
|
10
|
+
killPromise: Promise<void>;
|
9
11
|
|
10
12
|
get childProcess() {
|
11
13
|
return this.worker;
|
@@ -14,10 +16,11 @@ export abstract class ChildProcessWorker extends EventEmitter implements Runtime
|
|
14
16
|
constructor(options: RuntimeWorkerOptions) {
|
15
17
|
super();
|
16
18
|
this.pluginId = options.packageJson.name;
|
19
|
+
|
17
20
|
}
|
18
21
|
|
19
22
|
setupWorker() {
|
20
|
-
this.worker.on('close', (
|
23
|
+
this.worker.on('close', () => this.emit('error', new Error('close')));
|
21
24
|
this.worker.on('disconnect', () => this.emit('error', new Error('disconnect')));
|
22
25
|
this.worker.on('exit', (code, signal) => this.emit('exit', code, signal));
|
23
26
|
this.worker.on('error', e => this.emit('error', e));
|
@@ -27,6 +30,8 @@ export abstract class ChildProcessWorker extends EventEmitter implements Runtime
|
|
27
30
|
if (stdio)
|
28
31
|
stdio.on('error', e => this.emit('error', e));
|
29
32
|
}
|
33
|
+
|
34
|
+
this.killPromise = once(this.worker, 'exit').then(() => {}).catch(() => {});
|
30
35
|
}
|
31
36
|
|
32
37
|
get pid() {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { EventEmitter } from "events";
|
1
|
+
import { EventEmitter, once } from "events";
|
2
2
|
import worker_threads from "worker_threads";
|
3
3
|
import { RpcMessage, RpcPeer, RpcSerializer } from "../../rpc";
|
4
4
|
import { BufferSerializer } from '../../rpc-buffer-serializer';
|
@@ -47,6 +47,7 @@ interface PortMessage {
|
|
47
47
|
export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
|
48
48
|
worker: worker_threads.Worker;
|
49
49
|
port: worker_threads.MessagePort;
|
50
|
+
killPromise: Promise<void>;
|
50
51
|
|
51
52
|
constructor(mainFilename: string, public pluginId: string, options: RuntimeWorkerOptions, workerOptions?: worker_threads.WorkerOptions, workerData?: any, transferList: Array<worker_threads.TransferListItem> = []) {
|
52
53
|
super();
|
@@ -82,6 +83,8 @@ export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
|
|
82
83
|
this.port.on('close', () => {
|
83
84
|
this.emit('error', new Error('port closed'));
|
84
85
|
});
|
86
|
+
|
87
|
+
this.killPromise = once(this.worker, 'exit').then(() => {}).catch(() => {});
|
85
88
|
}
|
86
89
|
|
87
90
|
get pid() {
|
@@ -1,7 +1,7 @@
|
|
1
|
+
import net from "net";
|
2
|
+
import { Readable } from "stream";
|
1
3
|
import { RpcMessage, RpcPeer } from "../../rpc";
|
2
4
|
import { PluginDebug } from "../plugin-debug";
|
3
|
-
import { Readable } from "stream";
|
4
|
-
import net from "net";
|
5
5
|
|
6
6
|
export interface RuntimeWorkerOptions {
|
7
7
|
packageJson: any;
|
@@ -16,6 +16,7 @@ export interface RuntimeWorker {
|
|
16
16
|
pid: number;
|
17
17
|
stdout: Readable;
|
18
18
|
stderr: Readable;
|
19
|
+
killPromise: Promise<any>;
|
19
20
|
|
20
21
|
kill(): void;
|
21
22
|
|
@@ -23,15 +24,12 @@ export interface RuntimeWorker {
|
|
23
24
|
|
24
25
|
on(event: 'error', listener: (err: Error) => void): this;
|
25
26
|
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
26
|
-
on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
27
27
|
|
28
28
|
once(event: 'error', listener: (err: Error) => void): this;
|
29
29
|
once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
30
|
-
once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
31
30
|
|
32
31
|
removeListener(event: 'error', listener: (err: Error) => void): this;
|
33
32
|
removeListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
34
|
-
removeListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
35
33
|
|
36
34
|
send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void;
|
37
35
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
export interface RefreshPromise<T> {
|
2
|
+
promise: Promise<T>;
|
3
|
+
cacheDuration: number;
|
4
|
+
}
|
5
|
+
|
6
|
+
export function singletonPromise<T>(rp: undefined | RefreshPromise<T>, method: () => Promise<T>, cacheDuration = 0) {
|
7
|
+
if (rp?.promise)
|
8
|
+
return rp;
|
9
|
+
|
10
|
+
const promise = method();
|
11
|
+
if (!rp) {
|
12
|
+
rp = {
|
13
|
+
promise,
|
14
|
+
cacheDuration,
|
15
|
+
}
|
16
|
+
}
|
17
|
+
else {
|
18
|
+
rp.promise = promise;
|
19
|
+
}
|
20
|
+
promise.finally(() => setTimeout(() => rp.promise = undefined, rp.cacheDuration));
|
21
|
+
return rp;
|
22
|
+
}
|
23
|
+
|
24
|
+
export class TimeoutError<T> extends Error {
|
25
|
+
constructor(public promise: Promise<T>) {
|
26
|
+
super('Operation Timed Out');
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
export function timeoutPromise<T>(timeout: number, promise: Promise<T>): Promise<T> {
|
31
|
+
return new Promise<T>((resolve, reject) => {
|
32
|
+
const t = setTimeout(() => reject(new TimeoutError(promise)), timeout);
|
33
|
+
|
34
|
+
promise
|
35
|
+
.then(v => {
|
36
|
+
clearTimeout(t);
|
37
|
+
resolve(v);
|
38
|
+
})
|
39
|
+
.catch(e => {
|
40
|
+
clearTimeout(t);
|
41
|
+
reject(e);
|
42
|
+
});
|
43
|
+
})
|
44
|
+
}
|
45
|
+
|
46
|
+
export function timeoutFunction<T>(timeout: number, f: (isTimedOut: () => boolean) => Promise<T>): Promise<T> {
|
47
|
+
return new Promise<T>((resolve, reject) => {
|
48
|
+
let isTimedOut = false;
|
49
|
+
const promise = f(() => isTimedOut);
|
50
|
+
|
51
|
+
const t = setTimeout(() => {
|
52
|
+
isTimedOut = true;
|
53
|
+
reject(new TimeoutError(promise));
|
54
|
+
}, timeout);
|
55
|
+
|
56
|
+
promise
|
57
|
+
.then(v => {
|
58
|
+
clearTimeout(t);
|
59
|
+
resolve(v);
|
60
|
+
})
|
61
|
+
.catch(e => {
|
62
|
+
clearTimeout(t);
|
63
|
+
reject(e);
|
64
|
+
});
|
65
|
+
})
|
66
|
+
}
|
67
|
+
|
68
|
+
export function createPromiseDebouncer<T>() {
|
69
|
+
let current: Promise<T>;
|
70
|
+
|
71
|
+
return (func: () => Promise<T>): Promise<T> => {
|
72
|
+
if (!current)
|
73
|
+
current = func().finally(() => current = undefined);
|
74
|
+
return current;
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
export function createMapPromiseDebouncer<T>() {
|
79
|
+
const map = new Map<string, Promise<T>>();
|
80
|
+
|
81
|
+
return (key: any, debounce: number, func: () => Promise<T>): Promise<T> => {
|
82
|
+
const keyStr = JSON.stringify(key);
|
83
|
+
let value = map.get(keyStr);
|
84
|
+
if (!value) {
|
85
|
+
value = func().finally(() => {
|
86
|
+
if (!debounce) {
|
87
|
+
map.delete(keyStr);
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
setTimeout(() => map.delete(keyStr), debounce);
|
91
|
+
});
|
92
|
+
map.set(keyStr, value);
|
93
|
+
}
|
94
|
+
return value;
|
95
|
+
}
|
96
|
+
}
|