@alwaysai/device-agent 2.0.2 → 2.0.3

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 (121) hide show
  1. package/lib/application-control/config.js +1 -1
  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 +2 -2
  5. package/lib/application-control/install.js.map +1 -1
  6. package/lib/cloud-connection/base-message-handler.d.ts.map +1 -1
  7. package/lib/cloud-connection/base-message-handler.js +5 -4
  8. package/lib/cloud-connection/base-message-handler.js.map +1 -1
  9. package/lib/cloud-connection/connection-manager.js +2 -2
  10. package/lib/cloud-connection/connection-manager.js.map +1 -1
  11. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  12. package/lib/cloud-connection/device-agent-cloud-connection.js +3 -8
  13. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  14. package/lib/cloud-connection/device-agent-message-handler.d.ts.map +1 -1
  15. package/lib/cloud-connection/device-agent-message-handler.js +19 -18
  16. package/lib/cloud-connection/device-agent-message-handler.js.map +1 -1
  17. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  18. package/lib/cloud-connection/live-updates-handler.js +11 -4
  19. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  20. package/lib/cloud-connection/passthrough-handler.d.ts +3 -3
  21. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  22. package/lib/cloud-connection/passthrough-handler.js +97 -74
  23. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  24. package/lib/cloud-connection/shadow-handler.js +3 -3
  25. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  26. package/lib/cloud-connection/shadow.d.ts.map +1 -1
  27. package/lib/cloud-connection/shadow.js +1 -1
  28. package/lib/cloud-connection/shadow.js.map +1 -1
  29. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  30. package/lib/cloud-connection/transaction-manager.js +17 -7
  31. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  32. package/lib/cloud-connection/transaction-manager.test.js +52 -44
  33. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  34. package/lib/device-control/device-control.d.ts.map +1 -1
  35. package/lib/device-control/device-control.js +13 -9
  36. package/lib/device-control/device-control.js.map +1 -1
  37. package/lib/docker/docker-compose.d.ts.map +1 -1
  38. package/lib/docker/docker-compose.js +1 -1
  39. package/lib/docker/docker-compose.js.map +1 -1
  40. package/lib/environment.d.ts.map +1 -1
  41. package/lib/environment.js +1 -1
  42. package/lib/environment.js.map +1 -1
  43. package/lib/index.js +12 -0
  44. package/lib/index.js.map +1 -1
  45. package/lib/infrastructure/config-check-utility.js +2 -2
  46. package/lib/infrastructure/config-check-utility.js.map +1 -1
  47. package/lib/infrastructure/legacy-migration/legacy-migration.d.ts.map +1 -1
  48. package/lib/infrastructure/legacy-migration/legacy-migration.js +6 -10
  49. package/lib/infrastructure/legacy-migration/legacy-migration.js.map +1 -1
  50. package/lib/local-connection/rabbitmq-container.d.ts +6 -0
  51. package/lib/local-connection/rabbitmq-container.d.ts.map +1 -0
  52. package/lib/local-connection/rabbitmq-container.js +111 -0
  53. package/lib/local-connection/rabbitmq-container.js.map +1 -0
  54. package/lib/local-connection/rabbitmq-container.test.d.ts +2 -0
  55. package/lib/local-connection/rabbitmq-container.test.d.ts.map +1 -0
  56. package/lib/local-connection/rabbitmq-container.test.js +219 -0
  57. package/lib/local-connection/rabbitmq-container.test.js.map +1 -0
  58. package/lib/subcommands/device/clean.d.ts.map +1 -1
  59. package/lib/subcommands/device/clean.js +15 -17
  60. package/lib/subcommands/device/clean.js.map +1 -1
  61. package/lib/subcommands/device/index.d.ts.map +1 -1
  62. package/lib/subcommands/device/index.js +3 -1
  63. package/lib/subcommands/device/index.js.map +1 -1
  64. package/lib/subcommands/device/local-connection.d.ts +2 -0
  65. package/lib/subcommands/device/local-connection.d.ts.map +1 -0
  66. package/lib/subcommands/device/local-connection.js +17 -0
  67. package/lib/subcommands/device/local-connection.js.map +1 -0
  68. package/lib/subcommands/index.d.ts +4 -1
  69. package/lib/subcommands/index.d.ts.map +1 -1
  70. package/lib/subcommands/index.js +1 -3
  71. package/lib/subcommands/index.js.map +1 -1
  72. package/lib/util/check-for-updates.d.ts.map +1 -1
  73. package/lib/util/check-for-updates.js +2 -2
  74. package/lib/util/check-for-updates.js.map +1 -1
  75. package/lib/util/file.d.ts.map +1 -1
  76. package/lib/util/file.js +6 -1
  77. package/lib/util/file.js.map +1 -1
  78. package/lib/util/file.test.js +1 -1
  79. package/lib/util/file.test.js.map +1 -1
  80. package/lib/util/get-device-id.d.ts.map +1 -1
  81. package/lib/util/get-device-id.js +1 -1
  82. package/lib/util/get-device-id.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/application-control/config.ts +1 -1
  85. package/src/application-control/install.ts +3 -2
  86. package/src/cloud-connection/base-message-handler.ts +10 -5
  87. package/src/cloud-connection/connection-manager.ts +2 -2
  88. package/src/cloud-connection/device-agent-cloud-connection.ts +6 -10
  89. package/src/cloud-connection/device-agent-message-handler.ts +10 -7
  90. package/src/cloud-connection/live-updates-handler.ts +12 -5
  91. package/src/cloud-connection/passthrough-handler.ts +79 -38
  92. package/src/cloud-connection/shadow-handler.ts +3 -3
  93. package/src/cloud-connection/shadow.ts +3 -1
  94. package/src/cloud-connection/transaction-manager.test.ts +60 -41
  95. package/src/cloud-connection/transaction-manager.ts +26 -12
  96. package/src/device-control/device-control.ts +23 -13
  97. package/src/docker/docker-compose.ts +3 -1
  98. package/src/environment.ts +1 -2
  99. package/src/index.ts +19 -0
  100. package/src/infrastructure/config-check-utility.ts +2 -2
  101. package/src/infrastructure/legacy-migration/legacy-migration.ts +8 -13
  102. package/src/local-connection/rabbitmq-container.test.ts +255 -0
  103. package/src/local-connection/rabbitmq-container.ts +151 -0
  104. package/src/subcommands/device/clean.ts +20 -19
  105. package/src/subcommands/device/index.ts +3 -1
  106. package/src/subcommands/device/local-connection.ts +16 -0
  107. package/src/subcommands/index.ts +1 -3
  108. package/src/util/check-for-updates.ts +4 -2
  109. package/src/util/file.test.ts +1 -1
  110. package/src/util/file.ts +7 -1
  111. package/src/util/get-device-id.ts +3 -1
  112. package/lib/local-connection/rabbitmq-connection.d.ts +0 -7
  113. package/lib/local-connection/rabbitmq-connection.d.ts.map +0 -1
  114. package/lib/local-connection/rabbitmq-connection.js +0 -95
  115. package/lib/local-connection/rabbitmq-connection.js.map +0 -1
  116. package/lib/subcommands/rabbitmq-connection.d.ts +0 -2
  117. package/lib/subcommands/rabbitmq-connection.d.ts.map +0 -1
  118. package/lib/subcommands/rabbitmq-connection.js +0 -14
  119. package/lib/subcommands/rabbitmq-connection.js.map +0 -1
  120. package/src/local-connection/rabbitmq-connection.ts +0 -124
  121. package/src/subcommands/rabbitmq-connection.ts +0 -11
