@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,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Session Manager - Single source of truth for Chrome Debug MCP sessions
|
|
3
|
+
* Consolidates process tracking, port discovery, and resource management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import net from 'net';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Unified Session Manager - handles all session-related resources atomically
|
|
14
|
+
*/
|
|
15
|
+
export class UnifiedSessionManager {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.sessionId = options.sessionId || this.generateSessionId();
|
|
18
|
+
this.skipCleanup = options.skipCleanup || false;
|
|
19
|
+
this.verbose = options.verbose || false;
|
|
20
|
+
|
|
21
|
+
// Session file paths
|
|
22
|
+
this.sessionDir = path.join(os.tmpdir(), 'chromedebug-sessions');
|
|
23
|
+
this.sessionFile = path.join(this.sessionDir, `${this.sessionId}.json`);
|
|
24
|
+
this.lockFile = null;
|
|
25
|
+
|
|
26
|
+
// Session state
|
|
27
|
+
this.state = {
|
|
28
|
+
sessionId: this.sessionId,
|
|
29
|
+
pid: process.pid,
|
|
30
|
+
startTime: Date.now(),
|
|
31
|
+
port: null,
|
|
32
|
+
processes: [],
|
|
33
|
+
locks: [],
|
|
34
|
+
heartbeat: Date.now()
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.heartbeatInterval = null;
|
|
38
|
+
this.initialized = false;
|
|
39
|
+
this.cleanupHandlersRegistered = false;
|
|
40
|
+
this.eventHandlers = new Map(); // Track event handlers for cleanup
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate cryptographically secure session ID
|
|
45
|
+
*/
|
|
46
|
+
generateSessionId() {
|
|
47
|
+
return 'session_' + crypto.randomBytes(16).toString('hex');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialize session with atomic operations
|
|
52
|
+
*/
|
|
53
|
+
async initialize() {
|
|
54
|
+
if (this.initialized) return this.state;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Ensure session directory exists
|
|
58
|
+
await this.ensureSessionDirectory();
|
|
59
|
+
|
|
60
|
+
// Clean up stale sessions first (if not skipping cleanup)
|
|
61
|
+
if (!this.skipCleanup) {
|
|
62
|
+
await this.cleanupStaleSessions();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Atomically create session file
|
|
66
|
+
await this.createSessionFile();
|
|
67
|
+
|
|
68
|
+
// Find and allocate port
|
|
69
|
+
const port = await this.allocatePort();
|
|
70
|
+
this.state.port = port;
|
|
71
|
+
|
|
72
|
+
// Update session file with port
|
|
73
|
+
await this.updateSessionFile();
|
|
74
|
+
|
|
75
|
+
// Start heartbeat
|
|
76
|
+
this.startHeartbeat();
|
|
77
|
+
|
|
78
|
+
// Setup cleanup handlers
|
|
79
|
+
this.setupCleanupHandlers();
|
|
80
|
+
|
|
81
|
+
this.initialized = true;
|
|
82
|
+
|
|
83
|
+
if (this.verbose) {
|
|
84
|
+
console.log(`Session initialized: ${this.sessionId}`);
|
|
85
|
+
console.log(` PID: ${this.state.pid}`);
|
|
86
|
+
console.log(` Port: ${this.state.port}`);
|
|
87
|
+
console.log(` Session file: ${this.sessionFile}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.state;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
await this.cleanup();
|
|
93
|
+
throw new Error(`Failed to initialize session: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Ensure session directory exists with proper permissions
|
|
99
|
+
*/
|
|
100
|
+
async ensureSessionDirectory() {
|
|
101
|
+
try {
|
|
102
|
+
await fs.promises.mkdir(this.sessionDir, {
|
|
103
|
+
recursive: true,
|
|
104
|
+
mode: 0o700 // Only owner can read/write
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code !== 'EEXIST') {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Atomically create session file
|
|
115
|
+
*/
|
|
116
|
+
async createSessionFile() {
|
|
117
|
+
try {
|
|
118
|
+
const sessionData = JSON.stringify(this.state, null, 2);
|
|
119
|
+
|
|
120
|
+
// Atomic write - fails if file exists
|
|
121
|
+
await fs.promises.writeFile(this.sessionFile, sessionData, {
|
|
122
|
+
flag: 'wx',
|
|
123
|
+
mode: 0o600 // Only owner can read/write
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error.code === 'EEXIST') {
|
|
128
|
+
throw new Error(`Session ${this.sessionId} already exists`);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Update session file atomically
|
|
136
|
+
*/
|
|
137
|
+
async updateSessionFile() {
|
|
138
|
+
this.state.heartbeat = Date.now();
|
|
139
|
+
|
|
140
|
+
const tempFile = `${this.sessionFile}.tmp`;
|
|
141
|
+
const sessionData = JSON.stringify(this.state, null, 2);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Defensive check: verify session file exists before updating
|
|
145
|
+
await fs.promises.access(this.sessionFile);
|
|
146
|
+
|
|
147
|
+
// Write to temp file first
|
|
148
|
+
await fs.promises.writeFile(tempFile, sessionData, { mode: 0o600 });
|
|
149
|
+
|
|
150
|
+
// Atomic rename
|
|
151
|
+
await fs.promises.rename(tempFile, this.sessionFile);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Clean up temp file if it exists
|
|
154
|
+
try {
|
|
155
|
+
await fs.promises.unlink(tempFile);
|
|
156
|
+
} catch {}
|
|
157
|
+
|
|
158
|
+
// Handle specific error cases
|
|
159
|
+
if (error.code === 'ENOENT') {
|
|
160
|
+
throw new Error(`Session file does not exist: ${this.sessionFile}`);
|
|
161
|
+
} else if (error.code === 'EPERM' || error.code === 'EACCES') {
|
|
162
|
+
throw new Error(`Permission denied accessing session file: ${this.sessionFile}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Allocate an available port with atomic check-and-lock to prevent race conditions
|
|
171
|
+
*/
|
|
172
|
+
async allocatePort() {
|
|
173
|
+
const preferredPorts = [
|
|
174
|
+
// Original range (maintain backward compatibility)
|
|
175
|
+
3000, 3001, 3002, 3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036,
|
|
176
|
+
// Safer fallback ranges (avoid common service conflicts)
|
|
177
|
+
8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090,
|
|
178
|
+
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const port of preferredPorts) {
|
|
182
|
+
// ATOMIC: Check availability AND lock in single operation to prevent race conditions
|
|
183
|
+
const lockResult = await this.atomicPortCheckAndLock(port);
|
|
184
|
+
if (lockResult.success) {
|
|
185
|
+
this.lockFile = lockResult.lockFile;
|
|
186
|
+
return port;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error('No available ports found');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if port is available
|
|
195
|
+
*/
|
|
196
|
+
async isPortAvailable(port) {
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
const server = net.createServer();
|
|
199
|
+
|
|
200
|
+
server.once('error', () => resolve(false));
|
|
201
|
+
server.once('listening', () => {
|
|
202
|
+
server.close();
|
|
203
|
+
resolve(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
server.listen(port);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Try to atomically lock a port
|
|
212
|
+
*/
|
|
213
|
+
async tryLockPort(port) {
|
|
214
|
+
const lockDir = path.join(this.sessionDir, 'port-locks');
|
|
215
|
+
const lockFile = path.join(lockDir, `port-${port}.lock`);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await fs.promises.mkdir(lockDir, { recursive: true, mode: 0o700 });
|
|
219
|
+
|
|
220
|
+
const lockData = {
|
|
221
|
+
sessionId: this.sessionId,
|
|
222
|
+
port,
|
|
223
|
+
pid: process.pid,
|
|
224
|
+
timestamp: Date.now()
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Atomic lock creation
|
|
228
|
+
await fs.promises.writeFile(lockFile, JSON.stringify(lockData), {
|
|
229
|
+
flag: 'wx',
|
|
230
|
+
mode: 0o600
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return { success: true, lockFile };
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.code === 'EEXIST') {
|
|
236
|
+
// Check if lock is stale
|
|
237
|
+
return await this.handleStaleLock(lockFile, port);
|
|
238
|
+
}
|
|
239
|
+
return { success: false };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Handle potentially stale port locks
|
|
245
|
+
*/
|
|
246
|
+
async handleStaleLock(lockFile, port) {
|
|
247
|
+
try {
|
|
248
|
+
const lockData = JSON.parse(await fs.promises.readFile(lockFile, 'utf8'));
|
|
249
|
+
const lockAge = Date.now() - lockData.timestamp;
|
|
250
|
+
|
|
251
|
+
// 30 second timeout for locks
|
|
252
|
+
if (lockAge > 30000) {
|
|
253
|
+
await fs.promises.unlink(lockFile);
|
|
254
|
+
return await this.tryLockPort(port);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { success: false };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// Corrupted lock file, try to remove and retry
|
|
260
|
+
try {
|
|
261
|
+
await fs.promises.unlink(lockFile);
|
|
262
|
+
return await this.tryLockPort(port);
|
|
263
|
+
} catch {
|
|
264
|
+
return { success: false };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Atomically check port availability and acquire lock to prevent race conditions
|
|
271
|
+
* This combines the port availability check with lock acquisition in one operation
|
|
272
|
+
*/
|
|
273
|
+
async atomicPortCheckAndLock(port) {
|
|
274
|
+
const lockDir = path.join(this.sessionDir, 'port-locks');
|
|
275
|
+
const lockFile = path.join(lockDir, `port-${port}.lock`);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// Ensure lock directory exists
|
|
279
|
+
await fs.promises.mkdir(lockDir, { recursive: true, mode: 0o700 });
|
|
280
|
+
|
|
281
|
+
// First, attempt to acquire the lock atomically
|
|
282
|
+
// If this fails, the port is already locked by another session
|
|
283
|
+
const lockData = {
|
|
284
|
+
sessionId: this.sessionId,
|
|
285
|
+
port,
|
|
286
|
+
pid: process.pid,
|
|
287
|
+
timestamp: Date.now()
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Atomic lock creation - will fail if file already exists
|
|
292
|
+
await fs.promises.writeFile(lockFile, JSON.stringify(lockData), {
|
|
293
|
+
flag: 'wx', // Create file, fail if exists (atomic)
|
|
294
|
+
mode: 0o600
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error.code === 'EEXIST') {
|
|
298
|
+
// Lock already exists, check if it's stale
|
|
299
|
+
return await this.handleStaleLock(lockFile, port);
|
|
300
|
+
}
|
|
301
|
+
return { success: false, error: error.message };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Lock acquired, now verify port is actually available
|
|
305
|
+
const isAvailable = await this.isPortAvailable(port);
|
|
306
|
+
|
|
307
|
+
if (!isAvailable) {
|
|
308
|
+
// Port is locked by us but not actually available, release lock
|
|
309
|
+
try {
|
|
310
|
+
await fs.promises.unlink(lockFile);
|
|
311
|
+
} catch {}
|
|
312
|
+
return { success: false, error: 'Port not available despite successful lock' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Success: port is available and locked by us
|
|
316
|
+
return { success: true, lockFile };
|
|
317
|
+
|
|
318
|
+
} catch (error) {
|
|
319
|
+
// Clean up any partial state
|
|
320
|
+
try {
|
|
321
|
+
await fs.promises.unlink(lockFile);
|
|
322
|
+
} catch {}
|
|
323
|
+
return { success: false, error: error.message };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Register a process for tracking
|
|
329
|
+
*/
|
|
330
|
+
async registerProcess(pid, type = 'unknown') {
|
|
331
|
+
const processInfo = {
|
|
332
|
+
pid,
|
|
333
|
+
type,
|
|
334
|
+
startTime: Date.now()
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
this.state.processes.push(processInfo);
|
|
338
|
+
await this.updateSessionFile();
|
|
339
|
+
|
|
340
|
+
if (this.verbose) {
|
|
341
|
+
console.log(`Registered process: PID ${pid} (${type}) [Session: ${this.sessionId}]`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Unregister a process
|
|
347
|
+
*/
|
|
348
|
+
async unregisterProcess(pid) {
|
|
349
|
+
this.state.processes = this.state.processes.filter(p => p.pid !== pid);
|
|
350
|
+
await this.updateSessionFile();
|
|
351
|
+
|
|
352
|
+
if (this.verbose) {
|
|
353
|
+
console.log(`Unregistered process: PID ${pid} [Session: ${this.sessionId}]`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Start heartbeat to keep session alive
|
|
359
|
+
*/
|
|
360
|
+
startHeartbeat() {
|
|
361
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
362
|
+
try {
|
|
363
|
+
await this.updateSessionFile();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
// Handle specific error cases that indicate session cleanup
|
|
366
|
+
if (error.code === 'ENOENT' || error.message.includes('Session file does not exist')) {
|
|
367
|
+
if (this.verbose) {
|
|
368
|
+
console.log(`Session file removed, stopping heartbeat for session ${this.sessionId}`);
|
|
369
|
+
}
|
|
370
|
+
this.stopHeartbeat();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Handle permission errors gracefully
|
|
375
|
+
if (error.code === 'EPERM' || error.code === 'EACCES' || error.message.includes('Permission denied')) {
|
|
376
|
+
console.warn(`Heartbeat permission error for session ${this.sessionId}:`, error.message);
|
|
377
|
+
this.stopHeartbeat();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Log other errors but don't stop heartbeat
|
|
382
|
+
console.error(`Heartbeat failed for session ${this.sessionId}:`, error.message);
|
|
383
|
+
}
|
|
384
|
+
}, 10000); // 10 second heartbeat
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Stop heartbeat timer and update state
|
|
389
|
+
*/
|
|
390
|
+
stopHeartbeat() {
|
|
391
|
+
if (this.heartbeatInterval) {
|
|
392
|
+
clearInterval(this.heartbeatInterval);
|
|
393
|
+
this.heartbeatInterval = null;
|
|
394
|
+
|
|
395
|
+
// Update state consistency when heartbeat stops
|
|
396
|
+
this.initialized = false;
|
|
397
|
+
|
|
398
|
+
if (this.verbose) {
|
|
399
|
+
console.log(`Heartbeat stopped for session ${this.sessionId}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get session information
|
|
406
|
+
*/
|
|
407
|
+
getSessionInfo() {
|
|
408
|
+
return {
|
|
409
|
+
...this.state,
|
|
410
|
+
sessionFile: this.sessionFile,
|
|
411
|
+
lockFile: this.lockFile,
|
|
412
|
+
initialized: this.initialized
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Find all active sessions
|
|
418
|
+
*/
|
|
419
|
+
static async findActiveSessions() {
|
|
420
|
+
const sessionDir = path.join(os.tmpdir(), 'chromedebug-sessions');
|
|
421
|
+
const sessions = [];
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const files = await fs.promises.readdir(sessionDir);
|
|
425
|
+
const sessionFiles = files.filter(f => f.endsWith('.json') && f.startsWith('session_'));
|
|
426
|
+
|
|
427
|
+
for (const file of sessionFiles) {
|
|
428
|
+
try {
|
|
429
|
+
const sessionData = JSON.parse(
|
|
430
|
+
await fs.promises.readFile(path.join(sessionDir, file), 'utf8')
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Check if session is still alive (heartbeat within 30 seconds)
|
|
434
|
+
const heartbeatAge = Date.now() - sessionData.heartbeat;
|
|
435
|
+
if (heartbeatAge < 30000) {
|
|
436
|
+
sessions.push(sessionData);
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
// Skip corrupted session files
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
// Directory might not exist
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return sessions;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Clean up stale sessions
|
|
451
|
+
*/
|
|
452
|
+
async cleanupStaleSessions() {
|
|
453
|
+
const sessionDir = path.join(os.tmpdir(), 'chromedebug-sessions');
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const files = await fs.promises.readdir(sessionDir);
|
|
457
|
+
let cleanedCount = 0;
|
|
458
|
+
|
|
459
|
+
for (const file of files) {
|
|
460
|
+
if (!file.endsWith('.json') || !file.startsWith('session_')) continue;
|
|
461
|
+
|
|
462
|
+
const sessionFile = path.join(sessionDir, file);
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const sessionData = JSON.parse(await fs.promises.readFile(sessionFile, 'utf8'));
|
|
466
|
+
const heartbeatAge = Date.now() - sessionData.heartbeat;
|
|
467
|
+
|
|
468
|
+
// Clean up sessions with heartbeat older than 1 minute
|
|
469
|
+
if (heartbeatAge > 60000) {
|
|
470
|
+
await fs.promises.unlink(sessionFile);
|
|
471
|
+
cleanedCount++;
|
|
472
|
+
|
|
473
|
+
if (this.verbose) {
|
|
474
|
+
console.log(`Cleaned up stale session: ${sessionData.sessionId}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
// Remove corrupted session files
|
|
479
|
+
try {
|
|
480
|
+
await fs.promises.unlink(sessionFile);
|
|
481
|
+
cleanedCount++;
|
|
482
|
+
} catch {}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Also clean up stale port locks
|
|
487
|
+
await this.cleanupStalePortLocks();
|
|
488
|
+
|
|
489
|
+
if (cleanedCount > 0 && this.verbose) {
|
|
490
|
+
console.log(`Cleaned up ${cleanedCount} stale sessions`);
|
|
491
|
+
}
|
|
492
|
+
} catch (error) {
|
|
493
|
+
// Directory might not exist
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Clean up stale port locks
|
|
499
|
+
*/
|
|
500
|
+
async cleanupStalePortLocks() {
|
|
501
|
+
const lockDir = path.join(this.sessionDir, 'port-locks');
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const files = await fs.promises.readdir(lockDir);
|
|
505
|
+
|
|
506
|
+
for (const file of files) {
|
|
507
|
+
if (!file.startsWith('port-') || !file.endsWith('.lock')) continue;
|
|
508
|
+
|
|
509
|
+
const lockFile = path.join(lockDir, file);
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const lockData = JSON.parse(await fs.promises.readFile(lockFile, 'utf8'));
|
|
513
|
+
const lockAge = Date.now() - lockData.timestamp;
|
|
514
|
+
|
|
515
|
+
// Clean up locks older than 30 seconds
|
|
516
|
+
if (lockAge > 30000) {
|
|
517
|
+
await fs.promises.unlink(lockFile);
|
|
518
|
+
}
|
|
519
|
+
} catch (error) {
|
|
520
|
+
// Remove corrupted lock files
|
|
521
|
+
try {
|
|
522
|
+
await fs.promises.unlink(lockFile);
|
|
523
|
+
} catch {}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (error) {
|
|
527
|
+
// Directory might not exist
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Setup cleanup handlers for graceful shutdown (prevents memory leaks)
|
|
533
|
+
*/
|
|
534
|
+
setupCleanupHandlers() {
|
|
535
|
+
// Prevent duplicate event handlers
|
|
536
|
+
if (this.cleanupHandlersRegistered) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const cleanup = async (reason) => {
|
|
541
|
+
if (this.verbose) {
|
|
542
|
+
console.log(`Session ${this.sessionId} exiting (${reason}), cleaning up...`);
|
|
543
|
+
}
|
|
544
|
+
await this.cleanup();
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// Create bound handlers and store them for removal
|
|
548
|
+
const exitHandler = () => cleanup('exit');
|
|
549
|
+
const sigintHandler = async () => {
|
|
550
|
+
await cleanup('SIGINT');
|
|
551
|
+
process.exit(0);
|
|
552
|
+
};
|
|
553
|
+
const sigtermHandler = async () => {
|
|
554
|
+
await cleanup('SIGTERM');
|
|
555
|
+
process.exit(0);
|
|
556
|
+
};
|
|
557
|
+
const uncaughtExceptionHandler = async (error) => {
|
|
558
|
+
console.error('Uncaught exception:', error);
|
|
559
|
+
await cleanup('uncaughtException');
|
|
560
|
+
process.exit(1);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
// Store handlers for cleanup
|
|
564
|
+
this.eventHandlers.set('exit', exitHandler);
|
|
565
|
+
this.eventHandlers.set('SIGINT', sigintHandler);
|
|
566
|
+
this.eventHandlers.set('SIGTERM', sigtermHandler);
|
|
567
|
+
this.eventHandlers.set('uncaughtException', uncaughtExceptionHandler);
|
|
568
|
+
|
|
569
|
+
// Register event handlers
|
|
570
|
+
process.on('exit', exitHandler);
|
|
571
|
+
process.on('SIGINT', sigintHandler);
|
|
572
|
+
process.on('SIGTERM', sigtermHandler);
|
|
573
|
+
process.on('uncaughtException', uncaughtExceptionHandler);
|
|
574
|
+
|
|
575
|
+
this.cleanupHandlersRegistered = true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Remove event handlers to prevent memory leaks
|
|
580
|
+
*/
|
|
581
|
+
removeCleanupHandlers() {
|
|
582
|
+
if (!this.cleanupHandlersRegistered) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Remove all registered event handlers
|
|
587
|
+
for (const [event, handler] of this.eventHandlers) {
|
|
588
|
+
process.removeListener(event, handler);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Clear the handlers map
|
|
592
|
+
this.eventHandlers.clear();
|
|
593
|
+
this.cleanupHandlersRegistered = false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Clean up all session resources
|
|
598
|
+
*/
|
|
599
|
+
async cleanup() {
|
|
600
|
+
try {
|
|
601
|
+
// Remove event handlers to prevent memory leaks
|
|
602
|
+
this.removeCleanupHandlers();
|
|
603
|
+
|
|
604
|
+
// Stop heartbeat
|
|
605
|
+
this.stopHeartbeat();
|
|
606
|
+
|
|
607
|
+
// Release port lock
|
|
608
|
+
if (this.lockFile) {
|
|
609
|
+
try {
|
|
610
|
+
await fs.promises.unlink(this.lockFile);
|
|
611
|
+
} catch {}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Remove session file
|
|
615
|
+
try {
|
|
616
|
+
await fs.promises.unlink(this.sessionFile);
|
|
617
|
+
} catch {}
|
|
618
|
+
|
|
619
|
+
if (this.verbose) {
|
|
620
|
+
console.log(`Session ${this.sessionId} cleaned up`);
|
|
621
|
+
}
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.error(`Error during session cleanup:`, error.message);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Global session manager instance
|
|
629
|
+
let globalSessionManager = null;
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get or create the global session manager
|
|
633
|
+
*/
|
|
634
|
+
export function getSessionManager(options = {}) {
|
|
635
|
+
if (!globalSessionManager) {
|
|
636
|
+
globalSessionManager = new UnifiedSessionManager(options);
|
|
637
|
+
}
|
|
638
|
+
return globalSessionManager;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Initialize session management with options
|
|
643
|
+
*/
|
|
644
|
+
export async function initializeSessionManager(options = {}) {
|
|
645
|
+
const sessionManager = getSessionManager(options);
|
|
646
|
+
await sessionManager.initialize();
|
|
647
|
+
return sessionManager;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Get current session information
|
|
652
|
+
*/
|
|
653
|
+
export function getSessionInfo() {
|
|
654
|
+
return globalSessionManager ? globalSessionManager.getSessionInfo() : null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Register a process for tracking
|
|
659
|
+
*/
|
|
660
|
+
export async function registerProcess(pid, type = 'unknown') {
|
|
661
|
+
if (globalSessionManager) {
|
|
662
|
+
await globalSessionManager.registerProcess(pid, type);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Unregister a process
|
|
668
|
+
*/
|
|
669
|
+
export async function unregisterProcess(pid) {
|
|
670
|
+
if (globalSessionManager) {
|
|
671
|
+
await globalSessionManager.unregisterProcess(pid);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Find all active sessions
|
|
677
|
+
*/
|
|
678
|
+
export const findActiveSessions = UnifiedSessionManager.findActiveSessions;
|