From 396068dfc3fbe160ee480302771d9cd56ecba779 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 8 Sep 2021 21:47:39 -0700 Subject: [PATCH] add plugin restart --- common/package-lock.json | 16 ++-- common/package.json | 2 +- plugins/core/package-lock.json | 13 +++ plugins/core/package.json | 1 + plugins/core/src/main.ts | 2 +- sdk/index.d.ts | 6 +- sdk/types.d.ts | 18 +++- sdk/types.generated.js | 2 + server/src/component/plugin.ts | 6 +- server/src/plugin/media.ts | 12 +-- server/src/plugin/plugin-api.ts | 37 +++++--- server/src/plugin/plugin-device.ts | 15 +-- server/src/plugin/plugin-host-api.ts | 132 +++++++++++++++++++++++++++ server/src/plugin/plugin-host.ts | 120 +++--------------------- server/src/plugin/plugin-remote.ts | 41 +-------- 15 files changed, 227 insertions(+), 196 deletions(-) create mode 100644 server/src/plugin/plugin-host-api.ts diff --git a/common/package-lock.json b/common/package-lock.json index 96fe0aa6a..c670d40fa 100644 --- a/common/package-lock.json +++ b/common/package-lock.json @@ -12,12 +12,12 @@ "@scrypted/sdk": "file:../sdk" }, "devDependencies": { - "@types/node": "^16.7.1" + "@types/node": "^16.9.0" } }, "../sdk": { "name": "@scrypted/sdk", - "version": "0.0.65", + "version": "0.0.69", "license": "ISC", "dependencies": { "@babel/core": "^7.2.2", @@ -71,9 +71,9 @@ "link": true }, "node_modules/@types/node": { - "version": "16.7.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", - "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", + "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==", "dev": true } }, @@ -118,9 +118,9 @@ } }, "@types/node": { - "version": "16.7.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", - "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", + "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==", "dev": true } } diff --git a/common/package.json b/common/package.json index b98630773..de566892e 100644 --- a/common/package.json +++ b/common/package.json @@ -12,6 +12,6 @@ "@scrypted/sdk": "file:../sdk" }, "devDependencies": { - "@types/node": "^16.7.1" + "@types/node": "^16.9.0" } } diff --git a/plugins/core/package-lock.json b/plugins/core/package-lock.json index 78a77c40f..217140cd7 100644 --- a/plugins/core/package-lock.json +++ b/plugins/core/package-lock.json @@ -17,6 +17,7 @@ "url-parse": "^1.4.7" }, "devDependencies": { + "@types/node": "^16.9.0", "@types/url-parse": "^1.4.4" } }, @@ -125,6 +126,12 @@ "resolved": "../../sdk", "link": true }, + "node_modules/@types/node": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", + "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==", + "dev": true + }, "node_modules/@types/url-parse": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.4.tgz", @@ -323,6 +330,12 @@ "webpack-inject-plugin": "^1.0.2" } }, + "@types/node": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", + "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==", + "dev": true + }, "@types/url-parse": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.4.tgz", diff --git a/plugins/core/package.json b/plugins/core/package.json index 693dfe993..86c37f9a7 100644 --- a/plugins/core/package.json +++ b/plugins/core/package.json @@ -33,6 +33,7 @@ "url-parse": "^1.4.7" }, "devDependencies": { + "@types/node": "^16.9.0", "@types/url-parse": "^1.4.4" } } diff --git a/plugins/core/src/main.ts b/plugins/core/src/main.ts index 13dcb5ac7..64ab7e76f 100644 --- a/plugins/core/src/main.ts +++ b/plugins/core/src/main.ts @@ -238,7 +238,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng }) ws.onclose = () => { - api.kill(); + api.removeListeners(); } } diff --git a/sdk/index.d.ts b/sdk/index.d.ts index faf39be03..1c24fb235 100644 --- a/sdk/index.d.ts +++ b/sdk/index.d.ts @@ -72,6 +72,7 @@ export class MixinDeviceBase implements DeviceState { storage: Storage; id?: string; interfaces?: string[]; + mixins?: string[]; metadata?: any; name?: string; providedInterfaces?: string[]; @@ -120,11 +121,6 @@ export class MixinDeviceBase implements DeviceState { ultraviolet?: number; luminance?: number; position?: Position; - - /** - * Called when the mixin has been removed or invalidated. - */ - release(): void; } declare const Scrypted: ScryptedStatic; diff --git a/sdk/types.d.ts b/sdk/types.d.ts index 4df9678b1..8f929d7cc 100644 --- a/sdk/types.d.ts +++ b/sdk/types.d.ts @@ -727,6 +727,10 @@ export interface DeviceManager { */ onDevicesChanged(devices: DeviceManifest): Promise; + /** + * Restart the plugin. May not happen immediately. + */ + requestRestart(): Promise; } /** * Device objects are created by DeviceProviders when new devices are discover and synced to Scrypted via the DeviceManager. @@ -825,22 +829,22 @@ export interface SystemManager { /** * Find a Scrypted device by id. */ - getDeviceById(id: string): ScryptedDevice; + getDeviceById(id: string): ScryptedDevice; /** * Find a Scrypted device by id. */ - getDeviceById(id: string): ScryptedDevice & T; + getDeviceById(id: string): ScryptedDevice & T; /** * Find a Scrypted device by name. */ - getDeviceByName(name: string): ScryptedDevice; + getDeviceByName(name: string): ScryptedDevice; /** * Find a Scrypted device by name. */ - getDeviceByName(name: string): ScryptedDevice & T; + getDeviceByName(name: string): ScryptedDevice & T; /** * Get the current state of a device. @@ -880,8 +884,12 @@ export interface MixinProvider { /** * Create a mixin that can be applied to the supplied device. */ - getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): any; + getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): Promise; + /** + * Release a mixin device that was previously returned from getMixin. + */ + releaseMixin(id: string, mixinDevice: any): Promise; } /** * The HttpRequestHandler allows handling of web requests under the endpoint path: /endpoint/npm-package-name/*. diff --git a/sdk/types.generated.js b/sdk/types.generated.js index 5d18307c3..04b650968 100644 --- a/sdk/types.generated.js +++ b/sdk/types.generated.js @@ -268,6 +268,7 @@ module.exports.ScryptedInterfaceDescriptors = { ], methods: [ "getVideoStream", + "getVideoStreamOptions", ] }, Lock: { @@ -511,6 +512,7 @@ module.exports.ScryptedInterfaceDescriptors = { methods: [ "canMixin", "getMixin", + "releaseMixin", ] }, HttpRequestHandler: { diff --git a/server/src/component/plugin.ts b/server/src/component/plugin.ts index 2eecbfc87..35ee8ba1f 100644 --- a/server/src/component/plugin.ts +++ b/server/src/component/plugin.ts @@ -31,9 +31,7 @@ export class PluginComponent { await host?.remote?.setNativeId?.(pluginDevice.nativeId, pluginDevice._id, storage); } async setMixins(id: string, mixins: string[]) { - const pluginDevice = this.scrypted.findPluginDeviceById(id); - setState(pluginDevice, ScryptedInterfaceProperty.mixins, [...new Set(mixins)] || []); - await this.scrypted.datastore.upsert(pluginDevice); + this.scrypted.stateManager.setState(id, ScryptedInterfaceProperty.mixins, [...new Set(mixins)] || []); const device = this.scrypted.invalidatePluginDevice(id); await device.handler.ensureProxy(); } @@ -54,7 +52,7 @@ export class PluginComponent { } async reload(pluginId: string) { const plugin = await this.scrypted.datastore.tryGet(Plugin, pluginId); - await this.scrypted.installPlugin(plugin); + await this.scrypted.runPlugin(plugin); } async getPackageJson(pluginId: string) { const plugin = await this.scrypted.datastore.tryGet(Plugin, pluginId); diff --git a/server/src/plugin/media.ts b/server/src/plugin/media.ts index df408e158..92ce82ec3 100644 --- a/server/src/plugin/media.ts +++ b/server/src/plugin/media.ts @@ -50,10 +50,10 @@ function addBuiltins(mediaManager: MediaManager, converters: BufferConverter[]) args.push('-y', "-vf", "select=eq(n\\,1)", "-vframes", "1", '-f', 'singlejpeg', tmpfile.name); const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args, { - // stdio: 'ignore', + stdio: 'ignore', }); - cp.stdout.on('data', data => console.log(data.toString())); - cp.stderr.on('data', data => console.error(data.toString())); + // cp.stdout.on('data', data => console.log(data.toString())); + // cp.stderr.on('data', data => console.error(data.toString())); cp.on('error', (code) => { console.error('ffmpeg error code', code); }) @@ -205,11 +205,11 @@ function addBuiltins(mediaManager: MediaManager, converters: BufferConverter[]) args.push(`tcp://127.0.0.1:${videoPort}`); const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args, { - // stdio: 'ignore', + stdio: 'ignore', }); cp.on('error', e => console.error('ffmpeg error', e)); - cp.stdout.on('data', data => console.log(data.toString())); - cp.stderr.on('data', data => console.error(data.toString())); + // cp.stdout.on('data', data => console.log(data.toString())); + // cp.stderr.on('data', data => console.error(data.toString())); const resolution = new Promise>(resolve => { cp.stdout.on('data', data => { diff --git a/server/src/plugin/plugin-api.ts b/server/src/plugin/plugin-api.ts index e03524478..02a4c3c84 100644 --- a/server/src/plugin/plugin-api.ts +++ b/server/src/plugin/plugin-api.ts @@ -30,9 +30,9 @@ export interface PluginAPI { getComponent(id: string): Promise; - getMediaManager(): Promise + getMediaManager(): Promise; - kill(): Promise; + requestRestart(): Promise; } class EventListenerRegisterProxy implements EventListenerRegister { @@ -46,19 +46,30 @@ class EventListenerRegisterProxy implements EventListenerRegister { } } -export class PluginAPIProxy implements PluginAPI { +export class PluginAPIManagedListeners { listeners = new Set(); - constructor(public api: PluginAPI, public mediaManager?: MediaManager) { - } - - logListener(listener: EventListenerRegister): EventListenerRegister { + manageListener(listener: EventListenerRegister): EventListenerRegister { + this.listeners.add(listener); return new EventListenerRegisterProxy(this.listeners, () => { this.listeners.delete(listener); listener.removeListener(); }); } + removeListeners() { + for (const l of [...this.listeners]) { + l.removeListener(); + } + this.listeners.clear(); + } +} + +export class PluginAPIProxy extends PluginAPIManagedListeners implements PluginAPI { + constructor(public api: PluginAPI, public mediaManager?: MediaManager) { + super(); + } + setState(nativeId: string, key: string, value: any): Promise { return this.api.setState(nativeId, key, value); } @@ -87,10 +98,10 @@ export class PluginAPIProxy implements PluginAPI { return this.api.removeDevice(id); } async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: any) => void): Promise { - return this.logListener(await this.api.listen(EventListener)); + return this.manageListener(await this.api.listen(EventListener)); } async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise { - return this.logListener(await this.api.listenDevice(id, event, callback)); + return this.manageListener(await this.api.listenDevice(id, event, callback)); } ioClose(id: string): Promise { return this.api.ioClose(id); @@ -110,11 +121,9 @@ export class PluginAPIProxy implements PluginAPI { async getMediaManager(): Promise { return this.mediaManager; } - async kill(): Promise { - for (const l of [...this.listeners]) { - l.removeListener(); - } - this.listeners.clear(); + + async requestRestart() { + return this.api.requestRestart(); } } diff --git a/server/src/plugin/plugin-device.ts b/server/src/plugin/plugin-device.ts index 1b486ec22..2dcaa3ae3 100644 --- a/server/src/plugin/plugin-device.ts +++ b/server/src/plugin/plugin-device.ts @@ -9,6 +9,7 @@ import { hasSameElements } from "../collection"; import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from "./descriptor"; interface MixinTable { + mixinProviderId: string; interfaces: string[]; proxy: Promise; } @@ -28,10 +29,10 @@ export class PluginDeviceProxyHandler implements ProxyHandler, ScryptedDevi const mixinTable = this.mixinTable; this.mixinTable = undefined; (async() => { - for (const mixin of await mixinTable) { + for (const mixinEntry of await mixinTable) { (async() => { - const proxy = await mixin.proxy; - proxy.release(); + const mixinProvider = this.scrypted.getDevice(mixinEntry.mixinProviderId) as ScryptedDevice & MixinProvider; + mixinProvider.releaseMixin(this.id, await mixinEntry.proxy); })().catch(() => {}); } })().catch(() => {});; @@ -65,19 +66,20 @@ export class PluginDeviceProxyHandler implements ProxyHandler, ScryptedDevi const mixinTable: MixinTable[] = []; mixinTable.unshift({ + mixinProviderId: undefined, interfaces: allInterfaces.slice(), proxy, }) for (const mixinId of getState(pluginDevice, ScryptedInterfaceProperty.mixins) || []) { - const mixin = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider; + const mixinProvider = this.scrypted.getDevice(mixinId) as ScryptedDevice & MixinProvider; const wrappedHandler = new PluginDeviceProxyHandler(this.scrypted, this.id); wrappedHandler.mixinTable = Promise.resolve(mixinTable.slice()); const wrappedProxy = new Proxy(wrappedHandler, wrappedHandler); try { - const interfaces = await (mixin.canMixin(type, allInterfaces) as any) as ScryptedInterface[]; + const interfaces = await (mixinProvider.canMixin(type, allInterfaces) as any) as ScryptedInterface[]; if (!interfaces) { console.warn(`mixin provider ${mixinId} can no longer mixin ${this.id}`); const mixins: string[] = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || []; @@ -89,13 +91,14 @@ export class PluginDeviceProxyHandler implements ProxyHandler, ScryptedDevi const host = this.scrypted.getPluginHostForDeviceId(mixinId); const deviceState = await host.remote.createDeviceState(this.id, async (property, value) => this.scrypted.stateManager.setPluginDeviceState(pluginDevice, property, value)); - const mixinProxy = await mixin.getMixin(wrappedProxy, allInterfaces, deviceState); + const mixinProxy = await mixinProvider.getMixin(wrappedProxy, allInterfaces, deviceState); if (!mixinProxy) throw new Error(`mixin provider ${mixinId} did not return mixin for ${this.id}`); allInterfaces.push(...interfaces); proxy = mixinProxy; mixinTable.unshift({ + mixinProviderId: mixinId, interfaces, proxy, }) diff --git a/server/src/plugin/plugin-host-api.ts b/server/src/plugin/plugin-host-api.ts new file mode 100644 index 000000000..bddd96731 --- /dev/null +++ b/server/src/plugin/plugin-host-api.ts @@ -0,0 +1,132 @@ +import { ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, ScryptedInterfaceProperty, MediaManager, HttpRequest } from '@scrypted/sdk/types' +import { ScryptedRuntime } from '../runtime'; +import { Plugin } from '../db-types'; +import { PluginAPI, PluginAPIManagedListeners } from './plugin-api'; +import { Logger } from '../logger'; +import { getState } from '../state'; +import { PluginHost } from './plugin-host'; +import debounce from 'lodash/debounce'; + + +export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAPI { + pluginId: string; + + restartDebounced = debounce(async () => { + const host = this.scrypted.plugins[this.pluginId]; + const logger = await this.getLogger(undefined); + if (host.api !== this) { + logger.log('w', 'plugin restart was requested, but a different instance was found. restart cancelled.'); + return; + } + + const plugin = await this.scrypted.datastore.tryGet(Plugin, this.pluginId); + this.scrypted.runPlugin(plugin); + }, 15000); + + constructor(public scrypted: ScryptedRuntime, plugin: Plugin, public pluginHost: PluginHost) { + super(); + this.pluginId = plugin._id; + } + + getMediaManager(): Promise { + return null; + } + + async deliverPush(endpoint: string, httpRequest: HttpRequest) { + return this.scrypted.deliverPush(endpoint, httpRequest); + } + + async getLogger(nativeId: string): Promise { + const device = this.scrypted.findPluginDevice(this.pluginId, nativeId); + return this.scrypted.getDeviceLogger(device); + } + + getComponent(id: string): Promise { + return this.scrypted.getComponent(id); + } + + setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise { + switch (property) { + case ScryptedInterfaceProperty.room: + case ScryptedInterfaceProperty.type: + case ScryptedInterfaceProperty.name: + const device = this.scrypted.findPluginDeviceById(id); + this.scrypted.stateManager.setPluginDeviceState(device, property, value); + return; + default: + throw new Error(`Not allowed to set property ${property}`); + } + } + + async ioClose(id: string) { + this.pluginHost.io.clients[id]?.close(); + this.pluginHost.ws[id]?.close(); + } + + async ioSend(id: string, message: string) { + this.pluginHost.io.clients[id]?.send(message); + this.pluginHost.ws[id]?.send(message); + } + + async setState(nativeId: string, key: string, value: any) { + this.scrypted.stateManager.setPluginState(this.pluginId, nativeId, key, value); + } + + async setStorage(nativeId: string, storage: { [key: string]: string }) { + const device = this.scrypted.findPluginDevice(this.pluginId, nativeId) + device.storage = storage; + this.scrypted.datastore.upsert(device); + } + + async onDevicesChanged(deviceManifest: DeviceManifest) { + const existing = this.scrypted.findPluginDevices(this.pluginId); + const newIds = deviceManifest.devices.map(device => device.nativeId); + const toRemove = existing.filter(e => e.nativeId && !newIds.includes(e.nativeId)); + + for (const remove of toRemove) { + await this.scrypted.removeDevice(remove); + } + + for (const upsert of deviceManifest.devices) { + await this.pluginHost.upsertDevice(upsert); + } + } + + async onDeviceDiscovered(device: Device) { + await this.pluginHost.upsertDevice(device); + } + + async onDeviceRemoved(nativeId: string) { + await this.scrypted.removeDevice(this.scrypted.findPluginDevice(this.pluginId, nativeId)) + } + + async onDeviceEvent(nativeId: any, eventInterface: any, eventData?: any) { + const plugin = this.scrypted.findPluginDevice(this.pluginId, nativeId); + this.scrypted.stateManager.notifyInterfaceEvent(plugin, eventInterface, eventData); + } + + async getDeviceById(id: string): Promise { + return this.scrypted.getDevice(id); + } + async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): Promise { + return this.manageListener(this.scrypted.stateManager.listen(EventListener)); + } + async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise { + const device = this.scrypted.findPluginDeviceById(id); + if (device) { + const self = this.scrypted.findPluginDevice(this.pluginId); + this.scrypted.getDeviceLogger(self).log('i', `requested listen ${getState(device, ScryptedInterfaceProperty.name)} ${JSON.stringify(event)}`); + } + return this.manageListener(this.scrypted.stateManager.listenDevice(id, event, callback)); + } + + async removeDevice(id: string) { + return this.scrypted.removeDevice(this.scrypted.findPluginDeviceById(id)); + } + + async requestRestart() { + const logger = await this.getLogger(undefined); + logger.log('i', 'plugin restart was requested'); + return this.restartDebounced(); + } +} \ No newline at end of file diff --git a/server/src/plugin/plugin-host.ts b/server/src/plugin/plugin-host.ts index 8bc28fdca..1e31839d4 100644 --- a/server/src/plugin/plugin-host.ts +++ b/server/src/plugin/plugin-host.ts @@ -18,6 +18,7 @@ import { once } from 'events'; import { PassThrough } from 'stream'; import { Console } from 'console' import { sleep } from '../sleep'; +import { PluginHostAPI } from './plugin-host-api'; export class PluginHost { worker: cluster.Worker; @@ -32,13 +33,13 @@ export class PluginHost { pingTimeout: 120000, }); ws: { [id: string]: WebSocket } = {}; - api: PluginAPI; + api: PluginHostAPI; pluginName: string; listener: EventListenerRegister; kill() { this.listener.removeListener(); - this.api.kill(); + this.api.removeListeners(); this.worker.process.kill(); this.io.close(); for (const s of Object.values(this.ws)) { @@ -61,6 +62,13 @@ export class PluginHost { return this.pluginName || 'no plugin name'; } + + async upsertDevice(upsert: Device) { + const pi = await this.scrypted.upsertDevice(this.pluginId, upsert); + await this.remote.setNativeId(pi.nativeId, pi._id, pi.storage || {}); + this.scrypted.invalidatePluginDevice(pi._id); + } + constructor(scrypted: ScryptedRuntime, plugin: Plugin, waitDebug?: Promise) { this.scrypted = scrypted; this.pluginId = plugin._id; @@ -122,113 +130,7 @@ export class PluginHost { const self = this; - async function upsertDevice(upsert: Device) { - const pi = await scrypted.upsertDevice(self.pluginId, upsert); - await self.remote.setNativeId(pi.nativeId, pi._id, pi.storage || {}); - scrypted.invalidatePluginDevice(pi._id); - } - - class PluginAPIImpl implements PluginAPI { - getMediaManager(): Promise { - return null; - } - - async deliverPush(endpoint: string, httpRequest: HttpRequest) { - return scrypted.deliverPush(endpoint, httpRequest); - } - - async getLogger(nativeId: string): Promise { - const device = scrypted.findPluginDevice(plugin._id, nativeId); - return self.scrypted.getDeviceLogger(device); - } - - getComponent(id: string): Promise { - return self.scrypted.getComponent(id); - } - - setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise { - switch (property) { - case ScryptedInterfaceProperty.room: - case ScryptedInterfaceProperty.type: - case ScryptedInterfaceProperty.name: - const device = scrypted.findPluginDeviceById(id); - scrypted.stateManager.setPluginDeviceState(device, property, value); - return; - default: - throw new Error(`Not allowed to set property ${property}`); - } - } - - async ioClose(id: string) { - self.io.clients[id]?.close(); - self.ws[id]?.close(); - } - - async ioSend(id: string, message: string) { - self.io.clients[id]?.send(message); - self.ws[id]?.send(message); - } - - async setState(nativeId: string, key: string, value: any) { - scrypted.stateManager.setPluginState(self.pluginId, nativeId, key, value); - } - - async setStorage(nativeId: string, storage: { [key: string]: string }) { - const device = scrypted.findPluginDevice(plugin._id, nativeId) - device.storage = storage; - scrypted.datastore.upsert(device); - } - - async onDevicesChanged(deviceManifest: DeviceManifest) { - const existing = scrypted.findPluginDevices(self.pluginId); - const newIds = deviceManifest.devices.map(device => device.nativeId); - const toRemove = existing.filter(e => e.nativeId && !newIds.includes(e.nativeId)); - - for (const remove of toRemove) { - await scrypted.removeDevice(remove); - } - - for (const upsert of deviceManifest.devices) { - await upsertDevice(upsert); - } - } - - async onDeviceDiscovered(device: Device) { - await upsertDevice(device); - } - - async onDeviceRemoved(nativeId: string) { - await scrypted.removeDevice(scrypted.findPluginDevice(plugin._id, nativeId)) - } - - async onDeviceEvent(nativeId: any, eventInterface: any, eventData?: any) { - const plugin = scrypted.findPluginDevice(self.pluginId, nativeId); - scrypted.stateManager.notifyInterfaceEvent(plugin, eventInterface, eventData); - } - - async getDeviceById(id: string): Promise { - return scrypted.getDevice(id); - } - async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): Promise { - return scrypted.stateManager.listen(EventListener); - } - async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise { - const device = scrypted.findPluginDeviceById(id); - if (device) { - const self = scrypted.findPluginDevice(plugin._id); - scrypted.getDeviceLogger(self).log('i', `requested listen ${getState(device, ScryptedInterfaceProperty.name)} ${JSON.stringify(event)}`); - } - return scrypted.stateManager.listenDevice(id, event, callback); - } - - async removeDevice(id: string) { - return scrypted.removeDevice(scrypted.findPluginDeviceById(id)); - } - - async kill() { - } - } - this.api = new PluginAPIImpl(); + this.api = new PluginHostAPI(scrypted, plugin, this); this.console = this.peer.eval('return console', undefined, undefined, true) as Promise; const zipBuffer = Buffer.from(plugin.zip, 'base64'); diff --git a/server/src/plugin/plugin-remote.ts b/server/src/plugin/plugin-remote.ts index bacab56b0..72290731b 100644 --- a/server/src/plugin/plugin-remote.ts +++ b/server/src/plugin/plugin-remote.ts @@ -155,6 +155,10 @@ class DeviceManagerImpl implements DeviceManager { this.systemManager = systemManager; } + async requestRestart() { + return this.api.requestRestart(); + } + getDeviceLogger(nativeId?: string): Logger { return new DeviceLogger(this.api, nativeId, this.getDeviceConsole?.(nativeId) || console); } @@ -247,43 +251,6 @@ interface WebSocketCallbacks { export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId: string): Promise { peer.addSerializer(Buffer, 'Buffer', new BufferSerializer()); - const listen = api.listen.bind(api); - - const registers = new Set(); - - class EventListenerRegisterObserver implements EventListenerRegister { - register: EventListenerRegister; - - constructor(register: EventListenerRegister) { - this.register = register; - } - - removeListener() { - registers.delete(this.register); - this.register.removeListener(); - } - } - - function manage(register: EventListenerRegister): EventListenerRegister { - registers.add(register); - return new EventListenerRegisterObserver(register); - } - - api.listen = async (EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void) => { - return manage(await listen(EventListener)); - } - - const listenDevice = api.listenDevice.bind(api); - api.listenDevice = async (id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise => { - return manage(await listenDevice(id, event, callback)); - } - - api.kill = async () => { - for (const register of registers) { - register.removeListener(); - } - } - const ret = await peer.eval('return getRemote(api, pluginId)', undefined, { api, pluginId,