@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.
Files changed (119) hide show
  1. package/lib/application-control/backup.js +3 -3
  2. package/lib/application-control/backup.js.map +1 -1
  3. package/lib/application-control/index.d.ts +4 -4
  4. package/lib/application-control/index.d.ts.map +1 -1
  5. package/lib/application-control/index.js +1 -4
  6. package/lib/application-control/index.js.map +1 -1
  7. package/lib/application-control/install.d.ts +1 -1
  8. package/lib/application-control/install.d.ts.map +1 -1
  9. package/lib/application-control/install.js +41 -54
  10. package/lib/application-control/install.js.map +1 -1
  11. package/lib/application-control/models.d.ts +0 -4
  12. package/lib/application-control/models.d.ts.map +1 -1
  13. package/lib/application-control/models.js +13 -22
  14. package/lib/application-control/models.js.map +1 -1
  15. package/lib/application-control/status.d.ts +0 -6
  16. package/lib/application-control/status.d.ts.map +1 -1
  17. package/lib/application-control/status.js +3 -19
  18. package/lib/application-control/status.js.map +1 -1
  19. package/lib/application-control/utils.d.ts +3 -0
  20. package/lib/application-control/utils.d.ts.map +1 -1
  21. package/lib/application-control/utils.js +50 -21
  22. package/lib/application-control/utils.js.map +1 -1
  23. package/lib/cloud-connection/cmd-status.d.ts +16 -0
  24. package/lib/cloud-connection/cmd-status.d.ts.map +1 -0
  25. package/lib/cloud-connection/cmd-status.js +49 -0
  26. package/lib/cloud-connection/cmd-status.js.map +1 -0
  27. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +10 -1
  28. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  29. package/lib/cloud-connection/device-agent-cloud-connection.js +73 -33
  30. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  31. package/lib/cloud-connection/passthrough-handler.d.ts +11 -0
  32. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -0
  33. package/lib/cloud-connection/passthrough-handler.js +59 -0
  34. package/lib/cloud-connection/passthrough-handler.js.map +1 -0
  35. package/lib/cloud-connection/publisher.d.ts +1 -0
  36. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  37. package/lib/cloud-connection/publisher.js +14 -0
  38. package/lib/cloud-connection/publisher.js.map +1 -1
  39. package/lib/cloud-connection/shadow-handler.d.ts +2 -3
  40. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  41. package/lib/cloud-connection/shadow-handler.js +18 -4
  42. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  43. package/lib/cloud-connection/shadow-handler.test.d.ts +2 -0
  44. package/lib/cloud-connection/shadow-handler.test.d.ts.map +1 -0
  45. package/lib/cloud-connection/shadow-handler.test.js +321 -0
  46. package/lib/cloud-connection/shadow-handler.test.js.map +1 -0
  47. package/lib/environment.d.ts +1 -0
  48. package/lib/environment.d.ts.map +1 -1
  49. package/lib/environment.js +2 -1
  50. package/lib/environment.js.map +1 -1
  51. package/lib/infrastructure/agent-config.d.ts +15 -48
  52. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  53. package/lib/infrastructure/agent-config.js.map +1 -1
  54. package/lib/infrastructure/agent-config.test.js +0 -6
  55. package/lib/infrastructure/agent-config.test.js.map +1 -1
  56. package/lib/infrastructure/system-id.js +2 -2
  57. package/lib/infrastructure/system-id.js.map +1 -1
  58. package/lib/infrastructure/tokens-and-device-cfg.d.ts.map +1 -1
  59. package/lib/infrastructure/tokens-and-device-cfg.js +5 -9
  60. package/lib/infrastructure/tokens-and-device-cfg.js.map +1 -1
  61. package/lib/local-connection/rabbitmq-connection.d.ts +4 -0
  62. package/lib/local-connection/rabbitmq-connection.d.ts.map +1 -0
  63. package/lib/local-connection/rabbitmq-connection.js +58 -0
  64. package/lib/local-connection/rabbitmq-connection.js.map +1 -0
  65. package/lib/subcommands/app/app.d.ts +2 -1
  66. package/lib/subcommands/app/app.d.ts.map +1 -1
  67. package/lib/subcommands/app/app.js +56 -23
  68. package/lib/subcommands/app/app.js.map +1 -1
  69. package/lib/subcommands/device/clean.js +4 -4
  70. package/lib/subcommands/device/clean.js.map +1 -1
  71. package/lib/subcommands/device/device.d.ts +1 -1
  72. package/lib/subcommands/device/device.d.ts.map +1 -1
  73. package/lib/subcommands/device/device.js +7 -9
  74. package/lib/subcommands/device/device.js.map +1 -1
  75. package/lib/subcommands/index.d.ts +0 -1
  76. package/lib/subcommands/index.d.ts.map +1 -1
  77. package/lib/subcommands/login.d.ts +0 -1
  78. package/lib/subcommands/login.d.ts.map +1 -1
  79. package/lib/subcommands/login.js +1 -9
  80. package/lib/subcommands/login.js.map +1 -1
  81. package/lib/util/fetch-with-timeout.d.ts +4 -0
  82. package/lib/util/fetch-with-timeout.d.ts.map +1 -0
  83. package/lib/util/fetch-with-timeout.js +15 -0
  84. package/lib/util/fetch-with-timeout.js.map +1 -0
  85. package/lib/util/require-logged-in-and-paid-plan.d.ts +2 -0
  86. package/lib/util/require-logged-in-and-paid-plan.d.ts.map +1 -0
  87. package/lib/util/require-logged-in-and-paid-plan.js +18 -0
  88. package/lib/util/require-logged-in-and-paid-plan.js.map +1 -0
  89. package/lib/util/timer.d.ts +2 -0
  90. package/lib/util/timer.d.ts.map +1 -0
  91. package/lib/util/timer.js +6 -0
  92. package/lib/util/timer.js.map +1 -0
  93. package/package.json +20 -32
  94. package/readme.md +100 -89
  95. package/src/application-control/backup.ts +3 -3
  96. package/src/application-control/index.ts +0 -6
  97. package/src/application-control/install.ts +53 -73
  98. package/src/application-control/models.ts +7 -19
  99. package/src/application-control/status.ts +3 -19
  100. package/src/application-control/utils.ts +61 -22
  101. package/src/cloud-connection/cmd-status.ts +52 -0
  102. package/src/cloud-connection/device-agent-cloud-connection.ts +94 -47
  103. package/src/cloud-connection/passthrough-handler.ts +67 -0
  104. package/src/cloud-connection/publisher.ts +21 -0
  105. package/src/cloud-connection/shadow-handler.test.ts +361 -0
  106. package/src/cloud-connection/shadow-handler.ts +28 -7
  107. package/src/environment.ts +3 -0
  108. package/src/infrastructure/agent-config.test.ts +0 -7
  109. package/src/infrastructure/agent-config.ts +24 -2
  110. package/src/infrastructure/system-id.ts +1 -1
  111. package/src/infrastructure/tokens-and-device-cfg.ts +8 -13
  112. package/src/local-connection/rabbitmq-connection.ts +53 -0
  113. package/src/subcommands/app/app.ts +61 -27
  114. package/src/subcommands/device/clean.ts +4 -4
  115. package/src/subcommands/device/device.ts +8 -11
  116. package/src/subcommands/login.ts +1 -9
  117. package/src/util/fetch-with-timeout.ts +18 -0
  118. package/src/util/require-logged-in-and-paid-plan.ts +16 -0
  119. package/src/util/timer.ts +1 -0
