@alwaysai/device-agent 2.0.0 → 2.0.2-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/lib/application-control/environment-variables.d.ts +4 -0
  2. package/lib/application-control/environment-variables.d.ts.map +1 -1
  3. package/lib/application-control/environment-variables.js +17 -13
  4. package/lib/application-control/environment-variables.js.map +1 -1
  5. package/lib/application-control/install.d.ts +4 -1
  6. package/lib/application-control/install.d.ts.map +1 -1
  7. package/lib/application-control/install.js +16 -1
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/application-control/utils.d.ts.map +1 -1
  10. package/lib/application-control/utils.js +13 -0
  11. package/lib/application-control/utils.js.map +1 -1
  12. package/lib/cloud-connection/base-message-handler.d.ts +27 -0
  13. package/lib/cloud-connection/base-message-handler.d.ts.map +1 -0
  14. package/lib/cloud-connection/base-message-handler.js +72 -0
  15. package/lib/cloud-connection/base-message-handler.js.map +1 -0
  16. package/lib/cloud-connection/connection-manager.d.ts +20 -0
  17. package/lib/cloud-connection/connection-manager.d.ts.map +1 -0
  18. package/lib/cloud-connection/connection-manager.js +164 -0
  19. package/lib/cloud-connection/connection-manager.js.map +1 -0
  20. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +7 -23
  21. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  22. package/lib/cloud-connection/device-agent-cloud-connection.js +49 -517
  23. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  24. package/lib/cloud-connection/device-agent-message-handler.d.ts +22 -0
  25. package/lib/cloud-connection/device-agent-message-handler.d.ts.map +1 -0
  26. package/lib/cloud-connection/device-agent-message-handler.js +357 -0
  27. package/lib/cloud-connection/device-agent-message-handler.js.map +1 -0
  28. package/lib/cloud-connection/live-updates-handler.d.ts +1 -0
  29. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  30. package/lib/cloud-connection/live-updates-handler.js +13 -10
  31. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  32. package/lib/cloud-connection/message-dispatcher.d.ts +10 -0
  33. package/lib/cloud-connection/message-dispatcher.d.ts.map +1 -0
  34. package/lib/cloud-connection/message-dispatcher.js +27 -0
  35. package/lib/cloud-connection/message-dispatcher.js.map +1 -0
  36. package/lib/cloud-connection/publisher.d.ts +3 -2
  37. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  38. package/lib/cloud-connection/publisher.js +8 -4
  39. package/lib/cloud-connection/publisher.js.map +1 -1
  40. package/lib/cloud-connection/shadow-handler.d.ts +7 -0
  41. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  42. package/lib/cloud-connection/shadow-handler.js +77 -1
  43. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  44. package/lib/cloud-connection/shadow-handler.test.js +5 -2
  45. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  46. package/lib/cloud-connection/transaction-manager.d.ts +9 -4
  47. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  48. package/lib/cloud-connection/transaction-manager.js +22 -11
  49. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  50. package/lib/cloud-connection/transaction-manager.test.js +43 -14
  51. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  52. package/lib/jobs/job-handler.d.ts +23 -0
  53. package/lib/jobs/job-handler.d.ts.map +1 -0
  54. package/lib/jobs/job-handler.js +131 -0
  55. package/lib/jobs/job-handler.js.map +1 -0
  56. package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts +8 -0
  57. package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts.map +1 -0
  58. package/lib/secure-tunneling/secure-tunnel-message-handler.js +42 -0
  59. package/lib/secure-tunneling/secure-tunnel-message-handler.js.map +1 -0
  60. package/lib/subcommands/app/version.d.ts +2 -0
  61. package/lib/subcommands/app/version.d.ts.map +1 -1
  62. package/lib/subcommands/app/version.js +14 -2
  63. package/lib/subcommands/app/version.js.map +1 -1
  64. package/package.json +2 -2
  65. package/src/application-control/environment-variables.ts +31 -21
  66. package/src/application-control/install.ts +24 -3
  67. package/src/application-control/utils.ts +13 -0
  68. package/src/cloud-connection/base-message-handler.ts +118 -0
  69. package/src/cloud-connection/connection-manager.ts +196 -0
  70. package/src/cloud-connection/device-agent-cloud-connection.ts +104 -817
  71. package/src/cloud-connection/device-agent-message-handler.ts +642 -0
  72. package/src/cloud-connection/live-updates-handler.ts +26 -18
  73. package/src/cloud-connection/message-dispatcher.ts +33 -0
  74. package/src/cloud-connection/publisher.ts +28 -23
  75. package/src/cloud-connection/shadow-handler.test.ts +6 -2
  76. package/src/cloud-connection/shadow-handler.ts +129 -1
  77. package/src/cloud-connection/transaction-manager.test.ts +55 -24
  78. package/src/cloud-connection/transaction-manager.ts +42 -31
  79. package/src/jobs/job-handler.ts +146 -0
  80. package/src/secure-tunneling/secure-tunnel-message-handler.ts +56 -0
  81. package/src/subcommands/app/version.ts +20 -2
