botserver/packages/basic.gblib/services/vm2-process/index.ts

211 lines
6.7 KiB
TypeScript
Raw Normal View History

const crypto2 = require('crypto');
const { spawn } = require('child_process');
const CDP = require('chrome-remote-interface');
2022-11-12 21:33:45 -03:00
const {} = require('child_process');
const net = require('net');
2022-11-11 21:35:05 -03:00
import { GBLog } from 'botlib';
2022-11-12 21:33:45 -03:00
import { CollectionUtil } from 'pragmatismo-io-framework';
2022-11-11 21:35:05 -03:00
import { GBServer } from '../../../../src/app';
2022-11-13 22:56:09 -03:00
import { DebuggerService } from '../DebuggerService';
const finalStream = require('final-stream');
2022-11-12 21:33:45 -03:00
const waitUntil = condition => {
if (condition()) {
return Promise.resolve();
}
2022-11-12 21:33:45 -03:00
return new Promise(resolve => {
const interval = setInterval(() => {
if (!condition()) {
return;
}
clearInterval(interval);
resolve(0);
}, 0);
});
};
const createVm2Pool = ({ min, max, ...limits }) => {
2022-11-12 21:33:45 -03:00
limits = Object.assign(
{
cpu: 100,
memory: 2000,
time: 4000
},
limits
);
let limitError = null;
const ref = crypto2.randomBytes(20).toString('hex');
2022-11-12 21:33:45 -03:00
const kill = x => {
spawn('sh', ['-c', `pkill -9 -f ${ref}`]);
};
let stderrCache = '';
2022-11-12 21:33:45 -03:00
const run = async (code, scope) => {
const childProcess = spawn(
'cpulimit',
[
'-ql',
limits.cpu,
'--',
2022-11-12 21:33:45 -03:00
'node',
2022-11-13 22:56:09 -03:00
`${limits.debug ? '--inspect=' + limits.debuggerPort : ''}`,
2022-11-12 21:33:45 -03:00
`--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();
});
2022-11-12 21:33:45 -03:00
childProcess.stderr.on('data', data => {
stderrCache = stderrCache + data.toString();
if (stderrCache.includes('failed: address already in use')) {
limitError = stderrCache;
kill(process);
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].state = 0;
GBServer.globals.debuggers[limits.botId].stateInfo = stderrCache;
2022-11-12 21:33:45 -03:00
}
if (stderrCache.includes('FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory')) {
limitError = 'code execution exceeed allowed memory';
kill(process);
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].state = 0;
GBServer.globals.debuggers[limits.botId].stateInfo = "Fail";
2022-11-12 21:33:45 -03:00
}
});
2022-11-12 21:33:45 -03:00
let socket = null;
2022-11-11 21:35:05 -03:00
await waitUntil(() => childProcess.socket);
2022-11-12 21:33:45 -03:00
socket = net.createConnection(childProcess.socket);
socket.write(JSON.stringify({ code, scope }) + '\n');
2022-11-11 21:35:05 -03:00
2022-11-13 22:56:09 -03:00
// Only attach if called by debugger/run.
2022-11-12 21:33:45 -03:00
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;
2022-11-13 22:56:09 -03:00
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 (!DebuggerService.systemVariables.filter(x => x === v.name)[0]) {
if (v.value.value) {
variablesText = `${variablesText} \n ${v.name}: ${v.value.value}`;
}
}
2022-11-12 21:33:45 -03:00
});
2022-11-13 22:56:09 -03:00
}
2022-11-13 23:11:52 -03:00
GBServer.globals.debuggers[limits.botId].scope = variablesText;
2022-11-13 22:56:09 -03:00
GBLog.info(`BASIC: Breakpoint variables: ${variablesText}`); // (zero-based)
2022-11-12 21:33:45 -03:00
2022-11-13 22:56:09 -03:00
// Processes breakpoint hits.
2022-11-12 21:33:45 -03:00
2022-11-13 22:56:09 -03:00
if (hitBreakpoints.length >= 1) {
GBLog.info(`BASIC: Break at line ${frame.location.lineNumber + 1}`); // (zero-based)
2022-11-12 21:33:45 -03:00
GBServer.globals.debuggers[limits.botId].state = 2;
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].stateInfo = "Break";
2022-11-13 22:56:09 -03:00
} else {
2022-11-13 23:38:04 -03:00
GBLog.verbose(`BASIC: Configuring breakpoints if any for ${limits.botId}...`);
2022-11-13 22:56:09 -03:00
// 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
}
});
GBLog.info(`BASIC break defined ${breakpointId} for ${limits.botId}`);
} catch (error) {
GBLog.info(`BASIC error defining defining ${brk} for ${limits.botId}. ${error}`);
}
});
await client.Debugger.resume();
2022-11-12 21:33:45 -03:00
}
});
2022-11-13 22:56:09 -03:00
await client.Runtime.runIfWaitingForDebugger();
await client.Debugger.enable();
await client.Runtime.enable();
2022-11-12 21:33:45 -03:00
resolve(1);
} catch (err) {
GBLog.error(err);
kill(childProcess);
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].state = 0;
GBServer.globals.debuggers[limits.botId].stateInfo = "Stopped";
2022-11-12 21:33:45 -03:00
}
}).on('error', err => {
console.error(err);
kill(childProcess);
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].state = 0;
GBServer.globals.debuggers[limits.botId].stateInfo = "Stopped";
2022-11-12 21:33:45 -03:00
reject(err);
});
});
};
await debug();
}
const timer = setTimeout(() => {
limitError = 'code execution took too long and was killed';
2022-11-13 23:38:04 -03:00
kill(childProcess);
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].state = 0;
GBServer.globals.debuggers[limits.botId].stateInfo = limitError;
}, limits.time);
try {
let data = await finalStream(socket);
2022-11-12 21:33:45 -03:00
data = JSON.parse(data);
2022-11-13 23:38:04 -03:00
2022-11-12 21:33:45 -03:00
if (!data.length) {
return null;
}
if (data.error) {
throw new Error(data.error);
}
return data.result;
} catch (error) {
throw new Error(limitError || error);
} finally {
2022-11-12 21:33:45 -03:00
kill(childProcess);
2022-11-13 23:38:04 -03:00
GBServer.globals.debuggers[limits.botId].state = 0;
GBServer.globals.debuggers[limits.botId].stateInfo = 'Stopped';
clearTimeout(timer);
}
};
return {
2022-11-12 21:33:45 -03:00
run
};
};
exports.createVm2Pool = createVm2Pool;