session-manager.ts•11.9 kB
import { EventEmitter } from 'events';
import { Logger } from './logger';
import { ConfigManager } from './config';
import { SessionInfo } from '../schemas/session.schemas';
import { MetricsCollector } from './metrics';
import { DapStartRequest, DapAttachRequest } from '../schemas/dap-tools.schemas';
/**
* Session Manager
*
* Manages debugging sessions, handles capability negotiation,
* and ensures proper session lifecycle management.
*/
export class SessionManager extends EventEmitter {
private logger: Logger;
private config: ConfigManager;
private metrics: MetricsCollector;
private sessions: Map<string, SessionInfo> = new Map();
private nextSessionId: number = 1;
private sessionTimeouts: Map<string, NodeJS.Timeout> = new Map();
constructor() {
super();
this.logger = new Logger('SessionManager');
this.config = ConfigManager.getInstance();
this.metrics = MetricsCollector.getInstance();
this.setupSessionCleanup();
}
/**
* Initialize a new debugging session
*/
async initializeSession(config: any, capabilities: Record<string, any> = {}): Promise<string> {
this.logger.info('Initializing new debugging session');
this.metrics.startTimer('session.initialize');
const sessionId = this.generateSessionId();
const startTime = Date.now();
const session: SessionInfo = {
sessionId,
startTime,
status: 'initializing',
config,
capabilities,
currentThreadId: undefined,
currentFrame: undefined,
lastActivity: startTime,
createdAt: startTime,
updatedAt: startTime,
};
this.sessions.set(sessionId, session);
// Set session timeout
this.setSessionTimeout(sessionId);
this.logger.debug(`Session ${sessionId} initialized`);
this.emit('sessionCreated', session);
this.metrics.increment('sessions.total');
this.metrics.record('sessions.active', this.sessions.size);
return sessionId;
}
/**
* Activate a session
*/
async activateSession(sessionId: string, threadId?: number): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
session.status = 'active';
session.lastActivity = Date.now();
session.updatedAt = Date.now();
if (threadId) {
session.currentThreadId = threadId;
}
// Reset timeout
this.resetSessionTimeout(sessionId);
this.logger.info(`Session ${sessionId} activated`);
this.emit('sessionActivated', session);
this.metrics.increment('sessions.active', 1);
this.metrics.record('sessions.active', this.sessions.size);
}
/**
* Stop a session
*/
async stopSession(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
session.status = 'stopped';
session.lastActivity = Date.now();
session.updatedAt = Date.now();
this.clearSessionTimeout(sessionId);
this.sessions.set(sessionId, session);
this.logger.info(`Session ${sessionId} stopped`);
this.emit('sessionStopped', session);
this.metrics.increment('sessions.active', -1);
this.metrics.record('sessions.active', this.sessions.size);
}
/**
* Terminate a session
*/
async terminateSession(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
session.status = 'terminated';
session.lastActivity = Date.now();
session.updatedAt = Date.now();
this.clearSessionTimeout(sessionId);
this.sessions.delete(sessionId);
this.logger.info(`Session ${sessionId} terminated`);
this.emit('sessionTerminated', session);
this.metrics.increment('sessions.active', -1);
this.metrics.record('sessions.active', this.sessions.size);
}
/**
* Update session activity
*/
updateSessionActivity(sessionId: string, threadId?: number, frame?: any): void {
const session = this.sessions.get(sessionId);
if (!session) {
this.logger.warn(`Session ${sessionId} not found for activity update`);
return;
}
session.lastActivity = Date.now();
session.updatedAt = Date.now();
if (threadId !== undefined) {
session.currentThreadId = threadId;
}
if (frame) {
session.currentFrame = frame;
}
this.resetSessionTimeout(sessionId);
this.sessions.set(sessionId, session);
this.metrics.record('sessions.active', this.sessions.size);
}
/**
* Get session information
*/
getSession(sessionId: string): SessionInfo | undefined {
return this.sessions.get(sessionId);
}
/**
* Get all active sessions
*/
getActiveSessions(): SessionInfo[] {
return Array.from(this.sessions.values()).filter(
session => session.status === 'active' || session.status === 'initializing'
);
}
/**
* Get all sessions
*/
getAllSessions(): SessionInfo[] {
return Array.from(this.sessions.values());
}
/**
* Get session count
*/
getSessionCount(): number {
return this.sessions.size;
}
/**
* Check if session exists and is active
*/
isSessionActive(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
return session?.status === 'active';
}
/**
* Handle DAP launch request
*/
async handleLaunchRequest(
request: DapStartRequest
): Promise<{ sessionId: string; capabilities: Record<string, any> }> {
const capabilities = await this.negotiateCapabilities(request);
const sessionId = await this.initializeSession(request.arguments, capabilities);
this.logger.info(`Launch request received for session ${sessionId}`);
this.metrics.increment('dap.start.count');
return { sessionId, capabilities };
}
/**
* Handle DAP attach request
*/
async handleAttachRequest(
request: DapAttachRequest
): Promise<{ sessionId: string; capabilities: Record<string, any> }> {
const capabilities = await this.negotiateCapabilities(request);
const sessionId = await this.initializeSession(request.arguments, capabilities);
this.logger.info(`Attach request received for session ${sessionId}`);
this.metrics.increment('dap.attach.count');
return { sessionId, capabilities };
}
/**
* Negotiate capabilities with the debug adapter
*/
private async negotiateCapabilities(
request: DapStartRequest | DapAttachRequest
): Promise<Record<string, any>> {
const capabilities: Record<string, any> = {
supportsConfigurationDoneRequest: true,
supportsEvaluateForHovers: true,
supportsSetVariable: true,
supportsRestartRequest: true,
supportsStepBack: false,
supportsDataBreakpoints: false,
supportsInstructionBreakpoints: false,
supportsCompletionsQuery: false,
supportsModulesQuery: false,
supportsGotoTargetsRequest: false,
supportsLoadedSourcesRequest: false,
supportsTerminateDebuggee: true,
supportsTerminateRequest: true,
supportsDelayedStackTraceLoading: false,
supportsConditionalBreakpoints: true,
supportsHitConditionalBreakpoints: true,
supportsLogPoints: true,
supportsExceptionInfoRequest: true,
supportsSetExpression: false,
supportsTerminateThreadsRequest: false,
supportsSetFunctionBreakpoints: false,
};
// Language-specific capabilities
const config = request.arguments;
if (config.runtimeExecutable?.includes('node') || (config as any).program?.includes('node')) {
// Node.js specific capabilities
capabilities['supportsTerminateRequest'] = true;
capabilities['supportsExceptionOptions'] = true;
capabilities['supportsExceptionFilterOptions'] = false;
}
return capabilities;
}
/**
* Generate unique session ID
*/
private generateSessionId(): string {
return `session_${this.nextSessionId++}_${Date.now()}`;
}
/**
* Set session timeout
*/
private setSessionTimeout(sessionId: string): void {
const timeout = this.config.get('debugging.sessionTimeout', 3600000);
const timeoutId = setTimeout(() => {
this.handleSessionTimeout(sessionId);
}, timeout);
this.sessionTimeouts.set(sessionId, timeoutId);
}
/**
* Reset session timeout
*/
private resetSessionTimeout(sessionId: string): void {
if (this.sessionTimeouts.has(sessionId)) {
clearTimeout(this.sessionTimeouts.get(sessionId));
this.setSessionTimeout(sessionId);
}
}
/**
* Clear session timeout
*/
private clearSessionTimeout(sessionId: string): void {
if (this.sessionTimeouts.has(sessionId)) {
clearTimeout(this.sessionTimeouts.get(sessionId));
this.sessionTimeouts.delete(sessionId);
}
}
/**
* Handle session timeout
*/
private async handleSessionTimeout(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (session && (session.status === 'active' || session.status === 'initializing')) {
this.logger.warn(`Session ${sessionId} timed out, terminating`);
await this.terminateSession(sessionId);
this.emit('sessionTimeout', session);
}
}
/**
* Setup periodic session cleanup
*/
private setupSessionCleanup(): void {
// Check for inactive sessions every 5 minutes
setInterval(() => {
this.cleanupInactiveSessions();
}, 300000);
}
/**
* Clean up inactive sessions
*/
private cleanupInactiveSessions(): void {
const now = Date.now();
const timeout = this.config.get('debugging.sessionTimeout', 3600000);
const sessionsToTerminate: string[] = [];
for (const [sessionId, session] of Array.from(this.sessions.entries())) {
if (session.status === 'active' && now - session.lastActivity > timeout) {
sessionsToTerminate.push(sessionId);
}
}
if (sessionsToTerminate.length > 0) {
this.logger.info(`Cleaning up ${sessionsToTerminate.length} inactive sessions`);
sessionsToTerminate.forEach(sessionId => {
this.handleSessionTimeout(sessionId);
});
}
}
/**
* Shutdown all sessions
*/
async shutdown(): Promise<void> {
this.logger.info('Shutting down all sessions');
const sessionIds = Array.from(this.sessions.keys());
const terminationPromises = sessionIds.map(sessionId =>
this.terminateSession(sessionId).catch(error => {
this.logger.error(`Error terminating session ${sessionId}:`, error);
})
);
await Promise.all(terminationPromises);
this.clearAllTimeouts();
this.sessions.clear();
this.nextSessionId = 1;
this.logger.info('All sessions shut down');
this.emit('shutdownComplete');
}
/**
* Clear all timeouts
*/
private clearAllTimeouts(): void {
for (const timeoutId of Array.from(this.sessionTimeouts.values())) {
clearTimeout(timeoutId);
}
this.sessionTimeouts.clear();
}
/**
* Get session statistics
*/
getSessionStats(): {
total: number;
active: number;
initializing: number;
stopped: number;
terminated: number;
averageSessionTime: number;
} {
const sessions = Array.from(this.sessions.values());
const now = Date.now();
return {
total: sessions.length,
active: sessions.filter(s => s.status === 'active').length,
initializing: sessions.filter(s => s.status === 'initializing').length,
stopped: sessions.filter(s => s.status === 'stopped').length,
terminated: sessions.filter(s => s.status === 'terminated').length,
averageSessionTime:
sessions.length > 0
? sessions.reduce((sum, s) => sum + (now - s.startTime), 0) / sessions.length
: 0,
};
}
}