@@ -1,878 +1,165 @@
1
- // eslint-disable-next-line
2
- const awsIot = require('aws-iot-device-sdk');
3
1
  import {
4
- AppInstallResponsePayload,
5
- AppStateControlPayload,
6
- AppVersionControlInstallPayload,
7
- AppVersionControlUninstallPayload,
8
- DeviceActionPayload,
9
- ModelsInstallResponsePayload,
10
- SignedUrlsRequestPayload,
11
- ToCloudMessage,
12
- ToDeviceAgentMessage,
13
- buildAppLogsMessage,
14
- buildAppStateMessage,
15
- buildDeviceStatsMessage,
16
- buildSignedUrlsRequestMessage,
17
2
  buildToClientStatusResponseMessage,
18
3
  getToDeviceTopic,
19
- getUpdateDeltaStateFromMessage,
20
- keyMirrors,
21
- validateSecureTunnelShadowUpdate,
22
- validateToDeviceAgentMessage
4
+ keyMirrors
23
5
  } from '@alwaysai/device-agent-schemas';
24
- import {
25
- DEVICE_CERTIFICATE_FILE_PATH,
26
- DEVICE_PRIVATE_KEY_FILE_PATH
27
- } from 'alwaysai/lib/infrastructure';
28
- import { stringifyError } from 'alwaysai/lib/util';
29
- import { exec } from 'child_process';
30
6
  import { existsSync } from 'fs';
31
- import { promisify } from 'util';
32
- import {
33
- getAppLogs,
34
- installApp,
35
- restartApp,
36
- startApp,
37
- stopApp,
38
- uninstallApp,
39
- updateAppCfg,
40
- updateModelsWithPresignedUrls
41
- } from '../application-control';
42
- import { createAppBackup, rollbackApp } from '../application-control/backup';
43
- import { pruneModels } from '../application-control/models';
44
- import { reboot } from '../device-control/device-control';
45
7
  import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
46
- import { AgentConfigFile } from '../infrastructure/agent-config';
47
8
  import { getBootstrapPrivateKeyFilePath } from '../infrastructure/device-certificate';
48
9
  import { migrateFromLegacyCertsAndTokens } from '../infrastructure/legacy-migration/legacy-migration';
49
10
  import { requiredConfigFilesPresentAndValid } from '../infrastructure/required-config-checks';
50
11
  import { getIoTCoreEndpointUrl } from '../infrastructure/urls';
51
12
  import { SecureTunnelHandlerSingleton } from '../secure-tunneling/secure-tunneling';
52
- import AaiError from '../util/aai-error';
13
+ import { SecureTunnelMessageHandler } from '../secure-tunneling/secure-tunnel-message-handler';
53
14
  import { getDeviceAgentVersion } from '../util/check-for-updates';
54
- import { AWS_ROOT_CERTIFICATE_FILE_PATH } from '../util/directories';
55
15
  import { getDeviceUuid } from '../util/get-device-id';
56
16
  import { logger } from '../util/logger';
57
17
  import sleep from '../util/sleep';
58
18
  import { bootstrapProvision } from './bootstrap-provision';
59
19
  import { LiveUpdatesHandler } from './live-updates-handler';
60
- import { getAppStatePayload, getDeviceStatsPayload } from './messages';
61
20
  import { PassthroughHandler } from './passthrough-handler';
62
21
  import { Publisher } from './publisher';
63
- import { ShadowHandler, ShadowUpdate } from './shadow-handler';
22
+ import { ShadowHandler, ProjectShadowMessageHandler } from './shadow-handler';
23
+ import { JobHandler } from '../jobs/job-handler';
64
24
  import { TransactionManager } from './transaction-manager';
65
-
66
- const exec_promise = promisify(exec);
25
+ import { ConnectionManager } from './connection-manager';
26
+ import { DeviceAgentMessageHandler } from './device-agent-message-handler';
27
+ import { HandlerContext } from './base-message-handler';
67
28
 
