@alwaysai/device-agent 1.5.0 → 2.0.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 (273) hide show
  1. package/lib/application-control/config.d.ts.map +1 -1
  2. package/lib/application-control/config.js +8 -3
  3. package/lib/application-control/config.js.map +1 -1
  4. package/lib/application-control/environment-variables.d.ts +1 -5
  5. package/lib/application-control/environment-variables.d.ts.map +1 -1
  6. package/lib/application-control/environment-variables.js +9 -26
  7. package/lib/application-control/environment-variables.js.map +1 -1
  8. package/lib/application-control/environment-variables.test.js +27 -7
  9. package/lib/application-control/environment-variables.test.js.map +1 -1
  10. package/lib/application-control/index.d.ts +4 -4
  11. package/lib/application-control/index.d.ts.map +1 -1
  12. package/lib/application-control/index.js +1 -4
  13. package/lib/application-control/index.js.map +1 -1
  14. package/lib/application-control/install.d.ts.map +1 -1
  15. package/lib/application-control/install.js +8 -7
  16. package/lib/application-control/install.js.map +1 -1
  17. package/lib/application-control/models.d.ts +0 -11
  18. package/lib/application-control/models.d.ts.map +1 -1
  19. package/lib/application-control/models.js +5 -54
  20. package/lib/application-control/models.js.map +1 -1
  21. package/lib/application-control/utils.d.ts +0 -4
  22. package/lib/application-control/utils.d.ts.map +1 -1
  23. package/lib/application-control/utils.js +1 -24
  24. package/lib/application-control/utils.js.map +1 -1
  25. package/lib/cloud-connection/bootstrap-provision.js +3 -2
  26. package/lib/cloud-connection/bootstrap-provision.js.map +1 -1
  27. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +10 -15
  28. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  29. package/lib/cloud-connection/device-agent-cloud-connection.js +279 -250
  30. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  31. package/lib/cloud-connection/device-agent.d.ts.map +1 -1
  32. package/lib/cloud-connection/device-agent.js +11 -9
  33. package/lib/cloud-connection/device-agent.js.map +1 -1
  34. package/lib/cloud-connection/live-updates-handler.d.ts +18 -28
  35. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  36. package/lib/cloud-connection/live-updates-handler.js +54 -169
  37. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  38. package/lib/cloud-connection/live-updates-handler.test.js +71 -165
  39. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  40. package/lib/cloud-connection/passthrough-handler.d.ts +4 -1
  41. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  42. package/lib/cloud-connection/passthrough-handler.js +30 -11
  43. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  44. package/lib/cloud-connection/shadow-handler.d.ts +5 -3
  45. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  46. package/lib/cloud-connection/shadow-handler.js +59 -27
  47. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  48. package/lib/cloud-connection/shadow-handler.test.js +45 -57
  49. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  50. package/lib/cloud-connection/shadow.d.ts.map +1 -1
  51. package/lib/cloud-connection/shadow.js +2 -1
  52. package/lib/cloud-connection/shadow.js.map +1 -1
  53. package/lib/cloud-connection/transaction-manager.d.ts +4 -2
  54. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  55. package/lib/cloud-connection/transaction-manager.js +18 -29
  56. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  57. package/lib/cloud-connection/transaction-manager.test.js +3 -3
  58. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  59. package/lib/device-control/device-control.d.ts +8 -8
  60. package/lib/device-control/device-control.d.ts.map +1 -1
  61. package/lib/device-control/device-control.js +95 -71
  62. package/lib/device-control/device-control.js.map +1 -1
  63. package/lib/docker/docker-compose.d.ts.map +1 -1
  64. package/lib/docker/docker-compose.js +2 -1
  65. package/lib/docker/docker-compose.js.map +1 -1
  66. package/lib/infrastructure/agent-config.d.ts +2 -1
  67. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  68. package/lib/infrastructure/agent-config.js +7 -7
  69. package/lib/infrastructure/agent-config.js.map +1 -1
  70. package/lib/infrastructure/agent-config.test.js +3 -1
  71. package/lib/infrastructure/agent-config.test.js.map +1 -1
  72. package/lib/infrastructure/config-check-utility.d.ts +6 -0
  73. package/lib/infrastructure/config-check-utility.d.ts.map +1 -0
  74. package/lib/infrastructure/config-check-utility.js +67 -0
  75. package/lib/infrastructure/config-check-utility.js.map +1 -0
  76. package/lib/infrastructure/config-check-utility.test.d.ts +2 -0
  77. package/lib/infrastructure/config-check-utility.test.d.ts.map +1 -0
  78. package/lib/infrastructure/config-check-utility.test.js +109 -0
  79. package/lib/infrastructure/config-check-utility.test.js.map +1 -0
  80. package/lib/infrastructure/device-certificate.d.ts +10 -0
  81. package/lib/infrastructure/device-certificate.d.ts.map +1 -0
  82. package/lib/infrastructure/device-certificate.js +47 -0
  83. package/lib/infrastructure/device-certificate.js.map +1 -0
  84. package/lib/infrastructure/device-certificate.test.d.ts +2 -0
  85. package/lib/infrastructure/device-certificate.test.d.ts.map +1 -0
  86. package/lib/infrastructure/device-certificate.test.js +24 -0
  87. package/lib/infrastructure/device-certificate.test.js.map +1 -0
  88. package/lib/infrastructure/legacy-migration/legacy-file.test.d.ts +2 -0
  89. package/lib/infrastructure/legacy-migration/legacy-file.test.d.ts.map +1 -0
  90. package/lib/infrastructure/legacy-migration/legacy-file.test.js +61 -0
  91. package/lib/infrastructure/legacy-migration/legacy-file.test.js.map +1 -0
  92. package/lib/infrastructure/legacy-migration/legacy-files.d.ts +75 -0
  93. package/lib/infrastructure/legacy-migration/legacy-files.d.ts.map +1 -0
  94. package/lib/infrastructure/legacy-migration/legacy-files.js +75 -0
  95. package/lib/infrastructure/legacy-migration/legacy-files.js.map +1 -0
  96. package/lib/infrastructure/legacy-migration/legacy-migration.d.ts +6 -0
  97. package/lib/infrastructure/legacy-migration/legacy-migration.d.ts.map +1 -0
  98. package/lib/infrastructure/legacy-migration/legacy-migration.js +149 -0
  99. package/lib/infrastructure/legacy-migration/legacy-migration.js.map +1 -0
  100. package/lib/infrastructure/legacy-migration/legacy-migration.test.d.ts +2 -0
  101. package/lib/infrastructure/legacy-migration/legacy-migration.test.d.ts.map +1 -0
  102. package/lib/infrastructure/legacy-migration/legacy-migration.test.js +226 -0
  103. package/lib/infrastructure/legacy-migration/legacy-migration.test.js.map +1 -0
  104. package/lib/infrastructure/require-files-present-ready.test.d.ts +2 -0
  105. package/lib/infrastructure/require-files-present-ready.test.d.ts.map +1 -0
  106. package/lib/infrastructure/require-files-present-ready.test.js +44 -0
  107. package/lib/infrastructure/require-files-present-ready.test.js.map +1 -0
  108. package/lib/infrastructure/required-config-checks.d.ts +2 -0
  109. package/lib/infrastructure/required-config-checks.d.ts.map +1 -0
  110. package/lib/infrastructure/required-config-checks.js +30 -0
  111. package/lib/infrastructure/required-config-checks.js.map +1 -0
  112. package/lib/infrastructure/tokens-and-device-cfg.d.ts.map +1 -1
  113. package/lib/infrastructure/tokens-and-device-cfg.js +11 -8
  114. package/lib/infrastructure/tokens-and-device-cfg.js.map +1 -1
  115. package/lib/local-connection/rabbitmq-connection.d.ts.map +1 -1
  116. package/lib/local-connection/rabbitmq-connection.js +14 -14
  117. package/lib/local-connection/rabbitmq-connection.js.map +1 -1
  118. package/lib/secure-tunneling/secure-tunneling.d.ts +9 -9
  119. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -1
  120. package/lib/secure-tunneling/secure-tunneling.js +21 -16
  121. package/lib/secure-tunneling/secure-tunneling.js.map +1 -1
  122. package/lib/secure-tunneling/secure-tunneling.test.js +11 -13
  123. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -1
  124. package/lib/subcommands/app/analytics.d.ts.map +1 -1
  125. package/lib/subcommands/app/analytics.js +1 -2
  126. package/lib/subcommands/app/analytics.js.map +1 -1
  127. package/lib/subcommands/app/env-vars.d.ts +4 -0
  128. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  129. package/lib/subcommands/app/env-vars.js +52 -6
  130. package/lib/subcommands/app/env-vars.js.map +1 -1
  131. package/lib/subcommands/app/index.d.ts.map +1 -1
  132. package/lib/subcommands/app/index.js +1 -3
  133. package/lib/subcommands/app/index.js.map +1 -1
  134. package/lib/subcommands/app/models.d.ts +0 -11
  135. package/lib/subcommands/app/models.d.ts.map +1 -1
  136. package/lib/subcommands/app/models.js +2 -58
  137. package/lib/subcommands/app/models.js.map +1 -1
  138. package/lib/subcommands/app/shadow.d.ts.map +1 -1
  139. package/lib/subcommands/app/shadow.js +6 -5
  140. package/lib/subcommands/app/shadow.js.map +1 -1
  141. package/lib/subcommands/app/version.d.ts.map +1 -1
  142. package/lib/subcommands/app/version.js +2 -4
  143. package/lib/subcommands/app/version.js.map +1 -1
  144. package/lib/subcommands/config.d.ts +2 -0
  145. package/lib/subcommands/config.d.ts.map +1 -0
  146. package/lib/subcommands/config.js +39 -0
  147. package/lib/subcommands/config.js.map +1 -0
  148. package/lib/subcommands/device/clean.d.ts +1 -1
  149. package/lib/subcommands/device/clean.d.ts.map +1 -1
  150. package/lib/subcommands/device/clean.js +23 -13
  151. package/lib/subcommands/device/clean.js.map +1 -1
  152. package/lib/subcommands/device/index.d.ts.map +1 -1
  153. package/lib/subcommands/device/index.js +3 -1
  154. package/lib/subcommands/device/index.js.map +1 -1
  155. package/lib/subcommands/device/init.js +8 -8
  156. package/lib/subcommands/device/init.js.map +1 -1
  157. package/lib/subcommands/device/migrate.d.ts +2 -0
  158. package/lib/subcommands/device/migrate.d.ts.map +1 -0
  159. package/lib/subcommands/device/migrate.js +24 -0
  160. package/lib/subcommands/device/migrate.js.map +1 -0
  161. package/lib/subcommands/device/refresh.d.ts.map +1 -1
  162. package/lib/subcommands/device/refresh.js +1 -0
  163. package/lib/subcommands/device/refresh.js.map +1 -1
  164. package/lib/subcommands/index.d.ts +1 -1
  165. package/lib/subcommands/index.d.ts.map +1 -1
  166. package/lib/subcommands/index.js +3 -1
  167. package/lib/subcommands/index.js.map +1 -1
  168. package/lib/subcommands/rabbitmq-connection.d.ts +1 -1
  169. package/lib/subcommands/rabbitmq-connection.d.ts.map +1 -1
  170. package/lib/util/aai-error.d.ts +12 -0
  171. package/lib/util/aai-error.d.ts.map +1 -0
  172. package/lib/util/aai-error.js +11 -0
  173. package/lib/util/aai-error.js.map +1 -0
  174. package/lib/util/aws-regions.d.ts +2 -0
  175. package/lib/util/aws-regions.d.ts.map +1 -0
  176. package/lib/util/{cloud-mode-ready.js → aws-regions.js} +2 -20
  177. package/lib/util/aws-regions.js.map +1 -0
  178. package/lib/util/check-for-updates.d.ts.map +1 -1
  179. package/lib/util/check-for-updates.js +5 -28
  180. package/lib/util/check-for-updates.js.map +1 -1
  181. package/lib/util/clean-certs.d.ts.map +1 -1
  182. package/lib/util/clean-certs.js +5 -4
  183. package/lib/util/clean-certs.js.map +1 -1
  184. package/lib/util/directories.d.ts +4 -18
  185. package/lib/util/directories.d.ts.map +1 -1
  186. package/lib/util/directories.js +18 -32
  187. package/lib/util/directories.js.map +1 -1
  188. package/lib/util/file.d.ts +4 -0
  189. package/lib/util/file.d.ts.map +1 -1
  190. package/lib/util/file.js +65 -4
  191. package/lib/util/file.js.map +1 -1
  192. package/lib/util/get-device-id.d.ts.map +1 -1
  193. package/lib/util/get-device-id.js +7 -1
  194. package/lib/util/get-device-id.js.map +1 -1
  195. package/lib/util/http-client.js +3 -3
  196. package/lib/util/http-client.js.map +1 -1
  197. package/package.json +19 -17
  198. package/readme.md +12 -32
  199. package/src/application-control/config.ts +9 -12
  200. package/src/application-control/environment-variables.test.ts +28 -7
  201. package/src/application-control/environment-variables.ts +13 -40
  202. package/src/application-control/index.ts +3 -16
  203. package/src/application-control/install.ts +15 -10
  204. package/src/application-control/models.ts +6 -87
  205. package/src/application-control/utils.ts +0 -28
  206. package/src/cloud-connection/bootstrap-provision.ts +7 -7
  207. package/src/cloud-connection/device-agent-cloud-connection.ts +639 -525
  208. package/src/cloud-connection/device-agent.ts +16 -7
  209. package/src/cloud-connection/live-updates-handler.test.ts +121 -189
  210. package/src/cloud-connection/live-updates-handler.ts +99 -234
  211. package/src/cloud-connection/passthrough-handler.ts +55 -18
  212. package/src/cloud-connection/shadow-handler.test.ts +45 -57
  213. package/src/cloud-connection/shadow-handler.ts +103 -57
  214. package/src/cloud-connection/shadow.ts +4 -1
  215. package/src/cloud-connection/transaction-manager.test.ts +3 -3
  216. package/src/cloud-connection/transaction-manager.ts +53 -39
  217. package/src/device-control/device-control.ts +102 -70
  218. package/src/docker/docker-compose.ts +3 -2
  219. package/src/infrastructure/agent-config.test.ts +6 -2
  220. package/src/infrastructure/agent-config.ts +8 -7
  221. package/src/infrastructure/config-check-utility.test.ts +154 -0
  222. package/src/infrastructure/config-check-utility.ts +77 -0
  223. package/src/infrastructure/device-certificate.test.ts +40 -0
  224. package/src/infrastructure/device-certificate.ts +58 -0
  225. package/src/infrastructure/legacy-migration/legacy-file.test.ts +88 -0
  226. package/src/infrastructure/legacy-migration/legacy-files.ts +101 -0
  227. package/src/infrastructure/legacy-migration/legacy-migration.test.ts +396 -0
  228. package/src/infrastructure/legacy-migration/legacy-migration.ts +229 -0
  229. package/src/infrastructure/require-files-present-ready.test.ts +53 -0
  230. package/src/infrastructure/required-config-checks.ts +33 -0
  231. package/src/infrastructure/tokens-and-device-cfg.ts +12 -10
  232. package/src/local-connection/rabbitmq-connection.ts +22 -17
  233. package/src/secure-tunneling/secure-tunneling.test.ts +20 -22
  234. package/src/secure-tunneling/secure-tunneling.ts +41 -29
  235. package/src/subcommands/app/analytics.ts +2 -4
  236. package/src/subcommands/app/env-vars.ts +72 -9
  237. package/src/subcommands/app/index.ts +3 -11
  238. package/src/subcommands/app/models.ts +5 -81
  239. package/src/subcommands/app/shadow.ts +6 -5
  240. package/src/subcommands/app/version.ts +3 -4
  241. package/src/subcommands/config.ts +42 -0
  242. package/src/subcommands/device/clean.ts +31 -17
  243. package/src/subcommands/device/index.ts +3 -1
  244. package/src/subcommands/device/init.ts +11 -11
  245. package/src/subcommands/device/migrate.ts +20 -0
  246. package/src/subcommands/device/refresh.ts +1 -0
  247. package/src/subcommands/index.ts +3 -1
  248. package/src/util/aai-error.ts +20 -0
  249. package/src/util/{cloud-mode-ready.ts → aws-regions.ts} +0 -24
  250. package/src/util/check-for-updates.ts +14 -30
  251. package/src/util/clean-certs.ts +8 -4
  252. package/src/util/directories.ts +23 -67
  253. package/src/util/file.ts +83 -3
  254. package/src/util/get-device-id.ts +7 -7
  255. package/src/util/http-client.ts +2 -2
  256. package/lib/util/cloud-mode-ready.d.ts +0 -3
  257. package/lib/util/cloud-mode-ready.d.ts.map +0 -1
  258. package/lib/util/cloud-mode-ready.js.map +0 -1
  259. package/lib/util/download-file.d.ts +0 -6
  260. package/lib/util/download-file.d.ts.map +0 -1
  261. package/lib/util/download-file.js +0 -25
  262. package/lib/util/download-file.js.map +0 -1
  263. package/lib/util/fetch-with-timeout.d.ts +0 -4
  264. package/lib/util/fetch-with-timeout.d.ts.map +0 -1
  265. package/lib/util/fetch-with-timeout.js +0 -30
  266. package/lib/util/fetch-with-timeout.js.map +0 -1
  267. package/lib/util/parsing.d.ts +0 -2
  268. package/lib/util/parsing.d.ts.map +0 -1
  269. package/lib/util/parsing.js +0 -17
  270. package/lib/util/parsing.js.map +0 -1
  271. package/src/util/download-file.ts +0 -25
  272. package/src/util/fetch-with-timeout.ts +0 -35
  273. package/src/util/parsing.ts +0 -11
