@scrypted/server 0.0.88 → 0.0.92
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 +12 -4
- 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/plugin/plugin-volume.js +7 -2
- package/dist/plugin/plugin-volume.js.map +1 -1
- package/dist/runtime.js +13 -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 +7 -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 +16 -5
- package/src/plugin/plugin-remote.ts +1 -2
- package/src/plugin/plugin-repl.ts +1 -1
- package/src/plugin/plugin-volume.ts +6 -1
- package/src/runtime.ts +15 -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);
|
|
@@ -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`;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import mkdirp from 'mkdirp';
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export function getPluginVolume(pluginId: string) {
|
|
5
5
|
const volume = path.join(process.cwd(), 'volume');
|
|
6
6
|
const pluginVolume = path.join(volume, 'plugins', pluginId);
|
|
7
|
+
return pluginVolume;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ensurePluginVolume(pluginId: string) {
|
|
11
|
+
const pluginVolume = getPluginVolume(pluginId);
|
|
7
12
|
try {
|
|
8
13
|
mkdirp.sync(pluginVolume);
|
|
9
14
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -28,6 +28,8 @@ import { Alerts } from './services/alerts';
|
|
|
28
28
|
import { Info } from './services/info';
|
|
29
29
|
import io from 'engine.io';
|
|
30
30
|
import {spawn as ptySpawn} from 'node-pty';
|
|
31
|
+
import rimraf from 'rimraf';
|
|
32
|
+
import { getPluginVolume } from './plugin/plugin-volume';
|
|
31
33
|
|
|
32
34
|
interface DeviceProxyPair {
|
|
33
35
|
handler: PluginDeviceProxyHandler;
|
|
@@ -317,7 +319,7 @@ export class ScryptedRuntime {
|
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
if (!isEngineIOEndpoint && isUpgrade) {
|
|
320
|
-
this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, ws => {
|
|
322
|
+
this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => {
|
|
321
323
|
try {
|
|
322
324
|
const handler = this.getDevice<EngineIOHandler>(pluginDevice._id);
|
|
323
325
|
const id = 'ws-' + this.wsAtomic++;
|
|
@@ -328,8 +330,6 @@ export class ScryptedRuntime {
|
|
|
328
330
|
}
|
|
329
331
|
pluginHost.ws[id] = ws;
|
|
330
332
|
|
|
331
|
-
handler.onConnection(httpRequest, `ws://${id}`);
|
|
332
|
-
|
|
333
333
|
ws.on('message', async (message) => {
|
|
334
334
|
try {
|
|
335
335
|
pluginHost.remote.ioEvent(id, 'message', message)
|
|
@@ -346,6 +346,8 @@ export class ScryptedRuntime {
|
|
|
346
346
|
}
|
|
347
347
|
delete pluginHost.ws[id];
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
await handler.onConnection(httpRequest, `ws://${id}`);
|
|
349
351
|
}
|
|
350
352
|
catch (e) {
|
|
351
353
|
console.error('websocket plugin error', e);
|
|
@@ -424,6 +426,15 @@ export class ScryptedRuntime {
|
|
|
424
426
|
return proxyPair;
|
|
425
427
|
}
|
|
426
428
|
|
|
429
|
+
// should this be async?
|
|
430
|
+
invalidatePluginDeviceMixins(id: string) {
|
|
431
|
+
const proxyPair = this.devices[id];
|
|
432
|
+
if (!proxyPair)
|
|
433
|
+
return;
|
|
434
|
+
proxyPair.handler.invalidateMixinTable();
|
|
435
|
+
return proxyPair;
|
|
436
|
+
}
|
|
437
|
+
|
|
427
438
|
async installNpm(pkg: string, version?: string): Promise<PluginHost> {
|
|
428
439
|
const registry = (await axios(`https://registry.npmjs.org/${pkg}`)).data;
|
|
429
440
|
if (!version) {
|
|
@@ -573,6 +584,7 @@ export class ScryptedRuntime {
|
|
|
573
584
|
const plugin = await this.datastore.tryGet(Plugin, device.pluginId);
|
|
574
585
|
this.killPlugin(plugin);
|
|
575
586
|
await this.datastore.remove(plugin);
|
|
587
|
+
rimraf.sync(getPluginVolume(plugin._id));
|
|
576
588
|
}
|
|
577
589
|
this.stateManager.removeDevice(device._id);
|
|
578
590
|
|
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) {
|