@alwaysai/device-agent 0.1.0 → 0.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 (100) hide show
  1. package/lib/application-control/config.d.ts +0 -1
  2. package/lib/application-control/config.d.ts.map +1 -1
  3. package/lib/application-control/config.js +15 -29
  4. package/lib/application-control/config.js.map +1 -1
  5. package/lib/application-control/environment-variables.d.ts +7 -3
  6. package/lib/application-control/environment-variables.d.ts.map +1 -1
  7. package/lib/application-control/environment-variables.js +71 -35
  8. package/lib/application-control/environment-variables.js.map +1 -1
  9. package/lib/application-control/environment-variables.test.d.ts +2 -0
  10. package/lib/application-control/environment-variables.test.d.ts.map +1 -0
  11. package/lib/application-control/environment-variables.test.js +163 -0
  12. package/lib/application-control/environment-variables.test.js.map +1 -0
  13. package/lib/application-control/index.d.ts +3 -3
  14. package/lib/application-control/index.d.ts.map +1 -1
  15. package/lib/application-control/index.js +1 -3
  16. package/lib/application-control/index.js.map +1 -1
  17. package/lib/application-control/models.d.ts +0 -1
  18. package/lib/application-control/models.d.ts.map +1 -1
  19. package/lib/application-control/models.js +12 -26
  20. package/lib/application-control/models.js.map +1 -1
  21. package/lib/application-control/status.d.ts +3 -0
  22. package/lib/application-control/status.d.ts.map +1 -1
  23. package/lib/application-control/status.js +19 -1
  24. package/lib/application-control/status.js.map +1 -1
  25. package/lib/application-control/utils.d.ts.map +1 -1
  26. package/lib/application-control/utils.js +2 -2
  27. package/lib/application-control/utils.js.map +1 -1
  28. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +6 -3
  29. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  30. package/lib/cloud-connection/device-agent-cloud-connection.js +205 -151
  31. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  32. package/lib/cloud-connection/live-updates-handler.d.ts +3 -0
  33. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  34. package/lib/cloud-connection/live-updates-handler.js +23 -7
  35. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  36. package/lib/cloud-connection/live-updates-handler.test.d.ts +2 -0
  37. package/lib/cloud-connection/live-updates-handler.test.d.ts.map +1 -0
  38. package/lib/cloud-connection/live-updates-handler.test.js +57 -0
  39. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -0
  40. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  41. package/lib/cloud-connection/passthrough-handler.js +6 -3
  42. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  43. package/lib/cloud-connection/shadow-handler.d.ts +11 -3
  44. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  45. package/lib/cloud-connection/shadow-handler.js +22 -7
  46. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  47. package/lib/cloud-connection/shadow-handler.test.js +313 -228
  48. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  49. package/lib/cloud-connection/shadow.js +1 -1
  50. package/lib/cloud-connection/shadow.js.map +1 -1
  51. package/lib/environment.d.ts +1 -0
  52. package/lib/environment.d.ts.map +1 -1
  53. package/lib/environment.js +2 -1
  54. package/lib/environment.js.map +1 -1
  55. package/lib/infrastructure/agent-config.d.ts +3 -1
  56. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  57. package/lib/subcommands/app/env-vars.d.ts +1 -1
  58. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  59. package/lib/subcommands/app/env-vars.js +32 -5
  60. package/lib/subcommands/app/env-vars.js.map +1 -1
  61. package/lib/subcommands/app/index.d.ts.map +1 -1
  62. package/lib/subcommands/app/index.js +4 -1
  63. package/lib/subcommands/app/index.js.map +1 -1
  64. package/lib/subcommands/app/models.d.ts.map +1 -1
  65. package/lib/subcommands/app/models.js +6 -1
  66. package/lib/subcommands/app/models.js.map +1 -1
  67. package/lib/subcommands/app/shadow.d.ts +7 -0
  68. package/lib/subcommands/app/shadow.d.ts.map +1 -0
  69. package/lib/subcommands/app/shadow.js +48 -0
  70. package/lib/subcommands/app/shadow.js.map +1 -0
  71. package/lib/subcommands/app/version.js +2 -2
  72. package/lib/subcommands/app/version.js.map +1 -1
  73. package/lib/util/cloud-mode-ready.d.ts +2 -0
  74. package/lib/util/cloud-mode-ready.d.ts.map +1 -0
  75. package/lib/util/cloud-mode-ready.js +22 -0
  76. package/lib/util/cloud-mode-ready.js.map +1 -0
  77. package/package.json +1 -1
  78. package/readme.md +2 -2
  79. package/src/application-control/config.ts +30 -31
  80. package/src/application-control/environment-variables.test.ts +171 -0
  81. package/src/application-control/environment-variables.ts +102 -43
  82. package/src/application-control/index.ts +3 -9
  83. package/src/application-control/models.ts +14 -29
  84. package/src/application-control/status.ts +20 -0
  85. package/src/application-control/utils.ts +4 -2
  86. package/src/cloud-connection/device-agent-cloud-connection.ts +222 -153
  87. package/src/cloud-connection/live-updates-handler.test.ts +68 -0
  88. package/src/cloud-connection/live-updates-handler.ts +30 -7
  89. package/src/cloud-connection/passthrough-handler.ts +10 -3
  90. package/src/cloud-connection/shadow-handler.test.ts +329 -239
  91. package/src/cloud-connection/shadow-handler.ts +38 -12
  92. package/src/cloud-connection/shadow.ts +1 -1
  93. package/src/environment.ts +2 -0
  94. package/src/infrastructure/agent-config.ts +1 -1
  95. package/src/subcommands/app/env-vars.ts +38 -8
  96. package/src/subcommands/app/index.ts +4 -1
  97. package/src/subcommands/app/models.ts +10 -1
  98. package/src/subcommands/app/shadow.ts +48 -0
  99. package/src/subcommands/app/version.ts +2 -2
  100. package/src/util/cloud-mode-ready.ts +23 -0
