@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.
Files changed (123) hide show
  1. package/lib/application-control/environment-variables.d.ts +1 -0
  2. package/lib/application-control/environment-variables.d.ts.map +1 -1
  3. package/lib/application-control/environment-variables.js +22 -20
  4. package/lib/application-control/environment-variables.js.map +1 -1
  5. package/lib/application-control/environment-variables.test.js +37 -2
  6. package/lib/application-control/environment-variables.test.js.map +1 -1
  7. package/lib/application-control/install.js +1 -1
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +5 -5
  10. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  11. package/lib/cloud-connection/device-agent-cloud-connection.js +203 -178
  12. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  13. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  14. package/lib/cloud-connection/live-updates-handler.js +30 -25
  15. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  16. package/lib/cloud-connection/live-updates-handler.test.js +15 -0
  17. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  18. package/lib/cloud-connection/messages.d.ts +1 -3
  19. package/lib/cloud-connection/messages.d.ts.map +1 -1
  20. package/lib/cloud-connection/messages.js +1 -9
  21. package/lib/cloud-connection/messages.js.map +1 -1
  22. package/lib/cloud-connection/publisher.d.ts +1 -0
  23. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  24. package/lib/cloud-connection/publisher.js +3 -0
  25. package/lib/cloud-connection/publisher.js.map +1 -1
  26. package/lib/cloud-connection/shadow-handler.d.ts +10 -21
  27. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  28. package/lib/cloud-connection/shadow-handler.js +154 -100
  29. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  30. package/lib/cloud-connection/shadow-handler.test.js +140 -72
  31. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  32. package/lib/cloud-connection/transaction-manager.d.ts +26 -6
  33. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  34. package/lib/cloud-connection/transaction-manager.js +103 -22
  35. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  36. package/lib/cloud-connection/transaction-manager.test.js +179 -13
  37. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  38. package/lib/device-control/device-control.d.ts +2 -2
  39. package/lib/device-control/device-control.d.ts.map +1 -1
  40. package/lib/device-control/device-control.js.map +1 -1
  41. package/lib/secure-tunneling/secure-tunneling.d.ts +105 -0
  42. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
  43. package/lib/secure-tunneling/secure-tunneling.js +435 -0
  44. package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
  45. package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
  46. package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
  47. package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
  48. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
  49. package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
  50. package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
  51. package/lib/secure-tunneling/spawner-detached.js +107 -0
  52. package/lib/secure-tunneling/spawner-detached.js.map +1 -0
  53. package/lib/subcommands/app/analytics.d.ts +10 -0
  54. package/lib/subcommands/app/analytics.d.ts.map +1 -0
  55. package/lib/subcommands/app/analytics.js +79 -0
  56. package/lib/subcommands/app/analytics.js.map +1 -0
  57. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  58. package/lib/subcommands/app/env-vars.js +11 -16
  59. package/lib/subcommands/app/env-vars.js.map +1 -1
  60. package/lib/subcommands/app/index.d.ts.map +1 -1
  61. package/lib/subcommands/app/index.js +3 -1
  62. package/lib/subcommands/app/index.js.map +1 -1
  63. package/lib/subcommands/app/models.d.ts +0 -5
  64. package/lib/subcommands/app/models.d.ts.map +1 -1
  65. package/lib/subcommands/app/models.js +16 -56
  66. package/lib/subcommands/app/models.js.map +1 -1
  67. package/lib/subcommands/app/status.d.ts +1 -0
  68. package/lib/subcommands/app/status.d.ts.map +1 -1
  69. package/lib/subcommands/app/status.js +14 -3
  70. package/lib/subcommands/app/status.js.map +1 -1
  71. package/lib/subcommands/app/version.d.ts +2 -1
  72. package/lib/subcommands/app/version.d.ts.map +1 -1
  73. package/lib/subcommands/app/version.js +16 -3
  74. package/lib/subcommands/app/version.js.map +1 -1
  75. package/lib/util/cloud-mode-ready.d.ts +1 -0
  76. package/lib/util/cloud-mode-ready.d.ts.map +1 -1
  77. package/lib/util/cloud-mode-ready.js +36 -1
  78. package/lib/util/cloud-mode-ready.js.map +1 -1
  79. package/lib/util/parsing.d.ts +2 -0
  80. package/lib/util/parsing.d.ts.map +1 -0
  81. package/lib/util/parsing.js +17 -0
  82. package/lib/util/parsing.js.map +1 -0
  83. package/package.json +4 -6
  84. package/readme.md +146 -92
  85. package/src/application-control/environment-variables.test.ts +43 -3
  86. package/src/application-control/environment-variables.ts +29 -19
  87. package/src/application-control/install.ts +1 -1
  88. package/src/cloud-connection/device-agent-cloud-connection.ts +272 -247
  89. package/src/cloud-connection/live-updates-handler.test.ts +20 -0
  90. package/src/cloud-connection/live-updates-handler.ts +45 -52
  91. package/src/cloud-connection/messages.ts +1 -14
  92. package/src/cloud-connection/publisher.ts +4 -0
  93. package/src/cloud-connection/shadow-handler.test.ts +150 -73
  94. package/src/cloud-connection/shadow-handler.ts +247 -126
  95. package/src/cloud-connection/transaction-manager.test.ts +193 -18
  96. package/src/cloud-connection/transaction-manager.ts +174 -26
  97. package/src/device-control/device-control.ts +3 -3
  98. package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
  99. package/src/secure-tunneling/secure-tunneling.ts +606 -0
  100. package/src/secure-tunneling/spawner-detached.ts +123 -0
  101. package/src/subcommands/app/analytics.ts +102 -0
  102. package/src/subcommands/app/env-vars.ts +18 -16
  103. package/src/subcommands/app/index.ts +4 -3
  104. package/src/subcommands/app/models.ts +25 -57
  105. package/src/subcommands/app/status.ts +20 -3
  106. package/src/subcommands/app/version.ts +19 -4
  107. package/src/util/cloud-mode-ready.ts +36 -0
  108. package/src/util/parsing.ts +11 -0
  109. package/lib/cloud-connection/cmd-status.d.ts +0 -8
  110. package/lib/cloud-connection/cmd-status.d.ts.map +0 -1
  111. package/lib/cloud-connection/cmd-status.js +0 -62
  112. package/lib/cloud-connection/cmd-status.js.map +0 -1
  113. package/lib/cloud-connection/message-builder.d.ts +0 -7
  114. package/lib/cloud-connection/message-builder.d.ts.map +0 -1
  115. package/lib/cloud-connection/message-builder.js +0 -63
  116. package/lib/cloud-connection/message-builder.js.map +0 -1
  117. package/lib/secure-tunneling/index.d.ts +0 -5
  118. package/lib/secure-tunneling/index.d.ts.map +0 -1
  119. package/lib/secure-tunneling/index.js +0 -64
  120. package/lib/secure-tunneling/index.js.map +0 -1
  121. package/src/cloud-connection/cmd-status.ts +0 -71
  122. package/src/cloud-connection/message-builder.ts +0 -117
  123. 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
