310 lines
8.5 KiB
TypeScript
310 lines
8.5 KiB
TypeScript
import crypto2 from 'crypto';
|
|
import { spawn } from 'child_process';
|
|
import CDP from 'chrome-remote-interface';
|
|
import {} from 'child_process';
|
|
import net from 'net';
|
|
import { GBLog } from 'botlib';
|
|
import { CollectionUtil } from 'pragmatismo-io-framework';
|
|
import { GBServer } from '../../../../src/app.js';
|
|
import { DebuggerService } from '../DebuggerService.js';
|
|
import finalStream from 'final-stream';
|
|
import { GBLogEx } from '../../../core.gbapp/services/GBLogEx.js';
|
|
|
|
const waitUntil = condition => {
|
|
if (condition()) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
const interval = setInterval(() => {
|
|
if (!condition()) {
|
|
return;
|
|
}
|
|
|
|
clearInterval(interval);
|
|
resolve(0);
|
|
}, 0);
|
|
});
|
|
};
|
|
|
|
const systemVariables = [
|
|
'AggregateError',
|
|
'Array',
|
|
'ArrayBuffer',
|
|
'Atomics',
|
|
'BigInt',
|
|
'BigInt64Array',
|
|
'BigUint64Array',
|
|
'Boolean',
|
|
'DataView',
|
|
'Date',
|
|
'Error',
|
|
'EvalError',
|
|
'FinalizationRegistry',
|
|
'Float32Array',
|
|
'Float64Array',
|
|
'Function',
|
|
'Headers',
|
|
'Infinity',
|
|
'Int16Array',
|
|
'Int32Array',
|
|
'Int8Array',
|
|
'Intl',
|
|
'JSON',
|
|
'Map',
|
|
'Math',
|
|
'NaN',
|
|
'Number',
|
|
'Object',
|
|
'Promise',
|
|
'Proxy',
|
|
'RangeError',
|
|
'ReferenceError',
|
|
'Reflect',
|
|
'RegExp',
|
|
'Request',
|
|
'Response',
|
|
'Set',
|
|
'SharedArrayBuffer',
|
|
'String',
|
|
'Symbol',
|
|
'SyntaxError',
|
|
'TypeError',
|
|
'URIError',
|
|
'Uint16Array',
|
|
'Uint32Array',
|
|
'Uint8Array',
|
|
'Uint8ClampedArray',
|
|
'VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL',
|
|
'WeakMap',
|
|
'WeakRef',
|
|
'WeakSet',
|
|
'WebAssembly',
|
|
'__defineGetter__',
|
|
'__defineSetter__',
|
|
'__lookupGetter__',
|
|
'__lookupSetter__',
|
|
'__proto__',
|
|
'clearImmediate',
|
|
'clearInterval',
|
|
'clearTimeout',
|
|
'console',
|
|
'constructor',
|
|
'decodeURI',
|
|
'decodeURIComponent',
|
|
'dss',
|
|
'encodeURI',
|
|
'encodeURIComponent',
|
|
'escape',
|
|
'eval',
|
|
'fetch',
|
|
'global',
|
|
'globalThis',
|
|
'hasOwnProperty',
|
|
'isFinite',
|
|
'isNaN',
|
|
'isPrototypeOf',
|
|
'parseFloat',
|
|
'parseInt',
|
|
'process',
|
|
'propertyIsEnumerable',
|
|
'setImmediate',
|
|
'setInterval',
|
|
'setTimeout',
|
|
'toLocaleString',
|
|
'toString',
|
|
'undefined',
|
|
'unescape',
|
|
'valueOf'
|
|
];
|
|
|
|
export const createVm2Pool = ({ min, max, ...limits }) => {
|
|
limits = Object.assign(
|
|
{
|
|
cpu: 100,
|
|
memory: 2000,
|
|
time: 4000
|
|
},
|
|
limits
|
|
);
|
|
|
|
let limitError = null;
|
|
|
|
const ref = crypto2.randomBytes(20).toString('hex');
|
|
|
|
const kill = x => {
|
|
spawn('sh', ['-c', `pkill -9 -f ${ref}`]);
|
|
};
|
|
|
|
let stderrCache = '';
|
|
|
|
const run = async (code: any, scope: any) => {
|
|
// Configure environment variables
|
|
const env = Object.assign({}, process.env, {
|
|
NODE_ENV: 'production',
|
|
NODE_OPTIONS: '' // Clear NODE_OPTIONS if needed
|
|
});
|
|
|
|
const childProcess = spawn(
|
|
'/usr/bin/cpulimit',
|
|
[
|
|
'-ql',
|
|
limits.cpu,
|
|
'--',
|
|
'node',
|
|
`${limits.debug ? '--inspect=' + limits.debuggerPort : ''}`,
|
|
`--experimental-fetch`,
|
|
`--max-old-space-size=${limits.memory}`,
|
|
limits.script,
|
|
ref
|
|
],
|
|
{ cwd: limits.cwd, shell: true, env: env }
|
|
);
|
|
|
|
childProcess.stdout.on('data', data => {
|
|
childProcess['socket'] = childProcess['socket'] || data.toString().trim();
|
|
});
|
|
|
|
childProcess.stderr.on('data', data => {
|
|
stderrCache = stderrCache + data.toString();
|
|
if (stderrCache.includes('failed: address already in use')) {
|
|
limitError = stderrCache;
|
|
kill(process);
|
|
GBServer.globals.debuggers[limits.botId].state = 0;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = stderrCache;
|
|
} else if (
|
|
stderrCache.includes('FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory')
|
|
) {
|
|
limitError = 'code execution exceeed allowed memory';
|
|
kill(process);
|
|
GBServer.globals.debuggers[limits.botId].state = 0;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = 'Fail';
|
|
} else if (stderrCache.includes('Debugger attached.')) {
|
|
GBLogEx.info(min, `General Bots Debugger attached to Node .gbdialog process for ${limits.botId}.`);
|
|
}
|
|
});
|
|
|
|
let socket = null;
|
|
await waitUntil(() => childProcess['socket']);
|
|
|
|
GBServer.globals.debuggers[limits.botId].childProcess = ref;
|
|
|
|
// Only attach if called by debugger/run.
|
|
|
|
if (limits.debug) {
|
|
const debug = async () => {
|
|
return new Promise((resolve, reject) => {
|
|
CDP(async client => {
|
|
const { Debugger, Runtime } = client;
|
|
try {
|
|
GBServer.globals.debuggers[limits.botId].client = client;
|
|
|
|
await client.Debugger.paused(async ({ callFrames, reason, hitBreakpoints }) => {
|
|
const frame = callFrames[0];
|
|
|
|
// Build variable list ignoring system variables of script.
|
|
|
|
const scopeObjectId = frame.scopeChain[2].object.objectId;
|
|
const variables = await Runtime.getProperties({ objectId: scopeObjectId });
|
|
let variablesText = '';
|
|
if (variables && variables.result) {
|
|
await CollectionUtil.asyncForEach(variables.result, async v => {
|
|
if (!systemVariables.filter(x => x === v.name)[0]) {
|
|
if (v.value.value) {
|
|
variablesText = `${variablesText} \n ${v.name}: ${v.value.value}`;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
GBServer.globals.debuggers[limits.botId].scope = variablesText;
|
|
GBLogEx.info(min, `Breakpoint variables: ${variablesText}`); // (zero-based)
|
|
// Processes breakpoint hits.
|
|
|
|
if (hitBreakpoints.length >= 1) {
|
|
GBLogEx.info(min, `Break at line ${frame.location.lineNumber + 1}`); // (zero-based)
|
|
|
|
GBServer.globals.debuggers[limits.botId].state = 2;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = 'Break';
|
|
} else {
|
|
GBLog.verbose(`Configuring breakpoints if any for ${limits.botId}...`);
|
|
// Waits for debugger and setup breakpoints.
|
|
|
|
await CollectionUtil.asyncForEach(GBServer.globals.debuggers[limits.botId].breaks, async brk => {
|
|
try {
|
|
const { breakpointId } = await client.Debugger.setBreakpoint({
|
|
location: {
|
|
scriptId: frame.location.scriptId,
|
|
lineNumber: brk
|
|
}
|
|
});
|
|
GBLogEx.info(min, `BASIC break defined ${breakpointId} for ${limits.botId}`);
|
|
} catch (error) {
|
|
GBLogEx.info(min, `BASIC error defining ${brk} for ${limits.botId}. ${error}`);
|
|
}
|
|
});
|
|
await client.Debugger.resume();
|
|
}
|
|
});
|
|
|
|
await client.Runtime.runIfWaitingForDebugger();
|
|
await client.Debugger.enable();
|
|
await client.Runtime.enable();
|
|
|
|
resolve(1);
|
|
} catch (err) {
|
|
GBLog.error(err);
|
|
kill(childProcess);
|
|
GBServer.globals.debuggers[limits.botId].state = 0;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = 'Stopped';
|
|
}
|
|
}).on('error', err => {
|
|
console.error(err);
|
|
kill(childProcess);
|
|
GBServer.globals.debuggers[limits.botId].state = 0;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = 'Stopped';
|
|
reject(err);
|
|
});
|
|
});
|
|
};
|
|
|
|
await debug();
|
|
}
|
|
socket = net.createConnection(childProcess['socket']);
|
|
socket.write(JSON.stringify({ code, scope }) + '\n');
|
|
|
|
const timer = setTimeout(() => {
|
|
limitError = 'code execution took too long and was killed';
|
|
|
|
kill(childProcess);
|
|
GBServer.globals.debuggers[limits.botId].state = 0;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = limitError;
|
|
}, limits.time);
|
|
|
|
try {
|
|
let data = await finalStream(socket);
|
|
|
|
data = JSON.parse(data);
|
|
|
|
if (!data.length) {
|
|
return null;
|
|
}
|
|
if (data.error) {
|
|
throw new Error(data.error);
|
|
}
|
|
|
|
return data.result;
|
|
} catch (error) {
|
|
throw new Error(limitError || error);
|
|
} finally {
|
|
kill(childProcess);
|
|
|
|
GBServer.globals.debuggers[limits.botId].state = 0;
|
|
GBServer.globals.debuggers[limits.botId].stateInfo = 'Stopped';
|
|
clearTimeout(timer);
|
|
}
|
|
};
|
|
|
|
return {
|
|
run
|
|
};
|
|
};
|