@alwaysai/device-agent 0.0.12 → 0.0.13

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 (181) hide show
  1. package/lib/application-control/backup.d.ts.map +1 -1
  2. package/lib/application-control/backup.js +8 -2
  3. package/lib/application-control/backup.js.map +1 -1
  4. package/lib/application-control/config.d.ts +12 -4
  5. package/lib/application-control/config.d.ts.map +1 -1
  6. package/lib/application-control/config.js +59 -16
  7. package/lib/application-control/config.js.map +1 -1
  8. package/lib/application-control/environment-variables.d.ts.map +1 -1
  9. package/lib/application-control/environment-variables.js.map +1 -1
  10. package/lib/application-control/index.d.ts +4 -4
  11. package/lib/application-control/index.d.ts.map +1 -1
  12. package/lib/application-control/index.js +4 -3
  13. package/lib/application-control/index.js.map +1 -1
  14. package/lib/application-control/install.d.ts.map +1 -1
  15. package/lib/application-control/install.js +28 -14
  16. package/lib/application-control/install.js.map +1 -1
  17. package/lib/application-control/models.d.ts +7 -1
  18. package/lib/application-control/models.d.ts.map +1 -1
  19. package/lib/application-control/models.js +69 -39
  20. package/lib/application-control/models.js.map +1 -1
  21. package/lib/application-control/status.d.ts.map +1 -1
  22. package/lib/application-control/status.js +18 -14
  23. package/lib/application-control/status.js.map +1 -1
  24. package/lib/application-control/utils.d.ts +0 -2
  25. package/lib/application-control/utils.d.ts.map +1 -1
  26. package/lib/application-control/utils.js +7 -16
  27. package/lib/application-control/utils.js.map +1 -1
  28. package/lib/cloud-connection/app-install-status.d.ts +16 -0
  29. package/lib/cloud-connection/app-install-status.d.ts.map +1 -0
  30. package/lib/cloud-connection/app-install-status.js +53 -0
  31. package/lib/cloud-connection/app-install-status.js.map +1 -0
  32. package/lib/cloud-connection/bootstrap-provision.d.ts +2 -0
  33. package/lib/cloud-connection/bootstrap-provision.d.ts.map +1 -0
  34. package/lib/cloud-connection/bootstrap-provision.js +34 -0
  35. package/lib/cloud-connection/bootstrap-provision.js.map +1 -0
  36. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +12 -34
  37. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  38. package/lib/cloud-connection/device-agent-cloud-connection.js +169 -385
  39. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  40. package/lib/cloud-connection/device-agent.d.ts.map +1 -1
  41. package/lib/cloud-connection/device-agent.js +22 -26
  42. package/lib/cloud-connection/device-agent.js.map +1 -1
  43. package/lib/cloud-connection/live-updates-handler.d.ts +34 -0
  44. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -0
  45. package/lib/cloud-connection/live-updates-handler.js +167 -0
  46. package/lib/cloud-connection/live-updates-handler.js.map +1 -0
  47. package/lib/cloud-connection/messages.d.ts +14 -0
  48. package/lib/cloud-connection/messages.d.ts.map +1 -0
  49. package/lib/cloud-connection/messages.js +38 -0
  50. package/lib/cloud-connection/messages.js.map +1 -0
  51. package/lib/cloud-connection/publisher.d.ts +14 -0
  52. package/lib/cloud-connection/publisher.d.ts.map +1 -0
  53. package/lib/cloud-connection/publisher.js +44 -0
  54. package/lib/cloud-connection/publisher.js.map +1 -0
  55. package/lib/cloud-connection/shadow-handler.d.ts +34 -0
  56. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -0
  57. package/lib/cloud-connection/shadow-handler.js +94 -0
  58. package/lib/cloud-connection/shadow-handler.js.map +1 -0
  59. package/lib/cloud-connection/shadow.d.ts +16 -0
  60. package/lib/cloud-connection/shadow.d.ts.map +1 -0
  61. package/lib/cloud-connection/shadow.js +36 -0
  62. package/lib/cloud-connection/shadow.js.map +1 -0
  63. package/lib/device-control/device-control.d.ts.map +1 -1
  64. package/lib/device-control/device-control.js +1 -0
  65. package/lib/device-control/device-control.js.map +1 -1
  66. package/lib/docker/docker-cmd.js +1 -1
  67. package/lib/docker/docker-compose-cmd.d.ts.map +1 -1
  68. package/lib/docker/docker-compose-cmd.js +1 -1
  69. package/lib/docker/docker-compose-cmd.js.map +1 -1
  70. package/lib/endpoints.js +10 -10
  71. package/lib/endpoints.js.map +1 -1
  72. package/lib/environment.d.ts.map +1 -1
  73. package/lib/environment.js.map +1 -1
  74. package/lib/infrastructure/agent-config.d.ts +4 -14
  75. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  76. package/lib/infrastructure/agent-config.js +22 -15
  77. package/lib/infrastructure/agent-config.js.map +1 -1
  78. package/lib/infrastructure/agent-config.test.js +26 -18
  79. package/lib/infrastructure/agent-config.test.js.map +1 -1
  80. package/lib/infrastructure/system-id.d.ts +2 -0
  81. package/lib/infrastructure/system-id.d.ts.map +1 -0
  82. package/lib/infrastructure/system-id.js +21 -0
  83. package/lib/infrastructure/system-id.js.map +1 -0
  84. package/lib/infrastructure/tokens-and-device-cfg.d.ts +4 -0
  85. package/lib/infrastructure/tokens-and-device-cfg.d.ts.map +1 -0
  86. package/lib/infrastructure/tokens-and-device-cfg.js +31 -0
  87. package/lib/infrastructure/tokens-and-device-cfg.js.map +1 -0
  88. package/lib/infrastructure/urls.d.ts.map +1 -1
  89. package/lib/infrastructure/urls.js +3 -3
  90. package/lib/infrastructure/urls.js.map +1 -1
  91. package/lib/root.d.ts.map +1 -1
  92. package/lib/root.js +2 -7
  93. package/lib/root.js.map +1 -1
  94. package/lib/subcommands/app/app.d.ts.map +1 -1
  95. package/lib/subcommands/app/app.js +62 -60
  96. package/lib/subcommands/app/app.js.map +1 -1
  97. package/lib/subcommands/app/index.js +2 -2
  98. package/lib/subcommands/device/clean.d.ts +2 -0
  99. package/lib/subcommands/device/clean.d.ts.map +1 -0
  100. package/lib/subcommands/device/clean.js +29 -0
  101. package/lib/subcommands/device/clean.js.map +1 -0
  102. package/lib/subcommands/device/device.d.ts.map +1 -1
  103. package/lib/subcommands/device/device.js +40 -27
  104. package/lib/subcommands/device/device.js.map +1 -1
  105. package/lib/subcommands/device/index.d.ts.map +1 -1
  106. package/lib/subcommands/device/index.js +2 -1
  107. package/lib/subcommands/device/index.js.map +1 -1
  108. package/lib/subcommands/get-model-package.js +5 -5
  109. package/lib/subcommands/index.js +1 -1
  110. package/lib/subcommands/login.js +8 -8
  111. package/lib/subcommands/login.js.map +1 -1
  112. package/lib/util/clean-certs.d.ts +2 -0
  113. package/lib/util/clean-certs.d.ts.map +1 -0
  114. package/lib/util/clean-certs.js +16 -0
  115. package/lib/util/clean-certs.js.map +1 -0
  116. package/lib/util/directories.d.ts +16 -15
  117. package/lib/util/directories.d.ts.map +1 -1
  118. package/lib/util/directories.js +45 -26
  119. package/lib/util/directories.js.map +1 -1
  120. package/lib/util/get-device-id.d.ts +1 -1
  121. package/lib/util/get-device-id.d.ts.map +1 -1
  122. package/lib/util/get-device-id.js +14 -19
  123. package/lib/util/get-device-id.js.map +1 -1
  124. package/lib/util/http-client.d.ts +1 -1
  125. package/lib/util/http-client.d.ts.map +1 -1
  126. package/lib/util/http-client.js +10 -8
  127. package/lib/util/http-client.js.map +1 -1
  128. package/lib/util/logger.d.ts.map +1 -1
  129. package/lib/util/logger.js +4 -5
  130. package/lib/util/logger.js.map +1 -1
  131. package/lib/util/run-in-dir.d.ts.map +1 -1
  132. package/lib/util/run-in-dir.js +1 -0
  133. package/lib/util/run-in-dir.js.map +1 -1
  134. package/package.json +17 -8
  135. package/src/application-control/backup.ts +8 -3
  136. package/src/application-control/config.ts +75 -13
  137. package/src/application-control/environment-variables.ts +3 -3
  138. package/src/application-control/index.ts +19 -6
  139. package/src/application-control/install.ts +52 -28
  140. package/src/application-control/models.ts +100 -56
  141. package/src/application-control/status.ts +26 -21
  142. package/src/application-control/utils.ts +9 -20
  143. package/src/cloud-connection/app-install-status.ts +62 -0
  144. package/src/cloud-connection/bootstrap-provision.ts +40 -0
  145. package/src/cloud-connection/device-agent-cloud-connection.ts +257 -528
  146. package/src/cloud-connection/device-agent.ts +31 -38
  147. package/src/cloud-connection/live-updates-handler.ts +226 -0
  148. package/src/cloud-connection/messages.ts +39 -0
  149. package/src/cloud-connection/publisher.ts +65 -0
  150. package/src/cloud-connection/shadow-handler.ts +154 -0
  151. package/src/cloud-connection/shadow.ts +50 -0
  152. package/src/device-control/device-control.ts +1 -0
  153. package/src/docker/docker-cmd.ts +1 -1
  154. package/src/docker/docker-compose-cmd.ts +5 -2
  155. package/src/endpoints.ts +9 -9
  156. package/src/environment.ts +8 -3
  157. package/src/infrastructure/agent-config.test.ts +34 -23
  158. package/src/infrastructure/agent-config.ts +33 -20
  159. package/src/infrastructure/system-id.ts +18 -0
  160. package/src/infrastructure/tokens-and-device-cfg.ts +39 -0
  161. package/src/infrastructure/urls.ts +4 -2
  162. package/src/root.ts +2 -8
  163. package/src/subcommands/app/app.ts +64 -62
  164. package/src/subcommands/app/index.ts +3 -3
  165. package/src/subcommands/device/clean.ts +26 -0
  166. package/src/subcommands/device/device.ts +66 -50
  167. package/src/subcommands/device/index.ts +2 -1
  168. package/src/subcommands/get-model-package.ts +5 -5
  169. package/src/subcommands/index.ts +1 -1
  170. package/src/subcommands/login.ts +8 -8
  171. package/src/util/clean-certs.ts +12 -0
  172. package/src/util/directories.ts +68 -52
  173. package/src/util/get-device-id.ts +16 -18
  174. package/src/util/http-client.ts +18 -13
  175. package/src/util/logger.ts +6 -6
  176. package/src/util/run-in-dir.ts +2 -1
  177. package/lib/infrastructure/certificates-and-tokens.d.ts +0 -6
  178. package/lib/infrastructure/certificates-and-tokens.d.ts.map +0 -1
  179. package/lib/infrastructure/certificates-and-tokens.js +0 -43
  180. package/lib/infrastructure/certificates-and-tokens.js.map +0 -1
  181. package/src/infrastructure/certificates-and-tokens.ts +0 -53