68
29
  export class DeviceAgentCloudConnection {
30
+ private connectionManager: ConnectionManager;
31
+ private transactionManager: TransactionManager;
32
+ private liveUpdatesHandler: LiveUpdatesHandler;
69
33
  public shadowHandler: ShadowHandler;
70
34
  public publisher: Publisher;
71
- private liveUpdatesHandler: LiveUpdatesHandler;
72
- private txnMgr: TransactionManager;
73
- private device = awsIot.device;
74
35
 
75
- private clientId = getDeviceUuid();
76
- private host = getIoTCoreEndpointUrl();
77
- private port = 8883;
36
+ private readonly clientId = getDeviceUuid();
37
+ private readonly host = getIoTCoreEndpointUrl();
38
+ private readonly port = 8883;
78
39
  private readonly toDeviceTopic = getToDeviceTopic(this.clientId);
79
- private readonly secureTunnelNotifyTopic = `$aws/things/${this.clientId}/tunnels/notify`;
80
40
  private readonly secureTunnelHandler =
81
41
  SecureTunnelHandlerSingleton.getInstance();
82
42
 
83
43
  constructor() {
84
- this.device = awsIot.device({
85
- keyPath: DEVICE_PRIVATE_KEY_FILE_PATH,
86
- certPath: DEVICE_CERTIFICATE_FILE_PATH,
87
- caPath: AWS_ROOT_CERTIFICATE_FILE_PATH,
88
- clientId: this.clientId,
89
- host: this.host,
90
- port: this.port,
91
- keepalive: 10 // time before re-connect attempt on dropped connection, default is 400 seconds
92
- });
93
-
94
- this.publisher = new Publisher(this.device, this.clientId);
95
- this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
96
44
  this.liveUpdatesHandler = new LiveUpdatesHandler();
97
- this.txnMgr = new TransactionManager(
98
- this.publisher,
99
- this.liveUpdatesHandler
45
+
46
+ // Initialize & setup the connection
47
+ this.connectionManager = new ConnectionManager(
48
+ this.clientId,
49
+ this.host,
50
+ this.port
100
51
  );
101
52
 
102
- this.subscribe(this.toDeviceTopic);
103
- this.subscribe(this.secureTunnelNotifyTopic);
104
- for (const topic of this.shadowHandler.projectShadowTopics) {
105
- this.subscribe(topic);
106
- }
107
- this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.updateDelta);
108
- this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted);
53
+ this.publisher = new Publisher(this.connectionManager, this.clientId);
54
+ this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
109
55
 
110
- this.setupHandlers();
111
- }
56
+ this.connectionManager.connect(() => {
57
+ void this.shadowHandler.initShadows();
58
+ this.publisher.publish(JOB_HANDLER_TOPICS.START_NEXT, JSON.stringify({}));
59
+ });
112
60
 
113
- /*=================================================================
114
- Public interface
115
- =================================================================*/
61
+ this.transactionManager = new TransactionManager(this.liveUpdatesHandler);
116
62
 
117
- public getClientId(): string {
118
- return this.clientId;
119
- }
120
-
121
- public isCmdInProgress(projectId: string): boolean {
122
- return this.txnMgr.isOngoingTransactionForProjectID(projectId);
123
- }
63
+ // Construct a HandlerContext used by all the message handlers
64
+ const handlerContext: HandlerContext = {
65
+ clientId: this.clientId,
66
+ txnMgr: this.transactionManager,
67
+ publisher: this.publisher,
68
+ shadowHandler: this.shadowHandler,
69
+ liveUpdatesHandler: this.liveUpdatesHandler,
70
+ secureTunnelHandler: this.secureTunnelHandler
71
+ };
124
72
 
