@marktrue/dsh-mobile-remote 3.2.1-alpha.1 → 3.2.1-alpha.2-app

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 (349) hide show
  1. package/DSH-Remote-v3.2.1.apk +4 -0
  2. package/README.md +6 -253
  3. package/manifest.json +21 -0
  4. package/package.json +10 -65
  5. package/CHANGELOG.md +0 -796
  6. package/CONTRIBUTING.md +0 -59
  7. package/FAQ.md +0 -111
  8. package/cordis.patch.yml +0 -14
  9. package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +0 -122
  10. package/docs/01-PRD.md +0 -148
  11. package/docs/02-architecture.md +0 -249
  12. package/docs/03-api.md +0 -816
  13. package/docs/04-security.md +0 -96
  14. package/docs/05-test-cases.md +0 -326
  15. package/docs/06-install-run.md +0 -308
  16. package/docs/07-user-manual.md +0 -65
  17. package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +0 -86
  18. package/docs/09-compatibility.md +0 -195
  19. package/docs/10-release.md +0 -927
  20. package/docs/screenshots/01-home.png +0 -0
  21. package/docs/screenshots/02-notifications.png +0 -0
  22. package/docs/screenshots/03-sessions.png +0 -0
  23. package/docs/screenshots/04-chat.png +0 -0
  24. package/docs/screenshots/05-settings.png +0 -0
  25. package/docs/screenshots/06-new-session.png +0 -0
  26. package/docs/screenshots/pgy-appstore.jpg +0 -0
  27. package/docs/screenshots/pgy-mobile-app.jpg +0 -0
  28. package/docs/screenshots/pgy-pc-client.jpg +0 -0
  29. package/lib/account-usage.js +0 -145
  30. package/lib/app-alignment.js +0 -112
  31. package/lib/app-carrier-store.js +0 -1972
  32. package/lib/app-carrier.js +0 -602
  33. package/lib/client.js +0 -706
  34. package/lib/desktop-form.js +0 -738
  35. package/lib/endpoint-registry.js +0 -162
  36. package/lib/git-read-bundled-provider.js +0 -255
  37. package/lib/git-read-service.js +0 -322
  38. package/lib/index.js +0 -9292
  39. package/lib/plugin-content-digest.js +0 -80
  40. package/lib/plugin-self-update.js +0 -758
  41. package/lib/plugin-update-controller.js +0 -95
  42. package/lib/plugin-update-transaction.js +0 -322
  43. package/lib/plugin-update.js +0 -309
  44. package/lib/runtime-dependency-snapshot.js +0 -345
  45. package/lib/session-title-cache.js +0 -123
  46. package/lib/session-title-refresh.js +0 -261
  47. package/lib/update-fetch.js +0 -24
  48. package/lib/update-lock-arbiter.js +0 -158
  49. package/lib/update-supervisor.js +0 -312
  50. package/node_modules/ansi-regex/index.d.ts +0 -37
  51. package/node_modules/ansi-regex/index.js +0 -10
  52. package/node_modules/ansi-regex/license +0 -9
  53. package/node_modules/ansi-regex/package.json +0 -55
  54. package/node_modules/ansi-regex/readme.md +0 -78
  55. package/node_modules/ansi-styles/index.d.ts +0 -345
  56. package/node_modules/ansi-styles/index.js +0 -163
  57. package/node_modules/ansi-styles/license +0 -9
  58. package/node_modules/ansi-styles/package.json +0 -56
  59. package/node_modules/ansi-styles/readme.md +0 -152
  60. package/node_modules/camelcase/index.d.ts +0 -63
  61. package/node_modules/camelcase/index.js +0 -76
  62. package/node_modules/camelcase/license +0 -9
  63. package/node_modules/camelcase/package.json +0 -43
  64. package/node_modules/camelcase/readme.md +0 -99
  65. package/node_modules/cliui/CHANGELOG.md +0 -76
  66. package/node_modules/cliui/LICENSE.txt +0 -14
  67. package/node_modules/cliui/README.md +0 -115
  68. package/node_modules/cliui/index.js +0 -354
  69. package/node_modules/cliui/package.json +0 -65
  70. package/node_modules/color-convert/CHANGELOG.md +0 -54
  71. package/node_modules/color-convert/LICENSE +0 -21
  72. package/node_modules/color-convert/README.md +0 -68
  73. package/node_modules/color-convert/conversions.js +0 -839
  74. package/node_modules/color-convert/index.js +0 -81
  75. package/node_modules/color-convert/package.json +0 -48
  76. package/node_modules/color-convert/route.js +0 -97
  77. package/node_modules/color-name/LICENSE +0 -8
  78. package/node_modules/color-name/README.md +0 -11
  79. package/node_modules/color-name/index.js +0 -152
  80. package/node_modules/color-name/package.json +0 -28
  81. package/node_modules/decamelize/index.js +0 -13
  82. package/node_modules/decamelize/license +0 -21
  83. package/node_modules/decamelize/package.json +0 -38
  84. package/node_modules/decamelize/readme.md +0 -48
  85. package/node_modules/dijkstrajs/.travis.yml +0 -4
  86. package/node_modules/dijkstrajs/CONTRIBUTING.md +0 -8
  87. package/node_modules/dijkstrajs/LICENSE.md +0 -19
  88. package/node_modules/dijkstrajs/README.md +0 -18
  89. package/node_modules/dijkstrajs/dijkstra.js +0 -165
  90. package/node_modules/dijkstrajs/package.json +0 -60
  91. package/node_modules/dijkstrajs/test/dijkstra.test.js +0 -96
  92. package/node_modules/emoji-regex/LICENSE-MIT.txt +0 -20
  93. package/node_modules/emoji-regex/README.md +0 -73
  94. package/node_modules/emoji-regex/es2015/index.js +0 -6
  95. package/node_modules/emoji-regex/es2015/text.js +0 -6
  96. package/node_modules/emoji-regex/index.d.ts +0 -23
  97. package/node_modules/emoji-regex/index.js +0 -6
  98. package/node_modules/emoji-regex/package.json +0 -50
  99. package/node_modules/emoji-regex/text.js +0 -6
  100. package/node_modules/find-up/index.d.ts +0 -137
  101. package/node_modules/find-up/index.js +0 -89
  102. package/node_modules/find-up/license +0 -9
  103. package/node_modules/find-up/package.json +0 -53
  104. package/node_modules/find-up/readme.md +0 -156
  105. package/node_modules/get-caller-file/LICENSE.md +0 -6
  106. package/node_modules/get-caller-file/README.md +0 -41
  107. package/node_modules/get-caller-file/index.d.ts +0 -2
  108. package/node_modules/get-caller-file/index.js +0 -22
  109. package/node_modules/get-caller-file/index.js.map +0 -1
  110. package/node_modules/get-caller-file/package.json +0 -42
  111. package/node_modules/is-fullwidth-code-point/index.d.ts +0 -17
  112. package/node_modules/is-fullwidth-code-point/index.js +0 -50
  113. package/node_modules/is-fullwidth-code-point/license +0 -9
  114. package/node_modules/is-fullwidth-code-point/package.json +0 -42
  115. package/node_modules/is-fullwidth-code-point/readme.md +0 -39
  116. package/node_modules/locate-path/index.d.ts +0 -83
  117. package/node_modules/locate-path/index.js +0 -65
  118. package/node_modules/locate-path/license +0 -9
  119. package/node_modules/locate-path/package.json +0 -45
  120. package/node_modules/locate-path/readme.md +0 -122
  121. package/node_modules/p-limit/index.d.ts +0 -38
  122. package/node_modules/p-limit/index.js +0 -57
  123. package/node_modules/p-limit/license +0 -9
  124. package/node_modules/p-limit/package.json +0 -52
  125. package/node_modules/p-limit/readme.md +0 -101
  126. package/node_modules/p-locate/index.d.ts +0 -64
  127. package/node_modules/p-locate/index.js +0 -52
  128. package/node_modules/p-locate/license +0 -9
  129. package/node_modules/p-locate/package.json +0 -53
  130. package/node_modules/p-locate/readme.md +0 -90
  131. package/node_modules/p-try/index.d.ts +0 -39
  132. package/node_modules/p-try/index.js +0 -9
  133. package/node_modules/p-try/license +0 -9
  134. package/node_modules/p-try/package.json +0 -42
  135. package/node_modules/p-try/readme.md +0 -58
  136. package/node_modules/path-exists/index.d.ts +0 -28
  137. package/node_modules/path-exists/index.js +0 -23
  138. package/node_modules/path-exists/license +0 -9
  139. package/node_modules/path-exists/package.json +0 -39
  140. package/node_modules/path-exists/readme.md +0 -52
  141. package/node_modules/pngjs/.eslintignore +0 -2
  142. package/node_modules/pngjs/.eslintrc.json +0 -19
  143. package/node_modules/pngjs/.prettierignore +0 -3
  144. package/node_modules/pngjs/LICENSE +0 -20
  145. package/node_modules/pngjs/README.md +0 -429
  146. package/node_modules/pngjs/browser.js +0 -16096
  147. package/node_modules/pngjs/coverage/lcov-report/base.css +0 -354
  148. package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +0 -881
  149. package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +0 -554
  150. package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +0 -77
  151. package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +0 -647
  152. package/node_modules/pngjs/coverage/lcov-report/constants.js.html +0 -176
  153. package/node_modules/pngjs/coverage/lcov-report/crc.js.html +0 -200
  154. package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
  155. package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +0 -593
  156. package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +0 -152
  157. package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +0 -143
  158. package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +0 -611
  159. package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +0 -359
  160. package/node_modules/pngjs/coverage/lcov-report/index.html +0 -426
  161. package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +0 -365
  162. package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +0 -230
  163. package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +0 -248
  164. package/node_modules/pngjs/coverage/lcov-report/packer.js.html +0 -467
  165. package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +0 -128
  166. package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +0 -575
  167. package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +0 -404
  168. package/node_modules/pngjs/coverage/lcov-report/parser.js.html +0 -950
  169. package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +0 -116
  170. package/node_modules/pngjs/coverage/lcov-report/png.js.html +0 -662
  171. package/node_modules/pngjs/coverage/lcov-report/prettify.css +0 -101
  172. package/node_modules/pngjs/coverage/lcov-report/prettify.js +0 -1007
  173. package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  174. package/node_modules/pngjs/coverage/lcov-report/sorter.js +0 -169
  175. package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +0 -584
  176. package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +0 -215
  177. package/node_modules/pngjs/coverage/lcov.info +0 -2177
  178. package/node_modules/pngjs/lib/bitmapper.js +0 -267
  179. package/node_modules/pngjs/lib/bitpacker.js +0 -158
  180. package/node_modules/pngjs/lib/chunkstream.js +0 -189
  181. package/node_modules/pngjs/lib/constants.js +0 -32
  182. package/node_modules/pngjs/lib/crc.js +0 -40
  183. package/node_modules/pngjs/lib/filter-pack.js +0 -171
  184. package/node_modules/pngjs/lib/filter-parse-async.js +0 -24
  185. package/node_modules/pngjs/lib/filter-parse-sync.js +0 -21
  186. package/node_modules/pngjs/lib/filter-parse.js +0 -177
  187. package/node_modules/pngjs/lib/format-normaliser.js +0 -93
  188. package/node_modules/pngjs/lib/interlace.js +0 -95
  189. package/node_modules/pngjs/lib/packer-async.js +0 -50
  190. package/node_modules/pngjs/lib/packer-sync.js +0 -56
  191. package/node_modules/pngjs/lib/packer.js +0 -129
  192. package/node_modules/pngjs/lib/paeth-predictor.js +0 -16
  193. package/node_modules/pngjs/lib/parser-async.js +0 -165
  194. package/node_modules/pngjs/lib/parser-sync.js +0 -108
  195. package/node_modules/pngjs/lib/parser.js +0 -290
  196. package/node_modules/pngjs/lib/png-sync.js +0 -12
  197. package/node_modules/pngjs/lib/png.js +0 -194
  198. package/node_modules/pngjs/lib/sync-inflate.js +0 -168
  199. package/node_modules/pngjs/lib/sync-reader.js +0 -45
  200. package/node_modules/pngjs/package.json +0 -73
  201. package/node_modules/qrcode/README.md +0 -770
  202. package/node_modules/qrcode/bin/qrcode +0 -159
  203. package/node_modules/qrcode/helper/to-sjis-browser.js +0 -2
  204. package/node_modules/qrcode/helper/to-sjis.js +0 -105
  205. package/node_modules/qrcode/lib/browser.js +0 -76
  206. package/node_modules/qrcode/lib/can-promise.js +0 -7
  207. package/node_modules/qrcode/lib/core/alignment-pattern.js +0 -83
  208. package/node_modules/qrcode/lib/core/alphanumeric-data.js +0 -59
  209. package/node_modules/qrcode/lib/core/bit-buffer.js +0 -37
  210. package/node_modules/qrcode/lib/core/bit-matrix.js +0 -65
  211. package/node_modules/qrcode/lib/core/byte-data.js +0 -30
  212. package/node_modules/qrcode/lib/core/error-correction-code.js +0 -135
  213. package/node_modules/qrcode/lib/core/error-correction-level.js +0 -50
  214. package/node_modules/qrcode/lib/core/finder-pattern.js +0 -22
  215. package/node_modules/qrcode/lib/core/format-info.js +0 -29
  216. package/node_modules/qrcode/lib/core/galois-field.js +0 -69
  217. package/node_modules/qrcode/lib/core/kanji-data.js +0 -54
  218. package/node_modules/qrcode/lib/core/mask-pattern.js +0 -234
  219. package/node_modules/qrcode/lib/core/mode.js +0 -167
  220. package/node_modules/qrcode/lib/core/numeric-data.js +0 -43
  221. package/node_modules/qrcode/lib/core/polynomial.js +0 -62
  222. package/node_modules/qrcode/lib/core/qrcode.js +0 -495
  223. package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +0 -56
  224. package/node_modules/qrcode/lib/core/regex.js +0 -31
  225. package/node_modules/qrcode/lib/core/segments.js +0 -330
  226. package/node_modules/qrcode/lib/core/utils.js +0 -63
  227. package/node_modules/qrcode/lib/core/version-check.js +0 -9
  228. package/node_modules/qrcode/lib/core/version.js +0 -163
  229. package/node_modules/qrcode/lib/index.js +0 -12
  230. package/node_modules/qrcode/lib/renderer/canvas.js +0 -63
  231. package/node_modules/qrcode/lib/renderer/png.js +0 -78
  232. package/node_modules/qrcode/lib/renderer/svg-tag.js +0 -81
  233. package/node_modules/qrcode/lib/renderer/svg.js +0 -19
  234. package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +0 -85
  235. package/node_modules/qrcode/lib/renderer/terminal/terminal.js +0 -49
  236. package/node_modules/qrcode/lib/renderer/terminal.js +0 -9
  237. package/node_modules/qrcode/lib/renderer/utf8.js +0 -71
  238. package/node_modules/qrcode/lib/renderer/utils.js +0 -99
  239. package/node_modules/qrcode/lib/server.js +0 -138
  240. package/node_modules/qrcode/license +0 -10
  241. package/node_modules/qrcode/package.json +0 -77
  242. package/node_modules/require-directory/.jshintrc +0 -67
  243. package/node_modules/require-directory/.npmignore +0 -1
  244. package/node_modules/require-directory/.travis.yml +0 -3
  245. package/node_modules/require-directory/LICENSE +0 -22
  246. package/node_modules/require-directory/README.markdown +0 -184
  247. package/node_modules/require-directory/index.js +0 -86
  248. package/node_modules/require-directory/package.json +0 -40
  249. package/node_modules/require-main-filename/CHANGELOG.md +0 -26
  250. package/node_modules/require-main-filename/LICENSE.txt +0 -14
  251. package/node_modules/require-main-filename/README.md +0 -26
  252. package/node_modules/require-main-filename/index.js +0 -18
  253. package/node_modules/require-main-filename/package.json +0 -35
  254. package/node_modules/set-blocking/CHANGELOG.md +0 -26
  255. package/node_modules/set-blocking/LICENSE.txt +0 -14
  256. package/node_modules/set-blocking/README.md +0 -31
  257. package/node_modules/set-blocking/index.js +0 -7
  258. package/node_modules/set-blocking/package.json +0 -42
  259. package/node_modules/string-width/index.d.ts +0 -29
  260. package/node_modules/string-width/index.js +0 -47
  261. package/node_modules/string-width/license +0 -9
  262. package/node_modules/string-width/package.json +0 -56
  263. package/node_modules/string-width/readme.md +0 -50
  264. package/node_modules/strip-ansi/index.d.ts +0 -17
  265. package/node_modules/strip-ansi/index.js +0 -4
  266. package/node_modules/strip-ansi/license +0 -9
  267. package/node_modules/strip-ansi/package.json +0 -54
  268. package/node_modules/strip-ansi/readme.md +0 -46
  269. package/node_modules/which-module/LICENSE +0 -13
  270. package/node_modules/which-module/README.md +0 -58
  271. package/node_modules/which-module/index.js +0 -9
  272. package/node_modules/which-module/package.json +0 -41
  273. package/node_modules/wrap-ansi/index.js +0 -186
  274. package/node_modules/wrap-ansi/license +0 -9
  275. package/node_modules/wrap-ansi/package.json +0 -61
  276. package/node_modules/wrap-ansi/readme.md +0 -97
  277. package/node_modules/y18n/CHANGELOG.md +0 -35
  278. package/node_modules/y18n/LICENSE +0 -13
  279. package/node_modules/y18n/README.md +0 -109
  280. package/node_modules/y18n/index.js +0 -188
  281. package/node_modules/y18n/package.json +0 -39
  282. package/node_modules/yargs/CHANGELOG.md +0 -420
  283. package/node_modules/yargs/LICENSE +0 -21
  284. package/node_modules/yargs/README.md +0 -140
  285. package/node_modules/yargs/build/lib/apply-extends.d.ts +0 -2
  286. package/node_modules/yargs/build/lib/apply-extends.js +0 -65
  287. package/node_modules/yargs/build/lib/argsert.d.ts +0 -2
  288. package/node_modules/yargs/build/lib/argsert.js +0 -65
  289. package/node_modules/yargs/build/lib/command.d.ts +0 -64
  290. package/node_modules/yargs/build/lib/command.js +0 -416
  291. package/node_modules/yargs/build/lib/common-types.d.ts +0 -36
  292. package/node_modules/yargs/build/lib/common-types.js +0 -25
  293. package/node_modules/yargs/build/lib/completion-templates.d.ts +0 -2
  294. package/node_modules/yargs/build/lib/completion-templates.js +0 -50
  295. package/node_modules/yargs/build/lib/completion.d.ts +0 -21
  296. package/node_modules/yargs/build/lib/completion.js +0 -135
  297. package/node_modules/yargs/build/lib/is-promise.d.ts +0 -1
  298. package/node_modules/yargs/build/lib/is-promise.js +0 -9
  299. package/node_modules/yargs/build/lib/levenshtein.d.ts +0 -1
  300. package/node_modules/yargs/build/lib/levenshtein.js +0 -58
  301. package/node_modules/yargs/build/lib/middleware.d.ts +0 -10
  302. package/node_modules/yargs/build/lib/middleware.js +0 -57
  303. package/node_modules/yargs/build/lib/obj-filter.d.ts +0 -1
  304. package/node_modules/yargs/build/lib/obj-filter.js +0 -14
  305. package/node_modules/yargs/build/lib/parse-command.d.ts +0 -11
  306. package/node_modules/yargs/build/lib/parse-command.js +0 -36
  307. package/node_modules/yargs/build/lib/process-argv.d.ts +0 -2
  308. package/node_modules/yargs/build/lib/process-argv.js +0 -31
  309. package/node_modules/yargs/build/lib/usage.d.ts +0 -49
  310. package/node_modules/yargs/build/lib/usage.js +0 -540
  311. package/node_modules/yargs/build/lib/validation.d.ts +0 -34
  312. package/node_modules/yargs/build/lib/validation.js +0 -330
  313. package/node_modules/yargs/build/lib/yargs.d.ts +0 -274
  314. package/node_modules/yargs/build/lib/yargs.js +0 -1190
  315. package/node_modules/yargs/build/lib/yerror.d.ts +0 -4
  316. package/node_modules/yargs/build/lib/yerror.js +0 -11
  317. package/node_modules/yargs/index.js +0 -40
  318. package/node_modules/yargs/locales/be.json +0 -46
  319. package/node_modules/yargs/locales/de.json +0 -46
  320. package/node_modules/yargs/locales/en.json +0 -51
  321. package/node_modules/yargs/locales/es.json +0 -46
  322. package/node_modules/yargs/locales/fi.json +0 -49
  323. package/node_modules/yargs/locales/fr.json +0 -53
  324. package/node_modules/yargs/locales/hi.json +0 -49
  325. package/node_modules/yargs/locales/hu.json +0 -46
  326. package/node_modules/yargs/locales/id.json +0 -50
  327. package/node_modules/yargs/locales/it.json +0 -46
  328. package/node_modules/yargs/locales/ja.json +0 -51
  329. package/node_modules/yargs/locales/ko.json +0 -49
  330. package/node_modules/yargs/locales/nb.json +0 -44
  331. package/node_modules/yargs/locales/nl.json +0 -49
  332. package/node_modules/yargs/locales/nn.json +0 -44
  333. package/node_modules/yargs/locales/pirate.json +0 -13
  334. package/node_modules/yargs/locales/pl.json +0 -49
  335. package/node_modules/yargs/locales/pt.json +0 -45
  336. package/node_modules/yargs/locales/pt_BR.json +0 -48
  337. package/node_modules/yargs/locales/ru.json +0 -46
  338. package/node_modules/yargs/locales/th.json +0 -46
  339. package/node_modules/yargs/locales/tr.json +0 -48
  340. package/node_modules/yargs/locales/zh_CN.json +0 -48
  341. package/node_modules/yargs/locales/zh_TW.json +0 -47
  342. package/node_modules/yargs/package.json +0 -92
  343. package/node_modules/yargs/yargs.js +0 -14
  344. package/node_modules/yargs-parser/CHANGELOG.md +0 -601
  345. package/node_modules/yargs-parser/LICENSE.txt +0 -14
  346. package/node_modules/yargs-parser/README.md +0 -449
  347. package/node_modules/yargs-parser/index.js +0 -1032
  348. package/node_modules/yargs-parser/lib/tokenize-arg-string.js +0 -40
  349. package/node_modules/yargs-parser/package.json +0 -46
