UNPKG

blueshell

Version:

A Behavior Tree implementation in modern Javascript

382 lines 18.4 kB
"use strict"; /* eslint-disable no-console */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeManager = exports.APIFunctionNotFound = exports.DuplicateNodeAdded = void 0; const events_1 = require("events"); const inspector_1 = require("inspector"); const util_1 = __importDefault(require("util")); const ws_1 = __importDefault(require("ws")); const nodeManagerHelper_1 = require("./nodeManagerHelper"); const models_1 = require("../models"); class DuplicateNodeAdded extends Error { constructor(path) { super(`Key ${path} already exists! Cannot add new node.`); } } exports.DuplicateNodeAdded = DuplicateNodeAdded; class APIFunctionNotFound extends Error { constructor(apiFunction) { super(`Unknown request type: ${apiFunction}`); } } exports.APIFunctionNotFound = APIFunctionNotFound; // Manages information about what nodes are available in the BT for debugging (nodes must be registered // when they become available and unregistered when they are no longer available) class NodeManager extends events_1.EventEmitter { constructor() { super(); // maps node path to the bt node for that path this.nodePathMap = new Map(); // maps class/method to the breakpoint info for any breakpoints set on that class/method this.breakpointInfoMap = new Map(); // node inspection session this.session = new inspector_1.Session(); // eslint-disable-next-line @typescript-eslint/ban-types global.breakpointMethods = new Map(); this.session.connect(); } // Runs the web socket server that listens for commands from a client to query/set/remove breakpoints runServer() { this.session.post('Debugger.enable', () => undefined); this.server = new ws_1.default.Server({ // newer Ubuntu causes localhost to resolve to ipv6 ::1 preferably, and // the server doesn't listen on both protocols, which breaks things like // vscode remote development that only forward ipv4. host: '127.0.0.1', port: 8990, }); // should be empty but clear everything for good measure this.breakpointInfoMap.forEach(async (breakpointInfo, nodePathAndMethodName) => { try { await nodeManagerHelper_1.RuntimeWrappers.removeBreakpointFromFunction(this.session, breakpointInfo); this.breakpointInfoMap.delete(nodePathAndMethodName); } catch (err) { console.error('Failed to remove breakpoint', err); } }); this.breakpointInfoMap.clear(); global.breakpointMethods.clear(); // setup the connection handler this.server.on('connection', (clientSocket) => { // send the current cached breakpoints to the client if the client reconnects this.breakpointInfoMap.forEach((breakpointInfo) => { breakpointInfo.breakpoints.forEach((breakpoint) => { clientSocket.send(JSON.stringify({ request: 'placeBreakpoint', nodePath: breakpoint.nodePath, methodName: breakpointInfo.methodInfo.methodName, nodeName: breakpoint.nodeName, nodeParent: breakpoint.nodeParent, condition: breakpoint.condition, success: true, })); }); }); clientSocket.on('message', async (data) => { try { const dataObj = JSON.parse(data); // message should always have a request and nodePath const request = dataObj.request; const nodePath = dataObj.nodePath; switch (request) { // client is requesting the methods for a given node path case 'getMethodsForNode': { let methodInfo; let success = true; try { methodInfo = this.getMethodsForNode(nodePath); } catch { success = false; } clientSocket.send(JSON.stringify({ request: 'getMethodsForNode', success, nodePath, ...methodInfo, })); break; } // client is requesting to add (or modify) a breakpoint for a given node path/method case 'placeBreakpoint': { const methodName = dataObj.methodName; const condition = dataObj.condition; const success = await this.setBreakpoint(nodePath, methodName, condition); const node = this.nodePathMap.get(nodePath); const nodeName = node?.name; const nodeParent = node?.parent; clientSocket.send(JSON.stringify({ request: 'placeBreakpoint', nodePath: node?.path, methodName, nodeName, nodeParent, condition, success, })); break; } // client is requesting to remove a breakpoint by node path and method name case 'removeBreakpoint': { const methodName = dataObj.methodName; const success = await this.removeBreakpoint(nodePath, methodName); const node = this.nodePathMap.get(nodePath); clientSocket.send(JSON.stringify({ request: 'removeBreakpoint', nodePath: node?.path, methodName, success, })); break; } default: clientSocket.send(JSON.stringify({ request: dataObj.request, success: false, err: new APIFunctionNotFound(dataObj.request).message, })); } } catch (e) { console.error('Got exception while handling message in NodeManager', e); } }); }); } // Returns the list of methods (and which class they are inherited from) for the // bt node specified by the nodePath. Methods are sorted alphabetically getMethodsForNode(nodePath) { if (!this.nodePathMap.has(nodePath)) { throw new Error(`Requesting methods for node path: ${nodePath} which does not exist`); } else { const node = this.nodePathMap.get(nodePath); const nodeName = node.name; const nodeParent = node.parent; const listOfMethods = nodeManagerHelper_1.Utils.getMethodInfoForObject(node); return { listOfMethods, nodeName, nodeParent, }; } } // Uses the node inspector to set a breakpoint using the specified node and the details in breakpointInfo async _setBreakpoint(key, node, breakpointInfo) { const nodeName = node.name; let propertyName = breakpointInfo.methodInfo.methodName; // special case getters/setters const getOrSetMatch = propertyName.match(/^(get|set) (.+)$/); if (!!getOrSetMatch) { propertyName = getOrSetMatch[2]; } // find the class in the inheritance chain which contains the method or property let targetNode = node; while (targetNode && !Object.getOwnPropertyDescriptor(targetNode, propertyName)) { targetNode = Object.getPrototypeOf(targetNode); } // if we climbed to the top of the inheritance chain and still can't find the method or property, return failure if (!targetNode || !Object.getOwnPropertyDescriptor(targetNode, propertyName)) { throw new Error(`Could not find method ${propertyName} in inheritance chain for ${nodeName}`); } // special case getters/setters if (!!getOrSetMatch) { const getOrSet = getOrSetMatch[1]; const methodPropertyDescriptor = Object.getOwnPropertyDescriptor(targetNode, propertyName); if (!methodPropertyDescriptor) { throw new Error(`Could not find method property descriptor for ${breakpointInfo.methodInfo.methodName} ` + `in ${breakpointInfo.methodInfo.className}`); } if (getOrSet === 'get') { if (!methodPropertyDescriptor.get) { throw new Error(`get is undefined for ${propertyName} in ${breakpointInfo.methodInfo.className}`); } global.breakpointMethods.set(key, methodPropertyDescriptor.get.bind(targetNode)); } else { // getOrSet === 'set' if (!methodPropertyDescriptor.set) { throw new Error(`set is undefined for ${propertyName} in ${breakpointInfo.methodInfo.className}`); } global.breakpointMethods.set(key, methodPropertyDescriptor.set.bind(targetNode)); } } else { // not a getter/setter function global.breakpointMethods.set(key, targetNode[breakpointInfo.methodInfo.methodName].bind(targetNode)); } const objectId = await nodeManagerHelper_1.RuntimeWrappers.getObjectIdFromRuntimeEvaluate(this.session, key); const functionObjectId = await nodeManagerHelper_1.RuntimeWrappers.getFunctionObjectIdFromRuntimeProperties(this.session, objectId); // build up the condition for each node that has a breakpoint at this class/method const condition = nodeManagerHelper_1.Utils.createConditionString(Array.from(breakpointInfo.breakpoints.values())); await nodeManagerHelper_1.RuntimeWrappers.setBreakpointOnFunctionCall(this.session, functionObjectId, condition, breakpointInfo); } // Returns the NodeMethodInfo for the method in the specified node getNodeMethodInfo(node, methodName) { return this.getMethodsForNode(node.path).listOfMethods.find((method) => method.methodName === methodName); } // Sets a breakpoint on the specified method for the specified node with an optional additional condition. // If there's already a breakpoint for the class/method, then it removes that breakpoint before calling // _setBreakpoint which will create the breakpoint with the new details provided as input here async setBreakpoint(nodePath, methodName, breakpointCondition) { const debugString = `${nodePath}::${methodName}`; if (!this.nodePathMap.has(nodePath)) { console.error(`NodeManager - set breakpoint - Attempting to set breakpoint for ${debugString} ` + `but node does not exist`); return false; } else { const node = this.nodePathMap.get(nodePath); const className = this.getNodeMethodInfo(node, methodName)?.className; if (!className) { console.error(`NodeManager - set breakpoint - Attempting to set breakpoint for ${debugString} ` + `but method does not exist`); return false; } else { const key = `${className}::${methodName}`; let breakpointInfo = this.breakpointInfoMap.get(key); let first = false; if (!breakpointInfo) { // initialize breakpoint info if this is the first time we have a breakpoint for the class/method breakpointInfo = { methodInfo: { className, methodName }, breakpoints: new Map(), }; first = true; } const bp = breakpointInfo.breakpoints.get(nodePath); if (!bp || bp.condition !== breakpointCondition) { // either breakpoint on this node doesn't exist or the condition has changed, so proceed breakpointInfo.breakpoints.set(nodePath, { nodePath, condition: breakpointCondition, nodeName: node.name, nodeParent: node.parent, }); try { if (!first) { // breakpoint exists for this class/method, need to remove it and then re-create it await nodeManagerHelper_1.RuntimeWrappers.removeBreakpointFromFunction(this.session, breakpointInfo); await this._setBreakpoint(key, node, breakpointInfo); } else { // breakpoint doesn't exist, so just create it await this._setBreakpoint(key, node, breakpointInfo); this.breakpointInfoMap.set(key, breakpointInfo); } return true; } catch (err) { console.error('Got error setting breakpoint', err); return false; } } else { console.error(`NodeManager - set breakpoint - breakpoint already exists: ${key}`); return false; } } } } // Remove the breakpoint for the given node/method. This will handle if there are other // breakpoints set for the same method on the same class the node is inheriting the method from async removeBreakpoint(nodePath, methodName) { const node = this.nodePathMap.get(nodePath); if (!node) { console.error(`NodeManager - remove breakpoint - node does not exist ${nodePath}`); return false; } const methodInfo = this.getNodeMethodInfo(node, methodName); if (!methodInfo) { console.error(`NodeManager - remove breakpoint - method ${methodName} does not exist on node ${nodePath}`); return false; } const key = `${methodInfo.className}::${methodInfo.methodName}`; const keyAndPath = `${key}/${nodePath}`; const breakpointInfo = this.breakpointInfoMap.get(key); if (breakpointInfo) { const breakpointData = breakpointInfo.breakpoints.get(nodePath); if (breakpointData) { try { await nodeManagerHelper_1.RuntimeWrappers.removeBreakpointFromFunction(this.session, breakpointInfo); breakpointInfo.breakpoints.delete(nodePath); if (breakpointInfo.breakpoints.size === 0) { // this was the only breakpoint set for the key this.breakpointInfoMap.delete(key); global.breakpointMethods.delete(key); } else { await this._setBreakpoint(key, this.nodePathMap.get([...breakpointInfo.breakpoints][0][1].nodePath), breakpointInfo); } return true; } catch (err) { console.error('Got error removing breakpoint', err); return false; } } else { console.error(`NodeManager - remove breakpoint - did not find breakpoint for path: ${keyAndPath}`); return false; } } else { console.error(`NodeManager - remove breakpoint - did not find breakpoint id at all: ${keyAndPath}`); global.breakpointMethods.delete(key); return false; } } // Returns the singleton instance static getInstance() { if (!this.instance) { this.instance = new NodeManager(); } return this.instance; } static reset() { this.instance = null; } // Adds a bt node (and all its children) to be considered for debugging (setting/removing breakpoints) addNode(node) { const path = node.path; if (this.nodePathMap.has(path)) { throw new DuplicateNodeAdded(path); } else { this.nodePathMap.set(path, node); } if ((0, models_1.isParentNode)(node)) { node.getChildren().forEach((child) => { this.addNode(child); }); } } // Removes a bt node (and all its children) to be considered for debugging (setting/removing breakpoints) removeNode(node) { if ((0, models_1.isParentNode)(node)) { node.getChildren().forEach((child) => { this.removeNode(child); }); } this.nodePathMap.delete(node.path); } // Given a node path, return the bt node we have cached for that path getNode(path) { return this.nodePathMap.get(path); } // Shuts down the debug server async shutdown() { if (this.server) { await util_1.default.promisify(this.server.close.bind(this.server)); } } } exports.NodeManager = NodeManager; // singleton instance of the manager NodeManager.instance = null; //# sourceMappingURL=nodeManager.js.map