@@ -1,318 +1,55 @@
1
- const awsIot = require("aws-iot-device-sdk");
2
- import { getIoTCoreEndpointUrl } from "../infrastructure/urls";
3
- import { existsSync, createReadStream } from "fs";
1
+ // eslint-disable-next-line
2
+ const awsIot = require('aws-iot-device-sdk');
3
+ import { getIoTCoreEndpointUrl } from '../infrastructure/urls';
4
+ import { existsSync } from 'fs';
4
5
  import {
5
6
  getPrivateKeyFilePath,
6
7
  getCertificateFilePath,
7
- getRootCertificateFilePath,
8
- BOOTSTRAP_CLAIM_ID_FILE_PATH,
9
- DEVICE_CLAIM_ID_FILE_PATH,
10
- BOOTSTRAP_DEVICE_CERTIFICATE_FILE_PATH,
11
8
  BOOTSTRAP_DEVICE_PRIVATE_KEY_FILE_PATH,
12
9
  AWS_ROOT_CERTIFICATE_FILE_PATH,
13
- } from "../util/directories";
14
-
15
- import { BootstrapAgent } from "./device-agent";
16
-
17
- import sleep from "../util/sleep";
18
- import {
19
- startApp,
20
- stopApp,
21
- restartApp,
22
- getAppLogs,
23
- getAppStatus,
24
- } from "../application-control/status";
25
- import {
26
- getInstalledApps,
27
- installApp,
28
- uninstallApp,
29
- } from "../application-control/install";
10
+ BOOTSTRAP_CERTIFICATES_DIR_PATH
11
+ } from '../util/directories';
30
12
  import {
31
13
  keyMirrors,
32
14
  validateClientMessage,
33
- DeviceAgentMessagePayload,
34
- AppInstallStatusPacket,
35
15
  AppStateControlPacket,
36
16
  AppVersionControlPacket,
37
- LiveUpdatesToggles,
38
- AppLogsPacket,
39
- AppStatePacket,
40
17
  LiveUpdatesToggleMessage,
41
- DeviceStatsMessage,
42
- AppStateMessage,
43
- AppLogsMessage,
44
- AppInstallStatusMessage,
45
18
  SignedUrlsRequestMessage,
46
- DeviceAgentMessage,
47
19
  ClientMessage,
48
- AppDetailsPacket,
49
- getClientTopic,
50
- getCloudTopic,
51
- getDeviceTopic,
52
- } from "@alwaysai/device-agent-schemas";
53
- import { getDeviceId } from "../util/get-device-id";
54
- import { JsSpawner, logger } from "alwaysai/lib/util";
55
- import {
56
- getCpuUtil,
57
- getDiskUtil,
58
- getMemUtil,
59
- } from "../device-control/device-control";
60
- import { AgentConfigFile } from "../infrastructure/agent-config";
20
+ getDeviceTopic
21
+ } from '@alwaysai/device-agent-schemas';
22
+ import { getDeviceUuid } from '../util/get-device-id';
23
+ import { logger } from '../util/logger';
24
+ import { AgentConfigFile } from '../infrastructure/agent-config';
61
25
  import {
62
- buildApp,
63
- getAppConfig,
64
- getAppDir,
65
- } from "../application-control/utils";
66
- import { updateModelsWithPresignedUrls } from "../application-control/models";
67
- import { updateAppConfig } from "../application-control/config";
26
+ startApp,
27
+ stopApp,
28
+ restartApp,
29
+ updateModelsWithPresignedUrls,
30
+ installApp,
31
+ uninstallApp,
32
+ updateAppCfg
33
+ } from '../application-control';
34
+ import { AppConfigUpdate, ShadowHandler } from './shadow-handler';
35
+ import { Publisher } from './publisher';
36
+ import { LiveUpdatesHandler } from './live-updates-handler';
37
+ import { bootstrapProvision } from './bootstrap-provision';
38
+ import { AppInstallStatusManager } from './app-install-status';
39
+ import { AppConfig } from '@alwaysai/app-configuration-schemas';
68
40
 
