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