From 74bfd5808e05b1424f22169d359598366936d6ad Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 30 Aug 2021 13:36:34 -0700 Subject: [PATCH] console and repl --- plugins/core/package-lock.json | 4 +- plugins/core/package.json | 2 +- plugins/core/src/main.ts | 22 + plugins/core/ui/package-lock.json | 55 +- plugins/core/ui/package.json | 5 +- .../core/ui/src/components/ConsoleCard.vue | 39 ++ plugins/core/ui/src/components/Device.vue | 604 +++++++++--------- plugins/core/ui/src/components/REPLCard.vue | 48 ++ plugins/core/ui/src/plugins/vuetify.ts | 1 + plugins/google-home/package-lock.json | 4 +- plugins/google-home/package.json | 2 +- plugins/google-home/src/main.ts | 37 +- sdk/index.generated.js | 61 +- sdk/index.js | 39 +- sdk/package-lock.json | 4 +- sdk/package.json | 2 +- sdk/types.d.ts | 39 +- sdk/types.generated.js | 12 - server/src/component/plugin.ts | 4 + server/src/plugin/cluster-helper.ts | 15 + server/src/plugin/media.ts | 15 +- server/src/plugin/plugin-api.ts | 2 + server/src/plugin/plugin-host.ts | 207 +++++- server/src/plugin/plugin-remote.ts | 71 +- server/src/runtime.ts | 38 +- server/src/scrypted-main.ts | 4 +- 26 files changed, 824 insertions(+), 512 deletions(-) create mode 100644 plugins/core/ui/src/components/ConsoleCard.vue create mode 100644 plugins/core/ui/src/components/REPLCard.vue create mode 100644 server/src/plugin/cluster-helper.ts diff --git a/plugins/core/package-lock.json b/plugins/core/package-lock.json index 78e2bbbfc..b40032dad 100644 --- a/plugins/core/package-lock.json +++ b/plugins/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/core", - "version": "0.0.50", + "version": "0.0.51", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/core", - "version": "0.0.50", + "version": "0.0.51", "license": "Apache-2.0", "dependencies": { "@scrypted/sdk": "file:../../sdk", diff --git a/plugins/core/package.json b/plugins/core/package.json index 0fe80ddbd..9e4084063 100644 --- a/plugins/core/package.json +++ b/plugins/core/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/core", - "version": "0.0.50", + "version": "0.0.51", "description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.", "author": "Scrypted", "license": "Apache-2.0", diff --git a/plugins/core/src/main.ts b/plugins/core/src/main.ts index 209e845c0..92dc094a0 100644 --- a/plugins/core/src/main.ts +++ b/plugins/core/src/main.ts @@ -16,6 +16,7 @@ import fs from 'fs'; import { sendJSON } from './http-helpers'; import { Automation } from './automation'; import { AggregateDevice, createAggregateDevice } from './aggregate'; +import net from 'net'; const indexHtml = fs.readFileSync('dist/index.html').toString(); @@ -132,9 +133,30 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng async discoverDevices(duration: number) { } + async checkService(request: HttpRequest, ws: WebSocket, name: string): Promise { + const check = `/endpoint/@scrypted/core/engine.io/${name}/`; + if (!request.url.startsWith(check)) + return false; + const deviceId = request.url.substr(check.length).split('/')[0]; + const plugins = await systemManager.getComponent('plugins'); + const { nativeId, pluginId } = await plugins.getDeviceInfo(deviceId); + const port = await plugins.getRemoteServicePort(pluginId, name); + const socket = net.connect(port); + socket.on('data', data => ws.send(data)); + socket.resume(); + socket.write(nativeId?.toString() || 'undefined'); + ws.onclose = () => socket.destroy(); + ws.onmessage = message => socket.write(message.data); + return true; + } + async onConnection(request: HttpRequest, webSocketUrl: string): Promise { const ws = new WebSocket(webSocketUrl); + if (await this.checkService(request, ws, 'console') || await this.checkService(request, ws, 'repl')) { + return; + } + if (request.isPublicEndpoint) { ws.close(); return; diff --git a/plugins/core/ui/package-lock.json b/plugins/core/ui/package-lock.json index 7b70e5b15..b41b24133 100644 --- a/plugins/core/ui/package-lock.json +++ b/plugins/core/ui/package-lock.json @@ -38,7 +38,8 @@ "vue2-google-maps": "^0.10.7", "vuetify": "^2.5.8", "vuex": "^3.6.2", - "xterm": "^4.13.0" + "xterm": "^4.13.0", + "xterm-addon-fit": "^0.5.0" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.13.0", @@ -49,6 +50,8 @@ "@babel/preset-typescript": "^7.13.0", "@types/blob-stream": "^0.1.30", "@types/dom-webcodecs": "^0.1.0", + "@types/engine.io": "^3.1.7", + "@types/engine.io-client": "^3.1.5", "@types/lodash": "^4.14.172", "@types/mkdirp": "^1.0.1", "@types/node": "^16.0.0", @@ -2155,6 +2158,24 @@ "integrity": "sha512-e5DuY7mZedrV8rrgEnm1gPmy4h2x11ryUTnkjmbdR6SWGCS3Qvz1ZJudtBIrbPNEhnpGM634SL7ZHQDz7dsBCg==", "dev": true }, + "node_modules/@types/engine.io": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz", + "integrity": "sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/engine.io-client": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/engine.io-client/-/engine.io-client-3.1.5.tgz", + "integrity": "sha512-h2BKMLNVMQGQ0Q7WaQCJ/63LooRQhItnoFNXhbNVnjhNheWEO+sdpTFjZpzWP7TDl4M5lI3fsEAk3PFZfV67Yg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -18742,6 +18763,14 @@ "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.13.0.tgz", "integrity": "sha512-HVW1gdoLOTnkMaqQCr2r3mQy4fX9iSa5gWxKZ2UTYdLa4iqavv7QxJ8n1Ypse32shPVkhTYPLS6vHEFZp5ghzw==" }, + "node_modules/xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "peerDependencies": { + "xterm": "^4.0.0" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -20492,6 +20521,24 @@ "integrity": "sha512-e5DuY7mZedrV8rrgEnm1gPmy4h2x11ryUTnkjmbdR6SWGCS3Qvz1ZJudtBIrbPNEhnpGM634SL7ZHQDz7dsBCg==", "dev": true }, + "@types/engine.io": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz", + "integrity": "sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/engine.io-client": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/engine.io-client/-/engine.io-client-3.1.5.tgz", + "integrity": "sha512-h2BKMLNVMQGQ0Q7WaQCJ/63LooRQhItnoFNXhbNVnjhNheWEO+sdpTFjZpzWP7TDl4M5lI3fsEAk3PFZfV67Yg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -33900,6 +33947,12 @@ "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.13.0.tgz", "integrity": "sha512-HVW1gdoLOTnkMaqQCr2r3mQy4fX9iSa5gWxKZ2UTYdLa4iqavv7QxJ8n1Ypse32shPVkhTYPLS6vHEFZp5ghzw==" }, + "xterm-addon-fit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz", + "integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==", + "requires": {} + }, "y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/plugins/core/ui/package.json b/plugins/core/ui/package.json index 32bb049cb..59fc2df0f 100644 --- a/plugins/core/ui/package.json +++ b/plugins/core/ui/package.json @@ -40,7 +40,8 @@ "vue2-google-maps": "^0.10.7", "vuetify": "^2.5.8", "vuex": "^3.6.2", - "xterm": "^4.13.0" + "xterm": "^4.13.0", + "xterm-addon-fit": "^0.5.0" }, "devDependencies": { "@babel/plugin-proposal-class-properties": "^7.13.0", @@ -51,6 +52,8 @@ "@babel/preset-typescript": "^7.13.0", "@types/blob-stream": "^0.1.30", "@types/dom-webcodecs": "^0.1.0", + "@types/engine.io": "^3.1.7", + "@types/engine.io-client": "^3.1.5", "@types/lodash": "^4.14.172", "@types/mkdirp": "^1.0.1", "@types/node": "^16.0.0", diff --git a/plugins/core/ui/src/components/ConsoleCard.vue b/plugins/core/ui/src/components/ConsoleCard.vue new file mode 100644 index 000000000..73a6dde2c --- /dev/null +++ b/plugins/core/ui/src/components/ConsoleCard.vue @@ -0,0 +1,39 @@ + + \ No newline at end of file diff --git a/plugins/core/ui/src/components/Device.vue b/plugins/core/ui/src/components/Device.vue index 5286da1b8..007432c2d 100644 --- a/plugins/core/ui/src/components/Device.vue +++ b/plugins/core/ui/src/components/Device.vue @@ -3,208 +3,213 @@ - -
- + + +
{{ alert.title }}
+
+
+
+ + + + {{ name || "No Device Name" }} + - -
{{ alert.title }}
-
- - - - - - {{ name || "No Device Name" }} - - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + + + - + + - Logs + + + + + + + + + + + - - + + + - - Delete DeviceConsole + + REPL + + Logs + + + + + + Delete Device + + This will permanently delete the device. It can not be + undone. + + + + + + Cancel - - This will permanently delete the device. It can not be - undone.Delete Device + + + - - - - - Cancel - Delete Device - - - - - Save - -
- Saved. - There was an error while saving. Please check the logs. -
+ Save + + + Saved. + There was an error while saving. Please check the logs.
- - - + + +   Plugin Management + + + + + + Reload Plugin + + + + Storage - -   Plugin Management - - - - - - Reload Plugin - - - - Storage - - {{ pluginData.packageJson.name }}@{{ - pluginData.packageJson.version - }} - Install Update {{ pluginData.updateAvailable }} - - - + + {{ pluginData.packageJson.name }}@{{ + pluginData.packageJson.version + }} + Install Update {{ pluginData.updateAvailable }} + + @@ -217,113 +222,98 @@ - - - + + +   Managed Device + + + + Native ID: + {{ pluginData.nativeId }} + + + Storage - -   Managed Device - - - - Native ID: - {{ pluginData.nativeId }} - - - Storage - - {{ - ownerDevice.name - }} - - - + + {{ + ownerDevice.name + }} + + - - - + + +   Integrations and Extensions + + + + - -   Integrations and Extensions - + + + - - - - - - - - {{ mixin.name }} - - - - - + + {{ mixin.name }} + + + + - - - Storage - - - - - - - - - - - + + Storage + + + + + + + + + + - - - - {{ iface }} - - - - + + + {{ iface }} + + +
@@ -331,18 +321,15 @@ - - - - - + {{ iface }} - - - + + + + + + + + + + @@ -372,6 +362,8 @@ import VueSlider from "vue-slider-component"; import "vue-slider-component/theme/material.css"; import LogCard from "./builtin/LogCard.vue"; +import ConsoleCard from "./ConsoleCard.vue"; +import REPLCard from "./REPLCard.vue"; import { inferTypesFromInterfaces, getComponentWebPath, @@ -411,6 +403,7 @@ import Storage from "../common/Storage.vue"; import { checkUpdate, installNpm, getNpmPath } from "./script/plugin"; import AggregateDevice from "./aggregate/AggregateDevice.vue"; import Automation from "./automation/Automation.vue"; +import Vue from "vue"; const cardHeaderInterfaces = [ ScryptedInterface.OccupancySensor, @@ -424,9 +417,7 @@ const cardHeaderInterfaces = [ const cardUnderInterfaces = [ScryptedInterface.DeviceProvider]; -const noCardInterfaces = [ - ScryptedInterface.Settings, -] +const noCardInterfaces = [ScryptedInterface.Settings]; const cardInterfaces = [ ScryptedInterface.Brightness, @@ -464,7 +455,7 @@ function filterInterfaces(interfaces) { ); if (ret.includes(ScryptedInterface.Camera)) { - ret = ret.filter(iface => iface !== ScryptedInterface.VideoCamera); + ret = ret.filter((iface) => iface !== ScryptedInterface.VideoCamera); } return ret; }; @@ -505,6 +496,8 @@ export default { VueSlider, LogCard, + ConsoleCard, + REPLCard, Storage, @@ -544,6 +537,8 @@ export default { initialState() { return { showLogs: false, + showConsole: false, + showRepl: false, showDelete: false, showSave: false, showSaveError: false, @@ -566,9 +561,24 @@ export default { .replace(//g, ">"); }, - openLogs() { + async openConsole() { + this.showConsole = !this.showConsole; + if (this.showConsole) { + await Vue.nextTick(); + this.$vuetify.goTo(this.$refs.consoleEl); + } + }, + async openRepl() { + this.showRepl = !this.showRepl; + if (this.showRepl) { + await Vue.nextTick(); + this.$vuetify.goTo(this.$refs.replEl); + } + }, + async openLogs() { this.showLogs = !this.showLogs; if (this.showLogs) { + await Vue.nextTick(); this.$vuetify.goTo(this.$refs.logsEl); } }, @@ -614,7 +624,7 @@ export default { ); this.pluginData = pluginData; checkUpdate(pluginData.pluginId, pluginData.packageJson.version).then( - updateAvailable => (pluginData.updateAvailable = updateAvailable) + (updateAvailable) => (pluginData.updateAvailable = updateAvailable) ); const device = this.device; diff --git a/plugins/core/ui/src/components/REPLCard.vue b/plugins/core/ui/src/components/REPLCard.vue new file mode 100644 index 000000000..bae47f948 --- /dev/null +++ b/plugins/core/ui/src/components/REPLCard.vue @@ -0,0 +1,48 @@ + + \ No newline at end of file diff --git a/plugins/core/ui/src/plugins/vuetify.ts b/plugins/core/ui/src/plugins/vuetify.ts index 0ae25a09a..adcf05d07 100644 --- a/plugins/core/ui/src/plugins/vuetify.ts +++ b/plugins/core/ui/src/plugins/vuetify.ts @@ -1,3 +1,4 @@ +import "xterm/css/xterm.css"; import '@fortawesome/fontawesome-free/css/all.css' import Vue from 'vue' import Vuetify, { diff --git a/plugins/google-home/package-lock.json b/plugins/google-home/package-lock.json index 46daebd49..f097bbf03 100644 --- a/plugins/google-home/package-lock.json +++ b/plugins/google-home/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/google-home", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/google-home", - "version": "0.0.4", + "version": "0.0.5", "dependencies": { "actions-on-google": "^2.13.0", "axios": "^0.21.1", diff --git a/plugins/google-home/package.json b/plugins/google-home/package.json index 9718eb1f9..31e19e3e6 100644 --- a/plugins/google-home/package.json +++ b/plugins/google-home/package.json @@ -35,5 +35,5 @@ "@types/mdns": "^0.0.34", "@types/url-parse": "^1.4.3" }, - "version": "0.0.4" + "version": "0.0.5" } diff --git a/plugins/google-home/src/main.ts b/plugins/google-home/src/main.ts index d2daa949d..9354b7745 100644 --- a/plugins/google-home/src/main.ts +++ b/plugins/google-home/src/main.ts @@ -83,7 +83,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin this.plugins = systemManager.getComponent('plugins'); this.localEndpoint = new http.Server((req, res) => { - this.log.i('got request'); + this.console.log('got request'); res.writeHead(404); res.end(); }); @@ -91,7 +91,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin endpointManager.getInsecurePublicLocalEndpoint().then(endpoint => { const url = new URL(endpoint); - this.log.i(endpoint); + this.console.log(endpoint); const ad = mdns.createAdvertisement(mdns.tcp('scrypted-gh'), parseInt(url.port)); ad.start(); }); @@ -237,7 +237,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin for (const queryDevice of input.payload.devices) { const device = systemManager.getDeviceById(queryDevice.id); if (!device) { - this.log.e(`query for missing device ${queryDevice.id}`); + this.console.error(`query for missing device ${queryDevice.id}`); ret.payload.devices[queryDevice.id] = { online: false, }; @@ -247,7 +247,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin const { type } = device; const supportedType = supportedTypes[type]; if (!supportedType) { - this.log.e(`query for unsupported type ${type}`); + this.console.error(`query for unsupported type ${type}`); ret.payload.devices[queryDevice.id] = { online: false, }; @@ -264,7 +264,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin }, status); } catch (e) { - this.log.e(`query failure for ${device.name}`); + this.console.error(`query failure for ${device.name}`); ret.payload.devices[queryDevice.id] = { status: 'ERROR', online: false, @@ -289,6 +289,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin for (const commandDevice of command.devices) { const device = systemManager.getDeviceById(commandDevice.id); if (!device) { + this.log.e(`execute failed, device not found ${JSON.stringify(commandDevice)}`); const error: SmartHomeV1ExecuteResponseCommands = { ids: [commandDevice.id], status: 'ERROR', @@ -298,9 +299,12 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin continue; } + this.log.i(`executing command on ${device.name}`); + for (const execution of command.execution) { const commandHandler = commandHandlers[execution.command] if (!commandHandler) { + this.log.e(`execute failed, command not supported ${JSON.stringify(execution)}`); const error: SmartHomeV1ExecuteResponseCommands = { ids: [commandDevice.id], status: 'ERROR', @@ -315,6 +319,7 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin ret.payload.commands.push(result); } catch (e) { + this.log.e(`execution failed ${e}`); const error: SmartHomeV1ExecuteResponseCommands = { ids: [commandDevice.id], status: 'ERROR', @@ -375,12 +380,12 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin if (!Object.keys(report.payload.devices.states).length) return; - this.log.i('reporting state:'); - this.log.i(JSON.stringify(report, undefined, 2)); + this.console.log('reporting state:'); + this.console.log(JSON.stringify(report, undefined, 2)); if (this.app.jwt) { const result = await this.app.reportState(report); - this.log.i('report state result:') - this.log.i(result); + this.console.log('report state result:') + this.console.log(result); return; } @@ -397,8 +402,8 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin Authorization: `Bearer ${token_info}` }, }); - this.log.i('report state result:'); - this.log.i(JSON.stringify(response.data)); + this.console.log('report state result:'); + this.console.log(JSON.stringify(response.data)); } async requestSync() { @@ -420,8 +425,8 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin Authorization: `Bearer ${token_info}` } }); - this.log.i('request sync result:'); - this.log.i(JSON.stringify(response.data)); + this.console.log('request sync result:'); + this.console.log(JSON.stringify(response.data)); } async onRequest(request: HttpRequest, response: HttpResponse): Promise { @@ -432,19 +437,19 @@ class GoogleHome extends ScryptedDeviceBase implements HttpRequestHandler, Engin return; } - this.log.i(request.body); + this.console.log(request.body); const body = JSON.parse(request.body); try { const result = await this.app.handler(body, request.headers as Headers); const res = JSON.stringify(result.body); - this.log.i(res); + this.console.log(res); response.send(res, { headers: result.headers, code: result.status, }); } catch (e) { - this.log.e(`request error ${e}`); + this.console.error(`request error ${e}`); response.send(e.message, { code: 500, }); diff --git a/sdk/index.generated.js b/sdk/index.generated.js index 5b177342f..146b41087 100644 --- a/sdk/index.generated.js +++ b/sdk/index.generated.js @@ -1,3 +1,6 @@ +const { Console } = require('console'); +const { PassThrough } = require('stream'); + class ScryptedDeviceBase { constructor(nativeId) { this.nativeId = nativeId; @@ -17,6 +20,14 @@ class ScryptedDeviceBase { return this._log; } + get console() { + if (!this._console) { + this._console = deviceManager.getDeviceConsole(this.nativeId); + } + + return this._console; + } + _lazyLoadDeviceState() { if (!this._deviceState) { if (this.nativeId) { @@ -51,33 +62,33 @@ class MixinDeviceBase { } } -(function() { -function _createGetState(state) { - return function() { - this._lazyLoadDeviceState(); - return this._deviceState[state]; - }; -} +(function () { + function _createGetState(state) { + return function () { + this._lazyLoadDeviceState(); + return this._deviceState[state]; + }; + } -function _createSetState(state) { - return function(value) { - this._lazyLoadDeviceState(); - this._deviceState[state] = value; - }; -} + function _createSetState(state) { + return function (value) { + this._lazyLoadDeviceState(); + this._deviceState[state] = value; + }; + } -var fields = ["id","interfaces","metadata","name","providedInterfaces","providedName","providedRoom","providedType","providerId","room","type","on","brightness","colorTemperature","rgb","hsv","running","paused","docked","temperature","temperatureUnit","humidity","thermostatAvailableModes","thermostatMode","thermostatSetpoint","thermostatSetpointHigh","thermostatSetpointLow","lockState","entryOpen","batteryLevel","online","updateAvailable","fromMimeType","toMimeType","binaryState","intrusionDetected","powerDetected","motionDetected","occupied","flooded","ultraviolet","luminance","position", -]; -for (var field of fields) { - Object.defineProperty(ScryptedDeviceBase.prototype, field, { - set: _createSetState(field), - get: _createGetState(field), - }); - Object.defineProperty(MixinDeviceBase.prototype, field, { - set: _createSetState(field), - get: _createGetState(field), - }); -} + var fields = ["id", "interfaces", "metadata", "name", "providedInterfaces", "providedName", "providedRoom", "providedType", "providerId", "room", "type", "on", "brightness", "colorTemperature", "rgb", "hsv", "running", "paused", "docked", "temperature", "temperatureUnit", "humidity", "thermostatAvailableModes", "thermostatMode", "thermostatSetpoint", "thermostatSetpointHigh", "thermostatSetpointLow", "lockState", "entryOpen", "batteryLevel", "online", "updateAvailable", "fromMimeType", "toMimeType", "binaryState", "intrusionDetected", "powerDetected", "motionDetected", "occupied", "flooded", "ultraviolet", "luminance", "position", + ]; + for (var field of fields) { + Object.defineProperty(ScryptedDeviceBase.prototype, field, { + set: _createSetState(field), + get: _createGetState(field), + }); + Object.defineProperty(MixinDeviceBase.prototype, field, { + set: _createSetState(field), + get: _createGetState(field), + }); + } })(); diff --git a/sdk/index.js b/sdk/index.js index b1ac6f3b1..43be68acb 100644 --- a/sdk/index.js +++ b/sdk/index.js @@ -1,48 +1,13 @@ var sdk = require('./index.generated.js'); try { - var mediaManagerProxy; - try { - mediaManagerProxy = mediaManager; - } - catch (e) { - var mediaManagerApply = function(target, prop, argumentsList) { - var copy = []; - if (argumentsList) { - for (var i in argumentsList) { - copy.push(NativeBuffer.from(argumentsList[i])); - } - } - var ret = mediaManager[prop].apply(mediaManager, copy); - var p = global['Promise']; - if (!p || (!prop.startsWith('convert'))) { - return ret; - } - // convert the promise to the globally available Promise. - return new p((resolve, reject) => { - // todo: dont use native buffer as a return value - ret.then(r => NativeBuffer.toBuffer(resolve(r))) - .catch(e => reject(e)); - }); - }; - - mediaManagerProxy = new Proxy(function(){}, { - get: function(target, prop) { - return function() { - return mediaManagerApply(target, prop, arguments) - } - }, - apply: mediaManagerApply, - }) - } - sdk = Object.assign(sdk, { log, - android, deviceManager, endpointManager, - mediaManager: mediaManagerProxy, + mediaManager, systemManager, + pluginHostAPI, }); } catch (e) { diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 18c3e0889..d0a78cea6 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/sdk", - "version": "0.0.64", + "version": "0.0.67", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/sdk", - "version": "0.0.64", + "version": "0.0.67", "license": "ISC", "dependencies": { "@babel/core": "^7.2.2", diff --git a/sdk/package.json b/sdk/package.json index 8b383145f..c0d7585b7 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/sdk", - "version": "0.0.65", + "version": "0.0.67", "description": "", "main": "index.js", "scripts": { diff --git a/sdk/types.d.ts b/sdk/types.d.ts index 13d261700..50d9b2df1 100644 --- a/sdk/types.d.ts +++ b/sdk/types.d.ts @@ -1,4 +1,3 @@ - /** * DeviceState is returned by DeviceManager.getDeviceState, and allows getting/setting of a device provided by a DeviceProvider. */ @@ -662,14 +661,14 @@ export interface DeviceManager { getDeviceLogger(nativeId: string): Logger; /** - * Get the device state maintained by Scrypted. Setting properties on this state will update the state in Scrypted. + * Get the console for the device given a native id. */ - getDeviceState(): DeviceState; + getDeviceConsole?(nativeId?: string): Console; /** * Get the device state maintained by Scrypted. Setting properties on this state will update the state in Scrypted. */ - getDeviceState(nativeId: string): DeviceState; + getDeviceState(nativeId?: string): DeviceState; /** * Get the per script Storage object. @@ -835,22 +834,6 @@ export interface SystemManager { */ removeDevice(id: string): Promise; -} -/** - * Android provides limited access to the Android system, to send Intents to other applications, such as Tasker. See Android SDK documentation for more information. - */ -export interface Android { - /** - * Create a new Intent. Use one of the send methods to send broadcasts, start activities, or start services. - */ - newIntent(): Intent; - - sendBroadcast(intent: Intent): Promise; - - startActivity(intent: Intent): Promise; - - startService(intent: Intent): Promise; - } /** * MixinProviders can add and intercept interfaces to other devices to add or augment their behavior. @@ -996,7 +979,6 @@ export enum ScryptedInterface { MediaSource = "MediaSource", MessagingEndpoint = "MessagingEndpoint", OauthClient = "OauthClient", - Android = "Android", MixinProvider = "MixinProvider", HttpRequestHandler = "HttpRequestHandler", EngineIOHandler = "EngineIOHandler", @@ -1066,16 +1048,6 @@ export enum ScryptedMimeTypes { RTCAVAnswer = 'x-scrypted/x-rtc-av-answer', } -// export interface ZwaveManagerDevice extends ZwaveManager, ScryptedDevice { -// } - -/** - * Android Intent. - * See: https://developer.android.com/reference/android/content/Intent - */ -interface Intent { -} - export interface ScryptedInterfaceDescriptor { name: string; properties: ScryptedInterfaceProperty[]; @@ -1092,14 +1064,13 @@ export interface ScryptedStatic { * @deprecated */ log?: Logger, - scriptSettings?: Settings, - android?: Android, deviceManager?: DeviceManager, endpointManager?: EndpointManager, mediaManager?: MediaManager, systemManager: SystemManager, - // zwaveManager?: ZwaveManagerDevice, + + pluginHostAPI?: any; } diff --git a/sdk/types.generated.js b/sdk/types.generated.js index 0b7a8fa26..dce1984ce 100644 --- a/sdk/types.generated.js +++ b/sdk/types.generated.js @@ -104,7 +104,6 @@ module.exports.ScryptedInterface = { MediaSource: "MediaSource", MessagingEndpoint: "MessagingEndpoint", OauthClient: "OauthClient", - Android: "Android", MixinProvider: "MixinProvider", HttpRequestHandler: "HttpRequestHandler", EngineIOHandler: "EngineIOHandler", @@ -501,17 +500,6 @@ module.exports.ScryptedInterfaceDescriptors = { "onOauthCallback", ] }, - Android: { - name: "Android", - properties: [ - ], - methods: [ - "newIntent", - "sendBroadcast", - "startActivity", - "startService", - ] - }, MixinProvider: { name: "MixinProvider", properties: [ diff --git a/server/src/component/plugin.ts b/server/src/component/plugin.ts index 8fbd1b23e..206e40652 100644 --- a/server/src/component/plugin.ts +++ b/server/src/component/plugin.ts @@ -75,4 +75,8 @@ export class PluginComponent { id: this.scrypted.findPluginDevice(pluginId), } } + + async getRemoteServicePort(pluginId: string, name: string): Promise { + return this.scrypted.plugins[pluginId].remote.getServicePort(name); + } } \ No newline at end of file diff --git a/server/src/plugin/cluster-helper.ts b/server/src/plugin/cluster-helper.ts new file mode 100644 index 000000000..bed90e315 --- /dev/null +++ b/server/src/plugin/cluster-helper.ts @@ -0,0 +1,15 @@ +import net from 'net'; +import { once } from 'events'; + +export async function listenZeroCluster(server: net.Server) { + while (true) { + const port = 10000 + Math.round(Math.random() * 30000); + server.listen(port); + try { + await once(server, 'listening'); + return (server.address() as net.AddressInfo).port; + } + catch (e) { + } + } +} \ No newline at end of file diff --git a/server/src/plugin/media.ts b/server/src/plugin/media.ts index 50c2d8c99..0f809213e 100644 --- a/server/src/plugin/media.ts +++ b/server/src/plugin/media.ts @@ -7,8 +7,7 @@ import { once } from 'events'; import fs from 'fs'; import tmp from 'tmp'; import net from 'net'; -import { sleep } from "../sleep"; -import { AddressInfo } from "ws"; +import { listenZeroCluster } from "./cluster-helper"; const wrtc = require('wrtc'); Object.assign(global, wrtc); @@ -22,18 +21,6 @@ interface RTCSession { const rtcSessions: { [id: string]: RTCSession } = {}; -async function listenZeroCluster(server: net.Server) { - while (true) { - const port = 10000 + Math.round(Math.random() * 30000); - server.listen(port); - try { - await once(server, 'listening'); - return (server.address() as AddressInfo).port; - } - catch (e) { - } - } -} function addBuiltins(converters: BufferConverter[]) { converters.push({ diff --git a/server/src/plugin/plugin-api.ts b/server/src/plugin/plugin-api.ts index 73e6204ad..6fa6f7c2c 100644 --- a/server/src/plugin/plugin-api.ts +++ b/server/src/plugin/plugin-api.ts @@ -43,6 +43,8 @@ export interface PluginRemote { ioEvent(id: string, event: string, message?: any): Promise; createDeviceState(id: string, setState: (property: string, value: any) => Promise): Promise; + + getServicePort(name: string): Promise; } export interface MediaObjectRemote extends MediaObject { diff --git a/server/src/plugin/plugin-host.ts b/server/src/plugin/plugin-host.ts index 52ecd5aa6..5b33b2e2b 100644 --- a/server/src/plugin/plugin-host.ts +++ b/server/src/plugin/plugin-host.ts @@ -1,7 +1,7 @@ import cluster from 'cluster'; import { RpcMessage, RpcPeer } from '../rpc'; import AdmZip from 'adm-zip'; -import { ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, EngineIOHandler, ScryptedInterfaceProperty, MediaManager, SystemDeviceState } from '@scrypted/sdk/types' +import { ScryptedDevice, Device, DeviceManifest, EventDetails, EventListenerOptions, EventListenerRegister, EngineIOHandler, ScryptedInterfaceProperty, MediaManager, SystemDeviceState, ScryptedStatic } from '@scrypted/sdk/types' import { ScryptedRuntime } from '../runtime'; import { Plugin } from '../db-types'; import io from 'engine.io'; @@ -10,7 +10,14 @@ import { PluginAPI, PluginRemote } from './plugin-api'; import { Logger } from '../logger'; import { MediaManagerImpl } from './media'; import { getState } from '../state'; -import WebSocket from 'ws'; +import WebSocket, { EventEmitter } from 'ws'; +import { listenZeroCluster } from './cluster-helper'; +import { Server } from 'net'; +import repl, { REPLServer } from 'repl'; +import { once } from 'events'; +import { PassThrough } from 'stream'; +import { Console } from 'console' +import util from 'util'; export class PluginHost { worker: cluster.Worker; @@ -58,6 +65,10 @@ export class PluginHost { if (true) { this.worker = cluster.fork(); + this.worker.process.stdout.on('data', data => { + process.stdout.write(data); + }); + this.worker.process.stderr.on('data', data => process.stderr.write(data)); let connected = true; this.worker.on('disconnect', () => { @@ -107,7 +118,9 @@ export class PluginHost { } }); - attachPluginRemote(remote, async (systemManager) => new MediaManagerImpl(systemManager)); + attachPluginRemote(remote, { + createMediaManager: async (systemManager) => new MediaManagerImpl(systemManager), + }); } @@ -151,6 +164,7 @@ export class PluginHost { const device = scrypted.findPluginDevice(plugin._id, nativeId); return self.scrypted.getDeviceLogger(device); } + getComponent(id: string): Promise { return self.scrypted.getComponent(id); } @@ -283,7 +297,149 @@ export class PluginHost { } } -export async function startPluginCluster() { +async function createConsoleServer(events: EventEmitter): Promise { + const outputs = new Map(); + const appendOutput = (data: Buffer, nativeId: string) => { + if (!nativeId) + nativeId = undefined; + let buffers = outputs.get(nativeId); + if (!buffers) { + buffers = []; + outputs.set(nativeId, buffers); + } + buffers.push(data); + }; + events.on('stdout', appendOutput); + events.on('stderr', appendOutput); + + const server = new Server(async (socket) => { + let [filter] = await once(socket, 'data'); + filter = filter.toString().trim(); + if (filter === 'undefined') + filter = undefined; + + const buffers = outputs.get(filter); + if (buffers) { + const concat = Buffer.concat(buffers); + outputs.set(filter, [concat]); + socket.write(concat); + } + + const cb = (data: Buffer, nativeId: string) => { + if (nativeId !== filter) + return; + socket.write(data); + }; + events.on('stdout', cb) + events.on('stderr', cb) + + const cleanup = () => { + events.removeListener('stdout', cb); + events.removeListener('stderr', cb); + }; + + socket.on('close', cleanup); + socket.on('error', cleanup); + socket.on('end', cleanup); + }); + return listenZeroCluster(server); +} + +async function createREPLServer(events: EventEmitter): Promise { + const [[scrypted], [params], [plugin]] = await Promise.all([once(events, 'scrypted'), once(events, 'params'), once(events, 'plugin')]); + const { deviceManager, systemManager } = scrypted; + const server = new Server(async (socket) => { + let [filter] = await once(socket, 'data'); + filter = filter.toString().trim(); + if (filter === 'undefined') + filter = undefined; + + const chain: string[] = []; + const nativeIds: Map = deviceManager.nativeIds; + const reversed = new Map(); + for (const nativeId of nativeIds.keys()) { + reversed.set(nativeIds.get(nativeId).id, nativeId); + } + + while (filter) { + const { id } = nativeIds.get(filter); + const d = await systemManager.getDeviceById(id); + chain.push(filter); + filter = reversed.get(d.providerId); + } + + chain.reverse(); + let device = plugin; + for (const c of chain) { + device = await device.getDevice(c); + } + + const r = repl.start({ + terminal: true, + input: socket, + output: socket, + // writer(this: REPLServer, obj: any) { + // const ret = util.inspect(obj, { + // colors: true, + // }); + // return ret;//.replaceAll('\n', '\r\n'); + // }, + preview: false, + }); + + const ctx = Object.assign(params, { + device + }); + delete ctx.console; + Object.assign(r.context, ctx); + + const cleanup = () => { + r.close(); + }; + + socket.on('close', cleanup); + socket.on('error', cleanup); + socket.on('end', cleanup); + }); + return listenZeroCluster(server); +} + +export function startPluginClusterWorker() { + const events = new EventEmitter(); + + const getDeviceConsole = (nativeId?: string) => { + const stdout = new PassThrough(); + const stderr = new PassThrough(); + + stdout.on('data', data => events.emit('stdout', data, nativeId)); + stderr.on('data', data => this.events.emit('stderr', data, nativeId)); + + const ret = new Console(stdout, stderr); + + const methods = [ + 'log', 'warn', + 'dir', 'time', + 'timeEnd', 'timeLog', + 'trace', 'assert', + 'clear', 'count', + 'countReset', 'group', + 'groupEnd', 'table', + 'debug', 'info', + 'dirxml', 'error', + 'groupCollapsed', + ]; + + for (const m of methods) { + const old = (ret as any)[m].bind(ret); + (ret as any)[m] = (...args: any[]) => { + (console as any)[m](...args); + old(...args); + } + } + + return ret; + } + const peer = new RpcPeer((message, reject) => process.send(message, undefined, { swallowErrors: !reject, }, e => { @@ -291,16 +447,34 @@ export async function startPluginCluster() { reject(e); })); process.on('message', message => peer.handleMessage(message as RpcMessage)); - const scrypted = await attachPluginRemote(peer, async (systemManager) => new MediaManagerImpl(systemManager)); - process.on('uncaughtException', e => { - scrypted.log.e('uncaughtException'); - scrypted.log.e(e.toString()); - scrypted.log.e(e.stack); - }); - process.on('unhandledRejection', e => { - scrypted.log.e('unhandledRejection'); - scrypted.log.e(e.toString()); - }); + + const consolePort = createConsoleServer(events); + const replPort = createREPLServer(events); + + attachPluginRemote(peer, { + createMediaManager: async (systemManager) => new MediaManagerImpl(systemManager), + events, + getDeviceConsole, + async getServicePort(name) { + if (name === 'repl') + return replPort; + if (name === 'console') + return consolePort; + throw new Error(`unknown service ${name}`); + } + }).then(scrypted => { + events.emit('scrypted', scrypted); + + process.on('uncaughtException', e => { + scrypted.log.e('uncaughtException'); + scrypted.log.e(e.toString()); + scrypted.log.e(e.stack); + }); + process.on('unhandledRejection', e => { + scrypted.log.e('unhandledRejection'); + scrypted.log.e(e.toString()); + }); + }) } class LazyRemote implements PluginRemote { @@ -331,4 +505,9 @@ class LazyRemote implements PluginRemote { async createDeviceState(id: string, setState: (property: string, value: any) => Promise): Promise { return (await this.init).createDeviceState(id, setState); } + + async getServicePort(name: string): Promise { + return (await this.init).getServicePort(name); + + } } \ No newline at end of file diff --git a/server/src/plugin/plugin-remote.ts b/server/src/plugin/plugin-remote.ts index d06d3f7c2..19c4a3337 100644 --- a/server/src/plugin/plugin-remote.ts +++ b/server/src/plugin/plugin-remote.ts @@ -7,13 +7,16 @@ import { PluginAPI, PluginLogger, PluginRemote } from './plugin-api'; import { SystemManagerImpl } from './system'; import { RpcPeer } from '../rpc'; import { BufferSerializer } from './buffer-serializer'; +import { Console } from 'console'; +import { EventEmitter, PassThrough } from 'stream'; +import { Writable } from 'node:stream'; class DeviceLogger implements Logger { nativeId: string; api: PluginAPI; logger: Promise; - constructor(api: PluginAPI, nativeId: string) { + constructor(api: PluginAPI, nativeId: string, public console: any) { this.api = api; this.nativeId = nativeId; } @@ -25,7 +28,7 @@ class DeviceLogger implements Logger { } async log(level: string, message: string) { - console.log(message); + this.console.log(message); (await this.ensureLogger()).log(level, message); } @@ -135,12 +138,12 @@ class DeviceManagerImpl implements DeviceManager { nativeIds = new Map(); systemManager: SystemManagerImpl; - constructor(systemManager: SystemManagerImpl) { + constructor(systemManager: SystemManagerImpl, public events?: EventEmitter, public getDeviceConsole?: (nativeId?: string) => Console) { this.systemManager = systemManager; } getDeviceLogger(nativeId?: string): Logger { - return new DeviceLogger(this.api, nativeId); + return new DeviceLogger(this.api, nativeId, this.getDeviceConsole?.(nativeId) || console); } getDeviceState(nativeId?: any): DeviceState { @@ -172,27 +175,6 @@ class DeviceManagerImpl implements DeviceManager { } } - -class PushManagerImpl implements PushManager { - getSubscription(): Promise { - throw new Error('Method not implemented.'); - } - permissionState(options?: PushSubscriptionOptionsInit): Promise { - throw new Error('Method not implemented.'); - } - subscribe(options?: PushSubscriptionOptionsInit): Promise { - throw new Error('Method not implemented.'); - } - - getRegistrationId(): string { - return 'no-registration-id-fix-this'; - } - - getSenderId(): string { - return 'no-sender-id-fix-this'; - } -} - class StorageImpl implements Storage { api: PluginAPI; @@ -297,19 +279,25 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId: return ret; } -export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (systemManager: SystemManager) => Promise): Promise { +export interface PluginRemoteAttachOptions { + createMediaManager?: (systemManager: SystemManager) => Promise; + getServicePort?: (name: string) => Promise; + getDeviceConsole?: (nativeId?: string) => Console; + events?: EventEmitter; +} + +export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise { + const { createMediaManager, getServicePort, events, getDeviceConsole } = options || {}; + peer.addSerializer(Buffer, 'Buffer', new BufferSerializer()); - let done: any; - const retPromise = new Promise((resolve, reject) => { - done = resolve; - }); + let done: (scrypted: ScryptedStatic) => void; + const retPromise = new Promise(resolve => done = resolve); peer.params.getRemote = async (api: PluginAPI, pluginId: string) => { const systemManager = new SystemManagerImpl(); - const deviceManager = new DeviceManagerImpl(systemManager); + const deviceManager = new DeviceManagerImpl(systemManager, events, getDeviceConsole); const endpointManager = new EndpointManagerImpl(); - const pushManager = new PushManagerImpl(); const ioSockets: { [id: string]: WebSocketCallbacks } = {}; const mediaManager = await api.getMediaManager() || await createMediaManager(systemManager); @@ -336,6 +324,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy const localStorage = new StorageImpl(deviceManager, undefined); const remote: PluginRemote = { + getServicePort, createDeviceState(id: string, setState: (property: string, value: any) => Promise) { const handler = new DeviceStateProxyHandler(deviceManager, id, setState); return new Proxy(handler, handler); @@ -378,7 +367,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy } }, - async notify(id: string, eventTime: number, eventInterface: string, property: string, value: SystemDeviceState|any, changed?: boolean) { + async notify(id: string, eventTime: number, eventInterface: string, property: string, value: SystemDeviceState | any, changed?: boolean) { if (property) { const state = systemManager.state?.[id]; if (!state) { @@ -416,7 +405,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy volume.writeFileSync(name, entry.getData()); } - const params = { + const params: any = { // legacy android: {}, @@ -429,7 +418,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy error, end }; - + connect(undefined, { close: () => api.ioClose(id), }, (message: string) => api.ioSend(id, message)); @@ -442,7 +431,7 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy error, end }; - + connect(undefined, { close: () => api.ioClose(id), }, (message: string) => api.ioSend(id, message)); @@ -463,18 +452,24 @@ export async function attachPluginRemote(peer: RpcPeer, createMediaManager?: (sy const module = require(name); return module; }, - pushManager, deviceManager, systemManager, mediaManager, endpointManager, log, localStorage, - zwaveManager: null as any, + pluginHostAPI: api, + }; + + if (getDeviceConsole) { + params.console = getDeviceConsole(undefined); } + events?.emit('params', params); + try { peer.evalLocal(script, '/plugin/main.nodejs.js', params); + events?.emit('plugin', exports.default); return exports.default; } catch (e) { diff --git a/server/src/runtime.ts b/server/src/runtime.ts index e6524db55..47ee2fe47 100644 --- a/server/src/runtime.ts +++ b/server/src/runtime.ts @@ -170,9 +170,23 @@ export class ScryptedRuntime { async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean, handler: (req: Request, res: Response, endpointRequest: HttpRequest, pluginHost: PluginHost, pluginDevice: PluginDevice) => void) { + const isUpgrade = !!(req as any).upgradeHead; + + const end = (code: number, message: string) => { + if (isUpgrade) { + const socket = res.socket; + socket.write(`HTTP/1.1 ${code} ${message}\r\n` + + '\r\n'); + socket.destroy(); + } + else { + res.status(code); + res.send(message); + } + }; + if (!isPublicEndpoint && !res.locals.username) { - res.status(401); - res.send('Not logged in'); + end(401, 'Not Authorized'); return; } @@ -192,17 +206,17 @@ export class ScryptedRuntime { } const pluginDevice = this.findPluginDevice(endpoint) ?? this.findPluginDeviceById(endpoint); - if (!pluginHost || !pluginDevice) { - if (req.headers.connection?.toLowerCase() === 'upgrade' && (req.headers.upgrade?.toLowerCase() !== 'websocket' || !pluginDevice?.state.interfaces.value.includes(ScryptedInterface.EngineIOHandler))) { - const socket = res.socket; - socket.write('HTTP/1.1 404 Not Found\r\n' + - '\r\n'); - socket.destroy(); + + // check if upgrade requests can be handled. must be websocket. + if (isUpgrade) { + if (req.headers.upgrade?.toLowerCase() !== 'websocket' || !pluginDevice?.state.interfaces.value.includes(ScryptedInterface.EngineIOHandler)) { + end(404, 'Not Found'); return; } - else if (!pluginDevice?.state.interfaces.value.includes(ScryptedInterface.HttpRequestHandler)) { - res.status(404); - res.send() + } + else { + if (!pluginDevice?.state.interfaces.value.includes(ScryptedInterface.HttpRequestHandler)) { + end(404, 'Not Found'); return; } } @@ -300,7 +314,7 @@ export class ScryptedRuntime { endpointRequest, pluginDevice, }; - if (req.headers.upgrade) + if ((req as any).upgradeHead) pluginHost.io.handleUpgrade(req, res.socket, (req as any).upgradeHead) else pluginHost.io.handleRequest(req, res); diff --git a/server/src/scrypted-main.ts b/server/src/scrypted-main.ts index d0cdc85b1..97ad06359 100644 --- a/server/src/scrypted-main.ts +++ b/server/src/scrypted-main.ts @@ -8,7 +8,7 @@ import express from 'express'; import bodyParser from 'body-parser'; import cluster from 'cluster'; import net from 'net'; -import { startPluginCluster } from './plugin/plugin-host'; +import { startPluginClusterWorker as startPluginRemoteClusterWorker } from './plugin/plugin-host'; import { ScryptedRuntime } from './runtime'; import level from './level'; import { Plugin, ScryptedUser, Settings } from './db-types'; @@ -28,7 +28,7 @@ process.on('unhandledRejection', error => { if (!cluster.isMaster) { - startPluginCluster(); + startPluginRemoteClusterWorker(); } else { let workerInspectPort: number = undefined;