@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.
Files changed (139) hide show
  1. package/lib/application-control/config.js +2 -2
  2. package/lib/application-control/config.js.map +1 -1
  3. package/lib/application-control/install.d.ts.map +1 -1
  4. package/lib/application-control/install.js +1 -0
  5. package/lib/application-control/install.js.map +1 -1
  6. package/lib/application-control/models.d.ts +5 -0
  7. package/lib/application-control/models.d.ts.map +1 -1
  8. package/lib/application-control/models.js +24 -12
  9. package/lib/application-control/models.js.map +1 -1
  10. package/lib/application-control/status.d.ts.map +1 -1
  11. package/lib/application-control/status.js +10 -12
  12. package/lib/application-control/status.js.map +1 -1
  13. package/lib/application-control/utils.js +2 -2
  14. package/lib/application-control/utils.js.map +1 -1
  15. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +2 -2
  16. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  17. package/lib/cloud-connection/device-agent-cloud-connection.js +35 -15
  18. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  19. package/lib/cloud-connection/live-updates-handler.d.ts +3 -2
  20. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  21. package/lib/cloud-connection/live-updates-handler.js +24 -21
  22. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  23. package/lib/cloud-connection/live-updates-handler.test.js +132 -16
  24. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  25. package/lib/cloud-connection/passthrough-handler.d.ts +5 -3
  26. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/passthrough-handler.js +76 -62
  28. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.d.ts +2 -0
  30. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  31. package/lib/cloud-connection/shadow-handler.js +5 -5
  32. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  33. package/lib/cloud-connection/transaction-manager.d.ts +3 -0
  34. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  35. package/lib/cloud-connection/transaction-manager.js +11 -0
  36. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  37. package/lib/cloud-connection/transaction-manager.test.js +102 -0
  38. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  39. package/lib/device-control/device-control.d.ts +10 -2
  40. package/lib/device-control/device-control.d.ts.map +1 -1
  41. package/lib/device-control/device-control.js +86 -10
  42. package/lib/device-control/device-control.js.map +1 -1
  43. package/lib/docker/docker-compose.d.ts +14 -0
  44. package/lib/docker/docker-compose.d.ts.map +1 -0
  45. package/lib/docker/docker-compose.js +56 -0
  46. package/lib/docker/docker-compose.js.map +1 -0
  47. package/lib/index.js +2 -5
  48. package/lib/index.js.map +1 -1
  49. package/lib/infrastructure/agent-config.d.ts +45 -14
  50. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  51. package/lib/infrastructure/agent-config.js +30 -15
  52. package/lib/infrastructure/agent-config.js.map +1 -1
  53. package/lib/infrastructure/agent-config.test.js +3 -0
  54. package/lib/infrastructure/agent-config.test.js.map +1 -1
  55. package/lib/local-connection/rabbitmq-connection.js +11 -11
  56. package/lib/local-connection/rabbitmq-connection.js.map +1 -1
  57. package/lib/secure-tunneling/secure-tunneling.d.ts +14 -22
  58. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -1
  59. package/lib/secure-tunneling/secure-tunneling.js +34 -34
  60. package/lib/secure-tunneling/secure-tunneling.js.map +1 -1
  61. package/lib/secure-tunneling/secure-tunneling.test.js +18 -18
  62. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -1
  63. package/lib/subcommands/device/clean.js +5 -5
  64. package/lib/subcommands/device/clean.js.map +1 -1
  65. package/lib/subcommands/device/get-info.d.ts +2 -0
  66. package/lib/subcommands/device/get-info.d.ts.map +1 -0
  67. package/lib/subcommands/device/get-info.js +36 -0
  68. package/lib/subcommands/device/get-info.js.map +1 -0
  69. package/lib/subcommands/device/index.d.ts.map +1 -1
  70. package/lib/subcommands/device/index.js +11 -2
  71. package/lib/subcommands/device/index.js.map +1 -1
  72. package/lib/subcommands/device/init.d.ts +5 -0
  73. package/lib/subcommands/device/init.d.ts.map +1 -0
  74. package/lib/subcommands/device/{device.js → init.js} +2 -41
  75. package/lib/subcommands/device/init.js.map +1 -0
  76. package/lib/subcommands/device/refresh.d.ts +2 -0
  77. package/lib/subcommands/device/refresh.d.ts.map +1 -0
  78. package/lib/subcommands/device/refresh.js +24 -0
  79. package/lib/subcommands/device/refresh.js.map +1 -0
  80. package/lib/subcommands/device/restart.d.ts +2 -0
  81. package/lib/subcommands/device/restart.d.ts.map +1 -0
  82. package/lib/subcommands/device/restart.js +14 -0
  83. package/lib/subcommands/device/restart.js.map +1 -0
  84. package/lib/util/check-for-updates.d.ts +3 -0
  85. package/lib/util/check-for-updates.d.ts.map +1 -0
  86. package/lib/util/check-for-updates.js +69 -0
  87. package/lib/util/check-for-updates.js.map +1 -0
  88. package/lib/util/file.d.ts +7 -0
  89. package/lib/util/file.d.ts.map +1 -0
  90. package/lib/util/file.js +66 -0
  91. package/lib/util/file.js.map +1 -0
  92. package/lib/util/file.test.d.ts +2 -0
  93. package/lib/util/file.test.d.ts.map +1 -0
  94. package/lib/util/file.test.js +87 -0
  95. package/lib/util/file.test.js.map +1 -0
  96. package/package.json +8 -7
  97. package/readme.md +3 -3
  98. package/src/application-control/config.ts +1 -1
  99. package/src/application-control/install.ts +1 -0
  100. package/src/application-control/models.ts +36 -13
  101. package/src/application-control/status.ts +9 -7
  102. package/src/application-control/utils.ts +1 -1
  103. package/src/cloud-connection/device-agent-cloud-connection.ts +54 -30
  104. package/src/cloud-connection/live-updates-handler.test.ts +161 -20
  105. package/src/cloud-connection/live-updates-handler.ts +30 -26
  106. package/src/cloud-connection/passthrough-handler.ts +98 -76
  107. package/src/cloud-connection/shadow-handler.ts +19 -7
  108. package/src/cloud-connection/transaction-manager.test.ts +124 -0
  109. package/src/cloud-connection/transaction-manager.ts +15 -0
  110. package/src/device-control/device-control.ts +86 -11
  111. package/src/docker/docker-compose.ts +60 -0
  112. package/src/index.ts +2 -6
  113. package/src/infrastructure/agent-config.test.ts +3 -0
  114. package/src/infrastructure/agent-config.ts +38 -40
  115. package/src/local-connection/rabbitmq-connection.ts +8 -8
  116. package/src/secure-tunneling/secure-tunneling.test.ts +26 -26
  117. package/src/secure-tunneling/secure-tunneling.ts +48 -55
  118. package/src/subcommands/device/clean.ts +1 -1
  119. package/src/subcommands/device/get-info.ts +49 -0
  120. package/src/subcommands/device/index.ts +11 -2
  121. package/src/subcommands/device/{device.ts → init.ts} +0 -58
  122. package/src/subcommands/device/refresh.ts +22 -0
  123. package/src/subcommands/device/restart.ts +11 -0
  124. package/src/util/check-for-updates.ts +69 -0
  125. package/src/util/file.test.ts +90 -0
  126. package/src/util/file.ts +76 -0
  127. package/lib/docker/docker-compose-cmd.d.ts +0 -5
  128. package/lib/docker/docker-compose-cmd.d.ts.map +0 -1
  129. package/lib/docker/docker-compose-cmd.js +0 -16
  130. package/lib/docker/docker-compose-cmd.js.map +0 -1
  131. package/lib/subcommands/device/device.d.ts +0 -7
  132. package/lib/subcommands/device/device.d.ts.map +0 -1
  133. package/lib/subcommands/device/device.js.map +0 -1
  134. package/lib/util/safe-rimraf.d.ts +0 -2
  135. package/lib/util/safe-rimraf.d.ts.map +0 -1
  136. package/lib/util/safe-rimraf.js +0 -16
  137. package/lib/util/safe-rimraf.js.map +0 -1
  138. package/src/docker/docker-compose-cmd.ts +0 -15
  139. package/src/util/safe-rimraf.ts +0 -14
@@ -1,11 +1,13 @@
1
- // eslint-disable-next-line
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 { setupRabbitMQContainer } from '../local-connection/rabbitmq-connection';
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
- public async setup() {
28
- await setupRabbitMQContainer();
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
- function processPublish(passthroughHandler: PassthroughHandler) {
70
- while (messageQueue.length > 0) {
71
- const entry = messageQueue.shift();
72
- const { packet, msg } = entry;
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
- const parsedPacket = JSON.parse(packet);
75
- if (parsedPacket && parsedPacket['action']) {
76
- switch (parsedPacket['action']) {
77
- case 'analytics':
78
- ackQueue.push(msg);
79
- // FIXME: put real topic here
80
- passthroughHandler.publisher.publishToCloudWithAck(
81
- packet,
82
- (errOrResp) => {
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
- ShadowTopics,
25
- ProjectShadowUpdate
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 = { newAppCfg, updatedModels };
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
- // eslint-disable-next-line
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: agentJson.version,
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 test = await exec_promise(
129
- "last -x reboot | grep reboot | head -n 1 | awk '{ print $5, $6, $7, $8 }'"
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
- return new Date(test.stdout).toString();
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 { logger } from './util/logger';
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([]);