@alwaysai/device-agent 0.0.7 → 0.0.9
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.d.ts.map +1 -1
- package/lib/application-control/backup.js +2 -0
- package/lib/application-control/backup.js.map +1 -1
- package/lib/application-control/config.d.ts +17 -0
- package/lib/application-control/config.d.ts.map +1 -0
- package/lib/application-control/config.js +62 -0
- package/lib/application-control/config.js.map +1 -0
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/application-control/environment-variables.js +6 -14
- package/lib/application-control/environment-variables.js.map +1 -1
- package/lib/application-control/index.d.ts +2 -1
- package/lib/application-control/index.d.ts.map +1 -1
- package/lib/application-control/index.js +6 -1
- package/lib/application-control/index.js.map +1 -1
- package/lib/application-control/install.d.ts +16 -10
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +95 -57
- package/lib/application-control/install.js.map +1 -1
- package/lib/application-control/models.d.ts +3 -0
- package/lib/application-control/models.d.ts.map +1 -1
- package/lib/application-control/models.js +96 -20
- package/lib/application-control/models.js.map +1 -1
- package/lib/application-control/status.d.ts +3 -2
- package/lib/application-control/status.d.ts.map +1 -1
- package/lib/application-control/status.js +8 -6
- package/lib/application-control/status.js.map +1 -1
- package/lib/application-control/utils.d.ts +5 -0
- package/lib/application-control/utils.d.ts.map +1 -1
- package/lib/application-control/utils.js +47 -13
- package/lib/application-control/utils.js.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +42 -15
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +357 -195
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/infrastructure/agent-config.d.ts.map +1 -1
- package/lib/infrastructure/agent-config.js +7 -18
- package/lib/infrastructure/agent-config.js.map +1 -1
- package/lib/infrastructure/agent-config.test.js +47 -0
- package/lib/infrastructure/agent-config.test.js.map +1 -1
- package/lib/subcommands/app/app.js +1 -1
- package/lib/subcommands/app/app.js.map +1 -1
- package/lib/subcommands/login.d.ts.map +1 -1
- package/lib/subcommands/login.js +4 -3
- package/lib/subcommands/login.js.map +1 -1
- package/lib/util/copy-dir.d.ts.map +1 -1
- package/lib/util/copy-dir.js +3 -1
- package/lib/util/copy-dir.js.map +1 -1
- package/lib/util/run-in-dir.d.ts +2 -0
- package/lib/util/run-in-dir.d.ts.map +1 -0
- package/lib/util/run-in-dir.js +17 -0
- package/lib/util/run-in-dir.js.map +1 -0
- package/lib/util/sleep.d.ts +2 -0
- package/lib/util/sleep.d.ts.map +1 -0
- package/lib/util/sleep.js +9 -0
- package/lib/util/sleep.js.map +1 -0
- package/package.json +4 -3
- package/src/application-control/backup.ts +3 -0
- package/src/application-control/config.ts +61 -0
- package/src/application-control/environment-variables.ts +6 -12
- package/src/application-control/index.ts +5 -0
- package/src/application-control/install.ts +147 -68
- package/src/application-control/models.ts +136 -23
- package/src/application-control/status.ts +19 -9
- package/src/application-control/utils.ts +58 -13
- package/src/cloud-connection/device-agent-cloud-connection.ts +459 -216
- package/src/infrastructure/agent-config.test.ts +56 -0
- package/src/infrastructure/agent-config.ts +10 -19
- package/src/subcommands/app/app.ts +1 -1
- package/src/subcommands/login.ts +6 -4
- package/src/util/copy-dir.ts +3 -1
- package/src/util/run-in-dir.ts +15 -0
- package/src/util/sleep.ts +5 -0
- package/lib/util/run-cli-cmd.d.ts +0 -5
- package/lib/util/run-cli-cmd.d.ts.map +0 -1
- package/lib/util/run-cli-cmd.js +0 -24
- package/lib/util/run-cli-cmd.js.map +0 -1
- package/src/util/run-cli-cmd.ts +0 -18
|
@@ -5,178 +5,212 @@ import {
|
|
|
5
5
|
getCertificateFilePath,
|
|
6
6
|
getRootCertificateFilePath,
|
|
7
7
|
} from '../util/directories';
|
|
8
|
+
import sleep from '../util/sleep';
|
|
8
9
|
import {
|
|
9
10
|
startApp,
|
|
10
11
|
stopApp,
|
|
11
12
|
restartApp,
|
|
12
|
-
getAppStatus,
|
|
13
13
|
getAppLogs,
|
|
14
|
+
getAppStatus,
|
|
14
15
|
} from '../application-control/status';
|
|
15
|
-
import { installApp } from '../application-control/install';
|
|
16
|
-
import { getCpuUtil, getDiskUtil, getMemUtil } from '../device-control/device-control';
|
|
17
16
|
import {
|
|
18
|
-
|
|
17
|
+
getInstalledApps,
|
|
18
|
+
installApp,
|
|
19
|
+
uninstallApp,
|
|
20
|
+
} from '../application-control/install';
|
|
21
|
+
import {
|
|
22
|
+
keyMirrors,
|
|
23
|
+
validateClientMessage,
|
|
24
|
+
DeviceAgentMessagePayload,
|
|
25
|
+
AppInstallStatusPacket,
|
|
26
|
+
AppStateControlPacket,
|
|
27
|
+
AppVersionControlPacket,
|
|
28
|
+
LiveUpdatesToggles,
|
|
29
|
+
AppLogsPacket,
|
|
30
|
+
AppStatePacket,
|
|
31
|
+
LiveUpdatesToggleMessage,
|
|
19
32
|
DeviceStatsMessage,
|
|
20
|
-
AppState,
|
|
21
33
|
AppStateMessage,
|
|
22
|
-
ActionMessage,
|
|
23
|
-
AppLogs,
|
|
24
34
|
AppLogsMessage,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
AppInstallStatusMessage,
|
|
36
|
+
SignedUrlsRequestMessage,
|
|
37
|
+
DeviceAgentMessage,
|
|
38
|
+
ClientMessage,
|
|
39
|
+
AppDetailsPacket,
|
|
28
40
|
} from '@alwaysai/device-agent-schemas';
|
|
29
41
|
import { getDeviceId } from '../util/get-device-id';
|
|
42
|
+
import { JsSpawner, logger } from 'alwaysai/lib/util';
|
|
43
|
+
import { getCpuUtil, getDiskUtil, getMemUtil } from '../device-control/device-control';
|
|
30
44
|
import { AgentConfigFile } from '../infrastructure/agent-config';
|
|
45
|
+
import { buildApp, getAppConfig, getAppDir } from '../application-control/utils';
|
|
46
|
+
import { updateModelsWithPresignedUrls } from '../application-control/models';
|
|
47
|
+
import { updateAppConfig } from '../application-control/config';
|
|
31
48
|
|
|
32
49
|
export class DeviceAgentCloudConnection {
|
|
33
50
|
private clientId = getDeviceId();
|
|
34
51
|
private host = getIoTCoreEndpointUrl();
|
|
35
|
-
private
|
|
36
|
-
private
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
private appInstallStatus: AppInstallStatusPacket;
|
|
53
|
+
private liveUpdatesTimeout: ReturnType<typeof setTimeout>;
|
|
54
|
+
private liveUpdatesAlive = {
|
|
55
|
+
[keyMirrors.agentMessageType.device_stats]: false,
|
|
56
|
+
[keyMirrors.agentMessageType.app_state]: false,
|
|
57
|
+
[keyMirrors.agentMessageType.app_logs]: false,
|
|
58
|
+
};
|
|
59
|
+
private liveUpdatesSleepIntervals = {
|
|
60
|
+
[keyMirrors.agentMessageType.device_stats]: 5000,
|
|
61
|
+
[keyMirrors.agentMessageType.app_state]: 5000,
|
|
62
|
+
[keyMirrors.agentMessageType.app_logs]: 5000,
|
|
63
|
+
[keyMirrors.agentMessageType.app_install_status]: 5000,
|
|
64
|
+
};
|
|
65
|
+
private appLogStreams = new Set<string>();
|
|
66
|
+
private deviceType = 'aai-device';
|
|
67
|
+
private readonly shadowPrefix = `$aws/things/${this.clientId}/shadow/name/`;
|
|
68
|
+
private readonly shadowTopics = {
|
|
69
|
+
projects: {
|
|
70
|
+
updateDelta: `${this.shadowPrefix}projects/update/delta`,
|
|
71
|
+
getAccepted: `${this.shadowPrefix}projects/get/accepted`,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
private readonly toCloudTopic = `topic/to_cloud/${this.deviceType}/${this.clientId}`;
|
|
75
|
+
private readonly toClientTopic = `topic/to_client/${this.deviceType}/${this.clientId}`;
|
|
76
|
+
private readonly toDeviceTopic = `topic/to_device/${this.deviceType}/${this.clientId}`;
|
|
44
77
|
|
|
45
|
-
|
|
46
|
-
this.device = awsIot.device({
|
|
47
|
-
keyPath: getPrivateKeyFilePath(),
|
|
48
|
-
certPath: getCertificateFilePath(),
|
|
49
|
-
caPath: getRootCertificateFilePath(),
|
|
50
|
-
clientId: this.clientId,
|
|
51
|
-
host: this.host,
|
|
52
|
-
});
|
|
78
|
+
// device shadow utils
|
|
53
79
|
|
|
54
|
-
|
|
55
|
-
this.
|
|
80
|
+
public getShadowPrefix() {
|
|
81
|
+
return this.shadowPrefix;
|
|
56
82
|
}
|
|
57
83
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
private async handleNamedShadowUpdate({ payload }: { payload: string }) {
|
|
85
|
+
const delta = JSON.parse(payload);
|
|
86
|
+
const deltaKeys = Object.keys(delta);
|
|
87
|
+
|
|
88
|
+
for (const projectId of deltaKeys) {
|
|
89
|
+
const projectShadow = delta[projectId];
|
|
90
|
+
if (projectShadow.appConfig) {
|
|
91
|
+
const appConfig = projectShadow.appConfig;
|
|
92
|
+
const appDir = getAppDir(projectId);
|
|
93
|
+
await updateAppConfig(projectId, appConfig);
|
|
94
|
+
|
|
95
|
+
if (appConfig.models) {
|
|
96
|
+
this.publishCloudRequest({
|
|
97
|
+
messageType: keyMirrors.agentMessageType.signed_urls_request,
|
|
98
|
+
modelsOnlyUrlsRequest: {
|
|
99
|
+
projectId,
|
|
100
|
+
models: appConfig.models,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
61
104
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
105
|
+
if (appConfig.scripts && !appConfig.models) {
|
|
106
|
+
const appState = await getAppStatus({ projectId });
|
|
65
107
|
|
|
66
|
-
|
|
67
|
-
return this.publishable;
|
|
68
|
-
}
|
|
108
|
+
await buildApp({ appDir });
|
|
69
109
|
|
|
70
|
-
|
|
71
|
-
|
|
110
|
+
if (
|
|
111
|
+
appState.services.length &&
|
|
112
|
+
appState.services[0].state !== keyMirrors.appState.stopped
|
|
113
|
+
) {
|
|
114
|
+
restartApp({ projectId });
|
|
115
|
+
}
|
|
116
|
+
await this.publishReportedState(projectId);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
72
120
|
}
|
|
73
121
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
private async startAppLogStream(projectId: string) {
|
|
123
|
+
this.appLogStreams.add(projectId);
|
|
124
|
+
const readable = await getAppLogs({
|
|
125
|
+
projectId,
|
|
126
|
+
args: ['--tail', '100', '--no-log-prefix'],
|
|
127
|
+
});
|
|
128
|
+
readable.on('data', (chunk: Buffer) => {
|
|
129
|
+
if (!this.appLogStreams.has(projectId)) {
|
|
130
|
+
// why doesn't typescript know about this function?
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
readable.destroy();
|
|
133
|
+
logger.info(`App log stream terminated for project ${projectId}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const logStr = chunk.toString();
|
|
137
|
+
const message = {
|
|
138
|
+
messageType: keyMirrors.agentMessageType.app_logs,
|
|
139
|
+
appLogs: {
|
|
140
|
+
projectId,
|
|
141
|
+
logChunk: logStr,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
const packet = this.buildMessagePacket(
|
|
145
|
+
this.getClientId(),
|
|
146
|
+
this.toClientTopic,
|
|
147
|
+
message,
|
|
148
|
+
);
|
|
149
|
+
this.publishMessage(this.toClientTopic, JSON.stringify(packet));
|
|
150
|
+
});
|
|
80
151
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
152
|
+
readable.on('error', (error) => {
|
|
153
|
+
logger.error(`App log stream terminated for project ${projectId}: ${error}`);
|
|
154
|
+
});
|
|
84
155
|
|
|
85
|
-
|
|
86
|
-
|
|
156
|
+
readable.on('finished', () => {
|
|
157
|
+
logger.info(`App logs finished piping for project ${projectId}`);
|
|
158
|
+
});
|
|
87
159
|
}
|
|
88
160
|
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
// must contain app release hash
|
|
162
|
+
private initAppInstallStatus(installationStatus: AppInstallStatusPacket) {
|
|
163
|
+
this.appInstallStatus = installationStatus;
|
|
91
164
|
}
|
|
92
165
|
|
|
93
|
-
|
|
94
|
-
|
|
166
|
+
private updateAppInstallStatus(
|
|
167
|
+
installationStatus: Omit<AppInstallStatusPacket, 'appReleaseHash'>,
|
|
168
|
+
) {
|
|
169
|
+
this.appInstallStatus.status = installationStatus.status;
|
|
170
|
+
this.appInstallStatus.message = installationStatus.message;
|
|
95
171
|
}
|
|
96
|
-
}
|
|
97
172
|
|
|
98
|
-
|
|
99
|
-
|
|
173
|
+
private getAppInstallStatus(): AppInstallStatusPacket {
|
|
174
|
+
return this.appInstallStatus;
|
|
175
|
+
}
|
|
100
176
|
|
|
101
|
-
|
|
177
|
+
// Message Builders
|
|
178
|
+
private buildMessagePacket(
|
|
179
|
+
deviceId: string,
|
|
102
180
|
topic: string,
|
|
103
|
-
payload:
|
|
104
|
-
|
|
105
|
-
| ActionMessage
|
|
106
|
-
| AppStateMessage
|
|
107
|
-
| AppLogsMessage
|
|
108
|
-
| InstallationStatusMessage,
|
|
109
|
-
): Promise<DeviceAgentMessage> {
|
|
181
|
+
payload: DeviceAgentMessagePayload,
|
|
182
|
+
): DeviceAgentMessage {
|
|
110
183
|
const packet = {
|
|
111
184
|
timestamp: new Date().toUTCString(),
|
|
112
|
-
deviceId
|
|
185
|
+
deviceId,
|
|
113
186
|
topic,
|
|
114
187
|
payload,
|
|
115
188
|
};
|
|
116
189
|
return packet;
|
|
117
190
|
}
|
|
118
191
|
|
|
119
|
-
async
|
|
120
|
-
const
|
|
192
|
+
private async getAppStateMessage(): Promise<AppStateMessage> {
|
|
193
|
+
const appStateMessage: AppStatePacket[] = [];
|
|
121
194
|
const apps = await AgentConfigFile().getReadyApps();
|
|
122
|
-
for (const app of apps) {
|
|
123
|
-
const projectId = app.projectId;
|
|
124
|
-
const readable = await getAppLogs({ projectId });
|
|
125
|
-
const logs: any[] = [];
|
|
126
|
-
readable.setEncoding('utf8');
|
|
127
|
-
for await (const chunk of readable) {
|
|
128
|
-
logs.push(chunk);
|
|
129
|
-
}
|
|
130
|
-
const appLogs = {
|
|
131
|
-
projectId,
|
|
132
|
-
logs,
|
|
133
|
-
};
|
|
134
|
-
appLogsList.push(appLogs);
|
|
135
|
-
}
|
|
136
|
-
const applicationStatePackage = {
|
|
137
|
-
applicationLogs: appLogsList,
|
|
138
|
-
};
|
|
139
|
-
return applicationStatePackage;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// * This was causing massive CPU overhead
|
|
143
|
-
// const publishAppLogs = setInterval(async function () {
|
|
144
|
-
// const appLogsMessage = await getAppLogsMessage();
|
|
145
|
-
// const topic = `device/${deviceAgent.getClientId()}/topic/application-management`;
|
|
146
|
-
// const appLogsPacket = await buildMessagePacket(topic, appLogsMessage);
|
|
147
|
-
|
|
148
|
-
// deviceAgent.publishMessage(topic, JSON.stringify({ appLogsPacket }));
|
|
149
|
-
// }, deviceAgent.getPublishInterval());
|
|
150
|
-
|
|
151
|
-
async function getAppStateMessage(): Promise<AppStateMessage> {
|
|
152
|
-
const appStateMessage: AppState[] = [];
|
|
153
|
-
const apps = await AgentConfigFile().getApps();
|
|
154
195
|
for (const app of apps) {
|
|
155
196
|
const projectId = app.projectId;
|
|
156
197
|
const status = await getAppStatus({ projectId });
|
|
157
198
|
appStateMessage.push(status);
|
|
158
199
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
200
|
+
const appStatePackage = {
|
|
201
|
+
messageType: keyMirrors.agentMessageType.app_state,
|
|
202
|
+
appState: appStateMessage,
|
|
161
203
|
};
|
|
162
|
-
return
|
|
204
|
+
return appStatePackage;
|
|
163
205
|
}
|
|
164
206
|
|
|
165
|
-
|
|
166
|
-
if (deviceAgent.getPublishable()) {
|
|
167
|
-
const topic = `${deviceAgent.cloudTopicPrefix}application-management`;
|
|
168
|
-
const appStateMessage = await getAppStateMessage();
|
|
169
|
-
const appStatePacket = await buildMessagePacket(topic, appStateMessage);
|
|
170
|
-
deviceAgent.publishMessage(topic, JSON.stringify({ appStatePacket }));
|
|
171
|
-
}
|
|
172
|
-
}, deviceAgent.getPublishInterval());
|
|
173
|
-
|
|
174
|
-
async function getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
|
|
207
|
+
private async getDeviceStatsMessage(): Promise<DeviceStatsMessage> {
|
|
175
208
|
const cpuUsage = await getCpuUtil();
|
|
176
209
|
const diskUtil = await getDiskUtil();
|
|
177
210
|
const memUtil = await getMemUtil();
|
|
178
211
|
|
|
179
212
|
const deviceStatsMessage = {
|
|
213
|
+
messageType: keyMirrors.agentMessageType.device_stats,
|
|
180
214
|
deviceStats: {
|
|
181
215
|
cpuUsage,
|
|
182
216
|
diskUtil,
|
|
@@ -186,137 +220,346 @@ export function runDeviceAgentCloudInterface() {
|
|
|
186
220
|
return deviceStatsMessage;
|
|
187
221
|
}
|
|
188
222
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
// must be arrow function due to this context when function is passed as param
|
|
224
|
+
private getAppInstallStatusMessage = async (): Promise<AppInstallStatusMessage> => {
|
|
225
|
+
const appInstallStatus = this.getAppInstallStatus();
|
|
226
|
+
const appInstallStatusMessage = {
|
|
227
|
+
messageType: keyMirrors.agentMessageType.app_install_status,
|
|
228
|
+
appInstallStatus,
|
|
229
|
+
};
|
|
230
|
+
return appInstallStatusMessage;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
private async startPublishingLiveUpdates(
|
|
234
|
+
topic: string,
|
|
235
|
+
messageType: string,
|
|
236
|
+
getMessageData: () => Promise<DeviceAgentMessagePayload>,
|
|
237
|
+
) {
|
|
238
|
+
while (true) {
|
|
239
|
+
try {
|
|
240
|
+
const message = await getMessageData();
|
|
241
|
+
const packet = this.buildMessagePacket(this.getClientId(), topic, message);
|
|
242
|
+
this.publishMessage(topic, JSON.stringify(packet));
|
|
243
|
+
} catch (e) {
|
|
244
|
+
logger.error(`Error publishing live updates for ${messageType}: ${e.message}`);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
if (!this.continuePublishing(messageType)) {
|
|
248
|
+
logger.info(`Turned off live updates for ${messageType}`);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
await sleep(this.getLiveUpdatesInterval(messageType));
|
|
195
252
|
}
|
|
196
|
-
}
|
|
253
|
+
}
|
|
197
254
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
255
|
+
private continuePublishing(flag: string): boolean {
|
|
256
|
+
switch (flag) {
|
|
257
|
+
case keyMirrors.agentMessageType.device_stats:
|
|
258
|
+
case keyMirrors.agentMessageType.app_state:
|
|
259
|
+
return this.liveUpdatesAlive[flag];
|
|
260
|
+
case keyMirrors.agentMessageType.app_install_status:
|
|
261
|
+
return this.appInstallStatus.status === keyMirrors.appInstallStatus.in_progress;
|
|
262
|
+
default:
|
|
263
|
+
logger.error(`Unrecognized publishable flag ${flag}`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private getLiveUpdatesInterval(flag: string): number {
|
|
269
|
+
const exists = this.liveUpdatesSleepIntervals[flag];
|
|
270
|
+
if (exists) {
|
|
271
|
+
return exists;
|
|
272
|
+
}
|
|
273
|
+
logger.error(`Unrecognized live updates flag ${flag}`);
|
|
274
|
+
return -1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private setLiveUpdates(toggles: LiveUpdatesToggles) {
|
|
278
|
+
if (toggles.deviceStats) {
|
|
279
|
+
this.liveUpdatesAlive.device_stats = toggles.deviceStats;
|
|
280
|
+
}
|
|
281
|
+
if (toggles.appState) {
|
|
282
|
+
this.liveUpdatesAlive.app_state = toggles.appState;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private handleAppStateControl(payload: AppStateControlPacket) {
|
|
287
|
+
const { baseCommand, projectId } = payload;
|
|
288
|
+
switch (baseCommand) {
|
|
289
|
+
case keyMirrors.appStateControl.start:
|
|
290
|
+
startApp({ projectId });
|
|
291
|
+
break;
|
|
292
|
+
case keyMirrors.appStateControl.stop:
|
|
293
|
+
stopApp({ projectId });
|
|
294
|
+
break;
|
|
295
|
+
case keyMirrors.appStateControl.restart:
|
|
296
|
+
restartApp({ projectId });
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private handleAppVersionControl(payload: AppVersionControlPacket) {
|
|
302
|
+
const { projectId, appReleaseHash } = payload;
|
|
303
|
+
const signedUrlsRequest = { projectId, appReleaseHash };
|
|
304
|
+
this.publishCloudRequest({
|
|
305
|
+
messageType: keyMirrors.agentMessageType.signed_urls_request,
|
|
306
|
+
signedUrlsRequest,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private handleDeviceCommand = async (packet: any) => {
|
|
311
|
+
// TODO
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
private handleAgentCommand(message: LiveUpdatesToggleMessage) {
|
|
315
|
+
switch (message.messageType) {
|
|
316
|
+
case keyMirrors.clientMessageType.live_state_updates:
|
|
317
|
+
this.liveUpdatesBroker(message.liveUpdatesToggles);
|
|
318
|
+
break;
|
|
319
|
+
default:
|
|
320
|
+
logger.error(`Invalid agent action message type from message '${message}'`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private restartLiveUpdatesTimeout() {
|
|
325
|
+
clearTimeout(this.liveUpdatesTimeout);
|
|
326
|
+
this.liveUpdatesTimeout = setTimeout(() => {
|
|
327
|
+
this.setLiveUpdates({
|
|
328
|
+
deviceStats: false,
|
|
329
|
+
appState: false,
|
|
330
|
+
});
|
|
331
|
+
this.appLogStreams.clear();
|
|
332
|
+
// TODO: Make constant, not hard coded
|
|
333
|
+
}, 600000); // 10 min
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async liveUpdatesBroker({
|
|
337
|
+
deviceStats,
|
|
338
|
+
appState,
|
|
339
|
+
appLogs,
|
|
340
|
+
}: {
|
|
341
|
+
deviceStats?: boolean;
|
|
342
|
+
appState?: boolean;
|
|
343
|
+
appLogs?: {
|
|
344
|
+
projectId: string;
|
|
345
|
+
toggle: boolean;
|
|
206
346
|
};
|
|
207
|
-
|
|
347
|
+
}) {
|
|
348
|
+
this.restartLiveUpdatesTimeout();
|
|
349
|
+
if (deviceStats !== undefined) {
|
|
350
|
+
this.liveUpdatesAlive.device_stats = deviceStats;
|
|
351
|
+
if (deviceStats) {
|
|
352
|
+
this.startPublishingLiveUpdates(
|
|
353
|
+
this.toClientTopic,
|
|
354
|
+
keyMirrors.agentMessageType.device_stats,
|
|
355
|
+
this.getDeviceStatsMessage,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (appState !== undefined) {
|
|
361
|
+
this.liveUpdatesAlive.app_state = appState;
|
|
362
|
+
if (appState) {
|
|
363
|
+
this.startPublishingLiveUpdates(
|
|
364
|
+
this.toClientTopic,
|
|
365
|
+
keyMirrors.agentMessageType.app_state,
|
|
366
|
+
this.getAppStateMessage,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (appLogs !== undefined) {
|
|
372
|
+
if (appLogs.toggle) {
|
|
373
|
+
this.startAppLogStream(appLogs.projectId);
|
|
374
|
+
} else {
|
|
375
|
+
this.appLogStreams.delete(appLogs.projectId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private async publishReportedState(projectId) {
|
|
381
|
+
const newAppCfg = await getAppConfig(projectId);
|
|
382
|
+
const packet = {
|
|
383
|
+
state: {
|
|
384
|
+
reported: {
|
|
385
|
+
[projectId]: { appConfig: newAppCfg },
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
this.publishMessage(`${this.shadowPrefix}projects/update`, JSON.stringify(packet));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
|
|
393
|
+
const topic = this.toCloudTopic;
|
|
394
|
+
const deviceRequestPacket = this.buildMessagePacket(
|
|
395
|
+
this.getClientId(),
|
|
208
396
|
topic,
|
|
209
|
-
|
|
397
|
+
payload,
|
|
210
398
|
);
|
|
211
|
-
|
|
399
|
+
this.publishMessage(topic, JSON.stringify({ deviceRequestPacket }));
|
|
400
|
+
}
|
|
212
401
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
402
|
+
// Public Methods
|
|
403
|
+
|
|
404
|
+
public device = awsIot.device;
|
|
405
|
+
|
|
406
|
+
constructor() {
|
|
407
|
+
this.device = awsIot.device({
|
|
408
|
+
keyPath: getPrivateKeyFilePath(),
|
|
409
|
+
certPath: getCertificateFilePath(),
|
|
410
|
+
caPath: getRootCertificateFilePath(),
|
|
411
|
+
clientId: this.clientId,
|
|
412
|
+
host: this.host,
|
|
218
413
|
});
|
|
219
|
-
deviceAgent.device.publish(topic, JSON.stringify({ installationStatusPacket }));
|
|
220
|
-
if (installationStatus.status !== InstallationStatusEnum.IN_PROGRESS) {
|
|
221
|
-
clearInterval(interval);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
414
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
415
|
+
this.device.subscribe(this.toDeviceTopic);
|
|
416
|
+
this.device.subscribe(this.shadowTopics.projects.getAccepted);
|
|
417
|
+
this.device.subscribe(this.shadowTopics.projects.updateDelta);
|
|
418
|
+
}
|
|
229
419
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
420
|
+
public getClientId(): string {
|
|
421
|
+
return this.clientId;
|
|
422
|
+
}
|
|
233
423
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
case 'install':
|
|
239
|
-
/**
|
|
240
|
-
{
|
|
241
|
-
"action": "install",
|
|
242
|
-
"install": {
|
|
243
|
-
"releaseHash": "7fb2a812f9e7aa193208dac353521965da50d755085162066c125592f1ed760b",
|
|
244
|
-
"projectId": "786e4686-a681-4cff-9e17-1e7d385c0fdb"
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
*/
|
|
248
|
-
if (type === 'response') {
|
|
249
|
-
deviceAgent.setInstallationStatus({
|
|
250
|
-
status: InstallationStatusEnum.IN_PROGRESS,
|
|
251
|
-
applicationReleaseHash: actionPayload.releaseHash as string,
|
|
252
|
-
});
|
|
424
|
+
public publishMessage(topic: string, message: string) {
|
|
425
|
+
// TODO: topic validation
|
|
426
|
+
this.device.publish(topic, message);
|
|
427
|
+
}
|
|
253
428
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
applicationReleaseHash: actionPayload.releaseHash as string,
|
|
266
|
-
});
|
|
267
|
-
} catch (e) {
|
|
268
|
-
const reason: string = e.message;
|
|
269
|
-
deviceAgent.setInstallationStatus({
|
|
270
|
-
status: InstallationStatusEnum.FAILURE,
|
|
271
|
-
reason,
|
|
272
|
-
applicationReleaseHash: actionPayload.releaseHash as string,
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
})();
|
|
276
|
-
break;
|
|
277
|
-
}
|
|
278
|
-
publishDeviceRequest(actionPayload);
|
|
429
|
+
public async handleClientMessage({
|
|
430
|
+
topic,
|
|
431
|
+
message,
|
|
432
|
+
}: {
|
|
433
|
+
topic: string;
|
|
434
|
+
message: ClientMessage;
|
|
435
|
+
}) {
|
|
436
|
+
const payload = message.payload;
|
|
437
|
+
switch (payload.messageType) {
|
|
438
|
+
case keyMirrors.clientMessageType.app_state_control: {
|
|
439
|
+
this.handleAppStateControl(payload.appStateControl);
|
|
279
440
|
break;
|
|
280
|
-
|
|
281
|
-
|
|
441
|
+
}
|
|
442
|
+
case keyMirrors.clientMessageType.app_version_control: {
|
|
443
|
+
this.handleAppVersionControl(payload.appVersionControl);
|
|
282
444
|
break;
|
|
283
|
-
|
|
284
|
-
|
|
445
|
+
}
|
|
446
|
+
case keyMirrors.clientMessageType.live_state_updates: {
|
|
447
|
+
this.handleAgentCommand(payload);
|
|
285
448
|
break;
|
|
286
|
-
|
|
287
|
-
|
|
449
|
+
}
|
|
450
|
+
case keyMirrors.clientMessageType.app_install_cloud_response: {
|
|
451
|
+
const { projectId, appReleaseHash, appInstallPayload, modelsInstallPayload } =
|
|
452
|
+
payload.appInstallCloudResponse;
|
|
453
|
+
|
|
454
|
+
this.initAppInstallStatus({
|
|
455
|
+
status: keyMirrors.appInstallStatus.in_progress,
|
|
456
|
+
appReleaseHash,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
this.startPublishingLiveUpdates(
|
|
460
|
+
this.toClientTopic,
|
|
461
|
+
keyMirrors.agentMessageType.app_install_status,
|
|
462
|
+
this.getAppInstallStatusMessage,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Install the app and models
|
|
466
|
+
try {
|
|
467
|
+
const signedUrlsPayload = {
|
|
468
|
+
appInstallPayload,
|
|
469
|
+
modelsInstallPayload,
|
|
470
|
+
};
|
|
471
|
+
await installApp({
|
|
472
|
+
projectId,
|
|
473
|
+
appReleaseHash,
|
|
474
|
+
signedUrlsPayload,
|
|
475
|
+
});
|
|
476
|
+
this.updateAppInstallStatus({
|
|
477
|
+
status: keyMirrors.appInstallStatus.success,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// update app config shadow for project
|
|
481
|
+
await this.publishReportedState(projectId);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
console.error(e);
|
|
484
|
+
const message: string = e.message;
|
|
485
|
+
|
|
486
|
+
// uninstall the failed app to put system back in good state
|
|
487
|
+
await uninstallApp({ projectId });
|
|
488
|
+
this.updateAppInstallStatus({
|
|
489
|
+
status: keyMirrors.appInstallStatus.failure,
|
|
490
|
+
message,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// delete shadow for project
|
|
494
|
+
this.publishMessage(`${this.shadowPrefix}${projectId}/delete`, '');
|
|
495
|
+
}
|
|
288
496
|
break;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
deviceAgent.restartPublishableTimeout();
|
|
497
|
+
}
|
|
498
|
+
case keyMirrors.clientMessageType.models_install_cloud_response: {
|
|
499
|
+
const { projectId, newModels } = payload.modelsInstallCloudResponse;
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
await updateModelsWithPresignedUrls(projectId, newModels);
|
|
503
|
+
|
|
504
|
+
await this.publishReportedState(projectId);
|
|
505
|
+
} catch (e) {
|
|
506
|
+
console.error(e);
|
|
507
|
+
}
|
|
301
508
|
break;
|
|
509
|
+
}
|
|
302
510
|
default:
|
|
303
|
-
|
|
511
|
+
logger.error(`Invalid Client Message '${JSON.stringify(payload)}'`);
|
|
304
512
|
}
|
|
305
|
-
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
public async handleShadowTopic({ topic, payload }: { topic: string; payload: string }) {
|
|
516
|
+
const shadowName = topic.split('/')[5];
|
|
517
|
+
const message = JSON.parse(payload);
|
|
518
|
+
if (topic === this.shadowTopics.projects.updateDelta) {
|
|
519
|
+
this.handleNamedShadowUpdate({ payload });
|
|
520
|
+
} else if (topic === this.shadowTopics.projects.getAccepted) {
|
|
521
|
+
if (message.delta) {
|
|
522
|
+
this.handleNamedShadowUpdate({
|
|
523
|
+
payload: JSON.stringify(message.delta),
|
|
524
|
+
});
|
|
525
|
+
} else {
|
|
526
|
+
console.log(`No delta updates in shadow ${shadowName}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function runDeviceAgentCloudInterface() {
|
|
533
|
+
const deviceAgent = new DeviceAgentCloudConnection();
|
|
306
534
|
|
|
307
535
|
deviceAgent.device.on('connect', function () {
|
|
308
536
|
deviceAgent.publishMessage('connection', deviceAgent.getClientId());
|
|
309
537
|
console.log('Device Agent has connected to the cloud');
|
|
538
|
+
|
|
539
|
+
// Get shadow updates
|
|
540
|
+
deviceAgent.publishMessage(`${deviceAgent.getShadowPrefix()}projects/get`, '');
|
|
310
541
|
});
|
|
311
542
|
|
|
312
543
|
deviceAgent.device.on('disconnect', function () {
|
|
313
544
|
console.log('Device Agent has been disconnected from the cloud');
|
|
314
545
|
});
|
|
315
546
|
|
|
316
|
-
deviceAgent.device.on('message', function (topic, payload) {
|
|
317
|
-
// ToDo: insert valdiation here for incoming messages. Maybe we will use a JSON schema for the message structure.
|
|
547
|
+
deviceAgent.device.on('message', function (topic: string, payload: string) {
|
|
318
548
|
try {
|
|
319
|
-
|
|
549
|
+
const jsonPacket = JSON.parse(payload);
|
|
550
|
+
if (jsonPacket.hasOwnProperty('state')) {
|
|
551
|
+
deviceAgent.handleShadowTopic({
|
|
552
|
+
topic,
|
|
553
|
+
payload: JSON.stringify(jsonPacket.state),
|
|
554
|
+
});
|
|
555
|
+
} else {
|
|
556
|
+
const valid = validateClientMessage(jsonPacket);
|
|
557
|
+
if (!valid) {
|
|
558
|
+
console.error(JSON.stringify(validateClientMessage.errors));
|
|
559
|
+
} else {
|
|
560
|
+
deviceAgent.handleClientMessage({ topic, message: jsonPacket });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
320
563
|
} catch (error) {
|
|
321
564
|
console.error(error);
|
|
322
565
|
}
|