verify-lifecycle.mjs 2.6 KB

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