125
- public async handleMessage(topic: string, message: any) {
126
- logger.debug(
127
- `Received message: ${JSON.stringify({ topic, message }, null, 2)}`
73
+ // Instantiate & register message handlers for Project Shadow topics
74
+ const projectShadowMessageHandler = new ProjectShadowMessageHandler(
75
+ handlerContext
128
76
  );
129
- // ProjectShadow messages
130
- if (this.shadowHandler.projectShadowTopics.includes(topic)) {
131
- await this.handleProjectShadowMessage(topic, message);
132
- } else if (topic === this.toDeviceTopic) {
133
- await this.handleDeviceAgentMessage({
77
+ this.shadowHandler.projectShadowTopics.forEach((topic) => {
78
+ this.connectionManager.registerHandler(
134
79
  topic,
135
- message
136
- });
137
- // SecureTunnelNotify messages
138
- } else if (topic === this.secureTunnelNotifyTopic) {
139
- await this.secureTunnelHandler.secureTunnelNotifyHandler(message);
140
- // SecureTunnel messages
141
- } else if (
142
- topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
143
- ) {
144
- await this.handleSecureTunnelMessage(message);
145
- } else if (
146
- topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
147
- ) {
148
- logger.info(`Received secure tunnel deleteAccepted: ${message}`);
149
- await this.secureTunnelHandler.destroy();
150
- } else {
151
- logger.error(`Unexpected topic, ignoring! ${topic}`);
152
- }
153
- }
154
-
155
- public async stop() {
156
- // This method is currently only used by the CLI, and shadow messages can be
157
- // lost since we aren't waiting for responses so sleep for a short time to
158
- // receive them
159
- await sleep(1000);
160
- this.device.end();
161
- }
162
-
163
- /*=================================================================
164
- Private interface
165
- =================================================================*/
166
-
167
- private setupHandlers() {
168
- this.device.on('connect', (connack: any) => {
169
- logger.info('Device Agent has connected to the cloud');
170
- // FIXME: EI-709 Skip this request for now to prevent kicking off another
171
- // shadow update process if IoT Core disconnect occurs during app config update
172
- //this.shadowHandler.getShadowUpdates();
173
- void this.shadowHandler.updateSystemInfoShadow();
174
- });
175
-
176
- this.device.on('disconnect', () => {
177
- logger.warn('Device Agent has been disconnected from the cloud');
178
- });
179
-
180
- this.device.on('reconnect', () => {
181
- logger.info(
182
- `Device Agent attempting to re-connect ${new Date().toLocaleString()}`
80
+ projectShadowMessageHandler
183
81
  );
184
82
  });
185
83
 
186
- this.device.on('error', function (e) {
187
- logger.error(`Error connecting to cloud!\n${stringifyError(e)}`);
188
- });
189
-
190
- this.device.on('message', async (topic: string, payload: string) => {
191
- try {
192
- const jsonPacket = JSON.parse(payload);
193
- await this.handleMessage(topic, jsonPacket);
194
- } catch (e) {
195
- logger.error(`Error parsing message!\n${stringifyError(e)}`);
196
- }
197
- });
198
-
199
- this.device.on('offline', () => {
200
- logger.warn(`Device Agent is offline ${new Date().toLocaleString()}`);
201
- void this.logConnectionInfo();
202
- });
203
- }
204
-
205
- private async logConnectionInfo() {
206
- try {
207
- /**
208
- * We're using the 'netcat' or 'nc' command to test the connection to the IoT Core endpoint.
209
- * This command doesn't always exit (see below), so
210
- * we use timeout to break out of the prompt
211
- * and catch the resulting error/parse the resulting stderr
212
- *
213
- * Sample command for current host and port:
214
- * nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
215
- *
216
- * Sample output when port is not blocked and host is reachable:
217
- * $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443
218
- * Connection to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443 port [tcp/https] succeeded!
219
- *
220
- *
221
- * Sample output when port is blocked (will repeatedly try until ctrl-C out):
222
- * $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
223
- * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
224
- * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
225
- * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
226
- * ^C
227
- *
228
- *
229
- * Sample command/output when the port isn't enable on that host:
230
- * $ nc -zv -w 1 localhost 8883
231
- * nc: connect to localhost port 8883 (tcp) failed: Connection refused
232
- */
233
- await exec_promise(`nc -zv -w 1 ${this.host} ${this.port}`, {
234
- timeout: 2000
235
- });
236
- } catch (err) {
237
- const output = JSON.stringify(err['stderr']);
238
- if (output.indexOf('not known') !== -1) {
239
- logger.warn(
240
- 'Iot Core endpoint appears to be unreachable, internet connection may be unstable or the host may be down.'
241
- );
242
- } else if (output.indexOf('timed out') !== -1) {
243
- logger.warn(
244
- `Internet connection appears fine, however the endpoint was not reachable on the current connection port: ${this.port}\nPlease check if a firewall is in place.`
245
- );
246
- } else if (output.indexOf('refused') !== -1) {
247
- logger.warn(
248
- `The connection was refused, likely ${this.host} is not running a service on ${this.port}.`
249
- );
250
- } else {
251
- logger.warn(
252
- `Output from checking connection to ${this.host} on ${this.port}: ${output}`
253
- );
254
- }
255
- }
256
- }
257
-
258
- private async handleDeviceAgentMessage({
259
- topic,
260
- message
261
- }: {
262
- topic: string;
263
- message: ToDeviceAgentMessage;
264
- }) {
265
- const valid = validateToDeviceAgentMessage(message);
266
- if (!valid) {
267
- logger.error(
268
- `Error validating message: ${JSON.stringify(
269
- { topic, message, errors: validateToDeviceAgentMessage.errors },
270
- null,
271
- 2
272
- )}`
273
- );
274
- // TODO: Send generic error response
275
- return;
276
- }
277
- const txId = message.txId;
278
- const {
279
- app_state_control,
280
- app_version_control,
281
- live_state_updates,
282
- app_install_response,
283
- models_install_response,
284
- status_response,
285
- device_action
286
- } = keyMirrors.toDeviceAgentMessageType;
287
- switch (message.messageType) {
288
- case app_state_control: {
289
- // txId sent from cloud, just need to continue it
290
- const payload = message.payload;
291
- const projectId = payload.projectId;
292
-
293
- try {
294
- await this.txnMgr.runTransactionStep({
295
- func: () => this.handleAppStateControl(message.payload),
296
- projectId,
297
- txId,
298
- start: true,
299
- liveUpdatesPublishFn: async () =>
300
- this.publisher.publishToClient(
301
- buildToClientStatusResponseMessage(
302
- this.clientId,
303
- { status: keyMirrors.statusResponse.in_progress },
304
- txId
305
- ),
306
- logger.silly
307
- ),
308
- stepName: payload.baseCommand
309
- });
310
- } catch (e) {
311
- logger.error(
312
- `Error processing application state control request for ${projectId}!\n${stringifyError(
313
- e
314
- )}`
315
- );
316
- }
317
-
318
- break;
319
- }
320
- case app_version_control: {
321
- // txId sent from cloud, just need to continue it
322
- const payload = message.payload;
323
- const projectId = payload.projectId;
324
- try {
325
- await this.txnMgr.runTransactionStep({
326
- func: () => this.handleAppVersionControl(payload, txId),
327
- projectId,
328
- txId,
329
- start: true,
330
- liveUpdatesPublishFn: async () =>
331
- this.publisher.publishToClient(
332
- buildToClientStatusResponseMessage(
333
- this.clientId,
334
- { status: keyMirrors.statusResponse.in_progress },
335
- txId
336
- ),
337
- logger.silly
338
- ),
339
- stepName: payload.baseCommand
340
- });
341
- } catch (e) {
342
- logger.error(
343
- `Error processing application install request for ${projectId}!\n${stringifyError(
344
- e
345
- )}`
346
- );
347
- }
348
-
349
- break;
350
- }
351
- case live_state_updates: {
352
- const { deviceStats, appState, appLogs } = message.payload;
353
-
354
- if (deviceStats !== undefined) {
355
- if (deviceStats) {
356
- await this.liveUpdatesHandler.enable(
357
- keyMirrors.toClientMessageType.device_stats,
358
- async () =>
359
- this.publisher.publishToClient(
360
- buildDeviceStatsMessage(
361
- this.clientId,
362
- await getDeviceStatsPayload(),
363
- txId
364
- ),
365
- logger.silly
366
- )
367
- );
368
- } else {
369
- this.liveUpdatesHandler.disable(
370
- keyMirrors.toClientMessageType.device_stats
371
- );
372
- }
373
- }
374
-
375
- if (appState !== undefined) {
376
- if (appState) {
377
- await this.liveUpdatesHandler.enable(
378
- keyMirrors.toClientMessageType.app_state,
379
- async () =>
380
- this.publisher.publishToClient(
381
- buildAppStateMessage(
382
- this.clientId,
383
- await getAppStatePayload(),
384
- txId
385
- ),
386
- logger.silly
387
- )
388
- );
389
- } else {
390
- this.liveUpdatesHandler.disable(
391
- keyMirrors.toClientMessageType.app_state
392
- );
393
- }
394
- }
395
-
396
- if (appLogs !== undefined) {
397
- if (appLogs.toggle) {
398
- await this.liveUpdatesHandler.startStream(
399
- appLogs.projectId,
400
- async () =>
401
- await getAppLogs({
402
- projectId: appLogs.projectId,
403
- args: ['--tail', '100', '--no-log-prefix']
404
- }),
405
- async (logChunk: string) =>
406
- this.publisher.publishToClient(
407
- buildAppLogsMessage(
408
- this.clientId,
409
- {
410
- projectId: appLogs.projectId,
411
- logChunk
412
- },
413
- txId
414
- ),
415
- logger.silly
416
- )
417
- );
418
- } else {
419
- this.liveUpdatesHandler.stopStream(appLogs.projectId);
420
- }
421
- }
422
- break;
423
- }
424
- case app_install_response: {
425
- const payload = message.payload;
426
- const { projectId } = payload.appInstallResponse;
427
- if (txId !== this.txnMgr.getTransactionFromProject(projectId)) {
428
- throw new Error(
429
- `App install response received a message for a transaction ID ${txId} that is not currently underway (${this.txnMgr.getTransactionFromProject(
430
- projectId
431
- )})!`
84
+ // Instantiate & register message handlers for to-device and secureTunnel topics
85
+ this.connectionManager.registerHandler(
86
+ this.toDeviceTopic,
87
+ new DeviceAgentMessageHandler(
88
+ handlerContext,
89
+ (txId: string, errorMsg: string) => {
90
+ const msg = buildToClientStatusResponseMessage(
91
+ this.publisher.getClientId(),
92
+ {
93
+ status: keyMirrors.statusResponse.failure,
94
+ message: errorMsg
95
+ },
96
+ txId
432
97
  );
433
- }
434
- await this.txnMgr.runTransactionStep({
435
- func: () => this.handleAppInstallCloudResponsePayload(payload),
436
- projectId,
437
- txId,
438
- start: false,
439
- stepName: message.messageType
440
- });
441
- break;
442
- }
443
- case models_install_response: {
444
- // This message doesn't have appReleaseHash in it's payload, but
445
- // atomicCmd should be able to read it from the installed app
446
- const payload = message.payload;
447
- const { projectId } = payload.modelsInstallResponse;
448
- if (txId !== this.txnMgr.getTransactionFromProject(projectId)) {
449
- throw new Error(
450
- `Model install response received a message for a transaction ID ${txId} that is not currently underway (${this.txnMgr.getTransactionFromProject(
451
- projectId
452
- )})!`
98
+ this.publisher.publishToClient(msg);
99
+ },
100
+ (txId: string) => {
101
+ const msg = buildToClientStatusResponseMessage(
102
+ this.publisher.getClientId(),
103
+ { status: keyMirrors.statusResponse.success },
104
+ txId
453
105
  );
106
+ this.publisher.publishToClient(msg);
454
107
  }
455
- await this.txnMgr.runTransactionStep({
456
- func: () =>
457
- this.handleModelsInstallCloudResponsePayload(payload, txId),
458
- projectId,
459
- txId,
460
- start: false,
461
- stepName: message.messageType
462
- });
463
- break;
464
- }
465
- case status_response: {
466
- const { failure } = keyMirrors.statusResponse;
467
- if (message.payload.status === failure) {
468
- this.txnMgr.completeTransaction(
469
- txId,
470
- buildToClientStatusResponseMessage(
471
- this.clientId,
472
- {
473
- status: keyMirrors.statusResponse.failure,
474
- message: message.payload.message
475
- },
476
- txId
477
- )
478
- );
479
- }
480
- break;
481
- }
482
- case device_action: {
483
- try {
484
- this.publisher.publishToClient(
485
- buildToClientStatusResponseMessage(
486
- this.clientId,
487
- {
488
- status: keyMirrors.statusResponse.in_progress
489
- },
490
- txId
491
- )
492
- );
493
-
494
- await this.handleDeviceAction(message.payload);
495
-
496
- this.publisher.publishToClient(
497
- buildToClientStatusResponseMessage(
498
- this.clientId,
499
- {
500
- status: keyMirrors.statusResponse.success
501
- },
502
- txId
503
- )
504
- );
505
- } catch (e) {
506
- logger.error(
507
- `There was a problem performing device action '${
508
- message.payload.action
509
- }'!\n${stringifyError(e)}`
510
- );
511
- this.publisher.publishToClient(
512
- buildToClientStatusResponseMessage(
513
- this.clientId,
514
- {
515
- status: keyMirrors.statusResponse.failure,
516
- message: e.message
517
- },
518
- txId
519
- )
520
- );
521
- }
522
- break;
523
- }
524
- default:
525
- logger.error(
526
- `Invalid client message: '${JSON.stringify(
527
- { topic, message, txId },
528
- null,
529
- 2
530
- )}'`
531
- );
532
- }
533
- }
534
-
535
- private handleAppStateControl = async (
536
- payload: AppStateControlPayload
537
- ): Promise<boolean> => {
538
- const { baseCommand, projectId } = payload;
539
- switch (baseCommand) {
540
- case keyMirrors.appStateControl.start:
541
- await startApp({ projectId });
542
- break;
543
- case keyMirrors.appStateControl.stop:
544
- await stopApp({ projectId });
545
- break;
546
- case keyMirrors.appStateControl.restart:
547
- await restartApp({ projectId });
548
- break;
549
- }
550
- return true;
551
- };
552
-
553
- private handleAppVersionControl = async (
554
- payload:
555
- | AppVersionControlInstallPayload
556
- | AppVersionControlUninstallPayload,
557
- txId: string
558
- ): Promise<boolean> => {
559
- switch (payload.baseCommand) {
560
- case keyMirrors.appVersionControl.install: {
561
- const { projectId, appReleaseHash } = payload;
562
-
563
- const signedUrlsRequestPayload: SignedUrlsRequestPayload = {
564
- signedUrlsRequest: {
565
- projectId,
566
- appReleaseHash
567
- }
568
- };
569
- const message = buildSignedUrlsRequestMessage(
570
- this.clientId,
571
- signedUrlsRequestPayload,
572
- txId
573
- );
574
- await this.publishCloudRequest(message);
575
- return false;
576
- }
577
- case keyMirrors.appVersionControl.uninstall: {
578
- const { projectId } = payload;
579
- await this.atomicApplicationUninstall(projectId);
580
- return true;
581
- }
582
- default:
583
- logger.warn(
584
- `Ignore App Version Control packet: ${JSON.stringify(
585
- payload,
586
- null,
587
- 2
588
- )}`
589
- );
590
- return true;
591
- }
592
- };
593
-
594
- private handleAppInstallCloudResponsePayload = async (
595
- payload: AppInstallResponsePayload
596
- ): Promise<boolean> => {
597
- const {
598
- projectId,
599
- appReleaseHash,
600
- appInstallPayload,
601
- modelsInstallPayload
602
- } = payload.appInstallResponse;
603
- const signedUrlsPayload = {
604
- appInstallPayload,
605
- modelsInstallPayload
606
- };
607
- await this.atomicApplicationUpdate(async () => {
608
- this.shadowHandler.clearProjectShadow(projectId);
609
- await installApp({ projectId, appReleaseHash, signedUrlsPayload });
610
- }, projectId);
611
- return true;
612
- };
613
-
614
- private handleModelsInstallCloudResponsePayload = async (
615
- payload: ModelsInstallResponsePayload,
616
- txId: string
617
- ): Promise<boolean> => {
618
- const projectId = payload.modelsInstallResponse.projectId;
108
+ )
109
+ );
619
110
 
620
- const update = this.txnMgr.getAppCfgUpdateFromTxID(txId);
621
- if (update === undefined) {
622
- throw new Error(
623
- 'Unknown error while updating models via application config! No config present for model update.'
624
- );
625
- }
626
- const { appCfgUpdate, envVarUpdate } = update;
627
- if (appCfgUpdate) {
628
- await this.atomicApplicationUpdate(
629
- async () =>
630
- await updateModelsWithPresignedUrls({
631
- projectId,
632
- modelInstallPayloads: payload.modelsInstallResponse.newModels,
633
- newAppCfg: appCfgUpdate.newAppCfg
634
- }),
635
- projectId
636
- );
637
- }
111
+ const secureTunnelMessageHandler = new SecureTunnelMessageHandler(
112
+ handlerContext
113
+ );
114
+ this.connectionManager.registerHandler(
115
+ secureTunnelMessageHandler.getNotifyTopic(),
116
+ secureTunnelMessageHandler
117
+ );
638
118
 
639
- if (envVarUpdate) {
640
- await this.atomicApplicationUpdate(
641
- async () =>
642
- await this.shadowHandler.updateProjectEnvVars({
643
- projectId,
644
- envVars: envVarUpdate.envVars
645
- }),
646
- projectId,
647
- true
648
- );
649
- }
119
+ this.connectionManager.registerHandler(
120
+ this.shadowHandler.shadowTopics.secureTunnel.updateDelta,
121
+ secureTunnelMessageHandler
122
+ );
123
+ this.connectionManager.registerHandler(
124
+ this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted,
125
+ secureTunnelMessageHandler
126
+ );
650
127
 
651
- return true;
652
- };
128
+ const jobHandler = new JobHandler(handlerContext);
653
129
 
654
- private async handleDeviceAction(payload: DeviceActionPayload) {
655
- const { system_restart } = keyMirrors.deviceAction;
656
- switch (payload.action) {
657
- case system_restart: {
658
- await reboot();
659
- break;
660
- }
661
- default: {
662
- logger.info(
663
- `Unrecognized device action requested: '${payload.action}'.`
664
- );
665
- }
666
- }
667
- }
130
+ const JOB_HANDLER_TOPICS = jobHandler.getJobTopic();
668
131
 
669
- private async publishCloudRequest(message: ToCloudMessage) {
670
- this.publisher.publishToCloud(message);
671
- }
132
+ this.connectionManager.registerHandler(
133
+ JOB_HANDLER_TOPICS.NOTIFY_NEXT,
134
+ jobHandler
135
+ );
672
136
 
673
- private subscribe(topic: string) {
674
- logger.info(`Subscribing to ${topic}`);
675
- this.device.subscribe(topic);
137
+ this.connectionManager.registerHandler(
138
+ JOB_HANDLER_TOPICS.START_NEXT_ACCEPTED,
139
+ jobHandler
140
+ );
676
141
  }
677
142
 
678
- private async atomicApplicationUninstall(projectId: string) {
679
- try {
680
- await uninstallApp({ projectId });
681
- this.shadowHandler.clearProjectShadow(projectId);
682
- } catch (e) {
683
- logger.error(`Failed to uninstall ${projectId}!\n${stringifyError(e)}`);
684
- throw e;
685
- }
143
+ /*=================================================================
144
+ Public interface
145
+ =================================================================*/
146
+ public getClientId(): string {
147
+ return this.clientId;
686
148
  }
687
149
 
688
- // eslint-disable-next-line
689
- private async atomicApplicationUpdate<F extends () => any>(
690
- func: F,
691
- projectId: string,
692
- skipUpdateShadow?: boolean
693
- ): Promise<ReturnType<F>> {
694
- if (await AgentConfigFile().isAppPresent({ projectId })) {
695
- // Reject the application update if app is present but not ready
696
- if (!(await AgentConfigFile().isAppReady({ projectId }))) {
697
- throw new Error('Application already has installation in progress!');
698
- }
699
-
700
- // Try to create a backup, so that there is one available if something goes wrong in the next try:catch.
701
- try {
702
- await createAppBackup({ projectId });
703
- } catch (e) {
704
- logger.error(
705
- `Could not create a backup for the project: ${projectId}!\n${stringifyError(
706
- e
707
- )}`
708
- );
709
- }
710
- }
711
-
712
- try {
713
- const out: ReturnType<F> = await func();
714
- if (!skipUpdateShadow)
715
- await this.shadowHandler.updateProjectShadow(projectId);
716
- return out;
717
- } catch (errorAppUpdate) {
718
- logger.error(
719
- `Failed to update ${projectId}!\n${stringifyError(errorAppUpdate)}`
720
- );
721
- // If something goes wrong, first try to rollback
722
- try {
723
- await rollbackApp({ projectId });
724
- } catch (errorRollbackApp) {
725
- logger.error(
726
- `Application rollback failed for ${projectId}!\n${stringifyError(
727
- errorRollbackApp
728
- )}`
729
- );
730
- // and if that fails, uninstall the app as a last resort.
731
- try {
732
- await this.atomicApplicationUninstall(projectId);
733
- } catch {
734
- // atomicApplicationUninstall logs failure, so there's nothing to do here.
735
- }
736
- throw new AaiError(
737
- 'Application update and rollback failed, uninstalled the application!',
738
- { cause: errorAppUpdate }
739
- );
740
- }
741
- throw new Error(
742
- 'Application update failed, rolled the application back!',
743
- { cause: errorAppUpdate }
744
- );
745
- }
150
+ public isCmdInProgress(projectId: string): boolean {
151
+ return this.transactionManager.isOngoingTransactionForProjectID(projectId);
746
152
  }
747
153
 
748
- private handleProjectShadowConfigUpdate = async (
749
- update: ShadowUpdate,
750
- txId: string
751
- ): Promise<boolean> => {
752
- const { projectId, appCfgUpdate, envVarUpdate } = update;
753
-
754
- if (
755
- appCfgUpdate?.updatedModels &&
756
- Object.keys(appCfgUpdate.updatedModels).length
757
- ) {
758
- // When there are model updates request signed URLs and wait to apply config changes
759
- const { updatedModels } = appCfgUpdate;
760
-
761
- logger.debug(
762
- `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
763
- updatedModels
764
- )}`
765
- );
766
- const modelsOnlyUrlsRequestPayload: SignedUrlsRequestPayload = {
767
- modelsOnlyUrlsRequest: {
768
- projectId,
769
- models: updatedModels
770
- }
771
- };
772
- const message = buildSignedUrlsRequestMessage(
773
- this.clientId,
774
- modelsOnlyUrlsRequestPayload,
775
- txId
776
- );
777
- this.publisher.publishToCloud(message);
778
-
779
- this.txnMgr.setAppCfgUpdateToTx(txId, update);
780
-
781
- return false;
782
- }
783
-
784
- if (appCfgUpdate) {
785
- await this.atomicApplicationUpdate(async () => {
786
- await pruneModels({
787
- projectId,
788
- appCfg: appCfgUpdate.newAppCfg
789
- });
790
- await updateAppCfg({
791
- projectId,
792
- newAppCfg: appCfgUpdate.newAppCfg
793
- });
794
- }, projectId);
795
- }
796
-
797
- if (envVarUpdate) {
798
- await this.atomicApplicationUpdate(
799
- async () =>
800
- await this.shadowHandler.updateProjectEnvVars({
801
- projectId,
802
- envVars: envVarUpdate.envVars
803
- }),
804
- projectId,
805
- true
806
- );
807
- }
808
- return true;
809
- };
810
-
811
- private async handleProjectShadowMessage(topic: string, message: any) {
812
- const shadowUpdates = await this.shadowHandler.handleProjectShadow({
813
- topic,
814
- payload: message,
815
- clientToken: message.clientToken
816
- });
817
- if (shadowUpdates.length) {
818
- const shadowUpdatePromises: Promise<void>[] = [];
819
- for (const shadowUpdate of shadowUpdates) {
820
- const projectId = shadowUpdate.projectId;
821
- const txId = shadowUpdate.txId;
822
- shadowUpdatePromises.push(
823
- this.txnMgr
824
- .runTransactionStep({
825
- func: () =>
826
- this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
827
- projectId,
828
- txId,
829
- start: true,
830
- liveUpdatesPublishFn: async () =>
831
- this.publisher.publishToClient(
832
- buildToClientStatusResponseMessage(
833
- this.clientId,
834
- { status: keyMirrors.statusResponse.in_progress },
835
- txId
836
- ),
837
- logger.silly
838
- ),
839
- stepName: topic
840
- })
841
- .catch((e) => {
842
- logger.error(
843
- `There was an issue updating project shadow config for ${projectId}!\n${stringifyError(
844
- e
845
- )}`
846
- );
847
- })
848
- );
849
- }
850
-
851
- await Promise.all(shadowUpdatePromises);
852
- }
154
+ public async stop() {
155
+ // This method is currently only used by the CLI, and shadow messages can be
156
+ // lost since we aren't waiting for responses so sleep for a short time to
157
+ // receive them
158
+ await sleep(1000);
159
+ this.connectionManager.disconnect();
853
160
  }
854
-
855
- public async handleSecureTunnelMessage(payload: any): Promise<void> {
856
- logger.info(`Received secure tunnel update: ${JSON.stringify(payload)}`);
857
- const state = getUpdateDeltaStateFromMessage(payload);
858
- if (!state) {
859
- logger.debug(`No state found in message: ${JSON.stringify(payload)}`);
860
- return;
861
- }
862
- const valid = validateSecureTunnelShadowUpdate(state);
863
- if (!valid) {
864
- logger.error(
865
- `Error validating message: ${JSON.stringify(
866
- { payload, errors: validateSecureTunnelShadowUpdate.errors },
867
- null,
868
- 2
869
- )}`
870
- );
871
- return;
872
- }
873
- const secureTunnelUpdate =
874
- await this.secureTunnelHandler.syncShadowToDeviceState(payload);
875
- await this.shadowHandler.updateSecureTunnelShadow(secureTunnelUpdate);
161
+ public async handleMessage(topic: string, message: any) {
162
+ this.connectionManager.dispatch(topic, message);
876
163
  }
877
164
  }
878
165