- ToClientMessage,
25
- DeviceActionPayload
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 { getDeviceUuid } from '../util/get-device-id';
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
- setEnv
29
+ updateModelsWithPresignedUrls
40
30
  } from '../application-control';
41
- import { ShadowHandler, ShadowTopics, ShadowUpdate } from './shadow-handler';
42
- import { secureTunnelNotifyHandler } from '../secure-tunneling/index';
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 { getStatusResponsePayload } from './messages';
50
- import { ModelsInstallResponsePayload } from '@alwaysai/device-agent-schemas';
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 { createAppBackup, rollbackApp } from '../application-control/backup';
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
- buildSignedUrlsRequestMessage,
56
- buildStatusResponseMessage
57
- } from './message-builder';
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 = await buildSignedUrlsRequestMessage(
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
- installApp,
148
- [{ projectId, appReleaseHash, signedUrlsPayload }],
149
- projectId
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
- updateModelsWithPresignedUrls,
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.clearAppConfig(projectId);
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<T extends any[], R extends any>(
225
- func: (...args: T) => R,
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: R = await func(...args);
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 uninstallApp({ projectId });
259
- } finally {
260
- this.shadowHandler.clearAppConfig(projectId);
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
- // eslint-disable-next-line
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 = await buildSignedUrlsRequestMessage(
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
- updateAppCfg,
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: 8883,
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
- this.subscribe(this.shadowHandler.shadowTopics.projects.getAccepted);
415
- this.subscribe(this.shadowHandler.shadowTopics.projects.getRejected);
416
- this.subscribe(this.shadowHandler.shadowTopics.projects.updateDelta);
417
- this.subscribe(this.shadowHandler.shadowTopics.projects.updateAccepted);
418
- this.subscribe(this.shadowHandler.shadowTopics.projects.updateRejected);
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.cmdStatusMgr.isCmdInProgress(projectId);
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 keyMirrors.toDeviceAgentMessageType.app_state_control: {
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.addTransaction(txId, projectId);
469
- const completed = await this.atomicCmd({
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 keyMirrors.toDeviceAgentMessageType.app_version_control: {
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.addTransaction(txId, projectId);
496
- const completed = await this.atomicCmd({
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 keyMirrors.toDeviceAgentMessageType.live_state_updates: {
472
+ case live_state_updates: {
512
473
  const payload = message.payload;
513
474
  // TODO: Send response?
514
- await this.liveUpdatesHandler.handleToggles(payload, txId);
475
+ void this.liveUpdatesHandler.handleToggles(payload, txId);
515
476
  break;
516
477
  }
517
- case keyMirrors.toDeviceAgentMessageType.app_install_response: {
478
+ case app_install_response: {
518
479
  const payload = message.payload;
519
- const { projectId, appReleaseHash } = payload.appInstallResponse;
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
- const completed = await this.atomicCmd({
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 keyMirrors.toDeviceAgentMessageType.models_install_response: {
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
- const completed = await this.atomicCmd({
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
- if (completed) {
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 keyMirrors.toDeviceAgentMessageType.device_action: {
538
+ case device_action: {
564
539
  try {
565
- const successStatusResponsePayload = await getStatusResponsePayload(
566
- keyMirrors.statusResponse.success,
567
- ''
568
- );
569
- const successStatusResponseMessage = await buildStatusResponseMessage(
570
- successStatusResponsePayload,
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(successStatusResponseMessage);
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 = await getStatusResponsePayload(
581
- keyMirrors.statusResponse.failure,
582
- e.message
583
- );
584
- const failureStatusResponseMessage = await buildStatusResponseMessage(
585
- failureStatusResponsePayload,
586
- txId
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
- switch (topic) {
611
- case this.shadowHandler.shadowTopics.projects.getAccepted:
612
- case this.shadowHandler.shadowTopics.projects.updateDelta: {
613
- const shadowUpdates = await this.shadowHandler.handleShadowTopic({
614
- topic,
615
- payload: message.state,
616
- clientToken: message.clientToken
617
- });
618
- if (shadowUpdates.length) {
619
- for (const shadowUpdate of shadowUpdates) {
620
- const projectId = shadowUpdate.projectId;
621
- const txId = shadowUpdate.txId;
622
- try {
623
- this.txnMgr.addTransaction(txId, projectId);
624
- const completed = await this.atomicCmd({
625
- func: this.handleAppConfigUpdate,
626
- args: [shadowUpdate, txId],
627
- projectId,
628
- txId
629
- });
630
- if (completed) {
631
- this.txnMgr.completeTransaction(txId);
632
- }
633
- } catch (e) {
634
- logger.error(`Error handling shadow message: ${e.message}`);
635
- }
636
- }
637
- }
638
- break;
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