blueshell
Version:
A Behavior Tree implementation in modern Javascript
382 lines • 18.4 kB
JavaScript
;
/* 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