@alwaysai/device-agent 1.3.1 → 1.4.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 (91) hide show
  1. package/lib/application-control/environment-variables.d.ts.map +1 -1
  2. package/lib/application-control/environment-variables.js +9 -4
  3. package/lib/application-control/environment-variables.js.map +1 -1
  4. package/lib/application-control/environment-variables.test.js +1 -1
  5. package/lib/application-control/environment-variables.test.js.map +1 -1
  6. package/lib/application-control/install.d.ts.map +1 -1
  7. package/lib/application-control/install.js +6 -2
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/application-control/models.d.ts.map +1 -1
  10. package/lib/application-control/models.js +4 -2
  11. package/lib/application-control/models.js.map +1 -1
  12. package/lib/application-control/status.js +4 -5
  13. package/lib/application-control/status.js.map +1 -1
  14. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +3 -3
  15. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  16. package/lib/cloud-connection/device-agent-cloud-connection.js +114 -99
  17. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  18. package/lib/cloud-connection/live-updates-handler.d.ts +1 -0
  19. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  20. package/lib/cloud-connection/live-updates-handler.js +22 -4
  21. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  22. package/lib/cloud-connection/messages.d.ts.map +1 -1
  23. package/lib/cloud-connection/messages.js +3 -4
  24. package/lib/cloud-connection/messages.js.map +1 -1
  25. package/lib/cloud-connection/shadow-handler.d.ts +14 -21
  26. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/shadow-handler.js +162 -108
  28. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.test.js +100 -83
  30. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  31. package/lib/device-control/device-control.d.ts +7 -14
  32. package/lib/device-control/device-control.d.ts.map +1 -1
  33. package/lib/device-control/device-control.js +37 -14
  34. package/lib/device-control/device-control.js.map +1 -1
  35. package/lib/secure-tunneling/secure-tunneling.d.ts +105 -0
  36. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -0
  37. package/lib/secure-tunneling/secure-tunneling.js +435 -0
  38. package/lib/secure-tunneling/secure-tunneling.js.map +1 -0
  39. package/lib/secure-tunneling/secure-tunneling.test.d.ts +2 -0
  40. package/lib/secure-tunneling/secure-tunneling.test.d.ts.map +1 -0
  41. package/lib/secure-tunneling/secure-tunneling.test.js +1070 -0
  42. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -0
  43. package/lib/secure-tunneling/spawner-detached.d.ts +6 -0
  44. package/lib/secure-tunneling/spawner-detached.d.ts.map +1 -0
  45. package/lib/secure-tunneling/spawner-detached.js +107 -0
  46. package/lib/secure-tunneling/spawner-detached.js.map +1 -0
  47. package/lib/subcommands/app/analytics.d.ts.map +1 -1
  48. package/lib/subcommands/app/analytics.js +9 -13
  49. package/lib/subcommands/app/analytics.js.map +1 -1
  50. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  51. package/lib/subcommands/app/env-vars.js +11 -16
  52. package/lib/subcommands/app/env-vars.js.map +1 -1
  53. package/lib/subcommands/app/models.d.ts.map +1 -1
  54. package/lib/subcommands/app/models.js +12 -16
  55. package/lib/subcommands/app/models.js.map +1 -1
  56. package/lib/subcommands/device/clean.d.ts.map +1 -1
  57. package/lib/subcommands/device/clean.js +3 -1
  58. package/lib/subcommands/device/clean.js.map +1 -1
  59. package/lib/subcommands/device/device.d.ts.map +1 -1
  60. package/lib/subcommands/device/device.js +14 -6
  61. package/lib/subcommands/device/device.js.map +1 -1
  62. package/lib/util/cloud-mode-ready.d.ts +1 -0
  63. package/lib/util/cloud-mode-ready.d.ts.map +1 -1
  64. package/lib/util/cloud-mode-ready.js +36 -1
  65. package/lib/util/cloud-mode-ready.js.map +1 -1
  66. package/package.json +2 -2
  67. package/src/application-control/environment-variables.test.ts +1 -1
  68. package/src/application-control/environment-variables.ts +9 -6
  69. package/src/application-control/install.ts +7 -3
  70. package/src/application-control/models.ts +11 -6
  71. package/src/application-control/status.ts +8 -8
  72. package/src/cloud-connection/device-agent-cloud-connection.ts +161 -131
  73. package/src/cloud-connection/live-updates-handler.ts +34 -6
  74. package/src/cloud-connection/messages.ts +3 -4
  75. package/src/cloud-connection/shadow-handler.test.ts +101 -84
  76. package/src/cloud-connection/shadow-handler.ts +275 -133
  77. package/src/device-control/device-control.ts +46 -19
  78. package/src/secure-tunneling/secure-tunneling.test.ts +1239 -0
  79. package/src/secure-tunneling/secure-tunneling.ts +606 -0
  80. package/src/secure-tunneling/spawner-detached.ts +123 -0
  81. package/src/subcommands/app/analytics.ts +16 -13
  82. package/src/subcommands/app/env-vars.ts +18 -16
  83. package/src/subcommands/app/models.ts +20 -16
  84. package/src/subcommands/device/clean.ts +4 -1
  85. package/src/subcommands/device/device.ts +26 -10
  86. package/src/util/cloud-mode-ready.ts +36 -0
  87. package/lib/secure-tunneling/index.d.ts +0 -5
  88. package/lib/secure-tunneling/index.d.ts.map +0 -1
  89. package/lib/secure-tunneling/index.js +0 -64
  90. package/lib/secure-tunneling/index.js.map +0 -1
  91. package/src/secure-tunneling/index.ts +0 -74
