server: refactor runtime worker creation

This commit is contained in:
Koushik Dutta
2024-11-20 14:53:58 -08:00
parent 02a46a9202
commit 53cab91b02
19 changed files with 127 additions and 103 deletions

View File

@@ -25,7 +25,7 @@ import { WebSocketConnection } from './plugin-remote-websocket';
import { ensurePluginVolume, getScryptedVolume } from './plugin-volume';
import { createClusterForkWorker } from './runtime/cluster-fork-worker';
import { prepareZipSync } from './runtime/node-worker-common';
import { RuntimeWorker } from './runtime/runtime-worker';
import type { RuntimeWorker, RuntimeWorkerOptions } from './runtime/runtime-worker';
const serverVersion = require('../../package.json').version;
@@ -341,7 +341,15 @@ export class PluginHost {
if (!workerHost)
throw new UnsupportedRuntimeError(this.packageJson.scrypted.runtime);
let peer: Promise<RpcPeer>
let peer: Promise<RpcPeer>;
const runtimeWorkerOptions: RuntimeWorkerOptions = {
packageJson: this.packageJson,
env,
pluginDebug,
unzippedPath: this.unzippedPath,
zipFile: this.zipFile,
zipHash: this.zipHash,
};
if (!needsClusterForkWorker(this.packageJson.scrypted)) {
this.peer = new RpcPeer('host', this.pluginId, (message, reject, serializationContext) => {
if (connected) {
@@ -354,14 +362,7 @@ export class PluginHost {
peer = Promise.resolve(this.peer);
this.worker = workerHost(this.scrypted.mainFilename, this.pluginId, {
packageJson: this.packageJson,
env,
pluginDebug,
unzippedPath: this.unzippedPath,
zipFile: this.zipFile,
zipHash: this.zipHash,
}, this.scrypted);
this.worker = workerHost(this.scrypted.mainFilename, runtimeWorkerOptions, this.scrypted);
this.worker.setupRpcPeer(this.peer);
@@ -379,25 +380,28 @@ export class PluginHost {
});
const clusterSetup = setupCluster(this.peer);
const { runtimeWorker, forkPeer, clusterWorkerId } = createClusterForkWorker((async () => {
await clusterSetup.initializeCluster({
clusterId: this.scrypted.clusterId,
clusterSecret: this.scrypted.clusterSecret,
});
return this.scrypted.clusterFork;
})(),
this.zipHash, async () => fs.promises.readFile(this.zipFile),
this.packageJson.scrypted, this.packageJson, clusterSetup.connectRPCObject);
const { runtimeWorker, forkPeer, clusterWorkerId } = createClusterForkWorker(
runtimeWorkerOptions,
this.packageJson.scrypted,
(async () => {
await clusterSetup.initializeCluster({
clusterId: this.scrypted.clusterId,
clusterSecret: this.scrypted.clusterSecret,
});
return this.scrypted.clusterFork;
})(),
async () => fs.promises.readFile(this.zipFile),
clusterSetup.connectRPCObject);
forkPeer.then(peer => {
const originalPeer = this.peer;
originalPeer.killedSafe.finally(() => peer.kill());
this.peer = peer;
peer.killedSafe.finally(() => originalPeer.kill());
}).catch(() => {});
}).catch(() => { });
clusterWorkerId.then(clusterWorkerId => {
console.log('cluster worker id', clusterWorkerId);
}).catch(() => {});
}).catch(() => { });
this.worker = runtimeWorker;
peer = forkPeer;

View File

@@ -21,7 +21,7 @@ import { createClusterForkWorker } from './runtime/cluster-fork-worker';
import { NodeThreadWorker } from './runtime/node-thread-worker';
import { prepareZip } from './runtime/node-worker-common';
import { getBuiltinRuntimeHosts } from './runtime/runtime-host';
import { RuntimeWorker } from './runtime/runtime-worker';
import { RuntimeWorker, RuntimeWorkerOptions } from './runtime/runtime-worker';
import type { ClusterForkService } from '../services/cluster-fork';
import type { PluginComponent } from '../services/plugin';
import { ClusterManagerImpl } from '../scrypted-cluster-main';
@@ -216,10 +216,23 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
let nativeWorker: child_process.ChildProcess | worker_threads.Worker;
let clusterWorkerId: Promise<string>;
const runtimeWorkerOptions: RuntimeWorkerOptions = {
packageJson,
env: undefined,
pluginDebug: undefined,
zipFile,
unzippedPath,
zipHash,
};
// if running in a cluster, fork to a matching cluster worker only if necessary.
if (needsClusterForkWorker(options)) {
({ runtimeWorker, forkPeer, clusterWorkerId } = createClusterForkWorker(
api.getComponent('cluster-fork'), zipHash, () => zipAPI.getZip(), options, packageJson, scrypted.connectRPCObject)
runtimeWorkerOptions,
options,
api.getComponent('cluster-fork'),
() => zipAPI.getZip(),
scrypted.connectRPCObject)
);
}
else {
@@ -228,14 +241,7 @@ 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, pluginId, {
packageJson,
env: undefined,
pluginDebug: undefined,
zipFile,
unzippedPath,
zipHash,
}, undefined);
runtimeWorker = runtime(mainFilename, runtimeWorkerOptions, undefined);
if (runtimeWorker instanceof ChildProcessWorker) {
nativeWorker = runtimeWorker.childProcess;

View File

@@ -4,14 +4,16 @@ import { RpcMessage, RpcPeer } from "../../rpc";
import { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
export abstract class ChildProcessWorker extends EventEmitter implements RuntimeWorker {
public pluginId: string;
protected worker: child_process.ChildProcess;
get childProcess() {
return this.worker;
}
constructor(public pluginId: string, options: RuntimeWorkerOptions) {
constructor(options: RuntimeWorkerOptions) {
super();
this.pluginId = options.packageJson.name;
}
setupWorker() {

View File

@@ -5,15 +5,20 @@ import { RpcPeer } from "../../rpc";
import { PeerLiveness } from "../../scrypted-cluster-main";
import type { ClusterForkService } from "../../services/cluster-fork";
import { writeWorkerGenerator } from "../plugin-console";
import type { RuntimeWorker } from "./runtime-worker";
import type { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
export function createClusterForkWorker(
forkComponentPromise: Promise<ClusterForkService>,
zipHash: string,
getZip: () => Promise<Buffer>,
runtimeWorkerOptions: RuntimeWorkerOptions,
options: Partial<ClusterFork>,
packageJson: any,
forkComponentPromise: Promise<ClusterForkService>,
getZip: () => Promise<Buffer>,
connectRPCObject: (o: any) => Promise<any>) {
// these are specific to the cluster worker host
// and will be set there.
delete runtimeWorkerOptions.zipFile;
delete runtimeWorkerOptions.unzippedPath;
const waitKilled = new Deferred<void>();
waitKilled.promise.finally(() => events.emit('exit'));
const events = new EventEmitter();
@@ -38,21 +43,22 @@ export function createClusterForkWorker(
});
const peerLiveness = new PeerLiveness(waitKilled.promise);
const clusterForkResultPromise = forkComponentPromise.then(forkComponent => forkComponent.fork(peerLiveness, {
const clusterForkResultPromise = forkComponentPromise.then(forkComponent => forkComponent.fork(runtimeWorkerOptions, {
runtime: options.runtime || 'node',
id: options.id,
...options,
}, packageJson, zipHash, getZip));
clusterForkResultPromise.catch(() => {});
}, peerLiveness,
getZip));
clusterForkResultPromise.catch(() => { });
const clusterWorkerId = clusterForkResultPromise.then(clusterForkResult => clusterForkResult.clusterWorkerId);
clusterWorkerId.catch(() => {});
clusterWorkerId.catch(() => { });
const forkPeer = (async () => {
const clusterForkResult = await clusterForkResultPromise;
waitKilled.promise.finally(() => {
runtimeWorker.pid = undefined;
clusterForkResult.kill().catch(() => {});
clusterForkResult.kill().catch(() => { });
});
clusterForkResult.waitKilled().catch(() => { })
.finally(() => {

View File

@@ -11,8 +11,8 @@ export class CustomRuntimeWorker extends ChildProcessWorker {
serializer: ReturnType<typeof createRpcDuplexSerializer>;
fork: boolean;
constructor(pluginId: string, options: RuntimeWorkerOptions, runtime: ScryptedRuntime) {
super(pluginId, options);
constructor(options: RuntimeWorkerOptions, runtime: ScryptedRuntime) {
super(options);
const pluginDevice = runtime.findPluginDevice(this.pluginId);
const scryptedRuntimeArguments: ScryptedRuntimeArguments = pluginDevice.state.scryptedRuntimeArguments?.value;
@@ -27,7 +27,7 @@ export class CustomRuntimeWorker extends ChildProcessWorker {
// stdin, stdout, stderr, peer in, peer out
stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
env: Object.assign({}, process.env, env, {
SCRYYPTED_PLUGIN_ID: pluginId,
SCRYYPTED_PLUGIN_ID: this.pluginId,
SCRYPTED_DEBUG_PORT: pluginDebug?.inspectPort?.toString(),
SCRYPTED_UNZIPPED_PATH: options.unzippedPath,
SCRYPTED_ZIP_FILE: options.zipFile,

View File

@@ -28,8 +28,8 @@ export function isNodePluginChildProcess() {
export class NodeForkWorker extends ChildProcessWorker {
constructor(mainFilename: string, pluginId: string, options: RuntimeWorkerOptions) {
super(pluginId, options);
constructor(mainFilename: string, options: RuntimeWorkerOptions) {
super(options);
const { env, pluginDebug } = options;

View File

@@ -41,8 +41,8 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
return this._stderr;
}
constructor(pluginId: string, options: RuntimeWorkerOptions) {
super(pluginId, options);
constructor(options: RuntimeWorkerOptions) {
super(options);
const { env, pluginDebug } = options;
const args: string[] = [
@@ -148,7 +148,7 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
};
const pyVersion = require('py/package.json').version;
const pyPath = path.join(getPluginVolume(pluginId), 'py');
const pyPath = path.join(getPluginVolume(this.pluginId), 'py');
const portableInstallPath = path.join(pyPath, pyVersion);
const py = new PortablePython(pluginPythonVersion, portableInstallPath, portablePythonOptions);

View File

@@ -4,14 +4,14 @@ import { NodeForkWorker } from "./node-fork-worker";
import { PythonRuntimeWorker } from "./python-worker";
import type { RuntimeWorker, RuntimeWorkerOptions } from "./runtime-worker";
export type RuntimeHost = (mainFilename: string, pluginId: string, options: RuntimeWorkerOptions, runtime: ScryptedRuntime) => RuntimeWorker;
export type RuntimeHost = (mainFilename: string, options: RuntimeWorkerOptions, runtime: ScryptedRuntime) => RuntimeWorker;
export function getBuiltinRuntimeHosts() {
const pluginHosts = new Map<string, RuntimeHost>();
pluginHosts.set('custom', (_, pluginId, options, runtime) => new CustomRuntimeWorker(pluginId, options, runtime));
pluginHosts.set('python', (_, pluginId, options) => new PythonRuntimeWorker(pluginId, options));
pluginHosts.set('node', (mainFilename, pluginId, options) => new NodeForkWorker(mainFilename, pluginId, options));
pluginHosts.set('custom', (_, options, runtime) => new CustomRuntimeWorker(options, runtime));
pluginHosts.set('python', (_, options) => new PythonRuntimeWorker(options));
pluginHosts.set('node', (mainFilename, options) => new NodeForkWorker(mainFilename, options));
return pluginHosts;
}

View File

@@ -15,7 +15,7 @@ import type { PluginAPI } from './plugin/plugin-api';
import { getPluginVolume, getScryptedVolume } from './plugin/plugin-volume';
import { prepareZip } from './plugin/runtime/node-worker-common';
import { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
import type { RuntimeWorker } from './plugin/runtime/runtime-worker';
import type { RuntimeWorker, RuntimeWorkerOptions } from './plugin/runtime/runtime-worker';
import { RpcPeer } from './rpc';
import { createRpcDuplexSerializer } from './rpc-serializer';
import type { ScryptedRuntime } from './runtime';
@@ -103,7 +103,7 @@ export class ClusterForkResult extends PeerLiveness {
}
}
export type ClusterForkParam = (peerLiveness: PeerLiveness, runtime: string, packageJson: any, zipHash: string, getZip: () => Promise<Buffer>) => Promise<ClusterForkResult>;
export type ClusterForkParam = (runtime: string, options: RuntimeWorkerOptions, peerLiveness: PeerLiveness, getZip: () => Promise<Buffer>) => Promise<ClusterForkResult>;
export function startClusterClient(mainFilename: string) {
const originalClusterAddress = process.env.SCRYPTED_CLUSTER_ADDRESS;
@@ -179,12 +179,7 @@ export function startClusterClient(mainFilename: string) {
const clusterPeerSetup = setupCluster(peer);
await clusterPeerSetup.initializeCluster({ clusterId, clusterSecret });
const clusterForkParam: ClusterForkParam = async (
peerLiveness: PeerLiveness,
runtime: string,
packageJson: any,
zipHash: string,
getZip: () => Promise<Buffer>) => {
const clusterForkParam: ClusterForkParam = async (runtime, runtimeWorkerOptions, peerLiveness, getZip) => {
let runtimeWorker: RuntimeWorker;
const builtins = getBuiltinRuntimeHosts();
@@ -192,23 +187,22 @@ export function startClusterClient(mainFilename: string) {
if (!rt)
throw new Error('unknown runtime ' + runtime);
const pluginId: string = packageJson.name;
const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), zipHash, getZip);
const pluginId: string = runtimeWorkerOptions.packageJson.name;
const { zipFile, unzippedPath } = await prepareZip(getPluginVolume(pluginId), runtimeWorkerOptions.zipHash, getZip);
const volume = getScryptedVolume();
const pluginVolume = getPluginVolume(pluginId);
runtimeWorker = rt(mainFilename, pluginId, {
packageJson,
env: {
SCRYPTED_VOLUME: volume,
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
},
pluginDebug: undefined,
zipFile,
unzippedPath,
zipHash,
}, undefined);
runtimeWorkerOptions.zipFile = zipFile;
runtimeWorkerOptions.unzippedPath = unzippedPath;
runtimeWorkerOptions.env = {
...runtimeWorkerOptions.env,
SCRYPTED_VOLUME: volume,
SCRYPTED_PLUGIN_VOLUME: pluginVolume,
};
runtimeWorker = rt(mainFilename, runtimeWorkerOptions, undefined);
runtimeWorker.stdout.on('data', data => console.log(data.toString()));
runtimeWorker.stderr.on('data', data => console.error(data.toString()));

View File

@@ -1,11 +1,13 @@
import { ClusterWorker } from "@scrypted/types";
import { matchesClusterLabels } from "../cluster/cluster-labels";
import type { ScryptedRuntime } from "../runtime";
import type { ClusterForkOptions, ClusterForkParam, PeerLiveness, RunningClusterWorker } from "../scrypted-cluster-main";
import type { RuntimeWorkerOptions } from "../plugin/runtime/runtime-worker";
export class ClusterForkService {
constructor(public runtime: ScryptedRuntime) { }
async fork(peerLiveness: PeerLiveness, options: ClusterForkOptions, packageJson: any, zipHash: string, getZip: () => Promise<Buffer>) {
async fork(runtimeWorkerOptions: RuntimeWorkerOptions, options: ClusterForkOptions, peerLiveness: PeerLiveness, getZip: () => Promise<Buffer>) {
const matchingWorkers = [...this.runtime.clusterWorkers.entries()].map(([id, worker]) => ({
worker,
matches: matchesClusterLabels(options, worker.labels),
@@ -34,8 +36,8 @@ export class ClusterForkService {
}
const fork: ClusterForkParam = await worker.peer.getParam('fork');
const forkResult = await fork(peerLiveness, options.runtime, packageJson, zipHash, getZip);
options.id ||= this.runtime.findPluginDevice(packageJson.name)?._id;
const forkResult = await fork(options.runtime, runtimeWorkerOptions, peerLiveness, getZip);
options.id ||= this.runtime.findPluginDevice(runtimeWorkerOptions.packageJson.name)?._id;
worker.forks.add(options);
forkResult.waitKilled().catch(() => { }).finally(() => {
worker.forks.delete(options);
@@ -43,7 +45,7 @@ export class ClusterForkService {
forkResult.clusterWorkerId = worker.id;
return forkResult;
}
};
async getClusterWorkers() {
const ret: any = {};