@alwaysai/device-agent 1.3.1 → 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 (183) 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/environment-variables.d.ts.map +1 -1
  4. package/lib/application-control/environment-variables.js +9 -4
  5. package/lib/application-control/environment-variables.js.map +1 -1
  6. package/lib/application-control/environment-variables.test.js +1 -1
  7. package/lib/application-control/environment-variables.test.js.map +1 -1
  8. package/lib/application-control/install.d.ts.map +1 -1
  9. package/lib/application-control/install.js +7 -2
  10. package/lib/application-control/install.js.map +1 -1
  11. package/lib/application-control/models.d.ts +5 -0
  12. package/lib/application-control/models.d.ts.map +1 -1
  13. package/lib/application-control/models.js +28 -14
  14. package/lib/application-control/models.js.map +1 -1
  15. package/lib/application-control/status.d.ts.map +1 -1
  16. package/lib/application-control/status.js +14 -17
  17. package/lib/application-control/status.js.map +1 -1
  18. package/lib/application-control/utils.js +2 -2
  19. package/lib/application-control/utils.js.map +1 -1
  20. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +5 -5
  21. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  22. package/lib/cloud-connection/device-agent-cloud-connection.js +140 -105
  23. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  24. package/lib/cloud-connection/live-updates-handler.d.ts +4 -2
  25. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  26. package/lib/cloud-connection/live-updates-handler.js +46 -25
  27. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  28. package/lib/cloud-connection/live-updates-handler.test.js +132 -16
  29. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  30. package/lib/cloud-connection/messages.d.ts.map +1 -1
  31. package/lib/cloud-connection/messages.js +3 -4
  32. package/lib/cloud-connection/messages.js.map +1 -1
  33. package/lib/cloud-connection/passthrough-handler.d.ts +5 -3
  34. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  35. package/lib/cloud-connection/passthrough-handler.js +76 -62
  36. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  37. package/lib/cloud-connection/shadow-handler.d.ts +16 -21
  38. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  39. package/lib/cloud-connection/shadow-handler.js +162 -108
  40. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  41. package/lib/cloud-connection/shadow-handler.test.js +100 -83
  42. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  43. package/lib/cloud-connection/transaction-manager.d.ts +3 -0
  44. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  45. package/lib/cloud-connection/transaction-manager.js +11 -0
  46. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  47. package/lib/cloud-connection/transaction-manager.test.js +102 -0
  48. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  49. package/lib/device-control/device-control.d.ts +16 -15
  50. package/lib/device-control/device-control.d.ts.map +1 -1
  51. package/lib/device-control/device-control.js +117 -18
  52. package/lib/device-control/device-control.js.map +1 -1
  53. package/lib/docker/docker-compose.d.ts +14 -0
  54. package/lib/docker/docker-compose.d.ts.map +1 -0
  55. package/lib/docker/docker-compose.js +56 -0
  56. package/lib/docker/docker-compose.js.map +1 -0
  57. package/lib/index.js +2 -5
  58. package/lib/index.js.map +1 -1
  59. package/lib/infrastructure/agent-config.d.ts +45 -14
  60. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  61. package/lib/infrastructure/agent-config.js +30 -15
  62. package/lib/infrastructure/agent-config.js.map +1 -1
  63. package/lib/infrastructure/agent-config.test.js +3 -0
  64. package/lib/infrastructure/agent-config.test.js.map +1 -1
  65. package/lib/local-connection/rabbitmq-connection.js +11 -11
  66. package/lib/local-connection/rabbitmq-connection.js.map +1 -1
  67. package/lib/secure-tunneling/secure-tunneling.d.ts +97 -0
  68. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
  69. package/lib/secure-tunneling/secure-tunneling.js +435 -0
  70. package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
  71. package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
  72. package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
  73. package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
  74. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
  75. package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
  76. package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
  77. package/lib/secure-tunneling/spawner-detached.js +107 -0
  78. package/lib/secure-tunneling/spawner-detached.js.map +1 -0
  79. package/lib/subcommands/app/analytics.d.ts.map +1 -1
  80. package/lib/subcommands/app/analytics.js +9 -13
  81. package/lib/subcommands/app/analytics.js.map +1 -1
  82. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  83. package/lib/subcommands/app/env-vars.js +11 -16
  84. package/lib/subcommands/app/env-vars.js.map +1 -1
  85. package/lib/subcommands/app/models.d.ts.map +1 -1
  86. package/lib/subcommands/app/models.js +12 -16
  87. package/lib/subcommands/app/models.js.map +1 -1
  88. package/lib/subcommands/device/clean.d.ts.map +1 -1
  89. package/lib/subcommands/device/clean.js +8 -6
  90. package/lib/subcommands/device/clean.js.map +1 -1
  91. package/lib/subcommands/device/get-info.d.ts +2 -0
  92. package/lib/subcommands/device/get-info.d.ts.map +1 -0
  93. package/lib/subcommands/device/get-info.js +36 -0
  94. package/lib/subcommands/device/get-info.js.map +1 -0
  95. package/lib/subcommands/device/index.d.ts.map +1 -1
  96. package/lib/subcommands/device/index.js +11 -2
  97. package/lib/subcommands/device/index.js.map +1 -1
  98. package/lib/subcommands/device/init.d.ts +5 -0
  99. package/lib/subcommands/device/init.d.ts.map +1 -0
  100. package/lib/subcommands/device/{device.js → init.js} +5 -36
  101. package/lib/subcommands/device/init.js.map +1 -0
  102. package/lib/subcommands/device/refresh.d.ts +2 -0
  103. package/lib/subcommands/device/refresh.d.ts.map +1 -0
  104. package/lib/subcommands/device/refresh.js +24 -0
  105. package/lib/subcommands/device/refresh.js.map +1 -0
  106. package/lib/subcommands/device/restart.d.ts +2 -0
  107. package/lib/subcommands/device/restart.d.ts.map +1 -0
  108. package/lib/subcommands/device/restart.js +14 -0
  109. package/lib/subcommands/device/restart.js.map +1 -0
  110. package/lib/util/check-for-updates.d.ts +3 -0
  111. package/lib/util/check-for-updates.d.ts.map +1 -0
  112. package/lib/util/check-for-updates.js +69 -0
  113. package/lib/util/check-for-updates.js.map +1 -0
  114. package/lib/util/cloud-mode-ready.d.ts +1 -0
  115. package/lib/util/cloud-mode-ready.d.ts.map +1 -1
  116. package/lib/util/cloud-mode-ready.js +36 -1
  117. package/lib/util/cloud-mode-ready.js.map +1 -1
  118. package/lib/util/file.d.ts +7 -0
  119. package/lib/util/file.d.ts.map +1 -0
  120. package/lib/util/file.js +66 -0
  121. package/lib/util/file.js.map +1 -0
  122. package/lib/util/file.test.d.ts +2 -0
  123. package/lib/util/file.test.d.ts.map +1 -0
  124. package/lib/util/file.test.js +87 -0
  125. package/lib/util/file.test.js.map +1 -0
  126. package/package.json +8 -7
  127. package/readme.md +3 -3
  128. package/src/application-control/config.ts +1 -1
  129. package/src/application-control/environment-variables.test.ts +1 -1
  130. package/src/application-control/environment-variables.ts +9 -6
  131. package/src/application-control/install.ts +8 -3
  132. package/src/application-control/models.ts +47 -19
  133. package/src/application-control/status.ts +16 -14
  134. package/src/application-control/utils.ts +1 -1
  135. package/src/cloud-connection/device-agent-cloud-connection.ts +202 -148
  136. package/src/cloud-connection/live-updates-handler.test.ts +161 -20
  137. package/src/cloud-connection/live-updates-handler.ts +63 -31
  138. package/src/cloud-connection/messages.ts +3 -4
  139. package/src/cloud-connection/passthrough-handler.ts +98 -76
  140. package/src/cloud-connection/shadow-handler.test.ts +101 -84
  141. package/src/cloud-connection/shadow-handler.ts +287 -133
  142. package/src/cloud-connection/transaction-manager.test.ts +124 -0
  143. package/src/cloud-connection/transaction-manager.ts +15 -0
  144. package/src/device-control/device-control.ts +125 -23
  145. package/src/docker/docker-compose.ts +60 -0
  146. package/src/index.ts +2 -6
  147. package/src/infrastructure/agent-config.test.ts +3 -0
  148. package/src/infrastructure/agent-config.ts +38 -40
  149. package/src/local-connection/rabbitmq-connection.ts +8 -8
  150. package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
  151. package/src/secure-tunneling/secure-tunneling.ts +599 -0
  152. package/src/secure-tunneling/spawner-detached.ts +123 -0
  153. package/src/subcommands/app/analytics.ts +16 -13
  154. package/src/subcommands/app/env-vars.ts +18 -16
  155. package/src/subcommands/app/models.ts +20 -16
  156. package/src/subcommands/device/clean.ts +5 -2
  157. package/src/subcommands/device/get-info.ts +49 -0
  158. package/src/subcommands/device/index.ts +11 -2
  159. package/src/subcommands/device/{device.ts → init.ts} +5 -47
  160. package/src/subcommands/device/refresh.ts +22 -0
  161. package/src/subcommands/device/restart.ts +11 -0
  162. package/src/util/check-for-updates.ts +69 -0
  163. package/src/util/cloud-mode-ready.ts +36 -0
  164. package/src/util/file.test.ts +90 -0
  165. package/src/util/file.ts +76 -0
  166. package/lib/docker/docker-compose-cmd.d.ts +0 -5
  167. package/lib/docker/docker-compose-cmd.d.ts.map +0 -1
  168. package/lib/docker/docker-compose-cmd.js +0 -16
  169. package/lib/docker/docker-compose-cmd.js.map +0 -1
  170. package/lib/secure-tunneling/index.d.ts +0 -5
  171. package/lib/secure-tunneling/index.d.ts.map +0 -1
  172. package/lib/secure-tunneling/index.js +0 -64
  173. package/lib/secure-tunneling/index.js.map +0 -1
  174. package/lib/subcommands/device/device.d.ts +0 -7
  175. package/lib/subcommands/device/device.d.ts.map +0 -1
  176. package/lib/subcommands/device/device.js.map +0 -1
  177. package/lib/util/safe-rimraf.d.ts +0 -2
  178. package/lib/util/safe-rimraf.d.ts.map +0 -1
  179. package/lib/util/safe-rimraf.js +0 -16
  180. package/lib/util/safe-rimraf.js.map +0 -1
  181. package/src/docker/docker-compose-cmd.ts +0 -15
  182. package/src/secure-tunneling/index.ts +0 -74
  183. package/src/util/safe-rimraf.ts +0 -14
