@scrypted/server 0.94.22 → 0.94.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/plugin-api.d.ts +3 -10
- package/dist/plugin/plugin-host.d.ts +2 -0
- package/dist/plugin/plugin-host.js +30 -48
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-lazy-remote.d.ts +1 -1
- package/dist/plugin/plugin-lazy-remote.js +2 -2
- package/dist/plugin/plugin-lazy-remote.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +26 -48
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.d.ts +1 -2
- package/dist/plugin/plugin-remote.js +2 -2
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/custom-worker.d.ts +12 -0
- package/dist/plugin/runtime/custom-worker.js +72 -0
- package/dist/plugin/runtime/custom-worker.js.map +1 -0
- package/dist/plugin/runtime/node-thread-worker.d.ts +1 -1
- package/dist/plugin/runtime/node-thread-worker.js +2 -2
- package/dist/plugin/runtime/node-thread-worker.js.map +1 -1
- package/dist/plugin/runtime/node-worker-common.d.ts +13 -0
- package/dist/plugin/runtime/node-worker-common.js +76 -0
- package/dist/plugin/runtime/node-worker-common.js.map +1 -0
- package/dist/plugin/runtime/python-worker.js.map +1 -1
- package/dist/plugin/runtime/runtime-worker.d.ts +3 -0
- package/dist/runtime.d.ts +2 -4
- package/dist/runtime.js +3 -72
- package/dist/runtime.js.map +1 -1
- package/package.json +3 -3
- package/python/plugin_remote.py +34 -27
- package/python/plugin_volume.py +45 -0
- package/src/plugin/plugin-api.ts +3 -10
- package/src/plugin/plugin-host.ts +35 -50
- package/src/plugin/plugin-lazy-remote.ts +2 -2
- package/src/plugin/plugin-remote-worker.ts +28 -53
- package/src/plugin/plugin-remote.ts +3 -5
- package/src/plugin/runtime/custom-worker.ts +81 -0
- package/src/plugin/runtime/node-thread-worker.ts +2 -4
- package/src/plugin/runtime/node-worker-common.ts +77 -0
- package/src/plugin/runtime/python-worker.ts +1 -1
- package/src/plugin/runtime/runtime-worker.ts +3 -0
- package/src/runtime.ts +4 -81
| @@ -1,10 +1,10 @@ | |
| 1 1 | 
             
            import { ScryptedStatic, SystemManager } from '@scrypted/types';
         | 
| 2 | 
            -
            import AdmZip from 'adm-zip';
         | 
| 3 2 | 
             
            import { once } from 'events';
         | 
| 4 3 | 
             
            import fs from 'fs';
         | 
| 5 4 | 
             
            import net from 'net';
         | 
| 6 5 | 
             
            import path from 'path';
         | 
| 7 6 | 
             
            import { install as installSourceMapSupport } from 'source-map-support';
         | 
| 7 | 
            +
            import worker_threads from 'worker_threads';
         | 
| 8 8 | 
             
            import { computeClusterObjectHash } from '../cluster/cluster-hash';
         | 
| 9 9 | 
             
            import { ClusterObject, ConnectRPCObject } from '../cluster/connect-rpc-object';
         | 
| 10 10 | 
             
            import { listenZero } from '../listen-zero';
         | 
| @@ -14,11 +14,12 @@ import { MediaManagerImpl } from './media'; | |
| 14 14 | 
             
            import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
         | 
| 15 15 | 
             
            import { prepareConsoles } from './plugin-console';
         | 
| 16 16 | 
             
            import { getPluginNodePath, installOptionalDependencies } from './plugin-npm-dependencies';
         | 
| 17 | 
            -
            import { DeviceManagerImpl,  | 
| 17 | 
            +
            import { DeviceManagerImpl, attachPluginRemote, setupPluginRemote } from './plugin-remote';
         | 
| 18 18 | 
             
            import { PluginStats, startStatsUpdater } from './plugin-remote-stats';
         | 
| 19 19 | 
             
            import { createREPLServer } from './plugin-repl';
         | 
| 20 | 
            +
            import { getPluginVolume } from './plugin-volume';
         | 
| 20 21 | 
             
            import { NodeThreadWorker } from './runtime/node-thread-worker';
         | 
| 21 | 
            -
            import  | 
| 22 | 
            +
            import { prepareZip } from './runtime/node-worker-common';
         | 
| 22 23 |  | 
| 23 24 | 
             
            const serverVersion = require('../../package.json').version;
         | 
| 24 25 |  | 
| @@ -75,8 +76,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 75 76 | 
             
                        }
         | 
