@alwaysai/device-agent 2.0.0 → 2.0.1

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 (73) 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 +21 -0
  17. package/lib/cloud-connection/connection-manager.d.ts.map +1 -0
  18. package/lib/cloud-connection/connection-manager.js +158 -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/shadow-handler.d.ts +6 -0
  37. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  38. package/lib/cloud-connection/shadow-handler.js +74 -1
  39. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  40. package/lib/cloud-connection/transaction-manager.d.ts +8 -1
  41. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  42. package/lib/cloud-connection/transaction-manager.js +21 -9
  43. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  44. package/lib/cloud-connection/transaction-manager.test.js +43 -2
  45. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  46. package/lib/jobs/job-handler.d.ts +23 -0
  47. package/lib/jobs/job-handler.d.ts.map +1 -0
  48. package/lib/jobs/job-handler.js +131 -0
  49. package/lib/jobs/job-handler.js.map +1 -0
  50. package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts +8 -0
  51. package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts.map +1 -0
  52. package/lib/secure-tunneling/secure-tunnel-message-handler.js +42 -0
  53. package/lib/secure-tunneling/secure-tunnel-message-handler.js.map +1 -0
  54. package/lib/subcommands/app/version.d.ts +2 -0
  55. package/lib/subcommands/app/version.d.ts.map +1 -1
  56. package/lib/subcommands/app/version.js +14 -2
  57. package/lib/subcommands/app/version.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/application-control/environment-variables.ts +31 -21
  60. package/src/application-control/install.ts +24 -3
  61. package/src/application-control/utils.ts +13 -0
  62. package/src/cloud-connection/base-message-handler.ts +118 -0
  63. package/src/cloud-connection/connection-manager.ts +187 -0
  64. package/src/cloud-connection/device-agent-cloud-connection.ts +109 -816
  65. package/src/cloud-connection/device-agent-message-handler.ts +642 -0
  66. package/src/cloud-connection/live-updates-handler.ts +26 -18
  67. package/src/cloud-connection/message-dispatcher.ts +33 -0
  68. package/src/cloud-connection/shadow-handler.ts +125 -1
  69. package/src/cloud-connection/transaction-manager.test.ts +65 -3
  70. package/src/cloud-connection/transaction-manager.ts +41 -27
  71. package/src/jobs/job-handler.ts +146 -0
  72. package/src/secure-tunneling/secure-tunnel-message-handler.ts +56 -0
  73. package/src/subcommands/app/version.ts +20 -2
@@ -0,0 +1,33 @@
1
+ import { logger } from '../util/logger';
2
+
3
+ export interface MessageHandler<T = any> {
4
+ handle(payload: T, topic?: string): void;
5
+ }
6
+
7
+ export class MessageDispatcher<T = any> {
8
+ private handlers: Map<string, MessageHandler<T>> = new Map();
9
+
10
+ public registerHandler(topic: string, handler: MessageHandler): void {
11
+ this.handlers.set(topic, handler);
12
+ }
13
+
14
+ public dispatch(topic: string, payload: T): void {
15
+ const handler = this.handlers.get(topic);
16
+ if (handler) {
17
+ handler.handle(payload, topic);
18
+ } else {
19
+ this.handleUnknownMessage(topic, payload);
20
+ }
21
+ }
22
+
23
+ // Handle unknown or unregistered message topic
24
+ private handleUnknownMessage(topic: string, payload: T): void {
25
+ logger.error(
26
+ `No handler found for topic/type: ${topic} for message ${JSON.stringify(
27
+ payload,
28
+ null,
29
+ 2
30
+ )}`
31
+ );
32
+ }
33
+ }
@@ -3,13 +3,17 @@ import {
3
3
  validateAppConfig
4
4
  } from '@alwaysai/app-configuration-schemas';
