@alwaysai/device-agent 0.0.7 → 0.0.9

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 (77) hide show
  1. package/lib/application-control/backup.d.ts.map +1 -1
  2. package/lib/application-control/backup.js +2 -0
  3. package/lib/application-control/backup.js.map +1 -1
  4. package/lib/application-control/config.d.ts +17 -0
  5. package/lib/application-control/config.d.ts.map +1 -0
  6. package/lib/application-control/config.js +62 -0
  7. package/lib/application-control/config.js.map +1 -0
  8. package/lib/application-control/environment-variables.d.ts.map +1 -1
  9. package/lib/application-control/environment-variables.js +6 -14
  10. package/lib/application-control/environment-variables.js.map +1 -1
  11. package/lib/application-control/index.d.ts +2 -1
  12. package/lib/application-control/index.d.ts.map +1 -1
  13. package/lib/application-control/index.js +6 -1
  14. package/lib/application-control/index.js.map +1 -1
  15. package/lib/application-control/install.d.ts +16 -10
  16. package/lib/application-control/install.d.ts.map +1 -1
  17. package/lib/application-control/install.js +95 -57
  18. package/lib/application-control/install.js.map +1 -1
  19. package/lib/application-control/models.d.ts +3 -0
  20. package/lib/application-control/models.d.ts.map +1 -1
  21. package/lib/application-control/models.js +96 -20
  22. package/lib/application-control/models.js.map +1 -1
  23. package/lib/application-control/status.d.ts +3 -2
  24. package/lib/application-control/status.d.ts.map +1 -1
  25. package/lib/application-control/status.js +8 -6
  26. package/lib/application-control/status.js.map +1 -1
  27. package/lib/application-control/utils.d.ts +5 -0
  28. package/lib/application-control/utils.d.ts.map +1 -1
  29. package/lib/application-control/utils.js +47 -13
  30. package/lib/application-control/utils.js.map +1 -1
  31. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +42 -15
  32. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  33. package/lib/cloud-connection/device-agent-cloud-connection.js +357 -195
  34. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  35. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  36. package/lib/infrastructure/agent-config.js +7 -18
  37. package/lib/infrastructure/agent-config.js.map +1 -1
  38. package/lib/infrastructure/agent-config.test.js +47 -0
  39. package/lib/infrastructure/agent-config.test.js.map +1 -1
  40. package/lib/subcommands/app/app.js +1 -1
  41. package/lib/subcommands/app/app.js.map +1 -1
  42. package/lib/subcommands/login.d.ts.map +1 -1
  43. package/lib/subcommands/login.js +4 -3
  44. package/lib/subcommands/login.js.map +1 -1
  45. package/lib/util/copy-dir.d.ts.map +1 -1
  46. package/lib/util/copy-dir.js +3 -1
  47. package/lib/util/copy-dir.js.map +1 -1
  48. package/lib/util/run-in-dir.d.ts +2 -0
  49. package/lib/util/run-in-dir.d.ts.map +1 -0
  50. package/lib/util/run-in-dir.js +17 -0
  51. package/lib/util/run-in-dir.js.map +1 -0
  52. package/lib/util/sleep.d.ts +2 -0
  53. package/lib/util/sleep.d.ts.map +1 -0
  54. package/lib/util/sleep.js +9 -0
  55. package/lib/util/sleep.js.map +1 -0
  56. package/package.json +4 -3
  57. package/src/application-control/backup.ts +3 -0
  58. package/src/application-control/config.ts +61 -0
  59. package/src/application-control/environment-variables.ts +6 -12
  60. package/src/application-control/index.ts +5 -0
  61. package/src/application-control/install.ts +147 -68
  62. package/src/application-control/models.ts +136 -23
  63. package/src/application-control/status.ts +19 -9
  64. package/src/application-control/utils.ts +58 -13
  65. package/src/cloud-connection/device-agent-cloud-connection.ts +459 -216
  66. package/src/infrastructure/agent-config.test.ts +56 -0
  67. package/src/infrastructure/agent-config.ts +10 -19
  68. package/src/subcommands/app/app.ts +1 -1
  69. package/src/subcommands/login.ts +6 -4
  70. package/src/util/copy-dir.ts +3 -1
  71. package/src/util/run-in-dir.ts +15 -0
  72. package/src/util/sleep.ts +5 -0
  73. package/lib/util/run-cli-cmd.d.ts +0 -5
  74. package/lib/util/run-cli-cmd.d.ts.map +0 -1
  75. package/lib/util/run-cli-cmd.js +0 -24
  76. package/lib/util/run-cli-cmd.js.map +0 -1
  77. package/src/util/run-cli-cmd.ts +0 -18
