import * as vscode from 'vscode'; import * as crypto from 'crypto'; import { RspadeFormattingProvider } from './formatting_provider'; // Use the global WebSocket available in VS Code extension host declare const WebSocket: any; interface WebSocketMessage { type: string; data?: any; timestamp?: number; } export class DebugClient { private ws: any = null; // WebSocket instance private outputChannel: vscode.OutputChannel; private formattingProvider: RspadeFormattingProvider; private isConnecting: boolean = false; private reconnectTimer: NodeJS.Timer | null = null; private pingTimer: NodeJS.Timer | null = null; private sessionId: string | null = null; private serverKey: string | null = null; constructor(formattingProvider: RspadeFormattingProvider) { this.formattingProvider = formattingProvider; this.outputChannel = vscode.window.createOutputChannel('RSPade Debug Proxy'); this.outputChannel.show(); this.log('Debug client initialized'); } public async start(): Promise { this.log('Starting debug client...'); await this.connect(); } private async connect(): Promise { if (this.isConnecting || this.ws?.readyState === WebSocket.OPEN) { return; } this.isConnecting = true; try { // Get authentication from formatting provider await this.ensureAuthenticated(); const serverUrl = await this.formattingProvider.get_server_url(); if (!serverUrl) { throw new Error('No server URL configured'); } // Parse URL and construct WebSocket URL const url = new URL(serverUrl); const wsProtocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${url.host}/_ide/debug/ws`; this.log(`Connecting to WebSocket: ${wsUrl}`); // Create WebSocket (standard API doesn't support headers in constructor) // We'll send auth after connection this.ws = new WebSocket(wsUrl); this.setupEventHandlers(); } catch (error: any) { this.log(`Connection failed: ${error.message}`); this.isConnecting = false; this.scheduleReconnect(); } } private setupEventHandlers(): void { if (!this.ws) return; this.ws.onopen = () => { this.isConnecting = false; this.log('WebSocket connected, sending authentication...'); // Send authentication as first message const signature = crypto .createHmac('sha256', this.serverKey!) .update(this.sessionId!) .digest('hex'); this.sendMessage({ type: 'auth', data: { sessionId: this.sessionId, signature: signature } }); // Send initial hello message after auth setTimeout(() => { this.sendMessage({ type: 'hello', data: { name: 'VS Code Debug Client' } }); // Start ping timer this.startPingTimer(); }, 100); }; this.ws.onmessage = (event: any) => { try { const message = JSON.parse(event.data) as WebSocketMessage; this.handleMessage(message); } catch (error) { this.log(`Failed to parse message: ${error}`); } }; this.ws.onclose = () => { this.log('WebSocket disconnected'); this.ws = null; this.stopPingTimer(); this.scheduleReconnect(); }; this.ws.onerror = (error: any) => { this.log(`WebSocket error: ${error}`); }; } private handleMessage(message: WebSocketMessage): void { this.log(`Received: ${message.type}`, message.data); switch (message.type) { case 'welcome': this.log('✅ Authentication successful! Connected to debug proxy'); this.log(`Session ID: ${message.data?.sessionId}`); break; case 'pong': this.log(`PONG received! Server responded to ping`); break; case 'hello_response': this.log(`Server says: ${message.data?.message}`); break; case 'error': this.log(`❌ Error: ${message.data?.message}`); break; default: this.log(`Unknown message type: ${message.type}`); } } private sendMessage(message: WebSocketMessage): void { if (this.ws?.readyState === 1) { // 1 = OPEN in standard WebSocket API this.ws.send(JSON.stringify(message)); this.log(`Sent: ${message.type}`, message.data); } } private startPingTimer(): void { this.stopPingTimer(); // Send ping every 5 seconds this.pingTimer = setInterval(() => { this.sendMessage({ type: 'ping', data: { timestamp: Date.now() } }); this.log('PING sent to server'); }, 5000); } private stopPingTimer(): void { if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = null; } } private scheduleReconnect(): void { if (this.reconnectTimer) { return; } this.log('Scheduling reconnection in 5 seconds...'); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connect(); }, 5000); } private async ensureAuthenticated(): Promise { // Get auth data from formatting provider const authData = await this.formattingProvider.ensure_auth(); if (!authData) { throw new Error('Failed to authenticate'); } // Extract session ID and server key this.sessionId = authData.session_id; this.serverKey = authData.server_key; if (!this.sessionId || !this.serverKey) { throw new Error('Invalid auth data'); } } private log(message: string, data?: any): void { const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}`; if (data) { this.outputChannel.appendLine(`${logMessage}\n${JSON.stringify(data, null, 2)}`); } else { this.outputChannel.appendLine(logMessage); } } public dispose(): void { this.stopPingTimer(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.outputChannel.dispose(); } }