@@ -10,16 +10,27 @@ import {
10
10
  SignedUrlsRequestPayload,
11
11
  ToCloudMessage,
12
12
  ToDeviceAgentMessage,
13
- getToDeviceTopic,
13
+ buildAppLogsMessage,
14
+ buildAppStateMessage,
15
+ buildDeviceStatsMessage,
14
16
  buildSignedUrlsRequestMessage,
15
17
  buildToClientStatusResponseMessage,
16
- StatusResponsePayload,
18
+ getToDeviceTopic,
19
+ getUpdateDeltaStateFromMessage,
17
20
  keyMirrors,
18
- validateToDeviceAgentMessage,
19
- validateSecureTunnelShadowUpdate
21
+ validateSecureTunnelShadowUpdate,
22
+ validateToDeviceAgentMessage
20
23
  } from '@alwaysai/device-agent-schemas';
24
+ import {
25
+ DEVICE_CERTIFICATE_FILE_PATH,
26
+ DEVICE_PRIVATE_KEY_FILE_PATH
27
+ } from 'alwaysai/lib/infrastructure';
28
+ import { stringifyError } from 'alwaysai/lib/util';
29
+ import { exec } from 'child_process';
21
30
  import { existsSync } from 'fs';
31
+ import { promisify } from 'util';
22
32
  import {
33
+ getAppLogs,
23
34
  installApp,
24
35
  restartApp,
25
36
  startApp,
@@ -29,37 +40,33 @@ import {
29
40
  updateModelsWithPresignedUrls
30
41
  } from '../application-control';
31
42
  import { createAppBackup, rollbackApp } from '../application-control/backup';
43
+ import { pruneModels } from '../application-control/models';
32
44
  import { reboot } from '../device-control/device-control';
33
45
  import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
34
46
  import { AgentConfigFile } from '../infrastructure/agent-config';
47
+ import { getBootstrapPrivateKeyFilePath } from '../infrastructure/device-certificate';
48
+ import { migrateFromLegacyCertsAndTokens } from '../infrastructure/legacy-migration/legacy-migration';
49
+ import { requiredConfigFilesPresentAndValid } from '../infrastructure/required-config-checks';
35
50
  import { getIoTCoreEndpointUrl } from '../infrastructure/urls';
36
51
  import { SecureTunnelHandlerSingleton } from '../secure-tunneling/secure-tunneling';
37
- import { cloudModeReady } from '../util/cloud-mode-ready';
38
- import {
39
- AWS_ROOT_CERTIFICATE_FILE_PATH,
40
- BOOTSTRAP_CERTIFICATES_DIR_PATH,
41
- BOOTSTRAP_PRIVATE_KEY_FILE_PATH,
42
- DEVICE_CERTIFICATE_FILE_PATH,
43
- DEVICE_PRIVATE_KEY_FILE_PATH
44
- } from '../util/directories';
52
+ import AaiError from '../util/aai-error';
53
+ import { getDeviceAgentVersion } from '../util/check-for-updates';
54
+ import { AWS_ROOT_CERTIFICATE_FILE_PATH } from '../util/directories';
45
55
  import { getDeviceUuid } from '../util/get-device-id';
46
56
  import { logger } from '../util/logger';
47
57
  import sleep from '../util/sleep';
48
58
  import { bootstrapProvision } from './bootstrap-provision';
49
59
  import { LiveUpdatesHandler } from './live-updates-handler';
60
+ import { getAppStatePayload, getDeviceStatsPayload } from './messages';
50
61
  import { PassthroughHandler } from './passthrough-handler';
51
62
  import { Publisher } from './publisher';
52
63
  import { ShadowHandler, ShadowUpdate } from './shadow-handler';
53
64
  import { TransactionManager } from './transaction-manager';
54
- import { exec } from 'child_process';
55
- import { promisify } from 'util';
56
- import { pruneModels } from '../application-control/models';
57
- import { getDeviceAgentVersion } from '../util/check-for-updates';
58
65
 
59
66
  const exec_promise = promisify(exec);
60
67
 
61
68
  export class DeviceAgentCloudConnection {
62
- private shadowHandler: ShadowHandler;
69
+ public shadowHandler: ShadowHandler;
63
70
  public publisher: Publisher;
64
71
  private liveUpdatesHandler: LiveUpdatesHandler;
65
72
  private txnMgr: TransactionManager;
@@ -73,384 +80,182 @@ export class DeviceAgentCloudConnection {
73
80
  private readonly secureTunnelHandler =
74
81
  SecureTunnelHandlerSingleton.getInstance();
75
82
 
76
- private handleAppStateControl = async (
77
- payload: AppStateControlPayload
78
- ): Promise<boolean> => {
79
- const { baseCommand, projectId } = payload;
80
- switch (baseCommand) {
81
- case keyMirrors.appStateControl.start:
82
- await startApp({ projectId });
83
- break;
84
- case keyMirrors.appStateControl.stop:
85
- await stopApp({ projectId });
86
- break;
87
- case keyMirrors.appStateControl.restart:
88
- await restartApp({ projectId });
89
- break;
90
- }
91
- return true;
92
- };
83
+ constructor() {
84
+ this.device = awsIot.device({
85
+ keyPath: DEVICE_PRIVATE_KEY_FILE_PATH,
86
+ certPath: DEVICE_CERTIFICATE_FILE_PATH,
87
+ caPath: AWS_ROOT_CERTIFICATE_FILE_PATH,
88
+ clientId: this.clientId,
89
+ host: this.host,
90
+ port: this.port,
91
+ keepalive: 10 // time before re-connect attempt on dropped connection, default is 400 seconds
92
+ });
93
93
 
94
- private handleAppVersionControl = async (
95
- payload:
96
- | AppVersionControlInstallPayload
97
- | AppVersionControlUninstallPayload,
98
- txId: string
99
- ): Promise<boolean> => {
100
- switch (payload.baseCommand) {
101
- case keyMirrors.appVersionControl.install: {
102
- const { projectId, appReleaseHash } = payload;
94
+ this.publisher = new Publisher(this.device, this.clientId);
95
+ this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
96
+ this.liveUpdatesHandler = new LiveUpdatesHandler();
97
+ this.txnMgr = new TransactionManager(
98
+ this.publisher,
99
+ this.liveUpdatesHandler
100
+ );
103
101
 
104
- const signedUrlsRequestPayload: SignedUrlsRequestPayload = {
105
- signedUrlsRequest: {
106
- projectId,
107
- appReleaseHash
108
- }
109
- };
110
- const message = buildSignedUrlsRequestMessage(
111
- this.clientId,
112
- signedUrlsRequestPayload,
113
- txId
114
- );
115
- await this.publishCloudRequest(message);
116
- return false;
117
- }
118
- case keyMirrors.appVersionControl.uninstall: {
119
- const { projectId } = payload;
120
- await this.atomicApplicationUninstall(projectId);
121
- return true;
122
- }
123
- default:
124
- logger.warn(
125
- `Ignore App Version Control packet: ${JSON.stringify(
126
- payload,
127
- null,
128
- 2
129
- )}`
130
- );
131
- return true;
102
+ this.subscribe(this.toDeviceTopic);
103
+ this.subscribe(this.secureTunnelNotifyTopic);
104
+ for (const topic of this.shadowHandler.projectShadowTopics) {
105
+ this.subscribe(topic);
132
106
  }
133
- };
134
-
135
- private handleAppInstallCloudResponsePayload = async (
136
- payload: AppInstallResponsePayload
137
- ): Promise<boolean> => {
138
- const {
139
- projectId,
140
- appReleaseHash,
141
- appInstallPayload,
142
- modelsInstallPayload
143
- } = payload.appInstallResponse;
144
- const signedUrlsPayload = {
145
- appInstallPayload,
146
- modelsInstallPayload
147
- };
148
- await this.atomicApplicationUpdate(async () => {
149
- this.shadowHandler.clearProjectShadow(projectId);
150
- await installApp({ projectId, appReleaseHash, signedUrlsPayload });
151
- }, projectId);
152
- return true;
153
- };
107
+ this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.updateDelta);
108
+ this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted);
154
109
 
155
- private handleModelsInstallCloudResponsePayload = async (
156
- payload: ModelsInstallResponsePayload,
157
- txId: string
158
- ): Promise<boolean> => {
159
- const projectId = payload.modelsInstallResponse.projectId;
110
+ this.setupHandlers();
111
+ }
160
112
 
161
- const update = this.txnMgr.getAppCfgUpdateFromTxID(txId);
162
- if (update === undefined) {
163
- throw new Error(
164
- 'Unknown error while updating models via application config! No config present for model update.'
165
- );
166
- }
167
- const { appCfgUpdate, envVarUpdate } = update;
168
- if (appCfgUpdate) {
169
- await this.atomicApplicationUpdate(
170
- async () =>
171
- await updateModelsWithPresignedUrls({
172
- projectId,
173
- modelInstallPayloads: payload.modelsInstallResponse.newModels,
174
- newAppCfg: appCfgUpdate.newAppCfg
175
- }),
176
- projectId
177
- );
178
- }
113
+ /*=================================================================
114
+ Public interface
115
+ =================================================================*/
179
116
 
180
- if (envVarUpdate) {
181
- await this.atomicApplicationUpdate(
182
- async () =>
183
- await this.shadowHandler.updateProjectEnvVars({
184
- projectId,
185
- envVars: envVarUpdate.envVars
186
- }),
187
- projectId,
188
- true
189
- );
190
- }
117
+ public getClientId(): string {
118
+ return this.clientId;
119
+ }
191
120
 
192
- return true;
193
- };
121
+ public isCmdInProgress(projectId: string): boolean {
122
+ return this.txnMgr.isOngoingTransactionForProjectID(projectId);
123
+ }
194
124
 
195
- private async handleDeviceAction(payload: DeviceActionPayload) {
196
- const { system_restart } = keyMirrors.deviceAction;
197
- switch (payload.action) {
198
- case system_restart: {
199
- await reboot();
200
- break;
201
- }
202
- default: {
203
- logger.info(
204
- `Unrecognized device action requested: '${payload.action}'.`
205
- );
206
- }
125
+ public async handleMessage(topic: string, message: any) {
126
+ logger.debug(
127
+ `Received message: ${JSON.stringify({ topic, message }, null, 2)}`
128
+ );
129
+ // ProjectShadow messages
130
+ if (this.shadowHandler.projectShadowTopics.includes(topic)) {
131
+ await this.handleProjectShadowMessage(topic, message);
132
+ } else if (topic === this.toDeviceTopic) {
133
+ await this.handleDeviceAgentMessage({
134
+ topic,
135
+ message
136
+ });
137
+ // SecureTunnelNotify messages
138
+ } else if (topic === this.secureTunnelNotifyTopic) {
139
+ await this.secureTunnelHandler.secureTunnelNotifyHandler(message);
140
+ // SecureTunnel messages
141
+ } else if (
142
+ topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
143
+ ) {
144
+ await this.handleSecureTunnelMessage(message);
145
+ } else if (
146
+ topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
147
+ ) {
148
+ logger.info(`Received secure tunnel deleteAccepted: ${message}`);
149
+ await this.secureTunnelHandler.destroy();
150
+ } else {
151
+ logger.error(`Unexpected topic, ignoring! ${topic}`);
207
152
  }
208
153
  }
209
154
 
210
- private async publishCloudRequest(message: ToCloudMessage) {
211
- this.publisher.publishToCloud(message);
155
+ public async stop() {
156
+ // This method is currently only used by the CLI, and shadow messages can be
157
+ // lost since we aren't waiting for responses so sleep for a short time to
158
+ // receive them
159
+ await sleep(1000);
160
+ this.device.end();
212
161
  }
213
162
 
214
- private subscribe(topic: string) {
215
- logger.info(`Subscribing to ${topic}`);
216
- this.device.subscribe(topic);
217
- }
163
+ /*=================================================================
164
+ Private interface
165
+ =================================================================*/
218
166
 
219
- private async atomicApplicationUninstall(projectId: string) {
220
- try {
221
- await uninstallApp({ projectId });
222
- this.shadowHandler.clearProjectShadow(projectId);
223
- } catch (e) {
224
- logger.error(`Failed to uninstall ${projectId}: ${e.message}`);
225
- throw e;
226
- }
227
- }
167
+ private setupHandlers() {
168
+ this.device.on('connect', (connack: any) => {
169
+ logger.info('Device Agent has connected to the cloud');
170
+ // FIXME: EI-709 Skip this request for now to prevent kicking off another
171
+ // shadow update process if IoT Core disconnect occurs during app config update
172
+ //this.shadowHandler.getShadowUpdates();
173
+ void this.shadowHandler.updateSystemInfoShadow();
174
+ });
228
175
 
229
- // eslint-disable-next-line
230
- private async atomicApplicationUpdate <F extends () => any>(
231
- func: F,
232
- projectId: string,
233
- skipUpdateShadow?: boolean
234
- ): Promise<ReturnType<F>> {
235
- // First try to create a backup, so that there is one available if something goes wrong in the next try:catch.
236
- if (await AgentConfigFile().isAppPresent({ projectId })) {
176
+ this.device.on('disconnect', () => {
177
+ logger.warn('Device Agent has been disconnected from the cloud');
178
+ });
179
+
180
+ this.device.on('reconnect', () => {
181
+ logger.info(
182
+ `Device Agent attempting to re-connect ${new Date().toLocaleString()}`
183
+ );
184
+ });
185
+
186
+ this.device.on('error', function (e) {
187
+ logger.error(`Error connecting to cloud!\n${stringifyError(e)}`);
188
+ });
189
+
190
+ this.device.on('message', async (topic: string, payload: string) => {
237
191
  try {
238
- await createAppBackup({ projectId });
192
+ const jsonPacket = JSON.parse(payload);
193
+ await this.handleMessage(topic, jsonPacket);
239
194
  } catch (e) {
240
- logger.error(
241
- `Could not create a backup for the project: ${projectId}:\n${e.message}\n${e.stack}`
242
- );
195
+ logger.error(`Error parsing message!\n${stringifyError(e)}`);
243
196
  }
244
- }
197
+ });
245
198
 
246
- try {
247
- const out: ReturnType<F> = await func();
248
- if (!skipUpdateShadow)
249
- await this.shadowHandler.updateProjectShadow(projectId);
250
- return out;
251
- } catch (errorAppUpdate) {
252
- logger.error(
253
- `Failed to update ${projectId}:\n${JSON.stringify(errorAppUpdate)}}`
254
- );
255
- // If something goes wrong, first try to rollback
256
- try {
257
- await rollbackApp({ projectId });
258
- logger.error(
259
- `Application update failed, rolled back to previous version: ${errorAppUpdate}`
199
+ this.device.on('offline', () => {
200
+ logger.warn(`Device Agent is offline ${new Date().toLocaleString()}`);
201
+ void this.logConnectionInfo();
202
+ });
203
+ }
204
+
205
+ private async logConnectionInfo() {
206
+ try {
207
+ /**
208
+ * We're using the 'netcat' or 'nc' command to test the connection to the IoT Core endpoint.
209
+ * This command doesn't always exit (see below), so
210
+ * we use timeout to break out of the prompt
211
+ * and catch the resulting error/parse the resulting stderr
212
+ *
213
+ * Sample command for current host and port:
214
+ * nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
215
+ *
216
+ * Sample output when port is not blocked and host is reachable:
217
+ * $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443
218
+ * Connection to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443 port [tcp/https] succeeded!
219
+ *
220
+ *
221
+ * Sample output when port is blocked (will repeatedly try until ctrl-C out):
222
+ * $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
223
+ * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
224
+ * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
225
+ * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
226
+ * ^C
227
+ *
228
+ *
229
+ * Sample command/output when the port isn't enable on that host:
230
+ * $ nc -zv -w 1 localhost 8883
231
+ * nc: connect to localhost port 8883 (tcp) failed: Connection refused
232
+ */
233
+ await exec_promise(`nc -zv -w 1 ${this.host} ${this.port}`, {
234
+ timeout: 2000
235
+ });
236
+ } catch (err) {
237
+ const output = JSON.stringify(err['stderr']);
238
+ if (output.indexOf('not known') !== -1) {
239
+ logger.warn(
240
+ 'Iot Core endpoint appears to be unreachable, internet connection may be unstable or the host may be down.'
260
241
  );
261
- } catch (errorRollbackApp) {
262
- // and if that fails, uninstall the app as a last resort.
263
- try {
264
- await this.atomicApplicationUninstall(projectId);
265
- } catch {
266
- // atomicApplicationUninstall handles failing, so there's nothing to handle here.
267
- }
268
- logger.error(
269
- `Application update failed, rolled back to previous version: ${errorAppUpdate}`
242
+ } else if (output.indexOf('timed out') !== -1) {
243
+ logger.warn(
244
+ `Internet connection appears fine, however the endpoint was not reachable on the current connection port: ${this.port}\nPlease check if a firewall is in place.`
270
245
  );
271
- throw new Error(
272
- `Application update and rollback failed, uninstalled the application: ${errorAppUpdate}`
246
+ } else if (output.indexOf('refused') !== -1) {
247
+ logger.warn(
248
+ `The connection was refused, likely ${this.host} is not running a service on ${this.port}.`
273
249
  );
274
- }
275
- throw new Error(
276
- `Application update failed, rolled the application back: ${errorAppUpdate}`
277
- );
278
- }
279
- }
280
-
281
- private handleProjectShadowConfigUpdate = async (
282
- update: ShadowUpdate,
283
- txId: string
284
- ): Promise<boolean> => {
285
- const { projectId, appCfgUpdate, envVarUpdate } = update;
286
-
287
- if (
288
- appCfgUpdate?.updatedModels &&
289
- Object.keys(appCfgUpdate.updatedModels).length
290
- ) {
291
- // When there are model updates request signed URLs and wait to apply config changes
292
- const { updatedModels } = appCfgUpdate;
293
-
294
- logger.debug(
295
- `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
296
- updatedModels
297
- )}`
298
- );
299
- const modelsOnlyUrlsRequestPayload: SignedUrlsRequestPayload = {
300
- modelsOnlyUrlsRequest: {
301
- projectId,
302
- models: updatedModels
303
- }
304
- };
305
- const message = buildSignedUrlsRequestMessage(
306
- this.clientId,
307
- modelsOnlyUrlsRequestPayload,
308
- txId
309
- );
310
- this.publisher.publishToCloud(message);
311
-
312
- this.txnMgr.setAppCfgUpdateToTx(txId, update);
313
-
314
- return false;
315
- }
316
-
317
- if (appCfgUpdate) {
318
- await this.atomicApplicationUpdate(async () => {
319
- await pruneModels({
320
- projectId,
321
- appCfg: appCfgUpdate.newAppCfg
322
- });
323
- await updateAppCfg({
324
- projectId,
325
- newAppCfg: appCfgUpdate.newAppCfg
326
- });
327
- }, projectId);
328
- }
329
-
330
- if (envVarUpdate) {
331
- await this.atomicApplicationUpdate(
332
- async () =>
333
- await this.shadowHandler.updateProjectEnvVars({
334
- projectId,
335
- envVars: envVarUpdate.envVars
336
- }),
337
- projectId,
338
- true
339
- );
340
- }
341
- return true;
342
- };
343
-
344
- private async handleProjectShadowMessage(topic: string, message: any) {
345
- const shadowUpdates = await this.shadowHandler.handleProjectShadow({
346
- topic,
347
- payload: message,
348
- clientToken: message.clientToken
349
- });
350
- if (shadowUpdates.length) {
351
- const shadowUpdatePromises: Promise<void>[] = [];
352
- for (const shadowUpdate of shadowUpdates) {
353
- const projectId = shadowUpdate.projectId;
354
- const txId = shadowUpdate.txId;
355
- shadowUpdatePromises.push(
356
- this.txnMgr
357
- .runTransactionStep({
358
- func: () =>
359
- this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
360
- projectId,
361
- txId,
362
- start: true,
363
- stepName: topic
364
- })
365
- .catch((e: Error) => {
366
- logger.error(
367
- `There was an issue updating project shadow config: ${JSON.stringify(
368
- e
369
- )}`
370
- );
371
- })
250
+ } else {
251
+ logger.warn(
252
+ `Output from checking connection to ${this.host} on ${this.port}: ${output}`
372
253
  );
373
254
  }
374
-
375
- await Promise.all(shadowUpdatePromises);
376
- }
377
- }
378
-
379
- public async handleSecureTunnelMessage(payload: any): Promise<void> {
380
- logger.info(`Received secure tunnel update: ${JSON.stringify(payload)}`);
381
- const state = payload.state;
382
- if (!state) {
383
- logger.debug(`No state found in message: ${JSON.stringify(payload)}`);
384
- return;
385
- }
386
- const valid = validateSecureTunnelShadowUpdate(state);
387
- if (!valid) {
388
- logger.error(
389
- `Error validating message: ${JSON.stringify(
390
- { payload, errors: validateSecureTunnelShadowUpdate.errors },
391
- null,
392
- 2
393
- )}`
394
- );
395
- return;
396
- }
397
- const secureTunnelUpdate =
398
- await this.secureTunnelHandler.syncShadowToDeviceState(payload);
399
- await this.shadowHandler.updateSecureTunnelShadow(secureTunnelUpdate);
400
- return;
401
- }
402
-
403
- /*=================================================================
404
- Public interface
405
- =================================================================*/
406
-
407
- constructor() {
408
- this.device = awsIot.device({
409
- keyPath: DEVICE_PRIVATE_KEY_FILE_PATH,
410
- certPath: DEVICE_CERTIFICATE_FILE_PATH,
411
- caPath: AWS_ROOT_CERTIFICATE_FILE_PATH,
412
- clientId: this.clientId,
413
- host: this.host,
414
- port: this.port,
415
- keepalive: 1 // time before re-connect attempt on dropped connection, default is 400 seconds
416
- });
417
- this.publisher = new Publisher(this.device, this.clientId);
418
- this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
419
- this.liveUpdatesHandler = new LiveUpdatesHandler(
420
- this.publisher,
421
- this.clientId
422
- );
423
- this.txnMgr = new TransactionManager(
424
- this.publisher,
425
- this.liveUpdatesHandler
426
- );
427
-
428
- this.subscribe(this.toDeviceTopic);
429
- this.subscribe(this.secureTunnelNotifyTopic);
430
- for (const topic of this.shadowHandler.projectShadowTopics) {
431
- this.subscribe(topic);
432
255
  }
433
- this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.updateDelta);
434
- this.subscribe(this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted);
435
- }
436
-
437
- public getClientId(): string {
438
- return this.clientId;
439
- }
440
-
441
- public getToDeviceTopic() {
442
- return this.toDeviceTopic;
443
- }
444
-
445
- public isCmdInProgress(projectId: string): boolean {
446
- return this.txnMgr.isOngoingTransactionForProjectID(projectId);
447
- }
448
-
449
- public async updateProjectShadow(projectId: string) {
450
- await this.shadowHandler.updateProjectShadow(projectId);
451
256
  }
452
257
 
453
- public async handleDeviceAgentMessage({
258
+ private async handleDeviceAgentMessage({
454
259
  topic,
455
260
  message
456
261
  }: {
@@ -491,11 +296,22 @@ export class DeviceAgentCloudConnection {
491
296
  projectId,
492
297
  txId,
493
298
  start: true,
299
+ liveUpdatesPublishFn: async () =>
300
+ this.publisher.publishToClient(
301
+ buildToClientStatusResponseMessage(
302
+ this.clientId,
303
+ { status: keyMirrors.statusResponse.in_progress },
304
+ txId
305
+ ),
306
+ logger.silly
307
+ ),
494
308
  stepName: payload.baseCommand
495
309
  });
496
310
  } catch (e) {
497
311
  logger.error(
498
- `Error processing application state control request: ${e}!`
312
+ `Error processing application state control request for ${projectId}!\n${stringifyError(
313
+ e
314
+ )}`
499
315
  );
500
316
  }
501
317
 
@@ -511,18 +327,98 @@ export class DeviceAgentCloudConnection {
511
327
  projectId,
512
328
  txId,
513
329
  start: true,
330
+ liveUpdatesPublishFn: async () =>
331
+ this.publisher.publishToClient(
332
+ buildToClientStatusResponseMessage(
333
+ this.clientId,
334
+ { status: keyMirrors.statusResponse.in_progress },
335
+ txId
336
+ ),
337
+ logger.silly
338
+ ),
514
339
  stepName: payload.baseCommand
515
340
  });
516
341
  } catch (e) {
517
- logger.error(`Error processing application install request: ${e}!`);
342
+ logger.error(
343
+ `Error processing application install request for ${projectId}!\n${stringifyError(
344
+ e
345
+ )}`
346
+ );
518
347
  }
519
348
 
520
349
  break;
521
350
  }
522
351
  case live_state_updates: {
523
- const payload = message.payload;
524
- // TODO: Send response?
525
- void this.liveUpdatesHandler.handleToggles(payload, txId);
352
+ const { deviceStats, appState, appLogs } = message.payload;
353
+
354
+ if (deviceStats !== undefined) {
355
+ if (deviceStats) {
356
+ await this.liveUpdatesHandler.enable(
357
+ keyMirrors.toClientMessageType.device_stats,
358
+ async () =>
359
+ this.publisher.publishToClient(
360
+ buildDeviceStatsMessage(
361
+ this.clientId,
362
+ await getDeviceStatsPayload(),
363
+ txId
364
+ ),
365
+ logger.silly
366
+ )
367
+ );
368
+ } else {
369
+ this.liveUpdatesHandler.disable(
370
+ keyMirrors.toClientMessageType.device_stats
371
+ );
372
+ }
373
+ }
374
+
375
+ if (appState !== undefined) {
376
+ if (appState) {
377
+ await this.liveUpdatesHandler.enable(
378
+ keyMirrors.toClientMessageType.app_state,
379
+ async () =>
380
+ this.publisher.publishToClient(
381
+ buildAppStateMessage(
382
+ this.clientId,
383
+ await getAppStatePayload(),
384
+ txId
385
+ ),
386
+ logger.silly
387
+ )
388
+ );
389
+ } else {
390
+ this.liveUpdatesHandler.disable(
391
+ keyMirrors.toClientMessageType.app_state
392
+ );
393
+ }
394
+ }
395
+
396
+ if (appLogs !== undefined) {
397
+ if (appLogs.toggle) {
398
+ await this.liveUpdatesHandler.startStream(
399
+ appLogs.projectId,
400
+ async () =>
401
+ await getAppLogs({
402
+ projectId: appLogs.projectId,
403
+ args: ['--tail', '100', '--no-log-prefix']
404
+ }),
405
+ async (logChunk: string) =>
406
+ this.publisher.publishToClient(
407
+ buildAppLogsMessage(
408
+ this.clientId,
409
+ {
410
+ projectId: appLogs.projectId,
411
+ logChunk
412
+ },
413
+ txId
414
+ ),
415
+ logger.silly
416
+ )
417
+ );
418
+ } else {
419
+ this.liveUpdatesHandler.stopStream(appLogs.projectId);
420
+ }
421
+ }
526
422
  break;
527
423
  }
528
424
  case app_install_response: {
@@ -569,62 +465,59 @@ export class DeviceAgentCloudConnection {
569
465
  case status_response: {
570
466
  const { failure } = keyMirrors.statusResponse;
571
467
  if (message.payload.status === failure) {
572
- this.txnMgr.completeTransaction(txId);
573
-
574
- const failureStatusResponsePayload: StatusResponsePayload = {
575
- status: keyMirrors.statusResponse.failure,
576
- message: message.payload.message
577
- };
578
- // Send final status message
579
- const failureStatusResponseMessage =
468
+ this.txnMgr.completeTransaction(
469
+ txId,
580
470
  buildToClientStatusResponseMessage(
581
471
  this.clientId,
582
- failureStatusResponsePayload,
472
+ {
473
+ status: keyMirrors.statusResponse.failure,
474
+ message: message.payload.message
475
+ },
583
476
  txId
584
- );
585
- this.publisher.publishToClient(failureStatusResponseMessage);
477
+ )
478
+ );
586
479
  }
587
480
  break;
588
481
  }
589
482
  case device_action: {
590
483
  try {
591
- const statusResponsePayload: StatusResponsePayload = {
592
- status: keyMirrors.statusResponse.in_progress
593
- };
594
- const statusResponseMessage = buildToClientStatusResponseMessage(
595
- this.clientId,
596
- statusResponsePayload,
597
- txId
484
+ this.publisher.publishToClient(
485
+ buildToClientStatusResponseMessage(
486
+ this.clientId,
487
+ {
488
+ status: keyMirrors.statusResponse.in_progress
489
+ },
490
+ txId
491
+ )
598
492
  );
599
- this.publisher.publishToClient(statusResponseMessage);
600
493
 
601
494
  await this.handleDeviceAction(message.payload);
602
495
 
603
- const successStatusResponsePayload: StatusResponsePayload = {
604
- status: keyMirrors.statusResponse.success
605
- };
606
- const successStatusResponseMessage =
496
+ this.publisher.publishToClient(
607
497
  buildToClientStatusResponseMessage(
608
498
  this.clientId,
609
- successStatusResponsePayload,
499
+ {
500
+ status: keyMirrors.statusResponse.success
501
+ },
610
502
  txId
611
- );
612
- this.publisher.publishToClient(successStatusResponseMessage);
503
+ )
504
+ );
613
505
  } catch (e) {
614
506
  logger.error(
615
- `There was a problem performing device action '${message.payload.action}': ${e.message}`
507
+ `There was a problem performing device action '${
508
+ message.payload.action
509
+ }'!\n${stringifyError(e)}`
616
510
  );
617
- const failureStatusResponsePayload: StatusResponsePayload = {
618
- status: keyMirrors.statusResponse.failure,
619
- message: e.message
620
- };
621
- const failureStatusResponseMessage =
511
+ this.publisher.publishToClient(
622
512
  buildToClientStatusResponseMessage(
623
513
  this.clientId,
624
- failureStatusResponsePayload,
514
+ {
515
+ status: keyMirrors.statusResponse.failure,
516
+ message: e.message
517
+ },
625
518
  txId
626
- );
627
- this.publisher.publishToClient(failureStatusResponseMessage);
519
+ )
520
+ );
628
521
  }
629
522
  break;
630
523
  }
@@ -639,134 +532,347 @@ export class DeviceAgentCloudConnection {
639
532
  }
640
533
  }
641
534
 
642
- public async handleMessage(topic: string, message: any) {
643
- logger.debug(
644
- `Received message: ${JSON.stringify({ topic, message }, null, 2)}`
645
- );
646
- // ProjectShadow messages
647
- if (this.shadowHandler.projectShadowTopics.includes(topic)) {
648
- await this.handleProjectShadowMessage(topic, message);
649
- } else if (topic === this.toDeviceTopic) {
650
- await this.handleDeviceAgentMessage({
651
- topic,
652
- message
653
- });
654
- // SecureTunnelNotify messages
655
- } else if (topic === this.secureTunnelNotifyTopic) {
656
- await this.secureTunnelHandler.secureTunnelNotifyHandler(message);
657
- // SecureTunnel messages
658
- } else if (
659
- topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
660
- ) {
661
- await this.handleSecureTunnelMessage(message);
662
- } else if (
663
- topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
664
- ) {
665
- logger.info(`Received secure tunnel deleteAccepted: ${message}`);
666
- await this.secureTunnelHandler.destroy();
667
- } else {
668
- logger.error(`Unexpected topic, ignoring! ${topic}`);
535
+ private handleAppStateControl = async (
536
+ payload: AppStateControlPayload
537
+ ): Promise<boolean> => {
538
+ const { baseCommand, projectId } = payload;
539
+ switch (baseCommand) {
540
+ case keyMirrors.appStateControl.start:
541
+ await startApp({ projectId });
542
+ break;
543
+ case keyMirrors.appStateControl.stop:
544
+ await stopApp({ projectId });
545
+ break;
546
+ case keyMirrors.appStateControl.restart:
547
+ await restartApp({ projectId });
548
+ break;
669
549
  }
670
- }
550
+ return true;
551
+ };
671
552
 
672
- public async setupHandlers() {
673
- this.device.on('connect', (connack: any) => {
674
- logger.info('Device Agent has connected to the cloud');
675
- // FIXME: EI-709 Skip this request for now to prevent kicking off another
676
- // shadow update process if IoT Core disconnect occurs during app config update
677
- //this.shadowHandler.getShadowUpdates();
678
- void this.shadowHandler.updateSystemInfoShadow();
679
- });
553
+ private handleAppVersionControl = async (
554
+ payload:
555
+ | AppVersionControlInstallPayload
556
+ | AppVersionControlUninstallPayload,
557
+ txId: string
558
+ ): Promise<boolean> => {
559
+ switch (payload.baseCommand) {
560
+ case keyMirrors.appVersionControl.install: {
561
+ const { projectId, appReleaseHash } = payload;
680
562
 
681
- this.device.on('disconnect', () => {
682
- logger.warn('Device Agent has been disconnected from the cloud');
683
- });
563
+ const signedUrlsRequestPayload: SignedUrlsRequestPayload = {
564
+ signedUrlsRequest: {
565
+ projectId,
566
+ appReleaseHash
567
+ }
568
+ };
569
+ const message = buildSignedUrlsRequestMessage(
570
+ this.clientId,
571
+ signedUrlsRequestPayload,
572
+ txId
573
+ );
574
+ await this.publishCloudRequest(message);
575
+ return false;
576
+ }
577
+ case keyMirrors.appVersionControl.uninstall: {
578
+ const { projectId } = payload;
579
+ await this.atomicApplicationUninstall(projectId);
580
+ return true;
581
+ }
582
+ default:
583
+ logger.warn(
584
+ `Ignore App Version Control packet: ${JSON.stringify(
585
+ payload,
586
+ null,
587
+ 2
588
+ )}`
589
+ );
590
+ return true;
591
+ }
592
+ };
684
593
 
685
- this.device.on('reconnect', () => {
686
- logger.info(
687
- `Device Agent attempting to re-connect ${new Date().toLocaleString()}`
594
+ private handleAppInstallCloudResponsePayload = async (
595
+ payload: AppInstallResponsePayload
596
+ ): Promise<boolean> => {
597
+ const {
598
+ projectId,
599
+ appReleaseHash,
600
+ appInstallPayload,
601
+ modelsInstallPayload
602
+ } = payload.appInstallResponse;
603
+ const signedUrlsPayload = {
604
+ appInstallPayload,
605
+ modelsInstallPayload
606
+ };
607
+ await this.atomicApplicationUpdate(async () => {
608
+ this.shadowHandler.clearProjectShadow(projectId);
609
+ await installApp({ projectId, appReleaseHash, signedUrlsPayload });
610
+ }, projectId);
611
+ return true;
612
+ };
613
+
614
+ private handleModelsInstallCloudResponsePayload = async (
615
+ payload: ModelsInstallResponsePayload,
616
+ txId: string
617
+ ): Promise<boolean> => {
618
+ const projectId = payload.modelsInstallResponse.projectId;
619
+
620
+ const update = this.txnMgr.getAppCfgUpdateFromTxID(txId);
621
+ if (update === undefined) {
622
+ throw new Error(
623
+ 'Unknown error while updating models via application config! No config present for model update.'
688
624
  );
689
- });
625
+ }
626
+ const { appCfgUpdate, envVarUpdate } = update;
627
+ if (appCfgUpdate) {
628
+ await this.atomicApplicationUpdate(
629
+ async () =>
630
+ await updateModelsWithPresignedUrls({
631
+ projectId,
632
+ modelInstallPayloads: payload.modelsInstallResponse.newModels,
633
+ newAppCfg: appCfgUpdate.newAppCfg
634
+ }),
635
+ projectId
636
+ );
637
+ }
690
638
 
691
- this.device.on('error', function (error) {
692
- const errorString = error.message.toString();
693
- logger.error(`${errorString}`);
694
- });
639
+ if (envVarUpdate) {
640
+ await this.atomicApplicationUpdate(
641
+ async () =>
642
+ await this.shadowHandler.updateProjectEnvVars({
643
+ projectId,
644
+ envVars: envVarUpdate.envVars
645
+ }),
646
+ projectId,
647
+ true
648
+ );
649
+ }
695
650
 
696
- this.device.on('message', async (topic: string, payload: string) => {
697
- try {
698
- const jsonPacket = JSON.parse(payload);
699
- await this.handleMessage(topic, jsonPacket);
700
- } catch (e) {
701
- logger.error(`Error parsing message: ${e.message}`);
651
+ return true;
652
+ };
653
+
654
+ private async handleDeviceAction(payload: DeviceActionPayload) {
655
+ const { system_restart } = keyMirrors.deviceAction;
656
+ switch (payload.action) {
657
+ case system_restart: {
658
+ await reboot();
659
+ break;
702
660
  }
703
- });
661
+ default: {
662
+ logger.info(
663
+ `Unrecognized device action requested: '${payload.action}'.`
664
+ );
665
+ }
666
+ }
667
+ }
704
668
 
705
- this.device.on('offline', () => {
706
- logger.warn(`Device Agent is offline ${new Date().toLocaleString()}`);
707
- void this.logConnectionInfo();
708
- });
669
+ private async publishCloudRequest(message: ToCloudMessage) {
670
+ this.publisher.publishToCloud(message);
671
+ }
672
+
673
+ private subscribe(topic: string) {
674
+ logger.info(`Subscribing to ${topic}`);
675
+ this.device.subscribe(topic);
709
676
  }
710
677
 
711
- public async logConnectionInfo() {
678
+ private async atomicApplicationUninstall(projectId: string) {
712
679
  try {
713
- /**
714
- * We're using the 'netcat' or 'nc' command to test the connection to the IoT Core endpoint.
715
- * This command doesn't always exit (see below), so
716
- * we use timeout to break out of the prompt
717
- * and catch the resulting error/parse the resulting stderr
718
- *
719
- * Sample command for current host and port:
720
- * nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
721
- *
722
- * Sample output when port is not blocked and host is reachable:
723
- * $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443
724
- * Connection to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 443 port [tcp/https] succeeded!
725
- *
726
- *
727
- * Sample output when port is blocked (will repeatedly try until ctrl-C out):
728
- * $ nc -zv -w 1 a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com 8883
729
- * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
730
- * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
731
- * nc: connect to a3tzi5g7sq5zsj-ats.iot.us-west-2.amazonaws.com port 8883 (tcp) timed out: Operation now in progress
732
- * ^C
733
- *
734
- *
735
- * Sample command/output when the port isn't enable on that host:
736
- * $ nc -zv -w 1 localhost 8883
737
- * nc: connect to localhost port 8883 (tcp) failed: Connection refused
738
- */
739
- await exec_promise(`nc -zv -w 1 ${this.host} ${this.port}`, {
740
- timeout: 2000
741
- });
742
- } catch (err) {
743
- const output = JSON.stringify(err['stderr']);
744
- if (output.indexOf('not known') !== -1) {
745
- logger.warn(
746
- 'Iot Core endpoint appears to be unreachable, internet connection may be unstable or the host may be down.'
680
+ await uninstallApp({ projectId });
681
+ this.shadowHandler.clearProjectShadow(projectId);
682
+ } catch (e) {
683
+ logger.error(`Failed to uninstall ${projectId}!\n${stringifyError(e)}`);
684
+ throw e;
685
+ }
686
+ }
687
+
688
+ // eslint-disable-next-line
689
+ private async atomicApplicationUpdate<F extends () => any>(
690
+ func: F,
691
+ projectId: string,
692
+ skipUpdateShadow?: boolean
693
+ ): Promise<ReturnType<F>> {
694
+ if (await AgentConfigFile().isAppPresent({ projectId })) {
695
+ // Reject the application update if app is present but not ready
696
+ if (!(await AgentConfigFile().isAppReady({ projectId }))) {
697
+ throw new Error('Application already has installation in progress!');
698
+ }
699
+
700
+ // Try to create a backup, so that there is one available if something goes wrong in the next try:catch.
701
+ try {
702
+ await createAppBackup({ projectId });
703
+ } catch (e) {
704
+ logger.error(
705
+ `Could not create a backup for the project: ${projectId}!\n${stringifyError(
706
+ e
707
+ )}`
747
708
  );
748
- } else if (output.indexOf('timed out') !== -1) {
749
- logger.warn(
750
- `Internet connection appears fine, however the endpoint was not reachable on the current connection port: ${this.port}\nPlease check if a firewall is in place.`
709
+ }
710
+ }
711
+
712
+ try {
713
+ const out: ReturnType<F> = await func();
714
+ if (!skipUpdateShadow)
715
+ await this.shadowHandler.updateProjectShadow(projectId);
716
+ return out;
717
+ } catch (errorAppUpdate) {
718
+ logger.error(
719
+ `Failed to update ${projectId}!\n${stringifyError(errorAppUpdate)}`
720
+ );
721
+ // If something goes wrong, first try to rollback
722
+ try {
723
+ await rollbackApp({ projectId });
724
+ } catch (errorRollbackApp) {
725
+ logger.error(
726
+ `Application rollback failed for ${projectId}!\n${stringifyError(
727
+ errorRollbackApp
728
+ )}`
751
729
  );
752
- } else if (output.indexOf('refused') !== -1) {
753
- logger.warn(
754
- `The connection was refused, likely ${this.host} is not running a service on ${this.port}.`
730
+ // and if that fails, uninstall the app as a last resort.
731
+ try {
732
+ await this.atomicApplicationUninstall(projectId);
733
+ } catch {
734
+ // atomicApplicationUninstall logs failure, so there's nothing to do here.
735
+ }
736
+ throw new AaiError(
737
+ 'Application update and rollback failed, uninstalled the application!',
738
+ { cause: errorAppUpdate }
755
739
  );
756
- } else {
757
- logger.warn(
758
- `Output from checking connection to ${this.host} on ${this.port}: ${output}`
740
+ }
741
+ throw new Error(
742
+ 'Application update failed, rolled the application back!',
743
+ { cause: errorAppUpdate }
744
+ );
745
+ }
746
+ }
747
+
748
+ private handleProjectShadowConfigUpdate = async (
749
+ update: ShadowUpdate,
750
+ txId: string
751
+ ): Promise<boolean> => {
752
+ const { projectId, appCfgUpdate, envVarUpdate } = update;
753
+
754
+ if (
755
+ appCfgUpdate?.updatedModels &&
756
+ Object.keys(appCfgUpdate.updatedModels).length
757
+ ) {
758
+ // When there are model updates request signed URLs and wait to apply config changes
759
+ const { updatedModels } = appCfgUpdate;
760
+
761
+ logger.debug(
762
+ `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
763
+ updatedModels
764
+ )}`
765
+ );
766
+ const modelsOnlyUrlsRequestPayload: SignedUrlsRequestPayload = {
767
+ modelsOnlyUrlsRequest: {
768
+ projectId,
769
+ models: updatedModels
770
+ }
771
+ };
772
+ const message = buildSignedUrlsRequestMessage(
773
+ this.clientId,
774
+ modelsOnlyUrlsRequestPayload,
775
+ txId
776
+ );
777
+ this.publisher.publishToCloud(message);
778
+
779
+ this.txnMgr.setAppCfgUpdateToTx(txId, update);
780
+
781
+ return false;
782
+ }
783
+
784
+ if (appCfgUpdate) {
785
+ await this.atomicApplicationUpdate(async () => {
786
+ await pruneModels({
787
+ projectId,
788
+ appCfg: appCfgUpdate.newAppCfg
789
+ });
790
+ await updateAppCfg({
791
+ projectId,
792
+ newAppCfg: appCfgUpdate.newAppCfg
793
+ });
794
+ }, projectId);
795
+ }
796
+
797
+ if (envVarUpdate) {
798
+ await this.atomicApplicationUpdate(
799
+ async () =>
800
+ await this.shadowHandler.updateProjectEnvVars({
801
+ projectId,
802
+ envVars: envVarUpdate.envVars
803
+ }),
804
+ projectId,
805
+ true
806
+ );
807
+ }
808
+ return true;
809
+ };
810
+
811
+ private async handleProjectShadowMessage(topic: string, message: any) {
812
+ const shadowUpdates = await this.shadowHandler.handleProjectShadow({
813
+ topic,
814
+ payload: message,
815
+ clientToken: message.clientToken
816
+ });
817
+ if (shadowUpdates.length) {
818
+ const shadowUpdatePromises: Promise<void>[] = [];
819
+ for (const shadowUpdate of shadowUpdates) {
820
+ const projectId = shadowUpdate.projectId;
821
+ const txId = shadowUpdate.txId;
822
+ shadowUpdatePromises.push(
823
+ this.txnMgr
824
+ .runTransactionStep({
825
+ func: () =>
826
+ this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
827
+ projectId,
828
+ txId,
829
+ start: true,
830
+ liveUpdatesPublishFn: async () =>
831
+ this.publisher.publishToClient(
832
+ buildToClientStatusResponseMessage(
833
+ this.clientId,
834
+ { status: keyMirrors.statusResponse.in_progress },
835
+ txId
836
+ ),
837
+ logger.silly
838
+ ),
839
+ stepName: topic
840
+ })
841
+ .catch((e) => {
842
+ logger.error(
843
+ `There was an issue updating project shadow config for ${projectId}!\n${stringifyError(
844
+ e
845
+ )}`
846
+ );
847
+ })
759
848
  );
760
849
  }
850
+
851
+ await Promise.all(shadowUpdatePromises);
761
852
  }
762
853
  }
763
854
 
764
- public async stop() {
765
- // This method is currently only used by the CLI, and shadow messages can be
766
- // lost since we aren't waiting for responses so sleep for a short time to
767
- // receive them
768
- await sleep(1000);
769
- this.device.end();
855
+ public async handleSecureTunnelMessage(payload: any): Promise<void> {
856
+ logger.info(`Received secure tunnel update: ${JSON.stringify(payload)}`);
857
+ const state = getUpdateDeltaStateFromMessage(payload);
858
+ if (!state) {
859
+ logger.debug(`No state found in message: ${JSON.stringify(payload)}`);
860
+ return;
861
+ }
862
+ const valid = validateSecureTunnelShadowUpdate(state);
863
+ if (!valid) {
864
+ logger.error(
865
+ `Error validating message: ${JSON.stringify(
866
+ { payload, errors: validateSecureTunnelShadowUpdate.errors },
867
+ null,
868
+ 2
869
+ )}`
870
+ );
871
+ return;
872
+ }
873
+ const secureTunnelUpdate =
874
+ await this.secureTunnelHandler.syncShadowToDeviceState(payload);
875
+ await this.shadowHandler.updateSecureTunnelShadow(secureTunnelUpdate);
770
876
  }
771
877
  }
772
878
 
@@ -774,20 +880,28 @@ export async function runDeviceAgentCloudInterface() {
774
880
  logger.info(
775
881
  `Starting alwaysAI Device Agent v${await getDeviceAgentVersion()}`
776
882
  );
777
- if (cloudModeReady()) {
883
+ if (existsSync(getBootstrapPrivateKeyFilePath())) {
884
+ await bootstrapProvision();
885
+ return;
886
+ }
887
+
888
+ const filesAlreadyMigrated = await requiredConfigFilesPresentAndValid();
889
+ if (!filesAlreadyMigrated) {
890
+ logger.debug('Attempting configuration file migration.');
891
+ await migrateFromLegacyCertsAndTokens();
892
+ }
893
+
894
+ if (await requiredConfigFilesPresentAndValid()) {
778
895
  const deviceAgent = new DeviceAgentCloudConnection();
779
- await deviceAgent.setupHandlers();
780
896
  if (ALWAYSAI_ANALYTICS_PASSTHROUGH === true) {
897
+ const shadowHandler = deviceAgent.shadowHandler;
781
898
  const publisher = deviceAgent.publisher;
782
- const passthroughHandler = new PassthroughHandler(publisher);
899
+ const passthroughHandler = new PassthroughHandler(
900
+ publisher,
901
+ shadowHandler
902
+ );
783
903
  await passthroughHandler.setup();
784
904
  }
785
- } else if (existsSync(BOOTSTRAP_PRIVATE_KEY_FILE_PATH())) {
786
- await bootstrapProvision();
787
- } else if (existsSync(BOOTSTRAP_CERTIFICATES_DIR_PATH())) {
788
- throw new Error(
789
- "Device has not been created using 'aai-agent device init' or there has been an issue with device initialization"
790
- );
791
905
  } else {
792
906
  throw new Error(
793
907
  "Set device agent to local mode and retry the 'aai-agent device init' command"