Skip to main content
Glama
session.ts13.6 kB
/** * Debug Session * Represents a single PHP debug session with all debugging operations. */ import { EventEmitter } from 'events'; import { DbgpConnection } from '../dbgp/connection.js'; import { Breakpoint, DebugStatus, Property, StackFrame, Context, InitPacket, HitCondition, DbgpResponse, } from '../dbgp/types.js'; import { logger } from '../utils/logger.js'; export interface SessionState { id: string; status: DebugStatus; filename?: string; lineno?: number; ideKey?: string; startTime: Date; } export class DebugSession extends EventEmitter { public readonly id: string; public status: DebugStatus = 'starting'; public currentFile?: string; public currentLine?: number; public readonly startTime: Date; private breakpoints: Map<string, Breakpoint> = new Map(); private initialized: boolean = false; constructor(private connection: DbgpConnection) { super(); this.id = connection.id; this.startTime = new Date(); // Update state on responses connection.on('response', (response: DbgpResponse) => { if (response.status) { this.status = response.status; } if (response.message) { this.currentFile = response.message.filename; this.currentLine = response.message.lineno; } this.emit('stateChange', this.getState()); }); connection.on('close', () => { this.status = 'stopped'; this.emit('close'); }); connection.on('stream', (data) => { this.emit('output', data); }); } get initPacket(): InitPacket | null { return this.connection.initPacket; } getState(): SessionState { return { id: this.id, status: this.status, filename: this.currentFile, lineno: this.currentLine, ideKey: this.initPacket?.ideKey, startTime: this.startTime, }; } // === Initialization === async initialize(): Promise<void> { if (this.initialized) return; try { // Set preferred features await this.setFeature('max_depth', '3'); await this.setFeature('max_children', '128'); await this.setFeature('max_data', '2048'); await this.setFeature('show_hidden', '1'); this.initialized = true; logger.debug(`Session ${this.id} initialized`); } catch (error) { logger.error(`Failed to initialize session ${this.id}:`, error); throw error; } } // === Feature Negotiation === async setFeature(name: string, value: string): Promise<boolean> { try { const response = await this.connection.sendCommand('feature_set', { n: name, v: value, }); return response.success === true; } catch { return false; } } async getFeature(name: string): Promise<string | null> { try { const response = await this.connection.sendCommand('feature_get', { n: name, }); const data = response.data as Record<string, string>; return data['@_supported'] === '1' ? data['#text'] || null : null; } catch { return null; } } // === Breakpoint Operations === async setLineBreakpoint( filename: string, line: number, options?: { condition?: string; hitValue?: number; hitCondition?: HitCondition; temporary?: boolean; } ): Promise<Breakpoint> { const args: Record<string, string> = { t: options?.condition ? 'conditional' : 'line', f: this.normalizeFileUri(filename), n: line.toString(), }; if (options?.hitValue !== undefined) { args['h'] = options.hitValue.toString(); } if (options?.hitCondition) { args['o'] = options.hitCondition; } if (options?.temporary) { args['r'] = '1'; } const response = await this.connection.sendCommand( 'breakpoint_set', args, options?.condition ); if (response.error) { throw new Error(`Failed to set breakpoint: ${response.error.message}`); } const result = this.connection.parseBreakpointSet(response); const breakpoint: Breakpoint = { id: result.id, type: options?.condition ? 'conditional' : 'line', state: 'enabled', resolved: result.resolved, filename, lineno: line, expression: options?.condition, hitValue: options?.hitValue, hitCondition: options?.hitCondition, }; this.breakpoints.set(breakpoint.id, breakpoint); logger.debug(`Breakpoint set: ${breakpoint.id} at ${filename}:${line}`); return breakpoint; } async setExceptionBreakpoint(exception: string = '*'): Promise<Breakpoint> { const response = await this.connection.sendCommand('breakpoint_set', { t: 'exception', x: exception, }); if (response.error) { throw new Error(`Failed to set exception breakpoint: ${response.error.message}`); } const result = this.connection.parseBreakpointSet(response); const breakpoint: Breakpoint = { id: result.id, type: 'exception', state: 'enabled', exception, }; this.breakpoints.set(breakpoint.id, breakpoint); return breakpoint; } async setCallBreakpoint(functionName: string): Promise<Breakpoint> { const response = await this.connection.sendCommand('breakpoint_set', { t: 'call', m: functionName, }); if (response.error) { throw new Error(`Failed to set call breakpoint: ${response.error.message}`); } const result = this.connection.parseBreakpointSet(response); const breakpoint: Breakpoint = { id: result.id, type: 'call', state: 'enabled', function: functionName, }; this.breakpoints.set(breakpoint.id, breakpoint); return breakpoint; } async removeBreakpoint(breakpointId: string): Promise<boolean> { const response = await this.connection.sendCommand('breakpoint_remove', { d: breakpointId, }); if (!response.error) { this.breakpoints.delete(breakpointId); logger.debug(`Breakpoint removed: ${breakpointId}`); return true; } return false; } async updateBreakpoint( breakpointId: string, options: { state?: 'enabled' | 'disabled'; hitValue?: number; hitCondition?: HitCondition; } ): Promise<boolean> { const args: Record<string, string> = { d: breakpointId }; if (options.state) args['s'] = options.state; if (options.hitValue !== undefined) args['h'] = options.hitValue.toString(); if (options.hitCondition) args['o'] = options.hitCondition; const response = await this.connection.sendCommand('breakpoint_update', args); return !response.error; } async getBreakpoint(breakpointId: string): Promise<Breakpoint | null> { const response = await this.connection.sendCommand('breakpoint_get', { d: breakpointId, }); if (response.error) return null; const breakpoints = this.connection.parseBreakpoints(response); return breakpoints[0] || null; } async listBreakpoints(): Promise<Breakpoint[]> { const response = await this.connection.sendCommand('breakpoint_list'); const breakpoints = this.connection.parseBreakpoints(response); // Update local cache this.breakpoints.clear(); for (const bp of breakpoints) { this.breakpoints.set(bp.id, bp); } return breakpoints; } // === Execution Control === async run(): Promise<{ status: DebugStatus; file?: string; line?: number }> { const response = await this.connection.sendCommand('run'); return this.handleStepResponse(response); } async stepInto(): Promise<{ status: DebugStatus; file?: string; line?: number }> { const response = await this.connection.sendCommand('step_into'); return this.handleStepResponse(response); } async stepOver(): Promise<{ status: DebugStatus; file?: string; line?: number }> { const response = await this.connection.sendCommand('step_over'); return this.handleStepResponse(response); } async stepOut(): Promise<{ status: DebugStatus; file?: string; line?: number }> { const response = await this.connection.sendCommand('step_out'); return this.handleStepResponse(response); } async stop(): Promise<void> { await this.connection.sendCommand('stop'); this.status = 'stopped'; } async detach(): Promise<void> { await this.connection.sendCommand('detach'); } private handleStepResponse(response: DbgpResponse): { status: DebugStatus; file?: string; line?: number; } { const status = response.status || 'break'; this.status = status; if (response.message) { this.currentFile = response.message.filename; this.currentLine = response.message.lineno; } return { status, file: this.currentFile, line: this.currentLine, }; } // === Stack Inspection === async getStackDepth(): Promise<number> { const response = await this.connection.sendCommand('stack_depth'); const data = response.data as Record<string, string>; return parseInt(data['@_depth'] || '0', 10); } async getStackTrace(depth?: number): Promise<StackFrame[]> { const args: Record<string, string> = {}; if (depth !== undefined) { args['d'] = depth.toString(); } const response = await this.connection.sendCommand('stack_get', args); return this.connection.parseStackFrames(response); } // === Context and Variables === async getContexts(stackDepth: number = 0): Promise<Context[]> { const response = await this.connection.sendCommand('context_names', { d: stackDepth.toString(), }); return this.connection.parseContexts(response); } async getVariables( contextId: number = 0, stackDepth: number = 0 ): Promise<Property[]> { const response = await this.connection.sendCommand('context_get', { c: contextId.toString(), d: stackDepth.toString(), }); return this.connection.parseProperties(response); } async getVariable( name: string, options?: { contextId?: number; stackDepth?: number; maxDepth?: number; page?: number; } ): Promise<Property | null> { const args: Record<string, string> = { n: name, }; if (options?.contextId !== undefined) { args['c'] = options.contextId.toString(); } if (options?.stackDepth !== undefined) { args['d'] = options.stackDepth.toString(); } if (options?.maxDepth !== undefined) { args['m'] = options.maxDepth.toString(); } if (options?.page !== undefined) { args['p'] = options.page.toString(); } const response = await this.connection.sendCommand('property_get', args); if (response.error) { return null; } return this.connection.parseProperty(response); } async setVariable( name: string, value: string, options?: { contextId?: number; stackDepth?: number; type?: string; } ): Promise<boolean> { const args: Record<string, string> = { n: name, }; if (options?.contextId !== undefined) { args['c'] = options.contextId.toString(); } if (options?.stackDepth !== undefined) { args['d'] = options.stackDepth.toString(); } if (options?.type) { args['t'] = options.type; } const response = await this.connection.sendCommand('property_set', args, value); return response.success === true || !response.error; } // === Expression Evaluation === async evaluate( expression: string, stackDepth: number = 0 ): Promise<Property | null> { const response = await this.connection.sendCommand( 'eval', { d: stackDepth.toString() }, expression ); if (response.error) { throw new Error(`Evaluation error: ${response.error.message}`); } return this.connection.parseProperty(response); } // === Source Code === async getSource( fileUri: string, beginLine?: number, endLine?: number ): Promise<string | null> { const args: Record<string, string> = { f: this.normalizeFileUri(fileUri), }; if (beginLine !== undefined) args['b'] = beginLine.toString(); if (endLine !== undefined) args['e'] = endLine.toString(); const response = await this.connection.sendCommand('source', args); if (response.error) return null; const data = response.data as Record<string, string>; const encoding = data['@_encoding']; const content = data['#text'] || ''; if (encoding === 'base64' && content) { return Buffer.from(content, 'base64').toString('utf8'); } return content; } // === Stream Redirection === async redirectStdout(mode: 'disable' | 'copy' | 'redirect'): Promise<boolean> { const modeMap = { disable: '0', copy: '1', redirect: '2' }; const response = await this.connection.sendCommand('stdout', { c: modeMap[mode], }); return response.success === true; } async redirectStderr(mode: 'disable' | 'copy' | 'redirect'): Promise<boolean> { const modeMap = { disable: '0', copy: '1', redirect: '2' }; const response = await this.connection.sendCommand('stderr', { c: modeMap[mode], }); return response.success === true; } // === Utilities === private normalizeFileUri(path: string): string { if (path.startsWith('file://')) { return path; } return `file://${path}`; } close(): void { this.connection.close(); } get isConnected(): boolean { return this.connection.isConnected; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kpanuragh/xdebug-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server