@@ -5,178 +5,212 @@ import {
5
5
  getCertificateFilePath,
6
6
  getRootCertificateFilePath,
7
7
  } from '../util/directories';
8
+ import sleep from '../util/sleep';
8
9
  import {
9
10
  startApp,
10
11
  stopApp,
11
12
  restartApp,
12
- getAppStatus,
13
13
  getAppLogs,
14
+ getAppStatus,
14
15
  } from '../application-control/status';
15
- import { installApp } from '../application-control/install';
16
- import { getCpuUtil, getDiskUtil, getMemUtil } from '../device-control/device-control';
17
16
  import {
18
- DeviceAgentMessage,
17
+ getInstalledApps,
18
+ installApp,
19
+ uninstallApp,
20
+ } from '../application-control/install';
21
+ import {
22
+ keyMirrors,
23
+ validateClientMessage,
24
+ DeviceAgentMessagePayload,
25
+ AppInstallStatusPacket,
26
+ AppStateControlPacket,
27
+ AppVersionControlPacket,
28
+ LiveUpdatesToggles,
29
+ AppLogsPacket,
30
+ AppStatePacket,
31
+ LiveUpdatesToggleMessage,
19
32
  DeviceStatsMessage,
20
- AppState,
21
33
  AppStateMessage,
22
- ActionMessage,
23
- AppLogs,
24
34
  AppLogsMessage,
25
- InstallationStatusMessage,
26
- InstallationStatus,
27
- InstallationStatusEnum,
35
+ AppInstallStatusMessage,
36
+ SignedUrlsRequestMessage,
37
+ DeviceAgentMessage,
38
+ ClientMessage,
39
+ AppDetailsPacket,
28
40
  } from '@alwaysai/device-agent-schemas';
29
41
  import { getDeviceId } from '../util/get-device-id';
42
+ import { JsSpawner, logger } from 'alwaysai/lib/util';
43
+ import { getCpuUtil, getDiskUtil, getMemUtil } from '../device-control/device-control';
30
44
  import { AgentConfigFile } from '../infrastructure/agent-config';
45
+ import { buildApp, getAppConfig, getAppDir } from '../application-control/utils';
46
+ import { updateModelsWithPresignedUrls } from '../application-control/models';
47
+ import { updateAppConfig } from '../application-control/config';
31
48
 
