Files
rspade_system/app/RSpade/resource/vscode_extension/src/debug_client.ts
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 02:08:33 +00:00

233 lines
6.9 KiB
TypeScript
Executable File

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<void> {
this.log('Starting debug client...');
await this.connect();
}
private async connect(): Promise<void> {
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<void> {
// 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();
}
}