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