@@ -20,11 +20,11 @@ import {
20
20
  buildApp,
21
21
  downloadPackageUsingPresignedUrl,
22
22
  getAppDir,
23
- requireAppInstalled
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, updateAppCfgFile, writeAppCfgFile } from './config';
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 requireAppInstalled({ projectId });
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 requireAppInstalled({ projectId });
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 requireAppInstalled({ projectId });
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 requireAppInstalled({ projectId });
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 requireAppInstalled({ projectId });
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 { TARGET_JSON_FILE_NAME } from 'alwaysai/lib/constants';
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 nodeFetch from 'node-fetch';
8
- import { TargetJsonFile } from 'alwaysai/lib/core/app';
9
- import { appDeployLinuxAndRemoteDevice } from 'alwaysai/lib/components/app';
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 targetJson = TargetJsonFile(appDir);
34
- const targetConfig = targetJson.read();
35
- if (targetConfig.targetProtocol !== 'docker:') {
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
- appDeployLinuxAndRemoteDevice,
40
- [
41
- {
42
- yes: true,
43
- generateDockerCompose: true,
44
- logs: false,
45
- stop: false,
46
- targetJson,
47
- targetConfig
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.info(`Downloading app package from ${presignedUrl}`);
68
- const response = await nodeFetch(presignedUrl);
69
- if (response.status !== 200) {
70
- // If the URL is invalid; I think we shouldn't get here with the new changes
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
- `Status Code: ${response.status}, ${response.statusText}. Response: ${response.body}`
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
- private publisher: Publisher;
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
- const { projectId, appReleaseHash } = payload;
71
- const signedUrlsRequest = { projectId, appReleaseHash };
72
- this.publishCloudRequest({
73
- messageType: keyMirrors.agentMessageType.signed_urls_request,
74
- signedUrlsRequest
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
- // FIXME: Why do we delete the shadow? Doesn't this delete it for all projects?
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
- throw new Error(
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