@scrypted/server 0.0.87 → 0.0.91
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/event-registry.js +8 -2
- package/dist/event-registry.js.map +1 -1
- package/dist/plugin/plugin-console.js +44 -30
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-device.js +159 -69
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +3 -2
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +13 -5
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote.js +0 -1
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +1 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/runtime.js +10 -2
- 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 +6 -3
- package/python/rpc.py +5 -5
- package/src/event-registry.ts +7 -2
- package/src/plugin/plugin-console.ts +59 -34
- package/src/plugin/plugin-device.ts +187 -79
- package/src/plugin/plugin-host-api.ts +3 -2
- package/src/plugin/plugin-host.ts +17 -6
- package/src/plugin/plugin-remote.ts +1 -2
- package/src/plugin/plugin-repl.ts +1 -1
- package/src/runtime.ts +12 -3
- package/src/services/plugin.ts +1 -1
|
@@ -4,40 +4,67 @@ import { listenZero } from './listen-zero';
|
|
|
4
4
|
import { Server } from 'net';
|
|
5
5
|
import { once } from 'events';
|
|
6
6
|
import net from 'net'
|
|
7
|
-
import { Readable } from 'stream';
|
|
7
|
+
import { Readable, PassThrough } from 'stream';
|
|
8
|
+
import { Console } from 'console';
|
|
8
9
|
|
|
9
10
|
export interface ConsoleServer {
|
|
11
|
+
pluginConsole: Console;
|
|
10
12
|
readPort: number,
|
|
11
13
|
writePort: number,
|
|
12
14
|
readServer: net.Server,
|
|
13
15
|
writeServer: net.Server,
|
|
14
16
|
sockets: Set<net.Socket>;
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
export interface StdPassThroughs {
|
|
20
|
+
stdout: PassThrough;
|
|
21
|
+
stderr: PassThrough;
|
|
22
|
+
buffers: Buffer[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createConsoleServer(remoteStdout: Readable, remoteStderr: Readable) {
|
|
26
|
+
const outputs = new Map<string, StdPassThroughs>();
|
|
27
|
+
|
|
28
|
+
const getPassthroughs = (nativeId?: ScryptedNativeId) => {
|
|
19
29
|
if (!nativeId)
|
|
20
30
|
nativeId = undefined;
|
|
21
|
-
let
|
|
22
|
-
if (!
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
let pts = outputs.get(nativeId)
|
|
32
|
+
if (!pts) {
|
|
33
|
+
const stdout = new PassThrough();
|
|
34
|
+
const stderr = new PassThrough();
|
|
35
|
+
|
|
36
|
+
pts = {
|
|
37
|
+
stdout,
|
|
38
|
+
stderr,
|
|
39
|
+
buffers: [],
|
|
40
|
+
}
|
|
41
|
+
outputs.set(nativeId, pts);
|
|
42
|
+
|
|
43
|
+
const appendOutput = (data: Buffer) => {
|
|
44
|
+
const { buffers } = pts;
|
|
45
|
+
buffers.push(data);
|
|
46
|
+
// when we're over 4000 lines or whatever these buffer are,
|
|
47
|
+
// truncate down to 2000.
|
|
48
|
+
if (buffers.length > 4000)
|
|
49
|
+
pts.buffers = buffers.slice(buffers.length - 2000);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
stdout.on('data', appendOutput);
|
|
53
|
+
stderr.on('data', appendOutput);
|
|
25
54
|
}
|
|
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
55
|
|
|
33
|
-
|
|
56
|
+
return pts;
|
|
57
|
+
}
|
|
34
58
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
let pluginConsole: Console;
|
|
60
|
+
{
|
|
61
|
+
const { stdout, stderr } = getPassthroughs();
|
|
62
|
+
remoteStdout.pipe(stdout);
|
|
63
|
+
remoteStderr.pipe(stderr);
|
|
64
|
+
pluginConsole = new Console(stdout, stderr);
|
|
65
|
+
}
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
stderr.on('data', data => events.emit('stderr', data));
|
|
67
|
+
const sockets = new Set<net.Socket>();
|
|
41
68
|
|
|
42
69
|
const readServer = new Server(async (socket) => {
|
|
43
70
|
sockets.add(socket);
|
|
@@ -47,24 +74,22 @@ export async function createConsoleServer(stdout: Readable, stderr: Readable) {
|
|
|
47
74
|
if (filter === 'undefined')
|
|
48
75
|
filter = undefined;
|
|
49
76
|
|
|
50
|
-
const
|
|
77
|
+
const pts = outputs.get(filter);
|
|
78
|
+
const buffers = pts?.buffers;
|
|
51
79
|
if (buffers) {
|
|
52
80
|
const concat = Buffer.concat(buffers);
|
|
53
|
-
|
|
81
|
+
pts.buffers = [concat];
|
|
54
82
|
socket.write(concat);
|
|
55
83
|
}
|
|
56
84
|
|
|
57
|
-
const cb = (data: Buffer
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
events.on('stdout', cb)
|
|
63
|
-
events.on('stderr', cb)
|
|
85
|
+
const cb = (data: Buffer) => socket.write(data);
|
|
86
|
+
const { stdout, stderr } = getPassthroughs(filter);
|
|
87
|
+
stdout.on('data', cb);
|
|
88
|
+
stderr.on('data', cb);
|
|
64
89
|
|
|
65
90
|
const cleanup = () => {
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
stdout.removeListener('data', cb);
|
|
92
|
+
stderr.removeListener('data', cb);
|
|
68
93
|
socket.destroy();
|
|
69
94
|
socket.removeAllListeners();
|
|
70
95
|
sockets.delete(socket);
|
|
@@ -88,9 +113,8 @@ export async function createConsoleServer(stdout: Readable, stderr: Readable) {
|
|
|
88
113
|
if (filter === 'undefined')
|
|
89
114
|
filter = undefined;
|
|
90
115
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
socket.on('data', cb);
|
|
116
|
+
const { stdout } = getPassthroughs(filter);
|
|
117
|
+
socket.pipe(stdout, { end: false });
|
|
94
118
|
|
|
95
119
|
const cleanup = () => {
|
|
96
120
|
socket.destroy();
|
|
@@ -106,6 +130,7 @@ export async function createConsoleServer(stdout: Readable, stderr: Readable) {
|
|
|
106
130
|
const writePort = await listenZero(writeServer);
|
|
107
131
|
|
|
108
132
|
return {
|
|
133
|
+
pluginConsole,
|
|
109
134
|
readPort,
|
|
110
135
|
writePort,
|
|
111
136
|
readServer,
|
|
@@ -9,114 +9,220 @@ import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from
|
|
|
9
9
|
|
|
10
10
|
interface MixinTable {
|
|
11
11
|
mixinProviderId: string;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
entry: Promise<MixinTableEntry>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MixinTableEntry {
|
|
16
|
+
interfaces: Set<string>
|
|
17
|
+
allInterfaces: string[];
|
|
18
|
+
proxy: any;
|
|
19
|
+
error?: Error;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevice {
|
|
17
23
|
scrypted: ScryptedRuntime;
|
|
18
24
|
id: string;
|
|
19
|
-
mixinTable:
|
|
25
|
+
mixinTable: MixinTable[];
|
|
20
26
|
|
|
21
27
|
constructor(scrypted: ScryptedRuntime, id: string) {
|
|
22
28
|
this.scrypted = scrypted;
|
|
23
29
|
this.id = id;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
invalidateEntry(mixinEntry: MixinTable) {
|
|
33
|
+
if (!mixinEntry.mixinProviderId)
|
|
34
|
+
return;
|
|
35
|
+
(async () => {
|
|
36
|
+
const mixinProvider = this.scrypted.getDevice(mixinEntry.mixinProviderId) as ScryptedDevice & MixinProvider;
|
|
37
|
+
const { proxy } = await mixinEntry.entry;
|
|
38
|
+
mixinProvider?.releaseMixin(this.id, proxy);
|
|
39
|
+
})().catch(() => { });
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
// should this be async?
|
|
27
43
|
invalidate() {
|
|
28
44
|
const mixinTable = this.mixinTable;
|
|
29
45
|
this.mixinTable = undefined;
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
continue;
|
|
34
|
-
(async () => {
|
|
35
|
-
const mixinProvider = this.scrypted.getDevice(mixinEntry.mixinProviderId) as ScryptedDevice & MixinProvider;
|
|
36
|
-
mixinProvider?.releaseMixin(this.id, await mixinEntry.proxy);
|
|
37
|
-
})().catch(() => { });
|
|
38
|
-
}
|
|
39
|
-
})().catch(() => { });;
|
|
46
|
+
for (const mixinEntry of (mixinTable || [])) {
|
|
47
|
+
this.invalidateEntry(mixinEntry);
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
invalidateMixinTable() {
|
|
52
|
+
if (!this.mixinTable)
|
|
53
|
+
return this.invalidate();
|
|
54
|
+
|
|
55
|
+
let previousMixinIds = this.mixinTable?.map(entry => entry.mixinProviderId) || [];
|
|
56
|
+
previousMixinIds.pop();
|
|
57
|
+
previousMixinIds = previousMixinIds.reverse();
|
|
58
|
+
|
|
44
59
|
const pluginDevice = this.scrypted.findPluginDeviceById(this.id);
|
|
45
60
|
if (!pluginDevice)
|
|
46
|
-
|
|
61
|
+
return this.invalidate();
|
|
62
|
+
|
|
63
|
+
const mixins = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
|
|
64
|
+
// iterate the new mixin table to find the last good mixin,
|
|
65
|
+
// and resume creation from there.
|
|
66
|
+
let lastValidMixinId: string;
|
|
67
|
+
for (const mixinId of mixins) {
|
|
68
|
+
if (!previousMixinIds.length) {
|
|
69
|
+
// reached of the previous mixin table, meaning
|
|
70
|
+
// mixins were added.
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
const check = previousMixinIds.shift();
|
|
74
|
+
if (check !== mixinId)
|
|
75
|
+
break;
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
lastValidMixinId = mixinId;
|
|
78
|
+
}
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!pluginDevice.nativeId) {
|
|
54
|
-
const plugin = this.scrypted.plugins[pluginDevice.pluginId];
|
|
55
|
-
proxy = await plugin.module;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
const providerId = getState(pluginDevice, ScryptedInterfaceProperty.providerId);
|
|
59
|
-
const provider = this.scrypted.getDevice(providerId) as ScryptedDevice & DeviceProvider;
|
|
60
|
-
proxy = await provider.getDevice(pluginDevice.nativeId);
|
|
61
|
-
}
|
|
80
|
+
if (!lastValidMixinId)
|
|
81
|
+
return this.invalidate();
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
// invalidate and remove everything up to lastValidMixinId
|
|
84
|
+
while (true) {
|
|
85
|
+
const entry = this.mixinTable[0];
|
|
86
|
+
if (entry.mixinProviderId === lastValidMixinId)
|
|
87
|
+
break;
|
|
88
|
+
this.mixinTable.shift();
|
|
89
|
+
this.invalidateEntry(entry);
|
|
90
|
+
}
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
mixinProviderId: undefined,
|
|
72
|
-
interfaces: allInterfaces.slice(),
|
|
73
|
-
proxy,
|
|
74
|
-
})
|
|
92
|
+
this.ensureProxy(lastValidMixinId);
|
|
93
|
+
}
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
// this must not be async, because it potentially changes execution order.
|
|
96
|
+
ensureProxy(lastValidMixinId?: string): Promise<PluginDevice> {
|
|
97
|
+
const pluginDevice = this.scrypted.findPluginDeviceById(this.id);
|
|
98
|
+
if (!pluginDevice)
|
|
99
|
+
throw new Error(`device ${this.id} does not exist`);
|
|
100
|
+
|
|
101
|
+
let previousEntry: Promise<MixinTableEntry>;
|
|
102
|
+
if (!lastValidMixinId) {
|
|
103
|
+
if (this.mixinTable)
|
|
104
|
+
return Promise.resolve(pluginDevice);
|
|
105
|
+
|
|
106
|
+
this.mixinTable = [];
|
|
78
107
|
|
|
108
|
+
previousEntry = (async () => {
|
|
109
|
+
let proxy;
|
|
79
110
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
if (!pluginDevice.nativeId) {
|
|
112
|
+
const plugin = this.scrypted.plugins[pluginDevice.pluginId];
|
|
113
|
+
proxy = await plugin.module;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const providerId = getState(pluginDevice, ScryptedInterfaceProperty.providerId);
|
|
117
|
+
const provider = this.scrypted.getDevice(providerId) as ScryptedDevice & DeviceProvider;
|
|
118
|
+
proxy = await provider.getDevice(pluginDevice.nativeId);
|
|
87
119
|
}
|
|
88
120
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const wrappedProxy = new Proxy(wrappedHandler, wrappedHandler);
|
|
92
|
-
|
|
93
|
-
const host = this.scrypted.getPluginHostForDeviceId(mixinId);
|
|
94
|
-
const deviceState = await host.remote.createDeviceState(this.id,
|
|
95
|
-
async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value));
|
|
96
|
-
const mixinProxy = await mixinProvider.getMixin(wrappedProxy, allInterfaces, deviceState);
|
|
97
|
-
if (!mixinProxy)
|
|
98
|
-
throw new Error(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
|
|
99
|
-
allInterfaces.push(...interfaces);
|
|
100
|
-
proxy = mixinProxy;
|
|
101
|
-
|
|
102
|
-
mixinTable.unshift({
|
|
103
|
-
mixinProviderId: mixinId,
|
|
104
|
-
interfaces,
|
|
105
|
-
proxy,
|
|
106
|
-
})
|
|
121
|
+
if (!proxy)
|
|
122
|
+
console.warn('no device was returned by the plugin', this.id);
|
|
107
123
|
}
|
|
108
124
|
catch (e) {
|
|
109
|
-
console.warn(
|
|
125
|
+
console.warn('error occured retrieving device from plugin');
|
|
110
126
|
}
|
|
111
|
-
}
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
const interfaces: ScryptedInterface[] = getState(pluginDevice, ScryptedInterfaceProperty.providedInterfaces) || [];
|
|
129
|
+
return {
|
|
130
|
+
proxy,
|
|
131
|
+
interfaces: new Set<string>(interfaces),
|
|
132
|
+
allInterfaces: interfaces,
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
|
|
136
|
+
this.mixinTable.unshift({
|
|
137
|
+
mixinProviderId: undefined,
|
|
138
|
+
entry: previousEntry,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
if (!this.mixinTable)
|
|
143
|
+
throw new Error('mixin table partial invalidation was called with empty mixin table');
|
|
144
|
+
const prevTable = this.mixinTable.find(table => table.mixinProviderId === lastValidMixinId);
|
|
145
|
+
if (!prevTable)
|
|
146
|
+
throw new Error('mixin table partial invalidation was called with invalid lastValidMixinId');
|
|
147
|
+
previousEntry = prevTable.entry;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const type = getDisplayType(pluginDevice);
|
|
151
|
+
|
|
152
|
+
for (const mixinId of getState(pluginDevice, ScryptedInterfaceProperty.mixins) || []) {
|
|
153
|
+
if (lastValidMixinId) {
|
|
154
|
+
if (mixinId === lastValidMixinId)
|
|
155
|
+
lastValidMixinId = undefined;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
115
158
|
|
|
116
|
-
|
|
117
|
-
|
|
159
|
+
const wrappedMixinTable = this.mixinTable.slice();
|
|
160
|
+
|
|
161
|
+
this.mixinTable.unshift({
|
|
162
|
+
mixinProviderId: mixinId,
|
|
163
|
+
entry: (async () => {
|
|
164
|
+
let { allInterfaces } = await previousEntry;
|
|
165
|
+
try {
|
|
166
|
+
const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider;
|
|
167
|
+
const interfaces = await mixinProvider?.canMixin(type, allInterfaces) as any as ScryptedInterface[];
|
|
168
|
+
if (!interfaces) {
|
|
169
|
+
// this is not an error
|
|
170
|
+
// do not advertise interfaces so it is skipped during
|
|
171
|
+
// vtable lookup.
|
|
172
|
+
console.log(`mixin provider ${mixinId} can no longer mixin ${this.id}`);
|
|
173
|
+
const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || [];
|
|
174
|
+
this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, mixins.filter(mid => mid !== mixinId))
|
|
175
|
+
this.scrypted.datastore.upsert(pluginDevice);
|
|
176
|
+
return {
|
|
177
|
+
allInterfaces,
|
|
178
|
+
interfaces: new Set<string>(),
|
|
179
|
+
proxy: undefined as any,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
allInterfaces = allInterfaces.slice();
|
|
184
|
+
allInterfaces.push(...interfaces);
|
|
185
|
+
const combinedInterfaces = [...new Set(allInterfaces)];
|
|
186
|
+
|
|
187
|
+
const wrappedHandler = new PluginDeviceProxyHandler(this.scrypted, this.id);
|
|
188
|
+
wrappedHandler.mixinTable = wrappedMixinTable;
|
|
189
|
+
const wrappedProxy = new Proxy(wrappedHandler, wrappedHandler);
|
|
190
|
+
|
|
191
|
+
const host = this.scrypted.getPluginHostForDeviceId(mixinId);
|
|
192
|
+
const deviceState = await host.remote.createDeviceState(this.id,
|
|
193
|
+
async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value));
|
|
194
|
+
const mixinProxy = await mixinProvider.getMixin(wrappedProxy, allInterfaces as ScryptedInterface[], deviceState);
|
|
195
|
+
if (!mixinProxy)
|
|
196
|
+
throw new Error(`mixin provider ${mixinId} did not return mixin for ${this.id}`);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
interfaces: new Set<string>(interfaces),
|
|
200
|
+
allInterfaces: combinedInterfaces,
|
|
201
|
+
proxy: mixinProxy,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
// on any error, do not advertise interfaces
|
|
206
|
+
// on this mixin, so as to prevent total failure?
|
|
207
|
+
// this has been the behavior for a while,
|
|
208
|
+
// but maybe interfaces implemented by that mixin
|
|
209
|
+
// should rethrow the error caught here in applyMixin.
|
|
210
|
+
console.warn(e);
|
|
211
|
+
return {
|
|
212
|
+
allInterfaces,
|
|
213
|
+
interfaces: new Set<string>(),
|
|
214
|
+
error: e,
|
|
215
|
+
proxy: undefined as any,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
})(),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
118
221
|
|
|
119
|
-
return this.mixinTable.then(
|
|
222
|
+
return this.mixinTable[0].entry.then(entry => {
|
|
223
|
+
this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.interfaces, entry.allInterfaces);
|
|
224
|
+
return pluginDevice;
|
|
225
|
+
});
|
|
120
226
|
}
|
|
121
227
|
|
|
122
228
|
get(target: any, p: PropertyKey, receiver: any): any {
|
|
@@ -180,12 +286,14 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
|
|
|
180
286
|
if (!iface)
|
|
181
287
|
throw new Error(`unknown method ${method}`);
|
|
182
288
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
289
|
+
for (const mixin of this.mixinTable) {
|
|
290
|
+
const { interfaces, proxy } = await mixin.entry;
|
|
186
291
|
// this could be null?
|
|
187
|
-
if (
|
|
188
|
-
|
|
292
|
+
if (interfaces.has(iface)) {
|
|
293
|
+
if (!proxy)
|
|
294
|
+
throw new Error(`device is unavailable ${this.id} (mixin ${mixin.mixinProviderId})`);
|
|
295
|
+
return proxy[method](...argArray);
|
|
296
|
+
}
|
|
189
297
|
}
|
|
190
298
|
|
|
191
299
|
throw new Error(`${method} not implemented`)
|
|
@@ -47,8 +47,9 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
|
|
|
47
47
|
const mixins: string[] = getState(device, ScryptedInterfaceProperty.mixins) || [];
|
|
48
48
|
if (!mixins.includes(mixinProvider._id))
|
|
49
49
|
throw new Error(`${mixinProvider._id} is not a mixin provider for ${id}`);
|
|
50
|
-
const tableEntry =
|
|
51
|
-
|
|
50
|
+
const tableEntry = this.scrypted.devices[device._id].handler.mixinTable.find(entry => entry.mixinProviderId === mixinProvider._id);
|
|
51
|
+
const { interfaces } = await tableEntry.entry;
|
|
52
|
+
if (!interfaces.has(eventInterface))
|
|
52
53
|
throw new Error(`${mixinProvider._id} does not mixin ${eventInterface} for ${id}`);
|
|
53
54
|
this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
|
|
54
55
|
}
|
|
@@ -117,7 +117,6 @@ export class PluginHost {
|
|
|
117
117
|
} = (socket.request as any).scrypted;
|
|
118
118
|
|
|
119
119
|
const handler = this.scrypted.getDevice<EngineIOHandler>(pluginDevice._id);
|
|
120
|
-
handler.onConnection(endpointRequest, `io://${socket.id}`);
|
|
121
120
|
|
|
122
121
|
socket.on('message', message => {
|
|
123
122
|
this.remote.ioEvent(socket.id, 'message', message)
|
|
@@ -125,6 +124,8 @@ export class PluginHost {
|
|
|
125
124
|
socket.on('close', reason => {
|
|
126
125
|
this.remote.ioEvent(socket.id, 'close');
|
|
127
126
|
});
|
|
127
|
+
|
|
128
|
+
await handler.onConnection(endpointRequest, `io://${socket.id}`);
|
|
128
129
|
}
|
|
129
130
|
catch (e) {
|
|
130
131
|
console.error('engine.io plugin error', e);
|
|
@@ -235,7 +236,7 @@ export class PluginHost {
|
|
|
235
236
|
path.join(__dirname, '../../python', 'plugin-remote.py'),
|
|
236
237
|
)
|
|
237
238
|
|
|
238
|
-
this.worker = child_process.spawn('python3', args, {
|
|
239
|
+
this.worker = child_process.spawn('python3.7', args, {
|
|
239
240
|
// stdin, stdout, stderr, peer in, peer out
|
|
240
241
|
stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
241
242
|
env: Object.assign({}, process.env, env),
|
|
@@ -297,6 +298,11 @@ export class PluginHost {
|
|
|
297
298
|
this.worker.stderr.on('data', data => console.error(data.toString()));
|
|
298
299
|
this.consoleServer = createConsoleServer(this.worker.stdout, this.worker.stderr);
|
|
299
300
|
|
|
301
|
+
this.consoleServer.then(cs => {
|
|
302
|
+
const { pluginConsole } = cs;
|
|
303
|
+
pluginConsole.log('starting plugin', this.pluginId, this.packageJson.version);
|
|
304
|
+
});
|
|
305
|
+
|
|
300
306
|
this.worker.on('disconnect', () => {
|
|
301
307
|
connected = false;
|
|
302
308
|
logger.log('e', `${this.pluginName} disconnected`);
|
|
@@ -384,6 +390,11 @@ export function startPluginRemote() {
|
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
|
393
|
+
// the the plugin console is simply the default console
|
|
394
|
+
// and gets read from stderr/stdout.
|
|
395
|
+
if (!nativeId)
|
|
396
|
+
return console;
|
|
397
|
+
|
|
387
398
|
return getConsole(async (stdout, stderr) => {
|
|
388
399
|
const plugins = await api.getComponent('plugins');
|
|
389
400
|
const connect = async () => {
|
|
@@ -459,9 +470,9 @@ export function startPluginRemote() {
|
|
|
459
470
|
|
|
460
471
|
let _pluginConsole: Console;
|
|
461
472
|
const getPluginConsole = () => {
|
|
462
|
-
if (_pluginConsole)
|
|
463
|
-
|
|
464
|
-
_pluginConsole
|
|
473
|
+
if (!_pluginConsole)
|
|
474
|
+
_pluginConsole = getDeviceConsole(undefined);
|
|
475
|
+
return _pluginConsole;
|
|
465
476
|
}
|
|
466
477
|
|
|
467
478
|
attachPluginRemote(peer, {
|
|
@@ -474,7 +485,7 @@ export function startPluginRemote() {
|
|
|
474
485
|
pluginId = _pluginId;
|
|
475
486
|
peer.selfName = pluginId;
|
|
476
487
|
},
|
|
477
|
-
onPluginReady: async(scrypted, params, plugin) => {
|
|
488
|
+
onPluginReady: async (scrypted, params, plugin) => {
|
|
478
489
|
replPort = createREPLServer(scrypted, params, plugin);
|
|
479
490
|
},
|
|
480
491
|
getPluginConsole,
|
|
@@ -4,7 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/sdk/types'
|
|
5
5
|
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
|
6
6
|
import { SystemManagerImpl } from './system';
|
|
7
|
-
import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION
|
|
7
|
+
import { RpcPeer, RPCResultError, PROPERTY_PROXY_ONEWAY_METHODS, PROPERTY_JSON_DISABLE_SERIALIZATION } from '../rpc';
|
|
8
8
|
import { BufferSerializer } from './buffer-serializer';
|
|
9
9
|
import { EventEmitter } from 'events';
|
|
10
10
|
import { createWebSocketClass } from './plugin-remote-websocket';
|
|
@@ -407,7 +407,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
|
407
407
|
|
|
408
408
|
async loadZip(packageJson: any, zipData: Buffer, zipOptions?: PluginRemoteLoadZipOptions) {
|
|
409
409
|
const pluginConsole = getPluginConsole?.();
|
|
410
|
-
pluginConsole?.log('starting plugin', pluginId, packageJson.version);
|
|
411
410
|
const zip = new AdmZip(zipData);
|
|
412
411
|
await options?.onLoadZip?.(zip, packageJson);
|
|
413
412
|
const main = zip.getEntry('main.nodejs.js');
|
|
@@ -41,7 +41,7 @@ export async function createREPLServer(scrypted: ScryptedStatic, params: any, pl
|
|
|
41
41
|
delete ctx.WebSocket;
|
|
42
42
|
delete ctx.pluginHostAPI;
|
|
43
43
|
|
|
44
|
-
const replFilter = new Set<string>(['require', 'localStorage'])
|
|
44
|
+
const replFilter = new Set<string>(['require', 'localStorage', 'exports', '__filename'])
|
|
45
45
|
const replVariables = Object.keys(ctx).filter(key => !replFilter.has(key));
|
|
46
46
|
|
|
47
47
|
const welcome = `JavaScript REPL variables:\n${replVariables.map(key => ' ' + key).join('\n')}\n\n`;
|
package/src/runtime.ts
CHANGED
|
@@ -317,7 +317,7 @@ export class ScryptedRuntime {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
if (!isEngineIOEndpoint && isUpgrade) {
|
|
320
|
-
this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, ws => {
|
|
320
|
+
this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => {
|
|
321
321
|
try {
|
|
322
322
|
const handler = this.getDevice<EngineIOHandler>(pluginDevice._id);
|
|
323
323
|
const id = 'ws-' + this.wsAtomic++;
|
|
@@ -328,8 +328,6 @@ export class ScryptedRuntime {
|
|
|
328
328
|
}
|
|
329
329
|
pluginHost.ws[id] = ws;
|
|
330
330
|
|
|
331
|
-
handler.onConnection(httpRequest, `ws://${id}`);
|
|
332
|
-
|
|
333
331
|
ws.on('message', async (message) => {
|
|
334
332
|
try {
|
|
335
333
|
pluginHost.remote.ioEvent(id, 'message', message)
|
|
@@ -346,6 +344,8 @@ export class ScryptedRuntime {
|
|
|
346
344
|
}
|
|
347
345
|
delete pluginHost.ws[id];
|
|
348
346
|
});
|
|
347
|
+
|
|
348
|
+
await handler.onConnection(httpRequest, `ws://${id}`);
|
|
349
349
|
}
|
|
350
350
|
catch (e) {
|
|
351
351
|
console.error('websocket plugin error', e);
|
|
@@ -424,6 +424,15 @@ export class ScryptedRuntime {
|
|
|
424
424
|
return proxyPair;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
// should this be async?
|
|
428
|
+
invalidatePluginDeviceMixins(id: string) {
|
|
429
|
+
const proxyPair = this.devices[id];
|
|
430
|
+
if (!proxyPair)
|
|
431
|
+
return;
|
|
432
|
+
proxyPair.handler.invalidateMixinTable();
|
|
433
|
+
return proxyPair;
|
|
434
|
+
}
|
|
435
|
+
|
|
427
436
|
async installNpm(pkg: string, version?: string): Promise<PluginHost> {
|
|
428
437
|
const registry = (await axios(`https://registry.npmjs.org/${pkg}`)).data;
|
|
429
438
|
if (!version) {
|
package/src/services/plugin.ts
CHANGED
|
@@ -29,7 +29,7 @@ export class PluginComponent {
|
|
|
29
29
|
const pluginDevice = this.scrypted.findPluginDeviceById(id);
|
|
30
30
|
this.scrypted.stateManager.setPluginDeviceState(pluginDevice, ScryptedInterfaceProperty.mixins, [...new Set(mixins)] || []);
|
|
31
31
|
await this.scrypted.datastore.upsert(pluginDevice);
|
|
32
|
-
const device = this.scrypted.
|
|
32
|
+
const device = this.scrypted.invalidatePluginDeviceMixins(id);
|
|
33
33
|
await device?.handler.ensureProxy();
|
|
34
34
|
}
|
|
35
35
|
async getMixins(id: string) {
|