@@ -1,5 +1,12 @@
1
1
  import { LiveUpdatesHandler } from './live-updates-handler';
2
2
  import { Publisher } from './publisher';
3
+ import sleep from '../util/sleep';
4
+ import { getDeviceStatsPayload } from './messages';
5
+
6
+ jest.mock('../util/sleep');
7
+ jest.mock('./messages');
8
+
9
+ jest.mocked(getDeviceStatsPayload).mockResolvedValue({});
3
10
 
4
11
  global.setTimeout = jest.fn() as unknown as typeof setTimeout;
5
12
 
@@ -18,64 +25,163 @@ const testFalseToggles = {
18
25
  appState: false
19
26
  };
20
27
 
21
- const mockClient = jest.fn();
28
+ const mockClient = {
29
+ publish: jest.fn()
30
+ };
22
31
  const clientId = 'test-client';
23
32
  const emptyTxId = '';
24
33
 
25
- // NOTE: this was the way I found to mock private class functions
26
- const mockStartPublishingLiveUpdates = jest.spyOn(
27
- LiveUpdatesHandler.prototype as any,
28
- 'startPublishingLiveUpdates'
29
- );
30
- mockStartPublishingLiveUpdates.mockResolvedValue(null);
31
-
32
34
  describe('Test Live Updates Handler', () => {
33
35
  let liveUpdatesHandler: LiveUpdatesHandler;
34
36
  let publisher: Publisher;
35
37
 
36
38
  beforeEach(() => {
39
+ mockClient.publish = jest.fn();
40
+ jest.mocked(sleep).mockImplementation(async () => {
41
+ return;
42
+ });
37
43
  publisher = new Publisher(mockClient, clientId);
38
44
  liveUpdatesHandler = new LiveUpdatesHandler(publisher, clientId);
39
45
  jest.clearAllMocks();
40
46
  });
41
47
 
48
+ test('enable device stats', async () => {
49
+ mockClient.publish = jest.fn().mockImplementation(async () => {
50
+ liveUpdatesHandler.disableDeviceStatsLiveUpdates();
51
+ });
52
+
53
+ const enable = { deviceStats: true };
54
+ const promises = await liveUpdatesHandler.handleToggles(enable, emptyTxId);
55
+ await Promise.all(promises);
56
+
57
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
58
+ expect(mockClient.publish).toBeCalledTimes(1);
59
+ expect(jest.mocked(sleep)).toBeCalledTimes(1);
60
+ });
61
+
62
+ test('enable app state', async () => {
63
+ mockClient.publish = jest.fn().mockImplementation(async () => {
64
+ liveUpdatesHandler.disableAppStateLiveUpdates();
65
+ });
66
+
67
+ const enable = { appState: true };
68
+ const promises = await liveUpdatesHandler.handleToggles(enable, emptyTxId);
69
+ await Promise.all(promises);
70
+
71
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
72
+ expect(mockClient.publish).toBeCalledTimes(1);
73
+ expect(jest.mocked(sleep)).toBeCalledTimes(1);
74
+ });
75
+
42
76
  test('ignore subsequent enables', async () => {
43
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
44
- // called twice, once for device stats, once for app state
45
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
46
- // restartLiveUpdatesTimeout is always called once when handleToggles is called
77
+ // Block on the first sleep call
78
+ let doneWaiting;
79
+ const waitPromise = new Promise(function (resolve) {
80
+ doneWaiting = resolve;
81
+ });
82
+
83
+ let startSleeping;
84
+ const sleepPromise = new Promise(function (resolve) {
85
+ startSleeping = resolve;
86
+ });
87
+ jest.mocked(sleep).mockImplementation(async () => {
88
+ startSleeping();
89
+ await waitPromise;
90
+ });
91
+
92
+ const enable = { deviceStats: true };
93
+ await liveUpdatesHandler.handleToggles(enable, emptyTxId);
94
+ await sleepPromise;
95
+
47
96
  expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
97
+ expect(mockClient.publish).toBeCalledTimes(1);
98
+ expect(jest.mocked(sleep)).toBeCalledTimes(1);
48
99
 
49
- // Second call -> should not call startPublishingLiveUpdates should not be called
100
+ // Second call
50
101
  jest.clearAllMocks();
51
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
52
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
102
+
103
+ await liveUpdatesHandler.handleToggles(enable, emptyTxId);
104
+
53
105
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
106
+ expect(mockClient.publish).toBeCalledTimes(0);
107
+ expect(jest.mocked(sleep)).toBeCalledTimes(0);
108
+
109
+ // Disable and clean up
110
+ liveUpdatesHandler.disableDeviceStatsLiveUpdates();
111
+ doneWaiting();
54
112
  });
55
113
 
56
114
  test('test disable live updates', async () => {
115
+ let doneWaiting;
116
+ const waitPromise = new Promise(function (resolve) {
117
+ doneWaiting = resolve;
118
+ });
119
+
120
+ let startSleeping;
121
+ const sleepPromise = new Promise(function (resolve) {
122
+ startSleeping = resolve;
123
+ });
124
+
125
+ // Wait for two sleep calls, one for Device Stats and one for App State
126
+ jest
127
+ .mocked(sleep)
128
+ .mockImplementationOnce(async () => {
129
+ await waitPromise;
130
+ })
131
+ .mockImplementationOnce(async () => {
132
+ startSleeping();
133
+ await waitPromise;
134
+ });
135
+
57
136
  // Test calling handleToggles one time, enabling it
58
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
59
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
137
+ await liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
138
+ await sleepPromise;
139
+
60
140
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
141
+ expect(jest.mocked(sleep)).toBeCalledTimes(2);
61
142
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
62
143
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(true);
63
144
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
64
145
 
65
146
  // Check to see that attributes are properly set to false when disabled
66
147
  jest.clearAllMocks();
67
- void liveUpdatesHandler.handleToggles(testFalseToggles, emptyTxId);
68
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
148
+ await liveUpdatesHandler.handleToggles(testFalseToggles, emptyTxId);
149
+
69
150
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
151
+ expect(jest.mocked(sleep)).toBeCalledTimes(0);
70
152
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);
71
153
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(false);
72
154
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
155
+
156
+ doneWaiting();
73
157
  });
74
158
 
75
159
  test('timeout turns off live updates', async () => {
76
160
  jest.useFakeTimers({ legacyFakeTimers: true });
161
+ let doneWaiting;
162
+ const waitPromise = new Promise(function (resolve) {
163
+ doneWaiting = resolve;
164
+ });
165
+
166
+ let startSleeping;
167
+ const sleepPromise = new Promise(function (resolve) {
168
+ startSleeping = resolve;
169
+ });
170
+
171
+ // Wait for two sleep calls, one for Device Stats and one for App State
172
+ jest
173
+ .mocked(sleep)
174
+ .mockImplementationOnce(async () => {
175
+ await waitPromise;
176
+ })
177
+ .mockImplementationOnce(async () => {
178
+ startSleeping();
179
+ await waitPromise;
180
+ });
181
+
182
+ await liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
183
+ await sleepPromise;
77
184
 
78
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
79
185
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
80
186
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(true);
81
187
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
@@ -85,5 +191,40 @@ describe('Test Live Updates Handler', () => {
85
191
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);
86
192
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(false);
87
193
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
194
+
195
+ doneWaiting();
196
+ });
197
+
198
+ test("failure doesn't kill publish loop", async () => {
199
+ // Block on the first sleep call
200
+ let doneWaiting;
201
+ const waitPromise = new Promise(function (resolve) {
202
+ doneWaiting = resolve;
203
+ });
204
+
205
+ let startSleeping;
206
+ const sleepPromise = new Promise(function (resolve) {
207
+ startSleeping = resolve;
208
+ });
209
+ jest
210
+ .mocked(sleep)
211
+ .mockImplementationOnce(async () => {
212
+ throw new Error('Test error!');
213
+ })
214
+ .mockImplementationOnce(async () => {
215
+ startSleeping();
216
+ await waitPromise;
217
+ });
218
+
219
+ const enable = { deviceStats: true };
220
+ await liveUpdatesHandler.handleToggles(enable, emptyTxId);
221
+ await sleepPromise;
222
+
223
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
224
+ expect(mockClient.publish).toBeCalledTimes(2);
225
+ expect(jest.mocked(sleep)).toBeCalledTimes(2);
226
+
227
+ liveUpdatesHandler.disableDeviceStatsLiveUpdates();
228
+ doneWaiting();
88
229
  });
89
230
  });
