@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.
- package/lib/application-control/environment-variables.d.ts +4 -0
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/application-control/environment-variables.js +17 -13
- package/lib/application-control/environment-variables.js.map +1 -1
- package/lib/application-control/install.d.ts +4 -1
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +16 -1
- package/lib/application-control/install.js.map +1 -1
- package/lib/application-control/utils.d.ts.map +1 -1
- package/lib/application-control/utils.js +13 -0
- package/lib/application-control/utils.js.map +1 -1
- package/lib/cloud-connection/base-message-handler.d.ts +27 -0
- package/lib/cloud-connection/base-message-handler.d.ts.map +1 -0
- package/lib/cloud-connection/base-message-handler.js +72 -0
- package/lib/cloud-connection/base-message-handler.js.map +1 -0
- package/lib/cloud-connection/connection-manager.d.ts +21 -0
- package/lib/cloud-connection/connection-manager.d.ts.map +1 -0
- package/lib/cloud-connection/connection-manager.js +158 -0
- package/lib/cloud-connection/connection-manager.js.map +1 -0
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +7 -23
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +49 -517
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/device-agent-message-handler.d.ts +22 -0
- package/lib/cloud-connection/device-agent-message-handler.d.ts.map +1 -0
- package/lib/cloud-connection/device-agent-message-handler.js +357 -0
- package/lib/cloud-connection/device-agent-message-handler.js.map +1 -0
- package/lib/cloud-connection/live-updates-handler.d.ts +1 -0
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +13 -10
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/message-dispatcher.d.ts +10 -0
- package/lib/cloud-connection/message-dispatcher.d.ts.map +1 -0
- package/lib/cloud-connection/message-dispatcher.js +27 -0
- package/lib/cloud-connection/message-dispatcher.js.map +1 -0
- package/lib/cloud-connection/shadow-handler.d.ts +6 -0
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +74 -1
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +8 -1
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +21 -9
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +43 -2
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/jobs/job-handler.d.ts +23 -0
- package/lib/jobs/job-handler.d.ts.map +1 -0
- package/lib/jobs/job-handler.js +131 -0
- package/lib/jobs/job-handler.js.map +1 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts +8 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts.map +1 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.js +42 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.js.map +1 -0
- package/lib/subcommands/app/version.d.ts +2 -0
- package/lib/subcommands/app/version.d.ts.map +1 -1
- package/lib/subcommands/app/version.js +14 -2
- package/lib/subcommands/app/version.js.map +1 -1
- package/package.json +2 -2
- package/src/application-control/environment-variables.ts +31 -21
- package/src/application-control/install.ts +24 -3
- package/src/application-control/utils.ts +13 -0
- package/src/cloud-connection/base-message-handler.ts +118 -0
- package/src/cloud-connection/connection-manager.ts +187 -0
- package/src/cloud-connection/device-agent-cloud-connection.ts +109 -816
- package/src/cloud-connection/device-agent-message-handler.ts +642 -0
- package/src/cloud-connection/live-updates-handler.ts +26 -18
- package/src/cloud-connection/message-dispatcher.ts +33 -0
- package/src/cloud-connection/shadow-handler.ts +125 -1
- package/src/cloud-connection/transaction-manager.test.ts +65 -3
- package/src/cloud-connection/transaction-manager.ts +41 -27
- package/src/jobs/job-handler.ts +146 -0
- package/src/secure-tunneling/secure-tunnel-message-handler.ts +56 -0
- 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 {
|
|
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 {
|
|
116
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 {
|
|
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);
|