diff --git a/packages/basic.gblib/services/DebuggerService.ts b/packages/basic.gblib/services/DebuggerService.ts index 290f77f7..a34b6fdf 100644 --- a/packages/basic.gblib/services/DebuggerService.ts +++ b/packages/basic.gblib/services/DebuggerService.ts @@ -89,9 +89,7 @@ export class DebuggerService { */ maxLines: number = 2000; - debugMap = {}; conversationsMap = {}; - scopeMap = {}; watermarkMap = {}; /** @@ -108,54 +106,47 @@ export class DebuggerService { 'Debug Web Automation', false ); + + const botId = min.botId; + + GBServer.globals.debuggers[botId] = {}; + GBServer.globals.debuggers[botId].state = 1; + GBServer.globals.debuggers[botId].breaks = []; + } private client; - public async setBreakpoint({ botId, botApiKey, line }) { + public async breakpoint({ botId, botApiKey, line }) { - const client = GBServer.globals.debuggers[botId]; + GBServer.globals.debuggers[botId].breaks.push(Number.parseInt(line)); - async function mainScript({ Debugger }) { - return new Promise((fulfill, reject) => { - Debugger.scriptParsed((params) => { - const { scriptId, url } = params; - fulfill(scriptId); - }); - }); - } - - const scriptId = await mainScript(client); - const { breakpointId } = await await client.Debugger.setBreakpoint({ - location: { - scriptId, - lineNumber: line - 1 - } - }); } public async removeBreakPoint({ botId, botApiKey, line }) { + const client = GBServer.globals.debuggers[botId].client; + } public async continueRun({ botId, botApiKey, force }) { - const client = GBServer.globals.debuggers[botId]; + const client = GBServer.globals.debuggers[botId].client; client.Debugger.resume(); } public async stop({ botId, botApiKey, force }) { - const client = GBServer.globals.debuggers[botId]; + const client = GBServer.globals.debuggers[botId].client; client.close(); } public async stepOver({ botId, botApiKey }) { - const client = GBServer.globals.debuggers[botId]; + const client = GBServer.globals.debuggers[botId].client; client.stepOver(); } public async getExecutionContext({ botId, botApiKey, force }) { - const client = GBServer.globals.debuggers[botId]; + const client = GBServer.globals.debuggers[botId].client; const conversationId = this.conversationsMap[botId]; @@ -175,12 +166,17 @@ export class DebuggerService { }); } } - return { state:this.debugMap[botId].state, messages, scope: this.scopeMap[botId] }; + + return { state: GBServer.globals.debuggers[botId].state, messages, scope: GBServer.globals.debuggers[botId].scope }; } public async run({ botId, botApiKey, scriptName }) { + + GBLog.info(`BASIC: Running ${botId} in DEBUG mode.`); + + + GBServer.globals.debuggers[botId].state = 1; - this.debugMap[botId] = { state: 1 }; let min: GBMinInstance = GBServer.globals.minInstances.filter( p => p.instance.botId === botId @@ -210,28 +206,5 @@ export class DebuggerService { } } }); - - // Setup debugger. - - const client = GBServer.globals.debuggers[botId]; - - client.Debugger.paused(({ callFrames, reason, hitBreakpoints }) => { - - const frame = callFrames[0]; - if (hitBreakpoints.length > 1) { - GBLog.info(`.gbdialog break at line ${frame.location.lineNumber + 1}`); // (zero-based) - - const scope = `${frame.scopeChain[0].name} ${frame.scopeChain[0].object}`; - - this.scopeMap[botId] = scope; - } - else if (reason === ''){ - GBLog.info(`.gbdialog ${reason} at line ${frame.location.lineNumber + 1}`); // (zero-based) - } - }); - - await client.Runtime.runIfWaitingForDebugger(); - await client.Debugger.enable(); - await client.Debugger.setPauseOnExceptions('all'); } } \ No newline at end of file diff --git a/packages/basic.gblib/services/GBVMService.ts b/packages/basic.gblib/services/GBVMService.ts index 86174b35..843ac5c6 100644 --- a/packages/basic.gblib/services/GBVMService.ts +++ b/packages/basic.gblib/services/GBVMService.ts @@ -770,7 +770,7 @@ export class GBVMService extends GBService { // Creates a class DialogKeywords which is the *this* pointer // in BASIC. - const user = step ? await min.userProfile.get(step.context, {}) : null; + const user = step ? await min.userProfile.get(step.context, {}) : null; const sandbox = { user: user.systemUser }; @@ -826,9 +826,9 @@ export class GBVMService extends GBService { const runnerPath = urlJoin(process.cwd(), 'dist', 'packages', 'basic.gblib', 'services', 'vm2-process', 'vm2ProcessRunner.js'); try { - const { run, drain } = createVm2Pool({ - min: 1, - max: 1, + const { run } = createVm2Pool({ + min: 0, + max: 0, debuggerPort: 9222, botId: botId, cpu: 100, @@ -840,8 +840,7 @@ export class GBVMService extends GBService { const port = run.port; const result = await run(code, { filename: scriptPath, sandbox: sandbox }); - - drain(); + return result; } catch (error) { throw new Error(`BASIC RUNTIME ERR: ${error.message ? error.message : error}\n Stack:${error.stack}`); diff --git a/packages/basic.gblib/services/vm2-process/index.ts b/packages/basic.gblib/services/vm2-process/index.ts index fd2b35db..b8101438 100644 --- a/packages/basic.gblib/services/vm2-process/index.ts +++ b/packages/basic.gblib/services/vm2-process/index.ts @@ -1,23 +1,19 @@ const crypto2 = require('crypto'); -const Fs = require('fs'); -const Path = require('path'); const { spawn } = require('child_process'); const CDP = require('chrome-remote-interface'); -const { } = require('child_process'); -const { dirname } = require('path'); -const { fileURLToPath } = require('url'); +const {} = require('child_process'); const net = require('net'); import { GBLog } from 'botlib'; +import { CollectionUtil } from 'pragmatismo-io-framework'; import { GBServer } from '../../../../src/app'; -const genericPool = require('generic-pool'); const finalStream = require('final-stream'); -const waitUntil = (condition) => { +const waitUntil = condition => { if (condition()) { return Promise.resolve(); } - return new Promise((resolve) => { + return new Promise(resolve => { const interval = setInterval(() => { if (!condition()) { return; @@ -30,89 +26,141 @@ const waitUntil = (condition) => { }; const createVm2Pool = ({ min, max, ...limits }) => { - limits = Object.assign({ - cpu: 100, - memory: 2000, - time: 4000 - }, limits); + limits = Object.assign( + { + cpu: 100, + memory: 2000, + time: 4000 + }, + limits + ); let limitError = null; const ref = crypto2.randomBytes(20).toString('hex'); - const kill = (x) => { + const kill = x => { spawn('sh', ['-c', `pkill -9 -f ${ref}`]); }; let stderrCache = ''; - const factory = { - create: function () { - - const runner = spawn('cpulimit', [ - '-ql', limits.cpu, - '--', - 'node', `--inspect-brk=${limits.debuggerPort}`, `--experimental-fetch`, `--max-old-space-size=${limits.memory}`, - limits.script - , ref - ], { cwd: limits.cwd, shell: false }); - - runner.stdout.on('data', (data) => { - runner.socket = runner.socket || data.toString().trim(); - }); - - runner.stderr.on('data', (data) => { - stderrCache = stderrCache + data.toString(); - if (stderrCache.includes('failed: address already in use')) - { - limitError = stderrCache; - } - if (stderrCache.includes('FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory')) { - limitError = 'code execution exceeed allowed memory'; - } - }); - - return runner; - }, - - destroy: function (childProcess) { - kill(childProcess); - } - }; - - const pool = genericPool.createPool(factory, { min, max }); const run = async (code, scope) => { - const childProcess = await pool.acquire(); + const childProcess = spawn( + 'cpulimit', + [ + '-ql', + limits.cpu, + '--', + 'node', + `--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); + } + if (stderrCache.includes('FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory')) { + limitError = 'code execution exceeed allowed memory'; + kill(process); + } + }); + + let socket = null; await waitUntil(() => childProcess.socket); + socket = net.createConnection(childProcess.socket); + socket.write(JSON.stringify({ code, scope }) + '\n'); - const socket = net.createConnection(childProcess.socket); + // Only attach if called by /debugger/run. - CDP(async (client) => { - const { Debugger, Runtime } = client; - try { - GBServer.globals.debuggers[scope.botId] = client; - } catch (err) { - GBLog.error(err); - } finally { - client.close(); - } - }).on('error', (err) => { - console.error(err); - }); + 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.Runtime.runIfWaitingForDebugger(); + await client.Debugger.enable(); + + async function mainScript({ Debugger }) { + return new Promise((fulfill, reject) => { + Debugger.scriptParsed(params => { + const { scriptId, url } = params; + fulfill(scriptId); + }); + }); + } + + await CollectionUtil.asyncForEach(GBServer.globals.debuggers[limits.botId].breaks, async brk => { + const scriptId = await mainScript(client); + const { breakpointId } = await client.Debugger.setBreakpoint({ + location: { + scriptId, + lineNumber: brk + } + }); + GBLog.info(`BASIC break defined ${breakpointId} for ${limits.botId}`); + }); + + await client.Debugger.paused(({ callFrames, reason, hitBreakpoints }) => { + const frame = callFrames[0]; + if (hitBreakpoints.length > 1) { + GBLog.info(`.gbdialog break at line ${frame.location.lineNumber + 1}`); // (zero-based) + + const scope = `${frame.scopeChain[0].name} ${frame.scopeChain[0].object}`; + + GBServer.globals.debuggers[limits.botId].scope = scope; + GBServer.globals.debuggers[limits.botId].state = 2; + } else if (reason === '') { + GBLog.info(`.gbdialog ${reason} at line ${frame.location.lineNumber + 1}`); // (zero-based) + } + }); + + + resolve(1); + } catch (err) { + GBLog.error(err); + kill(childProcess); + } + }).on('error', err => { + console.error(err); + kill(childProcess); + reject(err); + }); + }); + }; + + await debug(); + } const timer = setTimeout(() => { limitError = 'code execution took too long and was killed'; kill(childProcess); }, limits.time); - socket.write(JSON.stringify({ code, scope }) + '\n'); - try { let data = await finalStream(socket); - - data = JSON.parse(data) + data = JSON.parse(data); + if (!data.length) { + return null; + } if (data.error) { throw new Error(data.error); } @@ -121,16 +169,13 @@ const createVm2Pool = ({ min, max, ...limits }) => { } catch (error) { throw new Error(limitError || error); } finally { + kill(childProcess); clearTimeout(timer); - pool.destroy(childProcess); } }; return { - run, - drain: () => { - pool.drain().then(() => pool.clear()); - } + run }; }; diff --git a/src/app.ts b/src/app.ts index c50fbcf6..faaa8cc2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -108,6 +108,7 @@ export class GBServer { GBServer.globals.minInstances = []; GBServer.globals.wwwroot = null; GBServer.globals.entryPointDialog = null; + GBServer.globals.debuggers = []; server.use(bodyParser.json()); server.use(bodyParser.urlencoded({ extended: true }));