| 76 77 | 
             
                        throw new Error(`unknown service ${name}`);
         | 
| 77 78 | 
             
                    },
         | 
| 78 | 
            -
                    async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any,  | 
| 79 | 
            -
                        const { clusterId, clusterSecret } = zipOptions;
         | 
| 79 | 
            +
                    async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any, getZip: () => Promise<Buffer>, zipOptions: PluginRemoteLoadZipOptions) {
         | 
| 80 | 
            +
                        const { clusterId, clusterSecret, zipHash } = zipOptions;
         | 
| 81 | 
            +
                        const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), zipHash, getZip);
         | 
| 80 82 |  | 
| 81 83 | 
             
                        const onProxySerialization = (value: any, proxyId: string, sourcePeerPort?: number) => {
         | 
| 82 84 | 
             
                            const properties = RpcPeer.prepareProxyProperties(value) || {};
         | 
| @@ -194,50 +196,20 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 194 196 | 
             
                                return value;
         | 
| 195 197 | 
             
                            }
         | 
| 196 198 | 
             
                        }
         | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
                             | 
| 202 | 
            -
                                 | 
| 203 | 
            -
                                if (fs.existsSync(fsDir))
         | 
| 204 | 
            -
                                    process.chdir(fsDir);
         | 
| 205 | 
            -
                                else
         | 
| 206 | 
            -
                                    process.chdir(zipOptions.unzippedPath);
         | 
| 207 | 
            -
                            }
         | 
| 208 | 
            -
             | 
| 209 | 
            -
                            // volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
         | 
| 210 | 
            -
                            pluginReader = name => {
         | 
| 211 | 
            -
                                const filename = path.join(zipOptions.unzippedPath, name);
         | 
| 212 | 
            -
                                if (!fs.existsSync(filename))
         | 
| 213 | 
            -
                                    return;
         | 
| 214 | 
            -
                                return fs.readFileSync(filename);
         | 
| 215 | 
            -
                            };
         | 
| 199 | 
            +
                        if (worker_threads.isMainThread) {
         | 
| 200 | 
            +
                            const fsDir = path.join(unzippedPath, 'fs')
         | 
| 201 | 
            +
                            if (fs.existsSync(fsDir))
         | 
| 202 | 
            +
                                process.chdir(fsDir);
         | 
| 203 | 
            +
                            else
         | 
| 204 | 
            +
                                process.chdir(unzippedPath);
         | 
| 216 205 | 
             
                        }
         | 
| 217 | 
            -
             | 
| 218 | 
            -
             | 
| 219 | 
            -
             | 
| 220 | 
            -
                             | 
| 221 | 
            -
             | 
| 222 | 
            -
                             | 
| 223 | 
            -
             | 
| 224 | 
            -
                            //         continue;
         | 
| 225 | 
            -
                            //     if (!entry.entryName.startsWith('fs/'))
         | 
| 226 | 
            -
                            //         continue;
         | 
| 227 | 
            -
                            //     const name = entry.entryName.substring('fs/'.length);
         | 
| 228 | 
            -
                            //     volume.mkdirpSync(path.dirname(name));
         | 
| 229 | 
            -
                            //     const data = entry.getData();
         | 
| 230 | 
            -
                            //     volume.writeFileSync(name, data);
         | 
| 231 | 
            -
                            // }
         | 
| 232 | 
            -
             | 
| 233 | 
            -
                            pluginReader = name => {
         | 
| 234 | 
            -
                                const entry = admZip.getEntry(name);
         | 
| 235 | 
            -
                                if (!entry)
         | 
| 236 | 
            -
                                    return;
         | 
| 237 | 
            -
                                return entry.getData();
         | 
| 238 | 
            -
                            }
         | 
| 239 | 
            -
                        }
         | 
| 240 | 
            -
                        zipData = undefined;
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                        const pluginReader = (name: string) => {
         | 
| 208 | 
            +
                            const filename = path.join(unzippedPath, name);
         | 
| 209 | 
            +
                            if (!fs.existsSync(filename))
         | 
| 210 | 
            +
                                return;
         | 
| 211 | 
            +
                            return fs.readFileSync(filename);
         | 
| 212 | 
            +
                        };
         | 
| 241 213 |  | 
| 242 214 | 
             
                        const pluginConsole = getPluginConsole?.();
         | 
| 243 215 | 
             
                        params.console = pluginConsole;
         | 
