@dynamicu/chromedebug-mcp 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- package/src/validation/schemas.js +391 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserDaemon - Shared Chrome Browser Instance Manager
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized Chrome browser management with enhanced security isolation.
|
|
5
|
+
* Multiple Claude instances can share a single Chrome browser while maintaining
|
|
6
|
+
* complete profile isolation and secure session management.
|
|
7
|
+
*
|
|
8
|
+
* Key Features:
|
|
9
|
+
* - Single shared Chrome browser with multiple isolated profiles
|
|
10
|
+
* - Session-based security isolation (no data leakage between sessions)
|
|
11
|
+
* - Automatic health monitoring and recovery
|
|
12
|
+
* - Graceful fallback to individual browsers on daemon issues
|
|
13
|
+
* - Resource limits to prevent DoS attacks
|
|
14
|
+
* - Persistent session registry with atomic operations
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import puppeteer from 'puppeteer';
|
|
18
|
+
import { randomUUID } from 'crypto';
|
|
19
|
+
import { promises as fs } from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import os from 'os';
|
|
22
|
+
import { ProfileManager } from './profile-manager.js';
|
|
23
|
+
import { PersistentRegistry } from './session-registry.js';
|
|
24
|
+
import { FailoverManager } from './failover-manager.js';
|
|
25
|
+
import { findAvailablePort } from '../utils.js';
|
|
26
|
+
import { getExtensionPath } from '../utils/extension-path.js';
|
|
27
|
+
|
|
28
|
+
export class BrowserDaemon {
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.browser = null;
|
|
31
|
+
this.isRunning = false;
|
|
32
|
+
this.startTime = null;
|
|
33
|
+
this.daemonId = randomUUID();
|
|
34
|
+
|
|
35
|
+
// Core managers for security and isolation
|
|
36
|
+
this.profileManager = new ProfileManager();
|
|
37
|
+
this.sessionRegistry = new PersistentRegistry();
|
|
38
|
+
this.failoverManager = new FailoverManager();
|
|
39
|
+
|
|
40
|
+
// Configuration with security defaults
|
|
41
|
+
this.config = {
|
|
42
|
+
maxConcurrentSessions: options.maxConcurrentSessions || 10,
|
|
43
|
+
healthCheckInterval: options.healthCheckInterval || 10000,
|
|
44
|
+
sessionTimeoutMs: options.sessionTimeoutMs || 30 * 60 * 1000, // 30 minutes
|
|
45
|
+
daemonPort: options.daemonPort || null, // Auto-detect
|
|
46
|
+
enableHeadless: options.enableHeadless || false,
|
|
47
|
+
...options
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Active sessions tracking
|
|
51
|
+
this.activeSessions = new Map(); // sessionId -> { contextId, profilePath, claudeInstanceId, lastActivity }
|
|
52
|
+
this.healthCheckTimer = null;
|
|
53
|
+
this.isShuttingDown = false;
|
|
54
|
+
|
|
55
|
+
// Bind methods to preserve context
|
|
56
|
+
this.performHealthCheck = this.performHealthCheck.bind(this);
|
|
57
|
+
this.gracefulShutdown = this.gracefulShutdown.bind(this);
|
|
58
|
+
|
|
59
|
+
// Setup cleanup handlers
|
|
60
|
+
this.setupProcessCleanupHandlers();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Setup process cleanup handlers to ensure graceful shutdown
|
|
65
|
+
*/
|
|
66
|
+
setupProcessCleanupHandlers() {
|
|
67
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
68
|
+
signals.forEach(signal => {
|
|
69
|
+
process.on(signal, async () => {
|
|
70
|
+
console.log(`[BrowserDaemon] Received ${signal}, initiating graceful shutdown...`);
|
|
71
|
+
await this.gracefulShutdown();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
process.on('uncaughtException', async (error) => {
|
|
77
|
+
console.error('[BrowserDaemon] Uncaught exception:', error);
|
|
78
|
+
await this.gracefulShutdown();
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
83
|
+
console.error('[BrowserDaemon] Unhandled rejection at:', promise, 'reason:', reason);
|
|
84
|
+
await this.gracefulShutdown();
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Starts the shared browser daemon
|
|
91
|
+
* @returns {Promise<DaemonStartResult>}
|
|
92
|
+
*/
|
|
93
|
+
async start() {
|
|
94
|
+
if (this.isRunning) {
|
|
95
|
+
throw new Error('Browser daemon is already running');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
console.log(`[BrowserDaemon] Starting daemon ${this.daemonId}...`);
|
|
100
|
+
|
|
101
|
+
// Initialize core managers
|
|
102
|
+
await this.profileManager.initialize();
|
|
103
|
+
await this.sessionRegistry.initialize();
|
|
104
|
+
await this.failoverManager.initialize();
|
|
105
|
+
|
|
106
|
+
// Find available port for debugging
|
|
107
|
+
this.config.daemonPort = this.config.daemonPort || await findAvailablePort(9222, 9300);
|
|
108
|
+
|
|
109
|
+
// Launch shared Chrome browser with security flags
|
|
110
|
+
this.browser = await this.launchSharedBrowser();
|
|
111
|
+
this.isRunning = true;
|
|
112
|
+
this.startTime = new Date();
|
|
113
|
+
|
|
114
|
+
// Note: Auto-restart functionality removed - browser disconnects will not trigger automatic restarts
|
|
115
|
+
|
|
116
|
+
// Start health monitoring
|
|
117
|
+
this.startHealthCheck();
|
|
118
|
+
|
|
119
|
+
console.log(`[BrowserDaemon] Daemon started successfully on port ${this.config.daemonPort}`);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
daemonId: this.daemonId,
|
|
124
|
+
port: this.config.daemonPort,
|
|
125
|
+
startTime: this.startTime,
|
|
126
|
+
maxSessions: this.config.maxConcurrentSessions
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('[BrowserDaemon] Failed to start daemon:', error);
|
|
130
|
+
await this.cleanup();
|
|
131
|
+
throw new Error(`Daemon startup failed: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Launches the shared Chrome browser with security configurations
|
|
137
|
+
* @returns {Promise<Browser>}
|
|
138
|
+
*/
|
|
139
|
+
async launchSharedBrowser() {
|
|
140
|
+
// Get Chrome extension path with fallback logic
|
|
141
|
+
const extensionPath = getExtensionPath();
|
|
142
|
+
|
|
143
|
+
// Build launch args
|
|
144
|
+
const args = [
|
|
145
|
+
`--remote-debugging-port=${this.config.daemonPort}`,
|
|
146
|
+
'--no-first-run',
|
|
147
|
+
'--no-default-browser-check',
|
|
148
|
+
'--disable-features=VizDisplayCompositor',
|
|
149
|
+
'--disable-setuid-sandbox',
|
|
150
|
+
'--no-sandbox',
|
|
151
|
+
'--disable-dev-shm-usage',
|
|
152
|
+
// Security: Disable potentially dangerous features
|
|
153
|
+
'--disable-plugins',
|
|
154
|
+
'--disable-java',
|
|
155
|
+
// Performance optimizations for shared use
|
|
156
|
+
'--max_old_space_size=4096',
|
|
157
|
+
'--no-zygote',
|
|
158
|
+
'--disable-background-timer-throttling',
|
|
159
|
+
'--disable-renderer-backgrounding'
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Add extension flags if extension found
|
|
163
|
+
if (extensionPath) {
|
|
164
|
+
args.push(
|
|
165
|
+
'--enable-extensions',
|
|
166
|
+
'--extensions-on-chrome-urls',
|
|
167
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
168
|
+
`--load-extension=${extensionPath}`
|
|
169
|
+
);
|
|
170
|
+
console.log(`[BrowserDaemon] Loading ChromeDebug extension from: ${extensionPath}`);
|
|
171
|
+
} else {
|
|
172
|
+
console.warn('[BrowserDaemon] ChromeDebug extension not found. Visual selection features will be unavailable.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const launchOptions = {
|
|
176
|
+
headless: this.config.enableHeadless,
|
|
177
|
+
args,
|
|
178
|
+
defaultViewport: null,
|
|
179
|
+
ignoreDefaultArgs: ['--disable-extensions', '--disable-default-apps', '--disable-component-extensions-with-background-pages'],
|
|
180
|
+
timeout: 60000,
|
|
181
|
+
// Use a temporary user data dir for the shared browser
|
|
182
|
+
userDataDir: await this.createSharedUserDataDir()
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
console.log(`[BrowserDaemon] Launching Chrome with debugging port ${this.config.daemonPort}`);
|
|
186
|
+
return await puppeteer.launch(launchOptions);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Creates a temporary user data directory for the shared browser
|
|
191
|
+
* @returns {Promise<string>}
|
|
192
|
+
*/
|
|
193
|
+
async createSharedUserDataDir() {
|
|
194
|
+
const tmpDir = os.tmpdir();
|
|
195
|
+
const sharedDirName = `chrome-daemon-${this.daemonId}`;
|
|
196
|
+
const sharedUserDataDir = path.join(tmpDir, sharedDirName);
|
|
197
|
+
|
|
198
|
+
await fs.mkdir(sharedUserDataDir, { recursive: true });
|
|
199
|
+
console.log(`[BrowserDaemon] Created shared user data directory: ${sharedUserDataDir}`);
|
|
200
|
+
|
|
201
|
+
return sharedUserDataDir;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Allocates a new isolated session for a Claude instance
|
|
206
|
+
* @param {string} claudeInstanceId - Unique identifier for the Claude instance
|
|
207
|
+
* @param {Object} options - Session allocation options
|
|
208
|
+
* @returns {Promise<SessionAllocation>}
|
|
209
|
+
*/
|
|
210
|
+
async allocateSession(claudeInstanceId, options = {}) {
|
|
211
|
+
if (!this.isRunning || !this.browser) {
|
|
212
|
+
throw new Error('Browser daemon is not running');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check session limits
|
|
216
|
+
if (this.activeSessions.size >= this.config.maxConcurrentSessions) {
|
|
217
|
+
throw new Error(`Maximum concurrent sessions reached (${this.config.maxConcurrentSessions})`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Validate Claude instance ID
|
|
221
|
+
if (!claudeInstanceId || typeof claudeInstanceId !== 'string') {
|
|
222
|
+
throw new Error('Valid claudeInstanceId is required');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const sessionId = randomUUID();
|
|
227
|
+
|
|
228
|
+
// Create isolated profile for this session
|
|
229
|
+
const profilePath = await this.profileManager.createSessionProfile(sessionId, claudeInstanceId);
|
|
230
|
+
|
|
231
|
+
// Create isolated browser context
|
|
232
|
+
const context = await this.browser.createBrowserContext({
|
|
233
|
+
// Each session gets its own isolated context
|
|
234
|
+
// Note: Puppeteer's createBrowserContext doesn't accept userDataDir
|
|
235
|
+
// Instead, we use Chrome's profile management
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Create a page in the context
|
|
239
|
+
const page = await context.newPage();
|
|
240
|
+
|
|
241
|
+
// Store session information
|
|
242
|
+
const sessionData = {
|
|
243
|
+
sessionId,
|
|
244
|
+
claudeInstanceId,
|
|
245
|
+
contextId: context._id || sessionId, // Use context ID or fallback to session ID
|
|
246
|
+
profilePath,
|
|
247
|
+
context,
|
|
248
|
+
page,
|
|
249
|
+
createdAt: new Date(),
|
|
250
|
+
lastActivity: new Date()
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
this.activeSessions.set(sessionId, sessionData);
|
|
254
|
+
|
|
255
|
+
// Register session persistently
|
|
256
|
+
await this.sessionRegistry.updateSession(sessionId, {
|
|
257
|
+
claudeInstanceId,
|
|
258
|
+
profilePath,
|
|
259
|
+
contextId: sessionData.contextId,
|
|
260
|
+
daemonId: this.daemonId,
|
|
261
|
+
createdAt: sessionData.createdAt,
|
|
262
|
+
lastActivity: sessionData.lastActivity
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
console.log(`[BrowserDaemon] Allocated session ${sessionId} for Claude instance ${claudeInstanceId}`);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
sessionId,
|
|
270
|
+
contextId: sessionData.contextId,
|
|
271
|
+
profilePath,
|
|
272
|
+
browserWSEndpoint: this.browser.wsEndpoint(),
|
|
273
|
+
debugPort: this.config.daemonPort,
|
|
274
|
+
connectionInfo: {
|
|
275
|
+
type: 'shared-daemon',
|
|
276
|
+
daemonId: this.daemonId,
|
|
277
|
+
sessionId
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('[BrowserDaemon] Session allocation failed:', error);
|
|
282
|
+
throw new Error(`Session allocation failed: ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Deallocates a session and cleans up resources
|
|
288
|
+
* @param {string} sessionId - Session ID to deallocate
|
|
289
|
+
* @returns {Promise<boolean>}
|
|
290
|
+
*/
|
|
291
|
+
async deallocateSession(sessionId) {
|
|
292
|
+
if (!sessionId || !this.activeSessions.has(sessionId)) {
|
|
293
|
+
console.warn(`[BrowserDaemon] Attempted to deallocate non-existent session: ${sessionId}`);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const sessionData = this.activeSessions.get(sessionId);
|
|
299
|
+
|
|
300
|
+
// Close browser context
|
|
301
|
+
if (sessionData.context) {
|
|
302
|
+
await sessionData.context.close();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Cleanup profile
|
|
306
|
+
await this.profileManager.cleanupProfile(sessionId);
|
|
307
|
+
|
|
308
|
+
// Remove from active sessions
|
|
309
|
+
this.activeSessions.delete(sessionId);
|
|
310
|
+
|
|
311
|
+
// Remove from persistent registry
|
|
312
|
+
await this.sessionRegistry.removeSession(sessionId);
|
|
313
|
+
|
|
314
|
+
console.log(`[BrowserDaemon] Deallocated session ${sessionId}`);
|
|
315
|
+
return true;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error(`[BrowserDaemon] Error deallocating session ${sessionId}:`, error);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Updates session activity timestamp
|
|
324
|
+
* @param {string} sessionId - Session ID to update
|
|
325
|
+
*/
|
|
326
|
+
async updateSessionActivity(sessionId) {
|
|
327
|
+
if (this.activeSessions.has(sessionId)) {
|
|
328
|
+
const sessionData = this.activeSessions.get(sessionId);
|
|
329
|
+
sessionData.lastActivity = new Date();
|
|
330
|
+
|
|
331
|
+
// Update persistent registry
|
|
332
|
+
await this.sessionRegistry.updateSession(sessionId, {
|
|
333
|
+
lastActivity: sessionData.lastActivity
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Gets connection information for an existing session
|
|
340
|
+
* @param {string} sessionId - Session ID
|
|
341
|
+
* @returns {Object|null} Connection info or null if not found
|
|
342
|
+
*/
|
|
343
|
+
getSessionConnection(sessionId) {
|
|
344
|
+
if (!this.activeSessions.has(sessionId)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const sessionData = this.activeSessions.get(sessionId);
|
|
349
|
+
return {
|
|
350
|
+
sessionId,
|
|
351
|
+
contextId: sessionData.contextId,
|
|
352
|
+
profilePath: sessionData.profilePath,
|
|
353
|
+
browserWSEndpoint: this.browser?.wsEndpoint(),
|
|
354
|
+
debugPort: this.config.daemonPort,
|
|
355
|
+
connectionInfo: {
|
|
356
|
+
type: 'shared-daemon',
|
|
357
|
+
daemonId: this.daemonId,
|
|
358
|
+
sessionId
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Starts health check monitoring
|
|
365
|
+
*/
|
|
366
|
+
startHealthCheck() {
|
|
367
|
+
if (this.healthCheckTimer) {
|
|
368
|
+
clearInterval(this.healthCheckTimer);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.healthCheckTimer = setInterval(this.performHealthCheck, this.config.healthCheckInterval);
|
|
372
|
+
console.log(`[BrowserDaemon] Started health check with ${this.config.healthCheckInterval}ms interval`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Performs periodic health checks and cleanup
|
|
377
|
+
*/
|
|
378
|
+
async performHealthCheck() {
|
|
379
|
+
if (this.isShuttingDown) return;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
// Check browser health - log status but don't restart
|
|
383
|
+
if (!this.browser || this.browser.process()?.killed) {
|
|
384
|
+
console.warn('[BrowserDaemon] Browser process is down. Claude can launch a new browser when needed.');
|
|
385
|
+
// Don't restart - let Claude handle recovery via MCP tools
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Clean up inactive sessions
|
|
390
|
+
const now = new Date();
|
|
391
|
+
const expiredSessions = [];
|
|
392
|
+
|
|
393
|
+
for (const [sessionId, sessionData] of this.activeSessions) {
|
|
394
|
+
const inactiveTime = now.getTime() - sessionData.lastActivity.getTime();
|
|
395
|
+
if (inactiveTime > this.config.sessionTimeoutMs) {
|
|
396
|
+
expiredSessions.push(sessionId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Clean up expired sessions
|
|
401
|
+
for (const sessionId of expiredSessions) {
|
|
402
|
+
console.log(`[BrowserDaemon] Cleaning up expired session: ${sessionId}`);
|
|
403
|
+
await this.deallocateSession(sessionId);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (expiredSessions.length > 0) {
|
|
407
|
+
console.log(`[BrowserDaemon] Cleaned up ${expiredSessions.length} expired sessions`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error('[BrowserDaemon] Health check failed:', error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Gets daemon status and statistics
|
|
418
|
+
* @returns {Object} Daemon status information
|
|
419
|
+
*/
|
|
420
|
+
getStatus() {
|
|
421
|
+
return {
|
|
422
|
+
daemonId: this.daemonId,
|
|
423
|
+
isRunning: this.isRunning,
|
|
424
|
+
startTime: this.startTime,
|
|
425
|
+
activeSessions: this.activeSessions.size,
|
|
426
|
+
maxSessions: this.config.maxConcurrentSessions,
|
|
427
|
+
debugPort: this.config.daemonPort,
|
|
428
|
+
browserConnected: this.browser && !this.browser.process()?.killed,
|
|
429
|
+
uptime: this.startTime ? Date.now() - this.startTime.getTime() : 0
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Performs graceful shutdown of the daemon
|
|
435
|
+
*/
|
|
436
|
+
async gracefulShutdown() {
|
|
437
|
+
if (this.isShuttingDown) return;
|
|
438
|
+
|
|
439
|
+
this.isShuttingDown = true;
|
|
440
|
+
console.log('[BrowserDaemon] Initiating graceful shutdown...');
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
// Stop health checks
|
|
444
|
+
if (this.healthCheckTimer) {
|
|
445
|
+
clearInterval(this.healthCheckTimer);
|
|
446
|
+
this.healthCheckTimer = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Clean up all active sessions
|
|
450
|
+
console.log(`[BrowserDaemon] Cleaning up ${this.activeSessions.size} active sessions...`);
|
|
451
|
+
for (const sessionId of this.activeSessions.keys()) {
|
|
452
|
+
await this.deallocateSession(sessionId);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Close browser
|
|
456
|
+
if (this.browser) {
|
|
457
|
+
await this.browser.close();
|
|
458
|
+
this.browser = null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Cleanup managers
|
|
462
|
+
await this.cleanup();
|
|
463
|
+
|
|
464
|
+
this.isRunning = false;
|
|
465
|
+
console.log('[BrowserDaemon] Graceful shutdown completed');
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.error('[BrowserDaemon] Error during graceful shutdown:', error);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Internal cleanup method
|
|
473
|
+
*/
|
|
474
|
+
async cleanup() {
|
|
475
|
+
try {
|
|
476
|
+
await this.profileManager.cleanup();
|
|
477
|
+
await this.sessionRegistry.cleanup();
|
|
478
|
+
await this.failoverManager.cleanup();
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error('[BrowserDaemon] Error during cleanup:', error);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Checks if the daemon is healthy and ready to accept sessions
|
|
486
|
+
* @returns {boolean}
|
|
487
|
+
*/
|
|
488
|
+
isHealthy() {
|
|
489
|
+
return this.isRunning &&
|
|
490
|
+
this.browser &&
|
|
491
|
+
!this.browser.process()?.killed &&
|
|
492
|
+
!this.isShuttingDown;
|
|
493
|
+
}
|
|
494
|
+
}
|