5
5
  import {
6
+ buildSignedUrlsRequestMessage,
7
+ buildToClientStatusResponseMessage,
6
8
  buildUpdateShadowMessage,
7
9
  EnvVars,
8
10
  generateTxId,
9
11
  getShadowTopic,
10
12
  getUpdateDeltaStateFromMessage,
13
+ keyMirrors,
11
14
  ProjectShadowUpdate,
12
15
  SecureTunnelShadowUpdate,
16
+ SignedUrlsRequestPayload,
13
17
  validateEnvVarSchemaShadowUpdate,
14
18
  validateProjectShadowUpdate
15
19
  } from '@alwaysai/device-agent-schemas';
@@ -19,11 +23,19 @@ import {
19
23
  ShadowProjectsUpdateAll
20
24
  } from '@alwaysai/device-agent-schemas/lib/shadow-schema';
21
25
  import { stringifyError } from 'alwaysai/lib/util';
22
- import { getAllEnvs, readAppCfgFile, setEnv } from '../application-control';
26
+ import {
27
+ getAllEnvs,
28
+ readAppCfgFile,
29
+ setEnv,
30
+ updateAppCfg
31
+ } from '../application-control';
23
32
  import { getSystemInformation } from '../device-control/device-control';
24
33
  import { logger } from '../util/logger';
25
34
  import { Publisher } from './publisher';
26
35
  import { AppConfigModels, getAppCfgModelsDiff } from './shadow';
36
+ import { pruneModels } from '../application-control/models';
37
+ import { MessageHandler } from './message-dispatcher';
38
+ import { BaseHandler } from './base-message-handler';
27
39
 
28
40
  export type AppConfigUpdate = {
29
41
  newAppCfg: AppConfig;
@@ -440,3 +452,115 @@ export class ShadowHandler {
440
452
  );
441
453
  }
442
454
  }
455
+
456
+ export class ProjectShadowMessageHandler
457
+ extends BaseHandler
458
+ implements MessageHandler<any>
459
+ {
460
+ public async handle(message: any, topic: string): Promise<void> {
461
+ const shadowUpdates = await this.shadowHandler.handleProjectShadow({
462
+ topic,
463
+ payload: message,
464
+ clientToken: message.clientToken
465
+ });
466
+ if (shadowUpdates.length) {
467
+ const shadowUpdatePromises: Promise<void>[] = [];
468
+ for (const shadowUpdate of shadowUpdates) {
469
+ const projectId = shadowUpdate.projectId;
470
+ const txId = shadowUpdate.txId;
471
+ shadowUpdatePromises.push(
472
+ this.txnMgr
473
+ .runTransactionStep({
474
+ func: () =>
475
+ this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
476
+ projectId,
477
+ txId,
478
+ start: true,
479
+ liveUpdatesPublishFn: async () =>
480
+ this.publisher.publishToClient(
481
+ buildToClientStatusResponseMessage(
482
+ this.clientId,
483
+ { status: keyMirrors.statusResponse.in_progress },
484
+ txId
485
+ ),
486
+ logger.silly
487
+ ),
488
+ stepName: topic
489
+ })
490
+ .catch((e) => {
491
+ logger.error(
492
+ `There was an issue updating project shadow config for ${projectId}!\n${stringifyError(
493
+ e
494
+ )}`
495
+ );
496
+ })
497
+ );
498
+ }
499
+
500
+ await Promise.all(shadowUpdatePromises);
501
+ }
502
+ }
503
+
504
+ private handleProjectShadowConfigUpdate = async (
505
+ update: ShadowUpdate,
506
+ txId: string
507
+ ): Promise<boolean> => {
508
+ const { projectId, appCfgUpdate, envVarUpdate } = update;
509
+
510
+ if (
511
+ appCfgUpdate?.updatedModels &&
512
+ Object.keys(appCfgUpdate.updatedModels).length
513
+ ) {
514
+ // When there are model updates request signed URLs and wait to apply config changes
515
+ const { updatedModels } = appCfgUpdate;
516
+
517
+ logger.debug(
518
+ `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
519
+ updatedModels
520
+ )}`
521
+ );
522
+ const modelsOnlyUrlsRequestPayload: SignedUrlsRequestPayload = {
523
+ modelsOnlyUrlsRequest: {
524
+ projectId,
525
+ models: updatedModels
526
+ }
527
+ };
528
+ const message = buildSignedUrlsRequestMessage(
529
+ this.clientId,
530
+ modelsOnlyUrlsRequestPayload,
531
+ txId
532
+ );
533
+ this.publisher.publishToCloud(message);
534
+
535
+ this.txnMgr.setAppCfgUpdateToTx(txId, update);
536
+
537
+ return false;
538
+ }
539
+
540
+ if (appCfgUpdate) {
541
+ await this.atomicApplicationUpdate(async () => {
542
+ await pruneModels({
543
+ projectId,
544
+ appCfg: appCfgUpdate.newAppCfg
545
+ });
546
+ await updateAppCfg({
547
+ projectId,
548
+ newAppCfg: appCfgUpdate.newAppCfg
549
+ });
550
+ }, projectId);
551
+ }
552
+
553
+ if (envVarUpdate) {
554
+ await this.atomicApplicationUpdate(
555
+ async () =>
556
+ await this.shadowHandler.updateProjectEnvVars({
557
+ projectId,
558
+ envVars: envVarUpdate.envVars
559
+ }),
560
+ projectId,
561
+ true
562
+ );
563
+ }
564
+ return true;
565
+ };
566
+ }
@@ -1,13 +1,17 @@
1
1
  import {
2
+ EnvVars,
2
3
  ToClientStatusResponseMessage,
3
4
  generateTxId,
4
- keyMirrors
5
+ keyMirrors,
6
+ buildToClientStatusResponseMessage
5
7
  } from '@alwaysai/device-agent-schemas';
