@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/python/plugin_repl.py +115 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrypted/server",
3
- "version": "0.94.4",
3
+ "version": "0.94.5",
4
4
  "description": "",
5
5
  "dependencies": {
6
6
  "@mapbox/node-pre-gyp": "^1.0.11",
@@ -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.contrib.telnet.server import TelnetServer
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(1)
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": print_formatted_text,
65
- "help": lambda *args, **kwargs: print_formatted_text("Help is not available in this environment"),
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.start()
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()