69
41
  export class DeviceAgentCloudConnection {
70
- private clientId = getDeviceId();
42
+ private shadowHandler: ShadowHandler;
43
+ private publisher: Publisher;
44
+ private liveUpdatesHandler: LiveUpdatesHandler;
45
+ private appInstallStatusMgr: AppInstallStatusManager;
46
+ private device = awsIot.device;
47
+
48
+ private clientId = getDeviceUuid();
71
49
  private host = getIoTCoreEndpointUrl();
72
- private appInstallStatus: AppInstallStatusPacket;
73
- private liveUpdatesTimeout: ReturnType<typeof setTimeout>;
74
- private liveUpdatesAlive = {
75
- [keyMirrors.agentMessageType.device_stats]: false,
76
- [keyMirrors.agentMessageType.app_state]: false,
77
- [keyMirrors.agentMessageType.app_logs]: false,
78
- };
79
- private liveUpdatesSleepIntervals = {
80
- [keyMirrors.agentMessageType.device_stats]: 5000,
81
- [keyMirrors.agentMessageType.app_state]: 5000,
82
- [keyMirrors.agentMessageType.app_logs]: 5000,
83
- [keyMirrors.agentMessageType.app_install_status]: 5000,
84
- };
85
- private appLogStreams = new Set<string>();
86
- private readonly shadowPrefix = `$aws/things/${this.clientId}/shadow/name/`;
87
- private readonly shadowTopics = {
88
- projects: {
89
- updateDelta: `${this.shadowPrefix}projects/update/delta`,
90
- getAccepted: `${this.shadowPrefix}projects/get/accepted`,
91
- },
92
- };
93
- private readonly toCloudTopic = getCloudTopic(this.clientId);
94
- private readonly toClientTopic = getClientTopic(this.clientId);
95
50
  private readonly toDeviceTopic = getDeviceTopic(this.clientId);
96
-
97
- // device shadow utils
98
-
99
- public getShadowPrefix() {
100
- return this.shadowPrefix;
101
- }
102
-
103
- private async handleNamedShadowUpdate({ payload }: { payload: string }) {
104
- const delta = JSON.parse(payload);
105
- const deltaKeys = Object.keys(delta);
106
-
107
- for (const projectId of deltaKeys) {
108
- const projectShadow = delta[projectId];
109
- if (projectShadow.appConfig) {
110
- const appConfig = projectShadow.appConfig;
111
- const appDir = getAppDir(projectId);
112
- await updateAppConfig(projectId, appConfig);
113
-
114
- if (appConfig.models) {
115
- this.publishCloudRequest({
116
- messageType: keyMirrors.agentMessageType.signed_urls_request,
117
- modelsOnlyUrlsRequest: {
118
- projectId,
119
- models: appConfig.models,
120
- },
121
- });
122
- }
123
-
124
- if (appConfig.scripts && !appConfig.models) {
125
- const appState = await getAppStatus({ projectId });
126
-
127
- await buildApp({ appDir });
128
-
129
- if (
130
- appState.services.length &&
131
- appState.services[0].state !== keyMirrors.appState.stopped
132
- ) {
133
- restartApp({ projectId });
134
- }
135
- await this.publishReportedState(projectId);
136
- }
137
- }
138
- }
139
- }
140
-
141
- private async startAppLogStream(projectId: string) {
142
- this.appLogStreams.add(projectId);
143
- const readable = await getAppLogs({
144
- projectId,
145
- args: ["--tail", "100", "--no-log-prefix"],
146
- });
147
- readable.on("data", (chunk: Buffer) => {
148
- if (!this.appLogStreams.has(projectId)) {
149
- // why doesn't typescript know about this function?
150
- // @ts-ignore
151
- readable.destroy();
152
- logger.info(`App log stream terminated for project ${projectId}`);
153
- return;
154
- }
155
- const logStr = chunk.toString();
156
- const message = {
157
- messageType: keyMirrors.agentMessageType.app_logs,
158
- appLogs: {
159
- projectId,
160
- logChunk: logStr,
161
- },
162
- };
163
- const packet = this.buildMessagePacket(
164
- this.getClientId(),
165
- this.toClientTopic,
166
- message
167
- );
168
- this.publishMessage(this.toClientTopic, JSON.stringify(packet));
169
- });
170
-
171
- readable.on("error", (error) => {
172
- logger.error(
173
- `App log stream terminated for project ${projectId}: ${error}`
174
- );
175
- });
176
-
177
- readable.on("finished", () => {
178
- logger.info(`App logs finished piping for project ${projectId}`);
179
- });
180
- }
181
-
182
- // must contain app release hash
183
- private initAppInstallStatus(installationStatus: AppInstallStatusPacket) {
184
- this.appInstallStatus = installationStatus;
185
- }
186
-
187
- private updateAppInstallStatus(
188
- installationStatus: Omit<AppInstallStatusPacket, "appReleaseHash">
189
- ) {
190
- this.appInstallStatus.status = installationStatus.status;
191
- this.appInstallStatus.message = installationStatus.message;
192
- }
193
-
194
- private getAppInstallStatus(): AppInstallStatusPacket {
195
- return this.appInstallStatus;
196
- }
197
-
198
- // Message Builders
199
- private buildMessagePacket(
200
- deviceId: string,
201
- topic: string,
202
- payload: DeviceAgentMessagePayload
203
- ): DeviceAgentMessage {
204
- const packet = {
205
- timestamp: new Date().toUTCString(),
206
- deviceId,
207
- topic,
208
- payload,
209
- };
210
- return packet;
211
- }
212
-
213
- private async getAppStateMessage(): Promise<AppStateMessage> {
214
- const appStateMessage: AppStatePacket[] = [];
215
- const apps = await AgentConfigFile().getReadyApps();
216
- for (const app of apps) {
217
- const projectId = app.projectId;
218
- const status = await getAppStatus({ projectId });
219
- appStateMessage.push(status);
220
- }
221
- const appStatePackage = {
222
- messageType: keyMirrors.agentMessageType.app_state,
223
- appState: appStateMessage,
224
- };
225
- return appStatePackage;
226
- }
227
-
228
- private async getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
229
- const cpuUsage = await getCpuUtil();
230
- const diskUtil = await getDiskUtil();
231
- const memUtil = await getMemUtil();
232
-
233
- const deviceStatsMessage = {
234
- messageType: keyMirrors.agentMessageType.device_stats,
235
- deviceStats: {
236
- cpuUsage,
237
- diskUtil,
238
- usedMemoryPercentage: memUtil,
239
- },
240
- };
241
- return deviceStatsMessage;
242
- }
243
-
244
- // must be arrow function due to this context when function is passed as param
245
- private getAppInstallStatusMessage =
246
- async (): Promise<AppInstallStatusMessage> => {
247
- const appInstallStatus = this.getAppInstallStatus();
248
- const appInstallStatusMessage = {
249
- messageType: keyMirrors.agentMessageType.app_install_status,
250
- appInstallStatus,
251
- };
252
- return appInstallStatusMessage;
253
- };
254
-
255
- private async startPublishingLiveUpdates(
256
- topic: string,
257
- messageType: string,
258
- getMessageData: () => Promise<DeviceAgentMessagePayload>
259
- ) {
260
- while (true) {
261
- try {
262
- const message = await getMessageData();
263
- const packet = this.buildMessagePacket(
264
- this.getClientId(),
265
- topic,
266
- message
267
- );
268
- this.publishMessage(topic, JSON.stringify(packet));
269
- } catch (e) {
270
- logger.error(
271
- `Error publishing live updates for ${messageType}: ${e.message}`
272
- );
273
- break;
274
- }
275
- if (!this.continuePublishing(messageType)) {
276
- logger.info(`Turned off live updates for ${messageType}`);
277
- break;
278
- }
279
- await sleep(this.getLiveUpdatesInterval(messageType));
280
- }
281
- }
282
-
283
- private continuePublishing(flag: string): boolean {
284
- switch (flag) {
285
- case keyMirrors.agentMessageType.device_stats:
286
- case keyMirrors.agentMessageType.app_state:
287
- return this.liveUpdatesAlive[flag];
288
- case keyMirrors.agentMessageType.app_install_status:
289
- return (
290
- this.appInstallStatus.status ===
291
- keyMirrors.appInstallStatus.in_progress
292
- );
293
- default:
294
- logger.error(`Unrecognized publishable flag ${flag}`);
295
- return false;
296
- }
297
- }
298
-
299
- private getLiveUpdatesInterval(flag: string): number {
300
- const exists = this.liveUpdatesSleepIntervals[flag];
301
- if (exists) {
302
- return exists;
303
- }
304
- logger.error(`Unrecognized live updates flag ${flag}`);
305
- return -1;
306
- }
307
-
308
- private setLiveUpdates(toggles: LiveUpdatesToggles) {
309
- if (toggles.deviceStats) {
310
- this.liveUpdatesAlive.device_stats = toggles.deviceStats;
311
- }
312
- if (toggles.appState) {
313
- this.liveUpdatesAlive.app_state = toggles.appState;
314
- }
315
- }
51
+ // FIXME: Add support for multiple simultaneous project updates
52
+ private newAppCfgQueue: AppConfig[] = [];
316
53
 