@@ -7,19 +7,19 @@ import {
7
7
  buildAppStateMessage,
8
8
  buildDeviceStatsMessage,
9
9
  StatusResponsePayload,
10
- buildToClientStatusResponseMessage
10
+ buildToClientStatusResponseMessage,
11
+ ToClientMessageTypeValue
11
12
  } from '@alwaysai/device-agent-schemas';
12
13
  import { getAppLogs } from '../application-control';
13
14
  import { logger } from '../util/logger';
14
15
  import sleep from '../util/sleep';
15
16
  import { Publisher } from './publisher';
16
17
  import { getAppStatePayload, getDeviceStatsPayload } from './messages';
17
- import { ToClientMessageTypeValue } from '@alwaysai/device-agent-schemas';
18
18
  import { ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS } from '../environment';
19
19
 
20
20
  const LIVE_UPDATES_TIMEOUT = ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS
21
21
  ? parseInt(ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS)
22
- : 60000;
22
+ : 80000;
23
23
 
24
24
  export class LiveUpdatesHandler {
25
25
  private publisher: Publisher;
@@ -42,14 +42,41 @@ export class LiveUpdatesHandler {
42
42
  private appLogStreams = new Set<string>();
43
43
  private transactionStatuses = new Set<string>();
44
44
 
45
+ private async getAppLogsWithRetry(
46
+ projectId: string
47
+ ): Promise<NodeJS.ReadableStream | null> {
48
+ // Retry starting logs until it starts successfully or is terminated
49
+ while (this.appLogStreams.has(projectId)) {
50
+ try {
51
+ return await getAppLogs({
52
+ projectId,
53
+ args: ['--tail', '100', '--no-log-prefix']
54
+ });
55
+ } catch (e) {
56
+ logger.info(
57
+ `Failed to start app logs, retrying in 1 second. Error: ${e}`
58
+ );
59
+ await sleep(1000);
60
+ }
61
+ }
62
+ // Case where logs were disabled prior to connecting
63
+ return null;
64
+ }
65
+
45
66
  private async startAppLogStream(projectId: string, txId: string) {
46
67
  logger.info(`Starting log stream for ${projectId}`);
47
68
 
48
69
  this.appLogStreams.add(projectId);
49
- const readable = await getAppLogs({
50
- projectId,
51
- args: ['--tail', '100', '--no-log-prefix']
52
- });
70
+
71
+ const readable = await this.getAppLogsWithRetry(projectId);
72
+
73
+ if (readable === null) {
74
+ logger.info(
75
+ `App log stream terminated for project ${projectId} prior to starting`
76
+ );
77
+ return;
78
+ }
79
+
53
80
  readable.on('data', async (chunk: Buffer) => {
54
81
  if (!this.appLogStreams.has(projectId)) {
55
82
  // why doesn't typescript know about this function?
@@ -107,22 +134,11 @@ export class LiveUpdatesHandler {
107
134
  return -1;
108
135
  }
109
136
 
110
- private setLiveUpdates(toggles: LiveStateUpdatesTogglePayload) {
111
- if (toggles.deviceStats !== undefined) {
112
- this.liveUpdatesAlive.device_stats = toggles.deviceStats;
113
- }
114
- if (toggles.appState !== undefined) {
115
- this.liveUpdatesAlive.app_state = toggles.appState;
116
- }
117
- }
118
-
119
137
  private restartLiveUpdatesTimeout() {
120
138
  clearTimeout(this.liveUpdatesTimeout);
121
139
  this.liveUpdatesTimeout = setTimeout(() => {
122
- this.setLiveUpdates({
123
- deviceStats: false,
124
- appState: false
125
- });
140
+ this.disableAppStateLiveUpdates();
141
+ this.disableDeviceStatsLiveUpdates();
126
142
  this.appLogStreams.clear();
127
143
  }, LIVE_UPDATES_TIMEOUT);
128
144
  }
@@ -133,17 +149,17 @@ export class LiveUpdatesHandler {
133
149
  txId: string
134
150
  ) {
135
151
  logger.info(`Turned on live updates for ${messageType}`);
136
- try {
137
- while (this.continuePublishing(messageType, txId)) {
152
+ while (this.continuePublishing(messageType, txId)) {
153
+ try {
138
154
  const message = await getMessage();
139
155
  this.publisher.publishToClient(message, logger.silly);
140
156
 
141
157
  await sleep(this.getLiveUpdatesInterval(messageType));
158
+ } catch (e) {
159
+ logger.error(
160
+ `Error publishing live updates for ${messageType}: ${e.message}`
161
+ );
142
162
  }
143
- } catch (e) {
144
- logger.error(
145
- `Error publishing live updates for ${messageType}: ${e.message}`
146
- );
147
163
  }
148
164
  logger.info(`Turned off live updates for ${messageType}`);
149
165
  }
@@ -160,14 +176,24 @@ export class LiveUpdatesHandler {
160
176
  );
161
177
  }
162
178
 
163
- public getDeviceStatsLiveUpdates() {
179
+ public getDeviceStatsLiveUpdates(): boolean {
164
180
  return this.liveUpdatesAlive.device_stats;
165
181
  }
166
182
 
167
- public getAppStateLiveUpdates() {
183
+ public disableDeviceStatsLiveUpdates() {
184
+ logger.info('Disabled live updates for device_stats');
185
+ this.liveUpdatesAlive.device_stats = false;
186
+ }
187
+
188
+ public getAppStateLiveUpdates(): boolean {
168
189
  return this.liveUpdatesAlive.app_state;
169
190
  }
170
191
 
192
+ public disableAppStateLiveUpdates() {
193
+ logger.info('Disabled live updates for app_state');
194
+ this.liveUpdatesAlive.app_state = false;
195
+ }
196
+
171
197
  public getAppLogsLiveUpdates() {
172
198
  return this.liveUpdatesAlive.app_logs;
173
199
  }
@@ -211,12 +237,14 @@ export class LiveUpdatesHandler {
211
237
  const { deviceStats, appState, appLogs } = toggles;
212
238
  this.restartLiveUpdatesTimeout();
213
239
 
240
+ const promises: Promise<void>[] = [];
241
+
214
242
  if (deviceStats !== undefined) {
215
243
  const currentDeviceStats = this.getDeviceStatsLiveUpdates();
216
244
  this.liveUpdatesAlive.device_stats = deviceStats;
217
245
  if (deviceStats && currentDeviceStats !== true) {
218
246
  // Don't wait for this call to finish since it loops until disabled
219
- void this.startPublishingLiveUpdates(
247
+ const deviceStatsPromise = this.startPublishingLiveUpdates(
220
248
  keyMirrors.toClientMessageType.device_stats,
221
249
  async () => {
222
250
  const payload = await getDeviceStatsPayload();
@@ -224,6 +252,7 @@ export class LiveUpdatesHandler {
224
252
  },
225
253
  txId
226
254
  );
255
+ promises.push(deviceStatsPromise);
227
256
  }
228
257
  }
229
258
 
@@ -232,7 +261,7 @@ export class LiveUpdatesHandler {
232
261
  this.liveUpdatesAlive.app_state = appState;
233
262
  if (appState && currentAppState !== true) {
234
263
  // Don't wait for this call to finish since it loops until disabled
235
- void this.startPublishingLiveUpdates(
264
+ const appStatePromise = this.startPublishingLiveUpdates(
236
265
  keyMirrors.toClientMessageType.app_state,
237
266
  async () => {
238
267
  const payload = await getAppStatePayload();
@@ -240,6 +269,7 @@ export class LiveUpdatesHandler {
240
269
  },
241
270
  txId
242
271
  );
272
+ promises.push(appStatePromise);
243
273
  }
244
274
  }
245
275
 
@@ -247,10 +277,12 @@ export class LiveUpdatesHandler {
247
277
  const currentAppLogs = this.getAppLogsLiveUpdates();
248
278
  if (appLogs.toggle && currentAppLogs !== true) {
249
279
  // Don't wait for this call to finish since it loops until disabled
250
- void this.startAppLogStream(appLogs.projectId, txId);
280
+ const appLogPromise = this.startAppLogStream(appLogs.projectId, txId);
281
+ promises.push(appLogPromise);
251
282
  } else {
252
283
  this.appLogStreams.delete(appLogs.projectId);
253
284
  }
254
285
  }
286
+ return promises;
255
287
  }
256
288
  }
@@ -12,15 +12,14 @@ import {
12
12
  import { AgentConfigFile } from '../infrastructure/agent-config';
13
13
 
14
14
  export async function getAppStatePayload(): Promise<AppStatePayload> {
15
- const appState: AppState[] = [];
15
+ const appStatePromises: Promise<AppState>[] = [];
16
16
  const apps = await AgentConfigFile().getApps();
17
17
  for (const app of apps) {
18
18
  const projectId = app.projectId;
19
- const status = await getAppState({ projectId });
20
- appState.push(status);
19
+ appStatePromises.push(getAppState({ projectId }));
21
20
  }
22
21
  const appStatePayload = {
23
- appState: appState
22
+ appState: await Promise.all(appStatePromises)
24
23
  };
25
24
  return appStatePayload;
26
25
  }
@@ -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
- }