@@ -17,10 +17,12 @@ import {
17
17
  LiveUpdatesToggleMessage,
18
18
  SignedUrlsRequestMessage,
19
19
  ClientMessage,
20
- getDeviceTopic
20
+ getDeviceTopic,
21
+ AppInstallCloudResponseMessage
21
22
  } from '@alwaysai/device-agent-schemas';
22
23
  import { getDeviceUuid } from '../util/get-device-id';
23
24
  import { logger } from '../util/logger';
25
+ import { cloudModeReady } from '../util/cloud-mode-ready';
24
26
  import { AgentConfigFile } from '../infrastructure/agent-config';
25
27
  import {
26
28
  startApp,
@@ -29,17 +31,19 @@ import {
29
31
  updateModelsWithPresignedUrls,
30
32
  installApp,
31
33
  uninstallApp,
32
- updateAppCfg
34
+ updateAppCfg,
35
+ setEnv
33
36
  } from '../application-control';
34
- import { AppConfigUpdate, ShadowHandler, ShadowTopics } from './shadow-handler';
37
+ import { ShadowHandler, ShadowTopics, ShadowUpdate } from './shadow-handler';
35
38
  import { Publisher } from './publisher';
36
39
  import { LiveUpdatesHandler } from './live-updates-handler';
37
40
  import { bootstrapProvision } from './bootstrap-provision';
38
- import { AppConfig } from '@alwaysai/app-configuration-schemas';
39
41
  import { CmdStatusManager } from './cmd-status';
40
42
  import { PassthroughHandler, runChannel } from './passthrough-handler';
41
43
  import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
42
44
  import { getAppInstallStatusMessage } from './messages';
45
+ import { ModelsInstallCloudResponseMessage } from '@alwaysai/device-agent-schemas/lib/schemas/client/application-action-schema';
46
+ import sleep from '../util/sleep';
43
47
 
44
48
  export class DeviceAgentCloudConnection {
45
49
  private shadowHandler: ShadowHandler;
@@ -52,9 +56,11 @@ export class DeviceAgentCloudConnection {
52
56
  private host = getIoTCoreEndpointUrl();
53
57
  private readonly toDeviceTopic = getDeviceTopic(this.clientId);
54
58
  // FIXME: Add support for multiple simultaneous project updates
55
- private newAppCfgQueue: AppConfig[] = [];
59
+ private appCfgUpdateQueue: ShadowUpdate[] = [];
56
60
 
57
- private async handleAppStateControl(payload: AppStateControlPacket) {
61
+ private handleAppStateControl = async (
62
+ payload: AppStateControlPacket
63
+ ): Promise<boolean> => {
58
64
  const { baseCommand, projectId } = payload;
59
65
  switch (baseCommand) {
60
66
  case keyMirrors.appStateControl.start:
@@ -67,29 +73,27 @@ export class DeviceAgentCloudConnection {
67
73
  await restartApp({ projectId });
68
74
  break;
69
75
  }
70
- }
76
+ return true;
77
+ };
71
78
 
72
- private async handleAppVersionControl(payload: AppVersionControlPacket) {
79
+ private handleAppVersionControl = async (
80
+ payload: AppVersionControlPacket
81
+ ): Promise<boolean> => {
73
82
  switch (payload.baseCommand) {
74
83
  case keyMirrors.appVersionControl.install: {
75
84
  const { projectId, appReleaseHash } = payload;
76
- await this.cmdStatusMgr.start(projectId);
77
- await this.liveUpdatesHandler.enableAppInstallStatus({
78
- projectId,
79
- appReleaseHash
80
- });
81
85
 
82
86
  const signedUrlsRequest = { projectId, appReleaseHash };
83
87
  await this.publishCloudRequest({
84
88
  messageType: keyMirrors.agentMessageType.signed_urls_request,
85
89
  signedUrlsRequest
86
90
  });
87
- break;
91
+ return false;
88
92
  }
89
93
  case keyMirrors.appVersionControl.uninstall: {
90
94
  const { projectId } = payload;
91
95
  await this.atomicApplicationUninstall(projectId);
92
- break;
96
+ return true;
93
97
  }
94
98
  default:
95
99
  logger.warn(
@@ -99,14 +103,13 @@ export class DeviceAgentCloudConnection {
99
103
  2
100
104
  )}`
101
105
  );
106
+ return true;
102
107
  }
103
- }
104
-
105
- private handleDeviceCommand = async (packet: any) => {
106
- // TODO
107
108
  };
108
109
 
109
- private async handleAgentCommand(message: LiveUpdatesToggleMessage) {
110
+ private handleAgentCommand = async (
111
+ message: LiveUpdatesToggleMessage
112
+ ): Promise<void> => {
110
113
  switch (message.messageType) {
111
114
  case keyMirrors.clientMessageType.live_state_updates:
112
115
  await this.liveUpdatesHandler.handleToggles(message.liveUpdatesToggles);
@@ -116,7 +119,63 @@ export class DeviceAgentCloudConnection {
116
119
  `Invalid agent action message type from message '${message}'`
117
120
  );
118
121
  }
119
- }
122
+ };
123
+
124
+ private handleAppInstallCloudResponse = async (
125
+ payload: AppInstallCloudResponseMessage
126
+ ): Promise<boolean> => {
127
+ const {
128
+ projectId,
129
+ appReleaseHash,
130
+ appInstallPayload,
131
+ modelsInstallPayload
132
+ } = payload.appInstallCloudResponse;
133
+ const signedUrlsPayload = {
134
+ appInstallPayload,
135
+ modelsInstallPayload
136
+ };
137
+ await this.atomicApplicationUpdate(
138
+ installApp,
139
+ [{ projectId, appReleaseHash, signedUrlsPayload }],
140
+ projectId
141
+ );
142
+ return true;
143
+ };
144
+
145
+ private handleModelsInstallCloudResponse = async (
146
+ payload: ModelsInstallCloudResponseMessage
147
+ ): Promise<boolean> => {
148
+ const update = this.appCfgUpdateQueue.shift();
149
+ if (update === undefined) {
150
+ throw new Error(
151
+ 'Unknown error while updating models via application config! No config present for model update.'
152
+ );
153
+ }
154
+ const { appCfgUpdate, envVarUpdate } = update;
155
+ const projectId = payload.modelsInstallCloudResponse.projectId;
156
+ if (appCfgUpdate) {
157
+ await this.atomicApplicationUpdate(
158
+ updateModelsWithPresignedUrls,
159
+ [
160
+ {
161
+ projectId,
162
+ modelInstallPayloads: payload.modelsInstallCloudResponse.newModels,
163
+ newAppCfg: appCfgUpdate.newAppCfg
164
+ }
165
+ ],
166
+ projectId
167
+ );
168
+ }
169
+
170
+ if (envVarUpdate) {
171
+ await this.atomicApplicationUpdate(
172
+ setEnv,
173
+ [{ projectId, envVars: envVarUpdate.envVars }],
174
+ projectId
175
+ );
176
+ }
177
+ return true;
178
+ };
120
179
 
121
180
  private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
122
181
  this.publisher.publishToCloud(payload);
@@ -128,45 +187,12 @@ export class DeviceAgentCloudConnection {
128
187
  }
129
188
 
130
189
  private async atomicApplicationUninstall(projectId: string) {
131
- const appReleaseHash = await AgentConfigFile().getAppVersion({
132
- projectId
133
- });
134
- await this.cmdStatusMgr.start(projectId);
135
- await this.liveUpdatesHandler.enableAppInstallStatus({
136
- projectId,
137
- appReleaseHash
138
- });
139
190
  try {
140
191
  await uninstallApp({ projectId });
141
192
  this.shadowHandler.clearAppConfig(projectId);
142
-
143
- await this.cmdStatusMgr.stop(projectId);
144
- await this.liveUpdatesHandler.disableAppInstallStatus({
145
- projectId
146
- });
147
- // Send final status message
148
- this.publisher.publishToClient(
149
- await getAppInstallStatusMessage(
150
- keyMirrors.appInstallStatus.success,
151
- '',
152
- appReleaseHash
153
- )
154
- );
155
193
  } catch (e) {
156
194
  logger.error(`Failed to uninstall ${projectId}: ${e.message}`);
157
- const message: string = e.message;
158
- await this.cmdStatusMgr.stop(projectId);
159
- await this.liveUpdatesHandler.disableAppInstallStatus({
160
- projectId
161
- });
162
- // Send final status message
163
- this.publisher.publishToClient(
164
- await getAppInstallStatusMessage(
165
- keyMirrors.appInstallStatus.failure,
166
- message,
167
- appReleaseHash
168
- )
169
- );
195
+ throw e;
170
196
  }
171
197
  }
172
198
 
@@ -174,37 +200,14 @@ export class DeviceAgentCloudConnection {
174
200
  private async atomicApplicationUpdate<T extends any[], R extends any>(
175
201
  func: (...args: T) => R,
176
202
  args: T,
177
- projectId: string,
178
- appReleaseHash: string
203
+ projectId: string
179
204
  ) {
180
- await this.cmdStatusMgr.start(projectId);
181
- await this.liveUpdatesHandler.enableAppInstallStatus({
182
- projectId,
183
- appReleaseHash
184
- });
185
-
186
- // Install the app and models
187
205
  try {
188
206
  const out: R = await func(...args);
189
- await this.cmdStatusMgr.stop(projectId);
190
- await this.liveUpdatesHandler.disableAppInstallStatus({
191
- projectId
192
- });
193
- // Send final status message
194
- this.publisher.publishToClient(
195
- await getAppInstallStatusMessage(
196
- keyMirrors.appInstallStatus.success,
197
- '',
198
- appReleaseHash
199
- )
200
- );
201
-
202
- // update app config shadow for project
203
- await this.shadowHandler.publishAppConfig(projectId);
207
+ await this.shadowHandler.updateProjectShadow(projectId);
204
208
  return out;
205
209
  } catch (e) {
206
- logger.error(`Failed to install ${projectId}: ${e.message}`);
207
- const message: string = e.message;
210
+ logger.error(`Failed to update ${projectId}:\n${e.message}\n${e.stack}`);
208
211
 
209
212
  // uninstall the failed app to put system back in good state
210
213
  // TODO: Replace this with rollback
@@ -212,7 +215,38 @@ export class DeviceAgentCloudConnection {
212
215
  await uninstallApp({ projectId });
213
216
  } finally {
214
217
  this.shadowHandler.clearAppConfig(projectId);
218
+ }
219
+ throw e;
220
+ }
221
+ }
215
222
 
223
+ // eslint-disable-next-line
224
+ private async atomicCmd<T extends any[]>(props: {
225
+ func: (...args: T) => Promise<boolean>;
226
+ args: T;
227
+ projectId: string;
228
+ appReleaseHash?: string;
229
+ }) {
230
+ const { func, args, projectId } = props;
231
+ let appReleaseHash = props.appReleaseHash;
232
+ if (appReleaseHash === undefined) {
233
+ try {
234
+ appReleaseHash = await AgentConfigFile().getAppVersion({
235
+ projectId
236
+ });
237
+ } catch (e) {
238
+ logger.warn(`Unable to get app release hash for ${projectId}!`);
239
+ appReleaseHash = '';
240
+ }
241
+ }
242
+ try {
243
+ await this.cmdStatusMgr.start(projectId);
244
+ await this.liveUpdatesHandler.enableAppInstallStatus({
245
+ projectId,
246
+ appReleaseHash
247
+ });
248
+ const completed = await func(...args);
249
+ if (completed) {
216
250
  await this.cmdStatusMgr.stop(projectId);
217
251
  await this.liveUpdatesHandler.disableAppInstallStatus({
218
252
  projectId
@@ -220,30 +254,48 @@ export class DeviceAgentCloudConnection {
220
254
  // Send final status message
221
255
  this.publisher.publishToClient(
222
256
  await getAppInstallStatusMessage(
223
- keyMirrors.appInstallStatus.failure,
224
- message,
257
+ keyMirrors.appInstallStatus.success,
258
+ '',
225
259
  appReleaseHash
226
260
  )
227
261
  );
228
262
  }
229
- }
230
- }
263
+ return completed;
264
+ } catch (e) {
265
+ logger.error(
266
+ `Failed to execute cmd for ${projectId}:\n${e.message}\n${e.stack}`
267
+ );
268
+ const message: string = e.message;
231
269
 
232
- private async handleAppConfigUpdates(appCfgUpdates: AppConfigUpdate[]) {
233
- for (const appConfigUpdate of appCfgUpdates) {
234
- const { projectId, newAppCfg, updatedModels } = appConfigUpdate;
235
- const appReleaseHash = await AgentConfigFile().getAppVersion({
270
+ // uninstall the failed app to put system back in good state
271
+ await this.cmdStatusMgr.stop(projectId);
272
+ await this.liveUpdatesHandler.disableAppInstallStatus({
236
273
  projectId
237
274
  });
238
- await this.cmdStatusMgr.start(projectId);
239
- await this.liveUpdatesHandler.enableAppInstallStatus({
240
- projectId,
241
- appReleaseHash
242
- });
275
+ // Send final status message
276
+ this.publisher.publishToClient(
277
+ await getAppInstallStatusMessage(
278
+ keyMirrors.appInstallStatus.failure,
279
+ message,
280
+ appReleaseHash
281
+ )
282
+ );
283
+ }
284
+ }
243
285
 
244
- if (updatedModels && Object.keys(updatedModels).length) {
245
- // Publish request for model urls
246
- this.newAppCfgQueue.push(newAppCfg);
286
+ private handleAppConfigUpdates = async (
287
+ updates: ShadowUpdate[]
288
+ ): Promise<boolean> => {
289
+ for (const update of updates) {
290
+ const { projectId, appCfgUpdate, envVarUpdate } = update;
291
+
292
+ if (
293
+ appCfgUpdate &&
294
+ appCfgUpdate.updatedModels &&
295
+ Object.keys(appCfgUpdate.updatedModels).length
296
+ ) {
297
+ // When there are model updates request signed URLs and wait to apply config changes
298
+ const { updatedModels } = appCfgUpdate;
247
299
 
248
300
  logger.debug(
249
301
  `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
@@ -257,28 +309,34 @@ export class DeviceAgentCloudConnection {
257
309
  models: updatedModels
258
310
  }
259
311
  });
260
- } else {
261
- // FIXME: do we need to send this up to the cloud?
262
- // should it be something other than appReleaseHash?
263
- const appReleaseHash = await AgentConfigFile().getAppVersion({
264
- projectId
265
- });
266
312
 
313
+ this.appCfgUpdateQueue.push(update);
314
+ return false;
315
+ }
316
+
317
+ if (appCfgUpdate) {
267
318
  await this.atomicApplicationUpdate(
268
319
  updateAppCfg,
269
320
  [
270
321
  {
271
322
  projectId,
272
- appReleaseHash,
273
- newAppCfg
323
+ newAppCfg: appCfgUpdate.newAppCfg
274
324
  }
275
325
  ],
276
- projectId,
277
- appReleaseHash
326
+ projectId
327
+ );
328
+ }
329
+
330
+ if (envVarUpdate) {
331
+ await this.atomicApplicationUpdate(
332
+ setEnv,
333
+ [{ projectId, envVars: envVarUpdate.envVars }],
334
+ projectId
278
335
  );
279
336
  }
280
337
  }
281
- }
338
+ return true;
339
+ };
282
340
 
283
341
  /*=================================================================
284
342
  Public interface
@@ -323,6 +381,10 @@ export class DeviceAgentCloudConnection {
323
381
  return this.cmdStatusMgr.isCmdInProgress(projectId);
324
382
  }
325
383
 
384
+ public async updateProjectShadow(projectId: string) {
385
+ await this.shadowHandler.updateProjectShadow(projectId);
386
+ }
387
+
326
388
  public async handleClientMessage({
327
389
  topic,
328
390
  message
@@ -339,70 +401,61 @@ export class DeviceAgentCloudConnection {
339
401
  2
340
402
  )}`
341
403
  );
404
+ // TODO: Send generic error response
342
405
  return;
343
406
  }
344
407
  const payload = message.payload;
345
408
  switch (payload.messageType) {
346
409
  case keyMirrors.clientMessageType.app_state_control: {
347
- await this.handleAppStateControl(payload.appStateControl);
410
+ const projectId = payload.appStateControl.projectId;
411
+ await this.atomicCmd({
412
+ func: this.handleAppStateControl,
413
+ args: [payload.appStateControl],
414
+ projectId
415
+ });
348
416
  break;
349
417
  }
350
418
  case keyMirrors.clientMessageType.app_version_control: {
351
- await this.handleAppVersionControl(payload.appVersionControl);
419
+ const projectId = payload.appVersionControl.projectId;
420
+ const appReleaseHash =
421
+ payload.appVersionControl.baseCommand ===
422
+ keyMirrors.appVersionControl.install
423
+ ? payload.appVersionControl.appReleaseHash
424
+ : undefined;
425
+ await this.atomicCmd({
426
+ func: this.handleAppVersionControl,
427
+ args: [payload.appVersionControl],
428
+ projectId,
429
+ appReleaseHash
430
+ });
352
431
  break;
353
432
  }
354
433
  case keyMirrors.clientMessageType.live_state_updates: {
434
+ // TODO: Send response?
355
435
  await this.handleAgentCommand(payload);
356
436
  break;
357
437
  }
358
438
  case keyMirrors.clientMessageType.app_install_cloud_response: {
359
- const {
360
- projectId,
361
- appReleaseHash,
362
- appInstallPayload,
363
- modelsInstallPayload
364
- } = payload.appInstallCloudResponse;
365
- const signedUrlsPayload = {
366
- appInstallPayload,
367
- modelsInstallPayload
368
- };
369
- await this.atomicApplicationUpdate(
370
- installApp,
371
- [{ projectId, appReleaseHash, signedUrlsPayload }],
439
+ const { projectId, appReleaseHash } = payload.appInstallCloudResponse;
440
+ await this.atomicCmd({
441
+ func: this.handleAppInstallCloudResponse,
442
+ args: [payload],
372
443
  projectId,
373
444
  appReleaseHash
374
- );
445
+ });
375
446
 
376
447
  break;
377
448
  }
378
449
  case keyMirrors.clientMessageType.models_install_cloud_response: {
379
- const { projectId, newModels } = payload.modelsInstallCloudResponse;
380
- const appReleaseHash = await AgentConfigFile().getAppVersion({
450
+ // This message doesn't have appReleaseHash in it's payload, but
451
+ // atomicCmd should be able to read it from the installed app
452
+ const { projectId } = payload.modelsInstallCloudResponse;
453
+ await this.atomicCmd({
454
+ func: this.handleModelsInstallCloudResponse,
455
+ args: [payload],
381
456
  projectId
382
457
  });
383
458
 
384
- const newAppCfg = this.newAppCfgQueue.shift();
385
- if (newAppCfg === undefined) {
386
- logger.error(
387
- 'Unknown error while updating models via application config! No config present for model update.'
388
- );
389
- return;
390
- }
391
-
392
- await this.atomicApplicationUpdate(
393
- updateModelsWithPresignedUrls,
394
- [
395
- {
396
- projectId,
397
- modelInstallPayloads: newModels,
398
- newAppCfg,
399
- appReleaseHash
400
- }
401
- ],
402
- projectId,
403
- appReleaseHash
404
- );
405
-
406
459
  break;
407
460
  }
408
461
  default:
@@ -423,12 +476,25 @@ export class DeviceAgentCloudConnection {
423
476
  switch (topic) {
424
477
  case this.shadowHandler.shadowTopics.projects.getAccepted:
425
478
  case this.shadowHandler.shadowTopics.projects.updateDelta: {
426
- const appConfigUpdates = await this.shadowHandler.handleShadowTopic({
479
+ const shadowUpdates = await this.shadowHandler.handleShadowTopic({
427
480
  topic,
428
481
  payload: message.state,
429
482
  clientToken: message.clientToken
430
483
  });
431
- await this.handleAppConfigUpdates(appConfigUpdates);
484
+ if (shadowUpdates.length) {
485
+ // FIXME: Take project ID of first shadow update. Most likely there will only be one update
486
+ // so this should be sufficient for now.
487
+ const projectId = shadowUpdates[0].projectId;
488
+ try {
489
+ await this.atomicCmd({
490
+ func: this.handleAppConfigUpdates,
491
+ args: [shadowUpdates],
492
+ projectId
493
+ });
494
+ } catch (e) {
495
+ logger.error(`Error handling shadow message: ${e.message}`);
496
+ }
497
+ }
432
498
  break;
433
499
  }
434
500
  case this.shadowHandler.shadowTopics.projects.getRejected:
@@ -483,14 +549,17 @@ export class DeviceAgentCloudConnection {
483
549
  });
484
550
  }
485
551
 
486
- public stop() {
552
+ public async stop() {
553
+ // FIXME: This method is currently only used by the CLI, and shadow messages
554
+ // can be lost since we aren't waiting for responses so sleep for a short
555
+ // time to receive them
556
+ await sleep(1000);
487
557
  this.device.end();
488
558
  }
489
559
  }
490
560
 
491
561
  export async function runDeviceAgentCloudInterface() {
492
- // FIXME: Check for KeyPath as well
493
- if (existsSync(DEVICE_CERTIFICATE_FILE_PATH)) {
562
+ if (cloudModeReady()) {
494
563
  const deviceAgent = new DeviceAgentCloudConnection();
495
564
  await deviceAgent.setupHandlers();
496
565
  if (ALWAYSAI_ANALYTICS_PASSTHROUGH === true) {
@@ -0,0 +1,68 @@
1
+ import { LiveUpdatesHandler } from './live-updates-handler';
2
+ import { Publisher } from './publisher';
3
+
4
+ const testTrueToggles = {
5
+ deviceStats: true,
6
+ appState: true
7
+ };
8
+
9
+ const testFalseToggles = {
10
+ deviceStats: false,
11
+ appState: false
12
+ };
13
+
14
+ const mockClient = jest.fn();
15
+ const clientId = 'test-client';
16
+
17
+ jest.spyOn(global, 'setTimeout');
18
+
19
+ // NOTE: this was the way I found to mock private class functions
20
+ const mockStartPublishingLiveUpdates = jest.spyOn(
21
+ LiveUpdatesHandler.prototype as any,
22
+ 'startPublishingLiveUpdates'
23
+ );
24
+ mockStartPublishingLiveUpdates.mockResolvedValue(null);
25
+
26
+ describe('Test Live Updates Handler', () => {
27
+ let liveUpdatesHandler: LiveUpdatesHandler;
28
+ let publisher: Publisher;
29
+
30
+ beforeEach(() => {
31
+ publisher = new Publisher(mockClient, clientId);
32
+ liveUpdatesHandler = new LiveUpdatesHandler(publisher);
33
+ jest.clearAllMocks();
34
+ });
35
+
36
+ test('ignore subsequent enables', async () => {
37
+ void liveUpdatesHandler.handleToggles(testTrueToggles);
38
+ // called twice, once for device stats, once for app state
39
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
40
+ // restartLiveUpdatesTimeout is always called once when handleToggles is called
41
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
42
+
43
+ // Second call -> should not call startPublishingLiveUpdates should not be called
44
+ jest.clearAllMocks();
45
+ void liveUpdatesHandler.handleToggles(testTrueToggles);
46
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
47
+ expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ test('test disable live updates', async () => {
51
+ // Test calling handleToggles one time, enabling it
52
+ void liveUpdatesHandler.handleToggles(testTrueToggles);
53
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
54
+ expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
55
+ expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
56
+ expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(true);
57
+ expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
58
+
59
+ // Check to see that attributes are properly set to false when disabled
60
+ jest.clearAllMocks();
61
+ void liveUpdatesHandler.handleToggles(testFalseToggles);
62
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
63
+ expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
64
+ expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);
65
+ expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(false);
66
+ expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
67
+ });
68
+ });