32
49
  export class DeviceAgentCloudConnection {
33
50
  private clientId = getDeviceId();
34
51
  private host = getIoTCoreEndpointUrl();
35
- private publishable = false;
36
- private readonly publishInterval = 10000;
37
- private readonly installationStatusInterval = 5000;
38
- private installationStatus: InstallationStatus;
39
- private publishableTimeout: ReturnType<typeof setTimeout>;
40
-
41
- public device = awsIot.device;
42
- public agentTopicPrefix = `destination/agent/device/${this.clientId}/topic/`;
43
- public cloudTopicPrefix = `destination/cloud/device/${this.clientId}/topic/`;
52
+ private appInstallStatus: AppInstallStatusPacket;
53
+ private liveUpdatesTimeout: ReturnType<typeof setTimeout>;
54
+ private liveUpdatesAlive = {
55
+ [keyMirrors.agentMessageType.device_stats]: false,
56
+ [keyMirrors.agentMessageType.app_state]: false,
57
+ [keyMirrors.agentMessageType.app_logs]: false,
58
+ };
59
+ private liveUpdatesSleepIntervals = {
60
+ [keyMirrors.agentMessageType.device_stats]: 5000,
61
+ [keyMirrors.agentMessageType.app_state]: 5000,
62
+ [keyMirrors.agentMessageType.app_logs]: 5000,
63
+ [keyMirrors.agentMessageType.app_install_status]: 5000,
64
+ };
65
+ private appLogStreams = new Set<string>();
66
+ private deviceType = 'aai-device';
67
+ private readonly shadowPrefix = `$aws/things/${this.clientId}/shadow/name/`;
68
+ private readonly shadowTopics = {
69
+ projects: {
70
+ updateDelta: `${this.shadowPrefix}projects/update/delta`,
71
+ getAccepted: `${this.shadowPrefix}projects/get/accepted`,
72
+ },
73
+ };
74
+ private readonly toCloudTopic = `topic/to_cloud/${this.deviceType}/${this.clientId}`;
75
+ private readonly toClientTopic = `topic/to_client/${this.deviceType}/${this.clientId}`;
76
+ private readonly toDeviceTopic = `topic/to_device/${this.deviceType}/${this.clientId}`;
44
77
 
45
- constructor() {
46
- this.device = awsIot.device({
47
- keyPath: getPrivateKeyFilePath(),
48
- certPath: getCertificateFilePath(),
49
- caPath: getRootCertificateFilePath(),
50
- clientId: this.clientId,
51
- host: this.host,
52
- });
78
+ // device shadow utils
53
79
 
54
- this.device.subscribe(`${this.agentTopicPrefix}command`);
55
- this.device.subscribe(`${this.agentTopicPrefix}response`);
80
+ public getShadowPrefix() {
81
+ return this.shadowPrefix;
56
82
  }
57
83
 
58
- public setInstallationStatus(installationStatus: InstallationStatus) {
59
- this.installationStatus = installationStatus;
60
- }
84
+ private async handleNamedShadowUpdate({ payload }: { payload: string }) {
85
+ const delta = JSON.parse(payload);
86
+ const deltaKeys = Object.keys(delta);
87
+
88
+ for (const projectId of deltaKeys) {
89
+ const projectShadow = delta[projectId];
90
+ if (projectShadow.appConfig) {
91
+ const appConfig = projectShadow.appConfig;
92
+ const appDir = getAppDir(projectId);
93
+ await updateAppConfig(projectId, appConfig);
94
+
95
+ if (appConfig.models) {
96
+ this.publishCloudRequest({
97
+ messageType: keyMirrors.agentMessageType.signed_urls_request,
98
+ modelsOnlyUrlsRequest: {
99
+ projectId,
100
+ models: appConfig.models,
101
+ },
102
+ });
103
+ }
61
104
 
62
- public getInstallationStatus(): InstallationStatus {
63
- return this.installationStatus;
64
- }
105
+ if (appConfig.scripts && !appConfig.models) {
106
+ const appState = await getAppStatus({ projectId });
65
107
 
66
- public getPublishable(): boolean {
67
- return this.publishable;
68
- }
108
+ await buildApp({ appDir });
69
109
 
70
- public setPublishable(enabled: boolean) {
71
- this.publishable = enabled;
110
+ if (
111
+ appState.services.length &&
112
+ appState.services[0].state !== keyMirrors.appState.stopped
113
+ ) {
114
+ restartApp({ projectId });
115
+ }
116
+ await this.publishReportedState(projectId);
117
+ }
118
+ }
119
+ }
72
120
  }
73
121
 
74
- public restartPublishableTimeout() {
75
- clearTimeout(this.publishableTimeout);
76
- this.publishableTimeout = setTimeout(() => {
77
- this.setPublishable(false);
78
- }, 600000); // 10 min
79
- }
122
+ private async startAppLogStream(projectId: string) {
123
+ this.appLogStreams.add(projectId);
124
+ const readable = await getAppLogs({
125
+ projectId,
126
+ args: ['--tail', '100', '--no-log-prefix'],
127
+ });
128
+ readable.on('data', (chunk: Buffer) => {
129
+ if (!this.appLogStreams.has(projectId)) {
130
+ // why doesn't typescript know about this function?
131
+ // @ts-ignore
132
+ readable.destroy();
133
+ logger.info(`App log stream terminated for project ${projectId}`);
134
+ return;
135
+ }
136
+ const logStr = chunk.toString();
137
+ const message = {
138
+ messageType: keyMirrors.agentMessageType.app_logs,
139
+ appLogs: {
140
+ projectId,
141
+ logChunk: logStr,
142
+ },
143
+ };
144
+ const packet = this.buildMessagePacket(
145
+ this.getClientId(),
146
+ this.toClientTopic,
147
+ message,
148
+ );
149
+ this.publishMessage(this.toClientTopic, JSON.stringify(packet));
150
+ });
80
151
 
81
- public getPublishInterval() {
82
- return this.publishInterval;
83
- }
152
+ readable.on('error', (error) => {
153
+ logger.error(`App log stream terminated for project ${projectId}: ${error}`);
154
+ });
84
155
 
85
- public getInstallationStatusInterval() {
86
- return this.installationStatusInterval;
156
+ readable.on('finished', () => {
157
+ logger.info(`App logs finished piping for project ${projectId}`);
158
+ });
87
159
  }
88
160
 
89
- public getClientId(): string {
90
- return this.clientId;
161
+ // must contain app release hash
162
+ private initAppInstallStatus(installationStatus: AppInstallStatusPacket) {
163
+ this.appInstallStatus = installationStatus;
91
164
  }
92
165
 
93
- public publishMessage(topic: string, message: string) {
94
- this.device.publish(topic, message);
166
+ private updateAppInstallStatus(
167
+ installationStatus: Omit<AppInstallStatusPacket, 'appReleaseHash'>,
168
+ ) {
169
+ this.appInstallStatus.status = installationStatus.status;
170
+ this.appInstallStatus.message = installationStatus.message;
95
171
  }
96
- }
97
172
 
98
- export function runDeviceAgentCloudInterface() {
99
- const deviceAgent = new DeviceAgentCloudConnection();
173
+ private getAppInstallStatus(): AppInstallStatusPacket {
174
+ return this.appInstallStatus;
175
+ }
100
176
 
101
- async function buildMessagePacket(
177
+ // Message Builders
178
+ private buildMessagePacket(
179
+ deviceId: string,
102
180
  topic: string,
103
- payload:
104
- | DeviceStatsMessage
105
- | ActionMessage
106
- | AppStateMessage
107
- | AppLogsMessage
108
- | InstallationStatusMessage,
109
- ): Promise<DeviceAgentMessage> {
181
+ payload: DeviceAgentMessagePayload,
182
+ ): DeviceAgentMessage {
110
183
  const packet = {
111
184
  timestamp: new Date().toUTCString(),
112
- deviceId: deviceAgent.getClientId(),
185
+ deviceId,
113
186
  topic,
114
187
  payload,
115
188
  };
116
189
  return packet;
117
190
  }
118
191
 
119
- async function getAppLogsMessage(): Promise<AppLogsMessage> {
120
- const appLogsList: AppLogs[] = [];
192
+ private async getAppStateMessage(): Promise<AppStateMessage> {
193
+ const appStateMessage: AppStatePacket[] = [];
121
194
  const apps = await AgentConfigFile().getReadyApps();
122
- for (const app of apps) {
123
- const projectId = app.projectId;
124
- const readable = await getAppLogs({ projectId });
125
- const logs: any[] = [];
126
- readable.setEncoding('utf8');
127
- for await (const chunk of readable) {
128
- logs.push(chunk);
129
- }
130
- const appLogs = {
131
- projectId,
132
- logs,
133
- };
134
- appLogsList.push(appLogs);
135
- }
136
- const applicationStatePackage = {
137
- applicationLogs: appLogsList,
138
- };
139
- return applicationStatePackage;
140
- }
141
-
142
- // * This was causing massive CPU overhead
143
- // const publishAppLogs = setInterval(async function () {
144
- // const appLogsMessage = await getAppLogsMessage();
145
- // const topic = `device/${deviceAgent.getClientId()}/topic/application-management`;
146
- // const appLogsPacket = await buildMessagePacket(topic, appLogsMessage);
147
-
148
- // deviceAgent.publishMessage(topic, JSON.stringify({ appLogsPacket }));
149
- // }, deviceAgent.getPublishInterval());
150
-
151
- async function getAppStateMessage(): Promise<AppStateMessage> {
152
- const appStateMessage: AppState[] = [];
153
- const apps = await AgentConfigFile().getApps();
154
195
  for (const app of apps) {
155
196
  const projectId = app.projectId;
156
197
  const status = await getAppStatus({ projectId });
157
198
  appStateMessage.push(status);
158
199
  }
159
- const applicationStatePackage = {
160
- applicationState: appStateMessage,
200
+ const appStatePackage = {
201
+ messageType: keyMirrors.agentMessageType.app_state,
202
+ appState: appStateMessage,
161
203
  };
162
- return applicationStatePackage;
204
+ return appStatePackage;
163
205
  }
164
206
 
165
- const publishAppState = setInterval(async function () {
166
- if (deviceAgent.getPublishable()) {
167
- const topic = `${deviceAgent.cloudTopicPrefix}application-management`;
168
- const appStateMessage = await getAppStateMessage();
169
- const appStatePacket = await buildMessagePacket(topic, appStateMessage);
170
- deviceAgent.publishMessage(topic, JSON.stringify({ appStatePacket }));
171
- }
172
- }, deviceAgent.getPublishInterval());
173
-
174
- async function getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
207
+ private async getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
175
208
  const cpuUsage = await getCpuUtil();
176
209
  const diskUtil = await getDiskUtil();
177
210
  const memUtil = await getMemUtil();
178
211
 
179
212
  const deviceStatsMessage = {
213
+ messageType: keyMirrors.agentMessageType.device_stats,
180
214
  deviceStats: {
181
215
  cpuUsage,
182
216
  diskUtil,
@@ -186,137 +220,346 @@ export function runDeviceAgentCloudInterface() {
186
220
  return deviceStatsMessage;
187
221
  }
188
222
 
189
- const publishDeviceStats = setInterval(async function () {
190
- if (deviceAgent.getPublishable()) {
191
- const topic = `${deviceAgent.cloudTopicPrefix}device-management`;
192
- const deviceStatsMessage = await getDeviceStatsMessage();
193
- const deviceStatsPacket = await buildMessagePacket(topic, deviceStatsMessage);
194
- deviceAgent.publishMessage(topic, JSON.stringify({ deviceStatsPacket }));
223
+ // must be arrow function due to this context when function is passed as param
224
+ private getAppInstallStatusMessage = async (): Promise<AppInstallStatusMessage> => {
225
+ const appInstallStatus = this.getAppInstallStatus();
226
+ const appInstallStatusMessage = {
227
+ messageType: keyMirrors.agentMessageType.app_install_status,
228
+ appInstallStatus,
229
+ };
230
+ return appInstallStatusMessage;
231
+ };
232
+
233
+ private async startPublishingLiveUpdates(
234
+ topic: string,
235
+ messageType: string,
236
+ getMessageData: () => Promise<DeviceAgentMessagePayload>,
237
+ ) {
238
+ while (true) {
239
+ try {
240
+ const message = await getMessageData();
241
+ const packet = this.buildMessagePacket(this.getClientId(), topic, message);
242
+ this.publishMessage(topic, JSON.stringify(packet));
243
+ } catch (e) {
244
+ logger.error(`Error publishing live updates for ${messageType}: ${e.message}`);
245
+ break;
246
+ }
247
+ if (!this.continuePublishing(messageType)) {
248
+ logger.info(`Turned off live updates for ${messageType}`);
249
+ break;
250
+ }
251
+ await sleep(this.getLiveUpdatesInterval(messageType));
195
252
  }
196
- }, deviceAgent.getPublishInterval());
253
+ }
197
254
 
198
- const publishDeviceRequest = async ({ projectId, releaseHash }) => {
199
- const topic = `${deviceAgent.cloudTopicPrefix}request`;
200
- const deviceRequestPackage = {
201
- timestamp: new Date().toUTCString(),
202
- deviceId: deviceAgent.getClientId(),
203
- projectId,
204
- releaseHash,
205
- topic,
255
+ private continuePublishing(flag: string): boolean {
256
+ switch (flag) {
257
+ case keyMirrors.agentMessageType.device_stats:
258
+ case keyMirrors.agentMessageType.app_state:
259
+ return this.liveUpdatesAlive[flag];
260
+ case keyMirrors.agentMessageType.app_install_status:
261
+ return this.appInstallStatus.status === keyMirrors.appInstallStatus.in_progress;
262
+ default:
263
+ logger.error(`Unrecognized publishable flag ${flag}`);
264
+ return false;
265
+ }
266
+ }
267
+
268
+ private getLiveUpdatesInterval(flag: string): number {
269
+ const exists = this.liveUpdatesSleepIntervals[flag];
270
+ if (exists) {
271
+ return exists;
272
+ }
273
+ logger.error(`Unrecognized live updates flag ${flag}`);
274
+ return -1;
275
+ }
276
+
277
+ private setLiveUpdates(toggles: LiveUpdatesToggles) {
278
+ if (toggles.deviceStats) {
279
+ this.liveUpdatesAlive.device_stats = toggles.deviceStats;
280
+ }
281
+ if (toggles.appState) {
282
+ this.liveUpdatesAlive.app_state = toggles.appState;
283
+ }
284
+ }
285
+
286
+ private handleAppStateControl(payload: AppStateControlPacket) {
287
+ const { baseCommand, projectId } = payload;
288
+ switch (baseCommand) {
289
+ case keyMirrors.appStateControl.start:
290
+ startApp({ projectId });
291
+ break;
292
+ case keyMirrors.appStateControl.stop:
293
+ stopApp({ projectId });
294
+ break;
295
+ case keyMirrors.appStateControl.restart:
296
+ restartApp({ projectId });
297
+ break;
298
+ }
299
+ }
300
+
301
+ private handleAppVersionControl(payload: AppVersionControlPacket) {
302
+ const { projectId, appReleaseHash } = payload;
303
+ const signedUrlsRequest = { projectId, appReleaseHash };
304
+ this.publishCloudRequest({
305
+ messageType: keyMirrors.agentMessageType.signed_urls_request,
306
+ signedUrlsRequest,
307
+ });
308
+ }
309
+
310
+ private handleDeviceCommand = async (packet: any) => {
311
+ // TODO
312
+ };
313
+
314
+ private handleAgentCommand(message: LiveUpdatesToggleMessage) {
315
+ switch (message.messageType) {
316
+ case keyMirrors.clientMessageType.live_state_updates:
317
+ this.liveUpdatesBroker(message.liveUpdatesToggles);
318
+ break;
319
+ default:
320
+ logger.error(`Invalid agent action message type from message '${message}'`);
321
+ }
322
+ }
323
+
324
+ private restartLiveUpdatesTimeout() {
325
+ clearTimeout(this.liveUpdatesTimeout);
326
+ this.liveUpdatesTimeout = setTimeout(() => {
327
+ this.setLiveUpdates({
328
+ deviceStats: false,
329
+ appState: false,
330
+ });
331
+ this.appLogStreams.clear();
332
+ // TODO: Make constant, not hard coded
333
+ }, 600000); // 10 min
334
+ }
335
+
336
+ private async liveUpdatesBroker({
337
+ deviceStats,
338
+ appState,
339
+ appLogs,
340
+ }: {
341
+ deviceStats?: boolean;
342
+ appState?: boolean;
343
+ appLogs?: {
344
+ projectId: string;
345
+ toggle: boolean;
206
346
  };
207
- deviceAgent.publishMessage(
347
+ }) {
348
+ this.restartLiveUpdatesTimeout();
349
+ if (deviceStats !== undefined) {
350
+ this.liveUpdatesAlive.device_stats = deviceStats;
351
+ if (deviceStats) {
352
+ this.startPublishingLiveUpdates(
353
+ this.toClientTopic,
354
+ keyMirrors.agentMessageType.device_stats,
355
+ this.getDeviceStatsMessage,
356
+ );
357
+ }
358
+ }
359
+
360
+ if (appState !== undefined) {
361
+ this.liveUpdatesAlive.app_state = appState;
362
+ if (appState) {
363
+ this.startPublishingLiveUpdates(
364
+ this.toClientTopic,
365
+ keyMirrors.agentMessageType.app_state,
366
+ this.getAppStateMessage,
367
+ );
368
+ }
369
+ }
370
+
371
+ if (appLogs !== undefined) {
372
+ if (appLogs.toggle) {
373
+ this.startAppLogStream(appLogs.projectId);
374
+ } else {
375
+ this.appLogStreams.delete(appLogs.projectId);
376
+ }
377
+ }
378
+ }
379
+
380
+ private async publishReportedState(projectId) {
381
+ const newAppCfg = await getAppConfig(projectId);
382
+ const packet = {
383
+ state: {
384
+ reported: {
385
+ [projectId]: { appConfig: newAppCfg },
386
+ },
387
+ },
388
+ };
389
+ this.publishMessage(`${this.shadowPrefix}projects/update`, JSON.stringify(packet));
390
+ }
391
+
392
+ private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
393
+ const topic = this.toCloudTopic;
394
+ const deviceRequestPacket = this.buildMessagePacket(
395
+ this.getClientId(),
208
396
  topic,
209
- JSON.stringify({ device_request: deviceRequestPackage }),
397
+ payload,
210
398
  );
211
- };
399
+ this.publishMessage(topic, JSON.stringify({ deviceRequestPacket }));
400
+ }
212
401
 
213
- const publishInstallationStatus = async (interval: NodeJS.Timeout) => {
214
- const topic = `${deviceAgent.cloudTopicPrefix}installation-status`;
215
- const installationStatus = deviceAgent.getInstallationStatus();
216
- const installationStatusPacket = await buildMessagePacket(topic, {
217
- installationStatus,
402
+ // Public Methods
403
+
404
+ public device = awsIot.device;
405
+
406
+ constructor() {
407
+ this.device = awsIot.device({
408
+ keyPath: getPrivateKeyFilePath(),
409
+ certPath: getCertificateFilePath(),
410
+ caPath: getRootCertificateFilePath(),
411
+ clientId: this.clientId,
412
+ host: this.host,
218
413
  });
219
- deviceAgent.device.publish(topic, JSON.stringify({ installationStatusPacket }));
220
- if (installationStatus.status !== InstallationStatusEnum.IN_PROGRESS) {
221
- clearInterval(interval);
222
- }
223
- };
224
414
 
225
- const handleMessageTopic = async ({ topic, payload }) => {
226
- const action = payload['action'];
227
- const actionPayload: any = payload[action];
228
- const type = topic.split('/').slice(-1)[0];
415
+ this.device.subscribe(this.toDeviceTopic);
416
+ this.device.subscribe(this.shadowTopics.projects.getAccepted);
417
+ this.device.subscribe(this.shadowTopics.projects.updateDelta);
418
+ }
229
419
 
230
- if (!type) {
231
- return false;
232
- }
420
+ public getClientId(): string {
421
+ return this.clientId;
422
+ }
233
423
 
234
- switch (action) {
235
- /**
236
- * Install app package based on the given project ID
237
- */
238
- case 'install':
239
- /**
240
- {
241
- "action": "install",
242
- "install": {
243
- "releaseHash": "7fb2a812f9e7aa193208dac353521965da50d755085162066c125592f1ed760b",
244
- "projectId": "786e4686-a681-4cff-9e17-1e7d385c0fdb"
245
- }
246
- }
247
- */
248
- if (type === 'response') {
249
- deviceAgent.setInstallationStatus({
250
- status: InstallationStatusEnum.IN_PROGRESS,
251
- applicationReleaseHash: actionPayload.releaseHash as string,
252
- });
424
+ public publishMessage(topic: string, message: string) {
425
+ // TODO: topic validation
426
+ this.device.publish(topic, message);
427
+ }
253
428
 
254
- const installationStatusPing = setInterval(
255
- () => publishInstallationStatus(installationStatusPing),
256
- deviceAgent.getInstallationStatusInterval(),
257
- );
258
-
259
- // Install the app with the given url
260
- await (async () => {
261
- try {
262
- await installApp(actionPayload);
263
- deviceAgent.setInstallationStatus({
264
- status: InstallationStatusEnum.SUCCESS,
265
- applicationReleaseHash: actionPayload.releaseHash as string,
266
- });
267
- } catch (e) {
268
- const reason: string = e.message;
269
- deviceAgent.setInstallationStatus({
270
- status: InstallationStatusEnum.FAILURE,
271
- reason,
272
- applicationReleaseHash: actionPayload.releaseHash as string,
273
- });
274
- }
275
- })();
276
- break;
277
- }
278
- publishDeviceRequest(actionPayload);
429
+ public async handleClientMessage({
430
+ topic,
431
+ message,
432
+ }: {
433
+ topic: string;
434
+ message: ClientMessage;
435
+ }) {
436
+ const payload = message.payload;
437
+ switch (payload.messageType) {
438
+ case keyMirrors.clientMessageType.app_state_control: {
439
+ this.handleAppStateControl(payload.appStateControl);
279
440
  break;
280
- case 'start':
281
- startApp({ projectId: actionPayload.projectId });
441
+ }
442
+ case keyMirrors.clientMessageType.app_version_control: {
443
+ this.handleAppVersionControl(payload.appVersionControl);
282
444
  break;
283
- case 'stop':
284
- stopApp({ projectId: actionPayload.projectId });
445
+ }
446
+ case keyMirrors.clientMessageType.live_state_updates: {
447
+ this.handleAgentCommand(payload);
285
448
  break;
286
- case 'restart':
287
- restartApp({ projectId: actionPayload.projectId });
449
+ }
450
+ case keyMirrors.clientMessageType.app_install_cloud_response: {
451
+ const { projectId, appReleaseHash, appInstallPayload, modelsInstallPayload } =
452
+ payload.appInstallCloudResponse;
453
+
454
+ this.initAppInstallStatus({
455
+ status: keyMirrors.appInstallStatus.in_progress,
456
+ appReleaseHash,
457
+ });
458
+
459
+ this.startPublishingLiveUpdates(
460
+ this.toClientTopic,
461
+ keyMirrors.agentMessageType.app_install_status,
462
+ this.getAppInstallStatusMessage,
463
+ );
464
+
465
+ // Install the app and models
466
+ try {
467
+ const signedUrlsPayload = {
468
+ appInstallPayload,
469
+ modelsInstallPayload,
470
+ };
471
+ await installApp({
472
+ projectId,
473
+ appReleaseHash,
474
+ signedUrlsPayload,
475
+ });
476
+ this.updateAppInstallStatus({
477
+ status: keyMirrors.appInstallStatus.success,
478
+ });
479
+
480
+ // update app config shadow for project
481
+ await this.publishReportedState(projectId);
482
+ } catch (e) {
483
+ console.error(e);
484
+ const message: string = e.message;
485
+
486
+ // uninstall the failed app to put system back in good state
487
+ await uninstallApp({ projectId });
488
+ this.updateAppInstallStatus({
489
+ status: keyMirrors.appInstallStatus.failure,
490
+ message,
491
+ });
492
+
493
+ // delete shadow for project
494
+ this.publishMessage(`${this.shadowPrefix}${projectId}/delete`, '');
495
+ }
288
496
  break;
289
- /**
290
- * Allow/disallow publishing on this device. By default, (publishable = false)
291
- */
292
- case 'publishable':
293
- /**
294
- {
295
- "action": "publishable",
296
- "publishable": true | false
297
- }
298
- */
299
- deviceAgent.setPublishable(actionPayload);
300
- deviceAgent.restartPublishableTimeout();
497
+ }
498
+ case keyMirrors.clientMessageType.models_install_cloud_response: {
499
+ const { projectId, newModels } = payload.modelsInstallCloudResponse;
500
+
501
+ try {
502
+ await updateModelsWithPresignedUrls(projectId, newModels);
503
+
504
+ await this.publishReportedState(projectId);
505
+ } catch (e) {
506
+ console.error(e);
507
+ }
301
508
  break;
509
+ }
302
510
  default:
303
- break;
511
+ logger.error(`Invalid Client Message '${JSON.stringify(payload)}'`);
304
512
  }
305
- };
513
+ }
514
+
515
+ public async handleShadowTopic({ topic, payload }: { topic: string; payload: string }) {
516
+ const shadowName = topic.split('/')[5];
517
+ const message = JSON.parse(payload);
518
+ if (topic === this.shadowTopics.projects.updateDelta) {
519
+ this.handleNamedShadowUpdate({ payload });
520
+ } else if (topic === this.shadowTopics.projects.getAccepted) {
521
+ if (message.delta) {
522
+ this.handleNamedShadowUpdate({
523
+ payload: JSON.stringify(message.delta),
524
+ });
525
+ } else {
526
+ console.log(`No delta updates in shadow ${shadowName}`);
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+ export function runDeviceAgentCloudInterface() {
533
+ const deviceAgent = new DeviceAgentCloudConnection();
306
534
 
307
535
  deviceAgent.device.on('connect', function () {
308
536
  deviceAgent.publishMessage('connection', deviceAgent.getClientId());
309
537
  console.log('Device Agent has connected to the cloud');
538
+
539
+ // Get shadow updates
540
+ deviceAgent.publishMessage(`${deviceAgent.getShadowPrefix()}projects/get`, '');
310
541
  });
311
542
 
312
543
  deviceAgent.device.on('disconnect', function () {
313
544
  console.log('Device Agent has been disconnected from the cloud');
314
545
  });
315
546
 
316
- deviceAgent.device.on('message', function (topic, payload) {
317
- // ToDo: insert valdiation here for incoming messages. Maybe we will use a JSON schema for the message structure.
547
+ deviceAgent.device.on('message', function (topic: string, payload: string) {
318
548
  try {
319
- handleMessageTopic({ topic, payload: JSON.parse(payload) });
549
+ const jsonPacket = JSON.parse(payload);
550
+ if (jsonPacket.hasOwnProperty('state')) {
551
+ deviceAgent.handleShadowTopic({
552
+ topic,
553
+ payload: JSON.stringify(jsonPacket.state),
554
+ });
555
+ } else {
556
+ const valid = validateClientMessage(jsonPacket);
557
+ if (!valid) {
558
+ console.error(JSON.stringify(validateClientMessage.errors));
559
+ } else {
560
+ deviceAgent.handleClientMessage({ topic, message: jsonPacket });
561
+ }
562
+ }
320
563
  } catch (error) {
321
564
  console.error(error);
322
565
  }