@scrypted/server 0.0.82 → 0.0.86
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.
Potentially problematic release.
This version of @scrypted/server might be problematic. Click here for more details.
- package/dist/plugin/plugin-console.js +93 -0
- package/dist/plugin/plugin-console.js.map +1 -0
- package/dist/plugin/plugin-device.js +1 -1
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host.js +72 -173
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +2 -2
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote.js +7 -9
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +69 -0
- package/dist/plugin/plugin-repl.js.map +1 -0
- package/dist/runtime.js +2 -21
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +33 -20
- package/dist/scrypted-main.js.map +1 -1
- package/dist/services/plugin.js +8 -0
- package/dist/services/plugin.js.map +1 -1
- package/package.json +1 -1
- package/python/plugin-remote.py +64 -24
- package/python/rpc.py +1 -1
- package/src/plugin/plugin-console.ts +115 -0
- package/src/plugin/plugin-device.ts +1 -1
- package/src/plugin/plugin-host.ts +70 -197
- package/src/plugin/plugin-npm-dependencies.ts +2 -3
- package/src/plugin/plugin-remote.ts +11 -10
- package/src/plugin/plugin-repl.ts +74 -0
- package/src/runtime.ts +2 -24
- package/src/scrypted-main.ts +36 -22
- package/src/services/plugin.ts +8 -0
- package/test/test-cert.json +4 -0
package/python/plugin-remote.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
from asyncio.futures import Future
|
|
3
|
+
from asyncio.streams import StreamReader, StreamWriter
|
|
2
4
|
import os
|
|
3
5
|
from os import sys
|
|
6
|
+
from sys import stderr, stdout
|
|
4
7
|
more = os.path.join(os.getcwd(), 'node_modules/@scrypted/sdk')
|
|
5
8
|
sys.path.insert(0, more)
|
|
6
9
|
import scrypted_python.scrypted_sdk
|
|
7
|
-
from scrypted_python.scrypted_sdk.types import MediaManager, MediaObject, ScryptedInterfaceProperty
|
|
10
|
+
from scrypted_python.scrypted_sdk.types import DeviceManifest, MediaManager, MediaObject, ScryptedInterfaceProperty
|
|
8
11
|
|
|
9
12
|
from collections.abc import Mapping
|
|
10
13
|
from genericpath import exists
|
|
@@ -13,7 +16,7 @@ import asyncio
|
|
|
13
16
|
from asyncio.events import AbstractEventLoop
|
|
14
17
|
import json
|
|
15
18
|
import aiofiles
|
|
16
|
-
from typing import TypedDict
|
|
19
|
+
from typing import Tuple, TypedDict
|
|
17
20
|
import base64
|
|
18
21
|
import time
|
|
19
22
|
import zipfile
|
|
@@ -50,9 +53,9 @@ class DeviceState(scrypted_python.scrypted_sdk.DeviceState):
|
|
|
50
53
|
|
|
51
54
|
def setScryptedProperty(self, property: str, value: Any):
|
|
52
55
|
if property == ScryptedInterfaceProperty.id.value:
|
|
53
|
-
raise Exception("id is read only")
|
|
56
|
+
raise Exception("id is read only")
|
|
54
57
|
if property == ScryptedInterfaceProperty.mixins.value:
|
|
55
|
-
raise Exception("mixins is read only")
|
|
58
|
+
raise Exception("mixins is read only")
|
|
56
59
|
if property == ScryptedInterfaceProperty.interfaces.value:
|
|
57
60
|
raise Exception("interfaces is a read only post-mixin computed property, use providedInterfaces");
|
|
58
61
|
|
|
@@ -66,6 +69,11 @@ class DeviceState(scrypted_python.scrypted_sdk.DeviceState):
|
|
|
66
69
|
self.systemManager.api.setState(self.nativeId, property, value)
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
class DeviceStorage:
|
|
73
|
+
id: str
|
|
74
|
+
nativeId: str
|
|
75
|
+
storage: Mapping[str, str] = {}
|
|
76
|
+
|
|
69
77
|
class DeviceManager(scrypted_python.scrypted_sdk.DeviceManager):
|
|
70
78
|
def __init__(self, nativeIds: Mapping[str, DeviceStorage], systemManager: SystemManager) -> None:
|
|
71
79
|
super().__init__()
|
|
@@ -80,6 +88,9 @@ class DeviceManager(scrypted_python.scrypted_sdk.DeviceManager):
|
|
|
80
88
|
async def onDeviceEvent(self, nativeId: str, eventInterface: str, eventData: Any = None) -> None:
|
|
81
89
|
await self.systemManager.api.onDeviceEvent(nativeId, eventInterface, eventData)
|
|
82
90
|
|
|
91
|
+
async def onDevicesChanged(self, devices: DeviceManifest) -> None:
|
|
92
|
+
return await self.systemManager.api.onDevicesChanged(devices)
|
|
93
|
+
|
|
83
94
|
|
|
84
95
|
class BufferSerializer(rpc.RpcSerializer):
|
|
85
96
|
def serialize(self, value):
|
|
@@ -88,20 +99,13 @@ class BufferSerializer(rpc.RpcSerializer):
|
|
|
88
99
|
def deserialize(self, value):
|
|
89
100
|
return base64.b64decode(value)
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
class DeviceStorage:
|
|
93
|
-
id: str
|
|
94
|
-
nativeId: str
|
|
95
|
-
storage: Mapping[str, str] = {}
|
|
96
|
-
|
|
97
|
-
|
|
98
102
|
class PluginRemote:
|
|
99
103
|
systemState: Mapping[str, Mapping[str, SystemDeviceState]] = {}
|
|
100
104
|
nativeIds: Mapping[str, DeviceStorage] = {}
|
|
101
105
|
pluginId: str
|
|
102
106
|
mediaManager: MediaManager
|
|
103
107
|
loop: AbstractEventLoop
|
|
104
|
-
consoles: Mapping[str,
|
|
108
|
+
consoles: Mapping[str, Future[Tuple[StreamReader, StreamWriter]]] = {}
|
|
105
109
|
|
|
106
110
|
def __init__(self, api, pluginId, loop: AbstractEventLoop):
|
|
107
111
|
self.api = api
|
|
@@ -115,14 +119,36 @@ class PluginRemote:
|
|
|
115
119
|
'setNativeId',
|
|
116
120
|
]
|
|
117
121
|
|
|
118
|
-
def
|
|
119
|
-
end: Optional[str] =
|
|
120
|
-
flush: bool =
|
|
121
|
-
|
|
122
|
-
if not
|
|
123
|
-
|
|
124
|
-
self.consoles[nativeId] =
|
|
125
|
-
|
|
122
|
+
async def print_async(self, nativeId: str, *values: object, sep: Optional[str] = ' ',
|
|
123
|
+
end: Optional[str] = '\n',
|
|
124
|
+
flush: bool = False,):
|
|
125
|
+
consoleFuture = self.consoles.get(nativeId)
|
|
126
|
+
if not consoleFuture:
|
|
127
|
+
consoleFuture = Future()
|
|
128
|
+
self.consoles[nativeId] = consoleFuture
|
|
129
|
+
plugins = await self.api.getComponent('plugins')
|
|
130
|
+
port = await plugins.getRemoteServicePort(self.pluginId, 'console-writer')
|
|
131
|
+
connection = await asyncio.open_connection(port = port)
|
|
132
|
+
_, writer = connection
|
|
133
|
+
if not nativeId:
|
|
134
|
+
nid = 'undefined'
|
|
135
|
+
else:
|
|
136
|
+
nid = nativeId
|
|
137
|
+
nid += '\n'
|
|
138
|
+
writer.write(nid.encode('utf8'))
|
|
139
|
+
consoleFuture.set_result(connection)
|
|
140
|
+
_, writer = await consoleFuture
|
|
141
|
+
strio = StringIO()
|
|
142
|
+
print(*values, sep=sep, end=end, flush=flush, file=strio)
|
|
143
|
+
strio.seek(0)
|
|
144
|
+
b = strio.read().encode('utf8')
|
|
145
|
+
writer.write(b)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def print(self, nativeId: str, *values: object, sep: Optional[str] = ' ',
|
|
149
|
+
end: Optional[str] = '\n',
|
|
150
|
+
flush: bool = False,):
|
|
151
|
+
asyncio.run_coroutine_threadsafe(self.print_async(nativeId, *values, sep=sep, end=end, flush=flush), self.loop)
|
|
126
152
|
|
|
127
153
|
async def loadZip(self, packageJson, zipData, options=None):
|
|
128
154
|
zipPath = options['filename']
|
|
@@ -139,13 +165,14 @@ class PluginRemote:
|
|
|
139
165
|
|
|
140
166
|
if 'requirements.txt' in zip.namelist():
|
|
141
167
|
requirements = zip.open('requirements.txt').read()
|
|
142
|
-
str_requirements = requirements.decode('utf8')
|
|
168
|
+
str_requirements = requirements.decode('utf8')
|
|
143
169
|
|
|
144
170
|
requirementstxt = os.path.join(python_modules, 'requirements.txt')
|
|
171
|
+
installed_requirementstxt = os.path.join(python_modules, 'installed-requirements.txt')
|
|
145
172
|
|
|
146
173
|
need_pip = True
|
|
147
174
|
try:
|
|
148
|
-
existing = open(
|
|
175
|
+
existing = open(installed_requirementstxt).read()
|
|
149
176
|
need_pip = existing != str_requirements
|
|
150
177
|
except:
|
|
151
178
|
pass
|
|
@@ -159,8 +186,21 @@ class PluginRemote:
|
|
|
159
186
|
f.close()
|
|
160
187
|
|
|
161
188
|
# os.system('pip install -r %s --target %s' % (requirementstxt, python_modules))
|
|
162
|
-
|
|
163
|
-
|
|
189
|
+
p = subprocess.Popen(['pip', 'install', '-r', requirementstxt, '--target', python_modules], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
190
|
+
while True:
|
|
191
|
+
line = p.stdout.readline()
|
|
192
|
+
if not line:
|
|
193
|
+
break
|
|
194
|
+
line = line.decode('utf8').rstrip('\r\n')
|
|
195
|
+
print(line)
|
|
196
|
+
result = p.wait()
|
|
197
|
+
print('pip install result %s' % result)
|
|
198
|
+
if result:
|
|
199
|
+
raise Exception('non-zero result from pip %s' % result)
|
|
200
|
+
|
|
201
|
+
f = open(installed_requirementstxt, 'wb')
|
|
202
|
+
f.write(requirements)
|
|
203
|
+
f.close()
|
|
164
204
|
else:
|
|
165
205
|
print('requirements.txt (up to date)')
|
|
166
206
|
print(str_requirements)
|
package/python/rpc.py
CHANGED
|
@@ -276,7 +276,7 @@ class RpcPeer:
|
|
|
276
276
|
print('failure', method, e)
|
|
277
277
|
tb = traceback.format_exc()
|
|
278
278
|
self.createErrorResult(
|
|
279
|
-
result, type(e).
|
|
279
|
+
result, type(e).__name__, str(e), tb)
|
|
280
280
|
|
|
281
281
|
if not message.get('oneway', False):
|
|
282
282
|
self.send(result)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ScryptedNativeId } from '@scrypted/sdk/types'
|
|
2
|
+
import { EventEmitter } from 'ws';
|
|
3
|
+
import { listenZero } from './listen-zero';
|
|
4
|
+
import { Server } from 'net';
|
|
5
|
+
import { once } from 'events';
|
|
6
|
+
import net from 'net'
|
|
7
|
+
import { Readable } from 'stream';
|
|
8
|
+
|
|
9
|
+
export interface ConsoleServer {
|
|
10
|
+
readPort: number,
|
|
11
|
+
writePort: number,
|
|
12
|
+
readServer: net.Server,
|
|
13
|
+
writeServer: net.Server,
|
|
14
|
+
sockets: Set<net.Socket>;
|
|
15
|
+
}
|
|
16
|
+
export async function createConsoleServer(stdout: Readable, stderr: Readable) {
|
|
17
|
+
const outputs = new Map<string, Buffer[]>();
|
|
18
|
+
const appendOutput = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
19
|
+
if (!nativeId)
|
|
20
|
+
nativeId = undefined;
|
|
21
|
+
let buffers = outputs.get(nativeId);
|
|
22
|
+
if (!buffers) {
|
|
23
|
+
buffers = [];
|
|
24
|
+
outputs.set(nativeId, buffers);
|
|
25
|
+
}
|
|
26
|
+
buffers.push(data);
|
|
27
|
+
// when we're over 4000 lines or whatever these buffer are,
|
|
28
|
+
// truncate down to 2000.
|
|
29
|
+
if (buffers.length > 4000)
|
|
30
|
+
outputs.set(nativeId, buffers.slice(buffers.length - 2000))
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sockets = new Set<net.Socket>();
|
|
34
|
+
|
|
35
|
+
const events = new EventEmitter();
|
|
36
|
+
events.on('stdout', appendOutput);
|
|
37
|
+
events.on('stderr', appendOutput);
|
|
38
|
+
|
|
39
|
+
stdout.on('data', data => events.emit('stdout', data));
|
|
40
|
+
stderr.on('data', data => events.emit('stderr', data));
|
|
41
|
+
|
|
42
|
+
const readServer = new Server(async (socket) => {
|
|
43
|
+
sockets.add(socket);
|
|
44
|
+
|
|
45
|
+
let [filter] = await once(socket, 'data');
|
|
46
|
+
filter = filter.toString().trim();
|
|
47
|
+
if (filter === 'undefined')
|
|
48
|
+
filter = undefined;
|
|
49
|
+
|
|
50
|
+
const buffers = outputs.get(filter);
|
|
51
|
+
if (buffers) {
|
|
52
|
+
const concat = Buffer.concat(buffers);
|
|
53
|
+
outputs.set(filter, [concat]);
|
|
54
|
+
socket.write(concat);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cb = (data: Buffer, nativeId: ScryptedNativeId) => {
|
|
58
|
+
if (nativeId !== filter)
|
|
59
|
+
return;
|
|
60
|
+
socket.write(data);
|
|
61
|
+
};
|
|
62
|
+
events.on('stdout', cb)
|
|
63
|
+
events.on('stderr', cb)
|
|
64
|
+
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
events.removeListener('stdout', cb);
|
|
67
|
+
events.removeListener('stderr', cb);
|
|
68
|
+
socket.destroy();
|
|
69
|
+
socket.removeAllListeners();
|
|
70
|
+
sockets.delete(socket);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
socket.on('close', cleanup);
|
|
74
|
+
socket.on('error', cleanup);
|
|
75
|
+
socket.on('end', cleanup);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const writeServer = new Server(async (socket) => {
|
|
79
|
+
sockets.add(socket);
|
|
80
|
+
const [data] = await once(socket, 'data');
|
|
81
|
+
let filter: string = data.toString();
|
|
82
|
+
const newline = filter.indexOf('\n');
|
|
83
|
+
if (newline !== -1) {
|
|
84
|
+
socket.unshift(Buffer.from(filter.substring(newline + 1)));
|
|
85
|
+
}
|
|
86
|
+
filter = filter.substring(0, newline);
|
|
87
|
+
|
|
88
|
+
if (filter === 'undefined')
|
|
89
|
+
filter = undefined;
|
|
90
|
+
|
|
91
|
+
const cb = (data: Buffer) => events.emit('stdout', data, filter);
|
|
92
|
+
|
|
93
|
+
socket.on('data', cb);
|
|
94
|
+
|
|
95
|
+
const cleanup = () => {
|
|
96
|
+
socket.destroy();
|
|
97
|
+
socket.removeAllListeners();
|
|
98
|
+
sockets.delete(socket);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
socket.once('close', cleanup);
|
|
102
|
+
socket.once('error', cleanup);
|
|
103
|
+
socket.once('end', cleanup);
|
|
104
|
+
});
|
|
105
|
+
const readPort = await listenZero(readServer);
|
|
106
|
+
const writePort = await listenZero(writeServer);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
readPort,
|
|
110
|
+
writePort,
|
|
111
|
+
readServer,
|
|
112
|
+
writeServer,
|
|
113
|
+
sockets,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -16,7 +16,6 @@ interface MixinTable {
|
|
|
16
16
|
export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
|
|
17
17
|
scrypted: ScryptedRuntime;
|
|
18
18
|
id: string;
|
|
19
|
-
// proxy: Promise<any>;
|
|
20
19
|
mixinTable: Promise<MixinTable[]>;
|
|
21
20
|
|
|
22
21
|
constructor(scrypted: ScryptedRuntime, id: string) {
|
|
@@ -24,6 +23,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
24
23
|
this.id = id;
|
|
25
24
|
}
|
|
26
25
|
|
|
26
|
+
// should this be async?
|
|
27
27
|
invalidate() {
|
|
28
28
|
const mixinTable = this.mixinTable;
|
|
29
29
|
this.mixinTable = undefined;
|