mirror of
https://github.com/actions/runner.git
synced 2026-01-16 08:42:55 +08:00
update extension and proxy for keepalive
This commit is contained in:
@@ -3,24 +3,187 @@
|
||||
*
|
||||
* Service worker that manages WebSocket connection to the proxy
|
||||
* and handles DAP protocol communication.
|
||||
*
|
||||
* NOTE: Chrome MV3 service workers can be terminated after ~30s of inactivity.
|
||||
* We handle this with:
|
||||
* 1. Keepalive pings to keep the WebSocket active
|
||||
* 2. Automatic reconnection when the service worker restarts
|
||||
* 3. Storing connection state in chrome.storage.session
|
||||
*/
|
||||
|
||||
// Connection state
|
||||
let ws = null;
|
||||
let connectionStatus = 'disconnected'; // disconnected, connecting, connected, paused, running
|
||||
let connectionStatus = 'disconnected'; // disconnected, connecting, connected, paused, running, error
|
||||
let sequenceNumber = 1;
|
||||
const pendingRequests = new Map(); // seq -> { resolve, reject, command }
|
||||
const pendingRequests = new Map(); // seq -> { resolve, reject, command, timeout }
|
||||
|
||||
// Reconnection state
|
||||
let reconnectAttempts = 0;
|
||||
const MAX_RECONNECT_ATTEMPTS = 10;
|
||||
const RECONNECT_BASE_DELAY = 1000; // Start with 1s, exponential backoff
|
||||
let reconnectTimer = null;
|
||||
let lastConnectedUrl = null;
|
||||
let wasConnectedBeforeIdle = false;
|
||||
|
||||
// Keepalive interval - send ping every 15s to keep service worker AND WebSocket alive
|
||||
// Chrome MV3 service workers get suspended after ~30s of inactivity
|
||||
// We need to send actual WebSocket messages to keep both alive
|
||||
const KEEPALIVE_INTERVAL = 15000;
|
||||
let keepaliveTimer = null;
|
||||
|
||||
// Default configuration
|
||||
const DEFAULT_URL = 'ws://localhost:4712';
|
||||
|
||||
/**
|
||||
* Initialize on service worker startup - check if we should reconnect
|
||||
*/
|
||||
async function initializeOnStartup() {
|
||||
console.log('[Background] Service worker starting up...');
|
||||
|
||||
try {
|
||||
// Restore state from session storage
|
||||
const data = await chrome.storage.session.get(['connectionUrl', 'shouldBeConnected', 'lastStatus']);
|
||||
|
||||
if (data.shouldBeConnected && data.connectionUrl) {
|
||||
console.log('[Background] Restoring connection after service worker restart');
|
||||
lastConnectedUrl = data.connectionUrl;
|
||||
wasConnectedBeforeIdle = true;
|
||||
|
||||
// Small delay to let things settle
|
||||
setTimeout(() => {
|
||||
connect(data.connectionUrl);
|
||||
}, 500);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Background] No session state to restore');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save connection state to session storage (survives service worker restart)
|
||||
*/
|
||||
async function saveConnectionState() {
|
||||
try {
|
||||
await chrome.storage.session.set({
|
||||
connectionUrl: lastConnectedUrl,
|
||||
shouldBeConnected: connectionStatus !== 'disconnected' && connectionStatus !== 'error',
|
||||
lastStatus: connectionStatus,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[Background] Failed to save connection state:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear connection state from session storage
|
||||
*/
|
||||
async function clearConnectionState() {
|
||||
try {
|
||||
await chrome.storage.session.remove(['connectionUrl', 'shouldBeConnected', 'lastStatus']);
|
||||
} catch (e) {
|
||||
console.warn('[Background] Failed to clear connection state:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start keepalive ping to prevent service worker termination
|
||||
* CRITICAL: We must send actual WebSocket messages to keep the connection alive.
|
||||
* Just having a timer is not enough - Chrome will suspend the service worker
|
||||
* and close the WebSocket with code 1001 after ~30s of inactivity.
|
||||
*/
|
||||
function startKeepalive() {
|
||||
stopKeepalive();
|
||||
|
||||
keepaliveTimer = setInterval(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
// Send a lightweight keepalive message over WebSocket
|
||||
// This does two things:
|
||||
// 1. Keeps the WebSocket connection active (prevents proxy timeout)
|
||||
// 2. Creates activity that keeps the Chrome service worker alive
|
||||
const keepaliveMsg = JSON.stringify({ type: 'keepalive', timestamp: Date.now() });
|
||||
ws.send(keepaliveMsg);
|
||||
console.log('[Background] Keepalive sent');
|
||||
} catch (e) {
|
||||
console.error('[Background] Keepalive error:', e);
|
||||
handleUnexpectedClose();
|
||||
}
|
||||
} else if (wasConnectedBeforeIdle || lastConnectedUrl) {
|
||||
// Connection was lost, try to reconnect
|
||||
console.log('[Background] Connection lost during keepalive check');
|
||||
handleUnexpectedClose();
|
||||
}
|
||||
}, KEEPALIVE_INTERVAL);
|
||||
|
||||
console.log('[Background] Keepalive timer started (interval: ' + KEEPALIVE_INTERVAL + 'ms)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop keepalive ping
|
||||
*/
|
||||
function stopKeepalive() {
|
||||
if (keepaliveTimer) {
|
||||
clearInterval(keepaliveTimer);
|
||||
keepaliveTimer = null;
|
||||
console.log('[Background] Keepalive timer stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unexpected connection close - attempt reconnection
|
||||
*/
|
||||
function handleUnexpectedClose() {
|
||||
if (reconnectTimer) {
|
||||
return; // Already trying to reconnect
|
||||
}
|
||||
|
||||
if (!lastConnectedUrl) {
|
||||
console.log('[Background] No URL to reconnect to');
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
console.error('[Background] Max reconnection attempts reached');
|
||||
connectionStatus = 'error';
|
||||
broadcastStatus();
|
||||
clearConnectionState();
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = Math.min(RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts), 30000);
|
||||
reconnectAttempts++;
|
||||
|
||||
console.log(`[Background] Scheduling reconnect attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${delay}ms`);
|
||||
connectionStatus = 'connecting';
|
||||
broadcastStatus();
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectTimer = null;
|
||||
if (connectionStatus !== 'connected' && connectionStatus !== 'paused' && connectionStatus !== 'running') {
|
||||
connect(lastConnectedUrl);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the WebSocket proxy
|
||||
*/
|
||||
function connect(url) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
console.log('[Background] Already connected');
|
||||
return;
|
||||
// Clean up existing connection
|
||||
if (ws) {
|
||||
try {
|
||||
ws.onclose = null; // Prevent triggering reconnect
|
||||
ws.close(1000, 'Reconnecting');
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
ws = null;
|
||||
}
|
||||
|
||||
// Clear any pending reconnect
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
connectionStatus = 'connecting';
|
||||
@@ -28,22 +191,35 @@ function connect(url) {
|
||||
|
||||
// Use provided URL or default
|
||||
const wsUrl = url || DEFAULT_URL;
|
||||
lastConnectedUrl = wsUrl;
|
||||
console.log(`[Background] Connecting to ${wsUrl}`);
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
} catch (e) {
|
||||
console.error('[Background] Failed to create WebSocket:', e);
|
||||
connectionStatus = 'error';
|
||||
broadcastStatus();
|
||||
handleUnexpectedClose();
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = async () => {
|
||||
console.log('[Background] WebSocket connected');
|
||||
connectionStatus = 'connected';
|
||||
reconnectAttempts = 0; // Reset on successful connection
|
||||
wasConnectedBeforeIdle = true;
|
||||
broadcastStatus();
|
||||
saveConnectionState();
|
||||
startKeepalive();
|
||||
|
||||
// Initialize DAP session
|
||||
try {
|
||||
await initializeDapSession();
|
||||
} catch (error) {
|
||||
console.error('[Background] Failed to initialize DAP session:', error);
|
||||
connectionStatus = 'error';
|
||||
broadcastStatus();
|
||||
// Don't set error status - the connection might still be usable
|
||||
// The DAP server might just need the job to progress
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,22 +233,40 @@ function connect(url) {
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log(`[Background] WebSocket closed: ${event.code} ${event.reason}`);
|
||||
console.log(`[Background] WebSocket closed: ${event.code} ${event.reason || '(no reason)'}`);
|
||||
ws = null;
|
||||
connectionStatus = 'disconnected';
|
||||
broadcastStatus();
|
||||
stopKeepalive();
|
||||
|
||||
// Reject any pending requests
|
||||
for (const [seq, pending] of pendingRequests) {
|
||||
if (pending.timeout) clearTimeout(pending.timeout);
|
||||
pending.reject(new Error('Connection closed'));
|
||||
}
|
||||
pendingRequests.clear();
|
||||
|
||||
// Determine if we should reconnect
|
||||
// Code 1000 = normal closure (user initiated)
|
||||
// Code 1001 = going away (service worker idle, browser closing, etc.)
|
||||
// Code 1006 = abnormal closure (connection lost)
|
||||
// Code 1011 = server error
|
||||
const shouldReconnect = event.code !== 1000;
|
||||
|
||||
if (shouldReconnect && wasConnectedBeforeIdle) {
|
||||
console.log('[Background] Unexpected close, will attempt reconnect');
|
||||
connectionStatus = 'connecting';
|
||||
broadcastStatus();
|
||||
handleUnexpectedClose();
|
||||
} else {
|
||||
connectionStatus = 'disconnected';
|
||||
wasConnectedBeforeIdle = false;
|
||||
broadcastStatus();
|
||||
clearConnectionState();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error('[Background] WebSocket error:', event);
|
||||
connectionStatus = 'error';
|
||||
broadcastStatus();
|
||||
// onclose will be called after onerror, so we handle reconnection there
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,14 +274,34 @@ function connect(url) {
|
||||
* Disconnect from the WebSocket proxy
|
||||
*/
|
||||
function disconnect() {
|
||||
// Stop any reconnection attempts
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
reconnectAttempts = 0;
|
||||
wasConnectedBeforeIdle = false;
|
||||
stopKeepalive();
|
||||
|
||||
if (ws) {
|
||||
// Send disconnect request to DAP server first
|
||||
sendDapRequest('disconnect', {}).catch(() => {});
|
||||
ws.close(1000, 'User disconnected');
|
||||
|
||||
// Prevent reconnection on this close
|
||||
const socket = ws;
|
||||
ws = null;
|
||||
socket.onclose = null;
|
||||
|
||||
try {
|
||||
socket.close(1000, 'User disconnected');
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
connectionStatus = 'disconnected';
|
||||
broadcastStatus();
|
||||
clearConnectionState();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,17 +353,24 @@ function sendDapRequest(command, args = {}) {
|
||||
};
|
||||
|
||||
console.log(`[Background] Sending DAP request: ${command} (seq: ${seq})`);
|
||||
pendingRequests.set(seq, { resolve, reject, command });
|
||||
|
||||
// Set timeout for request
|
||||
setTimeout(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (pendingRequests.has(seq)) {
|
||||
pendingRequests.delete(seq);
|
||||
reject(new Error(`Request timed out: ${command}`));
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
ws.send(JSON.stringify(request));
|
||||
pendingRequests.set(seq, { resolve, reject, command, timeout });
|
||||
|
||||
try {
|
||||
ws.send(JSON.stringify(request));
|
||||
} catch (e) {
|
||||
pendingRequests.delete(seq);
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Failed to send request: ${e.message}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,8 +384,10 @@ function handleDapMessage(message) {
|
||||
handleDapEvent(message);
|
||||
} else if (message.type === 'proxy-error') {
|
||||
console.error('[Background] Proxy error:', message.message);
|
||||
connectionStatus = 'error';
|
||||
broadcastStatus();
|
||||
// Don't immediately set error status - might be transient
|
||||
} else if (message.type === 'keepalive-ack') {
|
||||
// Keepalive acknowledged by proxy - connection is healthy
|
||||
console.log('[Background] Keepalive acknowledged');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +402,7 @@ function handleDapResponse(response) {
|
||||
}
|
||||
|
||||
pendingRequests.delete(response.request_seq);
|
||||
if (pending.timeout) clearTimeout(pending.timeout);
|
||||
|
||||
if (response.success) {
|
||||
console.log(`[Background] DAP response success: ${response.command}`);
|
||||
@@ -203,16 +427,20 @@ function handleDapEvent(event) {
|
||||
case 'stopped':
|
||||
connectionStatus = 'paused';
|
||||
broadcastStatus();
|
||||
saveConnectionState();
|
||||
break;
|
||||
|
||||
case 'continued':
|
||||
connectionStatus = 'running';
|
||||
broadcastStatus();
|
||||
saveConnectionState();
|
||||
break;
|
||||
|
||||
case 'terminated':
|
||||
connectionStatus = 'disconnected';
|
||||
wasConnectedBeforeIdle = false;
|
||||
broadcastStatus();
|
||||
clearConnectionState();
|
||||
break;
|
||||
|
||||
case 'output':
|
||||
@@ -228,13 +456,16 @@ function handleDapEvent(event) {
|
||||
* Broadcast connection status to popup and content scripts
|
||||
*/
|
||||
function broadcastStatus() {
|
||||
// Broadcast to all extension contexts
|
||||
chrome.runtime.sendMessage({ type: 'status-changed', status: connectionStatus }).catch(() => {});
|
||||
const statusMessage = { type: 'status-changed', status: connectionStatus };
|
||||
|
||||
// Broadcast to all extension contexts (popup)
|
||||
chrome.runtime.sendMessage(statusMessage).catch(() => {});
|
||||
|
||||
// Broadcast to content scripts
|
||||
chrome.tabs.query({ url: 'https://github.com/*/*/actions/runs/*/job/*' }, (tabs) => {
|
||||
if (chrome.runtime.lastError) return;
|
||||
tabs.forEach((tab) => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'status-changed', status: connectionStatus }).catch(() => {});
|
||||
chrome.tabs.sendMessage(tab.id, statusMessage).catch(() => {});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -244,6 +475,7 @@ function broadcastStatus() {
|
||||
*/
|
||||
function broadcastEvent(event) {
|
||||
chrome.tabs.query({ url: 'https://github.com/*/*/actions/runs/*/job/*' }, (tabs) => {
|
||||
if (chrome.runtime.lastError) return;
|
||||
tabs.forEach((tab) => {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'dap-event', event }).catch(() => {});
|
||||
});
|
||||
@@ -258,10 +490,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
|
||||
switch (message.type) {
|
||||
case 'get-status':
|
||||
sendResponse({ status: connectionStatus });
|
||||
sendResponse({ status: connectionStatus, reconnecting: reconnectTimer !== null });
|
||||
return false;
|
||||
|
||||
case 'connect':
|
||||
reconnectAttempts = 0; // Reset attempts on manual connect
|
||||
connect(message.url || DEFAULT_URL);
|
||||
sendResponse({ status: connectionStatus });
|
||||
return false;
|
||||
@@ -288,5 +521,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize on startup
|
||||
initializeOnStartup();
|
||||
|
||||
// Log startup
|
||||
console.log('[Background] Actions DAP Debugger background script loaded');
|
||||
|
||||
@@ -279,30 +279,17 @@ function appendOutput(text, type) {
|
||||
const output = document.querySelector('.dap-repl-output');
|
||||
if (!output) return;
|
||||
|
||||
const line = document.createElement('div');
|
||||
line.className = `dap-output-${type}`;
|
||||
if (type === 'error') line.classList.add('color-fg-danger');
|
||||
if (type === 'input') line.classList.add('color-fg-muted');
|
||||
|
||||
// Handle multi-line output
|
||||
// Handle multi-line output - each line gets its own div
|
||||
const lines = text.split('\n');
|
||||
lines.forEach((l, i) => {
|
||||
if (i > 0) {
|
||||
output.appendChild(document.createElement('br'));
|
||||
}
|
||||
const span = document.createElement('span');
|
||||
span.textContent = l;
|
||||
if (i === 0) {
|
||||
span.className = line.className;
|
||||
}
|
||||
output.appendChild(span);
|
||||
lines.forEach((l) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `dap-output-${type}`;
|
||||
if (type === 'error') div.classList.add('color-fg-danger');
|
||||
if (type === 'input') div.classList.add('color-fg-muted');
|
||||
div.textContent = l;
|
||||
output.appendChild(div);
|
||||
});
|
||||
|
||||
if (lines.length === 1) {
|
||||
line.textContent = text;
|
||||
output.appendChild(line);
|
||||
}
|
||||
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Get current status from background
|
||||
chrome.runtime.sendMessage({ type: 'get-status' }, (response) => {
|
||||
if (response && response.status) {
|
||||
updateStatusUI(response.status);
|
||||
if (response) {
|
||||
updateStatusUI(response.status, response.reconnecting);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for status changes
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === 'status-changed') {
|
||||
updateStatusUI(message.status);
|
||||
updateStatusUI(message.status, message.reconnecting);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -60,15 +60,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
/**
|
||||
* Update the UI to reflect current status
|
||||
*/
|
||||
function updateStatusUI(status) {
|
||||
function updateStatusUI(status, reconnecting = false) {
|
||||
// Update text
|
||||
const statusNames = {
|
||||
disconnected: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
connecting: reconnecting ? 'Reconnecting...' : 'Connecting...',
|
||||
connected: 'Connected',
|
||||
paused: 'Paused',
|
||||
running: 'Running',
|
||||
error: 'Error',
|
||||
error: 'Connection Error',
|
||||
};
|
||||
statusText.textContent = statusNames[status] || status;
|
||||
|
||||
@@ -80,11 +80,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const isConnecting = status === 'connecting';
|
||||
|
||||
connectBtn.disabled = isConnected || isConnecting;
|
||||
disconnectBtn.disabled = !isConnected;
|
||||
disconnectBtn.disabled = status === 'disconnected';
|
||||
|
||||
// Update connect button text
|
||||
if (isConnecting) {
|
||||
connectBtn.textContent = 'Connecting...';
|
||||
connectBtn.textContent = reconnecting ? 'Reconnecting...' : 'Connecting...';
|
||||
} else {
|
||||
connectBtn.textContent = 'Connect';
|
||||
}
|
||||
|
||||
@@ -36,14 +36,38 @@ console.log(`[Proxy] Starting WebSocket-to-TCP proxy`);
|
||||
console.log(`[Proxy] WebSocket: ws://localhost:${config.wsPort}`);
|
||||
console.log(`[Proxy] DAP Server: tcp://${config.dapHost}:${config.dapPort}`);
|
||||
|
||||
const wss = new WebSocket.Server({ port: config.wsPort });
|
||||
const wss = new WebSocket.Server({
|
||||
port: config.wsPort,
|
||||
// Enable ping/pong for connection health checks
|
||||
clientTracking: true,
|
||||
});
|
||||
|
||||
console.log(`[Proxy] WebSocket server listening on port ${config.wsPort}`);
|
||||
|
||||
// Ping all clients every 25 seconds to detect dead connections
|
||||
// This is shorter than Chrome's service worker timeout (~30s)
|
||||
const PING_INTERVAL = 25000;
|
||||
const pingInterval = setInterval(() => {
|
||||
wss.clients.forEach((ws) => {
|
||||
if (ws.isAlive === false) {
|
||||
console.log(`[Proxy] Client failed to respond to ping, terminating`);
|
||||
return ws.terminate();
|
||||
}
|
||||
ws.isAlive = false;
|
||||
ws.ping();
|
||||
});
|
||||
}, PING_INTERVAL);
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const clientId = `${req.socket.remoteAddress}:${req.socket.remotePort}`;
|
||||
console.log(`[Proxy] WebSocket client connected: ${clientId}`);
|
||||
|
||||
// Mark as alive for ping/pong tracking
|
||||
ws.isAlive = true;
|
||||
ws.on('pong', () => {
|
||||
ws.isAlive = true;
|
||||
});
|
||||
|
||||
// Connect to DAP TCP server
|
||||
const tcp = net.createConnection({
|
||||
host: config.dapHost,
|
||||
@@ -80,15 +104,26 @@ wss.on('connection', (ws, req) => {
|
||||
|
||||
// WebSocket → TCP: Add Content-Length framing
|
||||
ws.on('message', (data) => {
|
||||
if (!tcpConnected) {
|
||||
console.warn(`[Proxy] TCP not connected, dropping message`);
|
||||
return;
|
||||
}
|
||||
|
||||
const json = data.toString();
|
||||
try {
|
||||
// Validate it's valid JSON
|
||||
const parsed = JSON.parse(json);
|
||||
|
||||
// Handle keepalive messages from the browser extension - don't forward to DAP server
|
||||
if (parsed.type === 'keepalive') {
|
||||
console.log(`[Proxy] Keepalive received from client`);
|
||||
// Respond with a keepalive-ack to confirm the connection is alive
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'keepalive-ack', timestamp: Date.now() }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tcpConnected) {
|
||||
console.warn(`[Proxy] TCP not connected, dropping message`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Proxy] WS→TCP: ${parsed.command || parsed.event || 'message'}`);
|
||||
|
||||
// Add DAP framing
|
||||
@@ -163,6 +198,7 @@ wss.on('error', (err) => {
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log(`\n[Proxy] Shutting down...`);
|
||||
clearInterval(pingInterval);
|
||||
wss.clients.forEach((ws) => ws.close(1001, 'Server shutting down'));
|
||||
wss.close(() => {
|
||||
console.log(`[Proxy] Goodbye!`);
|
||||
|
||||
Reference in New Issue
Block a user