@alwaysai/device-agent 1.3.1 → 1.4.0

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 (91) hide show
  1. package/lib/application-control/environment-variables.d.ts.map +1 -1
  2. package/lib/application-control/environment-variables.js +9 -4
  3. package/lib/application-control/environment-variables.js.map +1 -1
  4. package/lib/application-control/environment-variables.test.js +1 -1
  5. package/lib/application-control/environment-variables.test.js.map +1 -1
  6. package/lib/application-control/install.d.ts.map +1 -1
  7. package/lib/application-control/install.js +6 -2
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/application-control/models.d.ts.map +1 -1
  10. package/lib/application-control/models.js +4 -2
  11. package/lib/application-control/models.js.map +1 -1
  12. package/lib/application-control/status.js +4 -5
  13. package/lib/application-control/status.js.map +1 -1
  14. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +3 -3
  15. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  16. package/lib/cloud-connection/device-agent-cloud-connection.js +114 -99
  17. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  18. package/lib/cloud-connection/live-updates-handler.d.ts +1 -0
  19. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  20. package/lib/cloud-connection/live-updates-handler.js +22 -4
  21. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  22. package/lib/cloud-connection/messages.d.ts.map +1 -1
  23. package/lib/cloud-connection/messages.js +3 -4
  24. package/lib/cloud-connection/messages.js.map +1 -1
  25. package/lib/cloud-connection/shadow-handler.d.ts +14 -21
  26. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/shadow-handler.js +162 -108
  28. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.test.js +100 -83
  30. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  31. package/lib/device-control/device-control.d.ts +7 -14
  32. package/lib/device-control/device-control.d.ts.map +1 -1
  33. package/lib/device-control/device-control.js +37 -14
  34. package/lib/device-control/device-control.js.map +1 -1
  35. package/lib/secure-tunneling/secure-tunneling.d.ts +105 -0
  36. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
  37. package/lib/secure-tunneling/secure-tunneling.js +435 -0
  38. package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
  39. package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
  40. package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
  41. package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
  42. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
  43. package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
  44. package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
  45. package/lib/secure-tunneling/spawner-detached.js +107 -0
  46. package/lib/secure-tunneling/spawner-detached.js.map +1 -0
  47. package/lib/subcommands/app/analytics.d.ts.map +1 -1
  48. package/lib/subcommands/app/analytics.js +9 -13
  49. package/lib/subcommands/app/analytics.js.map +1 -1
  50. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  51. package/lib/subcommands/app/env-vars.js +11 -16
  52. package/lib/subcommands/app/env-vars.js.map +1 -1
  53. package/lib/subcommands/app/models.d.ts.map +1 -1
  54. package/lib/subcommands/app/models.js +12 -16
  55. package/lib/subcommands/app/models.js.map +1 -1
  56. package/lib/subcommands/device/clean.d.ts.map +1 -1
  57. package/lib/subcommands/device/clean.js +3 -1
  58. package/lib/subcommands/device/clean.js.map +1 -1
  59. package/lib/subcommands/device/device.d.ts.map +1 -1
  60. package/lib/subcommands/device/device.js +14 -6
  61. package/lib/subcommands/device/device.js.map +1 -1
  62. package/lib/util/cloud-mode-ready.d.ts +1 -0
  63. package/lib/util/cloud-mode-ready.d.ts.map +1 -1
  64. package/lib/util/cloud-mode-ready.js +36 -1
  65. package/lib/util/cloud-mode-ready.js.map +1 -1
  66. package/package.json +2 -2
  67. package/src/application-control/environment-variables.test.ts +1 -1
  68. package/src/application-control/environment-variables.ts +9 -6
  69. package/src/application-control/install.ts +7 -3
  70. package/src/application-control/models.ts +11 -6
  71. package/src/application-control/status.ts +8 -8
  72. package/src/cloud-connection/device-agent-cloud-connection.ts +161 -131
  73. package/src/cloud-connection/live-updates-handler.ts +34 -6
  74. package/src/cloud-connection/messages.ts +3 -4
  75. package/src/cloud-connection/shadow-handler.test.ts +101 -84
  76. package/src/cloud-connection/shadow-handler.ts +275 -133
  77. package/src/device-control/device-control.ts +46 -19
  78. package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
  79. package/src/secure-tunneling/secure-tunneling.ts +606 -0
  80. package/src/secure-tunneling/spawner-detached.ts +123 -0
  81. package/src/subcommands/app/analytics.ts +16 -13
  82. package/src/subcommands/app/env-vars.ts +18 -16
  83. package/src/subcommands/app/models.ts +20 -16
  84. package/src/subcommands/device/clean.ts +4 -1
  85. package/src/subcommands/device/device.ts +26 -10
  86. package/src/util/cloud-mode-ready.ts +36 -0
  87. package/lib/secure-tunneling/index.d.ts +0 -5
  88. package/lib/secure-tunneling/index.d.ts.map +0 -1
  89. package/lib/secure-tunneling/index.js +0 -64
  90. package/lib/secure-tunneling/index.js.map +0 -1
  91. package/src/secure-tunneling/index.ts +0 -74
