@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,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome Extension Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages recording sessions with lease-based state management to prevent
|
|
5
|
+
* stuck recordings and ensure reliable session lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class ChromeExtensionSessionManager {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.STORAGE_KEY = 'chrome_pilot_recording_state';
|
|
11
|
+
this.LEASE_DURATION = 5000; // 5 seconds
|
|
12
|
+
this.MAX_DURATION = 30 * 60 * 1000; // 30 minutes
|
|
13
|
+
|
|
14
|
+
// Recording-only timer properties
|
|
15
|
+
this.activeRecordingTimer = null; // Only active during recording
|
|
16
|
+
this.ACTIVE_LEASE_RENEWAL = 30000; // 30 seconds, only while recording
|
|
17
|
+
|
|
18
|
+
// In-memory state cache for synchronous access (v2.1.2: Race condition fix)
|
|
19
|
+
this._recordingStateCache = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new recording session
|
|
24
|
+
* @param {Object} config - Session configuration
|
|
25
|
+
* @returns {Promise<Object>} Session creation result
|
|
26
|
+
*/
|
|
27
|
+
async createSession(config = {}) {
|
|
28
|
+
const sessionConfig = this._mergeDefaultConfig(config);
|
|
29
|
+
const sessionId = this._generateSessionId();
|
|
30
|
+
const ownerId = this._generateOwnerId();
|
|
31
|
+
const currentTime = Date.now();
|
|
32
|
+
|
|
33
|
+
const recordingState = {
|
|
34
|
+
sessionId,
|
|
35
|
+
isRecording: false,
|
|
36
|
+
startTime: currentTime,
|
|
37
|
+
lastLeaseRenewal: currentTime,
|
|
38
|
+
leaseExpiresAt: currentTime + this.LEASE_DURATION,
|
|
39
|
+
maxDuration: sessionConfig.maxDuration,
|
|
40
|
+
frameCount: 0,
|
|
41
|
+
settings: sessionConfig,
|
|
42
|
+
status: 'active',
|
|
43
|
+
ownerId,
|
|
44
|
+
debugInfo: [{
|
|
45
|
+
event: 'session_created',
|
|
46
|
+
timestamp: currentTime,
|
|
47
|
+
data: { sessionId, ownerId }
|
|
48
|
+
}],
|
|
49
|
+
lastActivity: currentTime,
|
|
50
|
+
warningFlags: []
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await this._saveRecordingState(recordingState);
|
|
55
|
+
this._logDebug('Session created', { sessionId, ownerId });
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
sessionId,
|
|
60
|
+
ownerId,
|
|
61
|
+
state: recordingState
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return this._createErrorResponse('STORAGE_ERROR', 'Failed to create session', error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start recording for a session
|
|
70
|
+
* @param {string} sessionId - Session identifier
|
|
71
|
+
* @param {string} ownerId - Owner identifier
|
|
72
|
+
* @returns {Promise<Object>} Start result
|
|
73
|
+
*/
|
|
74
|
+
async startRecording(sessionId, ownerId) {
|
|
75
|
+
try {
|
|
76
|
+
const state = await this._getRecordingState();
|
|
77
|
+
|
|
78
|
+
if (!state || state.sessionId !== sessionId) {
|
|
79
|
+
return this._createErrorResponse('INVALID_STATE', 'Session not found');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (state.ownerId !== ownerId) {
|
|
83
|
+
return this._createErrorResponse('INVALID_STATE', 'Unauthorized access to session');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (state.isRecording) {
|
|
87
|
+
return this._createErrorResponse('INVALID_STATE', 'Recording already in progress');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const currentTime = Date.now();
|
|
91
|
+
const updatedState = {
|
|
92
|
+
...state,
|
|
93
|
+
isRecording: true,
|
|
94
|
+
startTime: currentTime,
|
|
95
|
+
lastLeaseRenewal: currentTime,
|
|
96
|
+
leaseExpiresAt: currentTime + this.LEASE_DURATION,
|
|
97
|
+
status: 'active',
|
|
98
|
+
lastActivity: currentTime,
|
|
99
|
+
debugInfo: [...state.debugInfo, {
|
|
100
|
+
event: 'recording_started',
|
|
101
|
+
timestamp: currentTime,
|
|
102
|
+
data: { sessionId }
|
|
103
|
+
}]
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await this._saveRecordingState(updatedState);
|
|
107
|
+
|
|
108
|
+
// START polling ONLY for active recording
|
|
109
|
+
if (this.activeRecordingTimer) {
|
|
110
|
+
clearInterval(this.activeRecordingTimer);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.activeRecordingTimer = setInterval(async () => {
|
|
114
|
+
// Renew lease for active recording
|
|
115
|
+
await this.renewLease({
|
|
116
|
+
sessionId,
|
|
117
|
+
ownerId,
|
|
118
|
+
requestTime: Date.now()
|
|
119
|
+
});
|
|
120
|
+
// Clean up any other expired sessions
|
|
121
|
+
await this.cleanupExpiredSessions();
|
|
122
|
+
}, this.ACTIVE_LEASE_RENEWAL);
|
|
123
|
+
|
|
124
|
+
this._logDebug('Recording started with timer', { sessionId });
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
sessionId,
|
|
129
|
+
state: updatedState
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return this._createErrorResponse('STORAGE_ERROR', 'Failed to start recording', error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Renew lease for active session
|
|
138
|
+
* @param {Object} leaseRequest - Lease renewal request
|
|
139
|
+
* @returns {Promise<Object>} Lease response
|
|
140
|
+
*/
|
|
141
|
+
async renewLease(leaseRequest) {
|
|
142
|
+
const { sessionId, ownerId, requestTime, renewalDuration = this.LEASE_DURATION } = leaseRequest;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const state = await this._getRecordingState();
|
|
146
|
+
|
|
147
|
+
if (!state || state.sessionId !== sessionId) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
sessionId,
|
|
151
|
+
error: 'Session not found'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (state.ownerId !== ownerId) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
sessionId,
|
|
159
|
+
error: 'Unauthorized lease renewal'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const currentTime = Date.now();
|
|
164
|
+
const sessionDuration = currentTime - state.startTime;
|
|
165
|
+
|
|
166
|
+
// Check if max duration exceeded
|
|
167
|
+
if (sessionDuration > state.maxDuration) {
|
|
168
|
+
await this._forceStopSession(sessionId, 'MAX_DURATION');
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
sessionId,
|
|
172
|
+
error: 'Maximum duration exceeded',
|
|
173
|
+
maxDurationExceeded: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const newExpiration = currentTime + renewalDuration;
|
|
178
|
+
const updatedState = {
|
|
179
|
+
...state,
|
|
180
|
+
lastLeaseRenewal: currentTime,
|
|
181
|
+
leaseExpiresAt: newExpiration,
|
|
182
|
+
lastActivity: currentTime,
|
|
183
|
+
debugInfo: [...state.debugInfo.slice(-20), { // Keep last 20 debug entries
|
|
184
|
+
event: 'lease_renewed',
|
|
185
|
+
timestamp: currentTime,
|
|
186
|
+
data: { newExpiration, sessionDuration }
|
|
187
|
+
}]
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// v2.1.2: UPDATE STATE SYNCHRONOUSLY to fix race condition
|
|
191
|
+
// Cache the state immediately BEFORE async storage save
|
|
192
|
+
// This ensures isSessionValid() sees the updated expiration immediately
|
|
193
|
+
this._recordingStateCache = updatedState;
|
|
194
|
+
|
|
195
|
+
// Then persist to storage (async is fine, cache is already updated)
|
|
196
|
+
await this._saveRecordingState(updatedState);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
success: true,
|
|
200
|
+
sessionId,
|
|
201
|
+
newExpiration,
|
|
202
|
+
maxDurationExceeded: false
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
sessionId,
|
|
208
|
+
error: 'Failed to renew lease: ' + error.message
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Stop recording session
|
|
215
|
+
* @param {string} sessionId - Session identifier
|
|
216
|
+
* @param {string} ownerId - Owner identifier
|
|
217
|
+
* @returns {Promise<Object>} Stop result
|
|
218
|
+
*/
|
|
219
|
+
async stopRecording(sessionId, ownerId) {
|
|
220
|
+
try {
|
|
221
|
+
const state = await this._getRecordingState();
|
|
222
|
+
|
|
223
|
+
if (!state || state.sessionId !== sessionId) {
|
|
224
|
+
return this._createErrorResponse('INVALID_STATE', 'Session not found');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (state.ownerId !== ownerId) {
|
|
228
|
+
return this._createErrorResponse('INVALID_STATE', 'Unauthorized access to session');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const currentTime = Date.now();
|
|
232
|
+
const sessionDuration = currentTime - state.startTime;
|
|
233
|
+
const updatedState = {
|
|
234
|
+
...state,
|
|
235
|
+
isRecording: false,
|
|
236
|
+
status: 'stopped',
|
|
237
|
+
lastActivity: currentTime,
|
|
238
|
+
debugInfo: [...state.debugInfo, {
|
|
239
|
+
event: 'recording_stopped',
|
|
240
|
+
timestamp: currentTime,
|
|
241
|
+
data: { sessionId, sessionDuration, frameCount: state.frameCount }
|
|
242
|
+
}]
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await this._saveRecordingState(updatedState);
|
|
246
|
+
|
|
247
|
+
// STOP the polling immediately
|
|
248
|
+
if (this.activeRecordingTimer) {
|
|
249
|
+
clearInterval(this.activeRecordingTimer);
|
|
250
|
+
this.activeRecordingTimer = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this._logDebug('Recording stopped with timer cleared', { sessionId, sessionDuration });
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
success: true,
|
|
257
|
+
sessionId,
|
|
258
|
+
sessionDuration,
|
|
259
|
+
frameCount: state.frameCount,
|
|
260
|
+
state: updatedState
|
|
261
|
+
};
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return this._createErrorResponse('STORAGE_ERROR', 'Failed to stop recording', error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Force stop any active session (emergency cleanup)
|
|
269
|
+
* @param {string} reason - Reason for force stop
|
|
270
|
+
* @returns {Promise<Object>} Force stop result
|
|
271
|
+
*/
|
|
272
|
+
async forceStopAll(reason = 'MANUAL_FORCE_STOP') {
|
|
273
|
+
try {
|
|
274
|
+
const state = await this._getRecordingState();
|
|
275
|
+
|
|
276
|
+
if (!state) {
|
|
277
|
+
return { success: true, message: 'No active session to stop' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = await this._forceStopSession(state.sessionId, reason);
|
|
281
|
+
this._logDebug('Force stopped all sessions', { reason });
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return this._createErrorResponse('STORAGE_ERROR', 'Failed to force stop', error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get current session state
|
|
291
|
+
* @returns {Promise<Object|null>} Current recording state
|
|
292
|
+
*/
|
|
293
|
+
async getCurrentState() {
|
|
294
|
+
try {
|
|
295
|
+
return await this._getRecordingState();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this._logError('Failed to get current state', error);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Update frame count for session
|
|
304
|
+
* @param {string} sessionId - Session identifier
|
|
305
|
+
* @param {number} frameCount - New frame count
|
|
306
|
+
* @returns {Promise<boolean>} Update success
|
|
307
|
+
*/
|
|
308
|
+
async updateFrameCount(sessionId, frameCount) {
|
|
309
|
+
try {
|
|
310
|
+
const state = await this._getRecordingState();
|
|
311
|
+
|
|
312
|
+
if (!state || state.sessionId !== sessionId) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const updatedState = {
|
|
317
|
+
...state,
|
|
318
|
+
frameCount,
|
|
319
|
+
lastActivity: Date.now()
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
await this._saveRecordingState(updatedState);
|
|
323
|
+
return true;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
this._logError('Failed to update frame count', error);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Check if session is valid and active
|
|
332
|
+
* @param {string} sessionId - Session identifier
|
|
333
|
+
* @returns {Promise<Object>} Validity check result
|
|
334
|
+
*/
|
|
335
|
+
async isSessionValid(sessionId) {
|
|
336
|
+
try {
|
|
337
|
+
const state = await this._getRecordingState();
|
|
338
|
+
|
|
339
|
+
if (!state || state.sessionId !== sessionId) {
|
|
340
|
+
return { valid: false, reason: 'Session not found' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const currentTime = Date.now();
|
|
344
|
+
const isExpired = currentTime > state.leaseExpiresAt;
|
|
345
|
+
const isOverDuration = (currentTime - state.startTime) > state.maxDuration;
|
|
346
|
+
|
|
347
|
+
if (isExpired) {
|
|
348
|
+
await this._forceStopSession(sessionId, 'LEASE_EXPIRED');
|
|
349
|
+
return { valid: false, reason: 'Lease expired' };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isOverDuration) {
|
|
353
|
+
await this._forceStopSession(sessionId, 'MAX_DURATION');
|
|
354
|
+
return { valid: false, reason: 'Maximum duration exceeded' };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
valid: true,
|
|
359
|
+
state,
|
|
360
|
+
timeRemaining: state.maxDuration - (currentTime - state.startTime),
|
|
361
|
+
leaseTimeRemaining: state.leaseExpiresAt - currentTime
|
|
362
|
+
};
|
|
363
|
+
} catch (error) {
|
|
364
|
+
this._logError('Failed to validate session', error);
|
|
365
|
+
return { valid: false, reason: 'Validation error' };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Recover stuck sessions from previous runs
|
|
371
|
+
* Called during extension initialization and Chrome startup
|
|
372
|
+
* @returns {Promise<Object>} Recovery result
|
|
373
|
+
*/
|
|
374
|
+
async recoverSessions() {
|
|
375
|
+
try {
|
|
376
|
+
const state = await this._getRecordingState();
|
|
377
|
+
|
|
378
|
+
if (!state) {
|
|
379
|
+
this._logDebug('No sessions to recover');
|
|
380
|
+
return { recovered: 0, message: 'No sessions to recover' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const currentTime = Date.now();
|
|
384
|
+
const isExpired = currentTime > state.leaseExpiresAt;
|
|
385
|
+
const isOverDuration = (currentTime - state.startTime) > state.maxDuration;
|
|
386
|
+
// A session is only "stuck" if it's active, not recording, AND hasn't had activity for a reasonable time
|
|
387
|
+
// Allow active sessions to be idle for up to 30 minutes without being considered stuck
|
|
388
|
+
const isStuck = !state.isRecording && state.status === 'active' &&
|
|
389
|
+
(currentTime - state.lastActivity) > (30 * 60 * 1000); // 30 minutes
|
|
390
|
+
|
|
391
|
+
// More conservative timeout: only clean up sessions that are truly abandoned
|
|
392
|
+
// - 24 hours of total inactivity for stopped sessions
|
|
393
|
+
// - 4 hours of inactivity for active sessions that aren't recording
|
|
394
|
+
const inactivityTime = currentTime - state.lastActivity;
|
|
395
|
+
const isTrulyAbandoned = state.status === 'stopped'
|
|
396
|
+
? inactivityTime > (24 * 60 * 60 * 1000) // 24 hours for stopped sessions
|
|
397
|
+
: inactivityTime > (4 * 60 * 60 * 1000); // 4 hours for active sessions
|
|
398
|
+
|
|
399
|
+
if (isExpired || isOverDuration || isStuck || isTrulyAbandoned) {
|
|
400
|
+
let reason = 'RECOVERY_CLEANUP';
|
|
401
|
+
if (isExpired) reason = 'LEASE_EXPIRED';
|
|
402
|
+
else if (isOverDuration) reason = 'MAX_DURATION_EXCEEDED';
|
|
403
|
+
else if (isStuck) reason = 'STUCK_SESSION';
|
|
404
|
+
else if (isTrulyAbandoned) reason = 'ABANDONED_SESSION';
|
|
405
|
+
|
|
406
|
+
await this._forceStopSession(state.sessionId, reason);
|
|
407
|
+
|
|
408
|
+
this._logDebug('Recovered stuck session', {
|
|
409
|
+
sessionId: state.sessionId,
|
|
410
|
+
reason,
|
|
411
|
+
age: currentTime - state.startTime,
|
|
412
|
+
lastActivity: currentTime - state.lastActivity
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
recovered: 1,
|
|
417
|
+
message: `Recovered stuck session: ${state.sessionId}`,
|
|
418
|
+
reason,
|
|
419
|
+
sessionId: state.sessionId
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
this._logDebug('Session is healthy, no recovery needed', {
|
|
424
|
+
sessionId: state.sessionId,
|
|
425
|
+
status: state.status,
|
|
426
|
+
isRecording: state.isRecording
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return { recovered: 0, message: 'Session is healthy', sessionId: state.sessionId };
|
|
430
|
+
} catch (error) {
|
|
431
|
+
this._logError('Session recovery failed', error);
|
|
432
|
+
return {
|
|
433
|
+
recovered: 0,
|
|
434
|
+
error: error.message,
|
|
435
|
+
message: 'Recovery failed due to error'
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Private Methods
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Clean up expired sessions (only called during recording)
|
|
444
|
+
*/
|
|
445
|
+
async cleanupExpiredSessions() {
|
|
446
|
+
try {
|
|
447
|
+
const state = await this._getRecordingState();
|
|
448
|
+
if (!state) return;
|
|
449
|
+
|
|
450
|
+
// Skip if already stopped (prevents repeated messages)
|
|
451
|
+
if (state.status === 'stopped') {
|
|
452
|
+
// Optionally remove very old stopped sessions (>1 hour)
|
|
453
|
+
const hourAgo = Date.now() - 3600000;
|
|
454
|
+
if (state.lastActivity < hourAgo) {
|
|
455
|
+
await this._removeFromStorage();
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Only process active expired sessions
|
|
461
|
+
const currentTime = Date.now();
|
|
462
|
+
if (currentTime > state.leaseExpiresAt) {
|
|
463
|
+
console.log('[ChromeSessionManager] Cleaning up expired session', {
|
|
464
|
+
sessionId: state.sessionId
|
|
465
|
+
});
|
|
466
|
+
await this._forceStopSession(state.sessionId, 'LEASE_EXPIRED');
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
this._logError('Cleanup failed', error);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Perform cleanup of expired sessions (legacy method, now calls cleanupExpiredSessions)
|
|
475
|
+
*/
|
|
476
|
+
async _performCleanup() {
|
|
477
|
+
await this.cleanupExpiredSessions();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Force stop a session
|
|
482
|
+
*/
|
|
483
|
+
async _forceStopSession(sessionId, reason) {
|
|
484
|
+
const currentTime = Date.now();
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const state = await this._getRecordingState();
|
|
488
|
+
|
|
489
|
+
if (state && state.sessionId === sessionId) {
|
|
490
|
+
const updatedState = {
|
|
491
|
+
...state,
|
|
492
|
+
isRecording: false,
|
|
493
|
+
status: 'stopped',
|
|
494
|
+
lastActivity: currentTime,
|
|
495
|
+
debugInfo: [...state.debugInfo, {
|
|
496
|
+
event: 'force_stopped',
|
|
497
|
+
timestamp: currentTime,
|
|
498
|
+
data: { reason, sessionId }
|
|
499
|
+
}]
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
await this._saveRecordingState(updatedState);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
success: true,
|
|
507
|
+
sessionId,
|
|
508
|
+
reason,
|
|
509
|
+
stoppedAt: currentTime
|
|
510
|
+
};
|
|
511
|
+
} catch (error) {
|
|
512
|
+
return this._createErrorResponse('STORAGE_ERROR', 'Failed to force stop session', error);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Save recording state to Chrome storage
|
|
518
|
+
*/
|
|
519
|
+
async _saveRecordingState(state) {
|
|
520
|
+
// v2.1.2: Update cache synchronously BEFORE async storage save
|
|
521
|
+
this._recordingStateCache = state;
|
|
522
|
+
|
|
523
|
+
return new Promise((resolve, reject) => {
|
|
524
|
+
chrome.storage.local.set({ [this.STORAGE_KEY]: state }, () => {
|
|
525
|
+
if (chrome.runtime.lastError) {
|
|
526
|
+
reject(new Error(chrome.runtime.lastError.message));
|
|
527
|
+
} else {
|
|
528
|
+
resolve();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get recording state from Chrome storage
|
|
536
|
+
*/
|
|
537
|
+
async _getRecordingState() {
|
|
538
|
+
// v2.1.2: Use cache first for synchronous lease checks (race condition fix)
|
|
539
|
+
if (this._recordingStateCache) {
|
|
540
|
+
return this._recordingStateCache;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Otherwise fetch from storage
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
chrome.storage.local.get([this.STORAGE_KEY], (result) => {
|
|
546
|
+
if (chrome.runtime.lastError) {
|
|
547
|
+
reject(new Error(chrome.runtime.lastError.message));
|
|
548
|
+
} else {
|
|
549
|
+
const state = result[this.STORAGE_KEY] || null;
|
|
550
|
+
// Cache the state for future synchronous access
|
|
551
|
+
this._recordingStateCache = state;
|
|
552
|
+
resolve(state);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Remove recording state from Chrome storage
|
|
560
|
+
*/
|
|
561
|
+
async _removeFromStorage() {
|
|
562
|
+
// v2.1.2: Clear cache synchronously
|
|
563
|
+
this._recordingStateCache = null;
|
|
564
|
+
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
chrome.storage.local.remove([this.STORAGE_KEY], () => {
|
|
567
|
+
if (chrome.runtime.lastError) {
|
|
568
|
+
reject(new Error(chrome.runtime.lastError.message));
|
|
569
|
+
} else {
|
|
570
|
+
resolve();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Merge default configuration
|
|
578
|
+
*/
|
|
579
|
+
_mergeDefaultConfig(config) {
|
|
580
|
+
return {
|
|
581
|
+
maxDuration: this.MAX_DURATION,
|
|
582
|
+
leaseInterval: this.LEASE_DURATION,
|
|
583
|
+
activeLeaseRenewal: this.ACTIVE_LEASE_RENEWAL,
|
|
584
|
+
frameQuality: 0.3,
|
|
585
|
+
includeConsole: true,
|
|
586
|
+
captureAudio: false,
|
|
587
|
+
...config
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Generate unique session ID
|
|
593
|
+
*/
|
|
594
|
+
_generateSessionId() {
|
|
595
|
+
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Generate unique owner ID
|
|
600
|
+
*/
|
|
601
|
+
_generateOwnerId() {
|
|
602
|
+
return `owner_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Create standardized error response
|
|
607
|
+
*/
|
|
608
|
+
_createErrorResponse(type, message, originalError = null) {
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
error: {
|
|
612
|
+
type,
|
|
613
|
+
message,
|
|
614
|
+
timestamp: Date.now(),
|
|
615
|
+
recoverable: type !== 'STORAGE_ERROR',
|
|
616
|
+
originalError: originalError?.message
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Debug logging
|
|
623
|
+
*/
|
|
624
|
+
_logDebug(message, data = {}) {
|
|
625
|
+
console.log(`[ChromeSessionManager] ${message}`, data);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Error logging
|
|
630
|
+
*/
|
|
631
|
+
_logError(message, error) {
|
|
632
|
+
console.error(`[ChromeSessionManager] ${message}`, error);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Check if session manager is initialized and healthy
|
|
637
|
+
* @returns {Promise<boolean>} True if initialized and ready
|
|
638
|
+
*/
|
|
639
|
+
async isInitialized() {
|
|
640
|
+
try {
|
|
641
|
+
// Check if we can access storage
|
|
642
|
+
await this._getRecordingState();
|
|
643
|
+
return true;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
this._logError('isInitialized check failed', error);
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Check storage pressure and clean up if needed
|
|
652
|
+
*/
|
|
653
|
+
async checkStoragePressure() {
|
|
654
|
+
const usage = await this.getStorageUsage();
|
|
655
|
+
if (usage > 0.7) { // 70% full
|
|
656
|
+
console.warn('[ChromeSessionManager] Storage pressure detected, cleaning up old sessions');
|
|
657
|
+
await this.aggressiveCleanup();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Get storage usage as a ratio (0-1)
|
|
663
|
+
*/
|
|
664
|
+
async getStorageUsage() {
|
|
665
|
+
return new Promise(resolve => {
|
|
666
|
+
chrome.storage.local.getBytesInUse(bytes => {
|
|
667
|
+
const quota = chrome.storage.local.QUOTA_BYTES || 5242880; // 5MB default
|
|
668
|
+
resolve(bytes / quota);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Aggressive cleanup of old sessions when storage pressure is detected
|
|
675
|
+
*/
|
|
676
|
+
async aggressiveCleanup() {
|
|
677
|
+
try {
|
|
678
|
+
const state = await this._getRecordingState();
|
|
679
|
+
if (state && state.status === 'stopped') {
|
|
680
|
+
// Remove stopped sessions older than 1 hour during storage pressure
|
|
681
|
+
const hourAgo = Date.now() - 3600000;
|
|
682
|
+
if (state.lastActivity < hourAgo) {
|
|
683
|
+
await this._removeFromStorage();
|
|
684
|
+
console.log('[ChromeSessionManager] Removed old stopped session due to storage pressure');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} catch (error) {
|
|
688
|
+
this._logError('Aggressive cleanup failed', error);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Cleanup resources
|
|
694
|
+
*/
|
|
695
|
+
destroy() {
|
|
696
|
+
if (this.activeRecordingTimer) {
|
|
697
|
+
clearInterval(this.activeRecordingTimer);
|
|
698
|
+
this.activeRecordingTimer = null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Export for use in other parts of the extension
|
|
704
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
705
|
+
module.exports = ChromeExtensionSessionManager;
|
|
706
|
+
}
|