317
54
  private handleAppStateControl(payload: AppStateControlPacket) {
318
55
  const { baseCommand, projectId } = payload;
@@ -334,7 +71,7 @@ export class DeviceAgentCloudConnection {
334
71
  const signedUrlsRequest = { projectId, appReleaseHash };
335
72
  this.publishCloudRequest({
336
73
  messageType: keyMirrors.agentMessageType.signed_urls_request,
337
- signedUrlsRequest,
74
+ signedUrlsRequest
338
75
  });
339
76
  }
340
77
 
@@ -345,7 +82,7 @@ export class DeviceAgentCloudConnection {
345
82
  private handleAgentCommand(message: LiveUpdatesToggleMessage) {
346
83
  switch (message.messageType) {
347
84
  case keyMirrors.clientMessageType.live_state_updates:
348
- this.liveUpdatesBroker(message.liveUpdatesToggles);
85
+ this.liveUpdatesHandler.update(message.liveUpdatesToggles);
349
86
  break;
350
87
  default:
351
88
  logger.error(
@@ -354,91 +91,110 @@ export class DeviceAgentCloudConnection {
354
91
  }
355
92
  }
356
93
 
357
- private restartLiveUpdatesTimeout() {
358
- clearTimeout(this.liveUpdatesTimeout);
359
- this.liveUpdatesTimeout = setTimeout(() => {
360
- this.setLiveUpdates({
361
- deviceStats: false,
362
- appState: false,
363
- });
364
- this.appLogStreams.clear();
365
- // TODO: Make constant, not hard coded
366
- }, 600000); // 10 min
94
+ private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
95
+ this.publisher.publishToCloud(payload);
367
96
  }
368
97
 
369
- private async liveUpdatesBroker({
370
- deviceStats,
371
- appState,
372
- appLogs,
373
- }: {
374
- deviceStats?: boolean;
375
- appState?: boolean;
376
- appLogs?: {
377
- projectId: string;
378
- toggle: boolean;
379
- };
380
- }) {
381
- this.restartLiveUpdatesTimeout();
382
- if (deviceStats !== undefined) {
383
- this.liveUpdatesAlive.device_stats = deviceStats;
384
- if (deviceStats) {
385
- this.startPublishingLiveUpdates(
386
- this.toClientTopic,
387
- keyMirrors.agentMessageType.device_stats,
388
- this.getDeviceStatsMessage
389
- );
390
- }
391
- }
98
+ private subscribe(topic: string) {
99
+ logger.info(`Subscribing to ${topic}`);
100
+ this.device.subscribe(topic);
101
+ }
392
102
 
393
- if (appState !== undefined) {
394
- this.liveUpdatesAlive.app_state = appState;
395
- if (appState) {
396
- this.startPublishingLiveUpdates(
397
- this.toClientTopic,
398
- keyMirrors.agentMessageType.app_state,
399
- this.getAppStateMessage
400
- );
401
- }
402
- }
103
+ // eslint-disable-next-line
104
+ private async atomicApplicationUpdate<T extends any[], R extends any>(
105
+ func: (...args: T) => R,
106
+ args: T,
107
+ projectId: string,
108
+ appReleaseHash: string
109
+ ) {
110
+ this.appInstallStatusMgr.update(
111
+ appReleaseHash,
112
+ keyMirrors.appInstallStatus.in_progress
113
+ );
403
114
 
404
- if (appLogs !== undefined) {
405
- if (appLogs.toggle) {
406
- this.startAppLogStream(appLogs.projectId);
407
- } else {
408
- this.appLogStreams.delete(appLogs.projectId);
409
- }
115
+ this.liveUpdatesHandler.update({
116
+ appInstallStatus: { toggle: true, appReleaseHash }
117
+ });
118
+
119
+ // Install the app and models
120
+ try {
121
+ const out: R = await func(...args);
122
+ this.appInstallStatusMgr.update(
123
+ appReleaseHash,
124
+ keyMirrors.appInstallStatus.success
125
+ );
126
+ this.liveUpdatesHandler.update({
127
+ appInstallStatus: { toggle: false, appReleaseHash }
128
+ });
129
+
130
+ // update app config shadow for project
131
+ await this.shadowHandler.publishAppState(projectId);
132
+ return out;
133
+ } catch (e) {
134
+ logger.error(e);
135
+ const message: string = e.message;
136
+
137
+ // uninstall the failed app to put system back in good state
138
+ await uninstallApp({ projectId });
139
+ this.appInstallStatusMgr.update(
140
+ appReleaseHash,
141
+ keyMirrors.appInstallStatus.failure,
142
+ message
143
+ );
144
+ this.liveUpdatesHandler.update({
145
+ appInstallStatus: { toggle: false, appReleaseHash }
146
+ });
147
+
148
+ // delete shadow for project
149
+ // FIXME: Why do we delete the shadow? Doesn't this delete it for all projects?
150
+ this.shadowHandler.deleteProjectShadow();
410
151
  }
411
152
  }
412
153
 
413
- private async publishReportedState(projectId) {
414
- const newAppCfg = await getAppConfig(projectId);
415
- const packet = {
416
- state: {
417
- reported: {
418
- [projectId]: { appConfig: newAppCfg },
419
- },
420
- },
421
- };
422
- this.publishMessage(
423
- `${this.shadowPrefix}projects/update`,
424
- JSON.stringify(packet)
425
- );
426
- }
154
+ private async handleAppConfigUpdates(appConfgUpdates: AppConfigUpdate[]) {
155
+ for (const appConfigUpdate of appConfgUpdates) {
156
+ const { projectId, newAppCfg, updatedModels } = appConfigUpdate;
157
+ if (updatedModels && Object.keys(updatedModels).length) {
158
+ // Publish request for model urls
159
+ this.newAppCfgQueue.push(newAppCfg);
160
+
161
+ logger.debug(
162
+ `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
163
+ updatedModels
164
+ )}`
165
+ );
166
+ this.publisher.publishToCloud({
167
+ messageType: keyMirrors.agentMessageType.signed_urls_request,
168
+ modelsOnlyUrlsRequest: {
169
+ projectId,
170
+ models: updatedModels
171
+ }
172
+ });
173
+ } else {
174
+ // FIXME: do we need to send this up to the cloud?
175
+ // should it be something other than appReleaseHash?
176
+ const appReleaseHash = await AgentConfigFile().getAppVersion({
177
+ projectId
178
+ });
427
179
 
428
- private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
429
- const topic = this.toCloudTopic;
430
- const deviceRequestPacket = this.buildMessagePacket(
431
- this.getClientId(),
432
- topic,
433
- payload
434
- );
435
- this.publishMessage(topic, JSON.stringify({ deviceRequestPacket }));
180
+ await this.atomicApplicationUpdate(
181
+ updateAppCfg,
182
+ [
183
+ {
184
+ projectId,
185
+ appReleaseHash,
186
+ newAppCfg
187
+ }
188
+ ],
189
+ projectId,
190
+ appReleaseHash
191
+ );
192
+ }
193
+ }
436
194
  }
437
195
 
438
196
  // Public Methods
439
197
 
440
- public device = awsIot.device;
441
-
442
198
  constructor() {
443
199
  this.device = awsIot.device({
444
200
  keyPath: getPrivateKeyFilePath(),
@@ -447,25 +203,28 @@ export class DeviceAgentCloudConnection {
447
203
  clientId: this.clientId,
448
204
  host: this.host,
449
205
  port: 8883,
206
+ keepalive: 1 // time before re-connect attempt on dropped connection, default is 400 seconds
450
207
  });
208
+ this.publisher = new Publisher(this.device, this.clientId);
209
+ this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
210
+ this.appInstallStatusMgr = new AppInstallStatusManager();
211
+ this.liveUpdatesHandler = new LiveUpdatesHandler(
212
+ this.publisher,
213
+ this.appInstallStatusMgr
214
+ );
451
215
 
452
- this.device.subscribe(this.toDeviceTopic);
453
- this.device.subscribe(this.shadowTopics.projects.getAccepted);
454
- this.device.subscribe(this.shadowTopics.projects.updateDelta);
216
+ this.subscribe(this.toDeviceTopic);
217
+ this.subscribe(this.shadowHandler.shadowTopics.projects.getAccepted);
218
+ this.subscribe(this.shadowHandler.shadowTopics.projects.updateDelta);
455
219
  }
456
220
 
457
221
  public getClientId(): string {
458
222
  return this.clientId;
459
223
  }
460
224
 
461
- public publishMessage(topic: string, message: string) {
462
- // TODO: topic validation
463
- this.device.publish(topic, message, (err: any) => {});
464
- }
465
-
466
225
  public async handleClientMessage({
467
226
  topic,
468
- message,
227
+ message
469
228
  }: {
470
229
  topic: string;
471
230
  message: ClientMessage;
@@ -489,175 +248,145 @@ export class DeviceAgentCloudConnection {
489
248
  projectId,
490
249
  appReleaseHash,
491
250
  appInstallPayload,
492
- modelsInstallPayload,
251
+ modelsInstallPayload
493
252
  } = payload.appInstallCloudResponse;
494
-
495
- this.initAppInstallStatus({
496
- status: keyMirrors.appInstallStatus.in_progress,
497
- appReleaseHash,
498
- });
499
-
500
- this.startPublishingLiveUpdates(
501
- this.toClientTopic,
502
- keyMirrors.agentMessageType.app_install_status,
503
- this.getAppInstallStatusMessage
253
+ const signedUrlsPayload = {
254
+ appInstallPayload,
255
+ modelsInstallPayload
256
+ };
257
+ await this.atomicApplicationUpdate(
258
+ installApp,
259
+ [{ projectId, appReleaseHash, signedUrlsPayload }],
260
+ projectId,
261
+ appReleaseHash
504
262
  );
505
263
 
506
- // Install the app and models
507
- try {
508
- const signedUrlsPayload = {
509
- appInstallPayload,
510
- modelsInstallPayload,
511
- };
512
- await installApp({
513
- projectId,
514
- appReleaseHash,
515
- signedUrlsPayload,
516
- });
517
- this.updateAppInstallStatus({
518
- status: keyMirrors.appInstallStatus.success,
519
- });
520
-
521
- // update app config shadow for project
522
- await this.publishReportedState(projectId);
523
- } catch (e) {
524
- logger.error(e);
525
- const message: string = e.message;
526
-
527
- // uninstall the failed app to put system back in good state
528
- await uninstallApp({ projectId });
529
- this.updateAppInstallStatus({
530
- status: keyMirrors.appInstallStatus.failure,
531
- message,
532
- });
533
-
534
- // delete shadow for project
535
- this.publishMessage(`${this.shadowPrefix}${projectId}/delete`, "");
536
- }
537
264
  break;
538
265
  }
539
266
  case keyMirrors.clientMessageType.models_install_cloud_response: {
540
267
  const { projectId, newModels } = payload.modelsInstallCloudResponse;
268
+ const appReleaseHash = await AgentConfigFile().getAppVersion({
269
+ projectId
270
+ });
541
271
 
542
- try {
543
- await updateModelsWithPresignedUrls(projectId, newModels);
544
-
545
- await this.publishReportedState(projectId);
546
- } catch (e) {
547
- logger.error(e);
272
+ const newAppCfg = this.newAppCfgQueue.shift();
273
+ if (newAppCfg === undefined) {
274
+ throw new Error(
275
+ 'Unknown error while updating models via application config! No config present for model update.'
276
+ );
548
277
  }
278
+
279
+ await this.atomicApplicationUpdate(
280
+ updateModelsWithPresignedUrls,
281
+ [
282
+ {
283
+ projectId,
284
+ modelInstallPayloads: newModels,
285
+ newAppCfg,
286
+ appReleaseHash
287
+ }
288
+ ],
289
+ projectId,
290
+ appReleaseHash
291
+ );
292
+
549
293
  break;
550
294
  }
551
295
  default:
552
- logger.error(`Invalid Client Message '${JSON.stringify(payload)}'`);
296
+ logger.error(
297
+ `Invalid client message: '${JSON.stringify(
298
+ { topic, message },
299
+ null,
300
+ 2
301
+ )}'`
302
+ );
553
303
  }
554
304
  }
555
305
 
556
- public async handleShadowTopic({
557
- topic,
558
- payload,
559
- }: {
560
- topic: string;
561
- payload: string;
562
- }) {
563
- const shadowName = topic.split("/")[5];
564
- const message = JSON.parse(payload);
565
- if (topic === this.shadowTopics.projects.updateDelta) {
566
- this.handleNamedShadowUpdate({ payload });
567
- } else if (topic === this.shadowTopics.projects.getAccepted) {
568
- if (message.delta) {
569
- this.handleNamedShadowUpdate({
570
- payload: JSON.stringify(message.delta),
571
- });
572
- } else {
573
- logger.info(`No delta updates in shadow ${shadowName}`);
574
- }
575
- }
576
- }
577
- }
306
+ public async setupHandlers() {
307
+ this.device.on('connect', (connack: any) => {
308
+ logger.info('Device Agent has connected to the cloud');
578
309
 
579
- export async function runDeviceAgentCloudInterface() {
580
- switch (existsSync(getCertificateFilePath())) {
581
- case true:
582
- {
583
- const deviceAgent = new DeviceAgentCloudConnection();
584
-
585
- deviceAgent.device.on("connect", function (connack: any) {
586
- logger.info("Device Agent has connected to the cloud");
587
-
588
- // Get shadow updates
589
- deviceAgent.publishMessage(
590
- `${deviceAgent.getShadowPrefix()}projects/get`,
591
- ""
592
- );
593
- });
310
+ // Get shadow updates
311
+ this.shadowHandler.getShadowUpdates();
312
+ });
594
313
 
595
- deviceAgent.device.on("disconnect", function () {
596
- logger.info("Device Agent has been disconnected from the cloud");
597
- });
314
+ this.device.on('disconnect', () => {
315
+ logger.warn('Device Agent has been disconnected from the cloud');
316
+ });
598
317
 
599
- deviceAgent.device.on(
600
- "message",
601
- function (topic: string, payload: string) {
602
- console.log(topic);
603
- console.log(JSON.parse(payload));
604
- try {
605
- const jsonPacket = JSON.parse(payload);
606
- if (jsonPacket.hasOwnProperty("state")) {
607
- deviceAgent.handleShadowTopic({
608
- topic,
609
- payload: JSON.stringify(jsonPacket.state),
610
- });
611
- } else {
612
- const valid = validateClientMessage(jsonPacket);
613
- if (!valid) {
614
- console.error(JSON.stringify(validateClientMessage.errors));
615
- } else {
616
- deviceAgent.handleClientMessage({
617
- topic,
618
- message: jsonPacket,
619
- });
620
- }
621
- }
622
- } catch (error) {
623
- logger.error(error);
624
- }
625
- }
318
+ this.device.on('reconnect', () => {
319
+ logger.info(
320
+ `Device Agent attempting to re-connect ${new Date().toLocaleString()}`
321
+ );
322
+ });
323
+
324
+ this.device.on('message', async (topic: string, payload: string) => {
325
+ try {
326
+ const jsonPacket = JSON.parse(payload);
327
+ logger.debug(
328
+ `Received message: ${JSON.stringify({ topic, jsonPacket }, null, 2)}`
626
329
  );
627
330
 
628
- deviceAgent.device.on("packetsend", (packet: any) => {
629
- console.log({ packet: packet });
630
- });
631
- }
331
+ if (Object.prototype.hasOwnProperty.call(jsonPacket, 'state')) {
332
+ if (jsonPacket.clientToken === this.getClientId()) {
333
+ logger.debug(
334
+ `Ignoring message sent from self: ${JSON.stringify(
335
+ { topic, jsonPacket },
336
+ null,
337
+ 2
338
+ )}`
339
+ );
340
+ return;
341
+ }
632
342
 
633
- break;
634
- case false: {
635
- //set timer
636
- const clientId = getDeviceId();
637
- const bootstrapConfig = {
638
- keyPath: BOOTSTRAP_DEVICE_PRIVATE_KEY_FILE_PATH,
639
- certPath: BOOTSTRAP_DEVICE_CERTIFICATE_FILE_PATH,
640
- caPath: AWS_ROOT_CERTIFICATE_FILE_PATH,
641
- clientId,
642
- host: getIoTCoreEndpointUrl(),
643
- };
644
-
645
- const bootstrapAgent = new BootstrapAgent(bootstrapConfig);
646
- bootstrapAgent.subscribeToAllTopics();
647
-
648
- bootstrapAgent.publishMessage("$aws/certificates/create/json", "");
649
-
650
- bootstrapAgent.device.on("connect", () => {
651
- console.log("Your device is being provisioned");
652
- });
343
+ const appConfigUpdates = await this.shadowHandler.handleShadowTopic({
344
+ topic,
345
+ payload: jsonPacket.state
346
+ });
347
+ await this.handleAppConfigUpdates(appConfigUpdates);
348
+ } else {
349
+ const valid = validateClientMessage(jsonPacket);
350
+ if (!valid) {
351
+ logger.error(
352
+ `Error validating message: ${JSON.stringify(
353
+ validateClientMessage.errors,
354
+ null,
355
+ 2
356
+ )}`
357
+ );
358
+ } else {
359
+ this.handleClientMessage({
360
+ topic,
361
+ message: jsonPacket
362
+ });
363
+ }
364
+ }
365
+ } catch (error) {
366
+ logger.error(`Error parsing message: ${error}`);
367
+ }
368
+ });
653
369
 
654
- bootstrapAgent.device.on("message", (topic: string, payload: string) => {
655
- bootstrapAgent.handleAwsCertificateTopics(topic, payload);
656
- });
370
+ this.device.on('offline', () => {
371
+ logger.warn(`Device Agent is offline ${new Date().toLocaleString()}`);
372
+ });
373
+ }
374
+ }
657
375
 
658
- bootstrapAgent.device.on("packetsend", (packet: any) => {
659
- console.log({ packet: packet.subscriptions });
660
- });
661
- }
376
+ export async function runDeviceAgentCloudInterface() {
377
+ // FIXME: Check for KeyPath as well
378
+ if (existsSync(getCertificateFilePath())) {
379
+ const deviceAgent = new DeviceAgentCloudConnection();
380
+ await deviceAgent.setupHandlers();
381
+ } else if (existsSync(BOOTSTRAP_DEVICE_PRIVATE_KEY_FILE_PATH())) {
382
+ bootstrapProvision();
383
+ } else if (existsSync(BOOTSTRAP_CERTIFICATES_DIR_PATH())) {
384
+ throw new Error(
385
+ "Device has not been created using 'aai-agent device init' or there has been an issue with device initialization"
386
+ );
387
+ } else {
388
+ throw new Error(
389
+ "Set device agent to local mode and retry the 'aai-agent device init' command"
390
+ );
662
391
  }
663
392
  }