update-server.mjs 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /** Loopback-only update feeds and deterministic transport failures for Electron qualification. */
  2. import { createHash } from 'node:crypto'
  3. import { createServer } from 'node:http'
  4. import { once } from 'node:events'
  5. import { dump } from 'js-yaml'
  6. /**
  7. * Allocate a private listener with request barriers and no remote publication.
  8. * @returns {Promise<object>} Feed URL, scenario controls, observations, and awaited disposal.
  9. */
  10. export async function createUpdateServer() {
  11. const payload = Buffer.alloc(256 * 1024, 0x5a)
  12. const sha512 = createHash('sha512').update(payload).digest('base64')
  13. const requests = []
  14. const failures = []
  15. let mode = 'healthy'
  16. let version = '1.0.1-nightly.1'
  17. let policyMode = 'clear'
  18. let gate = Promise.withResolvers()
  19. let arrived = Promise.withResolvers()
  20. let origin
  21. const server = createServer((request, response) => {
  22. const path = new URL(request.url, origin).pathname
  23. requests.push(path)
  24. const selectedMode = mode
  25. const selectedGate = gate
  26. response.setHeader('Cache-Control', 'no-store')
  27. void (async () => {
  28. if (path === '/api/v0/check_client_update') {
  29. response.setHeader('Content-Type', 'application/json')
  30. if (policyMode === 'stall') return
  31. if (policyMode === 'failure') { response.writeHead(503).end('{"code":500}'); return }
  32. response.end(JSON.stringify(policyMode === 'force' ? {
  33. code: 40005, data: { show_content: { title: '需要更新', detail: '现有任务继续运行,请更新后继续操作。' },
  34. desktop_app_link: 'https://downloads.example.com/desktop' },
  35. } : { code: 0, data: { biz_code: 0, biz_data: null } }))
  36. return
  37. }
  38. if (path === '/nightly.yml') {
  39. if (selectedMode === 'feed-stall') return
  40. if (selectedMode === 'feed-404') { response.writeHead(404).end(); return }
  41. if (selectedMode === 'feed-408') { response.writeHead(408).end(); return }
  42. if (selectedMode === 'invalid-yaml') { response.end('files: ['); return }
  43. if (selectedMode === 'hold-check') { arrived.resolve(); await selectedGate.promise }
  44. response.setHeader('Content-Type', 'application/yaml')
  45. response.end(dump({ version, files: [{ url: `${origin}/payload.exe`, size: payload.length, sha512 }],
  46. path: 'payload.exe', sha512, releaseDate: '2026-09-10T00:00:00.000Z' }))
  47. return
  48. }
  49. if (path !== '/payload.exe' || selectedMode === 'download-404') { response.writeHead(404).end(); return }
  50. response.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': payload.length })
  51. if (selectedMode === 'download-stall') { response.write(payload.subarray(0, 1024)); return }
  52. if (selectedMode === 'corrupt') { response.end(Buffer.alloc(payload.length, 0x21)); return }
  53. if (selectedMode === 'disconnect') {
  54. response.write(payload.subarray(0, 1024), () => response.destroy())
  55. return
  56. }
  57. if (selectedMode === 'hold-download') {
  58. response.write(payload.subarray(0, 1024))
  59. arrived.resolve()
  60. await selectedGate.promise
  61. if (!response.destroyed) response.end(payload.subarray(1024))
  62. return
  63. }
  64. response.end(payload)
  65. })().catch(error => { failures.push(String(error)); response.destroy() })
  66. })
  67. const listening = once(server, 'listening')
  68. server.listen(0, '127.0.0.1')
  69. await listening
  70. origin = `http://127.0.0.1:${server.address().port}`
  71. return {
  72. url: `${origin}/`, payload, requests, failures,
  73. policy(next) { policyMode = next },
  74. select(nextMode, nextVersion = '1.0.1-nightly.1') {
  75. gate.resolve()
  76. gate = Promise.withResolvers()
  77. arrived = Promise.withResolvers()
  78. mode = nextMode
  79. version = nextVersion
  80. },
  81. arrived: () => arrived.promise,
  82. release: () => gate.resolve(),
  83. async close() {
  84. gate.resolve()
  85. const closed = new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
  86. server.closeAllConnections()
  87. await closed
  88. },
  89. }
  90. }