@@ -137,30 +137,40 @@ export async function getNetworkInfo() {
137
137
  }
138
138
 
139
139
  export async function getDockerVersion() {
140
+ let result: string | undefined;
140
141
  try {
141
142
  const spawner = JsSpawner();
142
- const result = JSON.parse(
143
- await spawner.run({
144
- exe: 'docker',
145
- args: ['info', '--format', 'json']
146
- })
147
- );
148
- return String(result.ClientInfo.Version);
143
+ result = await spawner.run({
144
+ exe: 'docker',
145
+ args: ['info', '--format', 'json']
146
+ });
147
+ const parsedResult = JSON.parse(result);
148
+ return String(parsedResult.ClientInfo.Version);
149
149
  } catch (e) {
150
- logger.warn(`Cannot get Docker version!\n${stringifyError(e)}`);
150
+ logger.warn(
151
+ `Cannot get Docker version! Result: ${result} Error:\n${stringifyError(
152
+ e
153
+ )}`
154
+ );
151
155
  return 'Not found';
152
156
  }
153
157
  }
154
158
 
155
159
  export async function getDockerComposeVersion() {
160
+ let result: string | undefined;
156
161
  try {
157
162
  const spawner = JsSpawner();
158
- return await spawner.run({
163
+ result = await spawner.run({
159
164
  exe: 'docker',
160
165
  args: ['compose', 'version']
161
166
  });
167
+ return result;
162
168
  } catch (e) {
163
- logger.warn(`Cannot get Docker Compose version!\n${stringifyError(e)}`);
169
+ logger.warn(
170
+ `Cannot get Docker Compose version! Results: ${result} Error:\n${stringifyError(
171
+ e
172
+ )}`
173
+ );
164
174
  return 'Not found';
165
175
  }
166
176
  }
@@ -173,7 +183,7 @@ export async function getNpmVersion() {
173
183
  args: ['--version']
174
184
  });
175
185
  } catch (e) {
176
- logger.warn(`Cannot get npm version!\n${stringifyError(e)}`);
186
+ logger.warn(`Cannot get npm version! Error:\n${stringifyError(e)}`);
177
187
  return 'Not found';
178
188
  }