| @@ -248,9 +220,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 248 220 | 
             
                                return require('fs');
         | 
| 249 221 | 
             
                            }
         | 
| 250 222 | 
             
                            try {
         | 
| 251 | 
            -
                                if (name.startsWith('.') &&  | 
| 223 | 
            +
                                if (name.startsWith('.') && unzippedPath) {
         | 
| 252 224 | 
             
                                    try {
         | 
| 253 | 
            -
                                        const c = path.join( | 
| 225 | 
            +
                                        const c = path.join(unzippedPath, name);
         | 
| 254 226 | 
             
                                        const module = require(c);
         | 
| 255 227 | 
             
                                        return module;
         | 
| 256 228 | 
             
                                    }
         | 
| @@ -314,7 +286,6 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 314 286 | 
             
                        peer.getParam('updateStats').then(updateStats => startStatsUpdater(allMemoryStats, updateStats));
         | 
| 315 287 |  | 
| 316 288 | 
             
                        const main = pluginReader('main.nodejs.js');
         | 
| 317 | 
            -
                        pluginReader = undefined;
         | 
| 318 289 | 
             
                        const script = main.toString();
         | 
| 319 290 |  | 
| 320 291 | 
             
                        scrypted.connect = (socket, options) => {
         | 
| @@ -328,6 +299,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 328 299 | 
             
                                packageJson,
         | 
| 329 300 | 
             
                                env: process.env,
         | 
| 330 301 | 
             
                                pluginDebug: undefined,
         | 
| 302 | 
            +
                                zipFile,
         | 
| 303 | 
            +
                                unzippedPath,
         | 
| 304 | 
            +
                                zipHash,
         | 
| 331 305 | 
             
                            });
         | 
| 332 306 |  | 
| 333 307 | 
             
                            const result = (async () => {
         | 
| @@ -368,7 +342,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 368 342 |  | 
| 369 343 | 
             
                                const forkOptions = Object.assign({}, zipOptions);
         | 
| 370 344 | 
             
                                forkOptions.fork = true;
         | 
| 371 | 
            -
                                return remote.loadZip(packageJson,  | 
| 345 | 
            +
                                return remote.loadZip(packageJson, getZip, forkOptions)
         | 
| 372 346 | 
             
                            })();
         | 
| 373 347 |  | 
| 374 348 | 
             
                            result.catch(() => ntw.kill());
         | 
| @@ -380,7 +354,8 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe | |
| 380 354 | 
             
                        }
         | 
| 381 355 |  | 
| 382 356 | 
             
                        try {
         | 
| 383 | 
            -
                             | 
| 357 | 
            +
                            const filename = zipOptions?.debug ? '/plugin/main.nodejs.js' : `/${pluginId}/main.nodejs.js`;
         | 
| 358 | 
            +
                            peer.evalLocal(script, filename, params);
         | 
| 384 359 |  | 
| 385 360 | 
             
                            if (zipOptions?.fork) {
         | 
| 386 361 | 
             
                                // pluginConsole?.log('plugin forked');
         | 
| @@ -455,15 +455,13 @@ export interface WebSocketCustomHandler { | |
| 455 455 | 
             
                methods: WebSocketMethods;
         | 
| 456 456 | 
             
            }
         | 
| 457 457 |  | 
| 458 | 
            -
            export type PluginReader = (name: string) => Buffer;
         | 
| 459 | 
            -
             | 
| 460 458 | 
             
            export interface PluginRemoteAttachOptions {
         | 
| 461 459 | 
             
                createMediaManager?: (systemManager: SystemManager, deviceManager: DeviceManagerImpl) => Promise<MediaManager>;
         | 
| 462 460 | 
             
                getServicePort?: (name: string, ...args: any[]) => Promise<number>;
         | 
| 463 461 | 
             
                getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
         | 
| 464 462 | 
             
                getPluginConsole?: () => Console;
         | 
| 465 463 | 
             
                getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
         | 
| 466 | 
            -
                onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any,  | 
| 464 | 
            +
                onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any, getZip: () => Promise<Buffer>, zipOptions: PluginRemoteLoadZipOptions) => Promise<any>;
         | 
| 467 465 | 
             
                onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
         | 
| 468 466 | 
             
            }
         | 
| 469 467 |  | 
| @@ -637,7 +635,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp | |
| 637 635 | 
             
                            done(ret);
         | 
| 638 636 | 
             
                        },
         | 
| 639 637 |  | 
| 640 | 
            -
                        async loadZip(packageJson: any,  | 
| 638 | 
            +
                        async loadZip(packageJson: any, getZip: () => Promise<Buffer>, zipOptions?: PluginRemoteLoadZipOptions) {
         | 
| 641 639 | 
             
                            const params: any = {
         | 
| 642 640 | 
             
                                __filename: undefined,
         | 
| 643 641 | 
             
                                deviceManager,
         | 
| @@ -660,7 +658,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp | |
| 660 658 | 
             
                            params.pluginRuntimeAPI = ret;
         | 
| 661 659 |  | 
| 662 660 | 
             
                            try {
         | 
| 663 | 
            -
                                return await options.onLoadZip(ret, params, packageJson,  | 
| 661 | 
            +
                                return await options.onLoadZip(ret, params, packageJson, getZip, zipOptions);
         | 
| 664 662 | 
             
                            }
         | 
| 665 663 | 
             
                            catch (e) {
         | 
| 666 664 | 
             
                                console.error('plugin start/fork failed', e)
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            import { ScryptedRuntimeArguments } from '@scrypted/types';
         | 
| 2 | 
            +
            import child_process from 'child_process';
         | 
| 3 | 
            +
            import { Readable, Writable } from 'stream';
         | 
| 4 | 
            +
            import { RpcMessage, RpcPeer } from "../../rpc";
         | 
| 5 | 
            +
            import { createRpcDuplexSerializer } from '../../rpc-serializer';
         | 
| 6 | 
            +
            import type { ScryptedRuntime } from '../../runtime';
         | 
| 7 | 
            +
            import { ChildProcessWorker } from "./child-process-worker";
         | 
| 8 | 
            +
            import { RuntimeWorkerOptions } from "./runtime-worker";
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            export class CustomRuntimeWorker extends ChildProcessWorker {
         | 
| 11 | 
            +
                serializer: ReturnType<typeof createRpcDuplexSerializer>;
         | 
| 12 | 
            +
                fork: boolean;
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                constructor(pluginId: string, options: RuntimeWorkerOptions, runtime: ScryptedRuntime) {
         | 
| 15 | 
            +
                    super(pluginId, options);
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    const pluginDevice = runtime.findPluginDevice(this.pluginId);
         | 
| 18 | 
            +
                    const scryptedRuntimeArguments: ScryptedRuntimeArguments = pluginDevice.state.scryptedRuntimeArguments?.value;
         | 
| 19 | 
            +
                    if (!scryptedRuntimeArguments)
         | 
| 20 | 
            +
                        throw new Error('custom runtime requires scryptedRuntimeArguments');
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    const { env, pluginDebug } = options;
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    const args = [...scryptedRuntimeArguments.arguments || []];
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    const opts: child_process.ForkOptions | child_process.SpawnOptions = {
         | 
| 27 | 
            +
                        // stdin, stdout, stderr, peer in, peer out
         | 
| 28 | 
            +
                        stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
         | 
| 29 | 
            +
                        env: Object.assign({
         | 
| 30 | 
            +
                            SCRYYPTED_PLUGIN_ID: pluginId,
         | 
| 31 | 
            +
                            SCRYPTED_DEBUG_PORT: pluginDebug?.inspectPort?.toString(),
         | 
| 32 | 
            +
                            SCRYPTED_UNZIPPED_PATH: options.unzippedPath,
         | 
| 33 | 
            +
                            SCRYPTED_ZIP_FILE: options.zipFile,
         | 
| 34 | 
            +
                            SCRYPTED_ZIP_HASH: options.zipHash,
         | 
| 35 | 
            +
                        }, process.env, env),
         | 
| 36 | 
            +
                    };
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    if (!scryptedRuntimeArguments.executable) {
         | 
| 39 | 
            +
                        this.fork = true;
         | 
| 40 | 
            +
                        const modulePath = args.shift();
         | 
| 41 | 
            +
                        if (!modulePath)
         | 
| 42 | 
            +
                            throw new Error('fork runtime requires modulePath in first argument.');
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                        (opts.stdio as any)[5] = 'ipc';
         | 
| 45 | 
            +
                        this.worker = child_process.fork(modulePath, args, opts);
         | 
| 46 | 
            +
                    }
         | 
| 47 | 
            +
                    else {
         | 
| 48 | 
            +
                        this.worker = child_process.spawn(scryptedRuntimeArguments.executable, args, opts);
         | 
| 49 | 
            +
                    }
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    this.setupWorker();
         | 
| 52 | 
            +
                }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                setupRpcPeer(peer: RpcPeer): void {
         | 
| 55 | 
            +
                    const peerin = this.worker.stdio[3] as Writable;
         | 
| 56 | 
            +
                    const peerout = this.worker.stdio[this.fork ? 3 : 4] as Readable;
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    const serializer = this.serializer = createRpcDuplexSerializer(peerin);
         | 
| 59 | 
            +
                    serializer.setupRpcPeer(peer);
         | 
| 60 | 
            +
                    peerout.on('data', data => serializer.onData(data));
         | 
| 61 | 
            +
                    peerin.on('error', e => {
         | 
| 62 | 
            +
                        this.emit('error', e);
         | 
| 63 | 
            +
                        serializer.onDisconnected();
         | 
| 64 | 
            +
                    });
         | 
| 65 | 
            +
                    peerout.on('error', e => {
         | 
| 66 | 
            +
                        this.emit('error', e)
         | 
| 67 | 
            +
                        serializer.onDisconnected();
         | 
| 68 | 
            +
                    });
         | 
| 69 | 
            +
                }
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void {
         | 
| 72 | 
            +
                    try {
         | 
| 73 | 
            +
                        if (!this.worker)
         | 
| 74 | 
            +
                            throw new Error('python worker has been killed');
         | 
| 75 | 
            +
                        this.serializer.sendMessage(message, reject, serializationContext);
         | 
| 76 | 
            +
                    }
         | 
| 77 | 
            +
                    catch (e) {
         | 
| 78 | 
            +
                        reject?.(e);
         | 
| 79 | 
            +
                    }
         | 
| 80 | 
            +
                }
         | 
| 81 | 
            +
            }
         | 
| @@ -1,10 +1,8 @@ | |
| 1 | 
            +
            import v8 from 'v8';
         | 
| 2 | 
            +
            import worker_threads from "worker_threads";
         | 
| 1 3 | 
             
            import { EventEmitter } from "ws";
         | 
| 2 4 | 
             
            import { RpcMessage, RpcPeer } from "../../rpc";
         | 
| 3 5 | 
             
            import { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
         | 
| 4 | 
            -
            import worker_threads from "worker_threads";
         | 
| 5 | 
            -
            import path from 'path';
         | 
| 6 | 
            -
            import { getPluginNodePath } from "../plugin-npm-dependencies";
         | 
| 7 | 
            -
            import v8 from 'v8';
         | 
| 8 6 |  | 
| 9 7 | 
             
            export class NodeThreadWorker extends EventEmitter implements RuntimeWorker {
         | 
| 10 8 | 
             
                terminated: boolean;
         | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            import AdmZip from 'adm-zip';
         | 
| 2 | 
            +
            import fs from 'fs';
         | 
| 3 | 
            +
            import path from 'path';
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            function createAdmZipHash(hash: string) {
         | 
| 6 | 
            +
                const extractVersion = "1-";
         | 
| 7 | 
            +
                return extractVersion + hash;
         | 
| 8 | 
            +
            }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            function prep(pluginVolume: string, hash: string) {
         | 
| 11 | 
            +
                hash = createAdmZipHash(hash);
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                const zipFilename = `${hash}.zip`;
         | 
| 14 | 
            +
                const zipDir = path.join(pluginVolume, 'zip');
         | 
| 15 | 
            +
                const zipFile = path.join(zipDir, zipFilename);
         | 
| 16 | 
            +
                const unzippedPath = path.join(zipDir, 'unzipped')
         | 
| 17 | 
            +
                const zipDirTmp = zipDir + '.tmp';
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                return {
         | 
| 20 | 
            +
                    unzippedPath,
         | 
| 21 | 
            +
                    zipFilename,
         | 
| 22 | 
            +
                    zipDir,
         | 
| 23 | 
            +
                    zipFile,
         | 
| 24 | 
            +
                    zipDirTmp,
         | 
| 25 | 
            +
                };
         | 
| 26 | 
            +
            }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            export async function prepareZip(pluginVolume: string, h: string, getZip: () => Promise<Buffer>) {
         | 
| 29 | 
            +
                const { zipFile, unzippedPath } = prep(pluginVolume, h);
         | 
| 30 | 
            +
                if (fs.existsSync(zipFile)) {
         | 
| 31 | 
            +
                    return {
         | 
| 32 | 
            +
                        zipFile,
         | 
| 33 | 
            +
                        unzippedPath,
         | 
| 34 | 
            +
                    }
         | 
| 35 | 
            +
                }
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                const zipBuffer = await getZip();
         | 
| 38 | 
            +
                return extractZip(pluginVolume, h, zipBuffer);
         | 
| 39 | 
            +
            }
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            export function prepareZipSync(pluginVolume: string, h: string, getZip: () => Buffer) {
         | 
| 42 | 
            +
                const { zipFile, unzippedPath } = prep(pluginVolume, h);
         | 
| 43 | 
            +
                if (fs.existsSync(zipFile)) {
         | 
| 44 | 
            +
                    return {
         | 
| 45 | 
            +
                        zipFile,
         | 
| 46 | 
            +
                        unzippedPath,
         | 
| 47 | 
            +
                    }
         | 
| 48 | 
            +
                }
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                const zipBuffer = getZip();
         | 
| 51 | 
            +
                return extractZip(pluginVolume, h, zipBuffer);
         | 
| 52 | 
            +
            }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            export function extractZip(pluginVolume: string, h: string, zipBuffer: Buffer) {
         | 
| 55 | 
            +
                const { zipDir, zipDirTmp, zipFilename, zipFile, unzippedPath } = prep(pluginVolume, h);
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                fs.rmSync(zipDirTmp, {
         | 
| 58 | 
            +
                    recursive: true,
         | 
| 59 | 
            +
                    force: true,
         | 
| 60 | 
            +
                });
         | 
| 61 | 
            +
                fs.rmSync(zipDir, {
         | 
| 62 | 
            +
                    recursive: true,
         | 
| 63 | 
            +
                    force: true,
         | 
| 64 | 
            +
                });
         | 
| 65 | 
            +
                fs.mkdirSync(zipDirTmp, {
         | 
| 66 | 
            +
                    recursive: true,
         | 
| 67 | 
            +
                });
         | 
| 68 | 
            +
                fs.writeFileSync(path.join(zipDirTmp, zipFilename), zipBuffer);
         | 
| 69 | 
            +
                const admZip = new AdmZip(zipBuffer);
         | 
| 70 | 
            +
                admZip.extractAllTo(path.join(zipDirTmp, 'unzipped'), true);
         | 
| 71 | 
            +
                fs.renameSync(zipDirTmp, zipDir);
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                return {
         | 
| 74 | 
            +
                    zipFile,
         | 
| 75 | 
            +
                    unzippedPath,
         | 
| 76 | 
            +
                }
         | 
| 77 | 
            +
            }
         | 
| @@ -2,12 +2,12 @@ import child_process from 'child_process'; | |
| 2 2 | 
             
            import fs from "fs";
         | 
| 3 3 | 
             
            import os from "os";
         | 
| 4 4 | 
             
            import path from 'path';
         | 
| 5 | 
            +
            import type { PortablePython as PortablePythonType } from 'py';
         | 
| 5 6 | 
             
            import { Readable, Writable } from 'stream';
         | 
| 6 7 | 
             
            import { RpcMessage, RpcPeer } from "../../rpc";
         | 
| 7 8 | 
             
            import { createRpcDuplexSerializer } from '../../rpc-serializer';
         | 
| 8 9 | 
             
            import { ChildProcessWorker } from "./child-process-worker";
         | 
| 9 10 | 
             
            import { RuntimeWorkerOptions } from "./runtime-worker";
         | 
| 10 | 
            -
            import type {PortablePython as PortablePythonType} from 'py'
         | 
| 11 11 |  | 
| 12 12 | 
             
            export class PythonRuntimeWorker extends ChildProcessWorker {
         | 
| 13 13 | 
             
                static {
         | 
    
        package/src/runtime.ts
    CHANGED
    
    | @@ -9,7 +9,6 @@ import fs from 'fs'; | |
| 9 9 | 
             
            import http, { ServerResponse } from 'http';
         | 
| 10 10 | 
             
            import https from 'https';
         | 
| 11 11 | 
             
            import net from 'net';
         | 
| 12 | 
            -
            import type { spawn as ptySpawn } from '@homebridge/node-pty-prebuilt-multiarch';
         | 
| 13 12 | 
             
            import path from 'path';
         | 
| 14 13 | 
             
            import { ParsedQs } from 'qs';
         | 
| 15 14 | 
             
            import semver from 'semver';
         | 
| @@ -40,13 +39,14 @@ import { RuntimeWorker, RuntimeWorkerOptions } from './plugin/runtime/runtime-wo | |
| 40 39 | 
             
            import { getIpAddress, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
         | 
| 41 40 | 
             
            import { AddressSettings } from './services/addresses';
         | 
| 42 41 | 
             
            import { Alerts } from './services/alerts';
         | 
| 42 | 
            +
            import { Backup } from './services/backup';
         | 
| 43 43 | 
             
            import { CORSControl } from './services/cors';
         | 
| 44 44 | 
             
            import { Info } from './services/info';
         | 
| 45 45 | 
             
            import { getNpmPackageInfo, PluginComponent } from './services/plugin';
         | 
| 46 46 | 
             
            import { ServiceControl } from './services/service-control';
         | 
| 47 47 | 
             
            import { UsersService } from './services/users';
         | 
| 48 48 | 
             
            import { getState, ScryptedStateManager, setState } from './state';
         | 
| 49 | 
            -
            import {  | 
| 49 | 
            +
            import { CustomRuntimeWorker } from './plugin/runtime/custom-worker';
         | 
| 50 50 |  | 
| 51 51 | 
             
            interface DeviceProxyPair {
         | 
| 52 52 | 
             
                handler: PluginDeviceProxyHandler;
         | 
| @@ -61,7 +61,7 @@ interface HttpPluginData { | |
| 61 61 | 
             
                pluginDevice: PluginDevice
         | 
| 62 62 | 
             
            }
         | 
| 63 63 |  | 
| 64 | 
            -
            export type RuntimeHost = (mainFilename: string, pluginId: string, options: RuntimeWorkerOptions) => RuntimeWorker;
         | 
| 64 | 
            +
            export type RuntimeHost = (mainFilename: string, pluginId: string, options: RuntimeWorkerOptions, runtime: ScryptedRuntime) => RuntimeWorker;
         | 
| 65 65 |  | 
| 66 66 | 
             
            export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
         | 
| 67 67 | 
             
                clusterId = crypto.randomBytes(3).toString('hex');
         | 
| @@ -74,17 +74,6 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> { | |
| 74 74 | 
             
                devicesLogger = this.logger.getLogger('device', 'Devices');
         | 
| 75 75 | 
             
                wss = new WebSocketServer({ noServer: true });
         | 
| 76 76 | 
             
                wsAtomic = 0;
         | 
| 77 | 
            -
                shellio: IOServer = new io.Server({
         | 
| 78 | 
            -
                    pingTimeout: 120000,
         | 
| 79 | 
            -
                    perMessageDeflate: true,
         | 
| 80 | 
            -
                    cors: (req, callback) => {
         | 
| 81 | 
            -
                        const header = this.getAccessControlAllowOrigin(req.headers);
         | 
| 82 | 
            -
                        callback(undefined, {
         | 
| 83 | 
            -
                            origin: header,
         | 
| 84 | 
            -
                            credentials: true,
         | 
| 85 | 
            -
                        })
         | 
| 86 | 
            -
                    },
         | 
| 87 | 
            -
                });
         | 
| 88 77 | 
             
                connectRPCObjectIO: IOServer = new io.Server({
         | 
| 89 78 | 
             
                    pingTimeout: 120000,
         | 
| 90 79 | 
             
                    perMessageDeflate: true,
         | 
| @@ -111,6 +100,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> { | |
| 111 100 | 
             
                    // ensure that all the users are loaded from the db.
         | 
| 112 101 | 
             
                    this.usersService.getAllUsers();
         | 
| 113 102 |  | 
| 103 | 
            +
                    this.pluginHosts.set('custom', (_, pluginId, options, runtime) => new CustomRuntimeWorker(pluginId, options, runtime));
         | 
| 114 104 | 
             
                    this.pluginHosts.set('python', (_, pluginId, options) => new PythonRuntimeWorker(pluginId, options));
         | 
| 115 105 | 
             
                    this.pluginHosts.set('node', (mainFilename, pluginId, options) => new NodeForkWorker(mainFilename, pluginId, options));
         | 
| 116 106 |  | 
| @@ -118,46 +108,6 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> { | |
| 118 108 |  | 
| 119 109 | 
             
                    this.addMiddleware();
         | 
| 120 110 |  | 
| 121 | 
            -
                    app.all('/engine.io/shell', (req, res) => {
         | 
| 122 | 
            -
                        if (res.locals.aclId) {
         | 
| 123 | 
            -
                            res.writeHead(401);
         | 
| 124 | 
            -
                            res.end();
         | 
| 125 | 
            -
                            return;
         | 
| 126 | 
            -
                        }
         | 
| 127 | 
            -
                        this.shellHandler(req, res);
         | 
| 128 | 
            -
                    });
         | 
| 129 | 
            -
             | 
| 130 | 
            -
                    this.shellio.on('connection', connection => {
         | 
| 131 | 
            -
                        try {
         | 
| 132 | 
            -
                            const spawn = require('@homebridge/node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
         | 
| 133 | 
            -
                            const cp = spawn(process.env.SHELL, [], {
         | 
| 134 | 
            -
                            });
         | 
| 135 | 
            -
                            cp.onData(data => connection.send(data));
         | 
| 136 | 
            -
                            connection.on('message', message => {
         | 
| 137 | 
            -
                                if (Buffer.isBuffer(message)) {
         | 
| 138 | 
            -
                                    cp.write(message.toString());
         | 
| 139 | 
            -
                                    return;
         | 
| 140 | 
            -
                                }
         | 
| 141 | 
            -
             | 
| 142 | 
            -
                                try {
         | 
| 143 | 
            -
                                    const parsed = JSON.parse(message.toString());
         | 
| 144 | 
            -
                                    if (parsed.dim) {
         | 
| 145 | 
            -
                                        cp.resize(parsed.dim.cols, parsed.dim.rows);
         | 
| 146 | 
            -
                                    }
         | 
| 147 | 
            -
                                } catch (e) {
         | 
| 148 | 
            -
                                    // we should only get here if an outdated core plugin
         | 
| 149 | 
            -
                                    // is sending us string data instead of buffer data
         | 
| 150 | 
            -
                                    cp.write(message.toString());
         | 
| 151 | 
            -
                                }
         | 
| 152 | 
            -
                            });
         | 
| 153 | 
            -
                            connection.on('close', () => cp.kill());
         | 
| 154 | 
            -
                            cp.onExit(() => connection.close());
         | 
| 155 | 
            -
                        }
         | 
| 156 | 
            -
                        catch (e) {
         | 
| 157 | 
            -
                            connection.close();
         | 
| 158 | 
            -
                        }
         | 
| 159 | 
            -
                    });
         | 
| 160 | 
            -
             | 
| 161 111 | 
             
                    app.all('/engine.io/connectRPCObject', (req, res) => this.connectRPCObjectHandler(req, res));
         | 
| 162 112 |  | 
| 163 113 | 
             
                    /*
         | 
| @@ -305,33 +255,6 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> { | |
| 305 255 | 
             
                    };
         | 
| 306 256 | 
             
                }
         | 
| 307 257 |  | 
| 308 | 
            -
                async shellHandler(req: Request, res: Response) {
         | 
| 309 | 
            -
                    const isUpgrade = isConnectionUpgrade(req.headers);
         | 
| 310 | 
            -
             | 
| 311 | 
            -
                    const end = (code: number, message: string) => {
         | 
| 312 | 
            -
                        if (isUpgrade) {
         | 
| 313 | 
            -
                            const socket = res.socket;
         | 
| 314 | 
            -
                            socket.write(`HTTP/1.1 ${code} ${message}\r\n` +
         | 
| 315 | 
            -
                                '\r\n');
         | 
| 316 | 
            -
                            socket.destroy();
         | 
| 317 | 
            -
                        }
         | 
| 318 | 
            -
                        else {
         | 
| 319 | 
            -
                            res.status(code);
         | 
| 320 | 
            -
                            res.send(message);
         | 
| 321 | 
            -
                        }
         | 
| 322 | 
            -
                    };
         | 
| 323 | 
            -
             | 
| 324 | 
            -
                    if (!res.locals.username) {
         | 
| 325 | 
            -
                        end(401, 'Not Authorized');
         | 
| 326 | 
            -
                        return;
         | 
| 327 | 
            -
                    }
         | 
| 328 | 
            -
             | 
| 329 | 
            -
                    if ((req as any).upgradeHead)
         | 
| 330 | 
            -
                        this.shellio.handleUpgrade(req, res.socket, (req as any).upgradeHead)
         | 
| 331 | 
            -
                    else
         | 
| 332 | 
            -
                        this.shellio.handleRequest(req, res);
         | 
| 333 | 
            -
                }
         | 
| 334 | 
            -
             | 
| 335 258 | 
             
                async connectRPCObjectHandler(req: Request, res: Response) {
         | 
| 336 259 | 
             
                    const isUpgrade = isConnectionUpgrade(req.headers);
         | 
| 337 260 |  |