@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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeartbeatManager - Manages heartbeats for multiple Chrome Debug sessions
|
|
3
|
+
*
|
|
4
|
+
* Coordinates heartbeat mechanisms across different session managers
|
|
5
|
+
* to ensure sessions stay alive and detect stale sessions for cleanup.
|
|
6
|
+
*
|
|
7
|
+
* Key Features:
|
|
8
|
+
* - Multi-session heartbeat coordination
|
|
9
|
+
* - Stale heartbeat detection
|
|
10
|
+
* - Automatic cleanup integration
|
|
11
|
+
* - Thread-safe heartbeat operations
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export class HeartbeatManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.heartbeats = new Map(); // sessionId -> { interval, lastUpdate }
|
|
17
|
+
this.HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
18
|
+
this.STALE_THRESHOLD = 5 * 60 * 1000; // 5 minutes
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Starts heartbeat for a session
|
|
23
|
+
* @param {string} sessionId - Session ID to start heartbeat for
|
|
24
|
+
* @param {Function} updateCallback - Optional callback to call on each heartbeat
|
|
25
|
+
*/
|
|
26
|
+
startHeartbeat(sessionId, updateCallback = null) {
|
|
27
|
+
// Stop existing heartbeat if running
|
|
28
|
+
this.stopHeartbeat(sessionId);
|
|
29
|
+
|
|
30
|
+
const heartbeatData = {
|
|
31
|
+
lastUpdate: new Date(),
|
|
32
|
+
callback: updateCallback,
|
|
33
|
+
interval: setInterval(() => {
|
|
34
|
+
heartbeatData.lastUpdate = new Date();
|
|
35
|
+
if (updateCallback && typeof updateCallback === 'function') {
|
|
36
|
+
updateCallback(sessionId).catch(error => {
|
|
37
|
+
console.error(`[HeartbeatManager] Heartbeat callback error for ${sessionId}:`, error);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}, this.HEARTBEAT_INTERVAL)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
this.heartbeats.set(sessionId, heartbeatData);
|
|
44
|
+
console.log(`[HeartbeatManager] Started heartbeat for session ${sessionId}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stops heartbeat for a session
|
|
49
|
+
* @param {string} sessionId - Session ID to stop heartbeat for
|
|
50
|
+
*/
|
|
51
|
+
stopHeartbeat(sessionId) {
|
|
52
|
+
const heartbeatData = this.heartbeats.get(sessionId);
|
|
53
|
+
if (heartbeatData) {
|
|
54
|
+
clearInterval(heartbeatData.interval);
|
|
55
|
+
this.heartbeats.delete(sessionId);
|
|
56
|
+
console.log(`[HeartbeatManager] Stopped heartbeat for session ${sessionId}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks if heartbeat is running for a session
|
|
62
|
+
* @param {string} sessionId - Session ID to check
|
|
63
|
+
* @returns {boolean} True if heartbeat is running
|
|
64
|
+
*/
|
|
65
|
+
isHeartbeatRunning(sessionId) {
|
|
66
|
+
return this.heartbeats.has(sessionId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Updates heartbeat timestamp for a session (manual update)
|
|
71
|
+
* @param {string} sessionId - Session ID to update
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async updateHeartbeat(sessionId) {
|
|
75
|
+
const heartbeatData = this.heartbeats.get(sessionId);
|
|
76
|
+
if (heartbeatData) {
|
|
77
|
+
heartbeatData.lastUpdate = new Date();
|
|
78
|
+
console.log(`[HeartbeatManager] Manual heartbeat update for session ${sessionId}`);
|
|
79
|
+
} else {
|
|
80
|
+
console.warn(`[HeartbeatManager] Cannot update heartbeat for unknown session ${sessionId}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets last heartbeat time for a session
|
|
86
|
+
* @param {string} sessionId - Session ID to check
|
|
87
|
+
* @returns {Date|null} Last heartbeat time or null if not found
|
|
88
|
+
*/
|
|
89
|
+
getLastHeartbeat(sessionId) {
|
|
90
|
+
const heartbeatData = this.heartbeats.get(sessionId);
|
|
91
|
+
return heartbeatData ? heartbeatData.lastUpdate : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Checks for stale heartbeats across all sessions
|
|
96
|
+
* @returns {Promise<string[]>} Array of session IDs with stale heartbeats
|
|
97
|
+
*/
|
|
98
|
+
async checkStaleHeartbeats() {
|
|
99
|
+
const now = new Date();
|
|
100
|
+
const staleSessionIds = [];
|
|
101
|
+
|
|
102
|
+
for (const [sessionId, heartbeatData] of this.heartbeats.entries()) {
|
|
103
|
+
const timeSinceHeartbeat = now.getTime() - heartbeatData.lastUpdate.getTime();
|
|
104
|
+
|
|
105
|
+
if (timeSinceHeartbeat > this.STALE_THRESHOLD) {
|
|
106
|
+
staleSessionIds.push(sessionId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (staleSessionIds.length > 0) {
|
|
111
|
+
console.log(`[HeartbeatManager] Found ${staleSessionIds.length} stale heartbeats:`, staleSessionIds);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return staleSessionIds;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets heartbeat status for all sessions
|
|
119
|
+
* @returns {Object} Heartbeat status summary
|
|
120
|
+
*/
|
|
121
|
+
getHeartbeatStatus() {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
const status = {
|
|
124
|
+
totalSessions: this.heartbeats.size,
|
|
125
|
+
activeSessions: 0,
|
|
126
|
+
staleSessions: 0,
|
|
127
|
+
sessionDetails: []
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (const [sessionId, heartbeatData] of this.heartbeats.entries()) {
|
|
131
|
+
const timeSinceHeartbeat = now.getTime() - heartbeatData.lastUpdate.getTime();
|
|
132
|
+
const isStale = timeSinceHeartbeat > this.STALE_THRESHOLD;
|
|
133
|
+
|
|
134
|
+
if (isStale) {
|
|
135
|
+
status.staleSessions++;
|
|
136
|
+
} else {
|
|
137
|
+
status.activeSessions++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
status.sessionDetails.push({
|
|
141
|
+
sessionId,
|
|
142
|
+
lastHeartbeat: heartbeatData.lastUpdate,
|
|
143
|
+
timeSinceHeartbeat,
|
|
144
|
+
isStale
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return status;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stops all heartbeats (cleanup method)
|
|
153
|
+
*/
|
|
154
|
+
stopAllHeartbeats() {
|
|
155
|
+
console.log(`[HeartbeatManager] Stopping all heartbeats (${this.heartbeats.size} sessions)`);
|
|
156
|
+
|
|
157
|
+
for (const [sessionId, heartbeatData] of this.heartbeats.entries()) {
|
|
158
|
+
clearInterval(heartbeatData.interval);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.heartbeats.clear();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Automatically cleans up stale heartbeats
|
|
166
|
+
* @returns {Promise<string[]>} Array of cleaned up session IDs
|
|
167
|
+
*/
|
|
168
|
+
async autoCleanupStaleHeartbeats() {
|
|
169
|
+
const staleSessionIds = await this.checkStaleHeartbeats();
|
|
170
|
+
|
|
171
|
+
for (const sessionId of staleSessionIds) {
|
|
172
|
+
this.stopHeartbeat(sessionId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (staleSessionIds.length > 0) {
|
|
176
|
+
console.log(`[HeartbeatManager] Auto-cleaned ${staleSessionIds.length} stale heartbeats`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return staleSessionIds;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Gets statistics about heartbeat performance
|
|
184
|
+
* @returns {Object} Heartbeat statistics
|
|
185
|
+
*/
|
|
186
|
+
getStatistics() {
|
|
187
|
+
const status = this.getHeartbeatStatus();
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
totalSessions: status.totalSessions,
|
|
191
|
+
activeSessions: status.activeSessions,
|
|
192
|
+
staleSessions: status.staleSessions,
|
|
193
|
+
stalePercentage: status.totalSessions > 0
|
|
194
|
+
? Math.round((status.staleSessions / status.totalSessions) * 100)
|
|
195
|
+
: 0,
|
|
196
|
+
heartbeatInterval: this.HEARTBEAT_INTERVAL,
|
|
197
|
+
staleThreshold: this.STALE_THRESHOLD
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client Service for MCP handlers to access authenticated endpoints
|
|
3
|
+
* This service handles service-to-service authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Use built-in fetch (Node.js 18+)
|
|
7
|
+
|
|
8
|
+
class HttpClientService {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.baseUrl = null;
|
|
11
|
+
this.serviceToken = process.env.MCP_SERVICE_TOKEN;
|
|
12
|
+
this.initialized = false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Initialize the HTTP client with server port
|
|
17
|
+
*/
|
|
18
|
+
async initialize() {
|
|
19
|
+
if (this.initialized) return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Get the current server port from configuration
|
|
23
|
+
let port = process.env.CHROME_PILOT_PORT || 3000;
|
|
24
|
+
|
|
25
|
+
// Try to load configLoader dynamically
|
|
26
|
+
try {
|
|
27
|
+
const { configLoader } = await import('../config-loader.js');
|
|
28
|
+
if (configLoader && typeof configLoader.getConfig === 'function') {
|
|
29
|
+
const config = configLoader.getConfig();
|
|
30
|
+
port = config.httpPort || port;
|
|
31
|
+
}
|
|
32
|
+
} catch (configError) {
|
|
33
|
+
console.warn('[HttpClient] Config loader not available, using default port:', port);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.baseUrl = `http://localhost:${port}`;
|
|
37
|
+
this.initialized = true;
|
|
38
|
+
console.log(`[HttpClient] Initialized with base URL: ${this.baseUrl}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('[HttpClient] Failed to initialize:', error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Make authenticated request to HTTP server
|
|
47
|
+
* @param {string} endpoint - API endpoint path
|
|
48
|
+
* @param {Object} options - Request options
|
|
49
|
+
* @returns {Promise<Object>} Response data
|
|
50
|
+
*/
|
|
51
|
+
async request(endpoint, options = {}) {
|
|
52
|
+
await this.initialize();
|
|
53
|
+
|
|
54
|
+
if (!this.serviceToken) {
|
|
55
|
+
throw new Error('MCP service token not available. Cannot make authenticated requests.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
59
|
+
const requestOptions = {
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'x-service-token': this.serviceToken,
|
|
64
|
+
...options.headers
|
|
65
|
+
},
|
|
66
|
+
...options
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (options.body && typeof options.body === 'object') {
|
|
70
|
+
requestOptions.body = JSON.stringify(options.body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
console.log(`[HttpClient] Making ${requestOptions.method} request to ${url}`);
|
|
75
|
+
const response = await fetch(url, requestOptions);
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorText = await response.text();
|
|
79
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
return data;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[HttpClient] Request failed for ${url}:`, error.message);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get workflow recording via HTTP
|
|
92
|
+
* @param {string} sessionId - Session ID
|
|
93
|
+
* @returns {Promise<Object>} Workflow recording data
|
|
94
|
+
*/
|
|
95
|
+
async getWorkflowRecording(sessionId) {
|
|
96
|
+
return this.request(`/chromedebug/workflow-recording/${sessionId}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List workflow recordings via HTTP
|
|
101
|
+
* @returns {Promise<Object>} List of recordings
|
|
102
|
+
*/
|
|
103
|
+
async listWorkflowRecordings() {
|
|
104
|
+
return this.request('/chromedebug/workflow-recordings');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get frame session info via HTTP
|
|
109
|
+
* @param {string} sessionId - Session ID
|
|
110
|
+
* @returns {Promise<Object>} Frame session info
|
|
111
|
+
*/
|
|
112
|
+
async getFrameSessionInfo(sessionId) {
|
|
113
|
+
return this.request(`/chromedebug/frame-session/${sessionId}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get specific frame via HTTP
|
|
118
|
+
* @param {string} sessionId - Session ID
|
|
119
|
+
* @param {number} frameIndex - Frame index
|
|
120
|
+
* @returns {Promise<Object>} Frame data
|
|
121
|
+
*/
|
|
122
|
+
async getFrame(sessionId, frameIndex) {
|
|
123
|
+
return this.request(`/chromedebug/frame/${sessionId}/${frameIndex}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Search frame logs via HTTP
|
|
128
|
+
* @param {string} sessionId - Session ID
|
|
129
|
+
* @param {string} searchText - Search text
|
|
130
|
+
* @param {string} logLevel - Log level filter
|
|
131
|
+
* @param {number} maxResults - Maximum results
|
|
132
|
+
* @returns {Promise<Object>} Search results
|
|
133
|
+
*/
|
|
134
|
+
async searchFrameLogs(sessionId, searchText, logLevel = 'all', maxResults = 50) {
|
|
135
|
+
const params = new URLSearchParams({
|
|
136
|
+
searchText,
|
|
137
|
+
logLevel,
|
|
138
|
+
maxResults: maxResults.toString()
|
|
139
|
+
});
|
|
140
|
+
return this.request(`/chromedebug/frame-logs/search/${sessionId}?${params}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get frame logs paginated via HTTP
|
|
145
|
+
* @param {string} sessionId - Session ID
|
|
146
|
+
* @param {number} frameIndex - Frame index
|
|
147
|
+
* @param {number} offset - Offset
|
|
148
|
+
* @param {number} limit - Limit
|
|
149
|
+
* @param {string} logLevel - Log level filter
|
|
150
|
+
* @param {string} searchText - Search text
|
|
151
|
+
* @returns {Promise<Object>} Paginated logs
|
|
152
|
+
*/
|
|
153
|
+
async getFrameLogsPaginated(sessionId, frameIndex, offset = 0, limit = 100, logLevel = 'all', searchText = null) {
|
|
154
|
+
const params = new URLSearchParams({
|
|
155
|
+
offset: offset.toString(),
|
|
156
|
+
limit: limit.toString(),
|
|
157
|
+
logLevel
|
|
158
|
+
});
|
|
159
|
+
if (searchText) {
|
|
160
|
+
params.append('searchText', searchText);
|
|
161
|
+
}
|
|
162
|
+
return this.request(`/chromedebug/frame-logs/${sessionId}/${frameIndex}?${params}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get screen interactions via HTTP
|
|
167
|
+
* @param {string} sessionId - Session ID
|
|
168
|
+
* @param {number} frameIndex - Optional frame index
|
|
169
|
+
* @param {string} type - Optional interaction type filter
|
|
170
|
+
* @returns {Promise<Object>} Screen interactions
|
|
171
|
+
*/
|
|
172
|
+
async getScreenInteractions(sessionId, frameIndex = null, type = null) {
|
|
173
|
+
const params = new URLSearchParams();
|
|
174
|
+
if (frameIndex !== null) {
|
|
175
|
+
params.append('frameIndex', frameIndex.toString());
|
|
176
|
+
}
|
|
177
|
+
if (type) {
|
|
178
|
+
params.append('type', type);
|
|
179
|
+
}
|
|
180
|
+
const queryString = params.toString();
|
|
181
|
+
const endpoint = `/chromedebug/screen-interactions/${sessionId}${queryString ? `?${queryString}` : ''}`;
|
|
182
|
+
return this.request(endpoint);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* List frame sessions via HTTP
|
|
187
|
+
* @returns {Promise<Object>} List of frame sessions
|
|
188
|
+
*/
|
|
189
|
+
async listFrameSessions() {
|
|
190
|
+
return this.request('/chromedebug/frame-sessions');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Export singleton instance
|
|
195
|
+
export const httpClient = new HttpClientService();
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Manager Service - Secure process management with input validation
|
|
3
|
+
* Extracted from original index.js with preserved security measures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Safely validates a process ID to prevent injection attacks
|
|
8
|
+
* @param {*} pid - The process ID to validate
|
|
9
|
+
* @returns {number|null} - Validated PID or null if invalid
|
|
10
|
+
*/
|
|
11
|
+
function validateProcessId(pid) {
|
|
12
|
+
// Security: Check for suspicious characters before parsing
|
|
13
|
+
const pidStr = String(pid).trim();
|
|
14
|
+
if (!/^\d+$/.test(pidStr)) {
|
|
15
|
+
// Contains non-digit characters - potential injection attempt
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const numericPid = parseInt(pidStr, 10);
|
|
20
|
+
if (isNaN(numericPid) || numericPid <= 0 || numericPid > 4194304) { // Max PID on Linux/macOS
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return numericPid;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Safely kills a process using Node.js native APIs to prevent command injection
|
|
28
|
+
* @param {number} pid - Validated process ID
|
|
29
|
+
* @param {string} signal - Signal to send ('SIGTERM' or 'SIGKILL')
|
|
30
|
+
* @returns {Promise<boolean>} - True if successful, false otherwise
|
|
31
|
+
*/
|
|
32
|
+
async function safeKillProcess(pid, signal = 'SIGTERM') {
|
|
33
|
+
const validatedPid = validateProcessId(pid);
|
|
34
|
+
if (!validatedPid) {
|
|
35
|
+
console.error(`Invalid process ID: ${pid}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Security: Use Node.js native process.kill() instead of shell commands
|
|
41
|
+
process.kill(validatedPid, signal);
|
|
42
|
+
return true;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === 'ESRCH') {
|
|
45
|
+
// Process doesn't exist
|
|
46
|
+
return true;
|
|
47
|
+
} else if (error.code === 'EPERM') {
|
|
48
|
+
console.error(`Permission denied killing process ${validatedPid}`);
|
|
49
|
+
return false;
|
|
50
|
+
} else {
|
|
51
|
+
console.error(`Error killing process ${validatedPid}: ${error.message}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find ChromeDebug MCP processes using Node.js process list instead of shell commands
|
|
59
|
+
* This prevents command injection while maintaining functionality
|
|
60
|
+
*/
|
|
61
|
+
async function findChromePilotProcesses() {
|
|
62
|
+
const currentPid = process.pid;
|
|
63
|
+
const pidsToKill = [];
|
|
64
|
+
const processDescriptions = [];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Security: Use Node.js process list instead of shell commands
|
|
68
|
+
// This approach uses the 'ps-list' module or similar safe approach
|
|
69
|
+
// For now, we'll use a safer implementation with spawn instead of exec
|
|
70
|
+
const { spawn } = await import('child_process');
|
|
71
|
+
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const ps = spawn('ps', ['aux'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
74
|
+
let stdout = '';
|
|
75
|
+
|
|
76
|
+
ps.stdout.on('data', (data) => {
|
|
77
|
+
stdout += data.toString();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ps.on('close', (code) => {
|
|
81
|
+
if (code !== 0) {
|
|
82
|
+
console.error('Failed to get process list');
|
|
83
|
+
resolve({ pidsToKill: [], processDescriptions: [] });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const lines = stdout.trim().split('\n');
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
// Security: Validate each line and extract PID safely
|
|
91
|
+
const parts = line.trim().split(/\\s+/);
|
|
92
|
+
if (parts.length < 2) continue;
|
|
93
|
+
|
|
94
|
+
const pid = validateProcessId(parts[1]);
|
|
95
|
+
if (!pid || pid === currentPid) continue;
|
|
96
|
+
|
|
97
|
+
// Check if it's a ChromeDebug MCP related process
|
|
98
|
+
let processType = '';
|
|
99
|
+
if (line.includes('src/index.js')) {
|
|
100
|
+
processType = 'MCP server';
|
|
101
|
+
pidsToKill.push(pid);
|
|
102
|
+
} else if (line.includes('standalone-server.js')) {
|
|
103
|
+
processType = 'HTTP server';
|
|
104
|
+
pidsToKill.push(pid);
|
|
105
|
+
} else if (line.includes('http-server.js')) {
|
|
106
|
+
processType = 'HTTP server';
|
|
107
|
+
pidsToKill.push(pid);
|
|
108
|
+
} else if (line.includes('chrome-pilot') && line.includes('node')) {
|
|
109
|
+
processType = 'ChromeDebug MCP process';
|
|
110
|
+
pidsToKill.push(pid);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (processType) {
|
|
114
|
+
processDescriptions.push(`${processType} (PID: ${pid})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
resolve({ pidsToKill, processDescriptions });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
ps.on('error', (error) => {
|
|
122
|
+
console.error('Error running ps command:', error);
|
|
123
|
+
resolve({ pidsToKill: [], processDescriptions: [] });
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Error finding ChromeDebug MCP processes:', error);
|
|
128
|
+
return { pidsToKill: [], processDescriptions: [] };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Process Manager class for secure process lifecycle management
|
|
134
|
+
*/
|
|
135
|
+
export class ProcessManager {
|
|
136
|
+
constructor() {
|
|
137
|
+
this.managedProcesses = new Set();
|
|
138
|
+
this.shutdownHandlers = [];
|
|
139
|
+
this.setupGracefulShutdown();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validates and registers a process ID for management
|
|
144
|
+
* @param {number} pid - Process ID to manage
|
|
145
|
+
* @returns {boolean} True if successfully registered
|
|
146
|
+
*/
|
|
147
|
+
registerProcess(pid) {
|
|
148
|
+
const validatedPid = validateProcessId(pid);
|
|
149
|
+
if (!validatedPid) {
|
|
150
|
+
console.error(`Cannot register invalid process ID: ${pid}`);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.managedProcesses.add(validatedPid);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Unregisters a process from management
|
|
160
|
+
* @param {number} pid - Process ID to unregister
|
|
161
|
+
*/
|
|
162
|
+
unregisterProcess(pid) {
|
|
163
|
+
const validatedPid = validateProcessId(pid);
|
|
164
|
+
if (validatedPid) {
|
|
165
|
+
this.managedProcesses.delete(validatedPid);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Safely kills a managed process
|
|
171
|
+
* @param {number} pid - Process ID to kill
|
|
172
|
+
* @param {string} signal - Signal to send
|
|
173
|
+
* @returns {Promise<boolean>} Success status
|
|
174
|
+
*/
|
|
175
|
+
async killProcess(pid, signal = 'SIGTERM') {
|
|
176
|
+
const result = await safeKillProcess(pid, signal);
|
|
177
|
+
if (result) {
|
|
178
|
+
this.unregisterProcess(pid);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Kills all managed processes
|
|
185
|
+
* @param {string} signal - Signal to send
|
|
186
|
+
* @returns {Promise<Object>} Kill results
|
|
187
|
+
*/
|
|
188
|
+
async killAllManagedProcesses(signal = 'SIGTERM') {
|
|
189
|
+
const results = {
|
|
190
|
+
killed: [],
|
|
191
|
+
failed: [],
|
|
192
|
+
total: this.managedProcesses.size
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
for (const pid of this.managedProcesses) {
|
|
196
|
+
const success = await safeKillProcess(pid, signal);
|
|
197
|
+
if (success) {
|
|
198
|
+
results.killed.push(pid);
|
|
199
|
+
this.managedProcesses.delete(pid);
|
|
200
|
+
} else {
|
|
201
|
+
results.failed.push(pid);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return results;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Finds and kills ChromeDebug MCP processes
|
|
210
|
+
* @returns {Promise<Object>} Cleanup results
|
|
211
|
+
*/
|
|
212
|
+
async cleanupChromePilotProcesses() {
|
|
213
|
+
const { pidsToKill, processDescriptions } = await findChromePilotProcesses();
|
|
214
|
+
|
|
215
|
+
const results = {
|
|
216
|
+
found: processDescriptions,
|
|
217
|
+
killed: [],
|
|
218
|
+
failed: [],
|
|
219
|
+
total: pidsToKill.length
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (pidsToKill.length === 0) {
|
|
223
|
+
return results;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.error('Found existing ChromeDebug MCP processes, cleaning up...');
|
|
227
|
+
|
|
228
|
+
// Try SIGTERM first
|
|
229
|
+
for (const pid of pidsToKill) {
|
|
230
|
+
const success = await safeKillProcess(pid, 'SIGTERM');
|
|
231
|
+
if (success) {
|
|
232
|
+
results.killed.push(pid);
|
|
233
|
+
} else {
|
|
234
|
+
results.failed.push(pid);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Wait a bit for graceful shutdown
|
|
239
|
+
if (results.failed.length > 0) {
|
|
240
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
241
|
+
|
|
242
|
+
// Try SIGKILL for remaining processes
|
|
243
|
+
const finalAttempts = [];
|
|
244
|
+
for (const pid of results.failed) {
|
|
245
|
+
const success = await safeKillProcess(pid, 'SIGKILL');
|
|
246
|
+
if (success) {
|
|
247
|
+
// Move from failed to killed
|
|
248
|
+
const index = results.failed.indexOf(pid);
|
|
249
|
+
results.failed.splice(index, 1);
|
|
250
|
+
results.killed.push(pid);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Adds a shutdown handler
|
|
260
|
+
* @param {Function} handler - Cleanup function to call on shutdown
|
|
261
|
+
*/
|
|
262
|
+
addShutdownHandler(handler) {
|
|
263
|
+
this.shutdownHandlers.push(handler);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Sets up graceful shutdown handling
|
|
268
|
+
*/
|
|
269
|
+
setupGracefulShutdown() {
|
|
270
|
+
const cleanup = async (signal) => {
|
|
271
|
+
console.error(`Received ${signal}, cleaning up...`);
|
|
272
|
+
|
|
273
|
+
// Run custom shutdown handlers
|
|
274
|
+
for (const handler of this.shutdownHandlers) {
|
|
275
|
+
try {
|
|
276
|
+
await handler();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('Error in shutdown handler:', error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Kill all managed processes
|
|
283
|
+
await this.killAllManagedProcesses('SIGTERM');
|
|
284
|
+
|
|
285
|
+
process.exit(0);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Register signal handlers
|
|
289
|
+
process.on('SIGINT', () => cleanup('SIGINT'));
|
|
290
|
+
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
|
291
|
+
|
|
292
|
+
// Handle uncaught exceptions
|
|
293
|
+
process.on('uncaughtException', (error) => {
|
|
294
|
+
console.error('Uncaught exception:', error);
|
|
295
|
+
cleanup('uncaughtException');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
299
|
+
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
|
300
|
+
cleanup('unhandledRejection');
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Gets statistics about managed processes
|
|
306
|
+
* @returns {Object} Process statistics
|
|
307
|
+
*/
|
|
308
|
+
getStatistics() {
|
|
309
|
+
return {
|
|
310
|
+
managedProcesses: this.managedProcesses.size,
|
|
311
|
+
shutdownHandlers: this.shutdownHandlers.length,
|
|
312
|
+
currentPid: process.pid
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Export utility functions for backward compatibility
|
|
318
|
+
export { validateProcessId, safeKillProcess, findChromePilotProcesses };
|