@@ -41,24 +41,27 @@ export async function setEnv(props: { projectId: string; envVars: EnvVars }) {
41
41
 
42
42
  const service = composeParsed['services'][s];
43
43
  const oldEnv: string[] | undefined = service['environment'];
44
+ const envVarsForService = envVars[s];
44
45
 
45
46
  const newEnvVarsObj = {};
46
47
  oldEnv?.forEach((envVarStr: string) => {
47
48
  const envVarSplit = envVarStr.split('=');
48
49
  const key = envVarSplit[0];
49
50
  const value = envVarSplit[1];
50
- newEnvVarsObj[key] = value;
51
+ if (envVarsForService[key] !== null) {
52
+ newEnvVarsObj[key] = value;
53
+ }
51
54
  });
52
55
 
53
- for (const envVar of Object.keys(envVars[s])) {
54
- newEnvVarsObj[envVar] = envVars[s][envVar];
56
+ for (const envVar of Object.keys(envVarsForService)) {
57
+ if (envVarsForService[envVar] !== null) {
58
+ newEnvVarsObj[envVar] = envVarsForService[envVar];
59
+ }
55
60
  }
56
61
 
57
62
  const envVarList: string[] = [];
58
63
  for (const envVar of Object.keys(newEnvVarsObj)) {
59
- envVarList.push(
60
- `${envVar}=${newEnvVarsObj[envVar] ? newEnvVarsObj[envVar] : ''}`
61
- );
64
+ envVarList.push(`${envVar}=${newEnvVarsObj[envVar]}`);
62
65
  }
63
66
 
64
67
  service['environment'] = envVarList;
@@ -113,12 +113,18 @@ export async function installApp(props: {
113
113
  const DACfg = await createAppCfgDeviceAgentConnection();
114
114
  await updateAppCfgFile({ projectId, newAppCfg: DACfg });
115
115
 
116
- await installAppBuildReqs({ appDir });
116
+ // Clean up original dependency files (models, virtual env)
117
+ const targetJsonFile = TargetJsonFile(appDir);
118
+ const hostSpawner = targetJsonFile.readHostSpawner();
119
+ await appCleanDocker({ targetHostSpawner: hostSpawner });
120
+
121
+ // Download model files
117
122
  await installModelsWithPresignedURLs(
118
123
  signedUrlsPayload.modelsInstallPayload,
119
124
  path.join(appDir, 'models')
120
125
  );
121
126
 
127
+ await installAppBuildReqs({ appDir });
122
128
  await buildApp({ appDir });
123
129
 
124
130
  await AgentConfigFile().setAppInstalled({
@@ -153,8 +159,6 @@ async function installAppBuildReqs(props: { appDir: string }) {
153
159
  async () => {
154
160
  const hostSpawner = targetJsonFile.readHostSpawner();
155
161
 
156
- await appCleanDocker({ targetHostSpawner: hostSpawner });
157
-
158
162
  const dockerImageId = await buildDockerImage({
159
163
  targetHostSpawner: hostSpawner,
160
164
  targetHardware: targetCfg.targetHardware,
@@ -89,14 +89,19 @@ export async function replaceModels(props: {
89
89
  ],
90
90
  appDir
91
91
  );
92
+ const modelsAddPromises: Promise<void>[] = [];
92
93
  for (const modelId of modelIds) {
93
- await appModelsAddComponent({
94
- yes: false,
95
- dir: appDir,
96
- id: modelId,
97
- addToProject: false
98
- });
94
+ modelsAddPromises.push(
95
+ appModelsAddComponent({
96
+ yes: false,
97
+ dir: appDir,
98
+ id: modelId,
99
+ addToProject: false
100
+ })
101
+ );
99
102
  }
103
+ await Promise.all(modelsAddPromises);
104
+
100
105
  await buildApp({ appDir });
101
106
  }
102
107
 
@@ -126,14 +126,14 @@ export async function getAppLogs(props: {
126
126
 
127
127
  const appDir = getAppDir(projectId);
128
128
 
129
- const serviceList = services
130
- ? services
131
- : await (async function () {
132
- const composeServices = await compose.configServices({ cwd: appDir });
133
- return composeServices.data.services;
134
- })();
135
-
136
- const argsList = args ? args : [];
129
+ const serviceList =
130
+ services ||
131
+ (await (async function () {
132
+ const composeServices = await compose.configServices({ cwd: appDir });
133
+ return composeServices.data.services;
134
+ })());
135
+
136
+ const argsList = args || [];
137
137
 
138
138
  // Use direct command with spawner in order to get a readable stream
139
139
  return await JsSpawner().runStreaming({
@@ -1,59 +1,57 @@
1
1
  // eslint-disable-next-line
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';
2
+ const awsIot = require('aws-iot-device-sdk');
12
3
  import {
13
- keyMirrors,
14
- SignedUrlsRequestPayload,
15
- getToDeviceTopic,
16
4
  AppInstallResponsePayload,
17
- validateToDeviceAgentMessage,
18
- ToDeviceAgentMessage,
19
- ToCloudMessage,
20
5
  AppStateControlPayload,
21
6
  AppVersionControlInstallPayload,
22
7
  AppVersionControlUninstallPayload,
23
8
  DeviceActionPayload,
9
+ ModelsInstallResponsePayload,
10
+ SignedUrlsRequestPayload,
11
+ ToCloudMessage,
12
+ ToDeviceAgentMessage,
13
+ getToDeviceTopic,
24
14
  buildSignedUrlsRequestMessage,
25
15
  buildToClientStatusResponseMessage,
26
- StatusResponsePayload
16
+ StatusResponsePayload,
17
+ keyMirrors,
18
+ validateToDeviceAgentMessage
27
19
  } from '@alwaysai/device-agent-schemas';
28
- import { getDeviceUuid } from '../util/get-device-id';
29
- import { logger } from '../util/logger';
30
- import { cloudModeReady } from '../util/cloud-mode-ready';
31
- import { AgentConfigFile } from '../infrastructure/agent-config';
20
+ import { existsSync } from 'fs';
32
21
  import {
22
+ installApp,
23
+ restartApp,
33
24
  startApp,
34
25
  stopApp,
35
- restartApp,
36
- updateModelsWithPresignedUrls,
37
- installApp,
38
26
  uninstallApp,
39
27
  updateAppCfg,
40
- setEnv
28
+ updateModelsWithPresignedUrls
41
29
  } from '../application-control';
42
- import { ShadowHandler, ShadowTopics, ShadowUpdate } from './shadow-handler';
43
- import { secureTunnelNotifyHandler } from '../secure-tunneling/index';
44
- import { Publisher } from './publisher';
45
- import { bootstrapProvision } from './bootstrap-provision';
46
- import { PassthroughHandler, runChannel } from './passthrough-handler';
30
+ import { createAppBackup, rollbackApp } from '../application-control/backup';
31
+ import { reboot } from '../device-control/device-control';
47
32
  import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
48
- import { ModelsInstallResponsePayload } from '@alwaysai/device-agent-schemas';
33
+ import { AgentConfigFile } from '../infrastructure/agent-config';
34
+ import { getIoTCoreEndpointUrl } from '../infrastructure/urls';
35
+ import { SecureTunnelHandlerSingleton } from '../secure-tunneling/secure-tunneling';
36
+ import { cloudModeReady } from '../util/cloud-mode-ready';
37
+ import {
38
+ AWS_ROOT_CERTIFICATE_FILE_PATH,
39
+ BOOTSTRAP_CERTIFICATES_DIR_PATH,
40
+ BOOTSTRAP_PRIVATE_KEY_FILE_PATH,
41
+ DEVICE_CERTIFICATE_FILE_PATH,
42
+ DEVICE_PRIVATE_KEY_FILE_PATH
43
+ } from '../util/directories';
44
+ import { getDeviceUuid } from '../util/get-device-id';
45
+ import { logger } from '../util/logger';
49
46
  import sleep from '../util/sleep';
50
- import { createAppBackup, rollbackApp } from '../application-control/backup';
47
+ import { bootstrapProvision } from './bootstrap-provision';
48
+ import { LiveUpdatesHandler } from './live-updates-handler';
49
+ import { PassthroughHandler, runChannel } from './passthrough-handler';
50
+ import { Publisher } from './publisher';
51
+ import { ShadowHandler, ShadowUpdate } from './shadow-handler';
51
52
  import { TransactionManager } from './transaction-manager';
52
- import { reboot } from '../device-control/device-control';
53
-
54
53
  import { exec } from 'child_process';
55
54
  import { promisify } from 'util';
56
- import { LiveUpdatesHandler } from './live-updates-handler';
57
55
 
58
56
  const exec_promise = promisify(exec);
59
57
 
@@ -69,6 +67,8 @@ export class DeviceAgentCloudConnection {
69
67
  private port = 8883;
70
68
  private readonly toDeviceTopic = getToDeviceTopic(this.clientId);
71
69
  private readonly secureTunnelNotifyTopic = `$aws/things/${this.clientId}/tunnels/notify`;
70
+ private readonly secureTunnelHandler =
71
+ SecureTunnelHandlerSingleton.getInstance();
72
72
  // FIXME: Add support for multiple simultaneous project updates
73
73
  private appCfgUpdateQueue: ShadowUpdate[] = [];
74
74
 
@@ -144,11 +144,10 @@ export class DeviceAgentCloudConnection {
144
144
  appInstallPayload,
145
145
  modelsInstallPayload
146
146
  };
147
- await this.atomicApplicationUpdate(
148
- installApp,
149
- [{ projectId, appReleaseHash, signedUrlsPayload }],
150
- projectId
151
- );
147
+ await this.atomicApplicationUpdate(async () => {
148
+ this.shadowHandler.clearProjectShadow(projectId);
149
+ await installApp({ projectId, appReleaseHash, signedUrlsPayload });
150
+ }, projectId);
152
151
  return true;
153
152
  };
154
153
 
@@ -165,23 +164,25 @@ export class DeviceAgentCloudConnection {
165
164
  const projectId = payload.modelsInstallResponse.projectId;
166
165
  if (appCfgUpdate) {
167
166
  await this.atomicApplicationUpdate(
168
- updateModelsWithPresignedUrls,
169
- [
170
- {
167
+ async () =>
168
+ await updateModelsWithPresignedUrls({
171
169
  projectId,
172
170
  modelInstallPayloads: payload.modelsInstallResponse.newModels,
173
171
  newAppCfg: appCfgUpdate.newAppCfg
174
- }
175
- ],
172
+ }),
176
173
  projectId
177
174
  );
178
175
  }
179
176
 
180
177
  if (envVarUpdate) {
181
178
  await this.atomicApplicationUpdate(
182
- setEnv,
183
- [{ projectId, envVars: envVarUpdate.envVars }],
184
- projectId
179
+ async () =>
180
+ await this.shadowHandler.updateProjectEnvVars({
181
+ projectId,
182
+ envVars: envVarUpdate.envVars
183
+ }),
184
+ projectId,
185
+ true
185
186
  );
186
187
  }
187
188
  return true;
@@ -214,7 +215,7 @@ export class DeviceAgentCloudConnection {
214
215
  private async atomicApplicationUninstall(projectId: string) {
215
216
  try {
216
217
  await uninstallApp({ projectId });
217
- this.shadowHandler.clearAppConfig(projectId);
218
+ this.shadowHandler.clearProjectShadow(projectId);
218
219
  } catch (e) {
219
220
  logger.error(`Failed to uninstall ${projectId}: ${e.message}`);
220
221
  throw e;
@@ -222,11 +223,11 @@ export class DeviceAgentCloudConnection {
222
223
  }
223
224
 
224
225
  // eslint-disable-next-line
225
- private async atomicApplicationUpdate<T extends any[], R extends any>(
226
- func: (...args: T) => R,
227
- args: T,
228
- projectId: string
229
- ) {
226
+ private async atomicApplicationUpdate <F extends () => any>(
227
+ func: F,
228
+ projectId: string,
229
+ skipUpdateShadow?: boolean
230
+ ): Promise<ReturnType<F>> {
230
231
  // First try to create a backup, so that there is one available if something goes wrong in the next try:catch.
231
232
  if (await AgentConfigFile().isAppPresent({ projectId })) {
232
233
  try {
@@ -239,9 +240,9 @@ export class DeviceAgentCloudConnection {
239
240
  }
240
241
 
241
242
  try {
242
- const out: R = await func(...args);
243
- this.shadowHandler.clearAppConfig(projectId);
244
- await this.shadowHandler.updateProjectShadow(projectId);
243
+ const out: ReturnType<F> = await func();
244
+ if (!skipUpdateShadow)
245
+ await this.shadowHandler.updateProjectShadow(projectId);
245
246
  return out;
246
247
  } catch (errorAppUpdate) {
247
248
  logger.error(
@@ -256,9 +257,9 @@ export class DeviceAgentCloudConnection {
256
257
  } catch (errorRollbackApp) {
257
258
  // and if that fails, uninstall the app as a last resort.
258
259
  try {
259
- await uninstallApp({ projectId });
260
- } finally {
261
- this.shadowHandler.clearAppConfig(projectId);
260
+ await this.atomicApplicationUninstall(projectId);
261
+ } catch {
262
+ // atomicApplicationUninstall handles failing, so there's nothing to handle here.
262
263
  }
263
264
  logger.error(
264
265
  `Application update failed, rolled back to previous version: ${errorAppUpdate}`
@@ -273,15 +274,14 @@ export class DeviceAgentCloudConnection {
273
274
  }
274
275
  }
275
276
 
276
- private handleAppConfigUpdate = async (
277
+ private handleProjectShadowConfigUpdate = async (
277
278
  update: ShadowUpdate,
278
279
  txId: string
279
280
  ): Promise<boolean> => {
280
281
  const { projectId, appCfgUpdate, envVarUpdate } = update;
281
282
 
282
283
  if (
283
- appCfgUpdate &&
284
- appCfgUpdate.updatedModels &&
284
+ appCfgUpdate?.updatedModels &&
285
285
  Object.keys(appCfgUpdate.updatedModels).length
286
286
  ) {
287
287
  // When there are model updates request signed URLs and wait to apply config changes
@@ -311,27 +311,64 @@ export class DeviceAgentCloudConnection {
311
311
 
312
312
  if (appCfgUpdate) {
313
313
  await this.atomicApplicationUpdate(
314
- updateAppCfg,
315
- [
316
- {
314
+ async () =>
315
+ await updateAppCfg({
317
316
  projectId,
318
317
  newAppCfg: appCfgUpdate.newAppCfg
319
- }
320
- ],
318
+ }),
321
319
  projectId
322
320
  );
323
321
  }
324
322
 
325
323
  if (envVarUpdate) {
326
324
  await this.atomicApplicationUpdate(
327
- setEnv,
328
- [{ projectId, envVars: envVarUpdate.envVars }],
329
- projectId
325
+ async () =>
326
+ await this.shadowHandler.updateProjectEnvVars({
327
+ projectId,
328
+ envVars: envVarUpdate.envVars
329
+ }),
330
+ projectId,
331
+ true
330
332
  );
331
333
  }
332
334
  return true;
333
335
  };
334
336
 
337
+ private async handleProjectShadowMessage(topic: string, message: any) {
338
+ const shadowUpdates = await this.shadowHandler.handleProjectShadow({
339
+ topic,
340
+ payload: message,
341
+ clientToken: message.clientToken
342
+ });
343
+ if (shadowUpdates.length) {
344
+ const shadowUpdatePromises: Promise<void>[] = [];
345
+ for (const shadowUpdate of shadowUpdates) {
346
+ const projectId = shadowUpdate.projectId;
347
+ const txId = shadowUpdate.txId;
348
+ shadowUpdatePromises.push(
349
+ this.txnMgr
350
+ .runTransactionStep({
351
+ func: () =>
352
+ this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
353
+ projectId,
354
+ txId,
355
+ start: true,
356
+ stepName: topic
357
+ })
358
+ .catch((e: Error) => {
359
+ logger.error(
360
+ `There was an issue updating project shadow config: ${JSON.stringify(
361
+ e
362
+ )}`
363
+ );
364
+ })
365
+ );
366
+ }
367
+
368
+ await Promise.all(shadowUpdatePromises);
369
+ }
370
+ }
371
+
335
372
  /*=================================================================
336
373
  Public interface
337
374
  =================================================================*/
@@ -359,11 +396,11 @@ export class DeviceAgentCloudConnection {
359
396
 
360
397
  this.subscribe(this.toDeviceTopic);
361
398
  this.subscribe(this.secureTunnelNotifyTopic);
362
- this.subscribe(this.shadowHandler.shadowTopics.projects.getAccepted);
363
- this.subscribe(this.shadowHandler.shadowTopics.projects.getRejected);
364
- this.subscribe(this.shadowHandler.shadowTopics.projects.updateDelta);
365
- this.subscribe(this.shadowHandler.shadowTopics.projects.updateAccepted);
366
- this.subscribe(this.shadowHandler.shadowTopics.projects.updateRejected);
399
+ for (const topic of this.shadowHandler.projectShadowTopics) {
400
+ this.subscribe(topic);
401
+ }
402
+ this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.updateDelta);
403
+ this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted);
367
404
  }
368
405
 
369
406
  public getClientId(): string {
@@ -374,10 +411,6 @@ export class DeviceAgentCloudConnection {
374
411
  return this.toDeviceTopic;
375
412
  }
376
413
 
377
- public getShadowTopics(): ShadowTopics {
378
- return this.shadowHandler.shadowTopics;
379
- }
380
-
381
414
  public isCmdInProgress(projectId: string): boolean {
382
415
  return this.txnMgr.isOngoingTransactionForProjectID(projectId);
383
416
  }
@@ -412,7 +445,8 @@ export class DeviceAgentCloudConnection {
412
445
  live_state_updates,
413
446
  app_install_response,
414
447
  models_install_response,
415
- status_response
448
+ status_response,
449
+ device_action
416
450
  } = keyMirrors.toDeviceAgentMessageType;
417
451
  switch (message.messageType) {
418
452
  case app_state_control: {
@@ -520,8 +554,20 @@ export class DeviceAgentCloudConnection {
520
554
  }
521
555
  break;
522
556
  }
523
- case keyMirrors.toDeviceAgentMessageType.device_action: {
557
+ case device_action: {
524
558
  try {
559
+ const statusResponsePayload: StatusResponsePayload = {
560
+ status: keyMirrors.statusResponse.in_progress
561
+ };
562
+ const statusResponseMessage = buildToClientStatusResponseMessage(
563
+ this.clientId,
564
+ statusResponsePayload,
565
+ txId
566
+ );
567
+ this.publisher.publishToClient(statusResponseMessage);
568
+
569
+ await this.handleDeviceAction(message.payload);
570
+
525
571
  const successStatusResponsePayload: StatusResponsePayload = {
526
572
  status: keyMirrors.statusResponse.success
527
573
  };
@@ -532,8 +578,6 @@ export class DeviceAgentCloudConnection {
532
578
  txId
533
579
  );
534
580
  this.publisher.publishToClient(successStatusResponseMessage);
535
-
536
- await this.handleDeviceAction(message.payload);
537
581
  } catch (e) {
538
582
  logger.error(
539
583
  `There was a problem performing device action '${message.payload.action}': ${e.message}`
@@ -570,50 +614,36 @@ export class DeviceAgentCloudConnection {
570
614
  logger.debug(
571
615
  `Received message: ${JSON.stringify({ topic, message }, null, 2)}`
572
616
  );
573
- switch (topic) {
574
- case this.shadowHandler.shadowTopics.projects.getAccepted:
575
- case this.shadowHandler.shadowTopics.projects.updateAccepted: {
576
- const shadowUpdates = await this.shadowHandler.handleShadowTopic({
577
- topic,
578
- payload: message.state,
579
- clientToken: message.clientToken
580
- });
581
- if (shadowUpdates.length) {
582
- for (const shadowUpdate of shadowUpdates) {
583
- const projectId = shadowUpdate.projectId;
584
- const txId = shadowUpdate.txId;
585
- try {
586
- await this.txnMgr.runTransactionStep({
587
- func: () => this.handleAppConfigUpdate(shadowUpdate, txId),
588
- projectId,
589
- txId,
590
- start: true,
591
- stepName: topic
592
- });
593
- } catch (e) {
594
- logger.error(`Error handling shadow message: ${e.message}`);
595
- }
596
- }
597
- }
598
- break;
599
- }
600
- case this.shadowHandler.shadowTopics.projects.getRejected:
601
- case this.shadowHandler.shadowTopics.projects.updateDelta:
602
- case this.shadowHandler.shadowTopics.projects.updateRejected:
603
- // Not handling these for now
604
- break;
605
- case this.toDeviceTopic:
606
- await this.handleDeviceAgentMessage({
607
- topic,
608
- message
609
- });
610
- break;
611
-
612
- case this.secureTunnelNotifyTopic:
613
- await secureTunnelNotifyHandler(message);
614
- break;
615
- default:
616
- logger.error(`Unexpected topic, ignoring! ${topic}`);
617
+ // ProjectShadow messages
618
+ if (this.shadowHandler.projectShadowTopics.includes(topic)) {
619
+ await this.handleProjectShadowMessage(topic, message);
620
+ } else if (topic === this.toDeviceTopic) {
621
+ await this.handleDeviceAgentMessage({
622
+ topic,
623
+ message
624
+ });
625
+ // SecureTunnelNotify messages
626
+ } else if (topic === this.secureTunnelNotifyTopic) {
627
+ await this.secureTunnelHandler.secureTunnelNotifyHandler(message);
628
+ // SecureTunnel messages
629
+ } else if (
630
+ topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
631
+ ) {
632
+ logger.info(`Received secure tunnel update: ${message}`);
633
+ const reported = await this.secureTunnelHandler.syncShadowToDeviceState(
634
+ message
635
+ );
636
+ this.publisher.publish(
637
+ this.shadowHandler.shadowTopics.secureTunnel.update,
638
+ JSON.stringify({ state: { reported } })
639
+ );
640
+ } else if (
641
+ topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
642
+ ) {
643
+ logger.info(`Received secure tunnel deleteAccepted: ${message}`);
644
+ await this.secureTunnelHandler.destroy();
645
+ } else {
646
+ logger.error(`Unexpected topic, ignoring! ${topic}`);
617
647
  }
618
648
  }
619
649
 
@@ -710,9 +740,9 @@ export class DeviceAgentCloudConnection {
710
740
  }
711
741
 
712
742
  public async stop() {
713
- // FIXME: This method is currently only used by the CLI, and shadow messages
714
- // can be lost since we aren't waiting for responses so sleep for a short
715
- // time to receive them
743
+ // This method is currently only used by the CLI, and shadow messages can be
744
+ // lost since we aren't waiting for responses so sleep for a short time to
745
+ // receive them
716
746
  await sleep(1000);
717
747
  this.device.end();
718
748
  }
@@ -7,15 +7,16 @@ import {
7
7
  buildAppStateMessage,
8
8
  buildDeviceStatsMessage,
9
9
  StatusResponsePayload,
10
- buildToClientStatusResponseMessage
10
+ buildToClientStatusResponseMessage,
11
+ ToClientMessageTypeValue
11
12
  } from '@alwaysai/device-agent-schemas';
12
13
  import { getAppLogs } from '../application-control';
13
14
  import { logger } from '../util/logger';
14
15
  import sleep from '../util/sleep';
15
16
  import { Publisher } from './publisher';
16
17
  import { getAppStatePayload, getDeviceStatsPayload } from './messages';
17
- import { ToClientMessageTypeValue } from '@alwaysai/device-agent-schemas';
18
18
  import { ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS } from '../environment';
19
+ import { read } from 'fs';
19
20
 
20
21
  const LIVE_UPDATES_TIMEOUT = ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS
21
22
  ? parseInt(ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS)
@@ -42,14 +43,41 @@ export class LiveUpdatesHandler {
42
43
  private appLogStreams = new Set<string>();
43
44
  private transactionStatuses = new Set<string>();
44
45
 
46
+ private async getAppLogsWithRetry(
47
+ projectId: string
48
+ ): Promise<NodeJS.ReadableStream | null> {
49
+ // Retry starting logs until it starts successfully or is terminated
50
+ while (this.appLogStreams.has(projectId)) {
51
+ try {
52
+ return await getAppLogs({
53
+ projectId,
54
+ args: ['--tail', '100', '--no-log-prefix']
55
+ });
56
+ } catch (e) {
57
+ logger.info(
58
+ `Failed to start app logs, retrying in 1 second. Error: ${e}`
59
+ );
60
+ await sleep(1000);
61
+ }
62
+ }
63
+ // Case where logs were disabled prior to connecting
64
+ return null;
65
+ }
66
+
45
67
  private async startAppLogStream(projectId: string, txId: string) {
46
68
  logger.info(`Starting log stream for ${projectId}`);
47
69
 
48
70
  this.appLogStreams.add(projectId);
49
- const readable = await getAppLogs({
50
- projectId,
51
- args: ['--tail', '100', '--no-log-prefix']
52
- });
71
+
72
+ const readable = await this.getAppLogsWithRetry(projectId);
73
+
74
+ if (readable === null) {
75
+ logger.info(
76
+ `App log stream terminated for project ${projectId} prior to starting`
77
+ );
78
+ return;
79
+ }
80
+
53
81
  readable.on('data', async (chunk: Buffer) => {
54
82
  if (!this.appLogStreams.has(projectId)) {
55
83
  // why doesn't typescript know about this function?
@@ -12,15 +12,14 @@ import {
12
12
  import { AgentConfigFile } from '../infrastructure/agent-config';
13
13
 
14
14
  export async function getAppStatePayload(): Promise<AppStatePayload> {
15
- const appState: AppState[] = [];
15
+ const appStatePromises: Promise<AppState>[] = [];
16
16
  const apps = await AgentConfigFile().getApps();
17
17
  for (const app of apps) {
18
18
  const projectId = app.projectId;
19
- const status = await getAppState({ projectId });
20
- appState.push(status);
19
+ appStatePromises.push(getAppState({ projectId }));
21
20
  }
22
21
  const appStatePayload = {
23
- appState: appState
22
+ appState: await Promise.all(appStatePromises)
24
23
  };
25
24
  return appStatePayload;
26
25
  }