@scrypted/server 0.94.4 → 0.94.5
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/package.json +1 -1
- package/python/plugin_repl.py +115 -12
package/package.json
CHANGED
package/python/plugin_repl.py
CHANGED
@@ -1,11 +1,25 @@
|
|
1
1
|
import asyncio
|
2
2
|
import concurrent.futures
|
3
|
+
from functools import partial
|
4
|
+
import inspect
|
5
|
+
import prompt_toolkit
|
3
6
|
from prompt_toolkit import print_formatted_text
|
4
|
-
from prompt_toolkit.
|
7
|
+
from prompt_toolkit.application import Application
|
8
|
+
import prompt_toolkit.application.current
|
9
|
+
import prompt_toolkit.key_binding.key_processor
|
10
|
+
import prompt_toolkit.contrib.telnet.server
|
11
|
+
from prompt_toolkit.contrib.telnet.server import TelnetServer, TelnetConnection
|
12
|
+
from prompt_toolkit.shortcuts import clear_title, set_title
|
5
13
|
from ptpython.repl import embed, PythonRepl
|
14
|
+
import ptpython.key_bindings
|
15
|
+
import ptpython.python_input
|
16
|
+
import ptpython.history_browser
|
17
|
+
import ptpython.layout
|
6
18
|
import socket
|
7
19
|
import telnetlib
|
8
20
|
import threading
|
21
|
+
import traceback
|
22
|
+
import types
|
9
23
|
from typing import List, Dict, Any
|
10
24
|
|
11
25
|
from scrypted_python.scrypted_sdk import ScryptedStatic, ScryptedDevice
|
@@ -13,10 +27,90 @@ from scrypted_python.scrypted_sdk import ScryptedStatic, ScryptedDevice
|
|
13
27
|
from rpc import maybe_await
|
14
28
|
|
15
29
|
|
30
|
+
# This section is a bit of a hack - prompt_toolkit has many assumptions
|
31
|
+
# that there is only one global Application, so multiple REPLs will confuse
|
32
|
+
# the library. The patches here allow us to scope a particular call stack
|
33
|
+
# to a particular REPL, and to get the current Application from the stack.
|
34
|
+
default_get_app = prompt_toolkit.application.current.get_app
|
35
|
+
def get_app_patched() -> Application[Any]:
|
36
|
+
stack = inspect.stack()
|
37
|
+
for frame in stack:
|
38
|
+
self_var = frame.frame.f_locals.get("self")
|
39
|
+
if self_var is not None and isinstance(self_var, Application):
|
40
|
+
return self_var
|
41
|
+
return default_get_app()
|
42
|
+
prompt_toolkit.application.current.get_app = get_app_patched
|
43
|
+
prompt_toolkit.key_binding.key_processor.get_app = get_app_patched
|
44
|
+
prompt_toolkit.contrib.telnet.server.get_app = get_app_patched
|
45
|
+
ptpython.python_input.get_app = get_app_patched
|
46
|
+
ptpython.key_bindings.get_app = get_app_patched
|
47
|
+
ptpython.history_browser.get_app = get_app_patched
|
48
|
+
ptpython.layout.get_app = get_app_patched
|
49
|
+
|
50
|
+
|
51
|
+
async def run_async_patched(self: PythonRepl) -> None:
|
52
|
+
# This is a patched version of PythonRepl.run_async to handle an
|
53
|
+
# AssertionError raised by prompt_toolkit when the TelnetServer exits.
|
54
|
+
# Original: https://github.com/prompt-toolkit/ptpython/blob/3.0.26/ptpython/repl.py#L215
|
55
|
+
|
56
|
+
"""
|
57
|
+
Run the REPL loop, but run the blocking parts in an executor, so that
|
58
|
+
we don't block the event loop. Both the input and output (which can
|
59
|
+
display a pager) will run in a separate thread with their own event
|
60
|
+
loop, this way ptpython's own event loop won't interfere with the
|
61
|
+
asyncio event loop from where this is called.
|
62
|
+
|
63
|
+
The "eval" however happens in the current thread, which is important.
|
64
|
+
(Both for control-C to work, as well as for the code to see the right
|
65
|
+
thread in which it was embedded).
|
66
|
+
"""
|
67
|
+
loop = asyncio.get_running_loop()
|
68
|
+
|
69
|
+
if self.terminal_title:
|
70
|
+
set_title(self.terminal_title)
|
71
|
+
|
72
|
+
self._add_to_namespace()
|
73
|
+
|
74
|
+
try:
|
75
|
+
while True:
|
76
|
+
try:
|
77
|
+
# Read.
|
78
|
+
try:
|
79
|
+
text = await loop.run_in_executor(None, self.read)
|
80
|
+
except EOFError:
|
81
|
+
return
|
82
|
+
except asyncio.CancelledError:
|
83
|
+
return
|
84
|
+
except AssertionError:
|
85
|
+
return
|
86
|
+
except BaseException:
|
87
|
+
# Something went wrong while reading input.
|
88
|
+
# (E.g., a bug in the completer that propagates. Don't
|
89
|
+
# crash the REPL.)
|
90
|
+
traceback.print_exc()
|
91
|
+
continue
|
92
|
+
|
93
|
+
# Eval.
|
94
|
+
await self.run_and_show_expression_async(text)
|
95
|
+
|
96
|
+
except KeyboardInterrupt as e:
|
97
|
+
# XXX: This does not yet work properly. In some situations,
|
98
|
+
# `KeyboardInterrupt` exceptions can end up in the event
|
99
|
+
# loop selector.
|
100
|
+
self._handle_keyboard_interrupt(e)
|
101
|
+
except SystemExit:
|
102
|
+
return
|
103
|
+
finally:
|
104
|
+
if self.terminal_title:
|
105
|
+
clear_title()
|
106
|
+
self._remove_from_namespace()
|
107
|
+
|
108
|
+
|
16
109
|
def configure(repl: PythonRepl) -> None:
|
17
110
|
repl.confirm_exit = False
|
18
111
|
repl.enable_system_bindings = False
|
19
112
|
repl.enable_mouse_support = False
|
113
|
+
repl.run_async = types.MethodType(run_async_patched, repl)
|
20
114
|
|
21
115
|
|
22
116
|
async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
@@ -29,9 +123,11 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
29
123
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
30
124
|
sock.settimeout(None)
|
31
125
|
sock.bind(('localhost', 0))
|
32
|
-
sock.listen(
|
126
|
+
sock.listen()
|
127
|
+
|
128
|
+
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
33
129
|
|
34
|
-
async def start_telnet_repl(future, filter) -> None:
|
130
|
+
async def start_telnet_repl(future: concurrent.futures.Future, filter: str) -> None:
|
35
131
|
if filter == "undefined":
|
36
132
|
filter = None
|
37
133
|
|
@@ -58,11 +154,13 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
58
154
|
telnet_port = s.getsockname()[1]
|
59
155
|
s.close()
|
60
156
|
|
61
|
-
async def interact(connection) -> None:
|
157
|
+
async def interact(connection: TelnetConnection) -> None:
|
158
|
+
repl_print = partial(print_formatted_text, output=connection.vt100_output)
|
62
159
|
global_dict = {
|
63
160
|
**globals(),
|
64
|
-
"print":
|
65
|
-
"help": lambda *args, **kwargs:
|
161
|
+
"print": repl_print,
|
162
|
+
"help": lambda *args, **kwargs: repl_print("Help is not available in this environment"),
|
163
|
+
"input": lambda *args, **kwargs: repl_print("Input is not available in this environment"),
|
66
164
|
}
|
67
165
|
locals_dict = {
|
68
166
|
"device": device,
|
@@ -77,13 +175,13 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
77
175
|
print_formatted_text(banner)
|
78
176
|
await embed(return_asyncio_coroutine=True, globals=global_dict, locals=locals_dict, configure=configure)
|
79
177
|
|
178
|
+
server_task: asyncio.Task = None
|
179
|
+
def ready_cb():
|
180
|
+
future.set_result((telnet_port, lambda: loop.call_soon_threadsafe(server_task.cancel)))
|
181
|
+
|
80
182
|
# Start the REPL server
|
81
183
|
telnet_server = TelnetServer(interact=interact, port=telnet_port, enable_cpr=False)
|
82
|
-
telnet_server.
|
83
|
-
|
84
|
-
future.set_result(telnet_port)
|
85
|
-
|
86
|
-
loop = asyncio.get_event_loop()
|
184
|
+
server_task = asyncio.create_task(telnet_server.run(ready_cb=ready_cb))
|
87
185
|
|
88
186
|
def handle_connection(conn: socket.socket):
|
89
187
|
conn.settimeout(None)
|
@@ -91,7 +189,7 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
91
189
|
|
92
190
|
future = concurrent.futures.Future()
|
93
191
|
loop.call_soon_threadsafe(loop.create_task, start_telnet_repl(future, filter))
|
94
|
-
telnet_port = future.result()
|
192
|
+
telnet_port, exit_server = future.result()
|
95
193
|
|
96
194
|
telnet_client = telnetlib.Telnet('localhost', telnet_port, timeout=None)
|
97
195
|
|
@@ -111,6 +209,9 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
111
209
|
if not data:
|
112
210
|
break
|
113
211
|
telnet_client.write(data)
|
212
|
+
telnet_client.close()
|
213
|
+
exit_server()
|
214
|
+
|
114
215
|
def forward_to_socket():
|
115
216
|
prompt_count = 0
|
116
217
|
while True:
|
@@ -128,6 +229,8 @@ async def createREPLServer(sdk: ScryptedStatic, plugin: ScryptedDevice) -> int:
|
|
128
229
|
if prompt_count < 5:
|
129
230
|
data = data.replace(b">>>", b" ")
|
130
231
|
conn.sendall(data)
|
232
|
+
conn.close()
|
233
|
+
exit_server()
|
131
234
|
|
132
235
|
threading.Thread(target=forward_to_telnet).start()
|
133
236
|
threading.Thread(target=forward_to_socket).start()
|