6
8
  import { TransactionManager } from './transaction-manager';
7
9
  import { v4 as uuidv4 } from 'uuid';
8
10
  import { Publisher } from './publisher';
9
11
  import { LiveUpdatesHandler } from './live-updates-handler';
10
12
  import { AppConfigUpdate, ShadowUpdate } from './shadow-handler';
13
+ import { AppContent } from './device-agent-message-handler';
14
+ import { AppConfig } from '@alwaysai/app-configuration-schemas';
11
15
 
12
16
  const mockClient = {
13
17
  publish: jest.fn()
@@ -58,11 +62,22 @@ describe('Test Transaction Manager', () => {
58
62
  test('Start a new transaction which completes in one step', async () => {
59
63
  const txId = generateTxId();
60
64
  const projectId = generateRandomProjectId();
65
+
66
+ const successFn = (txId: string) => {
67
+ const msg = buildToClientStatusResponseMessage(
68
+ clientId,
69
+ { status: keyMirrors.statusResponse.success },
70
+ txId
71
+ );
72
+ publisher.publishToClient(msg);
73
+ };
74
+
61
75
  await txnMgr.runTransactionStep({
62
76
  func: func_complete,
63
77
  projectId,
64
78
  txId,
65
- start: true
79
+ start: true,
80
+ successFn
66
81
  });
67
82
  expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
68
83
  expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
@@ -198,6 +213,19 @@ describe('Test Transaction Manager', () => {
198
213
  test('Handle error in step function', async () => {
199
214
  const txId = generateTxId();
200
215
  const projectId = generateRandomProjectId();
216
+
217
+ const errorFn = (txId: string, errorMsg: string) => {
218
+ const msg = buildToClientStatusResponseMessage(
219
+ clientId,
220
+ {
221
+ status: keyMirrors.statusResponse.failure,
222
+ message: errorMsg
223
+ },
224
+ txId
225
+ );
226
+ publisher.publishToClient(msg);
227
+ };
228
+
201
229
  await txnMgr.runTransactionStep({
202
230
  func: jest.fn().mockImplementation(() => {
203
231
  throw new Error('Test error!');
@@ -205,7 +233,8 @@ describe('Test Transaction Manager', () => {
205
233
  projectId,
206
234
  txId,
207
235
  start: true,
208
- stepName: 'step1'
236
+ stepName: 'step1',
237
+ errorFn
209
238
  });
210
239
  expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
211
240
  expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
@@ -369,4 +398,37 @@ describe('Test Transaction Manager', () => {
369
398
  expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
370
399
  expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(undefined);
371
400
  });
401
+
402
+ test('store appContent', async () => {
403
+ const txId = generateTxId();
404
+ const projectId = generateRandomProjectId();
405
+ const appCfg: AppConfig = {
406
+ models: {},
407
+ scripts: { start: 'python app.py' }
408
+ };
409
+ const envVars: EnvVars = { alwaysai: { TEST: '1' } };
410
+ const cfgUpdate: AppContent = {
411
+ projectId: projectId,
412
+ appCfg: appCfg,
413
+ envVars: envVars
414
+ };
415
+
416
+ await txnMgr.runTransactionStep({
417
+ func: func_incomplete,
418
+ projectId,
419
+ txId,
420
+ start: true
421
+ });
422
+
423
+ expect(txnMgr.getAppContentFromTxId(txId)).toEqual(undefined);
424
+
425
+ txnMgr.setAppContentToTx(txId, cfgUpdate);
426
+
427
+ expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
428
+ expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
429
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
430
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
431
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
432
+ expect(txnMgr.getAppContentFromTxId(txId)).toEqual(cfgUpdate);
433
+ });
372
434
  });
@@ -9,6 +9,7 @@ import { logger } from '../util/logger';
9
9
  import { keyMirror, stringifyError } from 'alwaysai/lib/util';
10
10
  import { CodedError } from '@carnesen/coded-error';
11
11
  import { ShadowUpdate } from './shadow-handler';
12
+ import { AppContent } from './device-agent-message-handler';
12
13
 
13
14
  interface TransactionDetails {
14
15
  txId: string;
@@ -18,14 +19,18 @@ interface TransactionDetails {
18
19
  update?: string;
19
20
  stop?: string;
20
21
  appCfgUpdate?: ShadowUpdate;
22
+ appContent?: AppContent;
21
23
  }
22
24
 
25
+ export type ErrorFunction = (txid: string, message: string) => void;
26
+
27
+ export type SuccessFunction = (txId: string) => void;
28
+
23
29
  export class TransactionManager {
24
30
  private detailsByTx: Record<string, TransactionDetails> = {};
25
31
  private detailsByProject: Record<string, TransactionDetails> = {};
26
- private liveUpdatesHandler: LiveUpdatesHandler;
32
+ private readonly liveUpdatesHandler: LiveUpdatesHandler;
27
33
  private publisher: Publisher;
28
-
29
34
  private async startTransaction(
30
35
  txId: string,
31
36
  projectId: string,
@@ -111,9 +116,19 @@ export class TransactionManager {
111
116
  start: boolean;
112
117
  liveUpdatesPublishFn?: () => Promise<void>;
113
118
  stepName?: string;
119
+ errorFn?: ErrorFunction;
120
+ successFn?: SuccessFunction;
114
121
  }) {
115
- const { func, projectId, txId, start, liveUpdatesPublishFn, stepName } =
116
- props;
122
+ const {
123
+ func,
124
+ projectId,
125
+ txId,
126
+ start,
127
+ liveUpdatesPublishFn,
128
+ stepName,
129
+ errorFn,
130
+ successFn
131
+ } = props;
117
132
  if (start) {
118
133
  await this.startTransaction(
119
134
  txId,
@@ -127,31 +142,20 @@ export class TransactionManager {
127
142
  try {
128
143
  const completed = await func();
129
144
  if (completed) {
130
- this.completeTransaction(
131
- txId,
132
- buildToClientStatusResponseMessage(
133
- this.publisher.getClientId(),
134
- { status: keyMirrors.statusResponse.success },
135
- txId
136
- )
137
- );
145
+ this.completeTransaction(txId);
146
+ if (successFn) {
147
+ successFn(txId);
148
+ }
138
149
  }
139
150
  } catch (e) {
140
151
  logger.error(
141
152
  `Failed to execute cmd for ${projectId}!\n${stringifyError(e)}`
142
153
  );
143
154
 
144
- this.completeTransaction(
145
- txId,
146
- buildToClientStatusResponseMessage(
147
- this.publisher.getClientId(),
148
- {
149
- status: keyMirrors.statusResponse.failure,
150
- message: e.message
151
- },
152
- txId
153
- )
154
- );
155
+ this.completeTransaction(txId);
156
+ if (errorFn) {
157
+ errorFn(txId, e.message);
158
+ }
155
159
  }
156
160
  }
157
161
 
@@ -181,6 +185,10 @@ export class TransactionManager {
181
185
  return this.detailsByTx[txId]?.appCfgUpdate;
182
186
  }
183
187
 
188
+ public getAppContentFromTxId(txId: string): AppContent | undefined {
189
+ return this.detailsByTx[txId]?.appContent;
190
+ }
191
+
184
192
  public setAppCfgUpdateToTx(txId: string, appCfgUpdate: ShadowUpdate) {
185
193
  if (this.isOngoingTransaction(txId)) {
186
194
  this.detailsByTx[txId].appCfgUpdate = appCfgUpdate;
@@ -190,6 +198,16 @@ export class TransactionManager {
190
198
  );
191
199
  }
192
200
 
201
+ public setAppContentToTx(txId: string, appContent: AppContent) {
202
+ if (this.isOngoingTransaction(txId)) {
203
+ logger.debug(`${txId}: Setting AppContent:${JSON.stringify(appContent)}`);
204
+ this.detailsByTx[txId].appContent = appContent;
205
+ } else
206
+ throw new Error(
207
+ `Could not set appCfgUpdate, the transaction ${txId} does not exist.`
208
+ );
209
+ }
210
+
193
211
  public completeTransaction(
194
212
  txId: string,
195
213
  messageToPublish?: ToClientStatusResponseMessage
@@ -212,9 +230,5 @@ export class TransactionManager {
212
230
  keyMirrors.toClientMessageType.status_response,
213
231
  txId
214
232
  );
215
-
216
- if (messageToPublish) {
217
- this.publisher.publishToClient(messageToPublish);
218
- }
219
233
  }
220
234
  }
@@ -0,0 +1,146 @@
1
+ import { MessageHandler } from '../cloud-connection/message-dispatcher';
2
+ import { logger } from '../util/logger';
3
+ import {
4
+ BaseHandler,
5
+ HandlerContext
6
+ } from '../cloud-connection/base-message-handler';
7
+ import { DeviceAgentMessageHandler } from '../cloud-connection/device-agent-message-handler';
8
+
9
+ class JobState {
10
+ private static instance: JobState;
11
+ public jobInProgress = false;
12
+
13
+ // Singleton pattern
14
+ private constructor() { } // eslint-disable-line
15
+
16
+ static getInstance(): JobState {
17
+ if (!JobState.instance) {
18
+ JobState.instance = new JobState();
19
+ }
20
+ return JobState.instance;
21
+ }
22
+ }
23
+
24
+ export class JobHandler extends BaseHandler implements MessageHandler {
25
+ private readonly msgHandler: DeviceAgentMessageHandler;
26
+
27
+ constructor(handlerContext: HandlerContext) {
28
+ super(handlerContext);
29
+ this.msgHandler = new DeviceAgentMessageHandler(
30
+ handlerContext,
31
+ this.handleJobError,
32
+ this.handleJobSuccess
33
+ );
34
+ }
35
+ private readonly state = JobState.getInstance();
36
+ private jobId = '';
37
+
38
+ public async handle(message: any, topic: string): Promise<void> {
39
+ const TOPICS = this.getJobTopic();
40
+
41
+ try {
42
+ switch (topic) {
43
+ case TOPICS.NOTIFY_NEXT:
44
+ await this.notifyNext(message);
45
+ break;
46
+ case TOPICS.START_NEXT_ACCEPTED:
47
+ this.startNextAccepted(message);
48
+ break;
49
+ default:
50
+ logger.info(`No handler for topic: ${topic}`);
51
+ }
52
+ } catch (error) {
53
+ logger.error(`Error handling job for topic: ${topic}`, error);
54
+ }
55
+ }
56
+
57
+ getJobTopic() {
58
+ const TOPICS = {
59
+ NOTIFY_NEXT: `$aws/things/${this.clientId}/jobs/notify-next`,
60
+ START_NEXT: `$aws/things/${this.clientId}/jobs/start-next`,
61
+ START_NEXT_ACCEPTED: `$aws/things/${this.clientId}/jobs/start-next/accepted`,
62
+ UPDATE: `$aws/things/${this.clientId}/jobs/${this.jobId}/update`
63
+ };
64
+ return TOPICS;
65
+ }
66
+
67
+ private async notifyNext(message: any): Promise<void> {
68
+ // Triggers when a job is completed and we need to notify the next job
69
+ logger.info('Invoking NOTIFY_NEXT handler');
70
+ const TOPICS = this.getJobTopic();
71
+ this.publisher.publish(TOPICS.START_NEXT, JSON.stringify({}));
72
+ }
73
+
74
+ private startNextAccepted(message: any): void {
75
+ if (this.state.jobInProgress) {
76
+ logger.info('Job already in progress');
77
+ return;
78
+ }
79
+
80
+ this.state.jobInProgress = true;
81
+
82
+ try {
83
+ if (message.execution) {
84
+ this.processJobExecution(message.execution);
85
+ } else {
86
+ this.state.jobInProgress = false;
87
+ }
88
+ } catch (error: unknown) {
89
+ this.handleError(error);
90
+ }
91
+ this.state.jobInProgress = false;
92
+ }
93
+
94
+ private processJobExecution(execution: any): void {
95
+ logger.info('Job in progress...');
96
+ const { jobDocument, jobId } = execution;
97
+ this.jobId = jobId;
98
+ this.msgHandler.handle(jobDocument);
99
+ }
100
+
101
+ private handleError(error: unknown): void {
102
+ if (error instanceof Error) {
103
+ logger.error(`Error starting next job`, error);
104
+ this.handleJobError('', error.message);
105
+ } else {
106
+ let errorMessage: string;
107
+ if (typeof error === 'string') {
108
+ errorMessage = error;
109
+ } else if (typeof error === 'object' && error !== null) {
110
+ try {
111
+ errorMessage = JSON.stringify(error);
112
+ } catch (jsonError) {
113
+ errorMessage = 'Unknown error (failed to stringify)';
114
+ }
115
+ } else {
116
+ errorMessage = 'Unknown error';
117
+ }
118
+ logger.error(`Unexpected error: ${errorMessage}`);
119
+ this.handleJobError('', errorMessage);
120
+ }
121
+ }
122
+
123
+ private updateJobStatus(status: string, message?: string): void {
124
+ const { UPDATE } = this.getJobTopic();
125
+ const payload = {
126
+ status,
127
+ statusDetails: {
128
+ message
129
+ }
130
+ };
131
+
132
+ this.publisher.publish(UPDATE, JSON.stringify(payload));
133
+ logger.info(`Marked job ${this.jobId} as ${status}`);
134
+ // Reset the job state
135
+ this.state.jobInProgress = false;
136
+ }
137
+
138
+ private readonly handleJobSuccess = (txId: string): void => {
139
+ this.updateJobStatus('SUCCEEDED');
140
+ };
141
+
142
+ private readonly handleJobError = (_txId: string, msg: string): void => {
143
+ logger.error(`Unexpected error type: ${msg}`);
144
+ this.updateJobStatus('FAILED', `Unexpected error: ${msg}`);
145
+ };
146
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ getUpdateDeltaStateFromMessage,
3
+ validateSecureTunnelShadowUpdate
4
+ } from '@alwaysai/device-agent-schemas';
5
+ import { MessageHandler } from '../cloud-connection/message-dispatcher';
6
+ import { logger } from '../util/logger';
7
+ import { BaseHandler } from '../cloud-connection/base-message-handler';
8
+
9
+ export class SecureTunnelMessageHandler
10
+ extends BaseHandler
11
+ implements MessageHandler<any>
12
+ {
13
+ public getNotifyTopic(): string {
14
+ return `$aws/things/${this.clientId}/tunnels/notify`;
15
+ }
16
+
17
+ public async handle(message: any, topic: string): Promise<void> {
18
+ const secureTunnelNotifyTopic = this.getNotifyTopic();
19
+ if (topic === secureTunnelNotifyTopic) {
20
+ await this.secureTunnelHandler.secureTunnelNotifyHandler(message);
21
+ } else if (
22
+ topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
23
+ ) {
24
+ logger.info(`Received secure tunnel update: ${JSON.stringify(message)}`);
25
+ await this.handleSecureTunnelMessage(message);
26
+ } else if (
27
+ topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
28
+ ) {
29
+ logger.info(`Received secure tunnel deleteAccepted: ${message}`);
30
+ await this.secureTunnelHandler.destroy();
31
+ }
32
+ }
33
+
34
+ public async handleSecureTunnelMessage(payload: any): Promise<void> {
35
+ logger.info(`Received secure tunnel update: ${JSON.stringify(payload)}`);
36
+ const state = getUpdateDeltaStateFromMessage(payload);
37
+ if (!state) {
38
+ logger.debug(`No state found in message: ${JSON.stringify(payload)}`);
39
+ return;
40
+ }
41
+ const valid = validateSecureTunnelShadowUpdate(state);
42
+ if (!valid) {
43
+ logger.error(
44
+ `Error validating message: ${JSON.stringify(
45
+ { payload, errors: validateSecureTunnelShadowUpdate.errors },
46
+ null,
47
+ 2
48
+ )}`
49
+ );
50
+ return;
51
+ }
52
+ const secureTunnelUpdate =
53
+ await this.secureTunnelHandler.syncShadowToDeviceState(payload);
54
+ await this.shadowHandler.updateSecureTunnelShadow(secureTunnelUpdate);
55
+ }
56
+ }
@@ -37,10 +37,26 @@ export const installAppCliLeaf = CliLeaf({
37
37
  description: 'Release Hash',
38
38
  required: false,
39
39
  hidden: true
40
+ }),
41
+ appCfg: CliStringInput({
42
+ description: 'Application Configuration',
43
+ required: false,
44
+ hidden: true
45
+ }),
46
+ envVars: CliStringInput({
47
+ description: 'Environment Variables configuration',
48
+ hidden: true,
49
+ required: false
40
50
  })
41
51
  },
42
52
  async action(_, opts) {
43
- const { project, releaseHash, 'release-hash': releaseHashNew } = opts;
53
+ const {
54
+ project,
55
+ releaseHash,
56
+ 'release-hash': releaseHashNew,
57
+ appCfg,
58
+ envVars
59
+ } = opts;
44
60
  if (releaseHash) {
45
61
  logger.warn(
46
62
  `--releaseHash is deprecated and will be removed in a future release. Please switch to --release-hash`
@@ -60,7 +76,9 @@ export const installAppCliLeaf = CliLeaf({
60
76
  payload: {
61
77
  baseCommand: keyMirrors.appVersionControl.install,
62
78
  projectId: project,
63
- appReleaseHash: releaseHashResolved
79
+ appReleaseHash: releaseHashResolved,
80
+ appCfg,
81
+ envVars
64
82
  }
65
83
  };
66
84
  await deviceAgent.handleMessage(topic, message);