mirror of
https://github.com/koush/scrypted.git
synced 2026-03-20 16:40:24 +00:00
add plugin restart
This commit is contained in:
16
common/package-lock.json
generated
16
common/package-lock.json
generated
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"@scrypted/sdk": "file:../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.7.1"
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
13
plugins/core/package-lock.json
generated
13
plugins/core/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"url-parse": "^1.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0",
|
||||
"@types/url-parse": "^1.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
})
|
||||
|
||||
ws.onclose = () => {
|
||||
api.kill();
|
||||
api.removeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
sdk/index.d.ts
vendored
6
sdk/index.d.ts
vendored
@@ -72,6 +72,7 @@ export class MixinDeviceBase<T> implements DeviceState {
|
||||
storage: Storage;
|
||||
id?: string;
|
||||
interfaces?: string[];
|
||||
mixins?: string[];
|
||||
metadata?: any;
|
||||
name?: string;
|
||||
providedInterfaces?: string[];
|
||||
@@ -120,11 +121,6 @@ export class MixinDeviceBase<T> implements DeviceState {
|
||||
ultraviolet?: number;
|
||||
luminance?: number;
|
||||
position?: Position;
|
||||
|
||||
/**
|
||||
* Called when the mixin has been removed or invalidated.
|
||||
*/
|
||||
release(): void;
|
||||
}
|
||||
|
||||
declare const Scrypted: ScryptedStatic;
|
||||
|
||||
18
sdk/types.d.ts
vendored
18
sdk/types.d.ts
vendored
@@ -727,6 +727,10 @@ export interface DeviceManager {
|
||||
*/
|
||||
onDevicesChanged(devices: DeviceManifest): Promise<void>;
|
||||
|
||||
/**
|
||||
* Restart the plugin. May not happen immediately.
|
||||
*/
|
||||
requestRestart(): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* 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<T>(id: string): ScryptedDevice & T;
|
||||
getDeviceById<T>(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<T>(name: string): ScryptedDevice & T;
|
||||
getDeviceByName<T>(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<any>;
|
||||
|
||||
/**
|
||||
* Release a mixin device that was previously returned from getMixin.
|
||||
*/
|
||||
releaseMixin(id: string, mixinDevice: any): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* The HttpRequestHandler allows handling of web requests under the endpoint path: /endpoint/npm-package-name/*.
|
||||
|
||||
@@ -268,6 +268,7 @@ module.exports.ScryptedInterfaceDescriptors = {
|
||||
],
|
||||
methods: [
|
||||
"getVideoStream",
|
||||
"getVideoStreamOptions",
|
||||
]
|
||||
},
|
||||
Lock: {
|
||||
@@ -511,6 +512,7 @@ module.exports.ScryptedInterfaceDescriptors = {
|
||||
methods: [
|
||||
"canMixin",
|
||||
"getMixin",
|
||||
"releaseMixin",
|
||||
]
|
||||
},
|
||||
HttpRequestHandler: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Array<string>>(resolve => {
|
||||
cp.stdout.on('data', data => {
|
||||
|
||||
@@ -30,9 +30,9 @@ export interface PluginAPI {
|
||||
|
||||
getComponent(id: string): Promise<any>;
|
||||
|
||||
getMediaManager(): Promise<MediaManager>
|
||||
getMediaManager(): Promise<MediaManager>;
|
||||
|
||||
kill(): Promise<void>;
|
||||
requestRestart(): Promise<void>;
|
||||
}
|
||||
|
||||
class EventListenerRegisterProxy implements EventListenerRegister {
|
||||
@@ -46,19 +46,30 @@ class EventListenerRegisterProxy implements EventListenerRegister {
|
||||
}
|
||||
}
|
||||
|
||||
export class PluginAPIProxy implements PluginAPI {
|
||||
export class PluginAPIManagedListeners {
|
||||
listeners = new Set<EventListenerRegister>();
|
||||
|
||||
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<void> {
|
||||
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<EventListenerRegister> {
|
||||
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<EventListenerRegister> {
|
||||
return this.logListener(await this.api.listenDevice(id, event, callback));
|
||||
return this.manageListener(await this.api.listenDevice(id, event, callback));
|
||||
}
|
||||
ioClose(id: string): Promise<void> {
|
||||
return this.api.ioClose(id);
|
||||
@@ -110,11 +121,9 @@ export class PluginAPIProxy implements PluginAPI {
|
||||
async getMediaManager(): Promise<MediaManager> {
|
||||
return this.mediaManager;
|
||||
}
|
||||
async kill(): Promise<void> {
|
||||
for (const l of [...this.listeners]) {
|
||||
l.removeListener();
|
||||
}
|
||||
this.listeners.clear();
|
||||
|
||||
async requestRestart() {
|
||||
return this.api.requestRestart();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { hasSameElements } from "../collection";
|
||||
import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from "./descriptor";
|
||||
|
||||
interface MixinTable {
|
||||
mixinProviderId: string;
|
||||
interfaces: string[];
|
||||
proxy: Promise<any>;
|
||||
}
|
||||
@@ -28,10 +29,10 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, 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<any>, 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<any>, 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,
|
||||
})
|
||||
|
||||
132
server/src/plugin/plugin-host-api.ts
Normal file
132
server/src/plugin/plugin-host-api.ts
Normal file
@@ -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<MediaManager> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async deliverPush(endpoint: string, httpRequest: HttpRequest) {
|
||||
return this.scrypted.deliverPush(endpoint, httpRequest);
|
||||
}
|
||||
|
||||
async getLogger(nativeId: string): Promise<Logger> {
|
||||
const device = this.scrypted.findPluginDevice(this.pluginId, nativeId);
|
||||
return this.scrypted.getDeviceLogger(device);
|
||||
}
|
||||
|
||||
getComponent(id: string): Promise<any> {
|
||||
return this.scrypted.getComponent(id);
|
||||
}
|
||||
|
||||
setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise<void> {
|
||||
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<T>(id: string): Promise<T & ScryptedDevice> {
|
||||
return this.scrypted.getDevice(id);
|
||||
}
|
||||
async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
|
||||
return this.manageListener(this.scrypted.stateManager.listen(EventListener));
|
||||
}
|
||||
async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<void>) {
|
||||
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<MediaManager> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async deliverPush(endpoint: string, httpRequest: HttpRequest) {
|
||||
return scrypted.deliverPush(endpoint, httpRequest);
|
||||
}
|
||||
|
||||
async getLogger(nativeId: string): Promise<Logger> {
|
||||
const device = scrypted.findPluginDevice(plugin._id, nativeId);
|
||||
return self.scrypted.getDeviceLogger(device);
|
||||
}
|
||||
|
||||
getComponent(id: string): Promise<any> {
|
||||
return self.scrypted.getComponent(id);
|
||||
}
|
||||
|
||||
setDeviceProperty(id: string, property: ScryptedInterfaceProperty, value: any): Promise<void> {
|
||||
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<T>(id: string): Promise<T & ScryptedDevice> {
|
||||
return scrypted.getDevice(id);
|
||||
}
|
||||
async listen(EventListener: (id: string, eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
|
||||
return scrypted.stateManager.listen(EventListener);
|
||||
}
|
||||
async listenDevice(id: string, event: string | EventListenerOptions, callback: (eventDetails: EventDetails, eventData: object) => void): Promise<EventListenerRegister> {
|
||||
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<Console>;
|
||||
const zipBuffer = Buffer.from(plugin.zip, 'base64');
|
||||
|
||||
@@ -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<PluginRemote> {
|
||||
peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
|
||||
|
||||
const listen = api.listen.bind(api);
|
||||
|
||||
const registers = new Set<EventListenerRegister>();
|
||||
|
||||
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<EventListenerRegister> => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user