@@ -1,758 +0,0 @@
1
- // 插件自更新(CONTEXT「插件自更新」「更新接续」)。
2
- //
3
- // 本模块负责**事务的推进**,副作用一律可注入;"重启后是否真的生效、失败怎么回滚"
4
- // 交给独立的外部监督器(`lib/update-supervisor.js`)——因为回滚绝不能依赖新版插件
5
- // 能否加载成功(评审 #32 note 1286 的 R1)。
6
- //
7
- // 关键约束(都来自那次评审):
8
- // - 产物目录按**内容标识**命名且不可变,同版本不同内容不会互相覆盖(R2);
9
- // - profile 目录靠**验证式查找**得到,不靠模块层级推断(R3);
10
- // - 下载地址由 `resolvePluginUrl()` 解析后接入执行链(R4);
11
- // - 期望内容标识以**解包后复算的值**为准(合成清单没有它也能正确收尾)(R5);
12
- // - 安装/解包用**异步子进程**,不在宿主主线程上阻塞 HTTP/SSE/agent(R6)。
13
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
14
- import { dirname, join } from "node:path";
15
- import { createHash } from "node:crypto";
16
- import { spawn, spawnSync } from "node:child_process";
17
- import { pluginContentDigest } from "./plugin-content-digest.js";
18
- import { fetchUpdateResource } from "./update-fetch.js";
19
- import { decidePluginUpdate, normalizeReleaseManifest } from "./plugin-update.js";
20
- import {
21
- TRANSACTION_PHASE,
22
- acquireLock,
23
- assertUpdateLease,
24
- serverOperationPath,
25
- switchOwnedDependency,
26
- validOperationId,
27
- writeOwnedTransaction,
28
- artifactDir,
29
- artifactRoot,
30
- convergeStaleTransaction,
31
- defaultCopyDir,
32
- dependencyKeyIn,
33
- dependencyKeysFor,
34
- LEGACY_DEPENDENCY_KEY,
35
- dirReady,
36
- newOperationId,
37
- readTransaction,
38
- releaseLock,
39
- snapshotDir,
40
- snapshotInstalled,
41
- snapshotReady,
42
- snapshotRoot,
43
- transactionFile,
44
- updateLockPath,
45
- workRoot,
46
- writeTransaction,
47
- } from "./plugin-update-transaction.js";
48
-
49
- export { TRANSACTION_PHASE };
50
- export { transactionFile };
51
- export { updateLockPath };
52
- export const updateLockFile = updateLockPath;
53
-
54
- /** 兼容旧名(状态文件即事务文件)。 */
55
- export const updateStateFile = transactionFile;
56
- export const readUpdateState = readTransaction;
57
- export const writeUpdateState = writeTransaction;
58
-
59
- /** 插件自更新的默认来源:上游仓库的 Release(清单优先,缺失则按 release 资产合成)。 */
60
- export const UPSTREAM_REPO = "201222-L/dsh-mobile-remote";
61
- export const UPSTREAM_MANIFEST_URL = `https://github.com/${UPSTREAM_REPO}/releases/latest/download/manifest.json`;
62
- export const UPSTREAM_RELEASES_API = `https://api.github.com/repos/${UPSTREAM_REPO}/releases/latest`;
63
-
64
- /** 更新来源的三种配置形态。 */
65
- export const UPDATE_SOURCE_KIND = { UPSTREAM: "upstream", OFF: "off", CUSTOM: "custom", INVALID: "invalid" };
66
-
67
- /** 检查结果(会随 bootstrap/diagnostics 下发,也是 PC 端提示的依据)。 */
68
- export const UPDATE_CHECK_STATE = {
69
- DISABLED: "disabled",
70
- UNAVAILABLE: "unavailable",
71
- UP_TO_DATE: "up-to-date",
72
- UPDATE_AVAILABLE: "update-available",
73
- LOCAL_AHEAD: "local-ahead",
74
- UNKNOWN: "unknown",
75
- };
76
-
77
- /** 自更新过程阶段(进度上报用)。 */
78
- export const UPDATE_STEP = {
79
- DOWNLOAD: "download",
80
- VERIFY: "verify",
81
- EXTRACT: "extract",
82
- SNAPSHOT: "snapshot",
83
- SWITCH: "switch",
84
- INSTALL: "install",
85
- RESTART: "restart",
86
- };
87
-
88
- /** 回滚最多一次:避免"重启风暴"。 */
89
- export const MAX_ROLLBACK_ATTEMPTS = 1;
90
-
91
- /** 异步跑一个子进程(**不阻塞宿主事件循环**)。 */
92
- export function runCommand(cmd, args, { cwd, timeoutMs = 10 * 60 * 1000 } = {}) {
93
- return new Promise((resolve) => {
94
- let child;
95
- try {
96
- // stdout 直接丢弃(**不能**设成 pipe 又不消费:管道写满后子进程会阻塞到超时,F5)。
97
- // stderr 保留尾部若干字节用于报错。
98
- child = spawn(cmd, args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
99
- } catch (e) {
100
- resolve({ ok: false, error: String(e?.message ?? e) });
101
- return;
102
- }
103
- const STDERR_CAP = 16 * 1024;
104
- let stderr = "";
105
- let timedOut = false;
106
- const timer = setTimeout(() => {
107
- timedOut = true;
108
- try {
109
- child.kill("SIGKILL");
110
- } catch {
111
- // 已经退出
112
- }
113
- }, timeoutMs);
114
- child.stderr?.on("data", (d) => {
115
- stderr += d.toString();
116
- if (stderr.length > STDERR_CAP) stderr = stderr.slice(-STDERR_CAP);
117
- });
118
- child.on("error", (e) => {
119
- clearTimeout(timer);
120
- resolve({ ok: false, error: String(e?.message ?? e) });
121
- });
122
- child.on("close", (code) => {
123
- clearTimeout(timer);
124
- if (timedOut) {
125
- resolve({ ok: false, error: `timeout-after-${Math.round(timeoutMs / 1000)}s${stderr ? `: ${stderr.trim()}` : ""}` });
126
- return;
127
- }
128
- resolve(code === 0 ? { ok: true } : { ok: false, error: (stderr || `exit ${code}`).trim() });
129
- });
130
- });
131
- }
132
-
133
- // ── 更新来源 ──────────────────────────────────────────────────────────────
134
-
135
- /** 解析配置里的更新来源。默认 `upstream`;`off` 表示完全不检查;其余必须是 https 地址。 */
136
- export function resolveManifestSource(configValue) {
137
- const v = typeof configValue === "string" ? configValue.trim() : "";
138
- if (v === "" || v === "off") return { kind: UPDATE_SOURCE_KIND.OFF };
139
- if (v === "upstream") return { kind: UPDATE_SOURCE_KIND.UPSTREAM, url: UPSTREAM_MANIFEST_URL, api: UPSTREAM_RELEASES_API };
140
- // 自建主机源就在同一台机器上:受保护端点走回环 http(凭据只发给这个 origin)。
141
- // 外部地址仍只允许 https,不放宽。
142
- if (/^https:\/\/\S+$/.test(v)) return { kind: UPDATE_SOURCE_KIND.CUSTOM, url: v };
143
- try {
144
- const u = new URL(v);
145
- if (u.protocol === "http:" && (u.hostname === "127.0.0.1" || u.hostname === "localhost") && u.pathname.startsWith("/")) {
146
- return { kind: UPDATE_SOURCE_KIND.CUSTOM, url: v, loopback: true };
147
- }
148
- } catch {
149
- // 交给下面判非法
150
- }
151
- return { kind: UPDATE_SOURCE_KIND.INVALID, value: v };
152
- }
153
-
154
- async function fetchJson(url, fetchImpl, userAgent, auth = {}) {
155
- try {
156
- const { response: res, finalUrl } = await fetchUpdateResource(url, { fetchImpl, ...auth,
157
- headers: { "user-agent": userAgent, accept: "application/json" }, signal: AbortSignal.timeout(10_000),
158
- });
159
- if (!res?.ok) return { ok: false, error: `manifest-http-${res?.status ?? "?"}` };
160
- return { ok: true, json: await res.json(), finalUrl };
161
- } catch (e) {
162
- return { ok: false, error: `manifest-fetch-failed: ${e?.message ?? e}` };
163
- }
164
- }
165
-
166
- /**
167
- * 从 GitHub Release 资产**合成**一份清单。上游尚未把 manifest.json 作为发布资产时,
168
- * 走这条路仍能拿到版本号与字节哈希(asset 自带的 digest),只是**没有内容标识**——
169
- * 执行时以解包后复算的内容标识为准(R5),判定阶段则如实标注 synthesized。
170
- */
171
- export async function synthesizeFromGithubRelease(apiUrl, { fetchImpl = fetch, userAgent = "dsh-mobile-remote" } = {}) {
172
- const r = await fetchJson(apiUrl, fetchImpl, userAgent);
173
- if (!r.ok) return { ok: false, error: r.error };
174
- const release = r.json ?? {};
175
- const version = String(release.tag_name ?? "").replace(/^v/, "");
176
- const asset = (Array.isArray(release.assets) ? release.assets : []).find((a) => typeof a?.name === "string" && a.name.endsWith(".tgz"));
177
- if (!version || !asset) return { ok: false, error: "release-asset-missing" };
178
- const digest = String(asset.digest ?? "");
179
- const manifest = normalizeReleaseManifest({
180
- schema: 1,
181
- plugin: {
182
- version,
183
- file: asset.name,
184
- url: asset.browser_download_url ?? "",
185
- size: Number.isFinite(asset.size) ? asset.size : undefined,
186
- sha256: digest.startsWith("sha256:") ? digest.slice("sha256:".length) : "",
187
- },
188
- });
189
- if (!manifest.ok) return { ok: false, error: manifest.error };
190
- return { ok: true, manifest, synthesized: true };
191
- }
192
-
193
- /** 主机源返回的是 `{ ok, manifest }` 信封;解包后再规范化(G1)。 */
194
- export function unwrapManifestEnvelope(raw) {
195
- if (raw && typeof raw === "object" && !Array.isArray(raw) && raw.manifest && typeof raw.manifest === "object" && !("schema" in raw)) {
196
- return raw.manifest;
197
- }
198
- return raw;
199
- }
200
-
201
- /** 取回并规范化发布清单。upstream 优先取清单资产;取不到再按 release 资产合成。 */
202
- export async function fetchReleaseManifest({ source, fetchImpl = fetch, userAgent = "dsh-mobile-remote", auth = {} } = {}) {
203
- if (!source || source.kind === UPDATE_SOURCE_KIND.OFF) return { ok: false, error: "update-source-off" };
204
- if (source.kind === UPDATE_SOURCE_KIND.INVALID) return { ok: false, error: "update-source-invalid" };
205
- if (source.kind === UPDATE_SOURCE_KIND.UPSTREAM) {
206
- const direct = await fetchJson(source.url, fetchImpl, userAgent, auth);
207
- if (direct.ok) {
208
- const manifest = normalizeReleaseManifest(unwrapManifestEnvelope(direct.json));
209
- if (manifest.ok) return { ok: true, manifest, synthesized: false, manifestUrl: direct.finalUrl };
210
- }
211
- const synthesized = await synthesizeFromGithubRelease(source.api, { fetchImpl, userAgent });
212
- return synthesized.ok ? { ...synthesized, manifestUrl: source.api } : synthesized;
213
- }
214
- const custom = await fetchJson(source.url, fetchImpl, userAgent, auth);
215
- if (!custom.ok) return { ok: false, error: custom.error };
216
- const manifest = normalizeReleaseManifest(unwrapManifestEnvelope(custom.json));
217
- if (!manifest.ok) return { ok: false, error: manifest.error };
218
- return { ok: true, manifest, synthesized: false, manifestUrl: custom.finalUrl };
219
- }
220
-
221
- /**
222
- * 解析插件包的下载地址(G1):
223
- * - 清单给了**绝对** url → 直接用;
224
- * - 清单给了**相对** url → 相对**清单自身地址**解析(主机源就是这种:`/m/api/update/plugin`);
225
- * - 只给了文件名 → 同样相对清单地址解析。
226
- */
227
- export function resolvePluginUrl(manifestUrl, plugin) {
228
- const raw = typeof plugin?.url === "string" ? plugin.url.trim() : "";
229
- const target = raw !== "" ? raw : (plugin?.file ?? "");
230
- if (target === "") return "";
231
- try {
232
- // 绝对地址直接返回(校验交给下载器的协议白名单)
233
- if (/^[a-z][a-z0-9+.-]*:/i.test(target)) return target;
234
- if (!manifestUrl) return "";
235
- return new URL(target, manifestUrl).toString();
236
- } catch {
237
- return "";
238
- }
239
- }
240
-
241
- /** 只允许 https(或本机回环 http,供自建主机源使用)。 */
242
- export function isAllowedDownloadUrl(url) {
243
- try {
244
- const u = new URL(url);
245
- if (u.protocol === "https:") return true;
246
- if (u.protocol === "http:" && (u.hostname === "127.0.0.1" || u.hostname === "localhost")) return true;
247
- return false;
248
- } catch {
249
- return false;
250
- }
251
- }
252
-
253
- /**
254
- * 该形态下如何更新;不能自更新时给出**与实际安装方式对应的那一条**降级指引(R11)。
255
- * `canSelfUpdate` 由调用方按宿主运行形态判定后传入。
256
- */
257
- export function planForShape(shape, { specifier = "", dirPath = "", canSelfUpdate = true, reason = "" } = {}) {
258
- const installCmd = "pnpm install";
259
- const base = (() => {
260
- switch (shape) {
261
- case "registry":
262
- return {
263
- summary: "从 npm 源升级:改写 profile 依赖版本后重新安装",
264
- steps: [`把 profile 的依赖版本改为新版(当前 ${specifier || "未知"})`, "重新安装依赖", "重启 DSH 让插件生效"],
265
- commands: [installCmd],
266
- };
267
- case "github-tag":
268
- return {
269
- summary: "git 依赖升级:把 tag 改到新版本后重新安装",
270
- steps: [`把 profile 依赖的 tag 改为 v<新版>(当前 ${specifier || "未知"})`, "重新安装依赖", "重启 DSH 让插件生效"],
271
- commands: [installCmd],
272
- };
273
- case "file-clone":
274
- return {
275
- summary: "本地 clone 升级:拉取新代码后重新安装",
276
- steps: [`在 clone 目录拉取新版本${dirPath ? `(${dirPath})` : ""}`, "重新安装依赖", "重启 DSH 让插件生效"],
277
- commands: [dirPath ? `git -C ${dirPath} pull` : "git pull", installCmd],
278
- };
279
- case "file-dir":
280
- return {
281
- summary: "本地目录升级:用新版内容替换该目录后重新安装",
282
- steps: [`用新版插件内容替换该目录${dirPath ? `(${dirPath})` : ""}`, "重新安装依赖", "重启 DSH 让插件生效"],
283
- commands: [installCmd],
284
- };
285
- case "tgz-file":
286
- return {
287
- summary: "tgz 安装升级:换上新版 tgz 后重新安装",
288
- steps: [`下载新版 tgz 并替换${dirPath ? `(${dirPath})` : "原包"}`, "重新安装依赖", "重启 DSH 让插件生效"],
289
- commands: [installCmd],
290
- };
291
- default:
292
- return {
293
- summary: "无法识别本机插件安装方式,按 docs/06-install-run.md 的三种方式之一处理",
294
- steps: ["确认插件是通过 clone、git tag 还是 tgz 安装的", "按对应方式更新到新版", "重启 DSH 让插件生效"],
295
- commands: [],
296
- };
297
- }
298
- })();
299
- return { ...base, shape, mode: canSelfUpdate ? "auto" : "guidance", reason };
300
- }
301
-
302
- /** 检查是否有新版(**不下载、不改动任何东西**)。 */
303
- export async function checkForUpdate({
304
- configValue = "upstream",
305
- localVersion = "",
306
- localContentSha256 = "",
307
- fetchImpl = fetch,
308
- userAgent = "dsh-mobile-remote",
309
- capabilities = null,
310
- auth = {},
311
- } = {}) {
312
- const withCaps = (r) => (capabilities ? { ...r, ...capabilities } : r);
313
- const source = resolveManifestSource(configValue);
314
- if (source.kind === UPDATE_SOURCE_KIND.OFF) {
315
- return withCaps({ state: UPDATE_CHECK_STATE.DISABLED, reason: "update-source-off", checkedAt: new Date().toISOString() });
316
- }
317
- if (source.kind === UPDATE_SOURCE_KIND.INVALID) {
318
- return withCaps({
319
- state: UPDATE_CHECK_STATE.UNAVAILABLE,
320
- reason: "update-source-invalid",
321
- source: String(source.value ?? ""),
322
- checkedAt: new Date().toISOString(),
323
- });
324
- }
325
- const fetched = await fetchReleaseManifest({ source, fetchImpl, userAgent, auth });
326
- if (!fetched.ok) {
327
- return withCaps({ state: UPDATE_CHECK_STATE.UNAVAILABLE, reason: fetched.error, source: source.kind, checkedAt: new Date().toISOString() });
328
- }
329
- const decision = decidePluginUpdate({ localVersion, localContentSha256, remote: fetched.manifest.plugin });
330
- return withCaps({
331
- state: decision.action,
332
- reason: decision.reason,
333
- source: source.kind,
334
- synthesized: Boolean(fetched.synthesized),
335
- manifestUrl: fetched.manifestUrl ?? "",
336
- release: fixedReleaseSnapshot(fetched.manifest, fetched.manifestUrl),
337
- remote: {
338
- version: fetched.manifest.plugin.version,
339
- contentSha256: fetched.manifest.plugin.contentSha256 || "",
340
- sha256: fetched.manifest.plugin.sha256 || "",
341
- size: fetched.manifest.plugin.size ?? null,
342
- url: resolvePluginUrl(fetched.manifestUrl ?? "", fetched.manifest.plugin),
343
- },
344
- checkedAt: new Date().toISOString(),
345
- });
346
- }
347
-
348
- export function fixedReleaseSnapshot(manifest, manifestUrl = "") {
349
- const app = manifest.app ? { ...manifest.app, url: resolvePluginUrl(manifestUrl, manifest.app) } : null;
350
- return { schema: 1, manifestUrl, app, plugin: { ...manifest.plugin, url: resolvePluginUrl(manifestUrl, manifest.plugin) } };
351
- }
352
-
353
- // ── 下载 / 解包 ───────────────────────────────────────────────────────────
354
-
355
- /**
356
- * 下载到文件并校验。**先落盘到临时名再改名**,避免半成品被当成成品。
357
- * `size`/`sha256` 任一不符即删除文件并失败——不做"先装上再说"。
358
- */
359
- export async function downloadVerified({ url, destPath, sha256, size = null, fetchImpl = fetch, onProgress, auth = {} } = {}) {
360
- if (!url) return { ok: false, error: "download-url-missing" };
361
- if (!isAllowedDownloadUrl(url)) return { ok: false, error: "download-url-not-allowed" };
362
- let response;
363
- try {
364
- ({ response } = await fetchUpdateResource(url, { fetchImpl, ...auth, signal: AbortSignal.timeout(120_000) }));
365
- } catch (e) {
366
- return { ok: false, error: `download-failed: ${e?.message ?? e}` };
367
- }
368
- if (!response?.ok) return { ok: false, error: `download-http-${response?.status ?? "?"}` };
369
-
370
- mkdirSync(dirname(destPath), { recursive: true });
371
- const tmpPath = `${destPath}.part`;
372
- try {
373
- const chunks = [];
374
- let bytes = 0;
375
- for await (const chunk of response.body ?? []) {
376
- chunks.push(Buffer.from(chunk));
377
- bytes += chunk.length;
378
- onProgress?.(bytes, size ?? response.headers?.get?.("content-length") ?? null);
379
- }
380
- const buf = Buffer.concat(chunks);
381
- bytes = buf.length;
382
- if (size !== null && bytes !== size) {
383
- return { ok: false, error: `download-size-mismatch: ${bytes} != ${size}` };
384
- }
385
- const digest = createHash("sha256").update(buf).digest("hex");
386
- if (sha256 && digest !== String(sha256).toLowerCase()) {
387
- return { ok: false, error: `download-sha256-mismatch: ${digest}` };
388
- }
389
- writeFileSync(tmpPath, buf);
390
- renameSync(tmpPath, destPath);
391
- return { ok: true, bytes, sha256: digest };
392
- } catch (e) {
393
- try {
394
- rmSync(tmpPath, { force: true });
395
- } catch {
396
- // 清理失败不掩盖原始错误
397
- }
398
- return { ok: false, error: `download-failed: ${e?.message ?? e}` };
399
- }
400
- }
401
-
402
- /** 默认解包器:系统 tar(异步,不阻塞宿主)。 */
403
- export function defaultRunTar(tgzPath, destDir) {
404
- return runCommand("tar", ["-xzf", tgzPath, "-C", destDir]);
405
- }
406
-
407
- /** 默认安装器:包管理器(异步、带超时)。 */
408
- export function defaultRunInstall(profileDir) {
409
- return runCommand("pnpm", ["install"], { cwd: profileDir });
410
- }
411
-
412
- /**
413
- * 解包并复核**内容标识**:把「发布侧声明的内容」与「真正落到磁盘上的内容」钉在一起。
414
- * 返回值里的 `digest` 就是**复算出的真实内容标识**——事务以它作为期望值(R5)。
415
- */
416
- export async function extractVerified({ tgzPath, workDir, contentSha256, runTar = defaultRunTar } = {}) {
417
- rmSync(workDir, { recursive: true, force: true });
418
- mkdirSync(workDir, { recursive: true });
419
- const r = await runTar(tgzPath, workDir);
420
- if (!r?.ok) return { ok: false, error: `extract-failed: ${r?.error ?? "unknown"}` };
421
- const packageDir = join(workDir, "package");
422
- if (!existsSync(packageDir)) return { ok: false, error: "extract-missing-package-dir" };
423
- const digest = pluginContentDigest(packageDir);
424
- const expected = String(contentSha256 ?? "").toLowerCase();
425
- if (expected && digest !== expected) {
426
- return { ok: false, error: `content-sha256-mismatch: ${digest}`, digest, packageDir };
427
- }
428
- return { ok: true, digest, packageDir };
429
- }
430
-
431
- /** 把解包好的版本目录记进 profile 的依赖声明(持久生效的关键,见 ADR 0019)。 */
432
- export function switchDependencySpecifier({ profileDir, nextSpecifier, keys = [LEGACY_DEPENDENCY_KEY], readJson = readJsonFile, writeJson = writeJsonFile } = {}) {
433
- const file = join(profileDir, "package.json");
434
- let doc;
435
- try {
436
- doc = readJson(file);
437
- } catch (e) {
438
- return { ok: false, error: `profile-package-unreadable: ${e?.message ?? e}` };
439
- }
440
- const deps = doc?.dependencies;
441
- if (!deps || typeof deps !== "object") return { ok: false, error: "profile-has-no-dependencies" };
442
- // 键可能是旧包名,也可能是带 scope 的新包名:改**实际存在的那个**,不新增第二个依赖。
443
- const key = dependencyKeyIn(doc, keys) || (keys.includes(LEGACY_DEPENDENCY_KEY) ? LEGACY_DEPENDENCY_KEY : keys[0]);
444
- const previous = typeof deps[key] === "string" ? deps[key] : "";
445
- if (previous === "") return { ok: false, error: "profile-dependency-missing" };
446
- deps[key] = nextSpecifier;
447
- try {
448
- writeJson(file, doc);
449
- } catch (e) {
450
- return { ok: false, error: `profile-package-unwritable: ${e?.message ?? e}` };
451
- }
452
- return { ok: true, previous, next: nextSpecifier, file, key };
453
- }
454
-
455
- function readJsonFile(file) {
456
- return JSON.parse(readFileSync(file, "utf8"));
457
- }
458
-
459
- function writeJsonFile(file, doc) {
460
- writeFileSync(file, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
461
- }
462
-
463
- // ── 重启 ──────────────────────────────────────────────────────────────────
464
-
465
- export function restartPlan({ form, pid = null, profile = "web", dshCommand = "dsh" } = {}) {
466
- switch (form) {
467
- case "web-systemd":
468
- return {
469
- mode: "supervised",
470
- steps: ["终止当前宿主进程", "由 systemd 的 Restart=always 用原参数拉起"],
471
- commands: pid ? [`kill -TERM ${pid}`] : ["systemctl --user restart dsh-web.service"],
472
- };
473
- case "web-foreground":
474
- return {
475
- mode: "respawn",
476
- steps: ["终止当前宿主进程", "由分离脚本重新拉起 dsh web"],
477
- commands: pid ? [`kill -TERM ${pid}`, `${dshCommand} web --profile ${profile}`] : [`${dshCommand} web --profile ${profile}`],
478
- };
479
- default:
480
- return {
481
- mode: "unsupported",
482
- steps: ["当前形态不支持自动重启(DSH Desktop / macOS 需要人工重启)"],
483
- commands: [],
484
- };
485
- }
486
- }
487
-
488
- /**
489
- * 识别本机可自动重启的形态。
490
- * **默认读真实 `process.env`**——生产调用漏传 env 会让前台 web 永远探测不到(评审 R12)。
491
- */
492
- export function detectRestartForm({ env = process.env, platform = process.platform, hasSystemdUnit = () => false } = {}) {
493
- if (platform === "win32" || platform === "darwin") return "unsupported";
494
- if (hasSystemdUnit()) return "web-systemd";
495
- if (env?.DSH_PROFILE || env?.DSH_WEB_FOREGROUND === "1") return "web-foreground";
496
- return "unsupported";
497
- }
498
-
499
- export function buildRestartScript({ plan, pid = null, delayMs = 3000 } = {}) {
500
- const seconds = Math.max(0, Math.round(delayMs / 1000));
501
- if (plan?.mode === "supervised") {
502
- return pid ? `sleep ${seconds}; kill -TERM ${pid}` : "";
503
- }
504
- if (plan?.mode === "respawn") {
505
- const respawn = plan.commands?.[plan.commands.length - 1] ?? "";
506
- if (!respawn) return "";
507
- const kill = pid ? `kill -TERM ${pid}; sleep 2; ` : "";
508
- return `sleep ${seconds}; ${kill}nohup ${respawn} >/dev/null 2>&1 &`;
509
- }
510
- return "";
511
- }
512
-
513
- /** 执行重启:分离一个 shell 去跑脚本。 */
514
- export function runRestart({ plan, pid = null, delayMs = 3000, spawnImpl = spawn, log = () => {} } = {}) {
515
- const script = buildRestartScript({ plan, pid, delayMs });
516
- if (script === "") return { ok: false, error: "restart-unsupported" };
517
- try {
518
- const child = spawnImpl("bash", ["-lc", script], { detached: true, stdio: "ignore" });
519
- child.unref?.();
520
- log(`mobile-remote: 已安排重启(${plan.mode}):${script}`);
521
- return { ok: true, script, pid: child.pid ?? null };
522
- } catch (e) {
523
- return { ok: false, error: `restart-spawn-failed: ${e?.message ?? e}` };
524
- }
525
- }
526
-
527
- /** 监督器冻结目录:启动前把自身代码复制到这里,不依赖之后可能被 pnpm 清理的模块目录。 */
528
- export function supervisorRoot(home, id) {
529
- // Only the server-generated UUID is a filesystem component, never the client request ID.
530
- serverOperationPath(home, id);
531
- return join(home, ".dsh", "mobile-remote", "supervisor", id);
532
- }
533
- export function freezeSupervisor({ pluginRoot, home, id, destDir = supervisorRoot(home, id), copyDir = defaultCopyDir } = {}) {
534
- const expected = supervisorRoot(home, id);
535
- if (destDir !== expected) return { ok: false, error: "supervisor-path-outside-root" };
536
- if (existsSync(destDir)) return { ok: false, error: "supervisor-already-frozen" };
537
- const src = join(pluginRoot, "lib");
538
- if (!existsSync(src)) return { ok: false, error: "plugin-lib-missing" };
539
- mkdirSync(destDir, { recursive: true });
540
- const result = copyDir(src, join(destDir, "lib"));
541
- if (!result?.ok) return { ok: false, error: "freeze-failed" };
542
- writeFileSync(join(destDir, "package.json"), '{"type":"module"}\n');
543
- const entry = join(destDir, "lib", "update-supervisor.js");
544
- return existsSync(entry) ? { ok: true, entry } : { ok: false, error: "freeze-missing-entry" };
545
- }
546
-
547
- /**
548
- * 启动监督器并**等它就绪回执**(F2)。
549
- * systemd 托管时必须经 transient unit 逃逸出宿主 cgroup,否则停宿主会把它一起杀掉;
550
- * 逃逸或就绪失败一律 fail-closed(调用方在**任何破坏性改动之前**据此中止)。
551
- */
552
- export function spawnSupervisorArmed({
553
- frozenEntry,
554
- stateFile,
555
- readyFile,
556
- opId,
557
- generation,
558
- needsEscape = false,
559
- unitName = "",
560
- nodePath = process.execPath,
561
- timeoutMs = 15_000,
562
- spawnImpl = spawn,
563
- runImpl = null,
564
- sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
565
- now = () => Date.now(),
566
- log = () => {},
567
- } = {}) {
568
- const args = [frozenEntry, "--state", stateFile, "--op", opId, "--generation", generation, "--arm", "--ready", readyFile];
569
- let mode = "detached";
570
- if (needsEscape) {
571
- const unit = unitName || `dsh-plugin-supervisor-${String(opId).slice(0, 8)}`;
572
- const run = runImpl ?? ((cmd, argv) => spawnSync(cmd, argv, { encoding: "utf8", env: { ...process.env, XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid()}` } }));
573
- const r = run("systemd-run", ["--user", "--unit", unit, "--collect", "--quiet", "--", nodePath, ...args]);
574
- if (r?.error) return { ok: false, error: `escape-failed: ${r.error.message ?? r.error}` };
575
- if (typeof r?.status === "number" && r.status !== 0) {
576
- return { ok: false, error: `escape-failed: exit ${r.status} ${String(r.stderr ?? "").trim()}` };
577
- }
578
- mode = "systemd-run";
579
- } else {
580
- try {
581
- const child = spawnImpl(nodePath, args, { detached: true, stdio: "ignore" });
582
- child.unref?.();
583
- } catch (e) {
584
- return { ok: false, error: `supervisor-spawn-failed: ${e?.message ?? e}` };
585
- }
586
- }
587
- return { ok: true, mode, readyFile, args };
588
- }
589
-
590
- /** 等监督器写出就绪回执;超时即失败(调用方 fail-closed)。 */
591
- export async function waitForSupervisorReady({
592
- readyFile,
593
- opId,
594
- generation,
595
- timeoutMs = 15_000,
596
- pollMs = 200,
597
- readFile = (f) => readFileSync(f, "utf8"),
598
- sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
599
- now = () => Date.now(),
600
- } = {}) {
601
- const deadline = now() + timeoutMs;
602
- while (now() < deadline) {
603
- try {
604
- const doc = JSON.parse(readFile(readyFile));
605
- if (doc && Number.isFinite(doc.pid) && (!opId || doc.opId === opId) && (!generation || doc.generation === generation)) return { ok: true, ready: doc };
606
- } catch {
607
- // 还没写好
608
- }
609
- await sleep(pollMs);
610
- }
611
- return { ok: false, error: "supervisor-not-ready" };
612
- }
613
-
614
- // ── 事务推进 ──────────────────────────────────────────────────────────────
615
-
616
- /**
617
- * 推进一次自更新事务。
618
- *
619
- * 顺序即安全性:**先下载校验 → 再解包复核内容标识 → 快照当前内容 → 最后才动 profile**。
620
- * 任何一步失败都可以直接放弃,此时还没有发生需要重启的变更。
621
- *
622
- * 返回 `{ok, step, error?, transaction}`;成功后事务处于 `restart-pending`,
623
- * 由调用方启动外部监督器接管(重启 + 健康确认 + 失败回滚)。
624
- */
625
- export async function runSelfUpdate({
626
- home, plugin, release = null, manifestUrl = "", profileDir, restartForm = "unsupported", hostPid = process.pid,
627
- port = 3080, basePath = "/m", token = "", profile = "web", dshCommand = "dsh", installedRoot = "", pluginRoot = "",
628
- needsEscape = false, supervisorTimeoutMs = 15_000, freezeImpl = freezeSupervisor, spawnArmedImpl = spawnSupervisorArmed,
629
- awaitReadyImpl = waitForSupervisorReady, opId = newOperationId(), fetchImpl = fetch, auth = {}, runTar = defaultRunTar,
630
- runInstall = defaultRunInstall, onStep = () => {}, log = () => {}, now = () => Date.now(),
631
- lease: admittedLease, initialTransaction, stateFile: operationStateFile, currentFile = transactionFile(home),
632
- lockPath = updateLockPath(home),
633
- } = {}) {
634
- if (!validOperationId(opId)) return { ok: false, step: "input", error: "invalid-operation-id" };
635
- if (!profileDir) return { ok: false, step: "profile", error: "profile-not-found" };
636
- const grant = acquireLock(lockPath, { opId, lease: admittedLease, stateFile: currentFile });
637
- if (!grant.ok) return { ok: false, step: "lock", error: grant.error, holder: grant.holder };
638
- const lease = grant.lease, id = lease.generation;
639
- const stateFile = operationStateFile || serverOperationPath(home, id);
640
- let tx = initialTransaction ?? { opId, lease, id, phase: "staging", startedAt: new Date(now()).toISOString(),
641
- profileDir, installedRoot, manifestUrl, release, restartForm, hostPid, port, basePath, token, profile, dshCommand,
642
- lockPath, currentFile, targetVersion: plugin?.version ?? "", targetContentSha256: plugin?.contentSha256 ?? "", attempts: 0 };
643
- let destructive = false, step = "supervisor";
644
- const save = (patch, expectedPhases) => { tx = writeOwnedTransaction(stateFile, { ...tx, ...patch }, { lockPath, lease, currentFile, expectedPhases }); return tx; };
645
- const fail = error => {
646
- // 一旦可能动过 profile/安装字节,只有已 armed 的监督器能结束这个事务(C5)。
647
- if (destructive) {
648
- save({ phase: "rolling-back", rolledBack: true, result: { ok: false, reason: error }, recoveryCause: error }, ["installing"]);
649
- return { ok: false, step, error, transaction: tx };
650
- }
651
- save({ phase: "failed", result: { ok: false, reason: error } }, ["staging"]);
652
- releaseLock(lockPath, lease, { stateFile });
653
- return { ok: false, step, error, transaction: tx };
654
- };
655
- try {
656
- save({ release: release || tx.release, lockPath, currentFile }, ["staging"]);
657
- const frozenDir = supervisorRoot(home, id);
658
- const frozen = freezeImpl({ pluginRoot, home, id, destDir: frozenDir });
659
- if (!frozen.ok) return fail(frozen.error);
660
- const readyFile = join(frozenDir, "ready.json");
661
- const armed = spawnArmedImpl({ frozenEntry: frozen.entry, stateFile, readyFile, opId, generation: lease.generation,
662
- needsEscape, unitName: `dsh-plugin-supervisor-${id}`, log });
663
- if (!armed.ok) return fail(armed.error);
664
- const ready = await awaitReadyImpl({ readyFile, opId, generation: lease.generation, timeoutMs: supervisorTimeoutMs });
665
- if (!ready.ok) return fail(ready.error);
666
- assertUpdateLease(lockPath, lease);
667
-
668
- const work = join(workRoot(home), id); mkdirSync(work, { recursive: true });
669
- const tgzPath = join(work, "plugin.tgz"); step = UPDATE_STEP.DOWNLOAD; onStep(step);
670
- const downloaded = await downloadVerified({ url: resolvePluginUrl(manifestUrl, plugin), destPath: tgzPath,
671
- sha256: plugin.sha256, size: plugin.size ?? null, fetchImpl, auth });
672
- assertUpdateLease(lockPath, lease);
673
- if (!downloaded.ok) return fail(downloaded.error);
674
- onStep(UPDATE_STEP.VERIFY); step = UPDATE_STEP.EXTRACT; onStep(step);
675
- const staged = await extractVerified({ tgzPath, workDir: join(work, "extract"), contentSha256: plugin.contentSha256, runTar });
676
- assertUpdateLease(lockPath, lease);
677
- if (!staged.ok) return fail(staged.error);
678
- const destDir = artifactDir(artifactRoot(home), staged.digest);
679
- if (existsSync(destDir)) {
680
- if (pluginContentDigest(destDir) !== staged.digest) return fail("artifact-integrity-mismatch");
681
- } else { mkdirSync(dirname(destDir), { recursive: true }); renameSync(staged.packageDir, destDir); }
682
-
683
- step = UPDATE_STEP.SNAPSHOT; onStep(step);
684
- if (!installedRoot || !existsSync(installedRoot)) return fail("installed-root-missing");
685
- const previousContent = pluginContentDigest(installedRoot);
686
- const previousSnapshot = snapshotDir(snapshotRoot(home), previousContent);
687
- if (!snapshotReady(previousSnapshot)) {
688
- if (existsSync(previousSnapshot)) return fail("snapshot-incomplete-or-corrupt");
689
- const snap = snapshotInstalled({ installedRoot, destDir: previousSnapshot });
690
- if (!snap.ok) return fail(snap.error);
691
- }
692
- if (pluginContentDigest(previousSnapshot) !== previousContent) return fail("snapshot-content-mismatch");
693
- const packageFile = readFileSync(join(profileDir, "package.json"), "utf8");
694
- const pnpmLock = existsSync(join(profileDir, "pnpm-lock.yaml")) ? readFileSync(join(profileDir, "pnpm-lock.yaml"), "utf8") : null;
695
- // 依赖键自描述(npm 正式渠道下包名带 scope,键随之改变)
696
- const dependencyKeys = dependencyKeysFor(pluginRoot);
697
- const dependencyKey = dependencyKeyIn(JSON.parse(packageFile), dependencyKeys) || LEGACY_DEPENDENCY_KEY;
698
- const oldSpecifier = JSON.parse(packageFile).dependencies?.[dependencyKey];
699
- // Durable recovery metadata precedes the first destructive write, so crashes also recover.
700
- save({ phase: "installing", artifactDir: destDir, targetContentSha256: staged.digest, previousContentSha256: previousContent,
701
- previousSnapshotDir: previousSnapshot, previousSpecifier: oldSpecifier, dependencyKey,
702
- profileSnapshot: { packageJson: packageFile, pnpmLock, dependencyKey },
703
- release: tx.release ? { ...tx.release, plugin: { ...tx.release.plugin, contentSha256: staged.digest } } : null }, ["staging"]);
704
- destructive = true; step = UPDATE_STEP.SWITCH; onStep(step);
705
- const switched = switchOwnedDependency({ profileDir, nextSpecifier: `file:${destDir}`, key: dependencyKey, lockPath, lease });
706
- if (!switched.ok) return fail(switched.error);
707
- step = UPDATE_STEP.INSTALL; onStep(step); assertUpdateLease(lockPath, lease);
708
- const installed = await runInstall(profileDir);
709
- assertUpdateLease(lockPath, lease);
710
- if (!installed?.ok) return fail(installed?.error ?? "install-failed");
711
- step = UPDATE_STEP.RESTART; onStep(step);
712
- save({ phase: "restart-pending", installedAt: new Date(now()).toISOString(), result: null }, ["installing"]);
713
- log(`mobile-remote: 已落位,监督器将确认内容 ${staged.digest.slice(0,12)}`);
714
- return { ok: true, step, transaction: tx };
715
- } catch(e) {
716
- if (e.message === "lease-lost") return { ok: false, step, error: "lease-lost" };
717
- try { return fail(e.message); } catch { return { ok: false, step, error: e.message }; }
718
- }
719
- }
720
-
721
- /**
722
- * 插件侧对事务的**只读报告/收敛**(F6):不再由新版插件自己回滚(那是监督器的职责),
723
- * 但**任何非终态**(staging / installing / restart-pending)在持有者已死且超时后
724
- * 都必须收敛为 FAILED,否则后续更新会永久 busy。
725
- */
726
- export function reportPendingUpdate({ stateFile, lockPath = "", localContentSha256 = "", supervisorAlive = false, timeoutMs = 10 * 60 * 1000, now = () => Date.now() } = {}) {
727
- const tx = readTransaction(stateFile);
728
- if (!tx) return { action: "none", reason: "no-transaction", transaction: null };
729
- if (isTerminalPhase(tx.phase)) return { action: "none", reason: `phase-${tx.phase}`, transaction: tx };
730
- if (tx.phase === TRANSACTION_PHASE.RESTART_PENDING) {
731
- const target = String(tx.targetContentSha256 ?? "").toLowerCase();
732
- if (target !== "" && String(localContentSha256 ?? "").toLowerCase() === target) {
733
- return { action: "matched", reason: "loaded-target-content", transaction: tx };
734
- }
735
- }
736
- if (!lockPath) return { action: "waiting", reason: "no-lock-path", transaction: tx };
737
- const converged = convergeStaleTransaction({ stateFile, lockPath, now, staleMs: timeoutMs });
738
- if (converged.action === "converged") {
739
- return { action: "expired", reason: "holder-gone", transaction: converged.transaction };
740
- }
741
- if (converged.action === "active") {
742
- return { action: "waiting", reason: supervisorAlive ? "supervisor-running" : "update-in-progress", transaction: tx };
743
- }
744
- return { action: "waiting", reason: `phase-${tx.phase}`, transaction: tx };
745
- }
746
-
747
- /** 事务是否已到终态(供 App 判断"结果待确认")。 */
748
- export function isTerminalPhase(phase) {
749
- return phase === TRANSACTION_PHASE.SETTLED || phase === TRANSACTION_PHASE.ROLLED_BACK || phase === TRANSACTION_PHASE.FAILED;
750
- }
751
-
752
- /** 回滚中间态:既不是"成功"也不是"终态"(App 不得据此推进,F7)。 */
753
- export function isRollingBack(phase) {
754
- return phase === TRANSACTION_PHASE.ROLLING_BACK;
755
- }
756
-
757
- /** 快照/产物目录是否可用(诊断用)。 */
758
- export { dirReady };