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) => { const childProcess = spawn( '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: false } ); 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 (GBServer.globals.debuggers[limits.botId]) { 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 }; };