@@ -2,28 +2,28 @@ import {
2
2
  AppConfig,
3
3
  validateAppConfig
4
4
  } from '@alwaysai/app-configuration-schemas';
5
- import { EnvVars, getAllEnvs, readAppCfgFile } from '../application-control';
5
+ import {
6
+ EnvVars,
7
+ getAllEnvs,
8
+ readAppCfgFile,
9
+ setEnv
10
+ } from '../application-control';
11
+ import { getSystemInformation } from '../device-control/device-control';
6
12
  import { logger } from '../util/logger';
7
13
  import { Publisher } from './publisher';
8
14
  import { AppConfigModels, getAppCfgModelsDiff } from './shadow';
9
- import { getSystemInformation } from '../device-control/device-control';
10
- import { generateTxId } from '@alwaysai/device-agent-schemas';
11
-
12
- export interface ShadowTopics {
13
- projects: {
14
- get: string;
15
- getAccepted: string;
16
- getRejected: string;
17
- update: string;
18
- updateDelta: string;
19
- updateAccepted: string;
20
- updateRejected: string;
21
- delete: string;
22
- };
23
- systemInfo: {
24
- update: string;
25
- };
26
- }
15
+ import {
16
+ generateTxId,
17
+ validateProjectShadowUpdate,
18
+ buildBaseShadowMessage,
19
+ buildUpdateProjectShadowMessage,
20
+ buildUpdateSystemInfoShadowMessage,
21
+ getShadowTopic,
22
+ ShadowProjectsUpdateAll,
23
+ getDesiredFromMessage,
24
+ ShadowTopics,
25
+ ProjectShadowUpdate
26
+ } from '@alwaysai/device-agent-schemas';
27
27
 
