@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,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager - Core session isolation for Chrome Debug
|
|
3
|
+
*
|
|
4
|
+
* Provides UUID-based session management to prevent conflicts when multiple
|
|
5
|
+
* Claude Code sessions launch Chrome simultaneously.
|
|
6
|
+
*
|
|
7
|
+
* Key Features:
|
|
8
|
+
* - UUID-based session identification
|
|
9
|
+
* - Isolated Chrome profile directories
|
|
10
|
+
* - Process tracking and cleanup
|
|
11
|
+
* - Heartbeat mechanism integration
|
|
12
|
+
* - Safe cleanup of only dead sessions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import { promises as fs } from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import os from 'os';
|
|
19
|
+
import { findAvailablePort, findAvailablePortWithLock, releasePortLock } from '../utils.js';
|
|
20
|
+
import { ProjectManager } from './project-manager.js';
|
|
21
|
+
|
|
22
|
+
export class SessionManager {
|
|
23
|
+
constructor(database = null) {
|
|
24
|
+
this.database = database;
|
|
25
|
+
this.sessionId = randomUUID();
|
|
26
|
+
this.sessionData = null;
|
|
27
|
+
this.heartbeatInterval = null;
|
|
28
|
+
|
|
29
|
+
// Project management for path resolution
|
|
30
|
+
this.projectManager = new ProjectManager();
|
|
31
|
+
this._projectModeInitialized = false;
|
|
32
|
+
|
|
33
|
+
// Session configuration
|
|
34
|
+
this.HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
35
|
+
this.STALE_THRESHOLD = 5 * 60 * 1000; // 5 minutes
|
|
36
|
+
|
|
37
|
+
// Add process cleanup handlers to ensure port locks are released
|
|
38
|
+
this.setupProcessCleanupHandlers();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Setup process cleanup handlers to ensure port locks are released on exit
|
|
43
|
+
*/
|
|
44
|
+
setupProcessCleanupHandlers() {
|
|
45
|
+
// Handle graceful shutdown
|
|
46
|
+
const cleanupHandler = async () => {
|
|
47
|
+
if (this.sessionData && this.sessionData.portLockFile) {
|
|
48
|
+
try {
|
|
49
|
+
await releasePortLock(this.sessionData.portLockFile);
|
|
50
|
+
console.log(`[SessionManager] Released port lock on process exit for session ${this.sessionData.sessionId}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn('[SessionManager] Error releasing port lock on exit:', error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
process.on('exit', cleanupHandler);
|
|
58
|
+
process.on('SIGINT', cleanupHandler);
|
|
59
|
+
process.on('SIGTERM', cleanupHandler);
|
|
60
|
+
process.on('uncaughtException', cleanupHandler);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initializes project mode if available (called automatically when needed)
|
|
65
|
+
*/
|
|
66
|
+
async initializeProjectMode() {
|
|
67
|
+
if (this._projectModeInitialized) {
|
|
68
|
+
return; // Already initialized
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const projectInfo = this.projectManager.detectProject();
|
|
73
|
+
if (projectInfo) {
|
|
74
|
+
await this.projectManager.initializeProjectStructure();
|
|
75
|
+
console.log(`[SessionManager] Using project-local storage: ${this.projectManager.getCurrentMode()}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log('[SessionManager] Using global storage mode');
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn('[SessionManager] Failed to initialize project mode, falling back to global:', error.message);
|
|
81
|
+
} finally {
|
|
82
|
+
this._projectModeInitialized = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a new isolated session with UUID-based profile path
|
|
88
|
+
* @param {number} preferredPort - Preferred debug port (optional)
|
|
89
|
+
* @returns {Promise<SessionData>} Created session data
|
|
90
|
+
*/
|
|
91
|
+
async createSession(preferredPort = null) {
|
|
92
|
+
// Ensure project mode is initialized
|
|
93
|
+
await this.initializeProjectMode();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Generate session metadata
|
|
97
|
+
const sessionId = this.sessionId;
|
|
98
|
+
const startTime = new Date();
|
|
99
|
+
const lastHeartbeat = new Date();
|
|
100
|
+
|
|
101
|
+
// Find available debug port with atomic lock coordination to prevent race conditions
|
|
102
|
+
const pidOffset = process.pid % 100;
|
|
103
|
+
const sessionOffset = parseInt(sessionId.split('-')[0], 16) % 50; // Use UUID for randomization
|
|
104
|
+
const basePort = preferredPort || (9222 + pidOffset + sessionOffset);
|
|
105
|
+
const portLockResult = await findAvailablePortWithLock(basePort, basePort + 200, sessionId);
|
|
106
|
+
const debugPort = portLockResult.port;
|
|
107
|
+
|
|
108
|
+
// Create UUID-based profile path using project-local or global storage
|
|
109
|
+
const sessionPrefix = sessionId.split('-')[0]; // First part of UUID for brevity
|
|
110
|
+
const timestamp = Date.now();
|
|
111
|
+
|
|
112
|
+
let profilePath;
|
|
113
|
+
if (this.projectManager.isProjectModeAvailable()) {
|
|
114
|
+
// Use project-local sessions directory
|
|
115
|
+
const sessionsDir = this.projectManager.getProjectSessionsDir();
|
|
116
|
+
profilePath = path.join(sessionsDir, `${sessionPrefix}-${timestamp}`, 'profile');
|
|
117
|
+
} else {
|
|
118
|
+
// Use global temp directory (existing behavior)
|
|
119
|
+
profilePath = path.join(
|
|
120
|
+
os.tmpdir(),
|
|
121
|
+
`chromedebug-${sessionPrefix}-${timestamp}`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create the profile directory
|
|
126
|
+
await fs.mkdir(profilePath, { recursive: true });
|
|
127
|
+
|
|
128
|
+
// Store session data
|
|
129
|
+
this.sessionData = {
|
|
130
|
+
sessionId,
|
|
131
|
+
processId: null, // Will be set when Chrome launches
|
|
132
|
+
startTime,
|
|
133
|
+
lastHeartbeat,
|
|
134
|
+
profilePath,
|
|
135
|
+
debugPort,
|
|
136
|
+
portLockFile: portLockResult.lockFile // Store lock file for cleanup
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
console.log(`[SessionManager] Created session ${sessionId} with profile: ${profilePath}`);
|
|
140
|
+
|
|
141
|
+
// Start heartbeat automatically
|
|
142
|
+
this.startHeartbeat();
|
|
143
|
+
|
|
144
|
+
return { ...this.sessionData };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('[SessionManager] Failed to create session:', error);
|
|
147
|
+
throw new Error(`Session creation failed: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Updates the heartbeat timestamp for this session
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
async updateHeartbeat() {
|
|
156
|
+
if (this.sessionData) {
|
|
157
|
+
this.sessionData.lastHeartbeat = new Date();
|
|
158
|
+
console.log(`[SessionManager] Updated heartbeat for session ${this.sessionData.sessionId}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Starts automatic heartbeat updates for this session
|
|
164
|
+
*/
|
|
165
|
+
startHeartbeat() {
|
|
166
|
+
if (this.heartbeatInterval) {
|
|
167
|
+
clearInterval(this.heartbeatInterval);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
171
|
+
await this.updateHeartbeat();
|
|
172
|
+
}, this.HEARTBEAT_INTERVAL);
|
|
173
|
+
|
|
174
|
+
console.log(`[SessionManager] Started heartbeat for session ${this.sessionId}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Stops heartbeat for this session
|
|
179
|
+
*/
|
|
180
|
+
stopHeartbeat() {
|
|
181
|
+
if (this.heartbeatInterval) {
|
|
182
|
+
clearInterval(this.heartbeatInterval);
|
|
183
|
+
this.heartbeatInterval = null;
|
|
184
|
+
console.log(`[SessionManager] Stopped heartbeat for session ${this.sessionId}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Checks if a process is still alive
|
|
190
|
+
* @param {number} pid - Process ID to check
|
|
191
|
+
* @returns {boolean} True if process is alive
|
|
192
|
+
*/
|
|
193
|
+
isProcessAlive(pid) {
|
|
194
|
+
if (!pid) return false;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
198
|
+
return true;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.code === 'ESRCH') {
|
|
201
|
+
return false; // Process does not exist
|
|
202
|
+
}
|
|
203
|
+
// Other errors (like EPERM) mean process exists but we can't signal it
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Checks if this session is alive (process + heartbeat)
|
|
210
|
+
* @param {string} sessionId - Session ID to check
|
|
211
|
+
* @returns {Promise<boolean>} True if session is alive
|
|
212
|
+
*/
|
|
213
|
+
async isSessionAlive(sessionId = null) {
|
|
214
|
+
const targetSessionId = sessionId || this.sessionId;
|
|
215
|
+
|
|
216
|
+
if (!this.sessionData || this.sessionData.sessionId !== targetSessionId) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check process health
|
|
221
|
+
if (this.sessionData.processId && !this.isProcessAlive(this.sessionData.processId)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check heartbeat freshness
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const timeSinceHeartbeat = now.getTime() - this.sessionData.lastHeartbeat.getTime();
|
|
228
|
+
|
|
229
|
+
if (timeSinceHeartbeat > this.STALE_THRESHOLD) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Sets the Chrome process ID for this session
|
|
238
|
+
* @param {number} processId - Chrome process ID
|
|
239
|
+
*/
|
|
240
|
+
setProcessId(processId) {
|
|
241
|
+
if (this.sessionData) {
|
|
242
|
+
this.sessionData.processId = processId;
|
|
243
|
+
console.log(`[SessionManager] Set process ID ${processId} for session ${this.sessionId}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Gets current session data
|
|
249
|
+
* @param {string} sessionId - Session ID to get data for
|
|
250
|
+
* @returns {Promise<SessionData|null>} Session data or null if not found
|
|
251
|
+
*/
|
|
252
|
+
async getSessionData(sessionId = null) {
|
|
253
|
+
const targetSessionId = sessionId || this.sessionId;
|
|
254
|
+
|
|
255
|
+
if (this.sessionData && this.sessionData.sessionId === targetSessionId) {
|
|
256
|
+
return { ...this.sessionData };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Safely cleans up a directory if it's a Chrome Debug directory
|
|
264
|
+
* @param {string} dirPath - Directory path to clean up
|
|
265
|
+
* @param {boolean} isProjectLocal - Whether this is a project-local directory
|
|
266
|
+
* @returns {Promise<boolean>} True if cleaned successfully
|
|
267
|
+
*/
|
|
268
|
+
async safeCleanupDirectory(dirPath, isProjectLocal = false) {
|
|
269
|
+
if (!dirPath || typeof dirPath !== 'string') {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const resolvedPath = path.resolve(dirPath);
|
|
275
|
+
|
|
276
|
+
// Security validation based on directory type
|
|
277
|
+
if (isProjectLocal) {
|
|
278
|
+
// Validate project-local directory is within project sessions directory
|
|
279
|
+
if (this.projectManager.isProjectModeAvailable()) {
|
|
280
|
+
const projectSessionsDir = path.resolve(this.projectManager.getProjectSessionsDir());
|
|
281
|
+
if (!resolvedPath.startsWith(projectSessionsDir)) {
|
|
282
|
+
console.warn(`[SessionManager] Refusing to clean unsafe project directory: ${dirPath}`);
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
console.warn(`[SessionManager] Cannot clean project directory when not in project mode: ${dirPath}`);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// Validate global directory is within temp and has chromedebug prefix
|
|
291
|
+
const tmpDir = os.tmpdir();
|
|
292
|
+
const resolvedTmpDir = path.resolve(tmpDir);
|
|
293
|
+
|
|
294
|
+
if (!resolvedPath.startsWith(resolvedTmpDir) ||
|
|
295
|
+
!path.basename(resolvedPath).startsWith('chromedebug-')) {
|
|
296
|
+
console.warn(`[SessionManager] Refusing to clean unsafe global directory: ${dirPath}`);
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await fs.rm(resolvedPath, { recursive: true, force: true });
|
|
302
|
+
console.log(`[SessionManager] Cleaned up ${isProjectLocal ? 'project-local' : 'global'} directory: ${resolvedPath}`);
|
|
303
|
+
return true;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error.code === 'ENOENT') {
|
|
306
|
+
return true; // Directory doesn't exist, consider it cleaned
|
|
307
|
+
}
|
|
308
|
+
console.error(`[SessionManager] Failed to cleanup directory ${dirPath}:`, error);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Cleans up stale Chrome Debug sessions
|
|
315
|
+
* Only cleans sessions with dead processes AND stale heartbeats
|
|
316
|
+
* @returns {Promise<CleanupResult>} Cleanup results
|
|
317
|
+
*/
|
|
318
|
+
async cleanupStaleSessions() {
|
|
319
|
+
const cleanupResult = {
|
|
320
|
+
cleanedSessions: [],
|
|
321
|
+
failedCleanups: [],
|
|
322
|
+
activeSessionsRemaining: 0
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const dirsToCheck = [];
|
|
327
|
+
|
|
328
|
+
// Check project-local sessions if available
|
|
329
|
+
if (this.projectManager.isProjectModeAvailable()) {
|
|
330
|
+
try {
|
|
331
|
+
const projectSessionsDir = this.projectManager.getProjectSessionsDir();
|
|
332
|
+
const projectEntries = await fs.readdir(projectSessionsDir);
|
|
333
|
+
dirsToCheck.push(...projectEntries.map(entry => ({
|
|
334
|
+
name: entry,
|
|
335
|
+
fullPath: path.join(projectSessionsDir, entry),
|
|
336
|
+
isProjectLocal: true
|
|
337
|
+
})));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.warn('[SessionManager] Could not read project sessions directory:', error.message);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Also check global temp directory for compatibility
|
|
344
|
+
const tmpDir = os.tmpdir();
|
|
345
|
+
const tmpEntries = await fs.readdir(tmpDir);
|
|
346
|
+
const chromePilotDirs = tmpEntries.filter(entry => entry.startsWith('chromedebug-'));
|
|
347
|
+
dirsToCheck.push(...chromePilotDirs.map(entry => ({
|
|
348
|
+
name: entry,
|
|
349
|
+
fullPath: path.join(tmpDir, entry),
|
|
350
|
+
isProjectLocal: false
|
|
351
|
+
})));
|
|
352
|
+
|
|
353
|
+
console.log(`[SessionManager] Found ${dirsToCheck.length} directories to evaluate`);
|
|
354
|
+
|
|
355
|
+
for (const dirInfo of dirsToCheck) {
|
|
356
|
+
try {
|
|
357
|
+
const dirPath = dirInfo.fullPath;
|
|
358
|
+
const dirName = dirInfo.name;
|
|
359
|
+
|
|
360
|
+
// Skip our own active session
|
|
361
|
+
if (this.sessionData &&
|
|
362
|
+
(dirPath === this.sessionData.profilePath ||
|
|
363
|
+
dirPath === path.dirname(this.sessionData.profilePath))) {
|
|
364
|
+
const isAlive = await this.isSessionAlive();
|
|
365
|
+
if (isAlive) {
|
|
366
|
+
cleanupResult.activeSessionsRemaining++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Try to extract session info from directory name
|
|
372
|
+
const parts = dirName.split('-');
|
|
373
|
+
let timestamp = null;
|
|
374
|
+
|
|
375
|
+
if (dirInfo.isProjectLocal) {
|
|
376
|
+
// Project-local format: {sessionPrefix}-{timestamp}
|
|
377
|
+
if (parts.length >= 2) {
|
|
378
|
+
timestamp = parseInt(parts[parts.length - 1], 10);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
// Global format: chromedebug-{sessionPrefix}-{timestamp}
|
|
382
|
+
if (parts.length >= 3) {
|
|
383
|
+
timestamp = parseInt(parts[parts.length - 1], 10);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!isNaN(timestamp)) {
|
|
388
|
+
const age = Date.now() - timestamp;
|
|
389
|
+
if (age > this.STALE_THRESHOLD) {
|
|
390
|
+
const cleaned = await this.safeCleanupDirectory(dirPath, dirInfo.isProjectLocal);
|
|
391
|
+
if (cleaned) {
|
|
392
|
+
cleanupResult.cleanedSessions.push(dirName);
|
|
393
|
+
} else {
|
|
394
|
+
cleanupResult.failedCleanups.push({ sessionId: dirName, reason: 'Cleanup failed' });
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
cleanupResult.activeSessionsRemaining++;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error(`[SessionManager] Error evaluating directory ${dirInfo.name}:`, error);
|
|
402
|
+
cleanupResult.failedCleanups.push({ sessionId: dirInfo.name, reason: error.message });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log(`[SessionManager] Cleanup complete: ${cleanupResult.cleanedSessions.length} cleaned, ${cleanupResult.activeSessionsRemaining} active remaining`);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error('[SessionManager] Error during stale session cleanup:', error);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return cleanupResult;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Forces cleanup of a specific session (for error recovery)
|
|
417
|
+
* @param {string} sessionId - Session ID to clean up
|
|
418
|
+
* @returns {Promise<boolean>} True if cleaned successfully
|
|
419
|
+
*/
|
|
420
|
+
async forceCleanupSession(sessionId) {
|
|
421
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
422
|
+
throw new Error('Invalid session ID provided for cleanup');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
// If it's our own session, clean it up directly
|
|
427
|
+
if (this.sessionData && this.sessionData.sessionId === sessionId) {
|
|
428
|
+
this.stopHeartbeat();
|
|
429
|
+
|
|
430
|
+
if (this.sessionData.processId) {
|
|
431
|
+
try {
|
|
432
|
+
process.kill(this.sessionData.processId, 'SIGTERM');
|
|
433
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
434
|
+
|
|
435
|
+
// Force kill if still alive
|
|
436
|
+
if (this.isProcessAlive(this.sessionData.processId)) {
|
|
437
|
+
process.kill(this.sessionData.processId, 'SIGKILL');
|
|
438
|
+
}
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.warn(`[SessionManager] Error killing process ${this.sessionData.processId}:`, error);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const cleaned = await this.safeCleanupDirectory(this.sessionData.profilePath);
|
|
445
|
+
|
|
446
|
+
// Release port lock
|
|
447
|
+
if (this.sessionData.portLockFile) {
|
|
448
|
+
try {
|
|
449
|
+
await releasePortLock(this.sessionData.portLockFile);
|
|
450
|
+
console.log(`[SessionManager] Released port lock for session ${sessionId}`);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.warn(`[SessionManager] Error releasing port lock for session ${sessionId}:`, error);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Reset session data
|
|
457
|
+
this.sessionData = null;
|
|
458
|
+
|
|
459
|
+
return cleaned;
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error(`Cannot force cleanup session ${sessionId}: session not found or not owned by this manager`);
|
|
462
|
+
}
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error(`[SessionManager] Error in force cleanup of session ${sessionId}:`, error);
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Gets current storage mode information
|
|
471
|
+
* @returns {object} Storage mode info
|
|
472
|
+
*/
|
|
473
|
+
getStorageInfo() {
|
|
474
|
+
return {
|
|
475
|
+
mode: this.projectManager.getCurrentMode(),
|
|
476
|
+
projectInfo: this.projectManager.projectInfo,
|
|
477
|
+
isProjectModeAvailable: this.projectManager.isProjectModeAvailable(),
|
|
478
|
+
projectStructure: this.projectManager.isProjectModeAvailable()
|
|
479
|
+
? this.projectManager.getProjectStructure()
|
|
480
|
+
: null,
|
|
481
|
+
globalStructure: this.projectManager.getGlobalStructure()
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Cleanup method called when SessionManager is destroyed
|
|
487
|
+
*/
|
|
488
|
+
async cleanup() {
|
|
489
|
+
console.log(`[SessionManager] Cleaning up session manager for ${this.sessionId}`);
|
|
490
|
+
|
|
491
|
+
this.stopHeartbeat();
|
|
492
|
+
|
|
493
|
+
if (this.sessionData) {
|
|
494
|
+
await this.forceCleanupSession(this.sessionData.sessionId);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|