@172ai/containers-mcp-server 1.7.1 → 1.7.3
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/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +43 -2
- package/dist/server.js.map +1 -1
- package/dist/services/buildService.d.ts +31 -56
- package/dist/services/buildService.d.ts.map +1 -1
- package/dist/services/buildService.js +234 -420
- package/dist/services/buildService.js.map +1 -1
- package/dist/services/executionService.d.ts +32 -55
- package/dist/services/executionService.d.ts.map +1 -1
- package/dist/services/executionService.js +280 -336
- package/dist/services/executionService.js.map +1 -1
- package/dist/services/streamingService.d.ts +95 -0
- package/dist/services/streamingService.d.ts.map +1 -0
- package/dist/services/streamingService.js +298 -0
- package/dist/services/streamingService.js.map +1 -0
- package/package.json +8 -7
|
@@ -4,6 +4,7 @@ exports.buildService = exports.BuildService = void 0;
|
|
|
4
4
|
const auth_1 = require("../auth");
|
|
5
5
|
const errorHandler_1 = require("../utils/errorHandler");
|
|
6
6
|
const enhancedErrorHandler_1 = require("../utils/enhancedErrorHandler");
|
|
7
|
+
const streamingService_1 = require("./streamingService");
|
|
7
8
|
/**
|
|
8
9
|
* Generate a unique request ID
|
|
9
10
|
*/
|
|
@@ -13,8 +14,8 @@ function generateRequestId() {
|
|
|
13
14
|
class BuildService {
|
|
14
15
|
constructor() {
|
|
15
16
|
this.mcpServer = null;
|
|
16
|
-
//
|
|
17
|
-
this.
|
|
17
|
+
// Real-time streaming management (replaces polling)
|
|
18
|
+
this.activeStreams = new Map();
|
|
18
19
|
this.progressUpdates = new Map();
|
|
19
20
|
this.streamAnalytics = {
|
|
20
21
|
totalBuilds: 0,
|
|
@@ -26,8 +27,6 @@ class BuildService {
|
|
|
26
27
|
errorsEncountered: 0,
|
|
27
28
|
lastActivity: new Date().toISOString()
|
|
28
29
|
};
|
|
29
|
-
this.maxConcurrentBuilds = 10;
|
|
30
|
-
this.globalMonitoringTimeout = 900000; // 15 minutes
|
|
31
30
|
this.httpClient = auth_1.authManager.getHttpClient();
|
|
32
31
|
}
|
|
33
32
|
/**
|
|
@@ -37,19 +36,27 @@ class BuildService {
|
|
|
37
36
|
this.mcpServer = server;
|
|
38
37
|
}
|
|
39
38
|
/**
|
|
40
|
-
*
|
|
39
|
+
* Get stream analytics and monitoring information
|
|
41
40
|
*/
|
|
42
41
|
getStreamAnalytics() {
|
|
43
42
|
return {
|
|
44
43
|
...this.streamAnalytics,
|
|
45
|
-
|
|
44
|
+
activeStreams: Array.from(this.activeStreams.values())
|
|
46
45
|
};
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
48
|
* Get progress update for a specific token
|
|
50
49
|
*/
|
|
51
50
|
getProgressUpdate(progressToken) {
|
|
52
|
-
|
|
51
|
+
const update = this.progressUpdates.get(progressToken);
|
|
52
|
+
if (!update)
|
|
53
|
+
return null;
|
|
54
|
+
// Check if expired (clean up stale data)
|
|
55
|
+
if (update.expiresAt && Date.now() > update.expiresAt) {
|
|
56
|
+
this.progressUpdates.delete(progressToken);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return update;
|
|
53
60
|
}
|
|
54
61
|
/**
|
|
55
62
|
* Get all progress updates for debugging
|
|
@@ -62,296 +69,46 @@ class BuildService {
|
|
|
62
69
|
return updates;
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
|
-
*
|
|
66
|
-
*/
|
|
67
|
-
cleanupInactiveMonitors() {
|
|
68
|
-
const now = Date.now();
|
|
69
|
-
for (const [token, monitor] of this.activeMonitors.entries()) {
|
|
70
|
-
if (monitor.status !== 'active' ||
|
|
71
|
-
(now - monitor.startTime) > this.globalMonitoringTimeout) {
|
|
72
|
-
if (monitor.timeoutHandle) {
|
|
73
|
-
clearTimeout(monitor.timeoutHandle);
|
|
74
|
-
}
|
|
75
|
-
this.activeMonitors.delete(token);
|
|
76
|
-
this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Phase 3: Cancel a specific build monitor
|
|
72
|
+
* Cancel build monitoring for a specific token
|
|
82
73
|
*/
|
|
83
74
|
async cancelBuildMonitoring(progressToken, reason = 'User requested cancellation') {
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
75
|
+
const stream = this.activeStreams.get(progressToken);
|
|
76
|
+
if (!stream) {
|
|
86
77
|
return false;
|
|
87
78
|
}
|
|
88
|
-
|
|
89
|
-
await this.sendProgressNotification(progressToken, monitor.lastProgress, 100, `Build monitoring cancelled: ${reason}`);
|
|
90
|
-
if (monitor.timeoutHandle) {
|
|
91
|
-
clearTimeout(monitor.timeoutHandle);
|
|
92
|
-
}
|
|
93
|
-
this.activeMonitors.delete(progressToken);
|
|
94
|
-
this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
|
|
95
|
-
console.log(`[MCP] Cancelled monitoring for build ${monitor.buildId} with token ${progressToken}`);
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Store progress notification with Phase 3 enhancements
|
|
100
|
-
* Since MCP doesn't support server-to-client push notifications,
|
|
101
|
-
* we store progress updates that can be retrieved via polling
|
|
102
|
-
*/
|
|
103
|
-
async sendProgressNotification(progressToken, progress, total = 100, message, metadata) {
|
|
104
|
-
if (!progressToken) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
79
|
+
console.log(`[Build] Cancelling monitoring for build ${stream.buildId} with token ${progressToken}`);
|
|
107
80
|
try {
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
...(metadata && { metadata })
|
|
116
|
-
};
|
|
117
|
-
// Store in memory for quick access (in production, this could be Redis/DB)
|
|
118
|
-
if (!this.progressUpdates) {
|
|
119
|
-
this.progressUpdates = new Map();
|
|
120
|
-
}
|
|
121
|
-
this.progressUpdates.set(progressToken, notificationData);
|
|
122
|
-
// Phase 3: Update analytics
|
|
123
|
-
this.streamAnalytics.notificationsSent++;
|
|
124
|
-
this.streamAnalytics.lastActivity = new Date().toISOString();
|
|
125
|
-
console.log(`[MCP] Progress update stored: ${progress}/${total} - ${message} (token: ${progressToken})`);
|
|
126
|
-
// Optional: Send to Claude Code chat if available
|
|
127
|
-
if (this.mcpServer && typeof this.mcpServer.sendChatMessage === 'function') {
|
|
128
|
-
try {
|
|
129
|
-
await this.mcpServer.sendChatMessage({
|
|
130
|
-
type: 'progress',
|
|
131
|
-
token: progressToken,
|
|
132
|
-
progress,
|
|
133
|
-
total,
|
|
134
|
-
message,
|
|
135
|
-
metadata
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
catch (chatError) {
|
|
139
|
-
// Chat notifications are best-effort
|
|
140
|
-
console.log(`[MCP] Chat notification failed (non-critical): ${chatError.message || 'Unknown error'}`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
81
|
+
// Update progress with cancellation notice
|
|
82
|
+
await this.storeProgressUpdate(progressToken, stream.lastProgress, 100, `Build monitoring cancelled: ${reason}`);
|
|
83
|
+
// Unsubscribe from stream
|
|
84
|
+
await streamingService_1.streamingService.unsubscribeFromStream(stream.streamId);
|
|
85
|
+
// Cleanup
|
|
86
|
+
this.completeStream(progressToken, 'cancelled');
|
|
87
|
+
return true;
|
|
143
88
|
}
|
|
144
89
|
catch (error) {
|
|
145
|
-
console.error(`[
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Phase 3: Enhanced build progress monitoring with multi-stream management
|
|
151
|
-
*/
|
|
152
|
-
startBuildProgressMonitoring(containerId, buildId, progressToken) {
|
|
153
|
-
// Phase 3: Check for concurrent build limits
|
|
154
|
-
this.cleanupInactiveMonitors();
|
|
155
|
-
if (this.activeMonitors.size >= this.maxConcurrentBuilds) {
|
|
156
|
-
console.log(`[MCP] Maximum concurrent builds reached (${this.maxConcurrentBuilds}), queuing build ${buildId}`);
|
|
157
|
-
this.sendProgressNotification(progressToken, 5, 100, `Build queued - ${this.activeMonitors.size} builds in progress`);
|
|
158
|
-
// In a real implementation, you might want to queue this build
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
// Phase 3: Create monitor entry
|
|
162
|
-
const monitor = {
|
|
163
|
-
containerId,
|
|
164
|
-
buildId,
|
|
165
|
-
progressToken,
|
|
166
|
-
startTime: Date.now(),
|
|
167
|
-
lastProgress: 10,
|
|
168
|
-
lastLogCount: 0,
|
|
169
|
-
status: 'active',
|
|
170
|
-
retryCount: 0
|
|
171
|
-
};
|
|
172
|
-
this.activeMonitors.set(progressToken, monitor);
|
|
173
|
-
this.streamAnalytics.totalBuilds++;
|
|
174
|
-
this.streamAnalytics.activeBuilds++;
|
|
175
|
-
// Run monitoring in background without blocking the main response
|
|
176
|
-
setImmediate(async () => {
|
|
177
|
-
console.log(`[MCP] Starting enhanced progress monitoring for build ${buildId} with token ${progressToken}`);
|
|
178
|
-
const enhancedMonitor = async () => {
|
|
179
|
-
const currentMonitor = this.activeMonitors.get(progressToken);
|
|
180
|
-
if (!currentMonitor || currentMonitor.status !== 'active') {
|
|
181
|
-
return; // Monitor was cancelled or completed
|
|
182
|
-
}
|
|
183
|
-
try {
|
|
184
|
-
// Phase 3: Enhanced timeout handling
|
|
185
|
-
const elapsed = Date.now() - currentMonitor.startTime;
|
|
186
|
-
if (elapsed > this.globalMonitoringTimeout) {
|
|
187
|
-
console.log(`[MCP] Build monitoring timeout for ${buildId} after ${elapsed}ms`);
|
|
188
|
-
currentMonitor.status = 'error';
|
|
189
|
-
await this.sendProgressNotification(progressToken, 100, 100, 'Build monitoring timeout - check build status manually', {
|
|
190
|
-
elapsedTime: elapsed,
|
|
191
|
-
reason: 'timeout'
|
|
192
|
-
});
|
|
193
|
-
this.completeMonitoring(progressToken, 'timeout');
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
// Phase 3: Smart progress calculation with retry logic
|
|
197
|
-
let buildStatus;
|
|
198
|
-
try {
|
|
199
|
-
buildStatus = await this.getBuildStatus({ containerId, buildId });
|
|
200
|
-
}
|
|
201
|
-
catch (statusError) {
|
|
202
|
-
currentMonitor.retryCount++;
|
|
203
|
-
if (currentMonitor.retryCount > 3) {
|
|
204
|
-
currentMonitor.status = 'error';
|
|
205
|
-
await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, `Build status check failed after ${currentMonitor.retryCount} retries`);
|
|
206
|
-
this.completeMonitoring(progressToken, 'api_error');
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
console.log(`[MCP] Build status check failed (attempt ${currentMonitor.retryCount}), retrying...`);
|
|
210
|
-
setTimeout(enhancedMonitor, 5000); // Exponential backoff would be better
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
// Reset retry count on successful API call
|
|
214
|
-
currentMonitor.retryCount = 0;
|
|
215
|
-
// Phase 3: Enhanced progress calculation
|
|
216
|
-
const apiProgress = buildStatus.progress || 0;
|
|
217
|
-
const timeBasedProgress = Math.min((elapsed / 300000) * 80, 80); // 80% progress over 5 minutes
|
|
218
|
-
const combinedProgress = Math.max(apiProgress, timeBasedProgress, currentMonitor.lastProgress);
|
|
219
|
-
// Send progress update if changed significantly
|
|
220
|
-
if (combinedProgress > currentMonitor.lastProgress + 5 ||
|
|
221
|
-
buildStatus.status !== currentMonitor.lastStatus) {
|
|
222
|
-
const progressMetadata = {
|
|
223
|
-
buildStatus: buildStatus.status,
|
|
224
|
-
elapsedTime: elapsed,
|
|
225
|
-
apiProgress,
|
|
226
|
-
timeBasedProgress,
|
|
227
|
-
buildId,
|
|
228
|
-
containerId
|
|
229
|
-
};
|
|
230
|
-
await this.sendProgressNotification(progressToken, Math.min(combinedProgress, 99), // Cap at 99% until completion
|
|
231
|
-
100, `Build ${buildStatus.status}: ${Math.round(combinedProgress)}% (${Math.round(elapsed / 1000)}s elapsed)`, progressMetadata);
|
|
232
|
-
currentMonitor.lastProgress = combinedProgress;
|
|
233
|
-
currentMonitor.lastStatus = buildStatus.status;
|
|
234
|
-
}
|
|
235
|
-
// Phase 3: Enhanced completion detection
|
|
236
|
-
if (buildStatus.status === 'completed') {
|
|
237
|
-
currentMonitor.status = 'completed';
|
|
238
|
-
const completionTime = Date.now() - currentMonitor.startTime;
|
|
239
|
-
await this.sendProgressNotification(progressToken, 100, 100, `Build completed successfully! (${Math.round(completionTime / 1000)}s total)`, {
|
|
240
|
-
completionTime,
|
|
241
|
-
buildId,
|
|
242
|
-
finalStatus: 'completed'
|
|
243
|
-
});
|
|
244
|
-
this.completeMonitoring(progressToken, 'completed', completionTime);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (buildStatus.status === 'failed') {
|
|
248
|
-
currentMonitor.status = 'failed';
|
|
249
|
-
await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, `Build failed: ${buildStatus.error || 'Unknown error'}`, {
|
|
250
|
-
error: buildStatus.error,
|
|
251
|
-
buildId,
|
|
252
|
-
finalStatus: 'failed'
|
|
253
|
-
});
|
|
254
|
-
this.completeMonitoring(progressToken, 'failed');
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (buildStatus.status === 'cancelled') {
|
|
258
|
-
currentMonitor.status = 'cancelled';
|
|
259
|
-
await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, 'Build was cancelled', {
|
|
260
|
-
buildId,
|
|
261
|
-
finalStatus: 'cancelled'
|
|
262
|
-
});
|
|
263
|
-
this.completeMonitoring(progressToken, 'cancelled');
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
// Phase 3: Enhanced log-based progress estimation
|
|
267
|
-
try {
|
|
268
|
-
const logs = await this.getBuildLogs(containerId, buildId);
|
|
269
|
-
if (logs.length > currentMonitor.lastLogCount) {
|
|
270
|
-
const newLogCount = logs.length - currentMonitor.lastLogCount;
|
|
271
|
-
currentMonitor.lastLogCount = logs.length;
|
|
272
|
-
// More sophisticated progress estimation
|
|
273
|
-
const logBasedProgress = Math.min(20 + (logs.length * 1.5), 90);
|
|
274
|
-
if (logBasedProgress > currentMonitor.lastProgress) {
|
|
275
|
-
await this.sendProgressNotification(progressToken, logBasedProgress, 100, `Build in progress - ${newLogCount} new log entries (${logs.length} total)`, { logCount: logs.length, newLogs: newLogCount });
|
|
276
|
-
currentMonitor.lastProgress = logBasedProgress;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
catch (logError) {
|
|
281
|
-
// Log errors are not critical for progress monitoring
|
|
282
|
-
console.log(`[MCP] Could not fetch logs for progress estimation: ${logError.message || 'Unknown error'}`);
|
|
283
|
-
}
|
|
284
|
-
// Continue monitoring if build is still running
|
|
285
|
-
if (['pending', 'running', 'in_progress'].includes(buildStatus.status)) {
|
|
286
|
-
// Phase 3: Adaptive polling interval based on build age and activity
|
|
287
|
-
const adaptiveInterval = elapsed > 180000 ? 5000 : 3000; // Slower polling for older builds
|
|
288
|
-
currentMonitor.timeoutHandle = setTimeout(enhancedMonitor, adaptiveInterval);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
console.error(`[MCP] Error in enhanced build progress monitoring:`, error);
|
|
293
|
-
currentMonitor.retryCount++;
|
|
294
|
-
this.streamAnalytics.errorsEncountered++;
|
|
295
|
-
if (currentMonitor.retryCount <= 3) {
|
|
296
|
-
await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, `Progress monitoring error (attempt ${currentMonitor.retryCount}) - retrying...`);
|
|
297
|
-
setTimeout(enhancedMonitor, 10000); // Wait 10s before retry
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
currentMonitor.status = 'error';
|
|
301
|
-
await this.sendProgressNotification(progressToken, currentMonitor.lastProgress, 100, 'Progress monitoring failed after multiple retries - build may still be running');
|
|
302
|
-
this.completeMonitoring(progressToken, 'monitoring_error');
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
// Start enhanced monitoring
|
|
307
|
-
await enhancedMonitor();
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Phase 3: Complete monitoring and update analytics
|
|
312
|
-
*/
|
|
313
|
-
completeMonitoring(progressToken, reason, completionTime) {
|
|
314
|
-
const monitor = this.activeMonitors.get(progressToken);
|
|
315
|
-
if (monitor) {
|
|
316
|
-
if (monitor.timeoutHandle) {
|
|
317
|
-
clearTimeout(monitor.timeoutHandle);
|
|
318
|
-
}
|
|
319
|
-
this.activeMonitors.delete(progressToken);
|
|
320
|
-
this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
|
|
321
|
-
// Update completion statistics
|
|
322
|
-
if (reason === 'completed') {
|
|
323
|
-
this.streamAnalytics.completedBuilds++;
|
|
324
|
-
if (completionTime) {
|
|
325
|
-
const currentAvg = this.streamAnalytics.averageBuildTime;
|
|
326
|
-
const totalCompleted = this.streamAnalytics.completedBuilds;
|
|
327
|
-
this.streamAnalytics.averageBuildTime =
|
|
328
|
-
((currentAvg * (totalCompleted - 1)) + completionTime) / totalCompleted;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
else if (reason === 'failed' || reason === 'monitoring_error' || reason === 'api_error') {
|
|
332
|
-
this.streamAnalytics.failedBuilds++;
|
|
333
|
-
}
|
|
334
|
-
console.log(`[MCP] Monitoring completed for ${monitor.buildId} (${reason})`);
|
|
90
|
+
console.error(`[Build] Error cancelling monitoring for token ${progressToken}:`, error);
|
|
91
|
+
return false;
|
|
335
92
|
}
|
|
336
93
|
}
|
|
337
94
|
/**
|
|
338
|
-
* Start a container build with
|
|
95
|
+
* Start a container build with real-time streaming progress
|
|
339
96
|
*/
|
|
340
97
|
async buildContainer(params, progressToken) {
|
|
341
98
|
try {
|
|
342
99
|
if (!params.containerId) {
|
|
343
100
|
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
344
101
|
}
|
|
345
|
-
//
|
|
102
|
+
// Initialize progress tracking with streaming notifications
|
|
346
103
|
if (progressToken) {
|
|
347
|
-
console.log(`[
|
|
348
|
-
await this.
|
|
104
|
+
console.log(`[Build] Starting build with streaming progress token: ${progressToken} for container: ${params.containerId}`);
|
|
105
|
+
await this.storeProgressUpdate(progressToken, 0, 100, 'Build initialization started');
|
|
349
106
|
}
|
|
350
107
|
const buildRequest = {
|
|
351
108
|
buildArgs: params.buildArgs || {},
|
|
352
109
|
target: params.target
|
|
353
110
|
};
|
|
354
|
-
//
|
|
111
|
+
// Start the build via API
|
|
355
112
|
const response = await this.httpClient.post(`/v1/containers/${params.containerId}/build`, buildRequest);
|
|
356
113
|
if (!response.data) {
|
|
357
114
|
throw errorHandler_1.ErrorHandler.createServerError('Invalid response from build start API');
|
|
@@ -368,11 +125,11 @@ class BuildService {
|
|
|
368
125
|
error: data.error,
|
|
369
126
|
logs: data.logs
|
|
370
127
|
};
|
|
371
|
-
//
|
|
128
|
+
// Set up real-time streaming for progress updates
|
|
372
129
|
if (progressToken) {
|
|
373
|
-
await this.
|
|
374
|
-
//
|
|
375
|
-
this.
|
|
130
|
+
await this.storeProgressUpdate(progressToken, 10, 100, `Build started - ID: ${buildResult.id}`);
|
|
131
|
+
// Create streaming subscription instead of polling
|
|
132
|
+
await this.setupStreamingMonitoring(params.containerId, buildResult.id, progressToken);
|
|
376
133
|
}
|
|
377
134
|
return buildResult;
|
|
378
135
|
}
|
|
@@ -404,6 +161,201 @@ class BuildService {
|
|
|
404
161
|
throw new errorHandler_1.ApiError(enhancedError.message, enhancedError.status, enhancedError.code, enhancedError, context.requestId, context.operation);
|
|
405
162
|
}
|
|
406
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Set up real-time streaming monitoring for build progress
|
|
166
|
+
* Replaces the old polling-based monitoring system
|
|
167
|
+
*/
|
|
168
|
+
async setupStreamingMonitoring(containerId, buildId, progressToken) {
|
|
169
|
+
try {
|
|
170
|
+
console.log(`[Build] Setting up real-time streaming for build ${buildId} with token ${progressToken}`);
|
|
171
|
+
// Create build stream
|
|
172
|
+
const streamSession = await streamingService_1.streamingService.createStream({
|
|
173
|
+
streamType: 'build',
|
|
174
|
+
resourceId: containerId,
|
|
175
|
+
eventFilters: [
|
|
176
|
+
'build-started',
|
|
177
|
+
'build-step-completed',
|
|
178
|
+
'build-completed',
|
|
179
|
+
'build-failed',
|
|
180
|
+
'build-cancelled'
|
|
181
|
+
],
|
|
182
|
+
ttlMinutes: 120, // 2 hours
|
|
183
|
+
metadata: {
|
|
184
|
+
buildId,
|
|
185
|
+
progressToken
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// Track active stream
|
|
189
|
+
const activeStream = {
|
|
190
|
+
containerId,
|
|
191
|
+
buildId,
|
|
192
|
+
progressToken,
|
|
193
|
+
streamId: streamSession.streamId,
|
|
194
|
+
startTime: Date.now(),
|
|
195
|
+
lastProgress: 10,
|
|
196
|
+
status: 'active'
|
|
197
|
+
};
|
|
198
|
+
this.activeStreams.set(progressToken, activeStream);
|
|
199
|
+
this.streamAnalytics.totalBuilds++;
|
|
200
|
+
this.streamAnalytics.activeBuilds++;
|
|
201
|
+
// Subscribe to stream events
|
|
202
|
+
await streamingService_1.streamingService.subscribeToStream(streamSession.streamId, (event) => {
|
|
203
|
+
this.handleBuildStreamEvent(event, progressToken);
|
|
204
|
+
});
|
|
205
|
+
console.log(`[Build] Real-time streaming active for build ${buildId} (stream: ${streamSession.streamId})`);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.error(`[Build] Failed to setup streaming monitoring for container ${containerId}:`, {
|
|
209
|
+
error: error?.message || error,
|
|
210
|
+
stack: error?.stack,
|
|
211
|
+
buildId,
|
|
212
|
+
progressToken,
|
|
213
|
+
baseUrl: this.httpClient.defaults.baseURL
|
|
214
|
+
});
|
|
215
|
+
// Fallback to basic progress tracking if streaming fails
|
|
216
|
+
await this.storeProgressUpdate(progressToken, 10, 100, `Build started - streaming unavailable: ${error?.message || 'Unknown error'}`);
|
|
217
|
+
// Don't throw - let build continue even if streaming fails
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Handle real-time build stream events
|
|
222
|
+
*/
|
|
223
|
+
async handleBuildStreamEvent(event, progressToken) {
|
|
224
|
+
try {
|
|
225
|
+
const stream = this.activeStreams.get(progressToken);
|
|
226
|
+
if (!stream) {
|
|
227
|
+
console.warn(`[Build] Received event for unknown progress token: ${progressToken}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log(`[Build] Processing ${event.eventType} for build ${stream.buildId}: ${event.data.message}`);
|
|
231
|
+
// Convert stream event to progress update
|
|
232
|
+
const progressUpdate = this.convertStreamEventToProgress(event, progressToken, stream);
|
|
233
|
+
// Store progress update
|
|
234
|
+
await this.storeProgressUpdate(progressToken, progressUpdate.progress, progressUpdate.total, progressUpdate.message, progressUpdate.metadata);
|
|
235
|
+
// Update stream tracking
|
|
236
|
+
stream.lastProgress = progressUpdate.progress;
|
|
237
|
+
// Handle completion events
|
|
238
|
+
if (['build-completed', 'build-failed', 'build-cancelled'].includes(event.eventType)) {
|
|
239
|
+
const completionTime = Date.now() - stream.startTime;
|
|
240
|
+
this.completeStream(progressToken, event.eventType.replace('build-', ''), completionTime);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.error(`[Build] Error handling stream event:`, error);
|
|
245
|
+
this.streamAnalytics.errorsEncountered++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Convert stream event to progress update format
|
|
250
|
+
*/
|
|
251
|
+
convertStreamEventToProgress(event, progressToken, stream) {
|
|
252
|
+
const baseProgress = {
|
|
253
|
+
progressToken,
|
|
254
|
+
total: 100,
|
|
255
|
+
timestamp: event.timestamp,
|
|
256
|
+
metadata: {
|
|
257
|
+
eventType: event.eventType,
|
|
258
|
+
buildId: stream.buildId,
|
|
259
|
+
containerId: stream.containerId,
|
|
260
|
+
streamId: stream.streamId,
|
|
261
|
+
...event.data.details
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
switch (event.eventType) {
|
|
265
|
+
case 'build-started':
|
|
266
|
+
return {
|
|
267
|
+
...baseProgress,
|
|
268
|
+
progress: 0,
|
|
269
|
+
message: event.data.message || 'Build started'
|
|
270
|
+
};
|
|
271
|
+
case 'build-step-completed':
|
|
272
|
+
return {
|
|
273
|
+
...baseProgress,
|
|
274
|
+
progress: event.data.progress || 0,
|
|
275
|
+
message: event.data.message || `Step ${event.data.stepNumber}/${event.data.totalSteps} completed`
|
|
276
|
+
};
|
|
277
|
+
case 'build-completed':
|
|
278
|
+
return {
|
|
279
|
+
...baseProgress,
|
|
280
|
+
progress: 100,
|
|
281
|
+
message: event.data.message || 'Build completed successfully!'
|
|
282
|
+
};
|
|
283
|
+
case 'build-failed':
|
|
284
|
+
return {
|
|
285
|
+
...baseProgress,
|
|
286
|
+
progress: Math.max(stream.lastProgress, 0),
|
|
287
|
+
message: event.data.message || 'Build failed'
|
|
288
|
+
};
|
|
289
|
+
case 'build-cancelled':
|
|
290
|
+
return {
|
|
291
|
+
...baseProgress,
|
|
292
|
+
progress: Math.max(stream.lastProgress, 0),
|
|
293
|
+
message: event.data.message || 'Build cancelled'
|
|
294
|
+
};
|
|
295
|
+
default:
|
|
296
|
+
return {
|
|
297
|
+
...baseProgress,
|
|
298
|
+
progress: Math.max(stream.lastProgress, 0),
|
|
299
|
+
message: event.data.message || `Unknown event: ${event.eventType}`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Store progress update with expiration
|
|
305
|
+
*/
|
|
306
|
+
async storeProgressUpdate(progressToken, progress, total = 100, message, metadata) {
|
|
307
|
+
if (!progressToken)
|
|
308
|
+
return;
|
|
309
|
+
try {
|
|
310
|
+
const notificationData = {
|
|
311
|
+
progressToken,
|
|
312
|
+
progress,
|
|
313
|
+
total,
|
|
314
|
+
message,
|
|
315
|
+
timestamp: new Date().toISOString(),
|
|
316
|
+
expiresAt: Date.now() + (30 * 60 * 1000), // 30-minute expiration
|
|
317
|
+
...(metadata && { metadata })
|
|
318
|
+
};
|
|
319
|
+
this.progressUpdates.set(progressToken, notificationData);
|
|
320
|
+
this.streamAnalytics.notificationsSent++;
|
|
321
|
+
this.streamAnalytics.lastActivity = new Date().toISOString();
|
|
322
|
+
console.log(`[Build] Progress stored: ${progress}/${total} - ${message} (token: ${progressToken})`);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error(`[Build] Failed to store progress update:`, error);
|
|
326
|
+
this.streamAnalytics.errorsEncountered++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Complete stream monitoring and update analytics
|
|
331
|
+
*/
|
|
332
|
+
completeStream(progressToken, reason, completionTime) {
|
|
333
|
+
const stream = this.activeStreams.get(progressToken);
|
|
334
|
+
if (!stream)
|
|
335
|
+
return;
|
|
336
|
+
// Unsubscribe from stream
|
|
337
|
+
streamingService_1.streamingService.unsubscribeFromStream(stream.streamId).catch(error => {
|
|
338
|
+
console.error(`[Build] Error unsubscribing from stream ${stream.streamId}:`, error);
|
|
339
|
+
});
|
|
340
|
+
// Update analytics
|
|
341
|
+
this.streamAnalytics.activeBuilds = Math.max(0, this.streamAnalytics.activeBuilds - 1);
|
|
342
|
+
if (reason === 'completed') {
|
|
343
|
+
this.streamAnalytics.completedBuilds++;
|
|
344
|
+
if (completionTime) {
|
|
345
|
+
const currentAvg = this.streamAnalytics.averageBuildTime;
|
|
346
|
+
const totalCompleted = this.streamAnalytics.completedBuilds;
|
|
347
|
+
this.streamAnalytics.averageBuildTime =
|
|
348
|
+
((currentAvg * (totalCompleted - 1)) + completionTime) / totalCompleted;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (reason === 'failed') {
|
|
352
|
+
this.streamAnalytics.failedBuilds++;
|
|
353
|
+
}
|
|
354
|
+
// Clean up
|
|
355
|
+
this.activeStreams.delete(progressToken);
|
|
356
|
+
console.log(`[Build] Stream monitoring completed for ${stream.buildId} (${reason})`);
|
|
357
|
+
}
|
|
358
|
+
// Keep existing API methods unchanged for backward compatibility
|
|
407
359
|
/**
|
|
408
360
|
* Get build status
|
|
409
361
|
*/
|
|
@@ -412,14 +364,11 @@ class BuildService {
|
|
|
412
364
|
if (!params.containerId) {
|
|
413
365
|
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
414
366
|
}
|
|
415
|
-
// Use the build logs endpoint to get build status
|
|
416
|
-
// The server provides build logs at /v1/containers/{id}/builds/{buildId}
|
|
417
367
|
let endpoint = `/v1/containers/${params.containerId}/builds`;
|
|
418
368
|
if (params.buildId) {
|
|
419
369
|
endpoint += `/${params.buildId}`;
|
|
420
370
|
}
|
|
421
371
|
else {
|
|
422
|
-
// List all builds for the container and get the latest
|
|
423
372
|
const builds = await this.listBuilds({ containerId: params.containerId, limit: 1 });
|
|
424
373
|
if (builds.builds.length === 0) {
|
|
425
374
|
throw errorHandler_1.ErrorHandler.createNotFoundError(`No builds found for container ${params.containerId}`);
|
|
@@ -450,7 +399,6 @@ class BuildService {
|
|
|
450
399
|
async listBuilds(params = {}) {
|
|
451
400
|
try {
|
|
452
401
|
if (!params.containerId) {
|
|
453
|
-
// No global builds endpoint available, return empty list
|
|
454
402
|
return {
|
|
455
403
|
builds: [],
|
|
456
404
|
total: 0,
|
|
@@ -466,14 +414,12 @@ class BuildService {
|
|
|
466
414
|
queryParams.append('limit', params.limit.toString());
|
|
467
415
|
if (params.offset)
|
|
468
416
|
queryParams.append('offset', params.offset.toString());
|
|
469
|
-
// Use the container-specific builds endpoint (for build logs)
|
|
470
417
|
const endpoint = `/v1/containers/${params.containerId}/builds`;
|
|
471
418
|
const response = await this.httpClient.get(`${endpoint}?${queryParams.toString()}`);
|
|
472
419
|
if (!response.data) {
|
|
473
420
|
throw errorHandler_1.ErrorHandler.createServerError('Invalid response format from builds list API');
|
|
474
421
|
}
|
|
475
422
|
const data = response.data;
|
|
476
|
-
// Handle case where response is an array directly (build logs)
|
|
477
423
|
let builds = [];
|
|
478
424
|
let total = 0;
|
|
479
425
|
if (Array.isArray(data)) {
|
|
@@ -485,7 +431,6 @@ class BuildService {
|
|
|
485
431
|
total = data.total || data.builds.length;
|
|
486
432
|
}
|
|
487
433
|
else {
|
|
488
|
-
// No builds found
|
|
489
434
|
builds = [];
|
|
490
435
|
total = 0;
|
|
491
436
|
}
|
|
@@ -499,7 +444,6 @@ class BuildService {
|
|
|
499
444
|
}
|
|
500
445
|
catch (error) {
|
|
501
446
|
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
502
|
-
// No builds found, return empty list
|
|
503
447
|
return {
|
|
504
448
|
builds: [],
|
|
505
449
|
total: 0,
|
|
@@ -519,19 +463,15 @@ class BuildService {
|
|
|
519
463
|
if (!containerId) {
|
|
520
464
|
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
521
465
|
}
|
|
522
|
-
// The logs are actually contained within the build details response
|
|
523
|
-
// Use the build details endpoint instead of a separate logs endpoint
|
|
524
466
|
let endpoint = `/v1/containers/${containerId}/builds`;
|
|
525
467
|
if (buildId) {
|
|
526
468
|
endpoint += `/${buildId}`;
|
|
527
469
|
}
|
|
528
470
|
else {
|
|
529
|
-
// Get the latest build first
|
|
530
471
|
const builds = await this.listBuilds({ containerId, limit: 1 });
|
|
531
472
|
if (builds.builds.length === 0) {
|
|
532
|
-
return [];
|
|
473
|
+
return [];
|
|
533
474
|
}
|
|
534
|
-
// Use the latest build ID
|
|
535
475
|
buildId = builds.builds[0].id;
|
|
536
476
|
endpoint += `/${buildId}`;
|
|
537
477
|
}
|
|
@@ -539,13 +479,11 @@ class BuildService {
|
|
|
539
479
|
if (!response.data) {
|
|
540
480
|
return [];
|
|
541
481
|
}
|
|
542
|
-
// Handle the API response format where logs are in the 'entries' field
|
|
543
482
|
const buildData = response.data;
|
|
544
483
|
const entries = buildData.entries || [];
|
|
545
484
|
if (!Array.isArray(entries)) {
|
|
546
|
-
return [];
|
|
485
|
+
return [];
|
|
547
486
|
}
|
|
548
|
-
// Transform entries to BuildLogEntry format
|
|
549
487
|
return entries.map((entry, index) => ({
|
|
550
488
|
id: entry.id || `${containerId}-${buildId}-${index}`,
|
|
551
489
|
containerId: containerId,
|
|
@@ -559,135 +497,11 @@ class BuildService {
|
|
|
559
497
|
}
|
|
560
498
|
catch (error) {
|
|
561
499
|
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
562
|
-
return [];
|
|
500
|
+
return [];
|
|
563
501
|
}
|
|
564
502
|
throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.getBuildLogs');
|
|
565
503
|
}
|
|
566
504
|
}
|
|
567
|
-
/**
|
|
568
|
-
* Cancel a running build
|
|
569
|
-
*/
|
|
570
|
-
async cancelBuild(containerId, buildId) {
|
|
571
|
-
try {
|
|
572
|
-
if (!containerId) {
|
|
573
|
-
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
574
|
-
}
|
|
575
|
-
if (!buildId) {
|
|
576
|
-
throw errorHandler_1.ErrorHandler.createValidationError('Build ID is required');
|
|
577
|
-
}
|
|
578
|
-
await this.httpClient.post(`/v1/containers/${containerId}/builds/${buildId}/cancel`);
|
|
579
|
-
return {
|
|
580
|
-
success: true,
|
|
581
|
-
message: `Build ${buildId} cancelled successfully`
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
catch (error) {
|
|
585
|
-
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
586
|
-
throw errorHandler_1.ErrorHandler.createNotFoundError(`Build ${buildId} not found`);
|
|
587
|
-
}
|
|
588
|
-
throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.cancelBuild');
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Retry a failed build
|
|
593
|
-
*/
|
|
594
|
-
async retryBuild(containerId, buildId) {
|
|
595
|
-
try {
|
|
596
|
-
if (!containerId) {
|
|
597
|
-
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
598
|
-
}
|
|
599
|
-
if (!buildId) {
|
|
600
|
-
throw errorHandler_1.ErrorHandler.createValidationError('Build ID is required');
|
|
601
|
-
}
|
|
602
|
-
const response = await this.httpClient.post(`/v1/containers/${containerId}/builds/${buildId}/retry`);
|
|
603
|
-
if (!response.data) {
|
|
604
|
-
throw errorHandler_1.ErrorHandler.createServerError('Invalid response from build retry API');
|
|
605
|
-
}
|
|
606
|
-
return response.data;
|
|
607
|
-
}
|
|
608
|
-
catch (error) {
|
|
609
|
-
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
610
|
-
throw errorHandler_1.ErrorHandler.createNotFoundError(`Build ${buildId} not found`);
|
|
611
|
-
}
|
|
612
|
-
throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.retryBuild');
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Get build statistics
|
|
617
|
-
*/
|
|
618
|
-
async getBuildStats(containerId) {
|
|
619
|
-
try {
|
|
620
|
-
let endpoint = '/v1/builds/stats';
|
|
621
|
-
if (containerId) {
|
|
622
|
-
endpoint = `/v1/containers/${containerId}/builds/stats`;
|
|
623
|
-
}
|
|
624
|
-
const response = await this.httpClient.get(endpoint);
|
|
625
|
-
if (!response.data) {
|
|
626
|
-
return { total: 0, completed: 0, failed: 0, running: 0, pending: 0 };
|
|
627
|
-
}
|
|
628
|
-
return {
|
|
629
|
-
total: response.data.total || 0,
|
|
630
|
-
completed: response.data.completed || 0,
|
|
631
|
-
failed: response.data.failed || 0,
|
|
632
|
-
running: response.data.running || 0,
|
|
633
|
-
pending: response.data.pending || 0
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
catch (error) {
|
|
637
|
-
if (error instanceof errorHandler_1.ApiError && error.status === 404) {
|
|
638
|
-
return { total: 0, completed: 0, failed: 0, running: 0, pending: 0 };
|
|
639
|
-
}
|
|
640
|
-
throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.getBuildStats');
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
/**
|
|
644
|
-
* Stream build logs (for real-time updates)
|
|
645
|
-
*/
|
|
646
|
-
async streamBuildLogs(containerId, buildId, onLog, onError, onComplete) {
|
|
647
|
-
try {
|
|
648
|
-
if (!containerId) {
|
|
649
|
-
throw errorHandler_1.ErrorHandler.createValidationError('Container ID is required');
|
|
650
|
-
}
|
|
651
|
-
if (!buildId) {
|
|
652
|
-
throw errorHandler_1.ErrorHandler.createValidationError('Build ID is required');
|
|
653
|
-
}
|
|
654
|
-
// For now, implement polling-based streaming
|
|
655
|
-
// In a real implementation, this could use Server-Sent Events or WebSockets
|
|
656
|
-
let lastLogCount = 0;
|
|
657
|
-
const pollInterval = 2000; // 2 seconds
|
|
658
|
-
const poll = async () => {
|
|
659
|
-
try {
|
|
660
|
-
const logs = await this.getBuildLogs(containerId, buildId);
|
|
661
|
-
// Send only new logs
|
|
662
|
-
if (logs.length > lastLogCount) {
|
|
663
|
-
const newLogs = logs.slice(lastLogCount);
|
|
664
|
-
newLogs.forEach(log => onLog(log));
|
|
665
|
-
lastLogCount = logs.length;
|
|
666
|
-
}
|
|
667
|
-
// Check if build is complete
|
|
668
|
-
const buildStatus = await this.getBuildStatus({ containerId, buildId });
|
|
669
|
-
if (buildStatus.status === 'completed' || buildStatus.status === 'failed' || buildStatus.status === 'cancelled') {
|
|
670
|
-
if (onComplete) {
|
|
671
|
-
onComplete();
|
|
672
|
-
}
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
// Continue polling
|
|
676
|
-
setTimeout(poll, pollInterval);
|
|
677
|
-
}
|
|
678
|
-
catch (error) {
|
|
679
|
-
if (onError) {
|
|
680
|
-
onError(error);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
// Start polling
|
|
685
|
-
poll();
|
|
686
|
-
}
|
|
687
|
-
catch (error) {
|
|
688
|
-
throw errorHandler_1.ErrorHandler.processError(error, 'BuildService.streamBuildLogs');
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
505
|
}
|
|
692
506
|
exports.BuildService = BuildService;
|
|
693
507
|
// Export singleton instance
|