179
189
  }
@@ -186,7 +196,7 @@ export async function getNodeVersion() {
186
196
  args: ['-v']
187
197
  });
188
198
  } catch (e) {
189
- logger.warn(`Cannot get Node version!\n${stringifyError(e)}`);
199
+ logger.warn(`Cannot get Node version! Error:\n${stringifyError(e)}`);
190
200
  return 'Not found';
191
201
  }
192
202
  }
@@ -230,7 +240,7 @@ export async function getLastBootTime() {
230
240
 
231
241
  return String(new Date(`${tokens[2]} ${tokens[3]} ${tokens[4]}`));
232
242
  } catch (e) {
233
- logger.error(`Issue getting last boot time!\n${stringifyError(e)}`);
243
+ logger.error(`Issue getting last boot time! Error:\n${stringifyError(e)}`);
234
244
  return undefined;
235
245
  }
236
246
  }
@@ -19,7 +19,9 @@ export function importDockerCompose():
19
19
  execSync('docker-compose -v').toString();
20
20
  logger.warn('Using docker-compose V1. Please consider updating to V2.');
21
21
  } catch (e) {
22
- logger.warn(`Could not determine compose!\n${stringifyError(e)}`);
22
+ logger.warn(
23
+ `Could not determine compose version! Error:\n${stringifyError(e)}`
24
+ );
23
25
  }
24
26
  }
25
27
  dockerCmd = 'docker-compose';
@@ -24,8 +24,7 @@ export const ALWAYSAI_LOCAL_CONNECTION_PORT =
24
24
  process.env.ALWAYSAI_LOCAL_CONNECTION_PORT;
25
25
  export const ALWAYSAI_LOCAL_CONNECTION_ANALYTICS_ROUTING_KEY =
26
26
  process.env.ALWAYSAI_LOCAL_CONNECTION_ANALYTICS_ROUTING_KEY;
27
- export const ALWAYSAI_TARGET_HW_OVERRIDE =
28
- process.env.ALWAYSAI_TARGET_HW_OVERRIDE;
27
+ export const ALWAYSAI_TARGET_HW_OVERRIDE = process.env.ALWAYSAI_TARGET_HW;
29
28
 
