@alwaysai/device-agent 0.0.13 → 0.0.15
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/bootstrap-provision.d.ts +1 -1
- package/lib/cloud-connection/bootstrap-provision.d.ts.map +1 -1
- package/lib/cloud-connection/bootstrap-provision.js +9 -9
- package/lib/cloud-connection/bootstrap-provision.js.map +1 -1
- package/lib/cloud-connection/cmd-status.d.ts +8 -0
- package/lib/cloud-connection/cmd-status.d.ts.map +1 -0
- package/lib/cloud-connection/cmd-status.js +62 -0
- package/lib/cloud-connection/cmd-status.js.map +1 -0
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +10 -2
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +156 -66
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/device-agent.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent.js +4 -3
- package/lib/cloud-connection/device-agent.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts +10 -18
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +50 -50
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/messages.d.ts +3 -1
- package/lib/cloud-connection/messages.d.ts.map +1 -1
- package/lib/cloud-connection/messages.js +13 -1
- package/lib/cloud-connection/messages.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 +3 -2
- package/lib/environment.js.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.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 +4 -3
- package/lib/subcommands/app/app.d.ts.map +1 -1
- package/lib/subcommands/app/app.js +78 -27
- package/lib/subcommands/app/app.js.map +1 -1
- package/lib/subcommands/app/index.js +2 -2
- 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 +9 -10
- 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/directories.d.ts +11 -12
- package/lib/util/directories.d.ts.map +1 -1
- package/lib/util/directories.js +24 -29
- package/lib/util/directories.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/logger.js +1 -0
- package/lib/util/logger.js.map +1 -1
- 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/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/bootstrap-provision.ts +13 -10
- package/src/cloud-connection/cmd-status.ts +71 -0
- package/src/cloud-connection/device-agent-cloud-connection.ts +211 -102
- package/src/cloud-connection/device-agent.ts +7 -4
- package/src/cloud-connection/live-updates-handler.ts +79 -86
- package/src/cloud-connection/messages.ts +22 -1
- 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 +4 -1
- package/src/index.ts +2 -2
- 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 +82 -31
- package/src/subcommands/app/index.ts +4 -4
- package/src/subcommands/device/clean.ts +4 -4
- package/src/subcommands/device/device.ts +13 -13
- package/src/subcommands/login.ts +1 -9
- package/src/util/directories.ts +31 -29
- package/src/util/fetch-with-timeout.ts +18 -0
- package/src/util/logger.ts +2 -0
- package/src/util/require-logged-in-and-paid-plan.ts +16 -0
- package/lib/cloud-connection/app-install-status.d.ts +0 -16
- package/lib/cloud-connection/app-install-status.d.ts.map +0 -1
- package/lib/cloud-connection/app-install-status.js +0 -53
- package/lib/cloud-connection/app-install-status.js.map +0 -1
- package/src/cloud-connection/app-install-status.ts +0 -62
|
@@ -8,13 +8,15 @@ import { getAppLogs } from '../application-control';
|
|
|
8
8
|
import { logger } from '../util/logger';
|
|
9
9
|
import sleep from '../util/sleep';
|
|
10
10
|
import { Publisher } from './publisher';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getAppInstallStatusMessage,
|
|
13
|
+
getAppStateMessage,
|
|
14
|
+
getDeviceStatsMessage
|
|
15
|
+
} from './messages';
|
|
12
16
|
import { AgentMessageTypeValue } from '@alwaysai/device-agent-schemas/lib/constants';
|
|
13
|
-
import { AppInstallStatusManager } from './app-install-status';
|
|
14
17
|
|
|
15
18
|
export class LiveUpdatesHandler {
|
|
16
19
|
private publisher: Publisher;
|
|
17
|
-
private appInstallStatusMgr: AppInstallStatusManager;
|
|
18
20
|
|
|
19
21
|
private liveUpdatesTimeout: ReturnType<typeof setTimeout>;
|
|
20
22
|
private liveUpdatesAlive = {
|
|
@@ -32,14 +34,6 @@ export class LiveUpdatesHandler {
|
|
|
32
34
|
private appLogStreams = new Set<string>();
|
|
33
35
|
private appInstallStatuses = new Set<string>();
|
|
34
36
|
|
|
35
|
-
constructor(
|
|
36
|
-
publisher: Publisher,
|
|
37
|
-
appInstallStatusMgr: AppInstallStatusManager
|
|
38
|
-
) {
|
|
39
|
-
this.publisher = publisher;
|
|
40
|
-
this.appInstallStatusMgr = appInstallStatusMgr;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
37
|
private async startAppLogStream(projectId: string) {
|
|
44
38
|
logger.info(`Starting log stream for ${projectId}`);
|
|
45
39
|
this.appLogStreams.add(projectId);
|
|
@@ -77,50 +71,19 @@ export class LiveUpdatesHandler {
|
|
|
77
71
|
});
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const appInstallStatusMessage = {
|
|
85
|
-
messageType: keyMirrors.agentMessageType.app_install_status,
|
|
86
|
-
appInstallStatus
|
|
87
|
-
};
|
|
88
|
-
return appInstallStatusMessage;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
public async startPublishingLiveUpdates<T extends any[]>(
|
|
92
|
-
messageType: AgentMessageTypeValue,
|
|
93
|
-
getMessageData: (...args: T) => Promise<DeviceAgentMessagePayload>,
|
|
94
|
-
args: T
|
|
95
|
-
) {
|
|
96
|
-
logger.info(`Turned on live updates for ${messageType}`);
|
|
97
|
-
// eslint-disable-next-line no-constant-condition
|
|
98
|
-
while (true) {
|
|
99
|
-
try {
|
|
100
|
-
const message = await getMessageData(...args);
|
|
101
|
-
this.publisher.publishToClient(message);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
logger.error(
|
|
104
|
-
`Error publishing live updates for ${messageType}: ${e.message}`
|
|
105
|
-
);
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
if (!this.continuePublishing(messageType, ...args)) {
|
|
109
|
-
logger.info(`Turned off live updates for ${messageType}`);
|
|
110
|
-
break;
|
|
111
|
-
}
|
|
112
|
-
await sleep(this.getLiveUpdatesInterval(messageType));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private continuePublishing(flag: AgentMessageTypeValue, data?: any): boolean {
|
|
74
|
+
private continuePublishing(
|
|
75
|
+
flag: AgentMessageTypeValue,
|
|
76
|
+
projectId?: string
|
|
77
|
+
): boolean {
|
|
117
78
|
switch (flag) {
|
|
118
79
|
case keyMirrors.agentMessageType.device_stats:
|
|
119
80
|
case keyMirrors.agentMessageType.app_state:
|
|
120
81
|
return this.liveUpdatesAlive[flag];
|
|
121
82
|
case keyMirrors.agentMessageType.app_install_status: {
|
|
122
|
-
|
|
123
|
-
|
|
83
|
+
if (!projectId) {
|
|
84
|
+
throw new Error(`Project ID not provided to continuePublishing!`);
|
|
85
|
+
}
|
|
86
|
+
return this.appInstallStatuses.has(projectId);
|
|
124
87
|
}
|
|
125
88
|
default:
|
|
126
89
|
logger.error(`Unrecognized publishable flag ${flag}`);
|
|
@@ -158,28 +121,73 @@ export class LiveUpdatesHandler {
|
|
|
158
121
|
}, 600000); // 10 min
|
|
159
122
|
}
|
|
160
123
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
124
|
+
private async startPublishingLiveUpdates<T extends any[]>(
|
|
125
|
+
messageType: AgentMessageTypeValue,
|
|
126
|
+
getMessageData: (...args: T) => Promise<DeviceAgentMessagePayload>,
|
|
127
|
+
args: T,
|
|
128
|
+
projectId?: string
|
|
129
|
+
) {
|
|
130
|
+
logger.info(`Turned on live updates for ${messageType}`);
|
|
131
|
+
// eslint-disable-next-line no-constant-condition
|
|
132
|
+
while (true) {
|
|
133
|
+
try {
|
|
134
|
+
const message = await getMessageData(...args);
|
|
135
|
+
this.publisher.publishToClient(message);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
logger.error(
|
|
138
|
+
`Error publishing live updates for ${messageType}: ${e.message}`
|
|
139
|
+
);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (!this.continuePublishing(messageType, projectId)) {
|
|
143
|
+
logger.info(`Turned off live updates for ${messageType}`);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
await sleep(this.getLiveUpdatesInterval(messageType));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/*=================================================================
|
|
151
|
+
Public interface
|
|
152
|
+
=================================================================*/
|
|
153
|
+
|
|
154
|
+
constructor(publisher: Publisher) {
|
|
155
|
+
this.publisher = publisher;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public async enableAppInstallStatus(props: {
|
|
159
|
+
projectId: string;
|
|
160
|
+
appReleaseHash: string;
|
|
177
161
|
}) {
|
|
162
|
+
const { projectId, appReleaseHash } = props;
|
|
163
|
+
this.liveUpdatesAlive.app_install_status = true;
|
|
164
|
+
this.appInstallStatuses.add(projectId);
|
|
165
|
+
// Don't wait for this call to finish since it loops until disabled
|
|
166
|
+
void this.startPublishingLiveUpdates(
|
|
167
|
+
keyMirrors.agentMessageType.app_install_status,
|
|
168
|
+
getAppInstallStatusMessage,
|
|
169
|
+
[keyMirrors.appInstallStatus.in_progress, '', appReleaseHash],
|
|
170
|
+
projectId
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async disableAppInstallStatus(props: { projectId: string }) {
|
|
175
|
+
const { projectId } = props;
|
|
176
|
+
this.appInstallStatuses.delete(projectId);
|
|
177
|
+
|
|
178
|
+
if (this.appInstallStatuses.size === 0) {
|
|
179
|
+
this.liveUpdatesAlive.app_install_status = false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public async handleToggles(toggles: LiveUpdatesToggles) {
|
|
184
|
+
const { deviceStats, appState, appLogs } = toggles;
|
|
178
185
|
this.restartLiveUpdatesTimeout();
|
|
179
186
|
if (deviceStats !== undefined) {
|
|
180
187
|
this.liveUpdatesAlive.device_stats = deviceStats;
|
|
181
188
|
if (deviceStats) {
|
|
182
|
-
this
|
|
189
|
+
// Don't wait for this call to finish since it loops until disabled
|
|
190
|
+
void this.startPublishingLiveUpdates(
|
|
183
191
|
keyMirrors.agentMessageType.device_stats,
|
|
184
192
|
getDeviceStatsMessage,
|
|
185
193
|
[]
|
|
@@ -190,7 +198,8 @@ export class LiveUpdatesHandler {
|
|
|
190
198
|
if (appState !== undefined) {
|
|
191
199
|
this.liveUpdatesAlive.app_state = appState;
|
|
192
200
|
if (appState) {
|
|
193
|
-
this
|
|
201
|
+
// Don't wait for this call to finish since it loops until disabled
|
|
202
|
+
void this.startPublishingLiveUpdates(
|
|
194
203
|
keyMirrors.agentMessageType.app_state,
|
|
195
204
|
getAppStateMessage,
|
|
196
205
|
[]
|
|
@@ -200,27 +209,11 @@ export class LiveUpdatesHandler {
|
|
|
200
209
|
|
|
201
210
|
if (appLogs !== undefined) {
|
|
202
211
|
if (appLogs.toggle) {
|
|
203
|
-
this
|
|
212
|
+
// Don't wait for this call to finish since it loops until disabled
|
|
213
|
+
void this.startAppLogStream(appLogs.projectId);
|
|
204
214
|
} else {
|
|
205
215
|
this.appLogStreams.delete(appLogs.projectId);
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
|
-
|
|
209
|
-
if (appInstallStatus !== undefined) {
|
|
210
|
-
if (appInstallStatus.toggle) {
|
|
211
|
-
this.liveUpdatesAlive.app_install_status = appInstallStatus.toggle;
|
|
212
|
-
this.appInstallStatuses.add(appInstallStatus.appReleaseHash);
|
|
213
|
-
this.startPublishingLiveUpdates(
|
|
214
|
-
keyMirrors.agentMessageType.app_install_status,
|
|
215
|
-
this.getAppInstallStatusMessage,
|
|
216
|
-
[appInstallStatus.appReleaseHash]
|
|
217
|
-
);
|
|
218
|
-
} else {
|
|
219
|
-
this.appInstallStatuses.delete(appInstallStatus.appReleaseHash);
|
|
220
|
-
}
|
|
221
|
-
if (this.appInstallStatuses.size === 0) {
|
|
222
|
-
this.liveUpdatesAlive.app_install_status = false;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
218
|
}
|
|
226
219
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AppInstallStatusMessage,
|
|
3
|
+
AppStatePacket,
|
|
4
|
+
keyMirrors
|
|
5
|
+
} from '@alwaysai/device-agent-schemas';
|
|
6
|
+
import { AppInstallStatusValue } from '@alwaysai/device-agent-schemas/lib/constants';
|
|
2
7
|
import { getAppStatus } from '../application-control';
|
|
3
8
|
import {
|
|
4
9
|
getCpuUtil,
|
|
@@ -22,6 +27,22 @@ export async function getAppStateMessage() {
|
|
|
22
27
|
return appStatePackage;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
export async function getAppInstallStatusMessage(
|
|
31
|
+
status: AppInstallStatusValue,
|
|
32
|
+
message: string,
|
|
33
|
+
appReleaseHash: string
|
|
34
|
+
): Promise<AppInstallStatusMessage> {
|
|
35
|
+
const appInstallStatusMessage: AppInstallStatusMessage = {
|
|
36
|
+
messageType: keyMirrors.agentMessageType.app_install_status,
|
|
37
|
+
appInstallStatus: {
|
|
38
|
+
status,
|
|
39
|
+
message,
|
|
40
|
+
appReleaseHash
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return appInstallStatusMessage;
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
export async function getDeviceStatsMessage() {
|
|
26
47
|
const cpuUsage = await getCpuUtil();
|
|
27
48
|
const diskUtil = await getDiskUtil();
|
|
@@ -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
|
+
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
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { AppConfig } from '@alwaysai/app-configuration-schemas';
|
|
2
|
+
import { readAppCfgFile } from '../application-control';
|
|
3
|
+
import { Publisher } from './publisher';
|
|
4
|
+
import { ShadowHandler } from './shadow-handler';
|
|
5
|
+
|
|
6
|
+
jest.mock('../application-control');
|
|
7
|
+
jest.mock('./publisher');
|
|
8
|
+
const mockClient = jest.fn();
|
|
9
|
+
const clientId = 'test-client';
|
|
10
|
+
const projectId1 = 'test-project';
|
|
11
|
+
const projectId2 = 'test-project-2';
|
|
12
|
+
|
|
13
|
+
describe('Test Shadow Handler', () => {
|
|
14
|
+
let publisher: Publisher;
|
|
15
|
+
let shadowHandler: ShadowHandler;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
publisher = new Publisher(mockClient, clientId);
|
|
19
|
+
shadowHandler = new ShadowHandler(clientId, publisher);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test.skip('reject buffer payload', async () => {
|
|
23
|
+
//FIXME: Invalid input is silently ignored, need input validation
|
|
24
|
+
expect(async () => {
|
|
25
|
+
await shadowHandler.handleShadowTopic({
|
|
26
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
27
|
+
payload: Buffer.from('test-payload')
|
|
28
|
+
});
|
|
29
|
+
}).toThrow(Error);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('ignore message from self', async () => {
|
|
33
|
+
const ogAppCfg1: AppConfig = {
|
|
34
|
+
scripts: {
|
|
35
|
+
start: 'python app.py'
|
|
36
|
+
},
|
|
37
|
+
models: {
|
|
38
|
+
'alwaysai/mobilenet_ssd': 2
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
|
|
42
|
+
|
|
43
|
+
const appCfg1: AppConfig = {
|
|
44
|
+
scripts: {
|
|
45
|
+
start: 'python app.py'
|
|
46
|
+
},
|
|
47
|
+
models: {
|
|
48
|
+
'alwaysai/yolo_v3': 4
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const payload = {
|
|
52
|
+
[projectId1]: {
|
|
53
|
+
appConfig: JSON.stringify(appCfg1)
|
|
54
|
+
},
|
|
55
|
+
clientToken: clientId
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
59
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
60
|
+
payload
|
|
61
|
+
});
|
|
62
|
+
expect(appCfgUpdates.length).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('handle shadow get response with updated models', async () => {
|
|
66
|
+
const ogAppCfg1: AppConfig = {
|
|
67
|
+
scripts: {
|
|
68
|
+
start: 'python app.py'
|
|
69
|
+
},
|
|
70
|
+
models: {
|
|
71
|
+
'alwaysai/mobilenet_ssd': 3
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
|
|
76
|
+
const appCfg1: AppConfig = {
|
|
77
|
+
scripts: {
|
|
78
|
+
start: 'python app.py'
|
|
79
|
+
},
|
|
80
|
+
models: {
|
|
81
|
+
'alwaysai/yolo_v3': 4
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const payload = {
|
|
86
|
+
delta: {
|
|
87
|
+
[projectId1]: {
|
|
88
|
+
appConfig: JSON.stringify(appCfg1)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
94
|
+
topic: shadowHandler.shadowTopics.projects.getAccepted,
|
|
95
|
+
payload
|
|
96
|
+
});
|
|
97
|
+
expect(appCfgUpdates.length).toBe(1);
|
|
98
|
+
expect(appCfgUpdates[0]).toEqual({
|
|
99
|
+
projectId: projectId1,
|
|
100
|
+
newAppCfg: appCfg1,
|
|
101
|
+
updatedModels: {
|
|
102
|
+
'alwaysai/yolo_v3': 4
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('handle shadow delta without app config', async () => {
|
|
108
|
+
const ogAppCfg1: AppConfig = {
|
|
109
|
+
scripts: {
|
|
110
|
+
start: 'python app.py'
|
|
111
|
+
},
|
|
112
|
+
models: {
|
|
113
|
+
'alwaysai/mobilenet_ssd': 2
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
|
|
117
|
+
|
|
118
|
+
const payload = {
|
|
119
|
+
[projectId1]: {}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
123
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
124
|
+
payload
|
|
125
|
+
});
|
|
126
|
+
expect(appCfgUpdates.length).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('handle shadow delta with updated models', async () => {
|
|
130
|
+
const ogAppCfg1: AppConfig = {
|
|
131
|
+
scripts: {
|
|
132
|
+
start: 'python app.py'
|
|
133
|
+
},
|
|
134
|
+
models: {
|
|
135
|
+
'alwaysai/mobilenet_ssd': 2
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
|
|
140
|
+
const appCfg1: AppConfig = {
|
|
141
|
+
scripts: {
|
|
142
|
+
start: 'python app.py'
|
|
143
|
+
},
|
|
144
|
+
models: {
|
|
145
|
+
'alwaysai/mobilenet_ssd': 3,
|
|
146
|
+
'alwaysai/yolo_v4': 5
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const payload = {
|
|
151
|
+
[projectId1]: {
|
|
152
|
+
appConfig: JSON.stringify(appCfg1)
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
157
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
158
|
+
payload
|
|
159
|
+
});
|
|
160
|
+
expect(appCfgUpdates.length).toBe(1);
|
|
161
|
+
expect(appCfgUpdates[0]).toEqual({
|
|
162
|
+
projectId: projectId1,
|
|
163
|
+
newAppCfg: appCfg1,
|
|
164
|
+
updatedModels: {
|
|
165
|
+
'alwaysai/mobilenet_ssd': 3,
|
|
166
|
+
'alwaysai/yolo_v4': 5
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('handle shadow delta with updated models for two apps', async () => {
|
|
172
|
+
const ogAppCfg1: AppConfig = {
|
|
173
|
+
scripts: {
|
|
174
|
+
start: 'python app.py'
|
|
175
|
+
},
|
|
176
|
+
models: {
|
|
177
|
+
'alwaysai/mobilenet_ssd': 2
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
jest.mocked(readAppCfgFile).mockResolvedValueOnce(ogAppCfg1);
|
|
181
|
+
const ogAppCfg2: AppConfig = {
|
|
182
|
+
scripts: {
|
|
183
|
+
start: 'python app.py'
|
|
184
|
+
},
|
|
185
|
+
models: {
|
|
186
|
+
'alwaysai/yolo_v4': 5
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
jest.mocked(readAppCfgFile).mockResolvedValueOnce(ogAppCfg2);
|
|
190
|
+
|
|
191
|
+
const appCfg1: AppConfig = {
|
|
192
|
+
scripts: {
|
|
193
|
+
start: 'python app.py'
|
|
194
|
+
},
|
|
195
|
+
models: {
|
|
196
|
+
'alwaysai/mobilenet_ssd': 3,
|
|
197
|
+
'alwaysai/yolo_v4': 5
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const appCfg2: AppConfig = {
|
|
201
|
+
scripts: {
|
|
202
|
+
start: 'python app.py'
|
|
203
|
+
},
|
|
204
|
+
models: {
|
|
205
|
+
'alwaysai/yolo_v4': 5,
|
|
206
|
+
'alwaysai/human_pose': 7
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const payload = {
|
|
210
|
+
[projectId1]: {
|
|
211
|
+
appConfig: JSON.stringify(appCfg1)
|
|
212
|
+
},
|
|
213
|
+
[projectId2]: {
|
|
214
|
+
appConfig: JSON.stringify(appCfg2)
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
219
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
220
|
+
payload
|
|
221
|
+
});
|
|
222
|
+
expect(appCfgUpdates.length).toBe(2);
|
|
223
|
+
expect(appCfgUpdates[0]).toEqual({
|
|
224
|
+
projectId: projectId1,
|
|
225
|
+
newAppCfg: appCfg1,
|
|
226
|
+
updatedModels: {
|
|
227
|
+
'alwaysai/mobilenet_ssd': 3,
|
|
228
|
+
'alwaysai/yolo_v4': 5
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
expect(appCfgUpdates[1]).toEqual({
|
|
232
|
+
projectId: projectId2,
|
|
233
|
+
newAppCfg: appCfg2,
|
|
234
|
+
updatedModels: {
|
|
235
|
+
'alwaysai/human_pose': 7
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('handle shadow delta with updated analytics', async () => {
|
|
241
|
+
const ogAppCfg1: AppConfig = {
|
|
242
|
+
scripts: {
|
|
243
|
+
start: 'python app.py'
|
|
244
|
+
},
|
|
245
|
+
models: {}
|
|
246
|
+
};
|
|
247
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
|
|
248
|
+
|
|
249
|
+
const appCfg1: AppConfig = {
|
|
250
|
+
scripts: {
|
|
251
|
+
start: 'python app.py'
|
|
252
|
+
},
|
|
253
|
+
models: {},
|
|
254
|
+
analytics: {
|
|
255
|
+
enable_cloud_publish: true
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const payload = {
|
|
260
|
+
[projectId1]: {
|
|
261
|
+
appConfig: JSON.stringify(appCfg1)
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
266
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
267
|
+
payload
|
|
268
|
+
});
|
|
269
|
+
expect(appCfgUpdates.length).toBe(1);
|
|
270
|
+
expect(appCfgUpdates[0]).toEqual({
|
|
271
|
+
projectId: projectId1,
|
|
272
|
+
newAppCfg: appCfg1
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('handle shadow delta with invalid app config', async () => {
|
|
277
|
+
const ogAppCfg1: AppConfig = {
|
|
278
|
+
scripts: {
|
|
279
|
+
start: 'python app.py'
|
|
280
|
+
},
|
|
281
|
+
models: {
|
|
282
|
+
'alwaysai/mobilenet_ssd': 2
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
|
|
287
|
+
const appCfg1 = {
|
|
288
|
+
scripts: {
|
|
289
|
+
start: 'python app.py'
|
|
290
|
+
},
|
|
291
|
+
models: {
|
|
292
|
+
'alwaysai/mobilenet_ssd': '3',
|
|
293
|
+
'alwaysai/yolo_v4': '5'
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const payload = {
|
|
298
|
+
[projectId1]: {
|
|
299
|
+
appConfig: JSON.stringify(appCfg1)
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const appCfgUpdates = await shadowHandler.handleShadowTopic({
|
|
304
|
+
topic: shadowHandler.shadowTopics.projects.updateDelta,
|
|
305
|
+
payload
|
|
306
|
+
});
|
|
307
|
+
expect(appCfgUpdates.length).toBe(0);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test.skip('publish app state', async () => {
|
|
311
|
+
// FIXME: For some reason publisher is not being called...
|
|
312
|
+
const testAppCfg: AppConfig = {
|
|
313
|
+
scripts: {
|
|
314
|
+
start: ''
|
|
315
|
+
},
|
|
316
|
+
models: {}
|
|
317
|
+
};
|
|
318
|
+
jest.mocked(readAppCfgFile).mockResolvedValue(testAppCfg);
|
|
319
|
+
|
|
320
|
+
await shadowHandler.publishAppState(projectId1);
|
|
321
|
+
expect(jest.mocked(readAppCfgFile)).toBeCalledWith({ projectId1 });
|
|
322
|
+
const packet = {
|
|
323
|
+
state: {
|
|
324
|
+
reported: {
|
|
325
|
+
[projectId1]: { appConfig: JSON.stringify(testAppCfg) }
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
clientToken: clientId
|
|
329
|
+
};
|
|
330
|
+
expect(jest.mocked(publisher.publish)).toBeCalledWith(
|
|
331
|
+
shadowHandler.shadowTopics.projects.update,
|
|
332
|
+
JSON.stringify(packet)
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('get shadow updates', async () => {
|
|
337
|
+
shadowHandler.getShadowUpdates();
|
|
338
|
+
expect(jest.mocked(publisher.publish)).toBeCalledWith(
|
|
339
|
+
shadowHandler.shadowTopics.projects.get,
|
|
340
|
+
JSON.stringify({
|
|
341
|
+
clientToken: clientId
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('delete project shadow', async () => {
|
|
347
|
+
shadowHandler.deleteProjectShadow(projectId1);
|
|
348
|
+
const packet = {
|
|
349
|
+
state: {
|
|
350
|
+
reported: {
|
|
351
|
+
[projectId1]: null
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
clientToken: clientId
|
|
355
|
+
};
|
|
356
|
+
expect(jest.mocked(publisher.publish)).toBeCalledWith(
|
|
357
|
+
shadowHandler.shadowTopics.projects.update,
|
|
358
|
+
JSON.stringify(packet)
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|