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>
233 lines
6.9 KiB
TypeScript
Executable File
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();
|
|
}
|
|
} |