30
29
  export function parseTargetHW(
31
30
  hardwareType?: string
package/src/index.ts CHANGED
@@ -11,11 +11,30 @@ import { runDeviceAgentCloudInterface } from './cloud-connection/device-agent-cl
11
11
  import { AgentConfigFile } from './infrastructure/agent-config';
12
12
  import { ALWAYSAI_DEVICE_AGENT_MODE } from './environment';
13
13
  import { checkForUpdatesAndPrompt } from './util/check-for-updates';
14
+ import { logger } from './util/logger';
15
+ import { getSystemId } from './infrastructure/system-id';
16
+ import { stringifyError } from 'alwaysai/lib/util';
14
17
 
15
18
  if (module === require.main) {
16
19
  if (!AgentConfigFile().exists()) {
17
20
  AgentConfigFile().initialize();
18
21
  }
22
+ logger.debug(`Starting Device Agent with system ID: ${getSystemId()}`);
23
+
24
+ // Catch any unhandled errors to prevent Device Agent crash
25
+ process.on('unhandledRejection', (reason: Error, p: Promise<any>) => {
26
+ logger.error(
27
+ `Unhandled Rejection at: ${JSON.stringify(p)} reason: ${stringifyError(
28
+ reason
29
+ )}`
30
+ );
31
+ });
32
+ process.on('uncaughtException', (error: Error) => {
33
+ logger.error(
34
+ `Caught exception: ${stringifyError(error)}\n` +
35
+ `Exception origin: ${error.stack}`
36
+ );
37
+ });
19
38
 
20
39
  if (ALWAYSAI_DEVICE_AGENT_MODE === 'cloud') {
21
40
  void runDeviceAgentCloudInterface();
@@ -14,7 +14,7 @@ export const validConfigMessage = 'Device config is present and valid.';
14
14
  export async function checkDeviceConfigPresent(
15
15
  baseDir?: string
16
16
  ): Promise<boolean> {
17
- logger.debug('Checking for required device configuraton file.');
17
+ logger.debug('Checking for required device configuration file.');
18
18
  const deviceConfig = DeviceConfigFile(
19
19
  baseDir
20
20
  ? join(baseDir, getDeviceConfigPath(), DEVICE_CONFIG_FILE_NAME)
@@ -48,7 +48,7 @@ export async function checkCertificatesPresent(baseDir?: string) {
48
48
  export async function checkAaiConfigPresent(
49
49
  baseDir?: string
50
50
  ): Promise<boolean> {
51
- logger.debug('Checking for required alwaysai configuraton file.');
51
+ logger.debug('Checking for required alwaysAI configuration file.');
52
52
  const localAaiCfg = new LocalAaiCfg(baseDir);
53
53
 
54
54
  try {
@@ -17,7 +17,7 @@ import rimraf from 'rimraf';
17
17
  import {
18
18
  checkRabbitMQContainerRunning,
19
19
  stopRabbitMQContainer
20
- } from '../../local-connection/rabbitmq-connection';
20
+ } from '../../local-connection/rabbitmq-container';
21
21
  import { copyDir } from '../../util/copy-dir';
22
22
  import {
23
23
  getDeviceAgentConfigPath,
@@ -54,7 +54,7 @@ export async function migrateAgentConfigFile(baseDir?: string) {
54
54
  try {
55
55
  parsedAgentConfig = legacyAgentConfigFile.read();
56
56
  } catch (e) {
57
- logger.error(`Error reading agent config for:\n${stringifyError(e)}`);
57
+ logger.error(`Error reading agent config! Error:\n${stringifyError(e)}`);
58
58
  logger.error(
59
59
  `Agent Config Errors: ${JSON.stringify(
60
60
  legacyAgentConfigFile.getErrors(),
@@ -73,7 +73,7 @@ export async function migrateAgentConfigFile(baseDir?: string) {
73
73
  // we can remove it after migrating it
74
74
  await rimraf(legacyAgentConfigFile.path);
75
75
  } catch (e) {
76
- logger.error(`Error writing agent config for:\n${stringifyError(e)}`);
76
+ logger.error(`Error writing agent config! Error:\n${stringifyError(e)}`);
77
77
  logger.error(
78
78
  `Agent Config Errors: ${JSON.stringify(
79
79
  newAgentConfig.getErrors(),
@@ -199,17 +199,12 @@ export async function migrateFromLegacyCertsAndTokens(
199
199
  await migrateAgentConfigFile(baseDir);
200
200
 
201
201
  // if rabbitmq container is present, stop container and migrate files
202
- try {
203
- if (await checkRabbitMQContainerRunning()) {
204
- await stopRabbitMQContainer();
202
+ if (await checkRabbitMQContainerRunning()) {
203
+ if ((await stopRabbitMQContainer()) === false) {
204
+ logger.error(
205
+ `You may need to manually stop the container by running docker-compose down in the following directory: ${getDeviceAgentDockerComposePath()}`
206
+ );
205
207
  }
206
- } catch (e) {
207
- logger.error(
208
- `You may need to manually stop the container by running docker-compose down in the following directory: ${getDeviceAgentDockerComposePath()}`
209
- );
210
- logger.debug(
211
- `Error in checking / stopping RabbitMQ container!\n${stringifyError(e)}`
212
- );
213
208
  }
214
209
  if (await exists(getDeviceAgentConfigPath(baseDir))) {
215
210
  await copyDir({
@@ -0,0 +1,255 @@
1
+ import {
2
+ checkRabbitMQContainerRunning,
3
+ runRabbitMQContainer,
4
+ stopRabbitMQContainer
5
+ } from './rabbitmq-container';
6
+
7
+ const mockJsSpawner = {
8
+ exists: jest.fn(),
9
+ mkdirp: jest.fn(),
10
+ writeFile: jest.fn()
11
+ };
12
+ jest.mock('alwaysai/lib/util/spawner', () => {
13
+ return {
14
+ JsSpawner: jest.fn(() => mockJsSpawner)
15
+ };
16
+ });
17
+
18
+ jest.mock('../docker/docker-compose', () => {
19
+ return {
20
+ compose: {
21
+ ps: jest.fn(),
22
+ upAll: jest.fn(),
23
+ down: jest.fn()
24
+ }
25
+ };
26
+ });
27
+ import { compose } from '../docker/docker-compose';
28
+
29
+ describe('Test check RabbitMQ container running', () => {
30
+ afterEach(() => {
31
+ jest.clearAllMocks();
32
+ });
33
+
34
+ test('Compose file not found', async () => {
35
+ mockJsSpawner.exists.mockResolvedValue(false);
36
+
37
+ const running = await checkRabbitMQContainerRunning();
38
+
39
+ expect(running).toBeFalsy();
40
+ });
41
+
42
+ test('Container not found', async () => {
43
+ mockJsSpawner.exists.mockResolvedValue(true);
44
+ jest.mocked(compose.ps).mockResolvedValue({
45
+ exitCode: 0,
46
+ out: '',
47
+ err: '',
48
+ data: {
49
+ services: []
50
+ }
51
+ });
52
+
53
+ const running = await checkRabbitMQContainerRunning();
54
+
55
+ expect(running).toBeFalsy();
56
+ });
57
+
58
+ test('Container is not up', async () => {
59
+ mockJsSpawner.exists.mockResolvedValue(true);
60
+ jest.mocked(compose.ps).mockResolvedValue({
61
+ exitCode: 0,
62
+ out: '',
63
+ err: '',
64
+ data: {
65
+ services: [
66
+ {
67
+ name: 'alwaysAIRabbitMQContainer',
68
+ command: '',
69
+ state: 'Down',
70
+ ports: []
71
+ }
72
+ ]
73
+ }
74
+ });
75
+
76
+ const running = await checkRabbitMQContainerRunning();
77
+
78
+ expect(running).toBeFalsy();
79
+ });
80
+
81
+ test('Container is up', async () => {
82
+ mockJsSpawner.exists.mockResolvedValue(true);
83
+ jest.mocked(compose.ps).mockResolvedValue({
84
+ exitCode: 0,
85
+ out: '',
86
+ err: '',
87
+ data: {
88
+ services: [
89
+ {
90
+ name: 'alwaysAIRabbitMQContainer',
91
+ command: '',
92
+ state: 'Up',
93
+ ports: []
94
+ }
95
+ ]
96
+ }
97
+ });
98
+
99
+ const running = await checkRabbitMQContainerRunning();
100
+
101
+ expect(running).toBeTruthy();
102
+ });
103
+ });
104
+
105
+ describe('Test run RabbitMQ container', () => {
106
+ afterEach(() => {
107
+ jest.clearAllMocks();
108
+ });
109
+
110
+ test('Compose file write fails', async () => {
111
+ mockJsSpawner.exists.mockResolvedValueOnce(false);
112
+ mockJsSpawner.mkdirp.mockImplementation(async () => {
113
+ return;
114
+ });
115
+ mockJsSpawner.writeFile.mockImplementation(async () => {
116
+ throw new Error('Failed to write file!');
117
+ });
118
+
119
+ const running = await runRabbitMQContainer();
120
+
121
+ expect(running).toBeFalsy();
122
+ });
123
+
124
+ test('Compose up fails', async () => {
125
+ mockJsSpawner.exists.mockResolvedValueOnce(true);
126
+ mockJsSpawner.writeFile.mockImplementation(async () => {
127
+ return;
128
+ });
129
+ jest.mocked(compose.upAll).mockResolvedValue({
130
+ exitCode: 1,
131
+ out: '',
132
+ err: ''
133
+ });
134
+
135
+ const running = await runRabbitMQContainer();
136
+
137
+ expect(running).toBeFalsy();
138
+ });
139
+
140
+ test('Container fails to come up', async () => {
141
+ // exists(deviceAgentConfigPath)
142
+ mockJsSpawner.exists.mockResolvedValueOnce(true);
143
+ mockJsSpawner.writeFile.mockImplementation(async () => {
144
+ return;
145
+ });
146
+ jest.mocked(compose.upAll).mockResolvedValue({
147
+ exitCode: 0,
148
+ out: '',
149
+ err: ''
150
+ });
151
+ // exists(deviceAgentDockerComposePath)
152
+ mockJsSpawner.exists.mockResolvedValueOnce(true);
153
+ jest.mocked(compose.ps).mockResolvedValue({
154
+ exitCode: 0,
155
+ out: '',
156
+ err: '',
157
+ data: {
158
+ services: [
159
+ {
160
+ name: 'alwaysAIRabbitMQContainer',
161
+ command: '',
162
+ state: 'Down',
163
+ ports: []
164
+ }
165
+ ]
166
+ }
167
+ });
168
+
169
+ const running = await runRabbitMQContainer({ max_attempts: 2 });
170
+
171
+ expect(jest.mocked(compose.ps)).toBeCalledTimes(2);
172
+ expect(running).toBeFalsy();
173
+ });
174
+
175
+ test('Container comes up successfully', async () => {
176
+ // exists(deviceAgentConfigPath)
177
+ mockJsSpawner.exists.mockResolvedValueOnce(true);
178
+ mockJsSpawner.writeFile.mockImplementation(async () => {
179
+ return;
180
+ });
181
+ jest.mocked(compose.upAll).mockResolvedValue({
182
+ exitCode: 0,
183
+ out: '',
184
+ err: ''
185
+ });
186
+ // exists(deviceAgentDockerComposePath)
187
+ mockJsSpawner.exists.mockResolvedValueOnce(true);
188
+ // Check status twice, one with container still down and next up
189
+ jest.mocked(compose.ps).mockResolvedValueOnce({
190
+ exitCode: 0,
191
+ out: '',
192
+ err: '',
193
+ data: {
194
+ services: [
195
+ {
196
+ name: 'alwaysAIRabbitMQContainer',
197
+ command: '',
198
+ state: 'Down',
199
+ ports: []
200
+ }
201
+ ]
202
+ }
203
+ });
204
+ jest.mocked(compose.ps).mockResolvedValueOnce({
205
+ exitCode: 0,
206
+ out: '',
207
+ err: '',
208
+ data: {
209
+ services: [
210
+ {
211
+ name: 'alwaysAIRabbitMQContainer',
212
+ command: '',
213
+ state: 'Up',
214
+ ports: []
215
+ }
216
+ ]
217
+ }
218
+ });
219
+
220
+ const running = await runRabbitMQContainer({ max_attempts: 2 });
221
+
222
+ expect(jest.mocked(compose.ps)).toBeCalledTimes(2);
223
+ expect(running).toBeTruthy();
224
+ });
225
+ });
226
+
227
+ describe('Test stopping RabbitMQ container', () => {
228
+ afterEach(() => {
229
+ jest.clearAllMocks();
230
+ });
231
+
232
+ test('Compose down fails', async () => {
233
+ jest.mocked(compose.down).mockResolvedValue({
234
+ exitCode: 1,
235
+ out: '',
236
+ err: ''
237
+ });
238
+
239
+ const stopped = await stopRabbitMQContainer();
240
+
241
+ expect(stopped).toBeFalsy();
242
+ });
243
+
244
+ test('Stop succeeds', async () => {
245
+ jest.mocked(compose.down).mockResolvedValue({
246
+ exitCode: 0,
247
+ out: '',
248
+ err: ''
249
+ });
250
+
251
+ const stopped = await stopRabbitMQContainer();
252
+
253
+ expect(stopped).toBeTruthy();
254
+ });
255
+ });
@@ -0,0 +1,151 @@
1
+ import { JsSpawner, stringifyError } from 'alwaysai/lib/util';
2
+ import * as YAML from 'yaml';
3
+ import { compose } from '../docker/docker-compose';
4
+ import {
5
+ getDeviceAgentConfigPath,
6
+ getDeviceAgentDockerComposePath
7
+ } from '../util/directories';
8
+ import { logger } from '../util/logger';
9
+ import sleep from '../util/sleep';
10
+ import { LOCAL_CONNECTION_PORT } from './constants';
11
+
12
+ const rabbitMQServiceName = 'alwaysAIRabbitMQ';
13
+ const rabbitMQContainerName = 'alwaysAIRabbitMQContainer';
14
+
15
+ /*
16
+ Utility functions to control the Local Connection container (RabbitMQ).
17
+
18
+ All exported functions handle their own errors and log their status, returning a
19
+ boolean indicating whether the action was successful.
20
+ */
21
+
22
+ export async function checkRabbitMQContainerRunning(): Promise<boolean> {
23
+ logger.debug('Checking Local Connection Container status');
24
+ const spawner = JsSpawner();
25
+ if (!(await spawner.exists(getDeviceAgentDockerComposePath()))) {
26
+ logger.warn('Local Connection configuration file is not present');
27
+ return false;
28
+ }
29
+ const containerData = await compose.ps({ cwd: getDeviceAgentConfigPath() });
30
+ for (const service of containerData.data.services) {
31
+ if (service?.name === rabbitMQContainerName) {
32
+ if (service.state.includes('Up')) {
33
+ logger.debug('Local Connection container is up.');
34
+ return true;
35
+ }
36
+ logger.debug(
37
+ `Local Connection container is not up. Status: ${service.state}`
38
+ );
39
+ return false;
40
+ }
41
+ }
42
+ logger.warn(
43
+ `Local Connection container not found in container data: ${JSON.stringify(
44
+ containerData,
45
+ null,
46
+ 2
47
+ )}`
48
+ );
49
+ return false;
50
+ }
51
+
52
+ async function writeRabbitMQDockerComposeFile() {
53
+ const spawner = JsSpawner();
54
+ if (!(await spawner.exists(getDeviceAgentConfigPath()))) {
55
+ await JsSpawner().mkdirp(getDeviceAgentConfigPath());
56
+ }
57
+
58
+ logger.debug(
59
+ `Writing Local Connection Docker compose files with Local Connection port: ${LOCAL_CONNECTION_PORT}`
60
+ );
61
+
62
+ const rabbitmqDockerComposeCmd = {
63
+ services: {
64
+ [rabbitMQServiceName]: {
65
+ container_name: rabbitMQContainerName,
66
+ image: 'rabbitmq:3.11',
67
+ ports: [`${LOCAL_CONNECTION_PORT}:${LOCAL_CONNECTION_PORT}`],
68
+ hostname: 'my-rabbit',
69
+ restart: 'on-failure',
70
+ environment: [`RABBITMQ_NODE_PORT=${LOCAL_CONNECTION_PORT}`],
71
+ volumes: [
72
+ './rabbitmq/:/var/lib/rabbitmq/',
73
+ './rabbitmq/log/:/var/log/rabbitmq/'
74
+ ]
75
+ }
76
+ }
77
+ };
78
+
79
+ const RabbitMQDockerComposeYaml = YAML.stringify(rabbitmqDockerComposeCmd);
80
+ await spawner.writeFile(
81
+ getDeviceAgentDockerComposePath(),
82
+ RabbitMQDockerComposeYaml
83
+ );
84
+ }
85
+
86
+ export async function runRabbitMQContainer(opts?: {
87
+ max_attempts: number;
88
+ }): Promise<boolean> {
89
+ const max_attempts = opts?.max_attempts || 10;
90
+ logger.debug('Starting the Local Connection container');
91
+ try {
92
+ await writeRabbitMQDockerComposeFile();
93
+ const upOut = await compose.upAll({
94
+ cwd: getDeviceAgentConfigPath()
95
+ });
96
+ logger.debug(
97
+ `Docker compose up for Local Connection:\n${JSON.stringify(
98
+ upOut,
99
+ null,
100
+ 2
101
+ )}`
102
+ );
103
+ if (upOut.exitCode !== 0) {
104
+ throw new Error(
105
+ `Failed to start Local Connection! stdout=${upOut.out} stderr=${upOut.err}`
106
+ );
107
+ }
108
+ // check if the container is up
109
+ let attempts = 0;
110
+ while (!(await checkRabbitMQContainerRunning())) {
111
+ attempts++;
112
+ if (attempts >= max_attempts) {
113
+ throw new Error(
114
+ 'Local Connection container failed to come up after start!'
115
+ );
116
+ }
117
+ await sleep(1000);
118
+ }
119
+ return true;
120
+ } catch (e) {
121
+ logger.error(
122
+ `Unable to start Local Connection container!\n${stringifyError(e)}`
123
+ );
124
+ return false;
125
+ }
126
+ }
127
+
128
+ export async function stopRabbitMQContainer(): Promise<boolean> {
129
+ logger.debug(`Stopping alwaysAI Local Connection container`);
130
+ try {
131
+ const downOut = await compose.down({ cwd: getDeviceAgentConfigPath() });
132
+ logger.debug(
133
+ `Local Connection docker-compose down: ${JSON.stringify(
134
+ downOut,
135
+ null,
136
+ 2
137
+ )}`
138
+ );
139
+ if (downOut.exitCode !== 0) {
140
+ throw new Error(
141
+ `Failed to stop Local Connection! stdout=${downOut.out} stderr=${downOut.err}`
142
+ );
143
+ }
144
+ return true;
145
+ } catch (e) {
146
+ logger.error(
147
+ `Unable to stop Local Connection Container:\n${stringifyError(e)}`
148
+ );
149
+ return false;
150
+ }
151
+ }
@@ -11,7 +11,7 @@ import { AgentConfigFile } from '../../infrastructure/agent-config';
11
11
  import {
12
12
  checkRabbitMQContainerRunning,
13
13
  stopRabbitMQContainer
14
- } from '../../local-connection/rabbitmq-connection';
14
+ } from '../../local-connection/rabbitmq-container';
15
15
  import {
16
16
  APP_ROOT,
17
17
  CREDENTIALS_FILE_PATH,
@@ -20,28 +20,24 @@ import {
20
20
  } from '../../util/directories';
21
21
  import { safeRimraf } from '../../util/file';
22
22
  import { logger } from '../../util/logger';
23
+ import { existsSync } from 'fs';
23
24
 
24
25
  export const cleanCliLeaf = CliLeaf({
25
26
  name: 'clean',
26
27
  description: 'Remove all provisioning files',
27
28
  async action(_, opts) {
28
29
  logger.info('Removing provisioning files.');
29
- try {
30
- if (await checkRabbitMQContainerRunning()) {
31
- await stopRabbitMQContainer();
30
+ if (await checkRabbitMQContainerRunning()) {
31
+ if ((await stopRabbitMQContainer()) === false) {
32
+ logger.error(
33
+ `You may need to manually stop the container by running docker-compose down in the following directory: ${getDeviceAgentDockerComposePath()}`
34
+ );
32
35
  }
33
- } catch (e) {
34
- logger.error(
35
- `You may need to manually stop the container by running docker-compose down in the following directory: ${getDeviceAgentDockerComposePath()}`
36
- );
37
- logger.debug(
38
- `Error in checking / stopping RabbitMQ container!\n${stringifyError(e)}`
39
- );
40
36
  }
41
37
 
42
38
  await safeRimraf(getDeviceAgentConfigPath());
43
39
 
44
- logger.debug('Checking for alwaysAI applications still running');
40
+ logger.debug('Checking for alwaysAI applications still installed');
45
41
  const apps = await AgentConfigFile().getApps();
46
42
  const uninstallAppPromises: Promise<void>[] = [];
47
43
  for (const app of apps) {
@@ -60,13 +56,18 @@ export const cleanCliLeaf = CliLeaf({
60
56
  DeviceConfigFile().remove();
61
57
 
62
58
  await safeRimraf(CREDENTIALS_FILE_PATH);
63
- const files = await readdir(APP_ROOT);
64
- logger.debug(`${APP_ROOT} folder contents ${JSON.stringify(files.length)}`);
65
- // This won't handle hidden files, but will remove if truly empty, which is safer
66
- if (files.length === 0) {
67
- logger.debug('Applications directory is empty, removing.');
68
- await safeRimraf(APP_ROOT);
59
+
60
+ if (existsSync(APP_ROOT)) {
61
+ const files = await readdir(APP_ROOT);
62
+ logger.debug(
63
+ `${APP_ROOT} folder contents ${JSON.stringify(files.length)}`
64
+ );
65
+ // This won't handle hidden files, but will remove if truly empty, which is safer
66
+ if (files.length === 0) {
67
+ logger.debug('Applications directory is empty, removing.');
68
+ await safeRimraf(APP_ROOT);
69
+ }
70
+ logger.info('Device configuration cleaned');
69
71
  }
70
- logger.info('Device configuration cleaned');
71
72
  }
72
73
  });
@@ -5,6 +5,7 @@ import { initCliLeaf } from './init';
5
5
  import { restartCliLeaf } from './restart';
6
6
  import { refreshCliLeaf } from './refresh';
7
7
  import { migrateCliLeaf } from './migrate';
8
+ import { stopLocalConnectionCliLeaf } from './local-connection';
8
9
 
9
10
  export const deviceCliBranch = CliBranch({
10
11
  name: 'device',
@@ -15,6 +16,7 @@ export const deviceCliBranch = CliBranch({
15
16
  refreshCliLeaf,
16
17
  cleanCliLeaf,
17
18
  restartCliLeaf,
18
- migrateCliLeaf
19
+ migrateCliLeaf,
20
+ stopLocalConnectionCliLeaf
19
21
  ]
20
22
  });