@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.
- package/lib/application-control/config.js +2 -2
- package/lib/application-control/config.js.map +1 -1
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/application-control/environment-variables.js +9 -4
- package/lib/application-control/environment-variables.js.map +1 -1
- package/lib/application-control/environment-variables.test.js +1 -1
- package/lib/application-control/environment-variables.test.js.map +1 -1
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +7 -2
- package/lib/application-control/install.js.map +1 -1
- package/lib/application-control/models.d.ts +5 -0
- package/lib/application-control/models.d.ts.map +1 -1
- package/lib/application-control/models.js +28 -14
- package/lib/application-control/models.js.map +1 -1
- package/lib/application-control/status.d.ts.map +1 -1
- package/lib/application-control/status.js +14 -17
- package/lib/application-control/status.js.map +1 -1
- package/lib/application-control/utils.js +2 -2
- package/lib/application-control/utils.js.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +5 -5
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +140 -105
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts +4 -2
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +46 -25
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.test.js +132 -16
- package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
- package/lib/cloud-connection/messages.d.ts.map +1 -1
- package/lib/cloud-connection/messages.js +3 -4
- package/lib/cloud-connection/messages.js.map +1 -1
- package/lib/cloud-connection/passthrough-handler.d.ts +5 -3
- package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
- package/lib/cloud-connection/passthrough-handler.js +76 -62
- package/lib/cloud-connection/passthrough-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts +16 -21
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +162 -108
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.test.js +100 -83
- package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +3 -0
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +11 -0
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +102 -0
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/device-control/device-control.d.ts +16 -15
- package/lib/device-control/device-control.d.ts.map +1 -1
- package/lib/device-control/device-control.js +117 -18
- package/lib/device-control/device-control.js.map +1 -1
- package/lib/docker/docker-compose.d.ts +14 -0
- package/lib/docker/docker-compose.d.ts.map +1 -0
- package/lib/docker/docker-compose.js +56 -0
- package/lib/docker/docker-compose.js.map +1 -0
- package/lib/index.js +2 -5
- package/lib/index.js.map +1 -1
- package/lib/infrastructure/agent-config.d.ts +45 -14
- package/lib/infrastructure/agent-config.d.ts.map +1 -1
- package/lib/infrastructure/agent-config.js +30 -15
- package/lib/infrastructure/agent-config.js.map +1 -1
- package/lib/infrastructure/agent-config.test.js +3 -0
- package/lib/infrastructure/agent-config.test.js.map +1 -1
- package/lib/local-connection/rabbitmq-connection.js +11 -11
- package/lib/local-connection/rabbitmq-connection.js.map +1 -1
- package/lib/secure-tunneling/secure-tunneling.d.ts +97 -0
- package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
- package/lib/secure-tunneling/secure-tunneling.js +435 -0
- package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
- package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
- package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
- package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
- package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
- package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
- package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
- package/lib/secure-tunneling/spawner-detached.js +107 -0
- package/lib/secure-tunneling/spawner-detached.js.map +1 -0
- package/lib/subcommands/app/analytics.d.ts.map +1 -1
- package/lib/subcommands/app/analytics.js +9 -13
- package/lib/subcommands/app/analytics.js.map +1 -1
- package/lib/subcommands/app/env-vars.d.ts.map +1 -1
- package/lib/subcommands/app/env-vars.js +11 -16
- package/lib/subcommands/app/env-vars.js.map +1 -1
- package/lib/subcommands/app/models.d.ts.map +1 -1
- package/lib/subcommands/app/models.js +12 -16
- package/lib/subcommands/app/models.js.map +1 -1
- package/lib/subcommands/device/clean.d.ts.map +1 -1
- package/lib/subcommands/device/clean.js +8 -6
- package/lib/subcommands/device/clean.js.map +1 -1
- package/lib/subcommands/device/get-info.d.ts +2 -0
- package/lib/subcommands/device/get-info.d.ts.map +1 -0
- package/lib/subcommands/device/get-info.js +36 -0
- package/lib/subcommands/device/get-info.js.map +1 -0
- package/lib/subcommands/device/index.d.ts.map +1 -1
- package/lib/subcommands/device/index.js +11 -2
- package/lib/subcommands/device/index.js.map +1 -1
- package/lib/subcommands/device/init.d.ts +5 -0
- package/lib/subcommands/device/init.d.ts.map +1 -0
- package/lib/subcommands/device/{device.js → init.js} +5 -36
- package/lib/subcommands/device/init.js.map +1 -0
- package/lib/subcommands/device/refresh.d.ts +2 -0
- package/lib/subcommands/device/refresh.d.ts.map +1 -0
- package/lib/subcommands/device/refresh.js +24 -0
- package/lib/subcommands/device/refresh.js.map +1 -0
- package/lib/subcommands/device/restart.d.ts +2 -0
- package/lib/subcommands/device/restart.d.ts.map +1 -0
- package/lib/subcommands/device/restart.js +14 -0
- package/lib/subcommands/device/restart.js.map +1 -0
- package/lib/util/check-for-updates.d.ts +3 -0
- package/lib/util/check-for-updates.d.ts.map +1 -0
- package/lib/util/check-for-updates.js +69 -0
- package/lib/util/check-for-updates.js.map +1 -0
- package/lib/util/cloud-mode-ready.d.ts +1 -0
- package/lib/util/cloud-mode-ready.d.ts.map +1 -1
- package/lib/util/cloud-mode-ready.js +36 -1
- package/lib/util/cloud-mode-ready.js.map +1 -1
- package/lib/util/file.d.ts +7 -0
- package/lib/util/file.d.ts.map +1 -0
- package/lib/util/file.js +66 -0
- package/lib/util/file.js.map +1 -0
- package/lib/util/file.test.d.ts +2 -0
- package/lib/util/file.test.d.ts.map +1 -0
- package/lib/util/file.test.js +87 -0
- package/lib/util/file.test.js.map +1 -0
- package/package.json +8 -7
- package/readme.md +3 -3
- package/src/application-control/config.ts +1 -1
- package/src/application-control/environment-variables.test.ts +1 -1
- package/src/application-control/environment-variables.ts +9 -6
- package/src/application-control/install.ts +8 -3
- package/src/application-control/models.ts +47 -19
- package/src/application-control/status.ts +16 -14
- package/src/application-control/utils.ts +1 -1
- package/src/cloud-connection/device-agent-cloud-connection.ts +202 -148
- package/src/cloud-connection/live-updates-handler.test.ts +161 -20
- package/src/cloud-connection/live-updates-handler.ts +63 -31
- package/src/cloud-connection/messages.ts +3 -4
- package/src/cloud-connection/passthrough-handler.ts +98 -76
- package/src/cloud-connection/shadow-handler.test.ts +101 -84
- package/src/cloud-connection/shadow-handler.ts +287 -133
- package/src/cloud-connection/transaction-manager.test.ts +124 -0
- package/src/cloud-connection/transaction-manager.ts +15 -0
- package/src/device-control/device-control.ts +125 -23
- package/src/docker/docker-compose.ts +60 -0
- package/src/index.ts +2 -6
- package/src/infrastructure/agent-config.test.ts +3 -0
- package/src/infrastructure/agent-config.ts +38 -40
- package/src/local-connection/rabbitmq-connection.ts +8 -8
- package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
- package/src/secure-tunneling/secure-tunneling.ts +599 -0
- package/src/secure-tunneling/spawner-detached.ts +123 -0
- package/src/subcommands/app/analytics.ts +16 -13
- package/src/subcommands/app/env-vars.ts +18 -16
- package/src/subcommands/app/models.ts +20 -16
- package/src/subcommands/device/clean.ts +5 -2
- package/src/subcommands/device/get-info.ts +49 -0
- package/src/subcommands/device/index.ts +11 -2
- package/src/subcommands/device/{device.ts → init.ts} +5 -47
- package/src/subcommands/device/refresh.ts +22 -0
- package/src/subcommands/device/restart.ts +11 -0
- package/src/util/check-for-updates.ts +69 -0
- package/src/util/cloud-mode-ready.ts +36 -0
- package/src/util/file.test.ts +90 -0
- package/src/util/file.ts +76 -0
- package/lib/docker/docker-compose-cmd.d.ts +0 -5
- package/lib/docker/docker-compose-cmd.d.ts.map +0 -1
- package/lib/docker/docker-compose-cmd.js +0 -16
- package/lib/docker/docker-compose-cmd.js.map +0 -1
- package/lib/secure-tunneling/index.d.ts +0 -5
- package/lib/secure-tunneling/index.d.ts.map +0 -1
- package/lib/secure-tunneling/index.js +0 -64
- package/lib/secure-tunneling/index.js.map +0 -1
- package/lib/subcommands/device/device.d.ts +0 -7
- package/lib/subcommands/device/device.d.ts.map +0 -1
- package/lib/subcommands/device/device.js.map +0 -1
- package/lib/util/safe-rimraf.d.ts +0 -2
- package/lib/util/safe-rimraf.d.ts.map +0 -1
- package/lib/util/safe-rimraf.js +0 -16
- package/lib/util/safe-rimraf.js.map +0 -1
- package/src/docker/docker-compose-cmd.ts +0 -15
- package/src/secure-tunneling/index.ts +0 -74
- 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 =
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
100
|
+
// Second call
|
|
50
101
|
jest.clearAllMocks();
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
123
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
appState.push(status);
|
|
19
|
+
appStatePromises.push(getAppState({ projectId }));
|
|
21
20
|
}
|
|
22
21
|
const appStatePayload = {
|
|
23
|
-
appState:
|
|
22
|
+
appState: await Promise.all(appStatePromises)
|
|
24
23
|
};
|
|
25
24
|
return appStatePayload;
|
|
26
25
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
const amqp = require('amqplib');
|
|
1
|
+
import * as amqp from 'amqplib';
|
|
3
2
|
import {
|
|
4
3
|
LOCAL_CONNECTION_HOST,
|
|
5
4
|
LOCAL_CONNECTION_PORT,
|
|
6
5
|
LOCAL_CONNECTION_ROUTING_KEY
|
|
7
6
|
} from '../local-connection/constants';
|
|
8
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
stopRabbitMQContainer,
|
|
9
|
+
setupRabbitMQContainer
|
|
10
|
+
} from '../local-connection/rabbitmq-connection';
|
|
9
11
|
import { logger } from '../util/logger';
|
|
10
12
|
import sleep from '../util/sleep';
|
|
11
13
|
import { Publisher } from './publisher';
|
|
@@ -16,21 +18,84 @@ const MAX_LOCAL_CONNECTION_ATTEMPTS = 10;
|
|
|
16
18
|
|
|
17
19
|
export class PassthroughHandler {
|
|
18
20
|
public publisher: Publisher;
|
|
19
|
-
public connection;
|
|
20
|
-
public channel;
|
|
21
|
+
public connection: amqp.Connection | undefined;
|
|
22
|
+
public channel: amqp.Channel;
|
|
21
23
|
public packetQueue;
|
|
22
24
|
|
|
23
25
|
constructor(publisher: Publisher) {
|
|
24
26
|
this.publisher = publisher;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
runChannel = async () => {
|
|
30
|
+
logger.debug('Beginning to consume packets');
|
|
31
|
+
await this.channel.consume(
|
|
32
|
+
this.packetQueue,
|
|
33
|
+
(msg) => {
|
|
34
|
+
// NOTE: this needs to be an arrow function and then the whole contents of processPublish are below
|
|
35
|
+
if (msg?.content !== undefined) {
|
|
36
|
+
const packet = JSON.parse(msg.content.toString());
|
|
37
|
+
messageQueue.push({ packet, msg });
|
|
38
|
+
while (messageQueue.length > 0) {
|
|
39
|
+
const entry = messageQueue.shift();
|
|
40
|
+
const { packet, msg } = entry;
|
|
41
|
+
try {
|
|
42
|
+
const parsedPacket = JSON.parse(packet);
|
|
43
|
+
if (parsedPacket?.['action']) {
|
|
44
|
+
switch (parsedPacket['action']) {
|
|
45
|
+
case 'analytics':
|
|
46
|
+
ackQueue.push(msg);
|
|
47
|
+
// FIXME: put real topic here
|
|
48
|
+
this.publisher.publishToCloudWithAck(
|
|
49
|
+
packet,
|
|
50
|
+
(errOrResp) => {
|
|
51
|
+
while (ackQueue.length > 0) {
|
|
52
|
+
const msg = ackQueue.shift();
|
|
53
|
+
if (errOrResp === true) {
|
|
54
|
+
this.channel.ack(msg); // acknowledge, allow queue to discard
|
|
55
|
+
} else if (errOrResp === false) {
|
|
56
|
+
this.channel.reject(msg, true); // reject and requeue
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
break;
|
|
62
|
+
case 'heartbeat':
|
|
63
|
+
this.channel.ack(msg);
|
|
64
|
+
logger.silly(
|
|
65
|
+
`Heartbeat package received & acknowledged: ${packet}`
|
|
66
|
+
);
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
this.channel.ack(msg);
|
|
70
|
+
logger.debug(
|
|
71
|
+
`Unknown 'action' package received & acknowledged: ${packet}`
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
this.channel.ack(msg);
|
|
77
|
+
logger.debug(
|
|
78
|
+
`Received & acknowledged a RabbitMQ Package of unknown structure: ${parsedPacket}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
logger.error(`There was a problem parsing RabbitMQ packet ${e}`);
|
|
83
|
+
this.channel.ack(msg);
|
|
84
|
+
logger.debug(`Problematic packet was acknowledged`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
async establishLocalConnection(): Promise<void> {
|
|
29
96
|
let connectAttempts = 0;
|
|
30
97
|
let connected = false;
|
|
31
|
-
logger.debug(
|
|
32
|
-
`Setting up alwaysAI Local Connection on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
33
|
-
);
|
|
98
|
+
logger.debug(`Establishing local connection...`);
|
|
34
99
|
while (
|
|
35
100
|
connectAttempts <= MAX_LOCAL_CONNECTION_ATTEMPTS &&
|
|
36
101
|
this.connection === undefined
|
|
@@ -40,6 +105,13 @@ export class PassthroughHandler {
|
|
|
40
105
|
`amqp://${LOCAL_CONNECTION_HOST}:${LOCAL_CONNECTION_PORT}`
|
|
41
106
|
);
|
|
42
107
|
this.channel = await this.connection.createChannel();
|
|
108
|
+
this.connection.on('error', async () => {
|
|
109
|
+
logger.error(`Local connection failed. Attempting to reconnect...`);
|
|
110
|
+
await stopRabbitMQContainer();
|
|
111
|
+
this.connection = undefined;
|
|
112
|
+
await this.setup();
|
|
113
|
+
});
|
|
114
|
+
|
|
43
115
|
connected = true;
|
|
44
116
|
} catch (e) {
|
|
45
117
|
const timeTillNextAttemptMs = 1000 + 1000 * connectAttempts;
|
|
@@ -53,84 +125,34 @@ export class PassthroughHandler {
|
|
|
53
125
|
}
|
|
54
126
|
}
|
|
55
127
|
if (connected === true) {
|
|
56
|
-
this.channel.prefetch(1); // This ensures we only get one packet at a time! This appears to have prevented throttling
|
|
128
|
+
await this.channel.prefetch(1); // This ensures we only get one packet at a time! This appears to have prevented throttling
|
|
57
129
|
this.packetQueue = `${LOCAL_CONNECTION_ROUTING_KEY}`;
|
|
58
130
|
await this.channel.assertQueue(this.packetQueue, {
|
|
59
131
|
durable: true
|
|
60
132
|
});
|
|
133
|
+
logger.info(`Local connection established.`);
|
|
61
134
|
} else {
|
|
62
135
|
throw new Error(
|
|
63
136
|
'Unable to establish connection to alwaysAI Local Connection, please try restarting Device Agent.'
|
|
64
137
|
);
|
|
65
138
|
}
|
|
66
139
|
}
|
|
67
|
-
}
|
|
68
140
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
public async setup() {
|
|
142
|
+
logger.debug(
|
|
143
|
+
`Setting up alwaysAI Local Connection on host: ${LOCAL_CONNECTION_HOST} and channel key: ${LOCAL_CONNECTION_ROUTING_KEY}`
|
|
144
|
+
);
|
|
145
|
+
await setupRabbitMQContainer();
|
|
73
146
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
while (ackQueue.length > 0) {
|
|
84
|
-
const msg = ackQueue.shift();
|
|
85
|
-
if (errOrResp === true) {
|
|
86
|
-
passthroughHandler.channel.ack(msg); // acknowledge, allow queue to discard
|
|
87
|
-
} else if (errOrResp === false) {
|
|
88
|
-
passthroughHandler.channel.reject(msg, true); // reject and requeue
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
break;
|
|
94
|
-
case 'heartbeat':
|
|
95
|
-
passthroughHandler.channel.ack(msg);
|
|
96
|
-
logger.debug(
|
|
97
|
-
`Heartbeat package received & acknowledged: ${packet}`
|
|
98
|
-
);
|
|
99
|
-
break;
|
|
100
|
-
default:
|
|
101
|
-
passthroughHandler.channel.ack(msg);
|
|
102
|
-
logger.debug(
|
|
103
|
-
`Unknown 'action' package received & acknowledged: ${packet}`
|
|
104
|
-
);
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
passthroughHandler.channel.ack(msg);
|
|
109
|
-
logger.debug(
|
|
110
|
-
`Received & acknowledged a RabbitMQ Package of unknown structure: ${parsedPacket}`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
} catch (e) {
|
|
114
|
-
logger.error(`There was a problem parsing RabbitMQ packet ${e}`);
|
|
115
|
-
passthroughHandler.channel.ack(msg);
|
|
116
|
-
logger.debug(`Problematic packet was acknowledged`);
|
|
147
|
+
await this.establishLocalConnection();
|
|
148
|
+
await this.runChannel();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logger.error(
|
|
151
|
+
`There was a problem maintaining RabbitMQ connection: ${error}`
|
|
152
|
+
);
|
|
153
|
+
throw new Error(
|
|
154
|
+
`There was a problem maintaining RabbitMQ connection: ${error}`
|
|
155
|
+
);
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
158
|
}
|
|
120
|
-
|
|
121
|
-
export async function runChannel(passthroughHandler: PassthroughHandler) {
|
|
122
|
-
logger.debug('Beginning to consume packets');
|
|
123
|
-
passthroughHandler.channel.consume(
|
|
124
|
-
passthroughHandler.packetQueue,
|
|
125
|
-
function (msg) {
|
|
126
|
-
if (msg.content !== undefined) {
|
|
127
|
-
const packet = JSON.parse(msg.content.toString());
|
|
128
|
-
messageQueue.push({ packet, msg });
|
|
129
|
-
processPublish(passthroughHandler);
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
|
|
134
|
-
}
|
|
135
|
-
);
|
|
136
|
-
}
|