@phystack/device-phyos 4.5.55-dev → 4.5.57-dev
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/bin/phydevice +0 -0
- package/package.json +10 -10
- package/bin/index.js +0 -2
- package/dist/edge-hub.js +0 -1125
- package/dist/edge-hub.js.map +0 -1
- package/dist/http-proxy.js +0 -54
- package/dist/http-proxy.js.map +0 -1
- package/dist/index.js +0 -491
- package/dist/index.js.map +0 -1
- package/dist/methods/devdevice.js +0 -123
- package/dist/methods/devdevice.js.map +0 -1
- package/dist/methods/environment.js +0 -356
- package/dist/methods/environment.js.map +0 -1
- package/dist/methods/index.js +0 -24
- package/dist/methods/index.js.map +0 -1
- package/dist/methods/network/ca.js +0 -48
- package/dist/methods/network/ca.js.map +0 -1
- package/dist/methods/network/hostname.js +0 -56
- package/dist/methods/network/hostname.js.map +0 -1
- package/dist/methods/network/lan.js +0 -193
- package/dist/methods/network/lan.js.map +0 -1
- package/dist/methods/network/wifi.js +0 -299
- package/dist/methods/network/wifi.js.map +0 -1
- package/dist/methods/password.js +0 -87
- package/dist/methods/password.js.map +0 -1
- package/dist/methods/reboot.js +0 -22
- package/dist/methods/reboot.js.map +0 -1
- package/dist/methods/ssh.js +0 -148
- package/dist/methods/ssh.js.map +0 -1
- package/dist/services/scheduler.service.js +0 -335
- package/dist/services/scheduler.service.js.map +0 -1
- package/dist/store/types/network-interface.type.js +0 -3
- package/dist/store/types/network-interface.type.js.map +0 -1
- package/dist/time-sync.js +0 -119
- package/dist/time-sync.js.map +0 -1
- package/dist/types/command.types.js +0 -8
- package/dist/types/command.types.js.map +0 -1
- package/dist/types/container.types.js +0 -3
- package/dist/types/container.types.js.map +0 -1
- package/dist/types/event.types.js +0 -9
- package/dist/types/event.types.js.map +0 -1
- package/dist/types/job.types.js +0 -15
- package/dist/types/job.types.js.map +0 -1
- package/dist/types/twin.types.js +0 -34
- package/dist/types/twin.types.js.map +0 -1
- package/dist/utilities/audio-control.js +0 -919
- package/dist/utilities/audio-control.js.map +0 -1
- package/dist/utilities/docker-progress-tracker.js +0 -33
- package/dist/utilities/docker-progress-tracker.js.map +0 -1
- package/dist/utilities/docker.js +0 -1090
- package/dist/utilities/docker.js.map +0 -1
- package/dist/utilities/instances.js +0 -236
- package/dist/utilities/instances.js.map +0 -1
- package/dist/utilities/jobs.js +0 -103
- package/dist/utilities/jobs.js.map +0 -1
- package/dist/utilities/local-twins.js +0 -204
- package/dist/utilities/local-twins.js.map +0 -1
- package/dist/utilities/network-settings.js +0 -147
- package/dist/utilities/network-settings.js.map +0 -1
- package/dist/utilities/symlink.js +0 -47
- package/dist/utilities/symlink.js.map +0 -1
- package/dist/utilities/sysinfo.js +0 -128
- package/dist/utilities/sysinfo.js.map +0 -1
- package/dist/utilities/twin-manager.js +0 -108
- package/dist/utilities/twin-manager.js.map +0 -1
package/dist/utilities/docker.js
DELETED
|
@@ -1,1090 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const dockerode_1 = __importDefault(require("dockerode"));
|
|
7
|
-
const lodash_1 = require("lodash");
|
|
8
|
-
const phy_logger_1 = require("@phystack/phy-logger");
|
|
9
|
-
const twin_types_1 = require("../types/twin.types");
|
|
10
|
-
const async_mutex_1 = require("async-mutex");
|
|
11
|
-
const twin_manager_1 = require("./twin-manager");
|
|
12
|
-
const docker_progress_tracker_1 = require("./docker-progress-tracker");
|
|
13
|
-
const uuid_1 = require("uuid");
|
|
14
|
-
const hub_client_1 = require("@phystack/hub-client");
|
|
15
|
-
const fs_1 = __importDefault(require("fs"));
|
|
16
|
-
const path_1 = __importDefault(require("path"));
|
|
17
|
-
const IMAGE_PULL_TIMES_FILE = '/data/settings/phyhub/docker-image-pull-times.json';
|
|
18
|
-
class DockerManager {
|
|
19
|
-
docker;
|
|
20
|
-
logger;
|
|
21
|
-
twinInstancesRunning;
|
|
22
|
-
imagesInProgress;
|
|
23
|
-
instances;
|
|
24
|
-
firstRunCompleted = false;
|
|
25
|
-
hubDevice = false;
|
|
26
|
-
phyHubClient = null;
|
|
27
|
-
signalsClient;
|
|
28
|
-
sessionId = null;
|
|
29
|
-
operationLock = new async_mutex_1.Mutex();
|
|
30
|
-
imagePullsInProgress = new Map();
|
|
31
|
-
imagePullTimes = new Map();
|
|
32
|
-
constructor() {
|
|
33
|
-
this.docker = new dockerode_1.default();
|
|
34
|
-
this.logger = new phy_logger_1.PhyLogger({
|
|
35
|
-
logToFile: false,
|
|
36
|
-
logToConsole: true,
|
|
37
|
-
includeTrace: true,
|
|
38
|
-
namespace: 'DockerManager',
|
|
39
|
-
});
|
|
40
|
-
this.twinInstancesRunning = new Map();
|
|
41
|
-
this.imagesInProgress = new Map();
|
|
42
|
-
this.instances = new Map();
|
|
43
|
-
this.loadImagePullTimes();
|
|
44
|
-
this.cleanupStaleImagePullTimes().catch((error) => {
|
|
45
|
-
this.logger.warn('cleanupStaleImagePullTimes(): Failed to cleanup stale entries', error);
|
|
46
|
-
});
|
|
47
|
-
this.listenToDockerEvents();
|
|
48
|
-
}
|
|
49
|
-
async initializeHubSignals(initSignalsParams, dataResidency) {
|
|
50
|
-
try {
|
|
51
|
-
this.phyHubClient = await (0, hub_client_1.connectPhyClient)({ moduleName: 'docker-crash', dataResidency });
|
|
52
|
-
this.signalsClient = await this.phyHubClient.initializeSignals(initSignalsParams);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
this.logger.error('Failed to initialize signals client for docker-crash signals:', error);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async listenToDockerEvents() {
|
|
59
|
-
try {
|
|
60
|
-
const eventStream = await this.docker.getEvents({
|
|
61
|
-
filters: {
|
|
62
|
-
event: ['die', 'stop', 'kill'],
|
|
63
|
-
type: ['container'],
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
eventStream.on('data', async (buffer) => {
|
|
67
|
-
try {
|
|
68
|
-
const event = JSON.parse(buffer.toString());
|
|
69
|
-
if (event.status === 'die') {
|
|
70
|
-
const exitCode = parseInt(event.Actor?.Attributes?.exitCode || '0', 10);
|
|
71
|
-
if (exitCode !== 0) {
|
|
72
|
-
await this.onContainerCrash(event.id);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
this.logger.error('listenToDockerEvents(): Error processing Docker event:', error);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
eventStream.on('error', (error) => {
|
|
81
|
-
this.logger.error('listenToDockerEvents(): Docker event stream error:', error);
|
|
82
|
-
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
83
|
-
});
|
|
84
|
-
eventStream.on('end', () => {
|
|
85
|
-
this.logger.warn('listenToDockerEvents(): Docker event stream ended, attempting to reconnect...');
|
|
86
|
-
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
this.logger.error('listenToDockerEvents(): Failed to set up Docker event listener:', error);
|
|
91
|
-
setTimeout(() => this.listenToDockerEvents(), 5000);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
isCredentialError(error) {
|
|
95
|
-
const msg = error?.message || '';
|
|
96
|
-
return (msg.includes('unauthorized') ||
|
|
97
|
-
msg.includes('authentication required') ||
|
|
98
|
-
msg.includes('access denied') ||
|
|
99
|
-
msg.includes('credential') ||
|
|
100
|
-
error?.statusCode === 401);
|
|
101
|
-
}
|
|
102
|
-
getDefaultPlatform() {
|
|
103
|
-
const arch = process.arch;
|
|
104
|
-
if (arch === 'x64') {
|
|
105
|
-
return 'linux/amd64';
|
|
106
|
-
}
|
|
107
|
-
else if (arch === 'arm64') {
|
|
108
|
-
return 'linux/arm64';
|
|
109
|
-
}
|
|
110
|
-
else if (arch === 'arm') {
|
|
111
|
-
return 'linux/arm/v7';
|
|
112
|
-
}
|
|
113
|
-
else if (arch === 'ia32') {
|
|
114
|
-
return 'linux/386';
|
|
115
|
-
}
|
|
116
|
-
return 'linux/amd64';
|
|
117
|
-
}
|
|
118
|
-
extractNameFromImage = (image) => {
|
|
119
|
-
try {
|
|
120
|
-
const regex = /[^\/]+\.([^\/:]+:[^\/]+)$/;
|
|
121
|
-
const match = image.match(regex);
|
|
122
|
-
return match && match.length > 0 ? match[1].split(':')[0] : null;
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
async onContainerCrash(containerId) {
|
|
129
|
-
try {
|
|
130
|
-
const twinManager = await (0, twin_manager_1.getTwinManagerInstance)();
|
|
131
|
-
if (!twinManager) {
|
|
132
|
-
throw new Error('Error getting twin manager instance');
|
|
133
|
-
}
|
|
134
|
-
this.hubDevice = await twinManager.getHubDevice();
|
|
135
|
-
let twinId;
|
|
136
|
-
for (const [id, instance] of this.instances.entries()) {
|
|
137
|
-
if (instance.container.id === containerId) {
|
|
138
|
-
twinId = id;
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const container = this.docker.getContainer(containerId);
|
|
143
|
-
const logs = await container.logs({ stdout: true, stderr: true, tail: 50 });
|
|
144
|
-
const logString = logs.toString();
|
|
145
|
-
const coreDumpDir = '/var/coredumps';
|
|
146
|
-
let coreDumpInfo;
|
|
147
|
-
try {
|
|
148
|
-
if (fs_1.default.existsSync(coreDumpDir)) {
|
|
149
|
-
const containerInspect = await container.inspect();
|
|
150
|
-
const crashTime = containerInspect.State.FinishedAt
|
|
151
|
-
? new Date(containerInspect.State.FinishedAt).getTime() - 5000
|
|
152
|
-
: 0;
|
|
153
|
-
const files = fs_1.default.readdirSync(coreDumpDir);
|
|
154
|
-
const coreFiles = files
|
|
155
|
-
.filter(fileName => fileName.startsWith('core.'))
|
|
156
|
-
.map(fileName => ({
|
|
157
|
-
name: fileName,
|
|
158
|
-
mtime: fs_1.default.statSync(`${coreDumpDir}/${fileName}`).mtime.getTime(),
|
|
159
|
-
}))
|
|
160
|
-
.filter(coreFile => crashTime === 0 || coreFile.mtime >= crashTime)
|
|
161
|
-
.sort((firstFile, secondFile) => secondFile.mtime - firstFile.mtime);
|
|
162
|
-
if (coreFiles.length > 0) {
|
|
163
|
-
coreDumpInfo = {
|
|
164
|
-
files: [coreFiles[0].name],
|
|
165
|
-
count: coreFiles.length,
|
|
166
|
-
};
|
|
167
|
-
this.logger.info(`onContainerCrash(): Found core dump for container ${containerId}: ${coreFiles[0].name}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
this.logger.warn(`onContainerCrash(): Error checking for core dumps`, error);
|
|
173
|
-
}
|
|
174
|
-
if (twinId) {
|
|
175
|
-
const reportedProperties = {
|
|
176
|
-
lastError: {
|
|
177
|
-
message: `container crashed`,
|
|
178
|
-
timestamp: new Date(),
|
|
179
|
-
errorObject: {
|
|
180
|
-
containerLogs: logString,
|
|
181
|
-
coreDumps: coreDumpInfo,
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
this.hubDevice.reportEdgeTwinProperties(twinId, reportedProperties);
|
|
186
|
-
}
|
|
187
|
-
const deviceStatus = await this.hubDevice.getDeviceStatus();
|
|
188
|
-
const twinResponse = await this.hubDevice.getEdgeInstance({ twinId });
|
|
189
|
-
let twin = undefined;
|
|
190
|
-
if (twinResponse && twinResponse.twin) {
|
|
191
|
-
twin = twinResponse.twin;
|
|
192
|
-
}
|
|
193
|
-
let ip = '127.0.0.1';
|
|
194
|
-
if (deviceStatus.ip && deviceStatus.ip.length > 0) {
|
|
195
|
-
ip = deviceStatus.ip[0].ipv4;
|
|
196
|
-
}
|
|
197
|
-
if (!this.phyHubClient) {
|
|
198
|
-
const initSignalsParams = {
|
|
199
|
-
deviceId: deviceStatus.deviceId,
|
|
200
|
-
installationId: twin?.properties.desired.installationId ?? 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
201
|
-
spaceId: deviceStatus.spaceId,
|
|
202
|
-
tenantId: deviceStatus.tenantId,
|
|
203
|
-
appVersion: 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
204
|
-
appId: 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
205
|
-
installationVersion: 'XXXXXXXXXXXXXXXXXXXXXXXX',
|
|
206
|
-
environment: deviceStatus.gridEnv,
|
|
207
|
-
dataResidency: deviceStatus.dataResidency.toUpperCase(),
|
|
208
|
-
country: 'SE',
|
|
209
|
-
accessToken: deviceStatus.accessKey,
|
|
210
|
-
clientUserAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
211
|
-
ip,
|
|
212
|
-
};
|
|
213
|
-
await this.initializeHubSignals(initSignalsParams, deviceStatus.dataResidency.toUpperCase());
|
|
214
|
-
}
|
|
215
|
-
if (this.phyHubClient && this.signalsClient) {
|
|
216
|
-
if (!this.sessionId) {
|
|
217
|
-
const instance = this.signalsClient.getInstanceProps();
|
|
218
|
-
this.sessionId = instance.sessionId;
|
|
219
|
-
const clientId = instance.clientId ?? (0, uuid_1.v4)();
|
|
220
|
-
await this.hubDevice.sendSessionSignal({
|
|
221
|
-
deviceId: this.hubDevice.deviceId,
|
|
222
|
-
data: {
|
|
223
|
-
c: instance.tenantId,
|
|
224
|
-
b: this.sessionId,
|
|
225
|
-
d: instance.sessionCreated,
|
|
226
|
-
f: instance.environment,
|
|
227
|
-
g: instance.dataResidency,
|
|
228
|
-
m: instance.country,
|
|
229
|
-
n: instance.locationAccuracy,
|
|
230
|
-
o: instance.latitude,
|
|
231
|
-
p: instance.longitude,
|
|
232
|
-
h: instance.spaceId,
|
|
233
|
-
i: instance.appId,
|
|
234
|
-
j: instance.appVersion,
|
|
235
|
-
k: instance.installationId,
|
|
236
|
-
l: instance.installationVersion,
|
|
237
|
-
q: instance.deviceId,
|
|
238
|
-
r: clientId,
|
|
239
|
-
e: instance.clientIp ?? instance.ip,
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
await this.hubDevice.sendClientSignal({
|
|
243
|
-
deviceId: this.hubDevice.deviceId,
|
|
244
|
-
data: {
|
|
245
|
-
c: instance.tenantId,
|
|
246
|
-
r: clientId,
|
|
247
|
-
ah: instance.clientCreated,
|
|
248
|
-
aj: instance.clientUserAgent,
|
|
249
|
-
g: instance.dataResidency,
|
|
250
|
-
m: instance.country,
|
|
251
|
-
o: instance.latitude,
|
|
252
|
-
p: instance.longitude,
|
|
253
|
-
n: instance.locationAccuracy,
|
|
254
|
-
e: instance.ip ?? instance.clientIp ?? undefined,
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
const timeWithoutZ = new Date().toISOString().replace(/Z$/, '');
|
|
259
|
-
let coreDumpPath;
|
|
260
|
-
if (coreDumpInfo && coreDumpInfo.files && coreDumpInfo.files.length > 0) {
|
|
261
|
-
const fileName = coreDumpInfo.files[0];
|
|
262
|
-
coreDumpPath = `${coreDumpDir}/${fileName}`;
|
|
263
|
-
}
|
|
264
|
-
const eventPayload = {
|
|
265
|
-
g: deviceStatus.dataResidency.toUpperCase(),
|
|
266
|
-
b: this.sessionId,
|
|
267
|
-
t: timeWithoutZ,
|
|
268
|
-
s: 'MONITOR_CONTAINER_CRASH',
|
|
269
|
-
u: false,
|
|
270
|
-
h: deviceStatus.spaceId,
|
|
271
|
-
ac: twinId,
|
|
272
|
-
ad: deviceStatus.deviceId,
|
|
273
|
-
c: deviceStatus.tenantId,
|
|
274
|
-
};
|
|
275
|
-
if (coreDumpPath) {
|
|
276
|
-
eventPayload.ae = coreDumpPath;
|
|
277
|
-
}
|
|
278
|
-
await this.hubDevice.sendEventSignal({
|
|
279
|
-
deviceId: this.hubDevice.deviceId,
|
|
280
|
-
data: eventPayload,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
this.logger.error(`onContainerCrash(): Error in reporting the container crash`, error);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
async imageExists(image) {
|
|
289
|
-
this.logger.info(`imageExists(): Checking if docker image ${image} exists`);
|
|
290
|
-
try {
|
|
291
|
-
await this.docker.getImage(image).inspect();
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
async pullImage(image, authConfig, abortController) {
|
|
299
|
-
this.logger.info(`pullImage(): Pulling docker image ${image}`);
|
|
300
|
-
return new Promise((resolve, reject) => {
|
|
301
|
-
const platform = this.getDefaultPlatform();
|
|
302
|
-
this.logger.info(`pullImage(): Using platform ${platform} for image ${image}`);
|
|
303
|
-
if (abortController.signal.aborted) {
|
|
304
|
-
this.logger.info(`pullImage(): Pull for image ${image} was aborted before starting`);
|
|
305
|
-
return resolve();
|
|
306
|
-
}
|
|
307
|
-
let isCompleted = false;
|
|
308
|
-
const streamErrors = [];
|
|
309
|
-
const abortHandler = () => {
|
|
310
|
-
if (isCompleted)
|
|
311
|
-
return;
|
|
312
|
-
this.logger.info(`pullImage(): Pull for image ${image} was aborted`);
|
|
313
|
-
isCompleted = true;
|
|
314
|
-
const imageProgress = this.imagesInProgress.get(image);
|
|
315
|
-
if (imageProgress) {
|
|
316
|
-
imageProgress.inProgress = false;
|
|
317
|
-
this.imagesInProgress.delete(image);
|
|
318
|
-
}
|
|
319
|
-
resolve();
|
|
320
|
-
};
|
|
321
|
-
abortController.signal.addEventListener('abort', abortHandler);
|
|
322
|
-
const timeoutId = setTimeout(() => {
|
|
323
|
-
if (!isCompleted) {
|
|
324
|
-
this.logger.error(`pullImage(): Timeout waiting for image ${image} to pull`);
|
|
325
|
-
abortController.abort();
|
|
326
|
-
}
|
|
327
|
-
}, 7200000);
|
|
328
|
-
const progressTracker = new docker_progress_tracker_1.DockerProgressTracker(image, 'DockerManager');
|
|
329
|
-
const imageProgress = {
|
|
330
|
-
inProgress: true,
|
|
331
|
-
layersProgress: new Map(),
|
|
332
|
-
};
|
|
333
|
-
this.imagesInProgress.set(image, imageProgress);
|
|
334
|
-
this.docker.pull(image, {
|
|
335
|
-
authconfig: authConfig,
|
|
336
|
-
platform,
|
|
337
|
-
architecture: process.arch,
|
|
338
|
-
os: 'linux',
|
|
339
|
-
}, (err, stream) => {
|
|
340
|
-
if (err) {
|
|
341
|
-
clearTimeout(timeoutId);
|
|
342
|
-
this.logger.error(`pullImage(): Error initiating pull for image ${image}`, err);
|
|
343
|
-
if (this.isCredentialError(err)) {
|
|
344
|
-
this.logger.error(`pullImage(): Credential error detected for image ${image}, releasing lock immediately`);
|
|
345
|
-
const imageProgress = this.imagesInProgress.get(image);
|
|
346
|
-
if (imageProgress) {
|
|
347
|
-
imageProgress.inProgress = false;
|
|
348
|
-
this.imagesInProgress.delete(image);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
isCompleted = true;
|
|
352
|
-
progressTracker.markFailed(err);
|
|
353
|
-
return reject(err);
|
|
354
|
-
}
|
|
355
|
-
if (abortController.signal.aborted) {
|
|
356
|
-
clearTimeout(timeoutId);
|
|
357
|
-
abortHandler();
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
this.docker.modem.followProgress(stream, (err, output) => {
|
|
361
|
-
clearTimeout(timeoutId);
|
|
362
|
-
if (isCompleted)
|
|
363
|
-
return;
|
|
364
|
-
const actualError = err ||
|
|
365
|
-
(streamErrors.length > 0
|
|
366
|
-
? new Error(`Pull failed with ${streamErrors.length} error(s): ${streamErrors.map(streamError => streamError.message).join('; ')}`)
|
|
367
|
-
: null);
|
|
368
|
-
if (actualError) {
|
|
369
|
-
this.logger.error(`pullImage(): Error pulling image ${image}`, actualError);
|
|
370
|
-
if (streamErrors.length > 0) {
|
|
371
|
-
this.logger.error(`pullImage(): Stream errors captured during pull:`, streamErrors.map(streamError => streamError.message));
|
|
372
|
-
}
|
|
373
|
-
imageProgress.inProgress = false;
|
|
374
|
-
this.imagesInProgress.delete(image);
|
|
375
|
-
isCompleted = true;
|
|
376
|
-
progressTracker.markFailed(actualError);
|
|
377
|
-
reject(actualError);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
this.logger.info(`pullImage(): Image ${image} download complete`);
|
|
381
|
-
(async () => {
|
|
382
|
-
try {
|
|
383
|
-
const imageExists = await this.imageExists(image);
|
|
384
|
-
if (!imageExists) {
|
|
385
|
-
const errorMsg = `Image ${image} pull reported success but image is not available locally. ` +
|
|
386
|
-
`This may indicate a network issue, proxy problem, or the image does not exist.`;
|
|
387
|
-
this.logger.error(`pullImage(): ${errorMsg}`);
|
|
388
|
-
imageProgress.inProgress = false;
|
|
389
|
-
this.imagesInProgress.delete(image);
|
|
390
|
-
isCompleted = true;
|
|
391
|
-
progressTracker.markFailed(new Error(errorMsg));
|
|
392
|
-
reject(new Error(errorMsg));
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
imageProgress.inProgress = false;
|
|
396
|
-
this.imagesInProgress.delete(image);
|
|
397
|
-
isCompleted = true;
|
|
398
|
-
progressTracker.markComplete();
|
|
399
|
-
(async () => {
|
|
400
|
-
try {
|
|
401
|
-
const imageObject = this.docker.getImage(image);
|
|
402
|
-
const imageInspection = await imageObject.inspect();
|
|
403
|
-
const imageId = imageInspection.Id;
|
|
404
|
-
const pullTimestamp = Math.floor(Date.now() / 1000);
|
|
405
|
-
this.imagePullTimes.set(imageId, pullTimestamp);
|
|
406
|
-
const pullTimeFormatted = new Date(pullTimestamp * 1000).toISOString();
|
|
407
|
-
this.logger.info(`pullImage(): Recorded pull time for ${image} ` +
|
|
408
|
-
`(ID: ${imageId.substring(0, 12)}): ${pullTimeFormatted}`);
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
this.logger.warn(`pullImage(): Failed to record pull time for ${image}`, error);
|
|
412
|
-
}
|
|
413
|
-
})();
|
|
414
|
-
this.logger.info(`pullImage(): Starting image management for ${image}`);
|
|
415
|
-
this.manageDockerImages(image).catch((error) => {
|
|
416
|
-
this.logger.error(`pullImage(): Error managing images after pull for ${image}`, error);
|
|
417
|
-
});
|
|
418
|
-
resolve();
|
|
419
|
-
}
|
|
420
|
-
catch (verifyError) {
|
|
421
|
-
const errorMsg = `Failed to verify image ${image} exists after pull: ` +
|
|
422
|
-
`${verifyError.message || verifyError}`;
|
|
423
|
-
this.logger.error(`pullImage(): ${errorMsg}`, verifyError);
|
|
424
|
-
imageProgress.inProgress = false;
|
|
425
|
-
this.imagesInProgress.delete(image);
|
|
426
|
-
isCompleted = true;
|
|
427
|
-
progressTracker.markFailed(verifyError);
|
|
428
|
-
reject(new Error(errorMsg));
|
|
429
|
-
}
|
|
430
|
-
})();
|
|
431
|
-
}, (event) => {
|
|
432
|
-
if (abortController.signal.aborted || isCompleted) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (event.error || event.errorDetail) {
|
|
436
|
-
const errorMsg = event.errorDetail?.message || event.error || 'Unknown pull error';
|
|
437
|
-
this.logger.error(`pullImage(): Error event during pull for ${image}: ${errorMsg}`, { event });
|
|
438
|
-
streamErrors.push(new Error(errorMsg));
|
|
439
|
-
}
|
|
440
|
-
progressTracker.processEvent(event);
|
|
441
|
-
if (event.id && event.progressDetail) {
|
|
442
|
-
imageProgress.layersProgress.set(event.id, event.progressDetail);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
async createContainer(twin, skipGpu = false) {
|
|
449
|
-
const { image, createOptions } = twin.properties.desired;
|
|
450
|
-
const parsedOptions = createOptions;
|
|
451
|
-
this.logger.info(`createContainer(): Twin object for ${twin.id}:`, JSON.stringify(twin, null, 2));
|
|
452
|
-
this.logger.info(`createContainer(): Parsed options for ${twin.id}:`, JSON.stringify(parsedOptions, null, 2));
|
|
453
|
-
this.logger.info(`createContainer(): Device requests for ${twin.id}:`, JSON.stringify(parsedOptions?.HostConfig?.DeviceRequests, null, 2));
|
|
454
|
-
const isHostNetwork = parsedOptions?.HostConfig?.NetworkMode === 'host';
|
|
455
|
-
const existingBinds = parsedOptions?.HostConfig?.Binds ?? [];
|
|
456
|
-
const coreDumpHostDir = '/var/coredumps';
|
|
457
|
-
try {
|
|
458
|
-
if (!fs_1.default.existsSync(coreDumpHostDir)) {
|
|
459
|
-
fs_1.default.mkdirSync(coreDumpHostDir, { recursive: true, mode: 0o777 });
|
|
460
|
-
this.logger.info(`createContainer(): Created core dump directory ${coreDumpHostDir}`);
|
|
461
|
-
}
|
|
462
|
-
try {
|
|
463
|
-
fs_1.default.chmodSync(coreDumpHostDir, 0o777);
|
|
464
|
-
this.logger.info(`createContainer(): Set permissions on ${coreDumpHostDir} to 777`);
|
|
465
|
-
}
|
|
466
|
-
catch (chmodError) {
|
|
467
|
-
this.logger.error(`createContainer(): CRITICAL: Could not set permissions on ${coreDumpHostDir}. ` +
|
|
468
|
-
`Container may not be able to write coredumps. Error:`, chmodError);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
this.logger.error(`createContainer(): Failed to create or verify ${coreDumpHostDir}, mount may fail`, error);
|
|
473
|
-
}
|
|
474
|
-
const coreDumpBind = '/var/coredumps:/var/coredumps:z';
|
|
475
|
-
const hasCoreDumpBind = existingBinds.some((bind) => bind.includes('/var/coredumps'));
|
|
476
|
-
const allBinds = hasCoreDumpBind ? existingBinds : [...existingBinds, coreDumpBind];
|
|
477
|
-
const existingUlimits = parsedOptions?.HostConfig?.Ulimits ?? [];
|
|
478
|
-
const hasCoreUlimit = existingUlimits.some((ulimit) => ulimit.Name === 'core');
|
|
479
|
-
const coreUlimits = hasCoreUlimit
|
|
480
|
-
? existingUlimits
|
|
481
|
-
: [
|
|
482
|
-
...existingUlimits,
|
|
483
|
-
{ Name: 'core', Soft: -1, Hard: -1 },
|
|
484
|
-
];
|
|
485
|
-
const containerConfig = {
|
|
486
|
-
Image: image,
|
|
487
|
-
Env: [
|
|
488
|
-
...(parsedOptions?.Env ?? []),
|
|
489
|
-
`TWIN_ID=${twin.id}`,
|
|
490
|
-
...(process.env.TZ ? [`TZ=${process.env.TZ}`] : []),
|
|
491
|
-
],
|
|
492
|
-
Entrypoint: parsedOptions?.Entrypoint,
|
|
493
|
-
Cmd: parsedOptions?.Cmd,
|
|
494
|
-
HostConfig: {
|
|
495
|
-
...parsedOptions?.HostConfig,
|
|
496
|
-
NetworkMode: isHostNetwork ? 'host' : 'default',
|
|
497
|
-
ExtraHosts: [isHostNetwork ? `phyos:127.0.0.1` : `phyos:172.26.128.1`],
|
|
498
|
-
RestartPolicy: parsedOptions?.HostConfig?.RestartPolicy ?? { Name: 'always' },
|
|
499
|
-
Privileged: parsedOptions?.HostConfig?.Privileged ?? false,
|
|
500
|
-
Memory: parsedOptions?.HostConfig?.Memory ?? 0,
|
|
501
|
-
CpuShares: parsedOptions?.HostConfig?.CpuShares ?? 0,
|
|
502
|
-
StorageOpt: parsedOptions?.HostConfig?.StorageOpt ?? {},
|
|
503
|
-
DiskQuota: parsedOptions?.HostConfig?.DiskQuota ?? 0,
|
|
504
|
-
ReadonlyRootfs: parsedOptions?.HostConfig?.ReadonlyRootfs ?? false,
|
|
505
|
-
BlkioWeight: parsedOptions?.HostConfig?.BlkioWeight ?? 0,
|
|
506
|
-
BlkioDeviceReadBps: parsedOptions?.HostConfig?.BlkioDeviceReadBps ?? [],
|
|
507
|
-
BlkioDeviceWriteBps: parsedOptions?.HostConfig?.BlkioDeviceWriteBps ?? [],
|
|
508
|
-
Devices: parsedOptions?.HostConfig?.Devices ?? [],
|
|
509
|
-
DeviceRequests: parsedOptions?.HostConfig?.DeviceRequests ?? [],
|
|
510
|
-
Binds: allBinds,
|
|
511
|
-
Ulimits: coreUlimits,
|
|
512
|
-
},
|
|
513
|
-
};
|
|
514
|
-
if (skipGpu && parsedOptions?.HostConfig?.DeviceRequests) {
|
|
515
|
-
this.logger.info(`createContainer(): Original DeviceRequests before filtering:`, JSON.stringify(parsedOptions.HostConfig.DeviceRequests, null, 2));
|
|
516
|
-
const filteredDeviceRequests = parsedOptions.HostConfig.DeviceRequests.filter((request) => {
|
|
517
|
-
const isGpuRequest = request.Capabilities &&
|
|
518
|
-
request.Capabilities.some((cap) => cap.includes('gpu'));
|
|
519
|
-
return !isGpuRequest;
|
|
520
|
-
});
|
|
521
|
-
if (containerConfig.HostConfig) {
|
|
522
|
-
containerConfig.HostConfig.DeviceRequests = filteredDeviceRequests;
|
|
523
|
-
this.logger.info(`createContainer(): Creating container for twin ${twin.id} without GPU support. Filtered DeviceRequests:`, JSON.stringify(containerConfig.HostConfig.DeviceRequests, null, 2));
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else if (parsedOptions?.HostConfig?.DeviceRequests) {
|
|
527
|
-
this.logger.info(`createContainer(): Original DeviceRequests:`, JSON.stringify(parsedOptions.HostConfig.DeviceRequests, null, 2));
|
|
528
|
-
if (containerConfig.HostConfig) {
|
|
529
|
-
containerConfig.HostConfig.DeviceRequests = parsedOptions.HostConfig.DeviceRequests;
|
|
530
|
-
const hasGpuRequest = parsedOptions.HostConfig.DeviceRequests.some((req) => req.Capabilities && req.Capabilities.some((cap) => cap.includes('gpu')));
|
|
531
|
-
if (hasGpuRequest) {
|
|
532
|
-
try {
|
|
533
|
-
const info = await this.docker.info();
|
|
534
|
-
const availableRuntimes = info.Runtimes ? Object.keys(info.Runtimes) : [];
|
|
535
|
-
this.logger.info(`Available Docker runtimes:`, JSON.stringify(availableRuntimes, null, 2));
|
|
536
|
-
if (availableRuntimes.includes('nvidia')) {
|
|
537
|
-
containerConfig.HostConfig.Runtime = 'nvidia';
|
|
538
|
-
this.logger.info(`createContainer(): Setting nvidia runtime for GPU support`);
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
this.logger.warn(`createContainer(): NVIDIA runtime not available, GPU may not work`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
catch (error) {
|
|
545
|
-
this.logger.warn(`createContainer(): Error checking for available runtimes:`, error);
|
|
546
|
-
containerConfig.HostConfig.Runtime = 'nvidia';
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
this.logger.info(`createContainer(): Applied DeviceRequests to container config:`, JSON.stringify(containerConfig.HostConfig.DeviceRequests, null, 2));
|
|
550
|
-
}
|
|
551
|
-
if (parsedOptions.HostConfig.DeviceRequests.some((req) => req.Capabilities && req.Capabilities.some((cap) => cap.includes('gpu')))) {
|
|
552
|
-
this.logger.info(`createContainer(): Creating container for twin ${twin.id} with GPU support`);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
if (!isHostNetwork) {
|
|
556
|
-
containerConfig.NetworkingConfig = {
|
|
557
|
-
EndpointsConfig: {
|
|
558
|
-
'phyos-edge-net': {
|
|
559
|
-
Aliases: [twin.id],
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
this.logger.info(`createContainer(): Creating container for twin ${twin.id}`, {
|
|
565
|
-
containerConfig,
|
|
566
|
-
});
|
|
567
|
-
this.logger.info(`createContainer(): Final container config for twin ${twin.id}:`, JSON.stringify(containerConfig, null, 2));
|
|
568
|
-
const container = await this.docker.createContainer(containerConfig);
|
|
569
|
-
try {
|
|
570
|
-
const containerDetails = await container.inspect();
|
|
571
|
-
this.logger.info(`createContainer(): Created container details for twin ${twin.id}:`, JSON.stringify(containerDetails, null, 2));
|
|
572
|
-
}
|
|
573
|
-
catch (error) {
|
|
574
|
-
this.logger.error(`createContainer(): Failed to inspect container for twin ${twin.id}:`, error);
|
|
575
|
-
}
|
|
576
|
-
return container;
|
|
577
|
-
}
|
|
578
|
-
async _startContainer(twin) {
|
|
579
|
-
if (this.twinInstancesRunning.get(twin.id)) {
|
|
580
|
-
this.logger.info(`startContainer(): Container for twin ${twin.id} is already running or starting`);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
this.twinInstancesRunning.set(twin.id, true);
|
|
584
|
-
try {
|
|
585
|
-
this.logger.info(`startContainer(): Attempting to create container for twin ${twin.id} with GPU support`);
|
|
586
|
-
try {
|
|
587
|
-
const container = await this.createContainer(twin, false);
|
|
588
|
-
this.logger.info(`startContainer(): Container created for twin ${twin.id}, preparing to start it`);
|
|
589
|
-
this.instances.set(twin.id, { container, twin });
|
|
590
|
-
await container.start();
|
|
591
|
-
this.logger.info(`startContainer(): Started container for twin ${twin.id}`);
|
|
592
|
-
const logs = await container.logs({ stdout: true, stderr: true });
|
|
593
|
-
this.logger.info(`Container logs for twin ${twin.id}:`, logs.toString());
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
this.logger.error(`startContainer(): Error starting container with GPU for twin ${twin.id}:`, error.message);
|
|
597
|
-
const isGpuError = error.message &&
|
|
598
|
-
((error.message.includes('could not select device driver') &&
|
|
599
|
-
error.message.includes('capabilities: [[gpu]]')) ||
|
|
600
|
-
error.message.includes('nvidia-container-cli: initialization error: nvml error: driver not loaded') ||
|
|
601
|
-
error.message.includes('nvidia-container-cli') ||
|
|
602
|
-
error.message.includes('unknown or invalid runtime name: nvidia') ||
|
|
603
|
-
error.message.includes('nvidia runtime') ||
|
|
604
|
-
error.message.includes('GPU') ||
|
|
605
|
-
error.message.includes('gpu'));
|
|
606
|
-
if (isGpuError) {
|
|
607
|
-
this.logger.warn(`startContainer(): GPU support not available for twin ${twin.id}, retrying without GPU capabilities. Error: ${error.message}`, { error: error.message });
|
|
608
|
-
const existingInstance = this.instances.get(twin.id);
|
|
609
|
-
if (existingInstance?.container) {
|
|
610
|
-
try {
|
|
611
|
-
await existingInstance.container.remove({ force: true });
|
|
612
|
-
this.logger.info(`startContainer(): Removed failed GPU container for twin ${twin.id}`);
|
|
613
|
-
}
|
|
614
|
-
catch (removeError) {
|
|
615
|
-
this.logger.info(`startContainer(): Could not remove container for twin ${twin.id}, may not exist`);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
this.instances.delete(twin.id);
|
|
619
|
-
const container = await this.createContainer(twin, true);
|
|
620
|
-
this.instances.set(twin.id, { container, twin });
|
|
621
|
-
await container.start();
|
|
622
|
-
this.logger.info(`startContainer(): Started container for twin ${twin.id} without GPU support`);
|
|
623
|
-
const logs = await container.logs({ stdout: true, stderr: true });
|
|
624
|
-
this.logger.info(`Container logs for twin ${twin.id}:`, logs.toString());
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
throw error;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
catch (error) {
|
|
632
|
-
this.logger.error(`startContainer(): Failed to start container for twin ${twin.id}`, error);
|
|
633
|
-
if (error.message.includes('port is already allocated')) {
|
|
634
|
-
this.logger.error(`startContainer(): Port conflict detected for twin ${twin.id}`);
|
|
635
|
-
}
|
|
636
|
-
this.instances.delete(twin.id);
|
|
637
|
-
this.twinInstancesRunning.set(twin.id, false);
|
|
638
|
-
throw error;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
abortImagePull(image) {
|
|
642
|
-
const pullInfo = this.imagePullsInProgress.get(image);
|
|
643
|
-
if (pullInfo) {
|
|
644
|
-
this.logger.info(`abortImagePull(): Aborting pull for image ${image}`);
|
|
645
|
-
pullInfo.abortController.abort();
|
|
646
|
-
const imageProgress = this.imagesInProgress.get(image);
|
|
647
|
-
if (imageProgress) {
|
|
648
|
-
imageProgress.inProgress = false;
|
|
649
|
-
this.imagesInProgress.delete(image);
|
|
650
|
-
}
|
|
651
|
-
setTimeout(() => {
|
|
652
|
-
if (this.imagePullsInProgress.has(image)) {
|
|
653
|
-
this.logger.warn(`abortImagePull(): Cleaning up stale pull info for ${image}`);
|
|
654
|
-
this.imagePullsInProgress.delete(image);
|
|
655
|
-
}
|
|
656
|
-
}, 5000);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
isImagePullInProgress(image) {
|
|
660
|
-
return this.imagePullsInProgress.has(image);
|
|
661
|
-
}
|
|
662
|
-
async ensureImageAvailable(image, authConfig) {
|
|
663
|
-
const imageExists = await this.imageExists(image);
|
|
664
|
-
if (imageExists)
|
|
665
|
-
return true;
|
|
666
|
-
const existingPull = this.imagePullsInProgress.get(image);
|
|
667
|
-
if (existingPull) {
|
|
668
|
-
this.logger.info(`ensureImageAvailable(): Pull already in progress for ${image}, waiting for it to complete`);
|
|
669
|
-
try {
|
|
670
|
-
await existingPull.promise;
|
|
671
|
-
const imageExists = await this.imageExists(image);
|
|
672
|
-
if (!imageExists) {
|
|
673
|
-
const errorMsg = `Existing pull for ${image} reported success but image is not available locally`;
|
|
674
|
-
this.logger.error(`ensureImageAvailable(): ${errorMsg}`);
|
|
675
|
-
this.imagePullsInProgress.delete(image);
|
|
676
|
-
return false;
|
|
677
|
-
}
|
|
678
|
-
this.logger.info(`ensureImageAvailable(): Existing pull for ${image} completed successfully ` +
|
|
679
|
-
`and image verified locally`);
|
|
680
|
-
return true;
|
|
681
|
-
}
|
|
682
|
-
catch (error) {
|
|
683
|
-
this.logger.error(`ensureImageAvailable(): Existing pull for ${image} failed`, error);
|
|
684
|
-
if (this.isCredentialError(error)) {
|
|
685
|
-
this.logger.error(`ensureImageAvailable(): Credential error detected for existing pull of ${image}`);
|
|
686
|
-
}
|
|
687
|
-
this.imagePullsInProgress.delete(image);
|
|
688
|
-
return false;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
this.logger.info(`ensureImageAvailable(): Starting new pull for ${image}`);
|
|
692
|
-
const abortController = new AbortController();
|
|
693
|
-
const pullPromise = this.pullImage(image, authConfig, abortController).finally(() => {
|
|
694
|
-
this.imagePullsInProgress.delete(image);
|
|
695
|
-
});
|
|
696
|
-
this.imagePullsInProgress.set(image, {
|
|
697
|
-
promise: pullPromise,
|
|
698
|
-
abortController,
|
|
699
|
-
});
|
|
700
|
-
try {
|
|
701
|
-
await pullPromise;
|
|
702
|
-
const imageExists = await this.imageExists(image);
|
|
703
|
-
if (!imageExists) {
|
|
704
|
-
const errorMsg = `Pull for ${image} reported success but image is not available locally`;
|
|
705
|
-
this.logger.error(`ensureImageAvailable(): ${errorMsg}`);
|
|
706
|
-
return false;
|
|
707
|
-
}
|
|
708
|
-
this.logger.info(`ensureImageAvailable(): Pull for ${image} completed successfully ` +
|
|
709
|
-
`and image verified locally`);
|
|
710
|
-
return true;
|
|
711
|
-
}
|
|
712
|
-
catch (error) {
|
|
713
|
-
this.logger.error(`ensureImageAvailable(): Pull failed for ${image}`, error);
|
|
714
|
-
if (this.isCredentialError(error)) {
|
|
715
|
-
this.logger.error(`ensureImageAvailable(): Credential error detected for pull of ${image}`);
|
|
716
|
-
}
|
|
717
|
-
return false;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
async startEdgeApp(twin) {
|
|
721
|
-
const { image } = twin.properties.desired;
|
|
722
|
-
if (!image) {
|
|
723
|
-
return {
|
|
724
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
725
|
-
error: {
|
|
726
|
-
message: 'No image found in desired properties',
|
|
727
|
-
timestamp: new Date(),
|
|
728
|
-
},
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
const hasRegistry = image.includes('/');
|
|
732
|
-
let authConfig;
|
|
733
|
-
let pullError = null;
|
|
734
|
-
if (hasRegistry) {
|
|
735
|
-
const [hostname] = image.split('/');
|
|
736
|
-
authConfig = twin.properties.desired.credentials;
|
|
737
|
-
if (!authConfig) {
|
|
738
|
-
this.logger.error(`startEdgeApp(): No credentials found for registry: ${hostname}`);
|
|
739
|
-
return {
|
|
740
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
741
|
-
error: {
|
|
742
|
-
message: `No credentials found for registry: ${hostname}`,
|
|
743
|
-
timestamp: new Date(),
|
|
744
|
-
},
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
try {
|
|
748
|
-
const pullSuccess = await this.ensureImageAvailable(image, {
|
|
749
|
-
username: authConfig.username,
|
|
750
|
-
password: authConfig.password,
|
|
751
|
-
serveraddress: authConfig.address,
|
|
752
|
-
});
|
|
753
|
-
if (!pullSuccess) {
|
|
754
|
-
this.logger.error(`startEdgeApp(): Failed to pull image ${image}`);
|
|
755
|
-
return {
|
|
756
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
757
|
-
error: {
|
|
758
|
-
message: `Failed to pull image ${image}`,
|
|
759
|
-
timestamp: new Date(),
|
|
760
|
-
},
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
catch (error) {
|
|
765
|
-
this.logger.error(`startEdgeApp(): Error pulling image ${image}`, error);
|
|
766
|
-
const errorMsg = error.message || '';
|
|
767
|
-
return {
|
|
768
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
769
|
-
error: {
|
|
770
|
-
message: this.isCredentialError(error)
|
|
771
|
-
? `Authentication failed for image ${image}. Please check your credentials.`
|
|
772
|
-
: `Failed to pull image ${image}: ${errorMsg}`,
|
|
773
|
-
timestamp: new Date(),
|
|
774
|
-
errorObject: error,
|
|
775
|
-
},
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
else {
|
|
780
|
-
try {
|
|
781
|
-
const pullSuccess = await this.ensureImageAvailable(image, {});
|
|
782
|
-
if (!pullSuccess) {
|
|
783
|
-
this.logger.error(`startEdgeApp(): Failed to pull local image ${image}`);
|
|
784
|
-
return {
|
|
785
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
786
|
-
error: {
|
|
787
|
-
message: `Failed to pull local image ${image}`,
|
|
788
|
-
timestamp: new Date(),
|
|
789
|
-
},
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
catch (error) {
|
|
794
|
-
this.logger.error(`startEdgeApp(): Error pulling local image ${image}`, error);
|
|
795
|
-
return {
|
|
796
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
797
|
-
error: {
|
|
798
|
-
message: `Failed to pull local image ${image}: ${error.message || ''}`,
|
|
799
|
-
timestamp: new Date(),
|
|
800
|
-
errorObject: error,
|
|
801
|
-
},
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
return this.operationLock.runExclusive(() => this._startEdgeApp(twin));
|
|
806
|
-
}
|
|
807
|
-
async _startEdgeApp(twin) {
|
|
808
|
-
const { image } = twin.properties.desired;
|
|
809
|
-
try {
|
|
810
|
-
const existingTwin = this.instances.get(twin.id);
|
|
811
|
-
const hasImageChanged = existingTwin && existingTwin.twin.properties.desired.image !== image;
|
|
812
|
-
if (existingTwin && hasImageChanged) {
|
|
813
|
-
const oldImage = existingTwin.twin.properties.desired.image;
|
|
814
|
-
this.logger.info(`startEdgeApp(): Twin ${twin.id} image has changed from ${oldImage} to ${image}, aborting old pull if in progress`);
|
|
815
|
-
this.abortImagePull(oldImage);
|
|
816
|
-
}
|
|
817
|
-
const hasConfigChanged = existingTwin &&
|
|
818
|
-
!(0, lodash_1.isEqual)(existingTwin.twin.properties.desired.containerCreateOptions, twin.properties.desired.containerCreateOptions);
|
|
819
|
-
if (existingTwin && (hasConfigChanged || hasImageChanged)) {
|
|
820
|
-
this.logger.info(`startEdgeApp(): Twin ${twin.id} configuration or image has changed, recreating container...`);
|
|
821
|
-
await this._stopAndRemoveContainer(twin.id);
|
|
822
|
-
}
|
|
823
|
-
const finalImageExists = await this.imageExists(image);
|
|
824
|
-
if (!finalImageExists) {
|
|
825
|
-
this.logger.error(`startEdgeApp(): Image ${image} not available locally`);
|
|
826
|
-
return {
|
|
827
|
-
status: twin_types_1.TwinStatusEnum.ImageNotFound,
|
|
828
|
-
error: {
|
|
829
|
-
message: `Image ${image} not available locally`,
|
|
830
|
-
timestamp: new Date(),
|
|
831
|
-
},
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
if (!this.instances.get(twin.id)) {
|
|
835
|
-
await this._startContainer(twin);
|
|
836
|
-
}
|
|
837
|
-
return {
|
|
838
|
-
status: twin_types_1.TwinStatusEnum.Online,
|
|
839
|
-
error: undefined,
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
catch (error) {
|
|
843
|
-
this.logger.error(`startEdgeApp(): Failed to start edge app for twin ${twin.id}`, error);
|
|
844
|
-
return {
|
|
845
|
-
status: twin_types_1.TwinStatusEnum.Exited,
|
|
846
|
-
error: {
|
|
847
|
-
message: `Failed to start edge app`,
|
|
848
|
-
timestamp: new Date(),
|
|
849
|
-
errorObject: error,
|
|
850
|
-
},
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
async _stopAndRemoveContainer(twinId) {
|
|
855
|
-
const containerInfo = this.instances.get(twinId);
|
|
856
|
-
if (!containerInfo) {
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
try {
|
|
860
|
-
const containerDetails = await containerInfo.container.inspect();
|
|
861
|
-
if (containerDetails.State.Running) {
|
|
862
|
-
await containerInfo.container.stop();
|
|
863
|
-
this.logger.info(`stopAndRemoveContainer(): Stopped container ${containerDetails.Id}`);
|
|
864
|
-
}
|
|
865
|
-
await containerInfo.container.remove({ force: true });
|
|
866
|
-
this.logger.info(`stopAndRemoveContainer(): Removed container ${containerDetails.Id} for twin ${twinId}`);
|
|
867
|
-
this.instances.delete(twinId);
|
|
868
|
-
}
|
|
869
|
-
catch (error) {
|
|
870
|
-
const isNoSuchContainerError = error.statusCode === 404 &&
|
|
871
|
-
(error.reason === 'no such container' ||
|
|
872
|
-
(error.json && error.json.message && error.json.message.includes('No such container')));
|
|
873
|
-
if (isNoSuchContainerError) {
|
|
874
|
-
this.logger.warn(`stopAndRemoveContainer(): Container for twin ${twinId} no longer exists, cleaning up references`, { error: error.message });
|
|
875
|
-
this.instances.delete(twinId);
|
|
876
|
-
}
|
|
877
|
-
else {
|
|
878
|
-
this.logger.error(`stopAndRemoveContainer(): Failed to stop/remove container for twin ${twinId}`, error);
|
|
879
|
-
throw error;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
finally {
|
|
883
|
-
this.twinInstancesRunning.set(twinId, false);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
async _stopAndRemoveAllContainers() {
|
|
887
|
-
if (this.firstRunCompleted) {
|
|
888
|
-
this.logger.info('stopAndRemoveAllContainers(): First run completed, skipping');
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
this.firstRunCompleted = true;
|
|
892
|
-
this.logger.info('stopAndRemoveAllContainers(): Stopping and removing all running containers');
|
|
893
|
-
try {
|
|
894
|
-
const containers = await this.docker.listContainers({ all: true });
|
|
895
|
-
await Promise.allSettled(containers.map(async (containerInfo) => {
|
|
896
|
-
const container = this.docker.getContainer(containerInfo.Id);
|
|
897
|
-
try {
|
|
898
|
-
const containerDetails = await container.inspect();
|
|
899
|
-
if (containerDetails.State.Running) {
|
|
900
|
-
await container.stop();
|
|
901
|
-
this.logger.info(`stopAndRemoveAllContainers(): Stopped container ${containerInfo.Id}`);
|
|
902
|
-
}
|
|
903
|
-
await container.remove();
|
|
904
|
-
this.logger.info(`stopAndRemoveAllContainers(): Removed container ${containerInfo.Id}`);
|
|
905
|
-
}
|
|
906
|
-
catch (error) {
|
|
907
|
-
this.logger.error(`stopAndRemoveAllContainers(): Failed to stop/remove container ${containerInfo.Id}`, error);
|
|
908
|
-
}
|
|
909
|
-
}));
|
|
910
|
-
}
|
|
911
|
-
catch (error) {
|
|
912
|
-
this.logger.error('stopAndRemoveAllContainers(): Failed to list running containers', error);
|
|
913
|
-
throw error;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
async stopAndRemoveContainer(twinId) {
|
|
917
|
-
return this.operationLock.runExclusive(() => this._stopAndRemoveContainer(twinId));
|
|
918
|
-
}
|
|
919
|
-
async stopAndRemoveAllContainers() {
|
|
920
|
-
return this.operationLock.runExclusive(() => this._stopAndRemoveAllContainers());
|
|
921
|
-
}
|
|
922
|
-
getInstances() {
|
|
923
|
-
return this.instances;
|
|
924
|
-
}
|
|
925
|
-
extractRepositoryName(image) {
|
|
926
|
-
this.logger.info(`extractRepositoryName(): Processing image: ${image}`);
|
|
927
|
-
let normalizedImage = image;
|
|
928
|
-
if (image.startsWith('docker.io/')) {
|
|
929
|
-
normalizedImage = image.substring('docker.io/'.length);
|
|
930
|
-
this.logger.info(`extractRepositoryName(): Removed docker.io/ prefix: ${normalizedImage}`);
|
|
931
|
-
}
|
|
932
|
-
const tagSeparatorIndex = normalizedImage.lastIndexOf(':');
|
|
933
|
-
const pathSeparatorIndex = normalizedImage.lastIndexOf('/');
|
|
934
|
-
if (tagSeparatorIndex === -1) {
|
|
935
|
-
this.logger.info(`extractRepositoryName(): No tag found, repository: ${normalizedImage}`);
|
|
936
|
-
return normalizedImage;
|
|
937
|
-
}
|
|
938
|
-
if (pathSeparatorIndex !== -1 && tagSeparatorIndex < pathSeparatorIndex) {
|
|
939
|
-
this.logger.info(`extractRepositoryName(): Colon is port separator, repository: ${normalizedImage}`);
|
|
940
|
-
return normalizedImage;
|
|
941
|
-
}
|
|
942
|
-
const repositoryName = normalizedImage.substring(0, tagSeparatorIndex);
|
|
943
|
-
this.logger.info(`extractRepositoryName(): Extracted repository: ${repositoryName}`);
|
|
944
|
-
return repositoryName;
|
|
945
|
-
}
|
|
946
|
-
async manageDockerImages(image) {
|
|
947
|
-
try {
|
|
948
|
-
this.logger.info(`manageDockerImages(): Starting management for ${image}`);
|
|
949
|
-
const repositoryName = this.extractRepositoryName(image);
|
|
950
|
-
const referenceFilters = [repositoryName];
|
|
951
|
-
if (!repositoryName.startsWith('docker.io/')) {
|
|
952
|
-
referenceFilters.push(`docker.io/${repositoryName}`);
|
|
953
|
-
}
|
|
954
|
-
this.logger.info(`manageDockerImages(): Querying images for repository: ${repositoryName}`);
|
|
955
|
-
let repositoryImages = await this.docker.listImages({
|
|
956
|
-
filters: { reference: referenceFilters },
|
|
957
|
-
});
|
|
958
|
-
if (repositoryImages.length === 0) {
|
|
959
|
-
this.logger.info(`manageDockerImages(): Reference filter returned no results, trying manual filter`);
|
|
960
|
-
const allImages = await this.docker.listImages();
|
|
961
|
-
repositoryImages = allImages.filter(imageInfo => {
|
|
962
|
-
const imageTags = imageInfo.RepoTags || [];
|
|
963
|
-
return imageTags.some((imageTag) => {
|
|
964
|
-
let normalizedTag = imageTag;
|
|
965
|
-
if (imageTag.startsWith('docker.io/')) {
|
|
966
|
-
normalizedTag = imageTag.substring('docker.io/'.length);
|
|
967
|
-
}
|
|
968
|
-
const tagRepositoryName = this.extractRepositoryName(normalizedTag);
|
|
969
|
-
return (tagRepositoryName === repositoryName || normalizedTag.startsWith(`${repositoryName}:`));
|
|
970
|
-
});
|
|
971
|
-
});
|
|
972
|
-
this.logger.info(`manageDockerImages(): Manual filter found ${repositoryImages.length} image(s)`);
|
|
973
|
-
}
|
|
974
|
-
if (repositoryImages.length === 0) {
|
|
975
|
-
this.logger.warn(`manageDockerImages(): No images found for repository ${repositoryName}`);
|
|
976
|
-
return;
|
|
977
|
-
}
|
|
978
|
-
if (repositoryImages.length <= 2) {
|
|
979
|
-
this.logger.info(`manageDockerImages(): Repository has ${repositoryImages.length} image(s), no cleanup needed`);
|
|
980
|
-
return;
|
|
981
|
-
}
|
|
982
|
-
this.logger.info(`manageDockerImages(): Determining pull times for ${repositoryImages.length} images`);
|
|
983
|
-
const imagesWithPullTimes = repositoryImages.map(imageInfo => {
|
|
984
|
-
const imageTags = imageInfo.RepoTags || imageInfo.RepoDigests || ['<untagged>'];
|
|
985
|
-
const trackedPullTime = this.imagePullTimes.get(imageInfo.Id);
|
|
986
|
-
const pullTime = trackedPullTime || imageInfo.Created;
|
|
987
|
-
const pullTimeFormatted = new Date(pullTime * 1000).toISOString();
|
|
988
|
-
const imageTagsString = imageTags.join(', ');
|
|
989
|
-
if (trackedPullTime) {
|
|
990
|
-
this.logger.info(`manageDockerImages(): Using tracked pull time for ${imageTagsString}: ` +
|
|
991
|
-
`${pullTimeFormatted}`);
|
|
992
|
-
}
|
|
993
|
-
else {
|
|
994
|
-
this.logger.info(`manageDockerImages(): Using Created time for ${imageTagsString}: ` +
|
|
995
|
-
`${pullTimeFormatted}`);
|
|
996
|
-
}
|
|
997
|
-
return {
|
|
998
|
-
imageInfo,
|
|
999
|
-
pullTime,
|
|
1000
|
-
imageTags,
|
|
1001
|
-
};
|
|
1002
|
-
});
|
|
1003
|
-
imagesWithPullTimes.sort((firstImage, secondImage) => secondImage.pullTime - firstImage.pullTime);
|
|
1004
|
-
this.logger.info(`manageDockerImages(): Sorted ${imagesWithPullTimes.length} images by pull time`);
|
|
1005
|
-
const imagesToRemove = imagesWithPullTimes.slice(2);
|
|
1006
|
-
this.logger.info(`manageDockerImages(): Removing ${imagesToRemove.length} oldest image(s), keeping newest 2`);
|
|
1007
|
-
for (const imageToRemove of imagesToRemove) {
|
|
1008
|
-
try {
|
|
1009
|
-
const imageTagsString = imageToRemove.imageTags.join(', ');
|
|
1010
|
-
const pullTimeFormatted = new Date(imageToRemove.pullTime * 1000).toISOString();
|
|
1011
|
-
this.logger.info(`manageDockerImages(): Removing image: ${imageTagsString} ` +
|
|
1012
|
-
`(Pulled: ${pullTimeFormatted})`);
|
|
1013
|
-
await this.docker.getImage(imageToRemove.imageInfo.Id).remove({ force: true });
|
|
1014
|
-
this.imagePullTimes.delete(imageToRemove.imageInfo.Id);
|
|
1015
|
-
this.persistImagePullTimes();
|
|
1016
|
-
this.logger.info(`manageDockerImages(): Successfully removed: ${imageToRemove.imageTags.join(', ')}`);
|
|
1017
|
-
}
|
|
1018
|
-
catch (error) {
|
|
1019
|
-
this.logger.error(`manageDockerImages(): Failed to remove image ${imageToRemove.imageInfo.Id}`, error);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
this.logger.info(`manageDockerImages(): Completed management for ${image}`);
|
|
1023
|
-
}
|
|
1024
|
-
catch (error) {
|
|
1025
|
-
this.logger.error(`manageDockerImages(): Error managing images for ${image}`, error);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
loadImagePullTimes() {
|
|
1029
|
-
try {
|
|
1030
|
-
if (fs_1.default.existsSync(IMAGE_PULL_TIMES_FILE)) {
|
|
1031
|
-
const data = JSON.parse(fs_1.default.readFileSync(IMAGE_PULL_TIMES_FILE, 'utf-8'));
|
|
1032
|
-
const validEntries = Object.entries(data)
|
|
1033
|
-
.filter(([_, value]) => typeof value === 'number' && value > 0)
|
|
1034
|
-
.map(([key, value]) => [key, value]);
|
|
1035
|
-
this.imagePullTimes = new Map(validEntries);
|
|
1036
|
-
this.logger.info(`loadImagePullTimes(): Loaded ${this.imagePullTimes.size} image pull times from disk`);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
catch (error) {
|
|
1040
|
-
this.logger.warn('loadImagePullTimes(): Failed to load image pull times, starting fresh', error);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
async cleanupStaleImagePullTimes() {
|
|
1044
|
-
try {
|
|
1045
|
-
const allImages = await this.docker.listImages();
|
|
1046
|
-
const existingImageIds = new Set(allImages.map(img => img.Id));
|
|
1047
|
-
let cleanedCount = 0;
|
|
1048
|
-
for (const [imageId] of this.imagePullTimes.entries()) {
|
|
1049
|
-
if (!existingImageIds.has(imageId)) {
|
|
1050
|
-
this.imagePullTimes.delete(imageId);
|
|
1051
|
-
cleanedCount++;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
if (cleanedCount > 0) {
|
|
1055
|
-
this.logger.info(`cleanupStaleImagePullTimes(): Removed ${cleanedCount} stale image pull time entries`);
|
|
1056
|
-
this.persistImagePullTimes();
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
catch (error) {
|
|
1060
|
-
this.logger.warn('cleanupStaleImagePullTimes(): Error during cleanup', error);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
persistImagePullTimes() {
|
|
1064
|
-
try {
|
|
1065
|
-
const dir = path_1.default.dirname(IMAGE_PULL_TIMES_FILE);
|
|
1066
|
-
if (!fs_1.default.existsSync(dir)) {
|
|
1067
|
-
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
1068
|
-
}
|
|
1069
|
-
const data = Object.fromEntries(this.imagePullTimes);
|
|
1070
|
-
const tempFile = `${IMAGE_PULL_TIMES_FILE}.tmp`;
|
|
1071
|
-
fs_1.default.writeFileSync(tempFile, JSON.stringify(data, null, 2));
|
|
1072
|
-
fs_1.default.renameSync(tempFile, IMAGE_PULL_TIMES_FILE);
|
|
1073
|
-
}
|
|
1074
|
-
catch (error) {
|
|
1075
|
-
this.logger.warn('persistImagePullTimes(): Failed to persist image pull times', error);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
async isContainerRunning(twinId) {
|
|
1079
|
-
try {
|
|
1080
|
-
const containers = await this.docker.listContainers();
|
|
1081
|
-
return containers.some(container => container.Names.some(name => name.includes(twinId)) && container.State === 'running');
|
|
1082
|
-
}
|
|
1083
|
-
catch (error) {
|
|
1084
|
-
this.logger.error(`isContainerRunning(): Error checking container status for ${twinId}:`, error);
|
|
1085
|
-
return false;
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
exports.default = DockerManager;
|
|
1090
|
-
//# sourceMappingURL=docker.js.map
|