@alwaysai/device-agent 1.4.0 → 1.5.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/config.js +2 -2
- package/lib/application-control/config.js.map +1 -1
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +1 -0
- package/lib/application-control/install.js.map +1 -1
- package/lib/application-control/models.d.ts +5 -0
- package/lib/application-control/models.d.ts.map +1 -1
- package/lib/application-control/models.js +24 -12
- package/lib/application-control/models.js.map +1 -1
- package/lib/application-control/status.d.ts.map +1 -1
- package/lib/application-control/status.js +10 -12
- package/lib/application-control/status.js.map +1 -1
- package/lib/application-control/utils.js +2 -2
- package/lib/application-control/utils.js.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +2 -2
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +35 -15
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts +3 -2
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +24 -21
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.test.js +132 -16
- package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
- package/lib/cloud-connection/passthrough-handler.d.ts +5 -3
- package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
- package/lib/cloud-connection/passthrough-handler.js +76 -62
- package/lib/cloud-connection/passthrough-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts +2 -0
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +5 -5
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +3 -0
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +11 -0
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +102 -0
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/device-control/device-control.d.ts +10 -2
- package/lib/device-control/device-control.d.ts.map +1 -1
- package/lib/device-control/device-control.js +86 -10
- package/lib/device-control/device-control.js.map +1 -1
- package/lib/docker/docker-compose.d.ts +14 -0
- package/lib/docker/docker-compose.d.ts.map +1 -0
- package/lib/docker/docker-compose.js +56 -0
- package/lib/docker/docker-compose.js.map +1 -0
- package/lib/index.js +2 -5
- package/lib/index.js.map +1 -1
- package/lib/infrastructure/agent-config.d.ts +45 -14
- package/lib/infrastructure/agent-config.d.ts.map +1 -1
- package/lib/infrastructure/agent-config.js +30 -15
- package/lib/infrastructure/agent-config.js.map +1 -1
- package/lib/infrastructure/agent-config.test.js +3 -0
- package/lib/infrastructure/agent-config.test.js.map +1 -1
- package/lib/local-connection/rabbitmq-connection.js +11 -11
- package/lib/local-connection/rabbitmq-connection.js.map +1 -1
- package/lib/secure-tunneling/secure-tunneling.d.ts +14 -22
- package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -1
- package/lib/secure-tunneling/secure-tunneling.js +34 -34
- package/lib/secure-tunneling/secure-tunneling.js.map +1 -1
- package/lib/secure-tunneling/secure-tunneling.test.js +18 -18
- package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -1
- package/lib/subcommands/device/clean.js +5 -5
- package/lib/subcommands/device/clean.js.map +1 -1
- package/lib/subcommands/device/get-info.d.ts +2 -0
- package/lib/subcommands/device/get-info.d.ts.map +1 -0
- package/lib/subcommands/device/get-info.js +36 -0
- package/lib/subcommands/device/get-info.js.map +1 -0
- package/lib/subcommands/device/index.d.ts.map +1 -1
- package/lib/subcommands/device/index.js +11 -2
- package/lib/subcommands/device/index.js.map +1 -1
- package/lib/subcommands/device/init.d.ts +5 -0
- package/lib/subcommands/device/init.d.ts.map +1 -0
- package/lib/subcommands/device/{device.js → init.js} +2 -41
- package/lib/subcommands/device/init.js.map +1 -0
- package/lib/subcommands/device/refresh.d.ts +2 -0
- package/lib/subcommands/device/refresh.d.ts.map +1 -0
- package/lib/subcommands/device/refresh.js +24 -0
- package/lib/subcommands/device/refresh.js.map +1 -0
- package/lib/subcommands/device/restart.d.ts +2 -0
- package/lib/subcommands/device/restart.d.ts.map +1 -0
- package/lib/subcommands/device/restart.js +14 -0
- package/lib/subcommands/device/restart.js.map +1 -0
- package/lib/util/check-for-updates.d.ts +3 -0
- package/lib/util/check-for-updates.d.ts.map +1 -0
- package/lib/util/check-for-updates.js +69 -0
- package/lib/util/check-for-updates.js.map +1 -0
- package/lib/util/file.d.ts +7 -0
- package/lib/util/file.d.ts.map +1 -0
- package/lib/util/file.js +66 -0
- package/lib/util/file.js.map +1 -0
- package/lib/util/file.test.d.ts +2 -0
- package/lib/util/file.test.d.ts.map +1 -0
- package/lib/util/file.test.js +87 -0
- package/lib/util/file.test.js.map +1 -0
- package/package.json +8 -7
- package/readme.md +3 -3
- package/src/application-control/config.ts +1 -1
- package/src/application-control/install.ts +1 -0
- package/src/application-control/models.ts +36 -13
- package/src/application-control/status.ts +9 -7
- package/src/application-control/utils.ts +1 -1
- package/src/cloud-connection/device-agent-cloud-connection.ts +54 -30
- package/src/cloud-connection/live-updates-handler.test.ts +161 -20
- package/src/cloud-connection/live-updates-handler.ts +30 -26
- package/src/cloud-connection/passthrough-handler.ts +98 -76
- package/src/cloud-connection/shadow-handler.ts +19 -7
- package/src/cloud-connection/transaction-manager.test.ts +124 -0
- package/src/cloud-connection/transaction-manager.ts +15 -0
- package/src/device-control/device-control.ts +86 -11
- package/src/docker/docker-compose.ts +60 -0
- package/src/index.ts +2 -6
- package/src/infrastructure/agent-config.test.ts +3 -0
- package/src/infrastructure/agent-config.ts +38 -40
- package/src/local-connection/rabbitmq-connection.ts +8 -8
- package/src/secure-tunneling/secure-tunneling.test.ts +26 -26
- package/src/secure-tunneling/secure-tunneling.ts +48 -55
- package/src/subcommands/device/clean.ts +1 -1
- package/src/subcommands/device/get-info.ts +49 -0
- package/src/subcommands/device/index.ts +11 -2
- package/src/subcommands/device/{device.ts → init.ts} +0 -58
- package/src/subcommands/device/refresh.ts +22 -0
- package/src/subcommands/device/restart.ts +11 -0
- package/src/util/check-for-updates.ts +69 -0
- package/src/util/file.test.ts +90 -0
- package/src/util/file.ts +76 -0
- package/lib/docker/docker-compose-cmd.d.ts +0 -5
- package/lib/docker/docker-compose-cmd.d.ts.map +0 -1
- package/lib/docker/docker-compose-cmd.js +0 -16
- package/lib/docker/docker-compose-cmd.js.map +0 -1
- package/lib/subcommands/device/device.d.ts +0 -7
- package/lib/subcommands/device/device.d.ts.map +0 -1
- package/lib/subcommands/device/device.js.map +0 -1
- package/lib/util/safe-rimraf.d.ts +0 -2
- package/lib/util/safe-rimraf.d.ts.map +0 -1
- package/lib/util/safe-rimraf.js +0 -16
- package/lib/util/safe-rimraf.js.map +0 -1
- package/src/docker/docker-compose-cmd.ts +0 -15
- package/src/util/safe-rimraf.ts +0 -14
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
const amqp = require('amqplib');
|
|
1
|
+
import * as amqp from 'amqplib';
|
|
3
2
|
import {
|
|
4
3
|
LOCAL_CONNECTION_HOST,
|
|
5
4
|
LOCAL_CONNECTION_PORT,
|
|
6
5
|
LOCAL_CONNECTION_ROUTING_KEY
|
|
7
6
|
} from '../local-connection/constants';
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
stopRabbitMQContainer,
|
|
9
|
+
setupRabbitMQContainer
|
|
10
|
+
} from '../local-connection/rabbitmq-connection';
|
|
9
11
|
import { logger } from '../util/logger';
|
|
10
12
|
import sleep from '../util/sleep';
|
|
11
13
|
import { Publisher } from './publisher';
|
|
@@ -16,21 +18,84 @@ const MAX_LOCAL_CONNECTION_ATTEMPTS = 10;
|
|
|
16
18
|
|
|
17
19
|
export class PassthroughHandler {
|
|
18
20
|
public publisher: Publisher;
|
|
19
|
-
public connection;
|
|
20
|
-
public channel;
|
|
21
|
+
public connection: amqp.Connection | undefined;
|
|
22
|
+
public channel: amqp.Channel;
|
|
21
23
|
public packetQueue;
|
|
22
24
|
|
|
23
25
|
constructor(publisher: Publisher) {
|
|
24
26
|
this.publisher = publisher;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
runChannel = async () => {
|
|
30
|
+
logger.debug('Beginning to consume packets');
|
|
31
|
+
await this.channel.consume(
|
|
32
|
+
this.packetQueue,
|
|
33
|
+
(msg) => {
|
|
34
|
+
// NOTE: this needs to be an arrow function and then the whole contents of processPublish are below
|
|
35
|
+
if (msg?.content !== undefined) {
|
|
36
|
+
const packet = JSON.parse(msg.content.toString());
|
|
37
|
+
messageQueue.push({ packet, msg });
|
|
38
|
+
while (messageQueue.length > 0) {
|
|
39
|
+
const entry = messageQueue.shift();
|
|
40
|
+
const { packet, msg } = entry;
|
|
41
|
+
try {
|
|
42
|
+
const parsedPacket = JSON.parse(packet);
|
|
43
|
+
if (parsedPacket?.['action']) {
|
|
44
|
+
switch (parsedPacket['action']) {
|
|
45
|
+
case 'analytics':
|
|
46
|
+
ackQueue.push(msg);
|
|
47
|
+
// FIXME: put real topic here
|
|
48
|
+
this.publisher.publishToCloudWithAck(
|
|
49
|
+
packet,
|
|
50
|
+
(errOrResp) => {
|
|
51
|
+
while (ackQueue.length > 0) {
|
|
52
|
+
const msg = ackQueue.shift();
|
|
53
|
+
if (errOrResp === true) {
|
|
54
|
+
this.channel.ack(msg); // acknowledge, allow queue to discard
|
|
55
|
+
} else if (errOrResp === false) {
|
|
56
|
+
this.channel.reject(msg, true); // reject and requeue
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
break;
|
|
62
|
+
case 'heartbeat':
|
|
63
|
+
this.channel.ack(msg);
|
|
64
|
+
logger.silly(
|
|
65
|
+
`Heartbeat package received & acknowledged: ${packet}`
|
|
66
|
+
);
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
this.channel.ack(msg);
|
|
70
|
+
logger.debug(
|
|
71
|
+
`Unknown 'action' package received & acknowledged: ${packet}`
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
this.channel.ack(msg);
|
|
77
|
+
logger.debug(
|
|
78
|
+
`Received & acknowledged a RabbitMQ Package of unknown structure: ${parsedPacket}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
logger.error(`There was a problem parsing RabbitMQ packet ${e}`);
|
|
83
|
+
this.channel.ack(msg);
|
|
84
|
+
logger.debug(`Problematic packet was acknowledged`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
async establishLocalConnection(): Promise<void> {
|
|
29
96
|
let connectAttempts = 0;
|
|
30
97
|
let connected = false;
|
|
31
|
-
logger.debug(
|
|
32
|
-
`Setting up alwaysAI Local Connection on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
33
|
-
);
|
|
98
|
+
logger.debug(`Establishing local connection...`);
|
|
34
99
|
while (
|
|
35
100
|
connectAttempts <= MAX_LOCAL_CONNECTION_ATTEMPTS &&
|
|
36
101
|
this.connection === undefined
|
|
@@ -40,6 +105,13 @@ export class PassthroughHandler {
|
|
|
40
105
|
`amqp://${LOCAL_CONNECTION_HOST}:${LOCAL_CONNECTION_PORT}`
|
|
41
106
|
);
|
|
42
107
|
this.channel = await this.connection.createChannel();
|
|
108
|
+
this.connection.on('error', async () => {
|
|
109
|
+
logger.error(`Local connection failed. Attempting to reconnect...`);
|
|
110
|
+
await stopRabbitMQContainer();
|
|
111
|
+
this.connection = undefined;
|
|
112
|
+
await this.setup();
|
|
113
|
+
});
|
|
114
|
+
|
|
43
115
|
connected = true;
|
|
44
116
|
} catch (e) {
|
|
45
117
|
const timeTillNextAttemptMs = 1000 + 1000 * connectAttempts;
|
|
@@ -53,84 +125,34 @@ export class PassthroughHandler {
|
|
|
53
125
|
}
|
|
54
126
|
}
|
|
55
127
|
if (connected === true) {
|
|
56
|
-
this.channel.prefetch(1); // This ensures we only get one packet at a time! This appears to have prevented throttling
|
|
128
|
+
await this.channel.prefetch(1); // This ensures we only get one packet at a time! This appears to have prevented throttling
|
|
57
129
|
this.packetQueue = `${LOCAL_CONNECTION_ROUTING_KEY}`;
|
|
58
130
|
await this.channel.assertQueue(this.packetQueue, {
|
|
59
131
|
durable: true
|
|
60
132
|
});
|
|
133
|
+
logger.info(`Local connection established.`);
|
|
61
134
|
} else {
|
|
62
135
|
throw new Error(
|
|
63
136
|
'Unable to establish connection to alwaysAI Local Connection, please try restarting Device Agent.'
|
|
64
137
|
);
|
|
65
138
|
}
|
|
66
139
|
}
|
|
67
|
-
}
|
|
68
140
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
public async setup() {
|
|
142
|
+
logger.debug(
|
|
143
|
+
`Setting up alwaysAI Local Connection on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
144
|
+
);
|
|
145
|
+
await setupRabbitMQContainer();
|
|
73
146
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
while (ackQueue.length > 0) {
|
|
84
|
-
const msg = ackQueue.shift();
|
|
85
|
-
if (errOrResp === true) {
|
|
86
|
-
passthroughHandler.channel.ack(msg); // acknowledge, allow queue to discard
|
|
87
|
-
} else if (errOrResp === false) {
|
|
88
|
-
passthroughHandler.channel.reject(msg, true); // reject and requeue
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
break;
|
|
94
|
-
case 'heartbeat':
|
|
95
|
-
passthroughHandler.channel.ack(msg);
|
|
96
|
-
logger.debug(
|
|
97
|
-
`Heartbeat package received & acknowledged: ${packet}`
|
|
98
|
-
);
|
|
99
|
-
break;
|
|
100
|
-
default:
|
|
101
|
-
passthroughHandler.channel.ack(msg);
|
|
102
|
-
logger.debug(
|
|
103
|
-
`Unknown 'action' package received & acknowledged: ${packet}`
|
|
104
|
-
);
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
passthroughHandler.channel.ack(msg);
|
|
109
|
-
logger.debug(
|
|
110
|
-
`Received & acknowledged a RabbitMQ Package of unknown structure: ${parsedPacket}`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
} catch (e) {
|
|
114
|
-
logger.error(`There was a problem parsing RabbitMQ packet ${e}`);
|
|
115
|
-
passthroughHandler.channel.ack(msg);
|
|
116
|
-
logger.debug(`Problematic packet was acknowledged`);
|
|
147
|
+
await this.establishLocalConnection();
|
|
148
|
+
await this.runChannel();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error(
|
|
151
|
+
`There was a problem maintaining RabbitMQ connection: ${error}`
|
|
152
|
+
);
|
|
153
|
+
throw new Error(
|
|
154
|
+
`There was a problem maintaining RabbitMQ connection: ${error}`
|
|
155
|
+
);
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
158
|
}
|
|
120
|
-
|
|
121
|
-
export async function runChannel(passthroughHandler: PassthroughHandler) {
|
|
122
|
-
logger.debug('Beginning to consume packets');
|
|
123
|
-
passthroughHandler.channel.consume(
|
|
124
|
-
passthroughHandler.packetQueue,
|
|
125
|
-
function (msg) {
|
|
126
|
-
if (msg.content !== undefined) {
|
|
127
|
-
const packet = JSON.parse(msg.content.toString());
|
|
128
|
-
messageQueue.push({ packet, msg });
|
|
129
|
-
processPublish(passthroughHandler);
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
|
|
134
|
-
}
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -21,8 +21,9 @@ import {
|
|
|
21
21
|
getShadowTopic,
|
|
22
22
|
ShadowProjectsUpdateAll,
|
|
23
23
|
getDesiredFromMessage,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
ProjectShadowUpdate,
|
|
25
|
+
buildUpdateSecureTunnelShadowMessage,
|
|
26
|
+
SecureTunnelShadowDescriptionReported
|
|
26
27
|
} from '@alwaysai/device-agent-schemas';
|
|
27
28
|
|
|
28
29
|
export type AppConfigUpdate = {
|
|
@@ -107,9 +108,7 @@ export class ShadowHandler {
|
|
|
107
108
|
txId: string;
|
|
108
109
|
projectId: string;
|
|
109
110
|
}): Promise<AppConfigUpdate | null> {
|
|
110
|
-
let appCfgUpdate: any;
|
|
111
111
|
let newAppCfg: any;
|
|
112
|
-
|
|
113
112
|
// Handle errors and validation
|
|
114
113
|
try {
|
|
115
114
|
newAppCfg = JSON.parse(appConfig);
|
|
@@ -139,10 +138,9 @@ export class ShadowHandler {
|
|
|
139
138
|
projectId
|
|
140
139
|
});
|
|
141
140
|
|
|
141
|
+
const appCfgUpdate: AppConfigUpdate = { newAppCfg };
|
|
142
142
|
if (updatedModels && Object.keys(updatedModels).length) {
|
|
143
|
-
appCfgUpdate =
|
|
144
|
-
} else {
|
|
145
|
-
appCfgUpdate = { newAppCfg };
|
|
143
|
+
appCfgUpdate.updatedModels = updatedModels;
|
|
146
144
|
}
|
|
147
145
|
return appCfgUpdate;
|
|
148
146
|
}
|
|
@@ -362,6 +360,20 @@ export class ShadowHandler {
|
|
|
362
360
|
);
|
|
363
361
|
}
|
|
364
362
|
|
|
363
|
+
public async updateSecureTunnelShadow(
|
|
364
|
+
secureTunnelShadowUpdate: SecureTunnelShadowDescriptionReported
|
|
365
|
+
) {
|
|
366
|
+
this.publisher.publish(
|
|
367
|
+
getShadowTopic(this.clientId, 'secure-tunnel', 'update'),
|
|
368
|
+
JSON.stringify(
|
|
369
|
+
buildUpdateSecureTunnelShadowMessage(
|
|
370
|
+
secureTunnelShadowUpdate,
|
|
371
|
+
this.clientId
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
365
377
|
public getProjectShadowUpdates() {
|
|
366
378
|
this.publisher.publish(
|
|
367
379
|
getShadowTopic(this.clientId, 'projects', 'get'),
|
|
@@ -7,6 +7,7 @@ import { TransactionManager } from './transaction-manager';
|
|
|
7
7
|
import { v4 as uuidv4 } from 'uuid';
|
|
8
8
|
import { Publisher } from './publisher';
|
|
9
9
|
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
10
|
+
import { AppConfigUpdate, ShadowUpdate } from './shadow-handler';
|
|
10
11
|
|
|
11
12
|
const mockClient = {
|
|
12
13
|
publish: jest.fn()
|
|
@@ -245,4 +246,127 @@ describe('Test Transaction Manager', () => {
|
|
|
245
246
|
|
|
246
247
|
expect(txnMgr.getProjectFromTransaction(txId)).toBeUndefined();
|
|
247
248
|
});
|
|
249
|
+
|
|
250
|
+
test('add an appCfgUpdate from txDetails', async () => {
|
|
251
|
+
const txId = generateTxId();
|
|
252
|
+
const projectId = generateRandomProjectId();
|
|
253
|
+
const cfgUpdate: ShadowUpdate = { txId: txId, projectId: projectId };
|
|
254
|
+
|
|
255
|
+
await txnMgr.runTransactionStep({
|
|
256
|
+
func: func_incomplete,
|
|
257
|
+
projectId,
|
|
258
|
+
txId,
|
|
259
|
+
start: true
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(undefined);
|
|
263
|
+
|
|
264
|
+
txnMgr.setAppCfgUpdateToTx(txId, cfgUpdate);
|
|
265
|
+
|
|
266
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
267
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
268
|
+
expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
|
|
269
|
+
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
|
|
270
|
+
expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
|
|
271
|
+
expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(cfgUpdate);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('update a tx while holding appCfgUpdate', async () => {
|
|
275
|
+
const txId = generateTxId();
|
|
276
|
+
const projectId = generateRandomProjectId();
|
|
277
|
+
const cfgUpdate: ShadowUpdate = { txId: txId, projectId: projectId };
|
|
278
|
+
|
|
279
|
+
await txnMgr.runTransactionStep({
|
|
280
|
+
func: func_incomplete,
|
|
281
|
+
projectId,
|
|
282
|
+
txId,
|
|
283
|
+
start: true
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
txnMgr.setAppCfgUpdateToTx(txId, cfgUpdate);
|
|
287
|
+
|
|
288
|
+
await txnMgr.runTransactionStep({
|
|
289
|
+
func: func_incomplete,
|
|
290
|
+
projectId,
|
|
291
|
+
txId,
|
|
292
|
+
start: false,
|
|
293
|
+
stepName: 'step2'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
297
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
298
|
+
expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
|
|
299
|
+
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
|
|
300
|
+
expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
|
|
301
|
+
expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(cfgUpdate);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('update an appCfgUpdate to the same tx ', async () => {
|
|
305
|
+
const txId = generateTxId();
|
|
306
|
+
const projectId = generateRandomProjectId();
|
|
307
|
+
const cfgUpdate: ShadowUpdate = { txId: txId, projectId: projectId };
|
|
308
|
+
|
|
309
|
+
const ogAppCfg1: AppConfigUpdate = {
|
|
310
|
+
newAppCfg: {
|
|
311
|
+
scripts: {
|
|
312
|
+
start: 'python app.py'
|
|
313
|
+
},
|
|
314
|
+
models: {}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const cfgUpdate2: ShadowUpdate = {
|
|
319
|
+
txId: txId,
|
|
320
|
+
projectId: projectId,
|
|
321
|
+
appCfgUpdate: ogAppCfg1
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
await txnMgr.runTransactionStep({
|
|
325
|
+
func: func_incomplete,
|
|
326
|
+
projectId,
|
|
327
|
+
txId,
|
|
328
|
+
start: true
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
txnMgr.setAppCfgUpdateToTx(txId, cfgUpdate);
|
|
332
|
+
|
|
333
|
+
await txnMgr.runTransactionStep({
|
|
334
|
+
func: func_incomplete,
|
|
335
|
+
projectId,
|
|
336
|
+
txId,
|
|
337
|
+
start: false,
|
|
338
|
+
stepName: 'step2'
|
|
339
|
+
});
|
|
340
|
+
txnMgr.setAppCfgUpdateToTx(txId, cfgUpdate2);
|
|
341
|
+
|
|
342
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
343
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
344
|
+
expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
|
|
345
|
+
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
|
|
346
|
+
expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
|
|
347
|
+
expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(cfgUpdate2);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('remove a tx, ensure appCfgUpdate is removed', async () => {
|
|
351
|
+
const txId = generateTxId();
|
|
352
|
+
const projectId = generateRandomProjectId();
|
|
353
|
+
const cfgUpdate: ShadowUpdate = { txId: txId, projectId: projectId };
|
|
354
|
+
|
|
355
|
+
await txnMgr.runTransactionStep({
|
|
356
|
+
func: func_incomplete,
|
|
357
|
+
projectId,
|
|
358
|
+
txId,
|
|
359
|
+
start: true
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
txnMgr.setAppCfgUpdateToTx(txId, cfgUpdate);
|
|
363
|
+
txnMgr.completeTransaction(txId);
|
|
364
|
+
|
|
365
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(undefined);
|
|
366
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(undefined);
|
|
367
|
+
expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
|
|
368
|
+
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
|
|
369
|
+
expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
|
|
370
|
+
expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(undefined);
|
|
371
|
+
});
|
|
248
372
|
});
|
|
@@ -8,6 +8,7 @@ import { Publisher } from './publisher';
|
|
|
8
8
|
import { logger } from '../util/logger';
|
|
9
9
|
import { keyMirror } from 'alwaysai/lib/util';
|
|
10
10
|
import { CodedError } from '@carnesen/coded-error';
|
|
11
|
+
import { ShadowUpdate } from './shadow-handler';
|
|
11
12
|
|
|
12
13
|
interface TransactionDetails {
|
|
13
14
|
txId: string;
|
|
@@ -16,6 +17,7 @@ interface TransactionDetails {
|
|
|
16
17
|
start: string;
|
|
17
18
|
update?: string;
|
|
18
19
|
stop?: string;
|
|
20
|
+
appCfgUpdate?: ShadowUpdate;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export class TransactionManager {
|
|
@@ -169,6 +171,19 @@ export class TransactionManager {
|
|
|
169
171
|
return txnDetails?.projectId;
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
public getAppCfgUpdateFromTxID(txId: string): ShadowUpdate | undefined {
|
|
175
|
+
return this.detailsByTx[txId]?.appCfgUpdate;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public setAppCfgUpdateToTx(txId: string, appCfgUpdate: ShadowUpdate) {
|
|
179
|
+
if (this.isOngoingTransaction(txId)) {
|
|
180
|
+
this.detailsByTx[txId].appCfgUpdate = appCfgUpdate;
|
|
181
|
+
} else
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Could not set appCfgUpdate, the transaction ${txId} does not exist.`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
172
187
|
public completeTransaction(txId: string): void {
|
|
173
188
|
const txDetails = this.detailsByTx[txId];
|
|
174
189
|
if (txDetails === undefined) {
|
|
@@ -4,7 +4,9 @@ import * as osu from 'node-os-utils';
|
|
|
4
4
|
import * as si from 'systeminformation';
|
|
5
5
|
import { exec } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
+
import { JsSpawner } from 'alwaysai/lib/util';
|
|
7
8
|
import { DeviceStatsPayload } from '@alwaysai/device-agent-schemas';
|
|
9
|
+
import { getDeviceAgentVersion } from '../util/check-for-updates';
|
|
8
10
|
|
|
9
11
|
const exec_promise = promisify(exec);
|
|
10
12
|
|
|
@@ -112,27 +114,100 @@ export async function getNetworkInfo() {
|
|
|
112
114
|
};
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
export async function getDockerVersion() {
|
|
118
|
+
try {
|
|
119
|
+
const spawner = JsSpawner();
|
|
120
|
+
const result = JSON.parse(
|
|
121
|
+
await spawner.run({
|
|
122
|
+
exe: 'docker',
|
|
123
|
+
args: ['info', '--format', 'json']
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
return result.ClientInfo.Version;
|
|
127
|
+
} catch (e) {
|
|
128
|
+
logger.warn(`Cannot get Docker version: ${e}`);
|
|
129
|
+
return 'Not found';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function getDockerComposeVersion() {
|
|
134
|
+
try {
|
|
135
|
+
const spawner = JsSpawner();
|
|
136
|
+
return await spawner.run({
|
|
137
|
+
exe: 'docker',
|
|
138
|
+
args: ['compose', 'version']
|
|
139
|
+
});
|
|
140
|
+
} catch (e) {
|
|
141
|
+
logger.warn(`Cannot get Docker Compose version: ${e}`);
|
|
142
|
+
return 'Not found';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function getNpmVersion() {
|
|
147
|
+
try {
|
|
148
|
+
const spawner = JsSpawner();
|
|
149
|
+
return await spawner.run({
|
|
150
|
+
exe: 'npm',
|
|
151
|
+
args: ['--version']
|
|
152
|
+
});
|
|
153
|
+
} catch (e) {
|
|
154
|
+
logger.warn(`Cannot get npm version: ${e}`);
|
|
155
|
+
return 'Not found';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function getNodeVersion() {
|
|
160
|
+
try {
|
|
161
|
+
const spawner = JsSpawner();
|
|
162
|
+
return await spawner.run({
|
|
163
|
+
exe: 'node',
|
|
164
|
+
args: ['-v']
|
|
165
|
+
});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
logger.warn(`Cannot get Node version: ${e}`);
|
|
168
|
+
return 'Not found';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
115
172
|
export async function getPackageVersions() {
|
|
116
|
-
|
|
117
|
-
const agentJson = require('../../package.json');
|
|
173
|
+
const agentVersion = await getDeviceAgentVersion();
|
|
118
174
|
// eslint-disable-next-line
|
|
119
175
|
const deviceAgentSchemasJson = require('../../node_modules/@alwaysai/device-agent-schemas/package.json');
|
|
176
|
+
|
|
177
|
+
// Concurrent asynchronous function call
|
|
178
|
+
const [dockerVersion, dockerComposeVersion, nodeVersion, npmVersion] =
|
|
179
|
+
await Promise.all([
|
|
180
|
+
getDockerVersion(),
|
|
181
|
+
getDockerComposeVersion(),
|
|
182
|
+
getNodeVersion(),
|
|
183
|
+
getNpmVersion()
|
|
184
|
+
]);
|
|
185
|
+
|
|
120
186
|
return {
|
|
121
|
-
agent:
|
|
122
|
-
deviceAgentSchemas: deviceAgentSchemasJson.version
|
|
187
|
+
agent: agentVersion,
|
|
188
|
+
deviceAgentSchemas: deviceAgentSchemasJson.version,
|
|
189
|
+
docker: dockerVersion,
|
|
190
|
+
dockerCompose: dockerComposeVersion,
|
|
191
|
+
node: nodeVersion,
|
|
192
|
+
npm: npmVersion
|
|
123
193
|
};
|
|
124
194
|
}
|
|
125
195
|
|
|
126
196
|
export async function getLastBootTime() {
|
|
127
197
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (test.stderr) {
|
|
132
|
-
logger.error(`Stderr when getting last boot time: ${test.stderr}`);
|
|
198
|
+
const out = await exec_promise('journalctl --list-boots');
|
|
199
|
+
if (!out || out.stderr) {
|
|
200
|
+
logger.error(`Stderr when getting last boot time: ${out.stderr}`);
|
|
133
201
|
return undefined;
|
|
134
202
|
}
|
|
135
|
-
|
|
203
|
+
|
|
204
|
+
const bootTimes = out.stdout.split('\n');
|
|
205
|
+
const latestBootStdout = (bootTimes.pop() || bootTimes.pop())?.trim(); // possible last \n causes '' at end of array
|
|
206
|
+
if (!latestBootStdout) return undefined;
|
|
207
|
+
|
|
208
|
+
const tokens = latestBootStdout.trim().split(' ');
|
|
209
|
+
|
|
210
|
+
return new Date(`${tokens[2]} ${tokens[3]} ${tokens[4]}`);
|
|
136
211
|
} catch (e) {
|
|
137
212
|
logger.error(`Issue getting last boot time: ${e.message}`);
|
|
138
213
|
return undefined;
|
|
@@ -148,7 +223,7 @@ export async function getSystemInformation(): Promise<SystemInformationShadowUpd
|
|
|
148
223
|
device: await getDeviceInfo(),
|
|
149
224
|
network: await getNetworkInfo(),
|
|
150
225
|
versions: await getPackageVersions(),
|
|
151
|
-
lastBootTime: await getLastBootTime()
|
|
226
|
+
lastBootTime: (await getLastBootTime())?.toString()
|
|
152
227
|
};
|
|
153
228
|
return systemInfo;
|
|
154
229
|
} catch (e) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as DockerComposeV1 from 'docker-compose';
|
|
2
|
+
import { v2 as DockerComposeV2 } from 'docker-compose';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { logger } from '../util/logger';
|
|
5
|
+
import { JsSpawner } from 'alwaysai/lib/util';
|
|
6
|
+
|
|
7
|
+
let dockerCmd = 'docker';
|
|
8
|
+
|
|
9
|
+
export function importDockerCompose():
|
|
10
|
+
| typeof DockerComposeV1
|
|
11
|
+
| typeof DockerComposeV2 {
|
|
12
|
+
try {
|
|
13
|
+
execSync('docker compose version').toString();
|
|
14
|
+
logger.debug('Using Docker Compose V2.');
|
|
15
|
+
return DockerComposeV2;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
try {
|
|
18
|
+
execSync('docker-compose -v').toString();
|
|
19
|
+
logger.warn('Using docker-compose V1. Please consider updating to V2.');
|
|
20
|
+
} catch (e) {
|
|
21
|
+
logger.warn(`Could not determine compose: ${e}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
dockerCmd = 'docker-compose';
|
|
25
|
+
return DockerComposeV1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const compose = importDockerCompose();
|
|
29
|
+
|
|
30
|
+
export async function runDockerComposeCmd(props: {
|
|
31
|
+
args: string[];
|
|
32
|
+
dir: string;
|
|
33
|
+
}) {
|
|
34
|
+
const { args, dir } = props;
|
|
35
|
+
const spawner = JsSpawner();
|
|
36
|
+
if (dockerCmd === 'docker') {
|
|
37
|
+
args.unshift('compose');
|
|
38
|
+
}
|
|
39
|
+
const output = await spawner.run({
|
|
40
|
+
exe: dockerCmd,
|
|
41
|
+
args,
|
|
42
|
+
cwd: dir
|
|
43
|
+
});
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runStreamingDockerComposeCmd(props: {
|
|
48
|
+
args: string[];
|
|
49
|
+
dir: string;
|
|
50
|
+
}) {
|
|
51
|
+
const { args, dir } = props;
|
|
52
|
+
if (dockerCmd === 'docker') {
|
|
53
|
+
args.unshift('compose');
|
|
54
|
+
}
|
|
55
|
+
return await JsSpawner().runStreaming({
|
|
56
|
+
exe: dockerCmd,
|
|
57
|
+
args,
|
|
58
|
+
cwd: dir
|
|
59
|
+
});
|
|
60
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,13 +10,9 @@ import { root } from './root';
|
|
|
10
10
|
import { runDeviceAgentCloudInterface } from './cloud-connection/device-agent-cloud-connection';
|
|
11
11
|
import { AgentConfigFile } from './infrastructure/agent-config';
|
|
12
12
|
import { ALWAYSAI_DEVICE_AGENT_MODE } from './environment';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
16
|
-
const { version } = require('../package.json');
|
|
13
|
+
import { checkForUpdatesAndPrompt } from './util/check-for-updates';
|
|
17
14
|
|
|
18
15
|
if (module === require.main) {
|
|
19
|
-
logger.info(`Starting alwaysAI Device Agent v${version}`);
|
|
20
16
|
if (!AgentConfigFile().exists()) {
|
|
21
17
|
AgentConfigFile().initialize();
|
|
22
18
|
}
|
|
@@ -24,6 +20,6 @@ if (module === require.main) {
|
|
|
24
20
|
if (ALWAYSAI_DEVICE_AGENT_MODE === 'cloud') {
|
|
25
21
|
void runDeviceAgentCloudInterface();
|
|
26
22
|
} else {
|
|
27
|
-
void runCliAndExit(root, {});
|
|
23
|
+
void runCliAndExit(root, { postRun: checkForUpdatesAndPrompt });
|
|
28
24
|
}
|
|
29
25
|
}
|
|
@@ -95,6 +95,9 @@ describe('Test Agent Config', () => {
|
|
|
95
95
|
expect(await configFile.getAppVersion({ projectId })).toEqual(version);
|
|
96
96
|
await configFile.setAppInstalled({ projectId, version });
|
|
97
97
|
expect(await configFile.getAppVersion({ projectId })).toEqual(version);
|
|
98
|
+
expect(await configFile.isAppReady({ projectId })).toEqual(true);
|
|
99
|
+
await configFile.setAppUninstalling({ projectId });
|
|
100
|
+
expect(await configFile.isAppReady({ projectId })).toEqual(false);
|
|
98
101
|
await configFile.setAppUninstalled({ projectId });
|
|
99
102
|
const apps = await configFile.getApps();
|
|
100
103
|
expect(apps).toEqual([]);
|