@ai-capabilities-suite/mcp-debugger-core
Version:
Core debugging engine for Node.js and TypeScript applications. Provides Inspector Protocol integration, breakpoint management, variable inspection, execution control, profiling, hang detection, and source map support.
1,150 lines • 42.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DebugSession = exports.SessionState = exports.HitCountOperator = exports.BreakpointType = void 0;
const path = __importStar(require("path"));
const inspector_client_1 = require("./inspector-client");
const process_spawner_1 = require("./process-spawner");
const breakpoint_manager_1 = require("./breakpoint-manager");
const cdp_breakpoint_operations_1 = require("./cdp-breakpoint-operations");
const variable_inspector_1 = require("./variable-inspector");
const source_map_manager_1 = require("./source-map-manager");
const cpu_profiler_1 = require("./cpu-profiler");
const memory_profiler_1 = require("./memory-profiler");
const performance_timeline_1 = require("./performance-timeline");
/**
* Breakpoint type enumeration
*/
var BreakpointType;
(function (BreakpointType) {
BreakpointType["STANDARD"] = "standard";
BreakpointType["LOGPOINT"] = "logpoint";
BreakpointType["EXCEPTION"] = "exception";
BreakpointType["FUNCTION"] = "function";
})(BreakpointType || (exports.BreakpointType = BreakpointType = {}));
/**
* Hit count operator for hit count breakpoints
*/
var HitCountOperator;
(function (HitCountOperator) {
HitCountOperator["EQUAL"] = "==";
HitCountOperator["GREATER"] = ">";
HitCountOperator["GREATER_EQUAL"] = ">=";
HitCountOperator["LESS"] = "<";
HitCountOperator["LESS_EQUAL"] = "<=";
HitCountOperator["MODULO"] = "%";
})(HitCountOperator || (exports.HitCountOperator = HitCountOperator = {}));
/**
* Debug session state
*/
var SessionState;
(function (SessionState) {
SessionState["STARTING"] = "starting";
SessionState["PAUSED"] = "paused";
SessionState["RUNNING"] = "running";
SessionState["TERMINATED"] = "terminated";
})(SessionState || (exports.SessionState = SessionState = {}));
/**
* Represents a single debug session with a target process
* Tracks session state, breakpoints, and watched variables
*/
class DebugSession {
constructor(id, config) {
this.process = null;
this.inspector = null;
this.state = SessionState.STARTING;
this.cdpBreakpointOps = null;
this.variableInspector = null;
this.watchedVariables = new Map();
this.watchedVariableChanges = new Map();
this.exceptionBreakpoints = new Map();
this.currentCallFrames = [];
this.currentFrameIndex = 0;
this.crashHandlers = [];
this.cpuProfiler = null;
this.memoryProfiler = null;
this.performanceTimeline = null;
this.id = id;
this.config = config;
this.breakpointManager = new breakpoint_manager_1.BreakpointManager();
this.sourceMapManager = new source_map_manager_1.SourceMapManager();
}
/**
* Start the debug session by spawning the process and connecting the inspector
*/
async start() {
if (this.state !== SessionState.STARTING) {
throw new Error(`Cannot start session in state: ${this.state}`);
}
// Validate file exists if args contain a file path
// Skip validation for npx, npm, yarn, etc. as they take package names
const skipValidationCommands = ["npx", "npm", "yarn", "pnpm", "bun"];
if (this.config.args &&
this.config.args.length > 0 &&
!skipValidationCommands.includes(this.config.command)) {
const firstArg = this.config.args[0];
// Check if it looks like a file path (not a flag starting with - and has file extension)
if (!firstArg.startsWith("-") && /\.(js|ts|mjs|cjs)$/.test(firstArg)) {
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
const filePath = path.isAbsolute(firstArg)
? firstArg
: path.resolve(this.config.cwd || process.cwd(), firstArg);
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
}
}
// Validate working directory exists
if (this.config.cwd) {
const fs = await Promise.resolve().then(() => __importStar(require("fs")));
if (!fs.existsSync(this.config.cwd)) {
throw new Error(`Working directory not found: ${this.config.cwd}`);
}
}
try {
// Spawn process with inspector
const { process: proc, wsUrl } = await (0, process_spawner_1.spawnWithInspector)(this.config.command, this.config.args || [], this.config.cwd);
this.process = proc;
// Connect inspector client
this.inspector = new inspector_client_1.InspectorClient(wsUrl);
await this.inspector.connect();
// Initialize CDP breakpoint operations
this.cdpBreakpointOps = new cdp_breakpoint_operations_1.CdpBreakpointOperations(this.inspector);
// Initialize variable inspector
this.variableInspector = new variable_inspector_1.VariableInspector(this.inspector);
this.variableInspector.setSourceMapManager(this.sourceMapManager);
// Initialize CPU profiler
this.cpuProfiler = new cpu_profiler_1.CPUProfiler(this.inspector);
// Initialize memory profiler
this.memoryProfiler = new memory_profiler_1.MemoryProfiler(this.inspector);
// Initialize performance timeline
this.performanceTimeline = new performance_timeline_1.PerformanceTimeline(this.inspector);
// Enable debugging domains
await this.inspector.send("Debugger.enable");
await this.inspector.send("Runtime.enable");
// Set up event handlers BEFORE calling runIfWaitingForDebugger
this.inspector.on("Debugger.paused", async (params) => {
this.state = SessionState.PAUSED;
this.currentCallFrames = params?.callFrames || [];
this.currentFrameIndex = 0; // Reset to top frame when paused
// Evaluate watched variables when paused
if (this.watchedVariables.size > 0) {
try {
const changes = await this.evaluateWatchedVariables();
this.watchedVariableChanges = changes;
}
catch (error) {
// Ignore errors during watch evaluation
}
}
});
this.inspector.on("Debugger.resumed", () => {
this.state = SessionState.RUNNING;
this.currentCallFrames = [];
this.currentFrameIndex = 0; // Reset frame index when resumed
});
// Handle process exit
this.process.on("exit", (code, signal) => {
this.handleProcessExit(code, signal);
});
// Handle process errors
this.process.on("error", (error) => {
this.handleProcessError(error);
});
// Tell the runtime to run if it's waiting for debugger
// This will trigger a Debugger.paused event at the first line
await this.inspector.send("Runtime.runIfWaitingForDebugger");
// Wait for the initial pause from --inspect-brk
// The process should pause at the first line after runIfWaitingForDebugger
await new Promise((resolve) => {
const timeout = setTimeout(() => {
// If we don't get a paused event within 1 second, assume we're paused
this.state = SessionState.PAUSED;
resolve();
}, 1000);
this.inspector.once("Debugger.paused", () => {
clearTimeout(timeout);
this.state = SessionState.PAUSED;
resolve();
});
});
}
catch (error) {
this.state = SessionState.TERMINATED;
await this.cleanup();
throw error;
}
}
/**
* Pause the running process
* Waits for the Debugger.paused event to ensure call frames are populated
*/
async pause() {
if (!this.inspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.RUNNING) {
throw new Error(`Cannot pause session in state: ${this.state}`);
}
// Send the pause command
await this.inspector.send("Debugger.pause");
// Wait for the Debugger.paused event with a reasonable timeout
// This ensures call frames are populated before we return
await new Promise((resolve) => {
const timeout = setTimeout(() => {
// Timeout - set state manually if event didn't fire
if (this.state === SessionState.RUNNING) {
this.state = SessionState.PAUSED;
}
resolve();
}, 1000); // Increased timeout to 1 second
const handler = () => {
clearTimeout(timeout);
resolve();
};
this.inspector.once("Debugger.paused", handler);
});
}
/**
* Resume execution of the paused process
*/
async resume() {
if (!this.inspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error(`Cannot resume session in state: ${this.state}`);
}
await this.inspector.send("Debugger.resume");
this.state = SessionState.RUNNING;
}
/**
* Step over the current line
* Executes the current line and pauses at the next line in the same scope
*/
async stepOver() {
if (!this.inspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error(`Cannot step over in state: ${this.state}`);
}
await this.inspector.send("Debugger.stepOver");
// State will be updated by Debugger.paused event
}
/**
* Step into the current line
* Executes the current line and pauses at the first line inside any called function
*/
async stepInto() {
if (!this.inspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error(`Cannot step into in state: ${this.state}`);
}
await this.inspector.send("Debugger.stepInto");
// State will be updated by Debugger.paused event
}
/**
* Step out of the current function
* Executes until the current function returns and pauses at the calling location
*/
async stepOut() {
if (!this.inspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error(`Cannot step out in state: ${this.state}`);
}
await this.inspector.send("Debugger.stepOut");
// State will be updated by Debugger.paused event
}
/**
* Clean up session resources
*/
async cleanup() {
// Prevent multiple cleanup calls
if (this.state === SessionState.TERMINATED) {
return;
}
this.state = SessionState.TERMINATED;
// Remove all breakpoints
if (this.cdpBreakpointOps &&
this.inspector &&
this.inspector.isConnected()) {
for (const breakpoint of this.breakpointManager.getAllBreakpoints()) {
if (breakpoint.cdpBreakpointId) {
try {
await this.cdpBreakpointOps.removeBreakpoint(breakpoint.cdpBreakpointId);
}
catch (error) {
// Ignore errors during cleanup
}
}
}
}
// Disconnect inspector first
if (this.inspector) {
try {
await this.inspector.disconnect();
}
catch (error) {
// Ignore errors during cleanup
}
this.inspector = null;
}
// Kill process if still running
if (this.process && !this.process.killed && this.process.exitCode === null) {
try {
// Try graceful termination first
this.process.kill('SIGTERM');
// Wait a bit for graceful shutdown
await new Promise((resolve) => {
const timeout = setTimeout(() => {
// Force kill if still running
if (this.process && !this.process.killed && this.process.exitCode === null) {
this.process.kill('SIGKILL');
}
resolve();
}, 1000);
if (this.process) {
this.process.once('exit', () => {
clearTimeout(timeout);
resolve();
});
}
else {
clearTimeout(timeout);
resolve();
}
});
}
catch (error) {
// Ignore errors during cleanup
}
}
this.process = null;
this.cdpBreakpointOps = null;
this.variableInspector = null;
this.cpuProfiler = null;
this.memoryProfiler = null;
this.performanceTimeline = null;
this.breakpointManager.clearAll();
this.watchedVariables.clear();
this.watchedVariableChanges.clear();
this.exceptionBreakpoints.clear();
this.crashHandlers = [];
// Clear source map cache
this.sourceMapManager.clearCache();
}
/**
* Get the current session state
*/
getState() {
return this.state;
}
/**
* Get the inspector client
*/
getInspector() {
return this.inspector;
}
/**
* Get the process handle
*/
getProcess() {
return this.process;
}
/**
* Set a breakpoint in the session
* Creates the breakpoint and sets it via CDP
* Maps TypeScript locations to JavaScript when source maps are available
* Requirements: 7.2
*/
async setBreakpoint(file, line, condition) {
if (!this.cdpBreakpointOps) {
throw new Error("Session not started");
}
// Try to map TypeScript location to JavaScript if it's a TypeScript file
let targetFile = file;
let targetLine = line;
if (file.endsWith(".ts") || file.endsWith(".tsx")) {
const compiledLocation = await this.sourceMapManager.mapSourceToCompiled({
file,
line,
column: 0,
});
if (compiledLocation) {
targetFile = compiledLocation.file;
targetLine = compiledLocation.line;
}
}
// Create breakpoint in manager with the original file/line
// This ensures the user sees the TypeScript location
const breakpoint = this.breakpointManager.createBreakpoint(file, line, condition);
// Set breakpoint via CDP using the compiled location if enabled
if (breakpoint.enabled) {
// Create a temporary breakpoint object with the compiled location for CDP
const cdpBreakpoint = {
...breakpoint,
file: targetFile,
line: targetLine,
};
const cdpBreakpointId = await this.cdpBreakpointOps.setBreakpoint(cdpBreakpoint);
if (cdpBreakpointId) {
this.breakpointManager.updateCdpBreakpointId(breakpoint.id, cdpBreakpointId);
}
}
return breakpoint;
}
/**
* Set a logpoint in the session
* Creates a logpoint that logs a message without pausing execution
* @param file File path where the logpoint should be set
* @param line Line number (1-indexed)
* @param logMessage Log message template with {variable} interpolation
* @returns The created logpoint
*/
async setLogpoint(file, line, logMessage) {
if (!this.cdpBreakpointOps) {
throw new Error("Session not started");
}
// Try to map TypeScript location to JavaScript if it's a TypeScript file
let targetFile = file;
let targetLine = line;
if (file.endsWith(".ts") || file.endsWith(".tsx")) {
const compiledLocation = await this.sourceMapManager.mapSourceToCompiled({
file,
line,
column: 0,
});
if (compiledLocation) {
targetFile = compiledLocation.file;
targetLine = compiledLocation.line;
}
}
// Create logpoint in manager with the original file/line
const logpoint = this.breakpointManager.createLogpoint(file, line, logMessage);
// Set logpoint via CDP using the compiled location if enabled
if (logpoint.enabled) {
const cdpLogpoint = {
...logpoint,
file: targetFile,
line: targetLine,
};
const cdpBreakpointId = await this.cdpBreakpointOps.setBreakpoint(cdpLogpoint);
if (cdpBreakpointId) {
this.breakpointManager.updateCdpBreakpointId(logpoint.id, cdpBreakpointId);
}
}
return logpoint;
}
/**
* Set a function breakpoint in the session
* Creates a breakpoint that pauses when a function with the given name is called
* @param functionName Function name or regex pattern
* @returns The created function breakpoint
*/
async setFunctionBreakpoint(functionName) {
if (!this.cdpBreakpointOps) {
throw new Error("Session not started");
}
// Create function breakpoint in manager
const breakpoint = this.breakpointManager.createFunctionBreakpoint(functionName);
// Set function breakpoint via CDP if enabled
if (breakpoint.enabled) {
const cdpBreakpointId = await this.cdpBreakpointOps.setBreakpoint(breakpoint);
if (cdpBreakpointId) {
this.breakpointManager.updateCdpBreakpointId(breakpoint.id, cdpBreakpointId);
}
}
return breakpoint;
}
/**
* Set hit count condition for a breakpoint
* @param id Breakpoint identifier
* @param condition Hit count condition
* @returns The updated breakpoint or undefined if not found
*/
setBreakpointHitCountCondition(id, condition) {
return this.breakpointManager.setHitCountCondition(id, condition);
}
/**
* Get the breakpoint manager for this session
*/
getBreakpointManager() {
return this.breakpointManager;
}
/**
* Set an exception breakpoint
* Configures the debugger to pause on caught and/or uncaught exceptions
* @param breakOnCaught Whether to break on caught exceptions
* @param breakOnUncaught Whether to break on uncaught exceptions
* @param exceptionFilter Optional regex pattern to filter exceptions by type/message
* @returns The created exception breakpoint
*/
async setExceptionBreakpoint(breakOnCaught, breakOnUncaught, exceptionFilter) {
if (!this.inspector) {
throw new Error("Session not started");
}
const id = `exception_${Date.now()}_${Math.random()
.toString(36)
.substring(2, 11)}`;
const exceptionBreakpoint = {
id,
breakOnCaught,
breakOnUncaught,
exceptionFilter,
enabled: true,
};
this.exceptionBreakpoints.set(id, exceptionBreakpoint);
// Configure CDP to pause on exceptions
await this.inspector.send("Debugger.setPauseOnExceptions", {
state: breakOnUncaught
? breakOnCaught
? "all"
: "uncaught"
: breakOnCaught
? "all"
: "none",
});
return exceptionBreakpoint;
}
/**
* Remove an exception breakpoint
* @param id Exception breakpoint identifier
* @returns True if the exception breakpoint was found and removed
*/
async removeExceptionBreakpoint(id) {
const exceptionBreakpoint = this.exceptionBreakpoints.get(id);
if (!exceptionBreakpoint) {
return false;
}
this.exceptionBreakpoints.delete(id);
// If no more exception breakpoints, disable exception pausing
if (this.exceptionBreakpoints.size === 0 && this.inspector) {
await this.inspector.send("Debugger.setPauseOnExceptions", {
state: "none",
});
}
return true;
}
/**
* Get all exception breakpoints
* @returns Array of all exception breakpoints
*/
getAllExceptionBreakpoints() {
return Array.from(this.exceptionBreakpoints.values());
}
/**
* Get an exception breakpoint by ID
* @param id Exception breakpoint identifier
* @returns The exception breakpoint or undefined if not found
*/
getExceptionBreakpoint(id) {
return this.exceptionBreakpoints.get(id);
}
/**
* Get a breakpoint by ID
*/
getBreakpoint(id) {
return this.breakpointManager.getBreakpoint(id);
}
/**
* Get all breakpoints
*/
getAllBreakpoints() {
return this.breakpointManager.getAllBreakpoints();
}
/**
* Remove a breakpoint from the session
* Removes from manager and from CDP
*/
async removeBreakpoint(id) {
const breakpoint = this.breakpointManager.getBreakpoint(id);
if (!breakpoint) {
return false;
}
// Remove from CDP if it has a CDP breakpoint ID
if (breakpoint.cdpBreakpointId && this.cdpBreakpointOps) {
await this.cdpBreakpointOps.removeBreakpoint(breakpoint.cdpBreakpointId);
}
// Remove from manager
return this.breakpointManager.removeBreakpoint(id);
}
/**
* Toggle a breakpoint's enabled state
*/
async toggleBreakpoint(id) {
const breakpoint = this.breakpointManager.toggleBreakpoint(id);
if (!breakpoint || !this.cdpBreakpointOps) {
return breakpoint;
}
// If now enabled, set via CDP
if (breakpoint.enabled && !breakpoint.cdpBreakpointId) {
const cdpBreakpointId = await this.cdpBreakpointOps.setBreakpoint(breakpoint);
if (cdpBreakpointId) {
this.breakpointManager.updateCdpBreakpointId(breakpoint.id, cdpBreakpointId);
}
}
// If now disabled, remove from CDP
else if (!breakpoint.enabled && breakpoint.cdpBreakpointId) {
await this.cdpBreakpointOps.removeBreakpoint(breakpoint.cdpBreakpointId);
this.breakpointManager.updateCdpBreakpointId(breakpoint.id, "");
}
return breakpoint;
}
/**
* Add a breakpoint to the session (legacy method for compatibility)
*/
addBreakpoint(breakpoint) {
// This is a legacy method - breakpoints should be created via setBreakpoint
// But we keep it for backward compatibility with tests
this.breakpointManager.addBreakpoint(breakpoint);
}
/**
* Add a watched variable
*/
addWatchedVariable(variable) {
this.watchedVariables.set(variable.name, variable);
}
/**
* Get a watched variable by name
*/
getWatchedVariable(name) {
return this.watchedVariables.get(name);
}
/**
* Get all watched variables
*/
getAllWatchedVariables() {
return Array.from(this.watchedVariables.values());
}
/**
* Remove a watched variable
*/
removeWatchedVariable(name) {
return this.watchedVariables.delete(name);
}
/**
* Check if the session is active (not terminated)
*/
isActive() {
return this.state !== SessionState.TERMINATED;
}
/**
* Check if the session is paused
*/
isPaused() {
return this.state === SessionState.PAUSED;
}
/**
* Evaluate an expression in the current execution context
* @param expression The JavaScript expression to evaluate
* @param callFrameId Optional call frame ID (uses current frame if not provided)
* @returns The evaluation result with type information
*/
async evaluateExpression(expression, callFrameId) {
if (!this.variableInspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error("Process must be paused to evaluate expressions");
}
// If no callFrameId provided, use the current frame
const frameId = callFrameId ||
this.currentCallFrames[this.currentFrameIndex]?.callFrameId;
if (!frameId) {
throw new Error("No call frames available");
}
return this.variableInspector.evaluateExpression(expression, frameId);
}
/**
* Get properties of an object by its object ID
* @param objectId The CDP object ID
* @returns Array of property descriptors
*/
async getObjectProperties(objectId) {
if (!this.variableInspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error("Process must be paused to inspect objects");
}
return this.variableInspector.getObjectProperties(objectId);
}
/**
* Inspect an object with nested property resolution
* @param objectId The CDP object ID
* @param maxDepth Maximum depth to traverse (default: 2)
* @returns Nested object structure
*/
async inspectObject(objectId, maxDepth = 2) {
if (!this.variableInspector) {
throw new Error("Session not started");
}
if (this.state !== SessionState.PAUSED) {
throw new Error("Process must be paused to inspect objects");
}
return this.variableInspector.inspectObject(objectId, maxDepth);
}
/**
* Get the current call frames
*/
getCurrentCallFrames() {
return this.currentCallFrames;
}
/**
* Get the call stack with formatted stack frames
* Returns stack frames with function names, files (absolute paths), and line numbers
* Maps JavaScript locations back to TypeScript when source maps are available
* Requirements: 4.1, 9.4, 7.3
*/
async getCallStack() {
if (this.state !== SessionState.PAUSED) {
throw new Error("Process must be paused to get call stack");
}
if (!this.currentCallFrames || this.currentCallFrames.length === 0) {
return [];
}
const frames = [];
for (const frame of this.currentCallFrames) {
// Extract file path from the URL
// CDP returns file URLs like "file:///absolute/path/to/file.js"
let filePath = frame.url || "";
// Convert file:// URL to absolute path
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7); // Remove 'file://'
}
// Ensure the path is absolute
// If it's not already absolute, make it absolute relative to cwd
if (!filePath.startsWith("/")) {
filePath = path.resolve(this.config.cwd || process.cwd(), filePath);
}
let line = frame.location.lineNumber + 1; // CDP uses 0-indexed lines
let column = frame.location.columnNumber;
// Try to map back to source location if source map is available
const sourceLocation = await this.sourceMapManager.mapCompiledToSource({
file: filePath,
line: line,
column: column,
});
if (sourceLocation) {
filePath = sourceLocation.file;
line = sourceLocation.line;
column = sourceLocation.column;
}
frames.push({
functionName: frame.functionName || "(anonymous)",
file: filePath,
line: line,
column: column,
callFrameId: frame.callFrameId,
});
}
return frames;
}
/**
* Get the call stack synchronously (without source map mapping)
* Use getCallStack() for source map support
* @deprecated Use getCallStack() instead for source map support
*/
getCallStackSync() {
if (this.state !== SessionState.PAUSED) {
throw new Error("Process must be paused to get call stack");
}
if (!this.currentCallFrames || this.currentCallFrames.length === 0) {
return [];
}
return this.currentCallFrames.map((frame) => {
// Extract file path from the URL
// CDP returns file URLs like "file:///absolute/path/to/file.js"
let filePath = frame.url || "";
// Convert file:// URL to absolute path
if (filePath.startsWith("file://")) {
filePath = filePath.substring(7); // Remove 'file://'
}
// Ensure the path is absolute
// If it's not already absolute, make it absolute relative to cwd
if (!filePath.startsWith("/")) {
filePath = path.resolve(this.config.cwd || process.cwd(), filePath);
}
return {
functionName: frame.functionName || "(anonymous)",
file: filePath,
line: frame.location.lineNumber + 1, // CDP uses 0-indexed lines
column: frame.location.columnNumber,
callFrameId: frame.callFrameId,
};
});
}
/**
* Evaluate watched variables and detect changes
* Should be called when the process pauses
*/
async evaluateWatchedVariables() {
const changes = new Map();
for (const [name, watched] of this.watchedVariables.entries()) {
try {
const result = await this.evaluateExpression(watched.expression);
const newValue = result.value;
if (watched.lastValue !== undefined && watched.lastValue !== newValue) {
changes.set(name, {
oldValue: watched.lastValue,
newValue: newValue,
});
}
watched.lastValue = newValue;
}
catch (error) {
// Ignore evaluation errors for watched variables
}
}
return changes;
}
/**
* Get the latest watched variable changes from the last pause
* Returns a map of variable names to their old and new values
*/
getWatchedVariableChanges() {
return new Map(this.watchedVariableChanges);
}
/**
* Clear the watched variable changes
*/
clearWatchedVariableChanges() {
this.watchedVariableChanges.clear();
}
/**
* Switch context to a different stack frame by index
* Updates the current frame for variable inspection
* Requirements: 4.2, 4.3
* @param frameIndex The index of the frame to switch to (0 = top frame)
*/
switchToFrame(frameIndex) {
if (this.state !== SessionState.PAUSED) {
throw new Error("Process must be paused to switch frames");
}
if (!this.currentCallFrames || this.currentCallFrames.length === 0) {
throw new Error("No call frames available");
}
if (frameIndex < 0 || frameIndex >= this.currentCallFrames.length) {
throw new Error(`Frame index ${frameIndex} out of range (0-${this.currentCallFrames.length - 1})`);
}
this.currentFrameIndex = frameIndex;
}
/**
* Get the current frame index
*/
getCurrentFrameIndex() {
return this.currentFrameIndex;
}
/**
* Get the call frame ID for the current frame
*/
getCurrentCallFrameId() {
if (!this.currentCallFrames || this.currentCallFrames.length === 0) {
return undefined;
}
if (this.currentFrameIndex >= this.currentCallFrames.length) {
return undefined;
}
return this.currentCallFrames[this.currentFrameIndex]?.callFrameId;
}
/**
* Get the source map manager for this session
*/
getSourceMapManager() {
return this.sourceMapManager;
}
/**
* Map a source location to compiled location using source maps
* Requirements: 7.2
*/
async mapSourceToCompiled(file, line, column = 0) {
return this.sourceMapManager.mapSourceToCompiled({ file, line, column });
}
/**
* Map a compiled location to source location using source maps
* Requirements: 7.3
*/
async mapCompiledToSource(file, line, column = 0) {
return this.sourceMapManager.mapCompiledToSource({ file, line, column });
}
/**
* Handle process exit event
* Detects unexpected terminations and cleans up resources
* Requirements: 8.1
*/
handleProcessExit(code, signal) {
const wasActive = this.state !== SessionState.TERMINATED;
// Only process if we haven't already handled this exit
if (!wasActive) {
return;
}
this.state = SessionState.TERMINATED;
// If the process exited unexpectedly (non-zero exit code or killed by signal)
// this is a crash
if (code !== 0 || signal !== null) {
const error = new Error(`Process crashed with ${signal ? `signal ${signal}` : `exit code ${code}`}`);
this.crashError = error;
// Call all registered crash handlers
for (const handler of this.crashHandlers) {
try {
handler(error);
}
catch (handlerError) {
// Ignore errors in crash handlers
}
}
// Trigger cleanup asynchronously
this.cleanup().catch(() => {
// Ignore cleanup errors during crash handling
});
}
else {
// Normal exit (code 0), still clean up
this.cleanup().catch(() => {
// Ignore cleanup errors
});
}
}
/**
* Handle process error event
* Detects spawn errors and other process-level errors
* Requirements: 8.1
*/
handleProcessError(error) {
this.state = SessionState.TERMINATED;
this.crashError = error;
// Call all registered crash handlers
for (const handler of this.crashHandlers) {
try {
handler(error);
}
catch (handlerError) {
// Ignore errors in crash handlers
}
}
// Trigger cleanup asynchronously
this.cleanup().catch(() => {
// Ignore cleanup errors during crash handling
});
}
/**
* Register a crash handler callback
* The handler will be called when the process crashes or terminates unexpectedly
* Multiple handlers can be registered and all will be called
* Requirements: 8.1
*/
onCrash(handler) {
this.crashHandlers.push(handler);
}
/**
* Get the crash error if the process crashed
* Returns undefined if the process terminated normally
* Requirements: 8.1
*/
getCrashError() {
return this.crashError;
}
/**
* Check if the process crashed
* Returns true if the process terminated unexpectedly
* Requirements: 8.1
*/
hasCrashed() {
return this.crashError !== undefined;
}
/**
* Start CPU profiling
* Begins collecting CPU profile data for performance analysis
*/
async startCPUProfile() {
if (!this.cpuProfiler) {
throw new Error("Session not started");
}
await this.cpuProfiler.start();
}
/**
* Stop CPU profiling and return the profile data
* @returns The captured CPU profile
*/
async stopCPUProfile() {
if (!this.cpuProfiler) {
throw new Error("Session not started");
}
return await this.cpuProfiler.stop();
}
/**
* Check if CPU profiling is currently active
*/
isCPUProfiling() {
return this.cpuProfiler?.isProfiling() || false;
}
/**
* Get the CPU profiler instance
*/
getCPUProfiler() {
return this.cpuProfiler;
}
/**
* Analyze a CPU profile to identify bottlenecks
* @param profile The CPU profile to analyze
* @returns Analysis results with top functions and bottlenecks
*/
analyzeCPUProfile(profile) {
if (!this.cpuProfiler) {
throw new Error("Session not started");
}
return this.cpuProfiler.analyzeProfile(profile);
}
/**
* Take a heap snapshot for memory analysis
* @returns The heap snapshot data
*/
async takeHeapSnapshot() {
if (!this.memoryProfiler) {
throw new Error("Session not started");
}
return await this.memoryProfiler.takeHeapSnapshot();
}
/**
* Get current memory usage statistics
* @returns Memory usage information
*/
async getMemoryUsage() {
if (!this.memoryProfiler) {
throw new Error("Session not started");
}
return await this.memoryProfiler.getMemoryUsage();
}
/**
* Start tracking heap allocations over time
* @param samplingInterval Sampling interval in bytes
*/
async startTrackingHeapObjects(samplingInterval) {
if (!this.memoryProfiler) {
throw new Error("Session not started");
}
await this.memoryProfiler.startTrackingHeapObjects(samplingInterval);
}
/**
* Stop tracking heap allocations
* @returns The final heap snapshot
*/
async stopTrackingHeapObjects() {
if (!this.memoryProfiler) {
throw new Error("Session not started");
}
return await this.memoryProfiler.stopTrackingHeapObjects();
}
/**
* Detect memory leaks by analyzing heap growth over time
* @param durationMs Duration to monitor in milliseconds
* @param intervalMs Interval between snapshots in milliseconds
* @returns Memory leak analysis
*/
async detectMemoryLeaks(durationMs, intervalMs) {
if (!this.memoryProfiler) {
throw new Error("Session not started");
}
return await this.memoryProfiler.detectMemoryLeaks(durationMs, intervalMs);
}
/**
* Generate a memory usage report
* @param snapshot Optional heap snapshot to analyze
* @returns Memory usage report
*/
async generateMemoryReport(snapshot) {
if (!this.memoryProfiler) {
throw new Error("Session not started");
}
return await this.memoryProfiler.generateMemoryReport(snapshot);
}
/**
* Get the memory profiler instance
*/
getMemoryProfiler() {
return this.memoryProfiler;
}
/**
* Start recording performance events
*/
async startPerformanceRecording() {
if (!this.performanceTimeline) {
throw new Error("Session not started");
}
await this.performanceTimeline.startRecording();
}
/**
* Stop recording performance events and get the report
* @returns Performance report
*/
async stopPerformanceRecording() {
if (!this.performanceTimeline) {
throw new Error("Session not started");
}
return await this.performanceTimeline.stopRecording();
}
/**
* Check if performance recording is active
*/
isPerformanceRecording() {
return this.performanceTimeline?.isRecording() || false;
}
/**
* Record a function call timing in the performance timeline
* @param functionName Name of the function
* @param file File path
* @param line Line number
* @param duration Duration in microseconds
*/
recordFunctionCall(functionName, file, line, duration) {
if (!this.performanceTimeline) {
throw new Error("Session not started");
}
this.performanceTimeline.recordFunctionCall(functionName, file, line, duration);
}
/**
* Get the performance timeline instance
*/
getPerformanceTimeline() {
return this.performanceTimeline;
}
}
exports.DebugSession = DebugSession;
//# sourceMappingURL=debug-session.js.map