28
28
  export type AppConfigUpdate = {
29
29
  newAppCfg: AppConfig;
@@ -44,101 +44,193 @@ export type ShadowUpdate = {
44
44
  export class ShadowHandler {
45
45
  private clientId: string;
46
46
  private publisher: Publisher;
47
- public readonly shadowPrefix: string;
48
- public readonly shadowTopics: ShadowTopics;
49
-
47
+ public projectShadowTopics: string[] = [];
48
+ public readonly shadowTopics: { [key: string]: any };
50
49
  constructor(clientId: string, publisher: Publisher) {
51
50
  this.clientId = clientId;
52
51
  this.publisher = publisher;
53
- this.shadowPrefix = `$aws/things/${this.clientId}/shadow/name/`;
54
52
  this.shadowTopics = {
55
- projects: {
56
- get: `${this.shadowPrefix}projects/get`,
57
- getAccepted: `${this.shadowPrefix}projects/get/accepted`,
58
- getRejected: `${this.shadowPrefix}projects/get/rejected`,
59
- update: `${this.shadowPrefix}projects/update`,
60
- updateDelta: `${this.shadowPrefix}projects/update/delta`,
61
- updateAccepted: `${this.shadowPrefix}projects/update/accepted`,
62
- updateRejected: `${this.shadowPrefix}projects/update/rejected`,
63
- delete: `${this.shadowPrefix}projects/delete`
53
+ project: {
54
+ get: getShadowTopic(clientId, 'projects', 'get'),
55
+ getAccepted: getShadowTopic(clientId, 'projects', 'get/accepted'),
56
+ getRejected: getShadowTopic(clientId, 'projects', 'get/rejected'),
57
+ update: getShadowTopic(clientId, 'projects', 'update'),
58
+ updateDelta: getShadowTopic(clientId, 'projects', 'update/delta'),
59
+ updateAccepted: getShadowTopic(clientId, 'projects', 'update/accepted'),
60
+ updateRejected: getShadowTopic(clientId, 'projects', 'update/rejected')
64
61
  },
65
62
  systemInfo: {
66
- update: `${this.shadowPrefix}system-info/update`
63
+ update: getShadowTopic(clientId, 'system-info', 'update')
64
+ },
65
+ secureTunnel: {
66
+ get: getShadowTopic(clientId, 'secure-tunnel', 'get'),
67
+ getAccepted: getShadowTopic(clientId, 'secure-tunnel', 'get/accepted'),
68
+ getRejected: getShadowTopic(clientId, 'secure-tunnel', 'get/rejected'),
69
+ update: getShadowTopic(clientId, 'secure-tunnel', 'update'),
70
+ updateDelta: getShadowTopic(clientId, 'secure-tunnel', 'update/delta'),
71
+ updateAccepted: getShadowTopic(
72
+ clientId,
73
+ 'secure-tunnel',
74
+ 'update/accepted'
75
+ ),
76
+ updateRejected: getShadowTopic(
77
+ clientId,
78
+ 'secure-tunnel',
79
+ 'update/rejected'
80
+ ),
81
+ delete: getShadowTopic(clientId, 'secure-tunnel', 'delete'),
82
+ deleteAccepted: getShadowTopic(
83
+ clientId,
84
+ 'secure-tunnel',
85
+ 'delete/accepted'
86
+ ),
87
+ deleteRejected: getShadowTopic(
88
+ clientId,
89
+ 'secure-tunnel',
90
+ 'delete/rejected'
91
+ )
67
92
  }
68
93
  };
94
+ this.projectShadowTopics.push(this.shadowTopics.project.getAccepted);
95
+ this.projectShadowTopics.push(this.shadowTopics.project.getRejected);
96
+ this.projectShadowTopics.push(this.shadowTopics.project.updateDelta);
97
+ this.projectShadowTopics.push(this.shadowTopics.project.updateAccepted);
98
+ this.projectShadowTopics.push(this.shadowTopics.project.updateRejected);
99
+ }
100
+
101
+ private async generateAppConfigUpdate({
102
+ appConfig,
103
+ txId,
104
+ projectId
105
+ }: {
106
+ appConfig: string;
107
+ txId: string;
108
+ projectId: string;
109
+ }): Promise<AppConfigUpdate | null> {
110
+ let appCfgUpdate: any;
111
+ let newAppCfg: any;
112
+
113
+ // Handle errors and validation
114
+ try {
115
+ newAppCfg = JSON.parse(appConfig);
116
+ } catch (error) {
117
+ logger.error(
118
+ `Could not parse the appConfig for transaction ${txId}!\n${error}`
119
+ );
120
+ return null;
121
+ }
122
+
123
+ if (!validateAppConfig(newAppCfg)) {
124
+ // FIXME: Raise an exception to be handled at higher layer
125
+ logger.error(
126
+ `Received invalid app config for ${projectId}!\n${JSON.stringify(
127
+ validateAppConfig.errors,
128
+ null,
129
+ 2
130
+ )}`
131
+ );
132
+ return null;
133
+ }
134
+
135
+ // If all ok, return the AppConfigUpdate
136
+ logger.info(`Found a delta for app config shadow. Updating ${projectId}`);
137
+ const { updatedModels } = await getAppCfgModelsDiff({
138
+ newAppCfg,
139
+ projectId
140
+ });
141
+
142
+ if (updatedModels && Object.keys(updatedModels).length) {
143
+ appCfgUpdate = { newAppCfg, updatedModels };
144
+ } else {
145
+ appCfgUpdate = { newAppCfg };
146
+ }
147
+ return appCfgUpdate;
69
148
  }
70
149
 
71
- private async handleNamedShadowUpdate({
150
+ private async processProjectShadowUpdates({
72
151
  delta
73
152
  }: {
74
153
  delta: any;
75
154
  }): Promise<ShadowUpdate[]> {
76
- const updates: ShadowUpdate[] = [];
77
-
78
- const deltaKeys = Object.keys(delta);
155
+ const shadowUpdatesPromises: Promise<ShadowUpdate | null>[] = [];
79
156
 
80
- for (const projectId of deltaKeys) {
81
- const projectShadow = delta[projectId];
82
- // For incoming shadow updates, there will be no TxID, so it needs to be generated here.
83
- const txId = generateTxId();
84
- const shadowUpdate: ShadowUpdate = { projectId, txId };
85
-
86
- if (projectShadow.appConfig) {
87
- let newAppCfg: any;
88
- try {
89
- newAppCfg = JSON.parse(projectShadow.appConfig);
90
- } catch (error) {
91
- logger.error(
92
- `Could not parse the appConfig for transaction ${txId}!\n${error}`
93
- );
94
- continue;
95
- }
96
- if (!validateAppConfig(newAppCfg)) {
97
- // FIXME: Raise an exception to be handled at higher layer
157
+ for (const [projectId, projectDelta] of Object.entries(delta)) {
158
+ if (projectDelta) {
159
+ const valid = validateProjectShadowUpdate(projectDelta);
160
+ if (!valid) {
98
161
  logger.error(
99
- `Received invalid app config for ${projectId}!\n${JSON.stringify(
100
- validateAppConfig.errors,
162
+ `Error validating shadow update: ${JSON.stringify(
163
+ { projectDelta, errors: validateProjectShadowUpdate.errors },
101
164
  null,
102
165
  2
103
166
  )}`
104
167
  );
105
168
  continue;
106
169
  }
107
- const { updatedModels } = await getAppCfgModelsDiff({
108
- newAppCfg,
109
- projectId
110
- });
111
170
 
112
- if (updatedModels && Object.keys(updatedModels).length) {
113
- shadowUpdate.appCfgUpdate = { newAppCfg, updatedModels };
114
- } else {
115
- shadowUpdate.appCfgUpdate = { newAppCfg };
116
- }
117
- } else {
118
- logger.info(
119
- `Ignoring app config shadow update for ${projectId} due to no delta`
171
+ shadowUpdatesPromises.push(
172
+ this.processProjectShadowUpdate({
173
+ projectId,
174
+ projectDelta
175
+ })
120
176
  );
121
177
  }
178
+ }
179
+ return (await Promise.all(shadowUpdatesPromises)).filter(
180
+ (promise) => promise !== null
181
+ ) as ShadowUpdate[];
182
+ }
122
183
 
123
- if (projectShadow.envVars) {
124
- const envVars = projectShadow.envVars;
125
- shadowUpdate.envVarUpdate = { envVars };
126
- } else {
127
- logger.info(
128
- `Ignoring app environment variable shadow update for ${projectId} due to no delta`
129
- );
130
- }
131
- if (shadowUpdate.appCfgUpdate || shadowUpdate.envVarUpdate) {
132
- updates.push(shadowUpdate);
184
+ private async processProjectShadowUpdate({
185
+ projectId,
186
+ projectDelta
187
+ }: {
188
+ projectId: string;
189
+ projectDelta: ProjectShadowUpdate;
190
+ }): Promise<ShadowUpdate | null> {
191
+ const txId = generateTxId();
192
+ const shadowUpdate: ShadowUpdate = { projectId, txId };
193
+
194
+ // Handle appConfig Updates
195
+ if (!projectDelta.appConfig) {
196
+ logger.info(
197
+ `Ignoring app config shadow update for ${projectId} due to no delta`
198
+ );
199
+ } else {
200
+ logger.info(`Found a delta for app config shadow. Updating ${projectId}`);
201
+ const appConfig = projectDelta.appConfig;
202
+ const appCfgUpdate = await this.generateAppConfigUpdate({
203
+ appConfig,
204
+ txId,
205
+ projectId
206
+ });
207
+
208
+ if (appCfgUpdate) {
209
+ shadowUpdate.appCfgUpdate = appCfgUpdate;
133
210
  }
134
211
  }
135
212
 
136
- return updates;
213
+ // Handle envVars Updates
214
+ if (!projectDelta.envVars) {
215
+ logger.info(
216
+ `Ignoring app environment variable shadow update for ${projectId} due to no delta`
217
+ );
218
+ } else {
219
+ logger.info(
220
+ `Found a delta for app environment variable shadow. Updating ${projectId}`
221
+ );
222
+ const envVars = projectDelta.envVars;
223
+ shadowUpdate.envVarUpdate = { envVars };
224
+ }
225
+
226
+ return shadowUpdate.appCfgUpdate || shadowUpdate.envVarUpdate
227
+ ? shadowUpdate
228
+ : null;
137
229
  }
138
230
 
139
231
  // Public interface
140
232
 
141
- public async handleShadowTopic({
233
+ public async handleProjectShadow({
142
234
  topic,
143
235
  payload,
144
236
  clientToken
@@ -148,12 +240,35 @@ export class ShadowHandler {
148
240
  clientToken: string;
149
241
  }): Promise<ShadowUpdate[]> {
150
242
  // TODO: make use a function like the other topic getters
151
- const shadowName = topic.split('/')[5];
243
+ if (!this.projectShadowTopics.includes(topic)) {
244
+ throw Error(`Topic ${topic} is not in the ${this.projectShadowTopics}`);
245
+ }
152
246
  switch (topic) {
153
- case this.shadowTopics.projects.updateAccepted:
247
+ case this.shadowTopics.project.updateAccepted: {
154
248
  if (clientToken === this.clientId) {
155
249
  logger.debug(
156
- `Ignoring delta caused by Device Agent: ${JSON.stringify(
250
+ `Ignoring message as it was caused by Device Agent itself: ${JSON.stringify(
251
+ { topic, payload },
252
+ null,
253
+ 2
254
+ )}`
255
+ );
256
+ break;
257
+ }
258
+ const desired = getDesiredFromMessage(payload);
259
+ if (!desired) {
260
+ logger.debug(
261
+ `No desired state found in message: ${JSON.stringify(payload)}`
262
+ );
263
+ } else {
264
+ return await this.processProjectShadowUpdates({ delta: desired });
265
+ }
266
+ break;
267
+ }
268
+ case this.shadowTopics.project.getAccepted: {
269
+ if (clientToken !== this.clientId) {
270
+ logger.debug(
271
+ `Ignoring message as it was caused by Device Agent itself: ${JSON.stringify(
157
272
  { topic, payload },
158
273
  null,
159
274
  2
@@ -161,19 +276,28 @@ export class ShadowHandler {
161
276
  );
162
277
  break;
163
278
  }
164
- return await this.handleNamedShadowUpdate({ delta: payload.desired });
165
- case this.shadowTopics.projects.getAccepted:
166
- if (payload['delta']) {
167
- return await this.handleNamedShadowUpdate({
168
- delta: payload['delta']
279
+ if (payload.state.delta) {
280
+ return await this.processProjectShadowUpdates({
281
+ delta: payload.state.delta
169
282
  });
170
283
  } else {
171
- logger.info(`No delta updates in named shadow '${shadowName}'`);
284
+ logger.info(
285
+ `No delta in projects.getAccepted in named shadow '${
286
+ topic.split('/')[5]
287
+ }'`
288
+ );
172
289
  }
173
290
  break;
291
+ }
292
+ case this.shadowTopics.project.getRejected:
293
+ case this.shadowTopics.project.updateDelta:
294
+ case this.shadowTopics.project.updateRejected: {
295
+ // Not handling these for now
296
+ break;
297
+ }
174
298
  default:
175
299
  logger.info(
176
- `Ignoring shadow message: ${JSON.stringify(
300
+ `Did not match a correct topic. Ignoring shadow message: ${JSON.stringify(
177
301
  { topic, payload },
178
302
  null,
179
303
  2
@@ -185,58 +309,76 @@ export class ShadowHandler {
185
309
 
186
310
  public async updateSystemInfoShadow() {
187
311
  const systemInfo = await getSystemInformation();
188
- const packet = {
189
- state: {
190
- reported: systemInfo
191
- },
192
- clientToken: this.clientId
193
- };
194
- const topic = this.shadowTopics.systemInfo.update;
195
- this.publisher.publish(topic, JSON.stringify(packet));
312
+ this.publisher.publish(
313
+ getShadowTopic(this.clientId, 'system-info', 'update'),
314
+ JSON.stringify(
315
+ buildUpdateSystemInfoShadowMessage(this.clientId, systemInfo)
316
+ )
317
+ );
196
318
  }
197
319
 
198
320
  public async updateProjectShadow(projectId: string) {
199
321
  const appCfg = await readAppCfgFile({ projectId });
200
322
  const envVars = await getAllEnvs({ projectId });
201
323
 
202
- const packet = {
203
- state: {
204
- reported: {
205
- [projectId]: {
206
- appConfig: JSON.stringify(appCfg),
207
- envVars
208
- }
209
- }
210
- },
211
- clientToken: this.clientId
324
+ const toReport: ShadowProjectsUpdateAll = {
325
+ [projectId]: {
326
+ appConfig: JSON.stringify(appCfg),
327
+ envVars
328
+ }
212
329
  };
213
- const topic = this.shadowTopics.projects.update;
214
- this.publisher.publish(topic, JSON.stringify(packet));
330
+ this.publisher.publish(
331
+ getShadowTopic(this.clientId, 'projects', 'update'),
332
+ JSON.stringify(
333
+ buildUpdateProjectShadowMessage({
334
+ clientId: this.clientId,
335
+ reported: toReport
336
+ })
337
+ )
338
+ );
215
339
  }
216
340
 
217
- public getShadowUpdates() {
218
- const topic = this.shadowTopics.projects.get;
219
- const packet = {
220
- clientToken: this.clientId
221
- };
222
- this.publisher.publish(topic, JSON.stringify(packet));
341
+ public async updateProjectEnvVars({
342
+ projectId,
343
+ envVars
344
+ }: {
345
+ projectId: string;
346
+ envVars: EnvVars;
347
+ }) {
348
+ await setEnv({ projectId, envVars });
349
+
350
+ this.publisher.publish(
351
+ getShadowTopic(this.clientId, 'projects', 'update'),
352
+ JSON.stringify(
353
+ buildUpdateProjectShadowMessage({
354
+ clientId: this.clientId,
355
+ reported: {
356
+ [projectId]: {
357
+ envVars
358
+ }
359
+ }
360
+ })
361
+ )
362
+ );
223
363
  }
224
364
 
225
- public clearAppConfig(projectId: string) {
226
- const topic = this.shadowTopics.projects.update;
227
- // TODO: We should actually send only desired and handle the delta
228
- // to update reported
229
- const packet = {
230
- state: {
231
- desired: {
232
- [projectId]: null
233
- },
234
- reported: {
235
- [projectId]: null
236
- }
237
- },
238
- clientToken: this.clientId
239
- };
240
- this.publisher.publish(topic, JSON.stringify(packet));
365
+ public getProjectShadowUpdates() {
366
+ this.publisher.publish(
367
+ getShadowTopic(this.clientId, 'projects', 'get'),
368
+ JSON.stringify(buildBaseShadowMessage(this.clientId))
369
+ );
370
+ }
371
+
372
+ public clearProjectShadow(projectId: string) {
373
+ this.publisher.publish(
374
+ getShadowTopic(this.clientId, 'projects', 'update'),
375
+ JSON.stringify(
376
+ buildUpdateProjectShadowMessage({
377
+ clientId: this.clientId,
378
+ reported: { [projectId]: null },
379
+ desired: { [projectId]: null }
380
+ })
381
+ )
382
+ );
241
383
  }
242
384
  }
@@ -1,38 +1,48 @@
1
- import { SystemInformationPayload } from '@alwaysai/device-agent-schemas';
1
+ import { SystemInformationShadowUpdate } from '@alwaysai/device-agent-schemas';
2
2
  import { logger } from '../util/logger';
3
3
  import * as osu from 'node-os-utils';
4
4
  import * as si from 'systeminformation';
5
5
  import { exec } from 'child_process';
6
6
  import { promisify } from 'util';
7
+ import { DeviceStatsPayload } from '@alwaysai/device-agent-schemas';
7
8
 
8
9
  const exec_promise = promisify(exec);
9
10
 
10
11
  // Device Stats
11
- export async function getCpuDetails() {
12
+ export async function getCpuDetails(): Promise<
13
+ DeviceStatsPayload['cpuDetails']
14
+ > {
12
15
  const cpuFree = await osu.cpu.free();
13
16
  const cpuTemp = await si.cpuTemperature();
14
- return {
15
- usedPerc: 100.0 - cpuFree,
16
- temperature: cpuTemp.main
17
- };
17
+ const cpuDetails: DeviceStatsPayload['cpuDetails'] = {};
18
+ if (cpuFree !== null) cpuDetails.usedPerc = 100 - cpuFree;
19
+ if (cpuTemp?.main !== null) cpuDetails.temperature = cpuTemp.main;
20
+ return cpuDetails;
18
21
  }
19
22
 
20
- export async function getDiskDetails() {
23
+ export async function getDiskDetails(): Promise<
24
+ DeviceStatsPayload['diskDetails']
25
+ > {
21
26
  // Types incorrectly specify diskname as required instead of optional
22
27
  // @ts-expect-error
23
28
  const driveInfo = await osu.drive.info();
24
- return {
25
- usedGb: parseFloat(driveInfo.usedGb),
26
- freeGb: parseFloat(driveInfo.freeGb)
27
- };
29
+ const diskDetails: DeviceStatsPayload['diskDetails'] = {};
30
+ if (driveInfo?.usedGb !== null)
31
+ diskDetails.usedGb = parseFloat(driveInfo.usedGb);
32
+ if (driveInfo?.freeGb !== null) {
33
+ diskDetails.freeGb = parseFloat(driveInfo.freeGb);
34
+ }
35
+ return diskDetails;
28
36
  }
29
37
 
30
- export async function getMemDetails() {
38
+ export async function getMemDetails(): Promise<
39
+ DeviceStatsPayload['memDetails']
40
+ > {
31
41
  const memInfo = await osu.mem.info();
32
- return {
33
- usedMb: memInfo.usedMemMb,
34
- freeMb: memInfo.freeMemMb
35
- };
42
+ const memDetails: DeviceStatsPayload['memDetails'] = {};
43
+ if (memInfo?.usedMemMb !== null) memDetails.usedMb = memInfo.usedMemMb;
44
+ if (memInfo?.freeMemMb !== null) memDetails.freeMb = memInfo.freeMemMb;
45
+ return memDetails;
36
46
  }
37
47
 
38
48
  // System information
@@ -113,15 +123,32 @@ export async function getPackageVersions() {
113
123
  };
114
124
  }
115
125
 
116
- export async function getSystemInformation(): Promise<SystemInformationPayload> {
126
+ export async function getLastBootTime() {
127
+ try {
128
+ const test = await exec_promise(
129
+ "last -x reboot | grep reboot | head -n 1 | awk '{ print $5, $6, $7, $8 }'"
130
+ );
131
+ if (test.stderr) {
132
+ logger.error(`Stderr when getting last boot time: ${test.stderr}`);
133
+ return undefined;
134
+ }
135
+ return new Date(test.stdout).toString();
136
+ } catch (e) {
137
+ logger.error(`Issue getting last boot time: ${e.message}`);
138
+ return undefined;
139
+ }
140
+ }
141
+
142
+ export async function getSystemInformation(): Promise<SystemInformationShadowUpdate> {
117
143
  try {
118
- const systemInfo: SystemInformationPayload = {
144
+ const systemInfo: SystemInformationShadowUpdate = {
119
145
  os: await getOsInfo(),
120
146
  cpu: await getCpuInfo(),
121
147
  disk: await getDiskInfo(),
122
148
  device: await getDeviceInfo(),
123
149
  network: await getNetworkInfo(),
124
- versions: await getPackageVersions()
150
+ versions: await getPackageVersions(),
151
+ lastBootTime: await getLastBootTime()
125
152
  };
126
153
  return systemInfo;
127
154
  } catch (e) {