verify-lifecycle.mjs 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import { spawn, spawnSync } from 'node:child_process'
  2. import { setTimeout as delay } from 'node:timers/promises'
  3. const port = Number(process.env.HERMES_YAOYAO_VERIFY_PORT || 18800)
  4. const entry = new URL('../dist-server/server/index.js', import.meta.url)
  5. function listenerPids(targetPort) {
  6. const result = spawnSync('lsof', ['-nP', `-iTCP:${targetPort}`, '-sTCP:LISTEN', '-t'], {
  7. encoding: 'utf8',
  8. })
  9. return new Set(result.stdout.split(/\s+/).map(value => value.trim()).filter(Boolean))
  10. }
  11. async function waitForHealth() {
  12. const deadline = Date.now() + 15_000
  13. while (Date.now() < deadline) {
  14. try {
  15. const response = await fetch(`http://127.0.0.1:${port}/healthz`, {
  16. headers: { Host: `127.0.0.1:${port}` },
  17. })
  18. if (response.ok) return
  19. } catch {
  20. // Server may still be materializing the production bundle.
  21. }
  22. await delay(150)
  23. }
  24. throw new Error('Timed out waiting for the verification server')
  25. }
  26. const gatewayBefore = listenerPids(9119)
  27. const child = spawn(process.execPath, [entry.pathname], {
  28. cwd: new URL('..', import.meta.url),
  29. env: {
  30. ...process.env,
  31. NODE_ENV: 'production',
  32. HERMES_YAOYAO_HOST: '127.0.0.1',
  33. HERMES_YAOYAO_PORT: String(port),
  34. HERMES_YAOYAO_UPSTREAM: process.env.HERMES_YAOYAO_UPSTREAM || 'http://127.0.0.1:9119',
  35. HERMES_YAOYAO_SUPERVISE_DASHBOARD: '0',
  36. },
  37. stdio: ['ignore', 'pipe', 'pipe'],
  38. })
  39. let stderr = ''
  40. child.stderr.on('data', chunk => { stderr += chunk.toString() })
  41. try {
  42. await waitForHealth()
  43. const webPids = listenerPids(port)
  44. if (webPids.size !== 1) throw new Error(`Expected one ${port} listener, received ${[...webPids].join(', ') || 'none'}`)
  45. } finally {
  46. child.kill('SIGTERM')
  47. await Promise.race([
  48. new Promise(resolve => child.once('exit', resolve)),
  49. delay(5_000).then(() => child.kill('SIGKILL')),
  50. ])
  51. }
  52. const gatewayAfter = listenerPids(9119)
  53. if ([...gatewayBefore].join(',') !== [...gatewayAfter].join(',')) {
  54. throw new Error(`9119 listener changed: before=${[...gatewayBefore]} after=${[...gatewayAfter]}`)
  55. }
  56. if (child.exitCode !== 0 && child.signalCode !== 'SIGTERM') {
  57. throw new Error(`Verification server failed: ${stderr.trim()}`)
  58. }
  59. process.stdout.write(`Lifecycle verified: one ${port} listener; 9119 unchanged (${[...gatewayAfter].join(',') || 'no listener'}).\n`)