@alwaysai/device-agent 1.3.0 → 1.3.1-2
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/lib/application-control/environment-variables.d.ts +1 -0
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/application-control/environment-variables.js +22 -20
- package/lib/application-control/environment-variables.js.map +1 -1
- package/lib/application-control/environment-variables.test.js +37 -2
- package/lib/application-control/environment-variables.test.js.map +1 -1
- package/lib/application-control/install.js +1 -1
- package/lib/application-control/install.js.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +5 -5
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +203 -178
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +30 -25
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.test.js +15 -0
- package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
- package/lib/cloud-connection/messages.d.ts +1 -3
- package/lib/cloud-connection/messages.d.ts.map +1 -1
- package/lib/cloud-connection/messages.js +1 -9
- package/lib/cloud-connection/messages.js.map +1 -1
- package/lib/cloud-connection/publisher.d.ts +1 -0
- package/lib/cloud-connection/publisher.d.ts.map +1 -1
- package/lib/cloud-connection/publisher.js +3 -0
- package/lib/cloud-connection/publisher.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts +10 -21
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +154 -100
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.test.js +140 -72
- package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +26 -6
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +103 -22
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +179 -13
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/device-control/device-control.d.ts +2 -2
- package/lib/device-control/device-control.d.ts.map +1 -1
- package/lib/device-control/device-control.js.map +1 -1
- package/lib/secure-tunneling/secure-tunneling.d.ts +105 -0
- package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
- package/lib/secure-tunneling/secure-tunneling.js +435 -0
- package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
- package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
- package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
- package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
- package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
- package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
- package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
- package/lib/secure-tunneling/spawner-detached.js +107 -0
- package/lib/secure-tunneling/spawner-detached.js.map +1 -0
- package/lib/subcommands/app/analytics.d.ts +10 -0
- package/lib/subcommands/app/analytics.d.ts.map +1 -0
- package/lib/subcommands/app/analytics.js +79 -0
- package/lib/subcommands/app/analytics.js.map +1 -0
- package/lib/subcommands/app/env-vars.d.ts.map +1 -1
- package/lib/subcommands/app/env-vars.js +11 -16
- package/lib/subcommands/app/env-vars.js.map +1 -1
- package/lib/subcommands/app/index.d.ts.map +1 -1
- package/lib/subcommands/app/index.js +3 -1
- package/lib/subcommands/app/index.js.map +1 -1
- package/lib/subcommands/app/models.d.ts +0 -5
- package/lib/subcommands/app/models.d.ts.map +1 -1
- package/lib/subcommands/app/models.js +16 -56
- package/lib/subcommands/app/models.js.map +1 -1
- package/lib/subcommands/app/status.d.ts +1 -0
- package/lib/subcommands/app/status.d.ts.map +1 -1
- package/lib/subcommands/app/status.js +14 -3
- package/lib/subcommands/app/status.js.map +1 -1
- package/lib/subcommands/app/version.d.ts +2 -1
- package/lib/subcommands/app/version.d.ts.map +1 -1
- package/lib/subcommands/app/version.js +16 -3
- package/lib/subcommands/app/version.js.map +1 -1
- package/lib/util/cloud-mode-ready.d.ts +1 -0
- package/lib/util/cloud-mode-ready.d.ts.map +1 -1
- package/lib/util/cloud-mode-ready.js +36 -1
- package/lib/util/cloud-mode-ready.js.map +1 -1
- package/lib/util/parsing.d.ts +2 -0
- package/lib/util/parsing.d.ts.map +1 -0
- package/lib/util/parsing.js +17 -0
- package/lib/util/parsing.js.map +1 -0
- package/package.json +4 -6
- package/readme.md +146 -92
- package/src/application-control/environment-variables.test.ts +43 -3
- package/src/application-control/environment-variables.ts +29 -19
- package/src/application-control/install.ts +1 -1
- package/src/cloud-connection/device-agent-cloud-connection.ts +272 -247
- package/src/cloud-connection/live-updates-handler.test.ts +20 -0
- package/src/cloud-connection/live-updates-handler.ts +45 -52
- package/src/cloud-connection/messages.ts +1 -14
- package/src/cloud-connection/publisher.ts +4 -0
- package/src/cloud-connection/shadow-handler.test.ts +150 -73
- package/src/cloud-connection/shadow-handler.ts +247 -126
- package/src/cloud-connection/transaction-manager.test.ts +193 -18
- package/src/cloud-connection/transaction-manager.ts +174 -26
- package/src/device-control/device-control.ts +3 -3
- package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
- package/src/secure-tunneling/secure-tunneling.ts +606 -0
- package/src/secure-tunneling/spawner-detached.ts +123 -0
- package/src/subcommands/app/analytics.ts +102 -0
- package/src/subcommands/app/env-vars.ts +18 -16
- package/src/subcommands/app/index.ts +4 -3
- package/src/subcommands/app/models.ts +25 -57
- package/src/subcommands/app/status.ts +20 -3
- package/src/subcommands/app/version.ts +19 -4
- package/src/util/cloud-mode-ready.ts +36 -0
- package/src/util/parsing.ts +11 -0
- package/lib/cloud-connection/cmd-status.d.ts +0 -8
- package/lib/cloud-connection/cmd-status.d.ts.map +0 -1
- package/lib/cloud-connection/cmd-status.js +0 -62
- package/lib/cloud-connection/cmd-status.js.map +0 -1
- package/lib/cloud-connection/message-builder.d.ts +0 -7
- package/lib/cloud-connection/message-builder.d.ts.map +0 -1
- package/lib/cloud-connection/message-builder.js +0 -63
- package/lib/cloud-connection/message-builder.js.map +0 -1
- package/lib/secure-tunneling/index.d.ts +0 -5
- package/lib/secure-tunneling/index.d.ts.map +0 -1
- package/lib/secure-tunneling/index.js +0 -64
- package/lib/secure-tunneling/index.js.map +0 -1
- package/src/cloud-connection/cmd-status.ts +0 -71
- package/src/cloud-connection/message-builder.ts +0 -117
- package/src/secure-tunneling/index.ts +0 -74
|
@@ -1,74 +1,75 @@
|
|
|
1
1
|
// eslint-disable-next-line
|
|
2
2
|
const awsIot = require('aws-iot-device-sdk');
|
|
3
|
-
import { getIoTCoreEndpointUrl } from '../infrastructure/urls';
|
|
4
|
-
import { existsSync } from 'fs';
|
|
5
|
-
import {
|
|
6
|
-
BOOTSTRAP_PRIVATE_KEY_FILE_PATH,
|
|
7
|
-
AWS_ROOT_CERTIFICATE_FILE_PATH,
|
|
8
|
-
BOOTSTRAP_CERTIFICATES_DIR_PATH,
|
|
9
|
-
DEVICE_PRIVATE_KEY_FILE_PATH,
|
|
10
|
-
DEVICE_CERTIFICATE_FILE_PATH
|
|
11
|
-
} from '../util/directories';
|
|
12
3
|
import {
|
|
13
|
-
keyMirrors,
|
|
14
|
-
validateToClientMessage,
|
|
15
|
-
SignedUrlsRequestPayload,
|
|
16
|
-
getToDeviceTopic,
|
|
17
4
|
AppInstallResponsePayload,
|
|
18
|
-
validateToDeviceAgentMessage,
|
|
19
|
-
ToDeviceAgentMessage,
|
|
20
|
-
ToCloudMessage,
|
|
21
5
|
AppStateControlPayload,
|
|
22
6
|
AppVersionControlInstallPayload,
|
|
23
7
|
AppVersionControlUninstallPayload,
|
|
24
|
-
|
|
25
|
-
|
|
8
|
+
DeviceActionPayload,
|
|
9
|
+
ModelsInstallResponsePayload,
|
|
10
|
+
SignedUrlsRequestPayload,
|
|
11
|
+
ToCloudMessage,
|
|
12
|
+
ToDeviceAgentMessage,
|
|
13
|
+
getToDeviceTopic,
|
|
14
|
+
buildSignedUrlsRequestMessage,
|
|
15
|
+
buildToClientStatusResponseMessage,
|
|
16
|
+
StatusResponsePayload,
|
|
17
|
+
keyMirrors,
|
|
18
|
+
validateToDeviceAgentMessage
|
|
26
19
|
} from '@alwaysai/device-agent-schemas';
|
|
27
|
-
import {
|
|
28
|
-
import { logger } from '../util/logger';
|
|
29
|
-
import { cloudModeReady } from '../util/cloud-mode-ready';
|
|
30
|
-
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
20
|
+
import { existsSync } from 'fs';
|
|
31
21
|
import {
|
|
22
|
+
installApp,
|
|
23
|
+
restartApp,
|
|
24
|
+
setEnv,
|
|
32
25
|
startApp,
|
|
33
26
|
stopApp,
|
|
34
|
-
restartApp,
|
|
35
|
-
updateModelsWithPresignedUrls,
|
|
36
|
-
installApp,
|
|
37
27
|
uninstallApp,
|
|
38
28
|
updateAppCfg,
|
|
39
|
-
|
|
29
|
+
updateModelsWithPresignedUrls
|
|
40
30
|
} from '../application-control';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import { Publisher } from './publisher';
|
|
44
|
-
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
45
|
-
import { bootstrapProvision } from './bootstrap-provision';
|
|
46
|
-
import { CmdStatusManager } from './cmd-status';
|
|
47
|
-
import { PassthroughHandler, runChannel } from './passthrough-handler';
|
|
31
|
+
import { createAppBackup, rollbackApp } from '../application-control/backup';
|
|
32
|
+
import { reboot } from '../device-control/device-control';
|
|
48
33
|
import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
34
|
+
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
35
|
+
import { getIoTCoreEndpointUrl } from '../infrastructure/urls';
|
|
36
|
+
import { SecureTunnelHandlerSingleton } from '../secure-tunneling/secure-tunneling';
|
|
37
|
+
import { cloudModeReady } from '../util/cloud-mode-ready';
|
|
38
|
+
import {
|
|
39
|
+
AWS_ROOT_CERTIFICATE_FILE_PATH,
|
|
40
|
+
BOOTSTRAP_CERTIFICATES_DIR_PATH,
|
|
41
|
+
BOOTSTRAP_PRIVATE_KEY_FILE_PATH,
|
|
42
|
+
DEVICE_CERTIFICATE_FILE_PATH,
|
|
43
|
+
DEVICE_PRIVATE_KEY_FILE_PATH
|
|
44
|
+
} from '../util/directories';
|
|
45
|
+
import { getDeviceUuid } from '../util/get-device-id';
|
|
46
|
+
import { logger } from '../util/logger';
|
|
51
47
|
import sleep from '../util/sleep';
|
|
52
|
-
import {
|
|
48
|
+
import { bootstrapProvision } from './bootstrap-provision';
|
|
49
|
+
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
50
|
+
import { PassthroughHandler, runChannel } from './passthrough-handler';
|
|
51
|
+
import { Publisher } from './publisher';
|
|
52
|
+
import { ShadowHandler, ShadowUpdate } from './shadow-handler';
|
|
53
53
|
import { TransactionManager } from './transaction-manager';
|
|
54
|
-
import {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
import { reboot } from '../device-control/device-control';
|
|
54
|
+
import { exec } from 'child_process';
|
|
55
|
+
import { promisify } from 'util';
|
|
56
|
+
|
|
57
|
+
const exec_promise = promisify(exec);
|
|
59
58
|
|
|
60
59
|
export class DeviceAgentCloudConnection {
|
|
61
60
|
private shadowHandler: ShadowHandler;
|
|
62
61
|
public publisher: Publisher;
|
|
63
|
-
private cmdStatusMgr: CmdStatusManager;
|
|
64
62
|
private liveUpdatesHandler: LiveUpdatesHandler;
|
|
65
63
|
private txnMgr: TransactionManager;
|
|
66
64
|
private device = awsIot.device;
|
|
67
65
|
|
|
68
66
|
private clientId = getDeviceUuid();
|
|
69
67
|
private host = getIoTCoreEndpointUrl();
|
|
68
|
+
private port = 8883;
|
|
70
69
|
private readonly toDeviceTopic = getToDeviceTopic(this.clientId);
|
|
71
70
|
private readonly secureTunnelNotifyTopic = `$aws/things/${this.clientId}/tunnels/notify`;
|
|
71
|
+
private readonly secureTunnelHandler =
|
|
72
|
+
SecureTunnelHandlerSingleton.getInstance();
|
|
72
73
|
// FIXME: Add support for multiple simultaneous project updates
|
|
73
74
|
private appCfgUpdateQueue: ShadowUpdate[] = [];
|
|
74
75
|
|
|
@@ -106,7 +107,8 @@ export class DeviceAgentCloudConnection {
|
|
|
106
107
|
appReleaseHash
|
|
107
108
|
}
|
|
108
109
|
};
|
|
109
|
-
const message =
|
|
110
|
+
const message = buildSignedUrlsRequestMessage(
|
|
111
|
+
this.clientId,
|
|
110
112
|
signedUrlsRequestPayload,
|
|
111
113
|
txId
|
|
112
114
|
);
|
|
@@ -143,11 +145,10 @@ export class DeviceAgentCloudConnection {
|
|
|
143
145
|
appInstallPayload,
|
|
144
146
|
modelsInstallPayload
|
|
145
147
|
};
|
|
146
|
-
await this.atomicApplicationUpdate(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
148
|
+
await this.atomicApplicationUpdate(async () => {
|
|
149
|
+
await this.shadowHandler.clearProjectShadow(projectId);
|
|
150
|
+
await installApp({ projectId, appReleaseHash, signedUrlsPayload });
|
|
151
|
+
}, projectId);
|
|
151
152
|
return true;
|
|
152
153
|
};
|
|
153
154
|
|
|
@@ -164,22 +165,19 @@ export class DeviceAgentCloudConnection {
|
|
|
164
165
|
const projectId = payload.modelsInstallResponse.projectId;
|
|
165
166
|
if (appCfgUpdate) {
|
|
166
167
|
await this.atomicApplicationUpdate(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
{
|
|
168
|
+
async () =>
|
|
169
|
+
await updateModelsWithPresignedUrls({
|
|
170
170
|
projectId,
|
|
171
171
|
modelInstallPayloads: payload.modelsInstallResponse.newModels,
|
|
172
172
|
newAppCfg: appCfgUpdate.newAppCfg
|
|
173
|
-
}
|
|
174
|
-
],
|
|
173
|
+
}),
|
|
175
174
|
projectId
|
|
176
175
|
);
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
if (envVarUpdate) {
|
|
180
179
|
await this.atomicApplicationUpdate(
|
|
181
|
-
setEnv,
|
|
182
|
-
[{ projectId, envVars: envVarUpdate.envVars }],
|
|
180
|
+
async () => await setEnv({ projectId, envVars: envVarUpdate.envVars }),
|
|
183
181
|
projectId
|
|
184
182
|
);
|
|
185
183
|
}
|
|
@@ -213,7 +211,7 @@ export class DeviceAgentCloudConnection {
|
|
|
213
211
|
private async atomicApplicationUninstall(projectId: string) {
|
|
214
212
|
try {
|
|
215
213
|
await uninstallApp({ projectId });
|
|
216
|
-
this.shadowHandler.
|
|
214
|
+
await this.shadowHandler.clearProjectShadow(projectId);
|
|
217
215
|
} catch (e) {
|
|
218
216
|
logger.error(`Failed to uninstall ${projectId}: ${e.message}`);
|
|
219
217
|
throw e;
|
|
@@ -221,11 +219,10 @@ export class DeviceAgentCloudConnection {
|
|
|
221
219
|
}
|
|
222
220
|
|
|
223
221
|
// eslint-disable-next-line
|
|
224
|
-
private async atomicApplicationUpdate<
|
|
225
|
-
func:
|
|
226
|
-
args: T,
|
|
222
|
+
private async atomicApplicationUpdate <F extends () => any>(
|
|
223
|
+
func: F,
|
|
227
224
|
projectId: string
|
|
228
|
-
) {
|
|
225
|
+
): Promise<ReturnType<F>> {
|
|
229
226
|
// First try to create a backup, so that there is one available if something goes wrong in the next try:catch.
|
|
230
227
|
if (await AgentConfigFile().isAppPresent({ projectId })) {
|
|
231
228
|
try {
|
|
@@ -238,8 +235,7 @@ export class DeviceAgentCloudConnection {
|
|
|
238
235
|
}
|
|
239
236
|
|
|
240
237
|
try {
|
|
241
|
-
const out:
|
|
242
|
-
this.shadowHandler.clearAppConfig(projectId);
|
|
238
|
+
const out: ReturnType<F> = await func();
|
|
243
239
|
await this.shadowHandler.updateProjectShadow(projectId);
|
|
244
240
|
return out;
|
|
245
241
|
} catch (errorAppUpdate) {
|
|
@@ -255,9 +251,9 @@ export class DeviceAgentCloudConnection {
|
|
|
255
251
|
} catch (errorRollbackApp) {
|
|
256
252
|
// and if that fails, uninstall the app as a last resort.
|
|
257
253
|
try {
|
|
258
|
-
await
|
|
259
|
-
}
|
|
260
|
-
|
|
254
|
+
await this.atomicApplicationUninstall(projectId);
|
|
255
|
+
} catch {
|
|
256
|
+
// atomicApplicationUninstall handles failing, so there's nothing to handle here.
|
|
261
257
|
}
|
|
262
258
|
logger.error(
|
|
263
259
|
`Application update failed, rolled back to previous version: ${errorAppUpdate}`
|
|
@@ -272,63 +268,7 @@ export class DeviceAgentCloudConnection {
|
|
|
272
268
|
}
|
|
273
269
|
}
|
|
274
270
|
|
|
275
|
-
|
|
276
|
-
private async atomicCmd<T extends any[]>(props: {
|
|
277
|
-
func: (...args: T) => Promise<boolean>;
|
|
278
|
-
args: T;
|
|
279
|
-
projectId: string;
|
|
280
|
-
txId: string;
|
|
281
|
-
}): Promise<boolean> {
|
|
282
|
-
const { func, args, projectId, txId } = props;
|
|
283
|
-
try {
|
|
284
|
-
await this.cmdStatusMgr.start(projectId);
|
|
285
|
-
await this.liveUpdatesHandler.enableTransactionStatus({
|
|
286
|
-
txId
|
|
287
|
-
});
|
|
288
|
-
const completed = await func(...args);
|
|
289
|
-
if (completed) {
|
|
290
|
-
await this.cmdStatusMgr.stop(projectId);
|
|
291
|
-
await this.liveUpdatesHandler.disableTransactionStatus({
|
|
292
|
-
txId
|
|
293
|
-
});
|
|
294
|
-
const successStatusResponsePayload = await getStatusResponsePayload(
|
|
295
|
-
keyMirrors.statusResponse.success,
|
|
296
|
-
''
|
|
297
|
-
);
|
|
298
|
-
// Send final status message
|
|
299
|
-
const message = await buildStatusResponseMessage(
|
|
300
|
-
successStatusResponsePayload,
|
|
301
|
-
txId
|
|
302
|
-
);
|
|
303
|
-
this.publisher.publishToClient(message);
|
|
304
|
-
}
|
|
305
|
-
return completed;
|
|
306
|
-
} catch (e) {
|
|
307
|
-
logger.error(
|
|
308
|
-
`Failed to execute cmd for ${projectId}:\n${e.message}\n${e.stack}`
|
|
309
|
-
);
|
|
310
|
-
const message: string = e.message;
|
|
311
|
-
|
|
312
|
-
// uninstall the failed app to put system back in good state
|
|
313
|
-
await this.cmdStatusMgr.stop(projectId);
|
|
314
|
-
await this.liveUpdatesHandler.disableTransactionStatus({
|
|
315
|
-
txId
|
|
316
|
-
});
|
|
317
|
-
const failureStatusResponsePayload = await getStatusResponsePayload(
|
|
318
|
-
keyMirrors.statusResponse.failure,
|
|
319
|
-
message
|
|
320
|
-
);
|
|
321
|
-
// Send final status message
|
|
322
|
-
const failureStatusResponseMessage = await buildStatusResponseMessage(
|
|
323
|
-
failureStatusResponsePayload,
|
|
324
|
-
txId
|
|
325
|
-
);
|
|
326
|
-
this.publisher.publishToClient(failureStatusResponseMessage);
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private handleAppConfigUpdate = async (
|
|
271
|
+
private handleProjectShadowConfigUpdate = async (
|
|
332
272
|
update: ShadowUpdate,
|
|
333
273
|
txId: string
|
|
334
274
|
): Promise<boolean> => {
|
|
@@ -353,7 +293,8 @@ export class DeviceAgentCloudConnection {
|
|
|
353
293
|
models: updatedModels
|
|
354
294
|
}
|
|
355
295
|
};
|
|
356
|
-
const message =
|
|
296
|
+
const message = buildSignedUrlsRequestMessage(
|
|
297
|
+
this.clientId,
|
|
357
298
|
modelsOnlyUrlsRequestPayload,
|
|
358
299
|
txId
|
|
359
300
|
);
|
|
@@ -365,27 +306,50 @@ export class DeviceAgentCloudConnection {
|
|
|
365
306
|
|
|
366
307
|
if (appCfgUpdate) {
|
|
367
308
|
await this.atomicApplicationUpdate(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
{
|
|
309
|
+
async () =>
|
|
310
|
+
await updateAppCfg({
|
|
371
311
|
projectId,
|
|
372
312
|
newAppCfg: appCfgUpdate.newAppCfg
|
|
373
|
-
}
|
|
374
|
-
],
|
|
313
|
+
}),
|
|
375
314
|
projectId
|
|
376
315
|
);
|
|
377
316
|
}
|
|
378
317
|
|
|
379
318
|
if (envVarUpdate) {
|
|
380
319
|
await this.atomicApplicationUpdate(
|
|
381
|
-
setEnv,
|
|
382
|
-
[{ projectId, envVars: envVarUpdate.envVars }],
|
|
320
|
+
async () => await setEnv({ projectId, envVars: envVarUpdate.envVars }),
|
|
383
321
|
projectId
|
|
384
322
|
);
|
|
385
323
|
}
|
|
386
324
|
return true;
|
|
387
325
|
};
|
|
388
326
|
|
|
327
|
+
private async handleProjectShadowMessage(topic: string, message: any) {
|
|
328
|
+
const shadowUpdates = await this.shadowHandler.handleProjectShadow({
|
|
329
|
+
topic,
|
|
330
|
+
payload: message,
|
|
331
|
+
clientToken: message.clientToken
|
|
332
|
+
});
|
|
333
|
+
if (shadowUpdates.length) {
|
|
334
|
+
for (const shadowUpdate of shadowUpdates) {
|
|
335
|
+
const projectId = shadowUpdate.projectId;
|
|
336
|
+
const txId = shadowUpdate.txId;
|
|
337
|
+
try {
|
|
338
|
+
await this.txnMgr.runTransactionStep({
|
|
339
|
+
func: () =>
|
|
340
|
+
this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
|
|
341
|
+
projectId,
|
|
342
|
+
txId,
|
|
343
|
+
start: true,
|
|
344
|
+
stepName: topic
|
|
345
|
+
});
|
|
346
|
+
} catch (e) {
|
|
347
|
+
logger.error(`Error handling shadow message: ${e.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
389
353
|
/*=================================================================
|
|
390
354
|
Public interface
|
|
391
355
|
=================================================================*/
|
|
@@ -397,25 +361,27 @@ export class DeviceAgentCloudConnection {
|
|
|
397
361
|
caPath: AWS_ROOT_CERTIFICATE_FILE_PATH,
|
|
398
362
|
clientId: this.clientId,
|
|
399
363
|
host: this.host,
|
|
400
|
-
port:
|
|
364
|
+
port: this.port,
|
|
401
365
|
keepalive: 1 // time before re-connect attempt on dropped connection, default is 400 seconds
|
|
402
366
|
});
|
|
403
367
|
this.publisher = new Publisher(this.device, this.clientId);
|
|
404
368
|
this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
|
|
405
|
-
this.cmdStatusMgr = new CmdStatusManager();
|
|
406
369
|
this.liveUpdatesHandler = new LiveUpdatesHandler(
|
|
407
370
|
this.publisher,
|
|
408
371
|
this.clientId
|
|
409
372
|
);
|
|
410
|
-
this.txnMgr = new TransactionManager(
|
|
373
|
+
this.txnMgr = new TransactionManager(
|
|
374
|
+
this.publisher,
|
|
375
|
+
this.liveUpdatesHandler
|
|
376
|
+
);
|
|
411
377
|
|
|
412
378
|
this.subscribe(this.toDeviceTopic);
|
|
413
379
|
this.subscribe(this.secureTunnelNotifyTopic);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
this.subscribe(this.shadowHandler.shadowTopics.
|
|
418
|
-
this.subscribe(this.shadowHandler.shadowTopics.
|
|
380
|
+
for (const topic of this.shadowHandler.projectShadowTopics) {
|
|
381
|
+
this.subscribe(topic);
|
|
382
|
+
}
|
|
383
|
+
this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.updateDelta);
|
|
384
|
+
this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted);
|
|
419
385
|
}
|
|
420
386
|
|
|
421
387
|
public getClientId(): string {
|
|
@@ -426,12 +392,8 @@ export class DeviceAgentCloudConnection {
|
|
|
426
392
|
return this.toDeviceTopic;
|
|
427
393
|
}
|
|
428
394
|
|
|
429
|
-
public getShadowTopics(): ShadowTopics {
|
|
430
|
-
return this.shadowHandler.shadowTopics;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
395
|
public isCmdInProgress(projectId: string): boolean {
|
|
434
|
-
return this.
|
|
396
|
+
return this.txnMgr.isOngoingTransactionForProjectID(projectId);
|
|
435
397
|
}
|
|
436
398
|
|
|
437
399
|
public async updateProjectShadow(projectId: string) {
|
|
@@ -458,23 +420,29 @@ export class DeviceAgentCloudConnection {
|
|
|
458
420
|
return;
|
|
459
421
|
}
|
|
460
422
|
const txId = message.txId;
|
|
423
|
+
const {
|
|
424
|
+
app_state_control,
|
|
425
|
+
app_version_control,
|
|
426
|
+
live_state_updates,
|
|
427
|
+
app_install_response,
|
|
428
|
+
models_install_response,
|
|
429
|
+
status_response,
|
|
430
|
+
device_action
|
|
431
|
+
} = keyMirrors.toDeviceAgentMessageType;
|
|
461
432
|
switch (message.messageType) {
|
|
462
|
-
case
|
|
433
|
+
case app_state_control: {
|
|
463
434
|
// txId sent from cloud, just need to continue it
|
|
464
435
|
const payload = message.payload;
|
|
465
436
|
const projectId = payload.projectId;
|
|
466
437
|
|
|
467
438
|
try {
|
|
468
|
-
this.txnMgr.
|
|
469
|
-
|
|
470
|
-
func: this.handleAppStateControl,
|
|
471
|
-
args: [message.payload],
|
|
439
|
+
await this.txnMgr.runTransactionStep({
|
|
440
|
+
func: () => this.handleAppStateControl(message.payload),
|
|
472
441
|
projectId,
|
|
473
|
-
txId
|
|
442
|
+
txId,
|
|
443
|
+
start: true,
|
|
444
|
+
stepName: payload.baseCommand
|
|
474
445
|
});
|
|
475
|
-
if (completed) {
|
|
476
|
-
this.txnMgr.completeTransaction(txId);
|
|
477
|
-
}
|
|
478
446
|
} catch (e) {
|
|
479
447
|
logger.error(
|
|
480
448
|
`Error processing application state control request: ${e}!`
|
|
@@ -483,40 +451,33 @@ export class DeviceAgentCloudConnection {
|
|
|
483
451
|
|
|
484
452
|
break;
|
|
485
453
|
}
|
|
486
|
-
case
|
|
454
|
+
case app_version_control: {
|
|
487
455
|
// txId sent from cloud, just need to continue it
|
|
488
456
|
const payload = message.payload;
|
|
489
457
|
const projectId = payload.projectId;
|
|
490
|
-
const appReleaseHash =
|
|
491
|
-
payload.baseCommand === keyMirrors.appVersionControl.install
|
|
492
|
-
? payload.appReleaseHash
|
|
493
|
-
: undefined;
|
|
494
458
|
try {
|
|
495
|
-
this.txnMgr.
|
|
496
|
-
|
|
497
|
-
func: this.handleAppVersionControl,
|
|
498
|
-
args: [payload, txId],
|
|
459
|
+
await this.txnMgr.runTransactionStep({
|
|
460
|
+
func: () => this.handleAppVersionControl(payload, txId),
|
|
499
461
|
projectId,
|
|
500
|
-
txId
|
|
462
|
+
txId,
|
|
463
|
+
start: true,
|
|
464
|
+
stepName: payload.baseCommand
|
|
501
465
|
});
|
|
502
|
-
if (completed) {
|
|
503
|
-
this.txnMgr.completeTransaction(txId);
|
|
504
|
-
}
|
|
505
466
|
} catch (e) {
|
|
506
467
|
logger.error(`Error processing application install request: ${e}!`);
|
|
507
468
|
}
|
|
508
469
|
|
|
509
470
|
break;
|
|
510
471
|
}
|
|
511
|
-
case
|
|
472
|
+
case live_state_updates: {
|
|
512
473
|
const payload = message.payload;
|
|
513
474
|
// TODO: Send response?
|
|
514
|
-
|
|
475
|
+
void this.liveUpdatesHandler.handleToggles(payload, txId);
|
|
515
476
|
break;
|
|
516
477
|
}
|
|
517
|
-
case
|
|
478
|
+
case app_install_response: {
|
|
518
479
|
const payload = message.payload;
|
|
519
|
-
const { projectId
|
|
480
|
+
const { projectId } = payload.appInstallResponse;
|
|
520
481
|
if (txId !== this.txnMgr.getTransactionFromProject(projectId)) {
|
|
521
482
|
throw new Error(
|
|
522
483
|
`App install response received a message for a transaction ID ${txId} that is not currently underway (${this.txnMgr.getTransactionFromProject(
|
|
@@ -524,19 +485,16 @@ export class DeviceAgentCloudConnection {
|
|
|
524
485
|
)})!`
|
|
525
486
|
);
|
|
526
487
|
}
|
|
527
|
-
|
|
528
|
-
func: this.handleAppInstallCloudResponsePayload,
|
|
529
|
-
args: [payload],
|
|
488
|
+
await this.txnMgr.runTransactionStep({
|
|
489
|
+
func: () => this.handleAppInstallCloudResponsePayload(payload),
|
|
530
490
|
projectId,
|
|
531
|
-
txId
|
|
491
|
+
txId,
|
|
492
|
+
start: false,
|
|
493
|
+
stepName: message.messageType
|
|
532
494
|
});
|
|
533
|
-
if (completed) {
|
|
534
|
-
this.txnMgr.completeTransaction(txId);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
495
|
break;
|
|
538
496
|
}
|
|
539
|
-
case
|
|
497
|
+
case models_install_response: {
|
|
540
498
|
// This message doesn't have appReleaseHash in it's payload, but
|
|
541
499
|
// atomicCmd should be able to read it from the installed app
|
|
542
500
|
const payload = message.payload;
|
|
@@ -548,43 +506,73 @@ export class DeviceAgentCloudConnection {
|
|
|
548
506
|
)})!`
|
|
549
507
|
);
|
|
550
508
|
}
|
|
551
|
-
|
|
552
|
-
func: this.handleModelsInstallCloudResponsePayload,
|
|
553
|
-
args: [payload],
|
|
509
|
+
await this.txnMgr.runTransactionStep({
|
|
510
|
+
func: () => this.handleModelsInstallCloudResponsePayload(payload),
|
|
554
511
|
projectId,
|
|
555
|
-
txId
|
|
512
|
+
txId,
|
|
513
|
+
start: false,
|
|
514
|
+
stepName: message.messageType
|
|
556
515
|
});
|
|
557
|
-
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
case status_response: {
|
|
519
|
+
const { failure } = keyMirrors.statusResponse;
|
|
520
|
+
if (message.payload.status === failure) {
|
|
558
521
|
this.txnMgr.completeTransaction(txId);
|
|
559
|
-
}
|
|
560
522
|
|
|
523
|
+
const failureStatusResponsePayload: StatusResponsePayload = {
|
|
524
|
+
status: keyMirrors.statusResponse.failure,
|
|
525
|
+
message: message.payload.message
|
|
526
|
+
};
|
|
527
|
+
// Send final status message
|
|
528
|
+
const failureStatusResponseMessage =
|
|
529
|
+
buildToClientStatusResponseMessage(
|
|
530
|
+
this.clientId,
|
|
531
|
+
failureStatusResponsePayload,
|
|
532
|
+
txId
|
|
533
|
+
);
|
|
534
|
+
this.publisher.publishToClient(failureStatusResponseMessage);
|
|
535
|
+
}
|
|
561
536
|
break;
|
|
562
537
|
}
|
|
563
|
-
case
|
|
538
|
+
case device_action: {
|
|
564
539
|
try {
|
|
565
|
-
const
|
|
566
|
-
keyMirrors.statusResponse.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
540
|
+
const statusResponsePayload: StatusResponsePayload = {
|
|
541
|
+
status: keyMirrors.statusResponse.in_progress
|
|
542
|
+
};
|
|
543
|
+
const statusResponseMessage = buildToClientStatusResponseMessage(
|
|
544
|
+
this.clientId,
|
|
545
|
+
statusResponsePayload,
|
|
571
546
|
txId
|
|
572
547
|
);
|
|
573
|
-
this.publisher.publishToClient(
|
|
548
|
+
this.publisher.publishToClient(statusResponseMessage);
|
|
574
549
|
|
|
575
550
|
await this.handleDeviceAction(message.payload);
|
|
551
|
+
|
|
552
|
+
const successStatusResponsePayload: StatusResponsePayload = {
|
|
553
|
+
status: keyMirrors.statusResponse.success
|
|
554
|
+
};
|
|
555
|
+
const successStatusResponseMessage =
|
|
556
|
+
buildToClientStatusResponseMessage(
|
|
557
|
+
this.clientId,
|
|
558
|
+
successStatusResponsePayload,
|
|
559
|
+
txId
|
|
560
|
+
);
|
|
561
|
+
this.publisher.publishToClient(successStatusResponseMessage);
|
|
576
562
|
} catch (e) {
|
|
577
563
|
logger.error(
|
|
578
564
|
`There was a problem performing device action '${message.payload.action}': ${e.message}`
|
|
579
565
|
);
|
|
580
|
-
const failureStatusResponsePayload =
|
|
581
|
-
keyMirrors.statusResponse.failure,
|
|
582
|
-
e.message
|
|
583
|
-
|
|
584
|
-
const failureStatusResponseMessage =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
566
|
+
const failureStatusResponsePayload: StatusResponsePayload = {
|
|
567
|
+
status: keyMirrors.statusResponse.failure,
|
|
568
|
+
message: e.message
|
|
569
|
+
};
|
|
570
|
+
const failureStatusResponseMessage =
|
|
571
|
+
buildToClientStatusResponseMessage(
|
|
572
|
+
this.clientId,
|
|
573
|
+
failureStatusResponsePayload,
|
|
574
|
+
txId
|
|
575
|
+
);
|
|
588
576
|
this.publisher.publishToClient(failureStatusResponseMessage);
|
|
589
577
|
}
|
|
590
578
|
break;
|
|
@@ -607,53 +595,36 @@ export class DeviceAgentCloudConnection {
|
|
|
607
595
|
logger.debug(
|
|
608
596
|
`Received message: ${JSON.stringify({ topic, message }, null, 2)}`
|
|
609
597
|
);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
640
|
-
case this.shadowHandler.shadowTopics.projects.getRejected:
|
|
641
|
-
case this.shadowHandler.shadowTopics.projects.updateAccepted:
|
|
642
|
-
case this.shadowHandler.shadowTopics.projects.updateRejected:
|
|
643
|
-
// Not handling these for now
|
|
644
|
-
break;
|
|
645
|
-
case this.toDeviceTopic:
|
|
646
|
-
await this.handleDeviceAgentMessage({
|
|
647
|
-
topic,
|
|
648
|
-
message
|
|
649
|
-
});
|
|
650
|
-
break;
|
|
651
|
-
|
|
652
|
-
case this.secureTunnelNotifyTopic:
|
|
653
|
-
await secureTunnelNotifyHandler(message);
|
|
654
|
-
break;
|
|
655
|
-
default:
|
|
656
|
-
logger.error(`Unexpected topic, ignoring! ${topic}`);
|
|
598
|
+
// ProjectShadow messages
|
|
599
|
+
if (this.shadowHandler.projectShadowTopics.includes(topic)) {
|
|
600
|
+
await this.handleProjectShadowMessage(topic, message);
|
|
601
|
+
} else if (topic === this.toDeviceTopic) {
|
|
602
|
+
await this.handleDeviceAgentMessage({
|
|
603
|
+
topic,
|
|
604
|
+
message
|
|
605
|
+
});
|
|
606
|
+
// SecureTunnelNotify messages
|
|
607
|
+
} else if (topic === this.secureTunnelNotifyTopic) {
|
|
608
|
+
await this.secureTunnelHandler.secureTunnelNotifyHandler(message);
|
|
609
|
+
// SecureTunnel messages
|
|
610
|
+
} else if (
|
|
611
|
+
topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
|
|
612
|
+
) {
|
|
613
|
+
logger.info(`Received secure tunnel update: ${message}`);
|
|
614
|
+
const reported = await this.secureTunnelHandler.syncShadowToDeviceState(
|
|
615
|
+
message
|
|
616
|
+
);
|
|
617
|
+
this.publisher.publish(
|
|
618
|
+
this.shadowHandler.shadowTopics.secureTunnel.update,
|
|
619
|
+
JSON.stringify({ state: { reported } })
|
|
620
|
+
);
|
|
621
|
+
} else if (
|
|
622
|
+
topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
|
|
623
|
+
) {
|
|
624
|
+
logger.info(`Received secure tunnel deleteAccepted: ${message}`);
|
|
625
|
+
await this.secureTunnelHandler.destroy();
|
|
626
|
+
} else {
|
|
627
|
+
logger.error(`Unexpected topic, ignoring! ${topic}`);
|
|
657
628
|
}
|
|
658
629
|
}
|
|
659
630
|
|
|
@@ -692,9 +663,63 @@ export class DeviceAgentCloudConnection {
|
|
|
692
663
|
|
|
693
664
|
this.device.on('offline', () => {
|
|
694
665
|
logger.warn(`Device Agent is offline ${new Date().toLocaleString()}`);
|
|
666
|
+
void this.logConnectionInfo();
|
|
695
667
|
});
|
|
696
668
|
}
|
|
697
669
|
|
|
670
|
+
public async logConnectionInfo() {
|
|
671
|
+
try {
|
|
672
|
+
/**
|
|
673
|
+
* We're using the 'netcat' or 'nc' command to test the connection to the IoT Core endpoint.
|
|
674
|
+
* This command doesn't always exit (see below), so
|
|
675
|
+
* we use timeout to break out of the prompt
|
|
676
|
+
* and catch the resulting error/parse the resulting stderr
|
|
677
|
+
*
|
|
678
|
+
* Sample command for current host and port:
|
|
679
|
+
* nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
|
|
680
|
+
*
|
|
681
|
+
* Sample output when port is not blocked and host is reachable:
|
|
682
|
+
* $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443
|
|
683
|
+
* Connection to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443 port [tcp/https] succeeded!
|
|
684
|
+
*
|
|
685
|
+
*
|
|
686
|
+
* Sample output when port is blocked (will repeatedly try until ctrl-C out):
|
|
687
|
+
* $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
|
|
688
|
+
* nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
|
|
689
|
+
* nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
|
|
690
|
+
* nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
|
|
691
|
+
* ^C
|
|
692
|
+
*
|
|
693
|
+
*
|
|
694
|
+
* Sample command/output when the port isn't enable on that host:
|
|
695
|
+
* $ nc -zv -w 1 localhost 8883
|
|
696
|
+
* nc: connect to localhost port 8883 (tcp) failed: Connection refused
|
|
697
|
+
*/
|
|
698
|
+
await exec_promise(`nc -zv -w 1 ${this.host} ${this.port}`, {
|
|
699
|
+
timeout: 2000
|
|
700
|
+
});
|
|
701
|
+
} catch (err) {
|
|
702
|
+
const output = JSON.stringify(err['stderr']);
|
|
703
|
+
if (output.indexOf('not known') !== -1) {
|
|
704
|
+
logger.warn(
|
|
705
|
+
'Iot Core endpoint appears to be unreachable, internet connection may be unstable or the host may be down.'
|
|
706
|
+
);
|
|
707
|
+
} else if (output.indexOf('timed out') !== -1) {
|
|
708
|
+
logger.warn(
|
|
709
|
+
`Internet connection appears fine, however the endpoint was not reachable on the current connection port: ${this.port}\nPlease check if a firewall is in place.`
|
|
710
|
+
);
|
|
711
|
+
} else if (output.indexOf('refused') !== -1) {
|
|
712
|
+
logger.warn(
|
|
713
|
+
`The connection was refused, likely ${this.host} is not running a service on ${this.port}.`
|
|
714
|
+
);
|
|
715
|
+
} else {
|
|
716
|
+
logger.warn(
|
|
717
|
+
`Output from checking connection to ${this.host} on ${this.port}: ${output}`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
698
723
|
public async stop() {
|
|
699
724
|
// FIXME: This method is currently only used by the CLI, and shadow messages
|
|
700
725
|
// can be lost since we aren't waiting for responses so sleep for a short
|