From 34a9e698ae31c71b2d0e29459cf0d81cdc3eebf8 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 2 Apr 2026 14:54:40 -0700 Subject: [PATCH] plugin: add type assertions for strictNullChecks in plugin-device and remote modules Fix strictNullChecks: - plugin-device.ts: consolidate entry/host assertions at declarations, use undefined! for proxy values, add definite assignment for mixinTable - plugin-remote.ts: add assertions for callbacks and nativeIds access - plugin-remote-worker.ts: fix clusterWorkerId as Promise, add assertions for worker and options properties --- server/src/plugin/plugin-device.ts | 32 +++++++++---------- server/src/plugin/plugin-remote-worker.ts | 38 ++++++++++++----------- server/src/plugin/plugin-remote.ts | 28 ++++++++--------- 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/server/src/plugin/plugin-device.ts b/server/src/plugin/plugin-device.ts index 0b6f7fcb7..5ea6d70bf 100644 --- a/server/src/plugin/plugin-device.ts +++ b/server/src/plugin/plugin-device.ts @@ -31,7 +31,7 @@ export const QueryInterfaceSymbol = Symbol("ScryptedPluginDeviceQueryInterface") export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { scrypted: ScryptedRuntime; id: string; - mixinTable: MixinTable[]; + mixinTable!: MixinTable[]; releasing = new Set(); static sortInterfaces(interfaces: string[]): string[] { @@ -65,8 +65,8 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { async getMixinProviderId(id: string, mixinDevice: any) { if (this.releasing.has(mixinDevice)) return true; - await this.scrypted.devices[id].handler.ensureProxy(); - for (const mixin of this.scrypted.devices[id].handler.mixinTable) { + await this.scrypted.devices[id]!.handler.ensureProxy(); + for (const mixin of this.scrypted.devices[id]!.handler.mixinTable) { const { proxy } = await mixin.entry; if (proxy === mixinDevice) { return mixin.mixinProviderId || id; @@ -78,7 +78,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { // should this be async? invalidate() { const mixinTable = this.mixinTable; - this.mixinTable = undefined; + this.mixinTable = undefined!; for (const mixinEntry of (mixinTable || [])) { this.invalidateEntry(mixinEntry); } @@ -102,7 +102,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { const mixins = getState(pluginDevice, ScryptedInterfaceProperty.mixins) || []; // iterate the new mixin table to find the last good mixin, // and resume creation from there. - let lastValidMixinId: string; + let lastValidMixinId: string | undefined; for (const mixinId of mixins) { if (!previousMixinIds.length) { // reached of the previous mixin table, meaning @@ -121,7 +121,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { // invalidate and remove everything up to lastValidMixinId while (true) { - const entry = this.mixinTable[0]; + const entry = this.mixinTable[0]!; if (entry.mixinProviderId === lastValidMixinId) break; this.mixinTable.shift(); @@ -149,7 +149,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { try { if (!pluginDevice.nativeId) { const plugin = this.scrypted.plugins[pluginDevice.pluginId]; - proxy = await plugin.module; + proxy = await plugin!.module; } else { const providerId = getState(pluginDevice, ScryptedInterfaceProperty.providerId); @@ -174,7 +174,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { })(); this.mixinTable.unshift({ - mixinProviderId: undefined, + mixinProviderId: undefined!, entry, }); } @@ -202,7 +202,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { }); } - return this.mixinTable[0].entry.then(entry => { + return this.mixinTable[0]!.entry.then(entry => { if (entry.error) { console.error('Mixin device creation completed with error. Merging with previous interface set to retain device descriptor.'); const previousInterfaces = getState(pluginDevice, ScryptedInterfaceProperty.interfaces) as string[] || []; @@ -219,7 +219,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { async rebuildEntry(pluginDevice: PluginDevice, mixinId: string, wrappedMixinTablePromise: Promise): Promise { const wrappedMixinTable = await wrappedMixinTablePromise; - const previousEntry = wrappedMixinTable[0].entry; + const previousEntry = wrappedMixinTable[0]!.entry; const type = getDisplayType(pluginDevice); @@ -255,7 +255,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { passthrough: true, allInterfaces, interfaces: new Set(), - proxy: undefined as any, + proxy: undefined!, }; } @@ -269,7 +269,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { const wrappedProxy = new Proxy(wrappedHandler, wrappedHandler); const implementer = await (mixinProvider as any)[QueryInterfaceSymbol](ScryptedInterface.MixinProvider); - const host = this.scrypted.getPluginHostForDeviceId(implementer); + const host = this.scrypted.getPluginHostForDeviceId(implementer)!; const propertyInterfaces = getPropertyInterfaces(host.api.descriptors || ScryptedInterfaceDescriptors); // todo: remove this and pass the setter directly. const deviceState = await host.remote.createDeviceState(this.id, @@ -304,8 +304,8 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { passthrough: false, allInterfaces, interfaces: new Set(), - error: e, - proxy: undefined as any, + error: e as Error, + proxy: undefined!, }; } } @@ -328,7 +328,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { if (p === RefreshSymbol || p === QueryInterfaceSymbol) return new Proxy(() => p, this); - if (ScryptedInterfaceDescriptors[ScryptedInterface.ScryptedDevice].methods.includes(prop)) + if (ScryptedInterfaceDescriptors[ScryptedInterface.ScryptedDevice]!.methods.includes(prop)) return (this as any)[p].bind(this); return new Proxy(() => prop, this); @@ -445,7 +445,7 @@ export class PluginDeviceProxyHandler implements PrimitiveProxyHandler { if (method === 'getReadmeMarkdown') { const pluginDevice = this.scrypted.findPluginDeviceById(this.id); if (pluginDevice && !pluginDevice.nativeId) { - const plugin = this.scrypted.plugins[pluginDevice.pluginId]; + const plugin = this.scrypted.plugins[pluginDevice.pluginId]!; if (!plugin.packageJson.scrypted.interfaces.includes(ScryptedInterface.Readme)) { const readmePath = path.join(plugin.unzippedPath, 'README.md'); if (fs.existsSync(readmePath)) { diff --git a/server/src/plugin/plugin-remote-worker.ts b/server/src/plugin/plugin-remote-worker.ts index 24e6c7118..be702d024 100644 --- a/server/src/plugin/plugin-remote-worker.ts +++ b/server/src/plugin/plugin-remote-worker.ts @@ -85,7 +85,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe [RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = (_api as any)[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS]; override setStorage(nativeId: string, storage: { [key: string]: any; }): Promise { - const id = deviceManager.nativeIds.get(nativeId).id; + const id = deviceManager.nativeIds.get(nativeId)!.id; for (const r of forks) { r.setNativeId(nativeId, id, storage); } @@ -104,7 +104,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe if (name === 'repl') { if (!replPort) throw new Error('REPL unavailable: Plugin not loaded.') - return [await replPort, process.env.SCRYPTED_CLUSTER_ADDRESS]; + return [await replPort, process.env.SCRYPTED_CLUSTER_ADDRESS!] as [number, string]; } throw new Error(`unknown service ${name}`); }, @@ -183,11 +183,11 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe process.on('uncaughtException', e => { getPluginConsole().error('uncaughtException', e); - scrypted.log.e('uncaughtException ' + (e.stack || e?.toString())); + scrypted.log!.e('uncaughtException ' + (e.stack || e?.toString())); }); process.on('unhandledRejection', e => { getPluginConsole().error('unhandledRejection', e); - scrypted.log.e('unhandledRejection ' + ((e as Error).stack || e?.toString())); + scrypted.log!.e('unhandledRejection ' + ((e as Error).stack || e?.toString())); }); installSourceMapSupport({ @@ -209,10 +209,10 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe await installOptionalDependencies(getPluginConsole(), packageJson); const main = await pluginReader(mainNodejs); - const script = main.toString(); + const script = main!.toString(); scrypted.connect = (socket, options) => { - process.send(options, socket); + process.send!(options, socket); } const pluginRemoteAPI: PluginRemote = scrypted.pluginRemoteAPI; @@ -221,12 +221,12 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe let forkPeer: Promise; let runtimeWorker: RuntimeWorker; let nativeWorker: child_process.ChildProcess | worker_threads.Worker; - let clusterWorkerId: Promise; + let clusterWorkerId: Promise | undefined; const runtimeWorkerOptions: RuntimeWorkerOptions = { packageJson, env: undefined, - pluginDebug: undefined, + pluginDebug: undefined!, zipFile, unzippedPath, zipHash, @@ -234,13 +234,15 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe // if running in a cluster, fork to a matching cluster worker only if necessary. if (utilizesClusterForkWorker(options)) { - ({ runtimeWorker, forkPeer, clusterWorkerId } = createClusterForkWorker( + const result = createClusterForkWorker( runtimeWorkerOptions, - options, + options!, api.getComponent('cluster-fork'), () => zipAPI.getZip(), - scrypted.connectRPCObject) - ); + scrypted.connectRPCObject); + runtimeWorker = result.runtimeWorker; + forkPeer = result.forkPeer; + clusterWorkerId = result.clusterWorkerId as Promise; } else { if (options?.runtime) { @@ -248,10 +250,10 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe const runtime = builtins.get(options.runtime); if (!runtime) throw new Error('unknown runtime ' + options.runtime); - runtimeWorker = runtime(mainFilename, runtimeWorkerOptions, undefined); + runtimeWorker = runtime(mainFilename, runtimeWorkerOptions, undefined!); if (runtimeWorker instanceof ChildProcessWorker) { - nativeWorker = runtimeWorker.childProcess; + nativeWorker = runtimeWorker.childProcess!; } } else { @@ -261,7 +263,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe const ntw = new NodeThreadWorker(mainFilename, pluginId, { packageJson, env: undefined, - pluginDebug: undefined, + pluginDebug: undefined!, zipFile, unzippedPath, zipHash, @@ -317,7 +319,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe [RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS] = (api as any)[RpcPeer.PROPERTY_PROXY_ONEWAY_METHODS]; override setStorage(nativeId: string, storage: { [key: string]: any; }): Promise { - const id = deviceManager.nativeIds.get(nativeId).id; + const id = deviceManager.nativeIds.get(nativeId)!.id; pluginRemoteAPI.setNativeId(nativeId, id, storage); for (const r of forks) { if (r === remote) @@ -361,7 +363,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe removeListener(event, listener) { return runtimeWorker.removeListener(event as any, listener); }, - nativeWorker, + nativeWorker: nativeWorker!, }; return { [Symbol.dispose]() { @@ -376,7 +378,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe try { const isModule = packageJson.type === 'module'; const filename = zipOptions?.debug ? pluginMainNodeJs : pluginIdMainNodeJs; - const sdkVersion = await pluginReader('sdk.json').then(b => JSON.parse(b.toString()).version).catch(() => { }); + const sdkVersion = await pluginReader('sdk.json').then(b => JSON.parse(b!.toString()).version).catch(() => { }); const mainNodeJsOnFilesystem = path.join(unzippedPath, mainNodejs); if (sdkVersion) { // todo: remove this, only existed in prerelease versions diff --git a/server/src/plugin/plugin-remote.ts b/server/src/plugin/plugin-remote.ts index 11d8d3fc4..cc26ddf63 100644 --- a/server/src/plugin/plugin-remote.ts +++ b/server/src/plugin/plugin-remote.ts @@ -51,7 +51,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId: delete state[id]; continue; } - state[id] = getAccessControlDeviceState(id, state[id]); + state[id] = getAccessControlDeviceState(id, state[id]!)!; } } @@ -67,11 +67,11 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId: if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice) { if (eventDetails.property === ScryptedInterfaceProperty.id) { // a change on the id property means device was deleted - remote.updateDeviceState(eventData, undefined); + remote.updateDeviceState(eventData, undefined!); } else { // a change on anything else is a descriptor update - remote.updateDeviceState(id, getAccessControlDeviceState(id)); + remote.updateDeviceState(id, getAccessControlDeviceState(id)!); } return; } @@ -127,7 +127,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp ioSockets[id] = callbacks; - callbacks.connect(undefined, { + callbacks.connect(undefined!, { close: (message) => connection.close(message), send: (message) => connection.send(message), }); @@ -140,14 +140,14 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp api = await options?.onGetRemote?.(api, pluginId) || api; const systemManager = new SystemManagerImpl(); - const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole, getMixinConsole); + const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole!, getMixinConsole!); const endpointManager = new EndpointManagerImpl(); - const clusterManager = new ClusterManagerImpl(undefined, api, undefined); + const clusterManager = new ClusterManagerImpl(undefined, api, undefined!); const hostMediaManager = await api.getMediaManager(); if (!hostMediaManager) { - peer.params['createMediaManager'] = async () => createMediaManager(systemManager, deviceManager); + peer.params['createMediaManager'] = async () => createMediaManager!(systemManager, deviceManager); } - const mediaManager = hostMediaManager || await createMediaManager(systemManager, deviceManager); + const mediaManager = hostMediaManager || await createMediaManager!(systemManager, deviceManager); peer.params['mediaManager'] = mediaManager; systemManager.api = api; @@ -163,11 +163,11 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp clusterManager, log, pluginHostAPI: api, - pluginRemoteAPI: undefined, + pluginRemoteAPI: undefined!, serverVersion: hostInfo?.serverVersion, - connect: undefined, - fork: undefined, - connectRPCObject: undefined, + connect: undefined!, + fork: undefined!, + connectRPCObject: undefined!, }; delete peer.params.getRemote; @@ -188,7 +188,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp 'ioEvent', 'setNativeId', ], - getServicePort, + getServicePort: getServicePort!, async createDeviceState(id: string, setState: (property: string, value: any) => Promise) { return deviceManager.createDeviceState(id, setState); }, @@ -300,7 +300,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp params.pluginRuntimeAPI = ret; try { - return await options.onLoadZip(ret, params, packageJson, zipAPI, zipOptions); + return await options!.onLoadZip!(ret, params, packageJson, zipAPI, zipOptions!); } catch (e) { console.error('plugin start/fork failed', e)