Files
runner/browser-ext/background/background.js
Francesco Renzi 15b7034088 wip extension
2026-01-15 21:16:55 +00:00

293 lines
7.6 KiB
JavaScript

/**
* Background Script - DAP Client
*
* Service worker that manages WebSocket connection to the proxy
* and handles DAP protocol communication.
*/
// Connection state
let ws = null;
let connectionStatus = 'disconnected'; // disconnected, connecting, connected, paused, running
let sequenceNumber = 1;
const pendingRequests = new Map(); // seq -> { resolve, reject, command }
// Default configuration
const DEFAULT_URL = 'ws://localhost:4712';
/**
* Connect to the WebSocket proxy
*/
function connect(url) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('[Background] Already connected');
return;
}
connectionStatus = 'connecting';
broadcastStatus();
// Use provided URL or default
const wsUrl = url || DEFAULT_URL;
console.log(`[Background] Connecting to ${wsUrl}`);
ws = new WebSocket(wsUrl);
ws.onopen = async () => {
console.log('[Background] WebSocket connected');
connectionStatus = 'connected';
broadcastStatus();
// Initialize DAP session
try {
await initializeDapSession();
} catch (error) {
console.error('[Background] Failed to initialize DAP session:', error);
connectionStatus = 'error';
broadcastStatus();
}
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
handleDapMessage(message);
} catch (error) {
console.error('[Background] Failed to parse message:', error);
}
};
ws.onclose = (event) => {
console.log(`[Background] WebSocket closed: ${event.code} ${event.reason}`);
ws = null;
connectionStatus = 'disconnected';
broadcastStatus();
// Reject any pending requests
for (const [seq, pending] of pendingRequests) {
pending.reject(new Error('Connection closed'));
}
pendingRequests.clear();
};
ws.onerror = (event) => {
console.error('[Background] WebSocket error:', event);
connectionStatus = 'error';
broadcastStatus();
};
}
/**
* Disconnect from the WebSocket proxy
*/
function disconnect() {
if (ws) {
// Send disconnect request to DAP server first
sendDapRequest('disconnect', {}).catch(() => {});
ws.close(1000, 'User disconnected');
ws = null;
}
connectionStatus = 'disconnected';
broadcastStatus();
}
/**
* Initialize DAP session (initialize + attach + configurationDone)
*/
async function initializeDapSession() {
// 1. Initialize
const initResponse = await sendDapRequest('initialize', {
clientID: 'browser-extension',
clientName: 'Actions DAP Debugger',
adapterID: 'github-actions-runner',
pathFormat: 'path',
linesStartAt1: true,
columnsStartAt1: true,
supportsVariableType: true,
supportsVariablePaging: true,
supportsRunInTerminalRequest: false,
supportsProgressReporting: false,
supportsInvalidatedEvent: true,
});
console.log('[Background] Initialize response:', initResponse);
// 2. Attach to running session
const attachResponse = await sendDapRequest('attach', {});
console.log('[Background] Attach response:', attachResponse);
// 3. Configuration done
const configResponse = await sendDapRequest('configurationDone', {});
console.log('[Background] ConfigurationDone response:', configResponse);
}
/**
* Send a DAP request and return a promise for the response
*/
function sendDapRequest(command, args = {}) {
return new Promise((resolve, reject) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
reject(new Error('Not connected'));
return;
}
const seq = sequenceNumber++;
const request = {
seq,
type: 'request',
command,
arguments: args,
};
console.log(`[Background] Sending DAP request: ${command} (seq: ${seq})`);
pendingRequests.set(seq, { resolve, reject, command });
// Set timeout for request
setTimeout(() => {
if (pendingRequests.has(seq)) {
pendingRequests.delete(seq);
reject(new Error(`Request timed out: ${command}`));
}
}, 30000);
ws.send(JSON.stringify(request));
});
}
/**
* Handle incoming DAP message (response or event)
*/
function handleDapMessage(message) {
if (message.type === 'response') {
handleDapResponse(message);
} else if (message.type === 'event') {
handleDapEvent(message);
} else if (message.type === 'proxy-error') {
console.error('[Background] Proxy error:', message.message);
connectionStatus = 'error';
broadcastStatus();
}
}
/**
* Handle DAP response
*/
function handleDapResponse(response) {
const pending = pendingRequests.get(response.request_seq);
if (!pending) {
console.warn(`[Background] No pending request for seq ${response.request_seq}`);
return;
}
pendingRequests.delete(response.request_seq);
if (response.success) {
console.log(`[Background] DAP response success: ${response.command}`);
pending.resolve(response.body || {});
} else {
console.error(`[Background] DAP response error: ${response.command} - ${response.message}`);
pending.reject(new Error(response.message || 'Unknown error'));
}
}
/**
* Handle DAP event
*/
function handleDapEvent(event) {
console.log(`[Background] DAP event: ${event.event}`, event.body);
switch (event.event) {
case 'initialized':
// DAP server is ready
break;
case 'stopped':
connectionStatus = 'paused';
broadcastStatus();
break;
case 'continued':
connectionStatus = 'running';
broadcastStatus();
break;
case 'terminated':
connectionStatus = 'disconnected';
broadcastStatus();
break;
case 'output':
// Output event - forward to content scripts
break;
}
// Broadcast event to all content scripts
broadcastEvent(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(() => {});
// Broadcast to content scripts
chrome.tabs.query({ url: 'https://github.com/*/*/actions/runs/*/job/*' }, (tabs) => {
tabs.forEach((tab) => {
chrome.tabs.sendMessage(tab.id, { type: 'status-changed', status: connectionStatus }).catch(() => {});
});
});
}
/**
* Broadcast DAP event to content scripts
*/
function broadcastEvent(event) {
chrome.tabs.query({ url: 'https://github.com/*/*/actions/runs/*/job/*' }, (tabs) => {
tabs.forEach((tab) => {
chrome.tabs.sendMessage(tab.id, { type: 'dap-event', event }).catch(() => {});
});
});
}
/**
* Message handler for requests from popup and content scripts
*/
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[Background] Received message:', message.type);
switch (message.type) {
case 'get-status':
sendResponse({ status: connectionStatus });
return false;
case 'connect':
connect(message.url || DEFAULT_URL);
sendResponse({ status: connectionStatus });
return false;
case 'disconnect':
disconnect();
sendResponse({ status: connectionStatus });
return false;
case 'dap-request':
// Handle DAP request from content script
sendDapRequest(message.command, message.args || {})
.then((body) => {
sendResponse({ success: true, body });
})
.catch((error) => {
sendResponse({ success: false, error: error.message });
});
return true; // Will respond asynchronously
default:
console.warn('[Background] Unknown message type:', message.type);
return false;
}
});
// Log startup
console.log('[Background] Actions DAP Debugger background script loaded');