@alwaysai/device-agent 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/application-control/environment-variables.d.ts +1 -1
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +3 -2
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +80 -59
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +2 -4
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.test.js +1 -1
- package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
- package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
- package/lib/cloud-connection/passthrough-handler.js +3 -6
- package/lib/cloud-connection/passthrough-handler.js.map +1 -1
- package/lib/cloud-connection/publisher.d.ts +5 -4
- package/lib/cloud-connection/publisher.d.ts.map +1 -1
- package/lib/cloud-connection/publisher.js +9 -8
- package/lib/cloud-connection/publisher.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts +1 -0
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +5 -6
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.test.js +9 -0
- package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +10 -0
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -0
- package/lib/cloud-connection/transaction-manager.js +41 -0
- package/lib/cloud-connection/transaction-manager.js.map +1 -0
- package/lib/cloud-connection/transaction-manager.test.d.ts +2 -0
- package/lib/cloud-connection/transaction-manager.test.d.ts.map +1 -0
- package/lib/cloud-connection/transaction-manager.test.js +63 -0
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -0
- package/lib/device-control/device-control.d.ts +35 -24
- package/lib/device-control/device-control.d.ts.map +1 -1
- package/lib/device-control/device-control.js +46 -11
- package/lib/device-control/device-control.js.map +1 -1
- package/lib/subcommands/app/env-vars.d.ts.map +1 -1
- package/lib/subcommands/app/env-vars.js +15 -9
- package/lib/subcommands/app/env-vars.js.map +1 -1
- package/lib/subcommands/device/clean.d.ts.map +1 -1
- package/lib/subcommands/device/clean.js +14 -12
- package/lib/subcommands/device/clean.js.map +1 -1
- package/lib/subcommands/device/device.d.ts +1 -0
- package/lib/subcommands/device/device.d.ts.map +1 -1
- package/lib/subcommands/device/device.js +23 -16
- package/lib/subcommands/device/device.js.map +1 -1
- package/lib/subcommands/device/index.js +1 -1
- package/lib/subcommands/device/index.js.map +1 -1
- package/lib/util/logger.d.ts.map +1 -1
- package/lib/util/logger.js +1 -1
- package/lib/util/logger.js.map +1 -1
- package/lib/util/safe-rimraf.d.ts +2 -0
- package/lib/util/safe-rimraf.d.ts.map +1 -0
- package/lib/util/safe-rimraf.js +16 -0
- package/lib/util/safe-rimraf.js.map +1 -0
- package/package.json +3 -2
- package/readme.md +1 -0
- package/src/application-control/environment-variables.ts +1 -1
- package/src/cloud-connection/device-agent-cloud-connection.ts +124 -80
- package/src/cloud-connection/live-updates-handler.test.ts +2 -2
- package/src/cloud-connection/live-updates-handler.ts +2 -8
- package/src/cloud-connection/passthrough-handler.ts +8 -7
- package/src/cloud-connection/publisher.ts +27 -10
- package/src/cloud-connection/shadow-handler.test.ts +9 -0
- package/src/cloud-connection/shadow-handler.ts +6 -14
- package/src/cloud-connection/transaction-manager.test.ts +73 -0
- package/src/cloud-connection/transaction-manager.ts +43 -0
- package/src/device-control/device-control.ts +50 -12
- package/src/subcommands/app/env-vars.ts +17 -10
- package/src/subcommands/device/clean.ts +22 -13
- package/src/subcommands/device/device.ts +31 -20
- package/src/subcommands/device/index.ts +2 -2
- package/src/util/logger.ts +15 -10
- package/src/util/safe-rimraf.ts +14 -0
- package/lib/cloud-connection/transaction-queue.d.ts +0 -12
- package/lib/cloud-connection/transaction-queue.d.ts.map +0 -1
- package/lib/cloud-connection/transaction-queue.js +0 -38
- package/lib/cloud-connection/transaction-queue.js.map +0 -1
- package/lib/cloud-connection/transaction-queue.test.d.ts +0 -2
- package/lib/cloud-connection/transaction-queue.test.d.ts.map +0 -1
- package/lib/cloud-connection/transaction-queue.test.js +0 -46
- package/lib/cloud-connection/transaction-queue.test.js.map +0 -1
- package/src/cloud-connection/transaction-queue.test.ts +0 -55
- package/src/cloud-connection/transaction-queue.ts +0 -40
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
2
2
|
import { Publisher } from './publisher';
|
|
3
3
|
|
|
4
|
+
global.setTimeout = jest.fn() as unknown as typeof setTimeout;
|
|
5
|
+
|
|
4
6
|
const testTrueToggles = {
|
|
5
7
|
deviceStats: true,
|
|
6
8
|
appState: true
|
|
@@ -15,8 +17,6 @@ const mockClient = jest.fn();
|
|
|
15
17
|
const clientId = 'test-client';
|
|
16
18
|
const emptyTxId = '';
|
|
17
19
|
|
|
18
|
-
jest.spyOn(global, 'setTimeout');
|
|
19
|
-
|
|
20
20
|
// NOTE: this was the way I found to mock private class functions
|
|
21
21
|
const mockStartPublishingLiveUpdates = jest.spyOn(
|
|
22
22
|
LiveUpdatesHandler.prototype as any,
|
|
@@ -69,7 +69,7 @@ export class LiveUpdatesHandler {
|
|
|
69
69
|
logChunk: logStr
|
|
70
70
|
};
|
|
71
71
|
const message = await buildAppLogsMessage(payload, this.clientId);
|
|
72
|
-
this.publisher.publishToClient(message);
|
|
72
|
+
this.publisher.publishToClient(message, logger.silly);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
readable.on('error', (error) => {
|
|
@@ -153,14 +153,8 @@ export class LiveUpdatesHandler {
|
|
|
153
153
|
const payload: ToClientMessagePayload = await payloadBuilderFunction(
|
|
154
154
|
...args
|
|
155
155
|
);
|
|
156
|
-
logger.debug(
|
|
157
|
-
`payload returned from builder: ${JSON.stringify(payload)}`
|
|
158
|
-
);
|
|
159
156
|
const message = await messageBuilderFunction(payload, txId);
|
|
160
|
-
logger.
|
|
161
|
-
`message returned from builder: ${JSON.stringify(message)}`
|
|
162
|
-
);
|
|
163
|
-
this.publisher.publishToClient(message);
|
|
157
|
+
this.publisher.publishToClient(message, logger.silly);
|
|
164
158
|
} catch (e) {
|
|
165
159
|
logger.error(
|
|
166
160
|
`Error publishing live updates for ${messageType}: ${e.message}`
|
|
@@ -92,22 +92,23 @@ function processPublish(passthroughHandler: PassthroughHandler) {
|
|
|
92
92
|
);
|
|
93
93
|
break;
|
|
94
94
|
case 'heartbeat':
|
|
95
|
-
logger.debug(`Recieved heartbeat packet: ${packet}`);
|
|
96
95
|
passthroughHandler.channel.ack(msg);
|
|
97
|
-
logger.debug(
|
|
96
|
+
logger.debug(
|
|
97
|
+
`Heartbeat package received & acknowledged: ${packet}`
|
|
98
|
+
);
|
|
98
99
|
break;
|
|
99
100
|
default:
|
|
100
|
-
logger.debug(`Recieved unknown 'action' packet: ${packet}`);
|
|
101
101
|
passthroughHandler.channel.ack(msg);
|
|
102
|
-
logger.debug(
|
|
102
|
+
logger.debug(
|
|
103
|
+
`Unknown 'action' package received & acknowledged: ${packet}`
|
|
104
|
+
);
|
|
103
105
|
break;
|
|
104
106
|
}
|
|
105
107
|
} else {
|
|
108
|
+
passthroughHandler.channel.ack(msg);
|
|
106
109
|
logger.debug(
|
|
107
|
-
`Received a RabbitMQ
|
|
110
|
+
`Received & acknowledged a RabbitMQ Package of unknown structure: ${parsedPacket}`
|
|
108
111
|
);
|
|
109
|
-
passthroughHandler.channel.ack(msg);
|
|
110
|
-
logger.debug(`Package of unknown structure acknowledged`);
|
|
111
112
|
}
|
|
112
113
|
} catch (e) {
|
|
113
114
|
logger.error(`There was a problem parsing RabbitMQ packet ${e}`);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getToClientTopic,
|
|
6
6
|
getToCloudTopic
|
|
7
7
|
} from '@alwaysai/device-agent-schemas';
|
|
8
|
+
import * as winston from 'winston';
|
|
8
9
|
|
|
9
10
|
export class Publisher {
|
|
10
11
|
private client: any;
|
|
@@ -19,8 +20,20 @@ export class Publisher {
|
|
|
19
20
|
this.toCloudTopic = getToCloudTopic(this.clientId);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
public publish(
|
|
23
|
+
public publish(
|
|
24
|
+
topic: string,
|
|
25
|
+
payload: string,
|
|
26
|
+
customLogger: winston.LeveledLogMethod = logger.debug
|
|
27
|
+
) {
|
|
23
28
|
// TODO: topic validation
|
|
29
|
+
// By default, log the published message at debug level, unless otherwise specified
|
|
30
|
+
customLogger(
|
|
31
|
+
`Publishing message:\nTopic: ${topic}\nMessage: ${JSON.stringify(
|
|
32
|
+
JSON.parse(payload),
|
|
33
|
+
null,
|
|
34
|
+
2
|
|
35
|
+
)}`
|
|
36
|
+
);
|
|
24
37
|
this.client.publish(topic, payload, (err: any) => {
|
|
25
38
|
if (err) {
|
|
26
39
|
logger.error(
|
|
@@ -52,21 +65,25 @@ export class Publisher {
|
|
|
52
65
|
|
|
53
66
|
public publishDeviceAgentMessage(
|
|
54
67
|
topic: string,
|
|
55
|
-
message: ToClientMessage | ToCloudMessage
|
|
68
|
+
message: ToClientMessage | ToCloudMessage,
|
|
69
|
+
logger?: winston.LeveledLogMethod
|
|
56
70
|
) {
|
|
57
71
|
const messageStr = JSON.stringify(message);
|
|
58
|
-
|
|
59
|
-
`Publishing message:\n${JSON.stringify({ topic, message }, null, 2)}`
|
|
60
|
-
);
|
|
61
|
-
this.publish(topic, messageStr);
|
|
72
|
+
this.publish(topic, messageStr, logger);
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
public publishToClient(
|
|
65
|
-
|
|
75
|
+
public publishToClient(
|
|
76
|
+
message: ToClientMessage,
|
|
77
|
+
logger?: winston.LeveledLogMethod
|
|
78
|
+
) {
|
|
79
|
+
this.publishDeviceAgentMessage(this.toClientTopic, message, logger);
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
public publishToCloud(
|
|
82
|
+
public publishToCloud(
|
|
83
|
+
message: ToCloudMessage,
|
|
84
|
+
logger?: winston.LeveledLogMethod
|
|
85
|
+
) {
|
|
69
86
|
// Can edit topic field in message here if we want
|
|
70
|
-
this.publishDeviceAgentMessage(this.toCloudTopic, message);
|
|
87
|
+
this.publishDeviceAgentMessage(this.toCloudTopic, message, logger);
|
|
71
88
|
}
|
|
72
89
|
}
|
|
@@ -113,6 +113,7 @@ describe('Test Shadow Handler', () => {
|
|
|
113
113
|
expect(updates.length).toBe(1);
|
|
114
114
|
expect(updates[0]).toEqual({
|
|
115
115
|
projectId: projectId1,
|
|
116
|
+
txId: expect.any(String),
|
|
116
117
|
appCfgUpdate: {
|
|
117
118
|
newAppCfg: appCfg1,
|
|
118
119
|
updatedModels: {
|
|
@@ -157,6 +158,7 @@ describe('Test Shadow Handler', () => {
|
|
|
157
158
|
expect(updates.length).toBe(1);
|
|
158
159
|
expect(updates[0]).toEqual({
|
|
159
160
|
projectId: projectId1,
|
|
161
|
+
txId: expect.any(String),
|
|
160
162
|
appCfgUpdate: {
|
|
161
163
|
newAppCfg: appCfg1,
|
|
162
164
|
updatedModels: {
|
|
@@ -222,6 +224,7 @@ describe('Test Shadow Handler', () => {
|
|
|
222
224
|
expect(updates.length).toBe(2);
|
|
223
225
|
expect(updates[0]).toEqual({
|
|
224
226
|
projectId: projectId1,
|
|
227
|
+
txId: expect.any(String),
|
|
225
228
|
appCfgUpdate: {
|
|
226
229
|
newAppCfg: appCfg1,
|
|
227
230
|
updatedModels: {
|
|
@@ -232,6 +235,7 @@ describe('Test Shadow Handler', () => {
|
|
|
232
235
|
});
|
|
233
236
|
expect(updates[1]).toEqual({
|
|
234
237
|
projectId: projectId2,
|
|
238
|
+
txId: expect.any(String),
|
|
235
239
|
appCfgUpdate: {
|
|
236
240
|
newAppCfg: appCfg2,
|
|
237
241
|
updatedModels: {
|
|
@@ -274,6 +278,7 @@ describe('Test Shadow Handler', () => {
|
|
|
274
278
|
expect(updates.length).toBe(1);
|
|
275
279
|
expect(updates[0]).toEqual({
|
|
276
280
|
projectId: projectId1,
|
|
281
|
+
txId: expect.any(String),
|
|
277
282
|
appCfgUpdate: {
|
|
278
283
|
newAppCfg: appCfg1
|
|
279
284
|
}
|
|
@@ -337,6 +342,7 @@ describe('Test Shadow Handler', () => {
|
|
|
337
342
|
expect(updates.length).toBe(1);
|
|
338
343
|
expect(updates[0]).toEqual({
|
|
339
344
|
projectId: projectId1,
|
|
345
|
+
txId: expect.any(String),
|
|
340
346
|
envVarUpdate: {
|
|
341
347
|
envVars: envVars1
|
|
342
348
|
}
|
|
@@ -361,6 +367,7 @@ describe('Test Shadow Handler', () => {
|
|
|
361
367
|
expect(updates.length).toBe(1);
|
|
362
368
|
expect(updates[0]).toEqual({
|
|
363
369
|
projectId: projectId1,
|
|
370
|
+
txId: expect.any(String),
|
|
364
371
|
envVarUpdate: {
|
|
365
372
|
envVars: envVars1
|
|
366
373
|
}
|
|
@@ -391,12 +398,14 @@ describe('Test Shadow Handler', () => {
|
|
|
391
398
|
expect(updates.length).toBe(2);
|
|
392
399
|
expect(updates[0]).toEqual({
|
|
393
400
|
projectId: projectId1,
|
|
401
|
+
txId: expect.any(String),
|
|
394
402
|
envVarUpdate: {
|
|
395
403
|
envVars: envVars1
|
|
396
404
|
}
|
|
397
405
|
});
|
|
398
406
|
expect(updates[1]).toEqual({
|
|
399
407
|
projectId: projectId2,
|
|
408
|
+
txId: expect.any(String),
|
|
400
409
|
envVarUpdate: {
|
|
401
410
|
envVars: envVars2
|
|
402
411
|
}
|
|
@@ -7,6 +7,7 @@ import { logger } from '../util/logger';
|
|
|
7
7
|
import { Publisher } from './publisher';
|
|
8
8
|
import { AppConfigModels, getAppCfgModelsDiff } from './shadow';
|
|
9
9
|
import { getSystemInformation } from '../device-control/device-control';
|
|
10
|
+
import { generateTxId } from '@alwaysai/device-agent-schemas';
|
|
10
11
|
|
|
11
12
|
export interface ShadowTopics {
|
|
12
13
|
projects: {
|
|
@@ -35,6 +36,7 @@ export type EnvVarUpdate = {
|
|
|
35
36
|
|
|
36
37
|
export type ShadowUpdate = {
|
|
37
38
|
projectId: string;
|
|
39
|
+
txId: string;
|
|
38
40
|
appCfgUpdate?: AppConfigUpdate;
|
|
39
41
|
envVarUpdate?: EnvVarUpdate;
|
|
40
42
|
};
|
|
@@ -77,7 +79,9 @@ export class ShadowHandler {
|
|
|
77
79
|
|
|
78
80
|
for (const projectId of deltaKeys) {
|
|
79
81
|
const projectShadow = delta[projectId];
|
|
80
|
-
|
|
82
|
+
// For incoming shadow updates, there will be no TxID, so it needs to be generated here.
|
|
83
|
+
const txId = generateTxId();
|
|
84
|
+
const shadowUpdate: ShadowUpdate = { projectId, txId };
|
|
81
85
|
|
|
82
86
|
if (projectShadow.appConfig) {
|
|
83
87
|
const newAppCfg = JSON.parse(projectShadow.appConfig);
|
|
@@ -140,7 +144,7 @@ export class ShadowHandler {
|
|
|
140
144
|
case this.shadowTopics.projects.updateDelta:
|
|
141
145
|
if (clientToken === this.clientId) {
|
|
142
146
|
logger.debug(
|
|
143
|
-
`Ignoring
|
|
147
|
+
`Ignoring delta caused by Device Agent: ${JSON.stringify(
|
|
144
148
|
{ topic, payload },
|
|
145
149
|
null,
|
|
146
150
|
2
|
|
@@ -179,9 +183,6 @@ export class ShadowHandler {
|
|
|
179
183
|
clientToken: this.clientId
|
|
180
184
|
};
|
|
181
185
|
const topic = this.shadowTopics.systemInfo.update;
|
|
182
|
-
logger.debug(
|
|
183
|
-
`Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
|
|
184
|
-
);
|
|
185
186
|
this.publisher.publish(topic, JSON.stringify(packet));
|
|
186
187
|
}
|
|
187
188
|
|
|
@@ -200,9 +201,6 @@ export class ShadowHandler {
|
|
|
200
201
|
clientToken: this.clientId
|
|
201
202
|
};
|
|
202
203
|
const topic = this.shadowTopics.projects.update;
|
|
203
|
-
logger.debug(
|
|
204
|
-
`Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
|
|
205
|
-
);
|
|
206
204
|
this.publisher.publish(topic, JSON.stringify(packet));
|
|
207
205
|
}
|
|
208
206
|
|
|
@@ -211,9 +209,6 @@ export class ShadowHandler {
|
|
|
211
209
|
const packet = {
|
|
212
210
|
clientToken: this.clientId
|
|
213
211
|
};
|
|
214
|
-
logger.debug(
|
|
215
|
-
`Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
|
|
216
|
-
);
|
|
217
212
|
this.publisher.publish(topic, JSON.stringify(packet));
|
|
218
213
|
}
|
|
219
214
|
|
|
@@ -232,9 +227,6 @@ export class ShadowHandler {
|
|
|
232
227
|
},
|
|
233
228
|
clientToken: this.clientId
|
|
234
229
|
};
|
|
235
|
-
logger.debug(
|
|
236
|
-
`Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
|
|
237
|
-
);
|
|
238
230
|
this.publisher.publish(topic, JSON.stringify(packet));
|
|
239
231
|
}
|
|
240
232
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { generateTxId } from '@alwaysai/device-agent-schemas';
|
|
2
|
+
import { TransactionManager } from './transaction-manager';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
describe('Test Transaction Manager', () => {
|
|
6
|
+
let txnMgr: TransactionManager;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
txnMgr = new TransactionManager();
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function generateRandomProjectId() {
|
|
14
|
+
return uuidv4();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('Add a new transaction', async () => {
|
|
18
|
+
const txId = generateTxId();
|
|
19
|
+
const projectId = generateRandomProjectId();
|
|
20
|
+
txnMgr.addTransaction(txId, projectId);
|
|
21
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
22
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('Add multiple transactions to different projects', async () => {
|
|
26
|
+
const numTransactions = 3;
|
|
27
|
+
const projectTxIdList: any = [];
|
|
28
|
+
for (let i = 0; i < numTransactions; i++) {
|
|
29
|
+
const txId = generateTxId();
|
|
30
|
+
const projectId = generateRandomProjectId();
|
|
31
|
+
txnMgr.addTransaction(txId, projectId);
|
|
32
|
+
projectTxIdList.push({ projectId, txId });
|
|
33
|
+
}
|
|
34
|
+
projectTxIdList.forEach(({ txId, projectId }) => {
|
|
35
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
36
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Attempt to add existing transaction to a project, results in failure', async () => {
|
|
41
|
+
const txId = generateTxId();
|
|
42
|
+
const projectId = generateRandomProjectId();
|
|
43
|
+
txnMgr.addTransaction(txId, projectId);
|
|
44
|
+
expect(() => txnMgr.addTransaction(txId, projectId)).toThrow(
|
|
45
|
+
`Transaction ID ${txId} already exists for this projectId ${projectId}!`
|
|
46
|
+
);
|
|
47
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
48
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('Attempt to add a transaction to a project with an ongoing transaction, results in failure', async () => {
|
|
52
|
+
const txId = generateTxId();
|
|
53
|
+
const txId2 = generateTxId();
|
|
54
|
+
const projectId = generateRandomProjectId();
|
|
55
|
+
txnMgr.addTransaction(txId, projectId);
|
|
56
|
+
expect(() => txnMgr.addTransaction(txId2, projectId)).toThrow(
|
|
57
|
+
`This Project ID ${projectId} already has an ongoing transaction!`
|
|
58
|
+
);
|
|
59
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
60
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
61
|
+
expect(txnMgr.getProjectFromTransaction(txId2)).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('Test remove transaction from queue', async () => {
|
|
65
|
+
const txId = generateTxId();
|
|
66
|
+
const projectId = generateRandomProjectId();
|
|
67
|
+
txnMgr.addTransaction(txId, projectId);
|
|
68
|
+
txnMgr.completeTransaction(txId);
|
|
69
|
+
|
|
70
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toBeUndefined();
|
|
71
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class TransactionManager {
|
|
2
|
+
private txToProject: Record<string, string> = {};
|
|
3
|
+
private projectToTx: Record<string, string> = {};
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this.txToProject = {};
|
|
7
|
+
this.projectToTx = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public addTransaction(txId: string, projectId: string) {
|
|
11
|
+
// Check if the Transaction already exists
|
|
12
|
+
if (this.txToProject[txId]) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Transaction ID ${txId} already exists for this projectId ${projectId}!`
|
|
15
|
+
);
|
|
16
|
+
} else {
|
|
17
|
+
// Check if there is any ongoing Transactions for Project ID
|
|
18
|
+
if (this.projectToTx[projectId]) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`This Project ID ${projectId} already has an ongoing transaction!`
|
|
21
|
+
);
|
|
22
|
+
} else {
|
|
23
|
+
// Map the Transaction ID with Project ID
|
|
24
|
+
this.txToProject[txId] = projectId;
|
|
25
|
+
this.projectToTx[projectId] = txId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public getTransactionFromProject(projectId: string) {
|
|
31
|
+
return this.projectToTx[projectId];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public getProjectFromTransaction(txId: string) {
|
|
35
|
+
return this.txToProject[txId];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public completeTransaction(txId: string) {
|
|
39
|
+
const projectId = this.txToProject[txId];
|
|
40
|
+
delete this.txToProject[txId];
|
|
41
|
+
delete this.projectToTx[projectId];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { SystemInformationPayload } from '@alwaysai/device-agent-schemas';
|
|
2
2
|
import { logger } from '../util/logger';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import * as osu from 'node-os-utils';
|
|
4
|
+
import * as si from 'systeminformation';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
|
|
8
|
+
const exec_promise = promisify(exec);
|
|
7
9
|
|
|
8
10
|
// Device Stats
|
|
9
11
|
export async function getCpuDetails() {
|
|
@@ -16,6 +18,8 @@ export async function getCpuDetails() {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export async function getDiskDetails() {
|
|
21
|
+
// Types incorrectly specify diskname as required instead of optional
|
|
22
|
+
// @ts-expect-error
|
|
19
23
|
const driveInfo = await osu.drive.info();
|
|
20
24
|
return {
|
|
21
25
|
usedGb: parseFloat(driveInfo.usedGb),
|
|
@@ -26,8 +30,8 @@ export async function getDiskDetails() {
|
|
|
26
30
|
export async function getMemDetails() {
|
|
27
31
|
const memInfo = await osu.mem.info();
|
|
28
32
|
return {
|
|
29
|
-
usedMb:
|
|
30
|
-
freeMb:
|
|
33
|
+
usedMb: memInfo.usedMemMb,
|
|
34
|
+
freeMb: memInfo.freeMemMb
|
|
31
35
|
};
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -52,7 +56,7 @@ export async function getCpuInfo() {
|
|
|
52
56
|
vendor: cpuInfo.vendor,
|
|
53
57
|
model: cpuInfo.model,
|
|
54
58
|
cores: cpuInfo.cores,
|
|
55
|
-
physicalCores: cpuInfo.
|
|
59
|
+
physicalCores: cpuInfo.physicalCores,
|
|
56
60
|
efficiencyCores: cpuInfo.efficiencyCores,
|
|
57
61
|
processors: cpuInfo.processors
|
|
58
62
|
};
|
|
@@ -78,16 +82,19 @@ export async function getDeviceInfo() {
|
|
|
78
82
|
model: deviceInfo.model,
|
|
79
83
|
version: deviceInfo.version,
|
|
80
84
|
serial:
|
|
81
|
-
deviceInfo.serial && deviceInfo.serial !== '-'
|
|
85
|
+
deviceInfo.serial && deviceInfo.serial !== '-'
|
|
86
|
+
? deviceInfo.serial
|
|
87
|
+
: undefined,
|
|
82
88
|
virtual: deviceInfo.virtual
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
export async function getNetworkInfo() {
|
|
87
93
|
const networkInterfaces = await si.networkInterfaces();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
|
|
95
|
+
const defaultNetworkInterface = Array.isArray(networkInterfaces)
|
|
96
|
+
? networkInterfaces.filter((iface: any) => iface.ip4 !== '127.0.0.1')[0]
|
|
97
|
+
: networkInterfaces;
|
|
91
98
|
return {
|
|
92
99
|
ipv4Address: defaultNetworkInterface.ip4,
|
|
93
100
|
ipv6Address: defaultNetworkInterface.ip6,
|
|
@@ -95,6 +102,17 @@ export async function getNetworkInfo() {
|
|
|
95
102
|
};
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
export async function getPackageVersions() {
|
|
106
|
+
// eslint-disable-next-line
|
|
107
|
+
const agentJson = require('../../package.json');
|
|
108
|
+
// eslint-disable-next-line
|
|
109
|
+
const deviceAgentSchemasJson = require('../../node_modules/@alwaysai/device-agent-schemas/package.json');
|
|
110
|
+
return {
|
|
111
|
+
agent: agentJson.version,
|
|
112
|
+
deviceAgentSchemas: deviceAgentSchemasJson.version
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
export async function getSystemInformation(): Promise<SystemInformationPayload> {
|
|
99
117
|
try {
|
|
100
118
|
const systemInfo: SystemInformationPayload = {
|
|
@@ -102,7 +120,8 @@ export async function getSystemInformation(): Promise<SystemInformationPayload>
|
|
|
102
120
|
cpu: await getCpuInfo(),
|
|
103
121
|
disk: await getDiskInfo(),
|
|
104
122
|
device: await getDeviceInfo(),
|
|
105
|
-
network: await getNetworkInfo()
|
|
123
|
+
network: await getNetworkInfo(),
|
|
124
|
+
versions: await getPackageVersions()
|
|
106
125
|
};
|
|
107
126
|
return systemInfo;
|
|
108
127
|
} catch (e) {
|
|
@@ -110,3 +129,22 @@ export async function getSystemInformation(): Promise<SystemInformationPayload>
|
|
|
110
129
|
}
|
|
111
130
|
return {};
|
|
112
131
|
}
|
|
132
|
+
|
|
133
|
+
export async function reboot() {
|
|
134
|
+
try {
|
|
135
|
+
// FIXME: This command must always be run with sudo. The user must enable access
|
|
136
|
+
// in the sudoers file for it to work.
|
|
137
|
+
// If passwordless access is not set, will prompt user for password. Use
|
|
138
|
+
// timeout to break out of the prompt
|
|
139
|
+
const result = await exec_promise('sudo /sbin/shutdown -r now', {
|
|
140
|
+
timeout: 5000
|
|
141
|
+
});
|
|
142
|
+
logger.info(result.stdout.trim());
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Could not reboot device. You may need to add passwordless access to '/sbin/shutdown'. ${JSON.stringify(
|
|
146
|
+
err
|
|
147
|
+
)}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { EnvVars, getAllEnvs } from '../../application-control';
|
|
8
8
|
import { DeviceAgentCloudConnection } from '../../cloud-connection/device-agent-cloud-connection';
|
|
9
9
|
import sleep from '../../util/sleep';
|
|
10
|
+
import { logger } from '../../util/logger';
|
|
10
11
|
|
|
11
12
|
export const getAllEnvsCliLeaf = CliLeaf({
|
|
12
13
|
name: 'get-all-envs',
|
|
@@ -49,25 +50,31 @@ export const setEnvCliLeaf = CliLeaf({
|
|
|
49
50
|
if (nameVal.length !== 2) {
|
|
50
51
|
throw new CliTerseError(`Invalid argument: ${arg}`);
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
const value = nameVal[1] === '' ? null : nameVal[1];
|
|
54
|
+
envVars[service][nameVal[0]] = value;
|
|
53
55
|
});
|
|
54
56
|
const deviceAgent = new DeviceAgentCloudConnection();
|
|
55
57
|
await deviceAgent.setupHandlers();
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
version: 3,
|
|
61
|
-
timestamp: 0,
|
|
59
|
+
// Update the shadow as a client
|
|
60
|
+
const topic = deviceAgent.getShadowTopics().projects.update;
|
|
61
|
+
const packet = {
|
|
62
62
|
state: {
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
desired: {
|
|
64
|
+
[project]: {
|
|
65
|
+
envVars
|
|
66
|
+
}
|
|
65
67
|
}
|
|
66
68
|
},
|
|
67
|
-
clientToken: '
|
|
69
|
+
clientToken: 'client'
|
|
68
70
|
};
|
|
71
|
+
logger.debug(
|
|
72
|
+
`Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
|
|
73
|
+
);
|
|
74
|
+
deviceAgent.publisher.publish(topic, JSON.stringify(packet));
|
|
75
|
+
// Sleep for extra time to ensure time for shadow response
|
|
76
|
+
await sleep(10000);
|
|
69
77
|
|
|
70
|
-
await deviceAgent.handleMessage(topic, message);
|
|
71
78
|
while (deviceAgent.isCmdInProgress(project)) {
|
|
72
79
|
await sleep(1000);
|
|
73
80
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CliLeaf } from '@alwaysai/alwayscli';
|
|
2
|
-
import { rimraf } from 'rimraf';
|
|
3
2
|
import { logger } from '../../util/logger';
|
|
4
3
|
import { AgentConfigFile } from '../../infrastructure/agent-config';
|
|
5
4
|
import { DeviceConfigFile } from 'alwaysai/lib/core/device';
|
|
6
5
|
import {
|
|
7
6
|
APP_ROOT,
|
|
8
7
|
CREDENTIALS_FILE_PATH,
|
|
9
|
-
DEVICE_AGENT_CFG_PATH
|
|
8
|
+
DEVICE_AGENT_CFG_PATH,
|
|
9
|
+
DEVICE_AGENT_DOCKER_COMPOSE_PATH
|
|
10
10
|
} from '../../util/directories';
|
|
11
11
|
import {
|
|
12
12
|
checkRabbitMQContainerRunning,
|
|
@@ -14,33 +14,42 @@ import {
|
|
|
14
14
|
} from '../../local-connection/rabbitmq-connection';
|
|
15
15
|
import { stopApp } from '../../application-control';
|
|
16
16
|
import { LOCAL_CERT_AND_KEY_DIR } from 'alwaysai/lib/paths';
|
|
17
|
+
import safeRimraf from '../../util/safe-rimraf';
|
|
17
18
|
|
|
18
19
|
export const cleanCliLeaf = CliLeaf({
|
|
19
20
|
name: 'clean',
|
|
20
21
|
description: 'Remove all provisioning files',
|
|
21
22
|
async action(_, opts) {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
logger.info('Removing provisioning files.');
|
|
24
|
+
try {
|
|
25
|
+
if (await checkRabbitMQContainerRunning()) {
|
|
26
|
+
await stopRabbitMQContainer();
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
logger.error(
|
|
30
|
+
`You may need to manually stop the container by running docker-compose down in the following directory: ${DEVICE_AGENT_DOCKER_COMPOSE_PATH}`
|
|
31
|
+
);
|
|
32
|
+
logger.debug(`Error in checking / stopping RabbitMQ container: ${e}`);
|
|
24
33
|
}
|
|
25
|
-
|
|
26
|
-
await
|
|
34
|
+
|
|
35
|
+
await safeRimraf(DEVICE_AGENT_CFG_PATH);
|
|
36
|
+
|
|
27
37
|
logger.debug('Checking for alwaysAI applications still running');
|
|
28
38
|
const apps = await AgentConfigFile().getApps();
|
|
29
39
|
for (const app of apps) {
|
|
30
40
|
const { projectId } = app;
|
|
31
41
|
await stopApp({ projectId });
|
|
32
42
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await rimraf(LOCAL_CERT_AND_KEY_DIR);
|
|
43
|
+
await safeRimraf(LOCAL_CERT_AND_KEY_DIR);
|
|
44
|
+
|
|
36
45
|
logger.debug(`Removing ${AgentConfigFile().path}`);
|
|
37
46
|
AgentConfigFile().remove();
|
|
38
47
|
logger.debug(`Removing ${DeviceConfigFile().path}`);
|
|
39
48
|
DeviceConfigFile().remove();
|
|
40
|
-
|
|
41
|
-
await
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
|
|
50
|
+
await safeRimraf(CREDENTIALS_FILE_PATH);
|
|
51
|
+
await safeRimraf(APP_ROOT);
|
|
52
|
+
|
|
44
53
|
logger.info('Device configuration cleaned');
|
|
45
54
|
}
|
|
46
55
|
});
|