@alwaysai/device-agent 0.0.13 → 0.0.14
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/backup.js +3 -3
- package/lib/application-control/backup.js.map +1 -1
- package/lib/application-control/index.d.ts +4 -4
- package/lib/application-control/index.d.ts.map +1 -1
- package/lib/application-control/index.js +1 -4
- package/lib/application-control/index.js.map +1 -1
- package/lib/application-control/install.d.ts +1 -1
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +41 -54
- package/lib/application-control/install.js.map +1 -1
- package/lib/application-control/models.d.ts +0 -4
- package/lib/application-control/models.d.ts.map +1 -1
- package/lib/application-control/models.js +13 -22
- package/lib/application-control/models.js.map +1 -1
- package/lib/application-control/status.d.ts +0 -6
- package/lib/application-control/status.d.ts.map +1 -1
- package/lib/application-control/status.js +3 -19
- package/lib/application-control/status.js.map +1 -1
- package/lib/application-control/utils.d.ts +3 -0
- package/lib/application-control/utils.d.ts.map +1 -1
- package/lib/application-control/utils.js +50 -21
- package/lib/application-control/utils.js.map +1 -1
- package/lib/cloud-connection/cmd-status.d.ts +16 -0
- package/lib/cloud-connection/cmd-status.d.ts.map +1 -0
- package/lib/cloud-connection/cmd-status.js +49 -0
- package/lib/cloud-connection/cmd-status.js.map +1 -0
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +10 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +73 -33
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/passthrough-handler.d.ts +11 -0
- package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -0
- package/lib/cloud-connection/passthrough-handler.js +59 -0
- package/lib/cloud-connection/passthrough-handler.js.map +1 -0
- package/lib/cloud-connection/publisher.d.ts +1 -0
- package/lib/cloud-connection/publisher.d.ts.map +1 -1
- package/lib/cloud-connection/publisher.js +14 -0
- package/lib/cloud-connection/publisher.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts +2 -3
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +18 -4
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.test.d.ts +2 -0
- package/lib/cloud-connection/shadow-handler.test.d.ts.map +1 -0
- package/lib/cloud-connection/shadow-handler.test.js +321 -0
- package/lib/cloud-connection/shadow-handler.test.js.map +1 -0
- package/lib/environment.d.ts +1 -0
- package/lib/environment.d.ts.map +1 -1
- package/lib/environment.js +2 -1
- package/lib/environment.js.map +1 -1
- package/lib/infrastructure/agent-config.d.ts +15 -48
- package/lib/infrastructure/agent-config.d.ts.map +1 -1
- package/lib/infrastructure/agent-config.js.map +1 -1
- package/lib/infrastructure/agent-config.test.js +0 -6
- package/lib/infrastructure/agent-config.test.js.map +1 -1
- package/lib/infrastructure/system-id.js +2 -2
- package/lib/infrastructure/system-id.js.map +1 -1
- package/lib/infrastructure/tokens-and-device-cfg.d.ts.map +1 -1
- package/lib/infrastructure/tokens-and-device-cfg.js +5 -9
- package/lib/infrastructure/tokens-and-device-cfg.js.map +1 -1
- package/lib/local-connection/rabbitmq-connection.d.ts +4 -0
- package/lib/local-connection/rabbitmq-connection.d.ts.map +1 -0
- package/lib/local-connection/rabbitmq-connection.js +58 -0
- package/lib/local-connection/rabbitmq-connection.js.map +1 -0
- package/lib/subcommands/app/app.d.ts +2 -1
- package/lib/subcommands/app/app.d.ts.map +1 -1
- package/lib/subcommands/app/app.js +56 -23
- package/lib/subcommands/app/app.js.map +1 -1
- package/lib/subcommands/device/clean.js +4 -4
- package/lib/subcommands/device/clean.js.map +1 -1
- package/lib/subcommands/device/device.d.ts +1 -1
- package/lib/subcommands/device/device.d.ts.map +1 -1
- package/lib/subcommands/device/device.js +7 -9
- package/lib/subcommands/device/device.js.map +1 -1
- package/lib/subcommands/index.d.ts +0 -1
- package/lib/subcommands/index.d.ts.map +1 -1
- package/lib/subcommands/login.d.ts +0 -1
- package/lib/subcommands/login.d.ts.map +1 -1
- package/lib/subcommands/login.js +1 -9
- package/lib/subcommands/login.js.map +1 -1
- package/lib/util/fetch-with-timeout.d.ts +4 -0
- package/lib/util/fetch-with-timeout.d.ts.map +1 -0
- package/lib/util/fetch-with-timeout.js +15 -0
- package/lib/util/fetch-with-timeout.js.map +1 -0
- package/lib/util/require-logged-in-and-paid-plan.d.ts +2 -0
- package/lib/util/require-logged-in-and-paid-plan.d.ts.map +1 -0
- package/lib/util/require-logged-in-and-paid-plan.js +18 -0
- package/lib/util/require-logged-in-and-paid-plan.js.map +1 -0
- package/lib/util/timer.d.ts +2 -0
- package/lib/util/timer.d.ts.map +1 -0
- package/lib/util/timer.js +6 -0
- package/lib/util/timer.js.map +1 -0
- package/package.json +20 -32
- package/readme.md +100 -89
- package/src/application-control/backup.ts +3 -3
- package/src/application-control/index.ts +0 -6
- package/src/application-control/install.ts +53 -73
- package/src/application-control/models.ts +7 -19
- package/src/application-control/status.ts +3 -19
- package/src/application-control/utils.ts +61 -22
- package/src/cloud-connection/cmd-status.ts +52 -0
- package/src/cloud-connection/device-agent-cloud-connection.ts +94 -47
- package/src/cloud-connection/passthrough-handler.ts +67 -0
- package/src/cloud-connection/publisher.ts +21 -0
- package/src/cloud-connection/shadow-handler.test.ts +361 -0
- package/src/cloud-connection/shadow-handler.ts +28 -7
- package/src/environment.ts +3 -0
- package/src/infrastructure/agent-config.test.ts +0 -7
- package/src/infrastructure/agent-config.ts +24 -2
- package/src/infrastructure/system-id.ts +1 -1
- package/src/infrastructure/tokens-and-device-cfg.ts +8 -13
- package/src/local-connection/rabbitmq-connection.ts +53 -0
- package/src/subcommands/app/app.ts +61 -27
- package/src/subcommands/device/clean.ts +4 -4
- package/src/subcommands/device/device.ts +8 -11
- package/src/subcommands/login.ts +1 -9
- package/src/util/fetch-with-timeout.ts +18 -0
- package/src/util/require-logged-in-and-paid-plan.ts +16 -0
- package/src/util/timer.ts +1 -0
|
@@ -20,11 +20,11 @@ import {
|
|
|
20
20
|
buildApp,
|
|
21
21
|
downloadPackageUsingPresignedUrl,
|
|
22
22
|
getAppDir,
|
|
23
|
-
|
|
23
|
+
requireAppReady
|
|
24
24
|
} from './utils';
|
|
25
25
|
import { MODEL_JSON_FILE_NAME } from 'alwaysai/lib/core/model';
|
|
26
26
|
import { APP_MODELS_DIRECTORY_NAME } from 'alwaysai/lib/constants';
|
|
27
|
-
import { readAppCfgFile,
|
|
27
|
+
import { readAppCfgFile, writeAppCfgFile } from './config';
|
|
28
28
|
import { AppConfig } from '@alwaysai/app-configuration-schemas';
|
|
29
29
|
|
|
30
30
|
export async function getAppModels(props: { projectId: string }) {
|
|
@@ -49,26 +49,12 @@ export async function getAppModels(props: { projectId: string }) {
|
|
|
49
49
|
return modelDetails;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export async function addModel(props: { projectId: string; modelId: string }) {
|
|
53
|
-
const { projectId, modelId } = props;
|
|
54
|
-
await requireAppInstalled({ projectId });
|
|
55
|
-
|
|
56
|
-
const appDir = getAppDir(projectId);
|
|
57
|
-
await appModelsAddComponent({
|
|
58
|
-
yes: false,
|
|
59
|
-
dir: appDir,
|
|
60
|
-
id: modelId,
|
|
61
|
-
addToProject: false
|
|
62
|
-
});
|
|
63
|
-
await buildApp({ appDir });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
52
|
export async function removeModel(props: {
|
|
67
53
|
projectId: string;
|
|
68
54
|
modelId: string;
|
|
69
55
|
}) {
|
|
70
56
|
const { projectId, modelId } = props;
|
|
71
|
-
await
|
|
57
|
+
await requireAppReady({ projectId });
|
|
72
58
|
|
|
73
59
|
const appDir = getAppDir(projectId);
|
|
74
60
|
|
|
@@ -90,7 +76,7 @@ export async function replaceModels(props: {
|
|
|
90
76
|
modelIds: string[];
|
|
91
77
|
}) {
|
|
92
78
|
const { projectId, modelIds } = props;
|
|
93
|
-
await
|
|
79
|
+
await requireAppReady({ projectId });
|
|
94
80
|
|
|
95
81
|
const appDir = getAppDir(projectId);
|
|
96
82
|
|
|
@@ -117,7 +103,7 @@ export async function replaceModels(props: {
|
|
|
117
103
|
|
|
118
104
|
export async function updateModels(props: { projectId: string }) {
|
|
119
105
|
const { projectId } = props;
|
|
120
|
-
await
|
|
106
|
+
await requireAppReady({ projectId });
|
|
121
107
|
|
|
122
108
|
const appDir = getAppDir(projectId);
|
|
123
109
|
await appModelsUpdateComponent({
|
|
@@ -223,6 +209,7 @@ export async function updateModelsWithPresignedUrls(props: {
|
|
|
223
209
|
await restartApp({ projectId });
|
|
224
210
|
|
|
225
211
|
logger.info(`Models installed for project ${projectId}`);
|
|
212
|
+
/* Leave error handling to higher level so errors are sent to cloud
|
|
226
213
|
} catch (e) {
|
|
227
214
|
logger.error(
|
|
228
215
|
'Error updating app models from presigned URL, restoring models.',
|
|
@@ -231,6 +218,7 @@ export async function updateModelsWithPresignedUrls(props: {
|
|
|
231
218
|
await spawner.rimraf(ogDir);
|
|
232
219
|
await copyDir({ srcPath: restoreDir, destPath: ogDir });
|
|
233
220
|
await writeAppCfgFile({ projectId, appCfg: ogAppCfg });
|
|
221
|
+
*/
|
|
234
222
|
} finally {
|
|
235
223
|
await spawner.rimraf(tmpDir);
|
|
236
224
|
await spawner.rimraf(restoreDir);
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import compose from 'docker-compose';
|
|
2
|
-
import { fetchAppReleaseHistory } from 'alwaysai/lib/infrastructure';
|
|
3
2
|
import { JsSpawner } from 'alwaysai/lib/util';
|
|
4
3
|
|
|
5
4
|
import { runDockerLogin } from '../docker/docker-cmd';
|
|
6
|
-
import { getAppDir, requireAppInstalled } from './utils';
|
|
5
|
+
import { getAppDir, requireAppInstalled, requireAppReady } from './utils';
|
|
7
6
|
import {
|
|
8
7
|
ServiceStatusPacket,
|
|
9
8
|
AppStatePacket,
|
|
@@ -13,21 +12,6 @@ import {
|
|
|
13
12
|
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
14
13
|
import { logger } from '../util/logger';
|
|
15
14
|
|
|
16
|
-
export async function listAppReleases(props: { projectId: string }) {
|
|
17
|
-
const { projectId } = props;
|
|
18
|
-
const releaseHistory = await fetchAppReleaseHistory(projectId);
|
|
19
|
-
return releaseHistory;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function listAppLatestRelease(props: { projectId: string }) {
|
|
23
|
-
const { projectId } = props;
|
|
24
|
-
const releaseHistory = await fetchAppReleaseHistory(projectId);
|
|
25
|
-
if (releaseHistory.length >= 1) {
|
|
26
|
-
return releaseHistory[0]['releaseHash'];
|
|
27
|
-
}
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
15
|
export async function getAppStatus(props: {
|
|
32
16
|
projectId: string;
|
|
33
17
|
}): Promise<AppStatePacket> {
|
|
@@ -117,7 +101,7 @@ export async function getAppLogs(props: {
|
|
|
117
101
|
args?: string[];
|
|
118
102
|
}): Promise<NodeJS.ReadableStream> {
|
|
119
103
|
const { projectId, services, args } = props;
|
|
120
|
-
await
|
|
104
|
+
await requireAppReady({ projectId });
|
|
121
105
|
|
|
122
106
|
const appDir = getAppDir(projectId);
|
|
123
107
|
|
|
@@ -143,7 +127,7 @@ export async function startApp(props: {
|
|
|
143
127
|
dockerLoginToken?: string;
|
|
144
128
|
}): Promise<void> {
|
|
145
129
|
const { projectId, dockerLoginToken } = props;
|
|
146
|
-
await
|
|
130
|
+
await requireAppReady({ projectId });
|
|
147
131
|
|
|
148
132
|
const appDir = getAppDir(projectId);
|
|
149
133
|
if (dockerLoginToken !== undefined) {
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import compose from 'docker-compose';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DOCKER_COMPOSE_FILE,
|
|
6
|
+
TARGET_JSON_FILE_NAME
|
|
7
|
+
} from 'alwaysai/lib/constants';
|
|
5
8
|
|
|
6
9
|
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
+
import { fetchWithTimeout } from '../util/fetch-with-timeout';
|
|
11
|
+
import {
|
|
12
|
+
getDockerComposeCmdForApp,
|
|
13
|
+
TargetJsonFile,
|
|
14
|
+
updateDockerComposeTargetHw,
|
|
15
|
+
writeDockerComposeFile,
|
|
16
|
+
writeStandaloneDockerfile
|
|
17
|
+
} from 'alwaysai/lib/core/app';
|
|
10
18
|
import { runInDir } from '../util/run-in-dir';
|
|
11
19
|
import { logger } from '../util/logger';
|
|
12
20
|
import { APP_ROOT } from '../util/directories';
|
|
21
|
+
import { JsSpawner } from 'alwaysai/lib/util/spawner';
|
|
13
22
|
|
|
14
23
|
export function getAppDir(projectId: string): string {
|
|
15
24
|
return path.join(APP_ROOT, projectId);
|
|
@@ -21,6 +30,11 @@ export async function requireAppInstalled(props: { projectId: string }) {
|
|
|
21
30
|
if (!(await AgentConfigFile().isAppPresent({ projectId }))) {
|
|
22
31
|
throw new Error('Application is not installed');
|
|
23
32
|
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function requireAppReady(props: { projectId: string }) {
|
|
36
|
+
const { projectId } = props;
|
|
37
|
+
await requireAppInstalled({ projectId });
|
|
24
38
|
if (!(await AgentConfigFile().isAppReady({ projectId }))) {
|
|
25
39
|
throw new Error('Application is not done installing or updating');
|
|
26
40
|
}
|
|
@@ -30,23 +44,28 @@ export async function buildApp(props: { appDir: string }) {
|
|
|
30
44
|
const { appDir } = props;
|
|
31
45
|
|
|
32
46
|
// Build standalone image and docker-compose
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
47
|
+
const targetJsonFile = TargetJsonFile(appDir);
|
|
48
|
+
const targetCfg = targetJsonFile.read();
|
|
49
|
+
if (targetCfg.targetProtocol !== 'docker:') {
|
|
36
50
|
throw new Error(`${TARGET_JSON_FILE_NAME} is not properly configured!`);
|
|
37
51
|
}
|
|
38
52
|
await runInDir(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
async () => {
|
|
54
|
+
await writeStandaloneDockerfile();
|
|
55
|
+
const spawner = JsSpawner();
|
|
56
|
+
const targetHardware = targetCfg.targetHardware;
|
|
57
|
+
if (await spawner.exists(DOCKER_COMPOSE_FILE)) {
|
|
58
|
+
await updateDockerComposeTargetHw({
|
|
59
|
+
targetHardware
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
await writeDockerComposeFile({
|
|
63
|
+
spawner: JsSpawner(),
|
|
64
|
+
cmd: await getDockerComposeCmdForApp({ targetHardware })
|
|
65
|
+
});
|
|
48
66
|
}
|
|
49
|
-
|
|
67
|
+
},
|
|
68
|
+
[],
|
|
50
69
|
appDir
|
|
51
70
|
);
|
|
52
71
|
|
|
@@ -59,17 +78,37 @@ export async function buildApp(props: { appDir: string }) {
|
|
|
59
78
|
}
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
class HTTPResponseError extends Error {
|
|
82
|
+
public response;
|
|
83
|
+
constructor(response) {
|
|
84
|
+
super(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
85
|
+
this.response = response;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const checkStatus = (response) => {
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
// response.status >= 200 && response.status < 300
|
|
92
|
+
return response;
|
|
93
|
+
} else {
|
|
94
|
+
throw new HTTPResponseError(response);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
62
98
|
export async function downloadPackageUsingPresignedUrl(props: {
|
|
63
99
|
localDest: string;
|
|
64
100
|
presignedUrl: string;
|
|
65
101
|
}): Promise<void> {
|
|
66
102
|
const { localDest, presignedUrl } = props;
|
|
67
|
-
logger.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
logger.debug(`Downloading package from ${presignedUrl}`);
|
|
104
|
+
let response: any;
|
|
105
|
+
try {
|
|
106
|
+
response = await fetchWithTimeout(presignedUrl);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const errorBody =
|
|
109
|
+
error.type === 'aborted' ? error : await error.response.text();
|
|
71
110
|
throw new Error(
|
|
72
|
-
`
|
|
111
|
+
`downloadPackageUsingPresignedUrl: Error=${error}\n${errorBody}`
|
|
73
112
|
);
|
|
74
113
|
}
|
|
75
114
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type CmdStatusType = 'idle' | 'in_progress';
|
|
2
|
+
|
|
3
|
+
export class CmdStatus {
|
|
4
|
+
private projectId: string;
|
|
5
|
+
private status: CmdStatusType;
|
|
6
|
+
|
|
7
|
+
constructor(projectId: string, status: CmdStatusType) {
|
|
8
|
+
this.projectId = projectId;
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public getProjectId() {
|
|
13
|
+
return this.projectId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public update(status: CmdStatusType) {
|
|
17
|
+
this.status = status;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public getStatus(): CmdStatusType {
|
|
21
|
+
return this.status;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CmdStatusManager {
|
|
26
|
+
private apps: { [projectId: string]: CmdStatus } = {};
|
|
27
|
+
|
|
28
|
+
public update(projectId: string, status: CmdStatusType) {
|
|
29
|
+
if (projectId in this.apps) {
|
|
30
|
+
this.apps[projectId].update(status);
|
|
31
|
+
} else {
|
|
32
|
+
const cmdStatus = new CmdStatus(projectId, status);
|
|
33
|
+
this.apps[projectId] = cmdStatus;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public getAppCmdStatus(projectId: string) {
|
|
38
|
+
if (projectId in this.apps) {
|
|
39
|
+
return this.apps[projectId].getStatus();
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`No status for ${projectId}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public anyCmdInProgress() {
|
|
45
|
+
for (const projectId in this.apps) {
|
|
46
|
+
if (this.apps[projectId].getStatus() === 'in_progress') {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -31,16 +31,20 @@ import {
|
|
|
31
31
|
uninstallApp,
|
|
32
32
|
updateAppCfg
|
|
33
33
|
} from '../application-control';
|
|
34
|
-
import { AppConfigUpdate, ShadowHandler } from './shadow-handler';
|
|
34
|
+
import { AppConfigUpdate, ShadowHandler, ShadowTopics } from './shadow-handler';
|
|
35
35
|
import { Publisher } from './publisher';
|
|
36
36
|
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
37
37
|
import { bootstrapProvision } from './bootstrap-provision';
|
|
38
38
|
import { AppInstallStatusManager } from './app-install-status';
|
|
39
39
|
import { AppConfig } from '@alwaysai/app-configuration-schemas';
|
|
40
|
+
import { CmdStatusManager, CmdStatusType } from './cmd-status';
|
|
41
|
+
import { PassthroughHandler, runChannel } from './passthrough-handler';
|
|
42
|
+
import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
|
|
40
43
|
|
|
41
44
|
export class DeviceAgentCloudConnection {
|
|
42
45
|
private shadowHandler: ShadowHandler;
|
|
43
|
-
|
|
46
|
+
public publisher: Publisher;
|
|
47
|
+
private cmdStatusMgr: CmdStatusManager;
|
|
44
48
|
private liveUpdatesHandler: LiveUpdatesHandler;
|
|
45
49
|
private appInstallStatusMgr: AppInstallStatusManager;
|
|
46
50
|
private device = awsIot.device;
|
|
@@ -67,12 +71,26 @@ export class DeviceAgentCloudConnection {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
private handleAppVersionControl(payload: AppVersionControlPacket) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
switch (payload.baseCommand) {
|
|
75
|
+
case keyMirrors.appVersionControl.install: {
|
|
76
|
+
const { projectId, appReleaseHash } = payload;
|
|
77
|
+
this.cmdStatusMgr.update(projectId, 'in_progress');
|
|
78
|
+
const signedUrlsRequest = { projectId, appReleaseHash };
|
|
79
|
+
this.publishCloudRequest({
|
|
80
|
+
messageType: keyMirrors.agentMessageType.signed_urls_request,
|
|
81
|
+
signedUrlsRequest
|
|
82
|
+
});
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
logger.warn(
|
|
87
|
+
`Ignore App Version Control packet: ${JSON.stringify(
|
|
88
|
+
payload,
|
|
89
|
+
null,
|
|
90
|
+
2
|
|
91
|
+
)}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
private handleDeviceCommand = async (packet: any) => {
|
|
@@ -126,6 +144,7 @@ export class DeviceAgentCloudConnection {
|
|
|
126
144
|
this.liveUpdatesHandler.update({
|
|
127
145
|
appInstallStatus: { toggle: false, appReleaseHash }
|
|
128
146
|
});
|
|
147
|
+
this.cmdStatusMgr.update(projectId, 'idle');
|
|
129
148
|
|
|
130
149
|
// update app config shadow for project
|
|
131
150
|
await this.shadowHandler.publishAppState(projectId);
|
|
@@ -144,16 +163,17 @@ export class DeviceAgentCloudConnection {
|
|
|
144
163
|
this.liveUpdatesHandler.update({
|
|
145
164
|
appInstallStatus: { toggle: false, appReleaseHash }
|
|
146
165
|
});
|
|
166
|
+
this.cmdStatusMgr.update(projectId, 'idle');
|
|
147
167
|
|
|
148
168
|
// delete shadow for project
|
|
149
|
-
|
|
150
|
-
this.shadowHandler.deleteProjectShadow();
|
|
169
|
+
this.shadowHandler.deleteProjectShadow(projectId);
|
|
151
170
|
}
|
|
152
171
|
}
|
|
153
172
|
|
|
154
173
|
private async handleAppConfigUpdates(appConfgUpdates: AppConfigUpdate[]) {
|
|
155
174
|
for (const appConfigUpdate of appConfgUpdates) {
|
|
156
175
|
const { projectId, newAppCfg, updatedModels } = appConfigUpdate;
|
|
176
|
+
this.cmdStatusMgr.update(projectId, 'in_progress');
|
|
157
177
|
if (updatedModels && Object.keys(updatedModels).length) {
|
|
158
178
|
// Publish request for model urls
|
|
159
179
|
this.newAppCfgQueue.push(newAppCfg);
|
|
@@ -207,6 +227,7 @@ export class DeviceAgentCloudConnection {
|
|
|
207
227
|
});
|
|
208
228
|
this.publisher = new Publisher(this.device, this.clientId);
|
|
209
229
|
this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
|
|
230
|
+
this.cmdStatusMgr = new CmdStatusManager();
|
|
210
231
|
this.appInstallStatusMgr = new AppInstallStatusManager();
|
|
211
232
|
this.liveUpdatesHandler = new LiveUpdatesHandler(
|
|
212
233
|
this.publisher,
|
|
@@ -222,6 +243,18 @@ export class DeviceAgentCloudConnection {
|
|
|
222
243
|
return this.clientId;
|
|
223
244
|
}
|
|
224
245
|
|
|
246
|
+
public getToDeviceTopic() {
|
|
247
|
+
return this.toDeviceTopic;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
public getShadowTopics(): ShadowTopics {
|
|
251
|
+
return this.shadowHandler.shadowTopics;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public getCmdStatus(projectId: string): CmdStatusType {
|
|
255
|
+
return this.cmdStatusMgr.getAppCmdStatus(projectId);
|
|
256
|
+
}
|
|
257
|
+
|
|
225
258
|
public async handleClientMessage({
|
|
226
259
|
topic,
|
|
227
260
|
message
|
|
@@ -229,6 +262,17 @@ export class DeviceAgentCloudConnection {
|
|
|
229
262
|
topic: string;
|
|
230
263
|
message: ClientMessage;
|
|
231
264
|
}) {
|
|
265
|
+
const valid = validateClientMessage(message);
|
|
266
|
+
if (!valid) {
|
|
267
|
+
logger.error(
|
|
268
|
+
`Error validating message: ${JSON.stringify(
|
|
269
|
+
{ topic, message, errors: validateClientMessage.errors },
|
|
270
|
+
null,
|
|
271
|
+
2
|
|
272
|
+
)}`
|
|
273
|
+
);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
232
276
|
const payload = message.payload;
|
|
233
277
|
switch (payload.messageType) {
|
|
234
278
|
case keyMirrors.clientMessageType.app_state_control: {
|
|
@@ -271,9 +315,10 @@ export class DeviceAgentCloudConnection {
|
|
|
271
315
|
|
|
272
316
|
const newAppCfg = this.newAppCfgQueue.shift();
|
|
273
317
|
if (newAppCfg === undefined) {
|
|
274
|
-
|
|
318
|
+
logger.error(
|
|
275
319
|
'Unknown error while updating models via application config! No config present for model update.'
|
|
276
320
|
);
|
|
321
|
+
return;
|
|
277
322
|
}
|
|
278
323
|
|
|
279
324
|
await this.atomicApplicationUpdate(
|
|
@@ -303,11 +348,32 @@ export class DeviceAgentCloudConnection {
|
|
|
303
348
|
}
|
|
304
349
|
}
|
|
305
350
|
|
|
351
|
+
public async handleMessage(topic: string, message: ClientMessage | any) {
|
|
352
|
+
switch (topic) {
|
|
353
|
+
case this.shadowHandler.shadowTopics.projects.getAccepted:
|
|
354
|
+
case this.shadowHandler.shadowTopics.projects.updateDelta: {
|
|
355
|
+
const appConfigUpdates = await this.shadowHandler.handleShadowTopic({
|
|
356
|
+
topic,
|
|
357
|
+
payload: message.state
|
|
358
|
+
});
|
|
359
|
+
await this.handleAppConfigUpdates(appConfigUpdates);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case this.toDeviceTopic:
|
|
363
|
+
this.handleClientMessage({
|
|
364
|
+
topic,
|
|
365
|
+
message
|
|
366
|
+
});
|
|
367
|
+
break;
|
|
368
|
+
default:
|
|
369
|
+
logger.error(`Unexpected topic, ignoring! ${topic}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
306
373
|
public async setupHandlers() {
|
|
307
374
|
this.device.on('connect', (connack: any) => {
|
|
308
375
|
logger.info('Device Agent has connected to the cloud');
|
|
309
376
|
|
|
310
|
-
// Get shadow updates
|
|
311
377
|
this.shadowHandler.getShadowUpdates();
|
|
312
378
|
});
|
|
313
379
|
|
|
@@ -321,47 +387,18 @@ export class DeviceAgentCloudConnection {
|
|
|
321
387
|
);
|
|
322
388
|
});
|
|
323
389
|
|
|
390
|
+
this.device.on('error', function (error) {
|
|
391
|
+
const errorString = error.message.toString();
|
|
392
|
+
logger.error(`${errorString}`);
|
|
393
|
+
});
|
|
394
|
+
|
|
324
395
|
this.device.on('message', async (topic: string, payload: string) => {
|
|
325
396
|
try {
|
|
326
397
|
const jsonPacket = JSON.parse(payload);
|
|
327
398
|
logger.debug(
|
|
328
399
|
`Received message: ${JSON.stringify({ topic, jsonPacket }, null, 2)}`
|
|
329
400
|
);
|
|
330
|
-
|
|
331
|
-
if (Object.prototype.hasOwnProperty.call(jsonPacket, 'state')) {
|
|
332
|
-
if (jsonPacket.clientToken === this.getClientId()) {
|
|
333
|
-
logger.debug(
|
|
334
|
-
`Ignoring message sent from self: ${JSON.stringify(
|
|
335
|
-
{ topic, jsonPacket },
|
|
336
|
-
null,
|
|
337
|
-
2
|
|
338
|
-
)}`
|
|
339
|
-
);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const appConfigUpdates = await this.shadowHandler.handleShadowTopic({
|
|
344
|
-
topic,
|
|
345
|
-
payload: jsonPacket.state
|
|
346
|
-
});
|
|
347
|
-
await this.handleAppConfigUpdates(appConfigUpdates);
|
|
348
|
-
} else {
|
|
349
|
-
const valid = validateClientMessage(jsonPacket);
|
|
350
|
-
if (!valid) {
|
|
351
|
-
logger.error(
|
|
352
|
-
`Error validating message: ${JSON.stringify(
|
|
353
|
-
validateClientMessage.errors,
|
|
354
|
-
null,
|
|
355
|
-
2
|
|
356
|
-
)}`
|
|
357
|
-
);
|
|
358
|
-
} else {
|
|
359
|
-
this.handleClientMessage({
|
|
360
|
-
topic,
|
|
361
|
-
message: jsonPacket
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
401
|
+
await this.handleMessage(topic, jsonPacket);
|
|
365
402
|
} catch (error) {
|
|
366
403
|
logger.error(`Error parsing message: ${error}`);
|
|
367
404
|
}
|
|
@@ -371,6 +408,10 @@ export class DeviceAgentCloudConnection {
|
|
|
371
408
|
logger.warn(`Device Agent is offline ${new Date().toLocaleString()}`);
|
|
372
409
|
});
|
|
373
410
|
}
|
|
411
|
+
|
|
412
|
+
public stop() {
|
|
413
|
+
this.device.end();
|
|
414
|
+
}
|
|
374
415
|
}
|
|
375
416
|
|
|
376
417
|
export async function runDeviceAgentCloudInterface() {
|
|
@@ -378,6 +419,12 @@ export async function runDeviceAgentCloudInterface() {
|
|
|
378
419
|
if (existsSync(getCertificateFilePath())) {
|
|
379
420
|
const deviceAgent = new DeviceAgentCloudConnection();
|
|
380
421
|
await deviceAgent.setupHandlers();
|
|
422
|
+
if (ALWAYSAI_ANALYTICS_PASSTHROUGH === true) {
|
|
423
|
+
const publisher = deviceAgent.publisher;
|
|
424
|
+
const passthroughHandler = new PassthroughHandler(publisher);
|
|
425
|
+
await passthroughHandler.setup();
|
|
426
|
+
runChannel(passthroughHandler);
|
|
427
|
+
}
|
|
381
428
|
} else if (existsSync(BOOTSTRAP_DEVICE_PRIVATE_KEY_FILE_PATH())) {
|
|
382
429
|
bootstrapProvision();
|
|
383
430
|
} else if (existsSync(BOOTSTRAP_CERTIFICATES_DIR_PATH())) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// eslint-disable-next-line
|
|
2
|
+
const amqp = require('amqplib');
|
|
3
|
+
import { setupRabbitMQContainer } from '../local-connection/rabbitmq-connection';
|
|
4
|
+
import { logger } from '../util/logger';
|
|
5
|
+
import { Publisher } from './publisher';
|
|
6
|
+
|
|
7
|
+
const messageQueue: any[] = [];
|
|
8
|
+
const ackQueue: any[] = [];
|
|
9
|
+
|
|
10
|
+
export class PassthroughHandler {
|
|
11
|
+
public publisher: Publisher;
|
|
12
|
+
public connection;
|
|
13
|
+
public channel;
|
|
14
|
+
public packetQueue;
|
|
15
|
+
|
|
16
|
+
constructor(publisher: Publisher) {
|
|
17
|
+
this.publisher = publisher;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async setup() {
|
|
21
|
+
await setupRabbitMQContainer();
|
|
22
|
+
this.connection = await amqp.connect('amqp://localhost');
|
|
23
|
+
this.channel = await this.connection.createChannel();
|
|
24
|
+
this.channel.prefetch(1); // This ensures we only get one packet at a time! This appears to have prevented throttling
|
|
25
|
+
this.packetQueue = 'edgeiq-analytics-publish';
|
|
26
|
+
await this.channel.assertQueue(this.packetQueue, {
|
|
27
|
+
durable: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function processPublish(passthroughHandler: PassthroughHandler) {
|
|
33
|
+
while (messageQueue.length > 0) {
|
|
34
|
+
const entry = messageQueue.shift();
|
|
35
|
+
const { packet, msg } = entry;
|
|
36
|
+
ackQueue.push(msg);
|
|
37
|
+
// FIXME: put real topic here
|
|
38
|
+
passthroughHandler.publisher.publishToCloudWithAck(packet, (errOrResp) => {
|
|
39
|
+
logger.debug('packet published to cloud?', errOrResp);
|
|
40
|
+
while (ackQueue.length > 0) {
|
|
41
|
+
const msg = ackQueue.shift();
|
|
42
|
+
if (errOrResp === true) {
|
|
43
|
+
passthroughHandler.channel.ack(msg); // acknowledge, allow queue to discard
|
|
44
|
+
} else if (errOrResp === false) {
|
|
45
|
+
passthroughHandler.channel.reject(msg, true); // reject and requeue
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runChannel(passthroughHandler: PassthroughHandler) {
|
|
53
|
+
logger.debug('Beginning to consume packets');
|
|
54
|
+
passthroughHandler.channel.consume(
|
|
55
|
+
passthroughHandler.packetQueue,
|
|
56
|
+
function (msg) {
|
|
57
|
+
if (msg.content !== undefined) {
|
|
58
|
+
const packet = JSON.parse(msg.content.toString());
|
|
59
|
+
messageQueue.push({ packet, msg });
|
|
60
|
+
processPublish(passthroughHandler);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -43,6 +43,27 @@ export class Publisher {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
public publishToCloudWithAck(
|
|
47
|
+
payload: string,
|
|
48
|
+
ackNackCallback: CallableFunction
|
|
49
|
+
) {
|
|
50
|
+
const topic = this.toCloudTopic;
|
|
51
|
+
logger.debug('payload received to publishWithAck', payload);
|
|
52
|
+
this.client.publish(topic, payload, { qos: 1 }, (err: any, resp: any) => {
|
|
53
|
+
if (err) {
|
|
54
|
+
logger.error(
|
|
55
|
+
`Error publishing message: \nTopic: ${topic}\nMessage: ${payload}\nError: ${err}`
|
|
56
|
+
);
|
|
57
|
+
ackNackCallback(false);
|
|
58
|
+
} else if (resp) {
|
|
59
|
+
logger.debug(
|
|
60
|
+
`Successfully published message: \nTopic: ${topic}\nMessage: ${payload}`
|
|
61
|
+
);
|
|
62
|
+
ackNackCallback(true);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
public publishDeviceAgentPayload(
|
|
47
68
|
topic: string,
|
|
48
69
|
payload: DeviceAgentMessagePayload
|