verify-lifecycle.mjs 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  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. },
  36. stdio: ['ignore', 'pipe', 'pipe'],
  37. })
  38. let stderr = ''
  39. child.stderr.on('data', chunk => { stderr += chunk.toString() })
  40. try {
  41. await waitForHealth()
  42. const webPids = listenerPids(port)
  43. if (webPids.size !== 1) throw new Error(`Expected one ${port} listener, received ${[...webPids].join(', ') || 'none'}`)
  44. } finally {
  45. child.kill('SIGTERM')
  46. await Promise.race([
  47. new Promise(resolve => child.once('exit', resolve)),
  48. delay(5_000).then(() => child.kill('SIGKILL')),
  49. ])
  50. }
  51. const gatewayAfter = listenerPids(9119)
  52. if ([...gatewayBefore].join(',') !== [...gatewayAfter].join(',')) {
  53. throw new Error(`9119 listener changed: before=${[...gatewayBefore]} after=${[...gatewayAfter]}`)
  54. }
  55. if (child.exitCode !== 0 && child.signalCode !== 'SIGTERM') {
  56. throw new Error(`Verification server failed: ${stderr.trim()}`)
  57. }
  58. process.stdout.write(`Lifecycle verified: one ${port} listener; 9119 unchanged (${[...gatewayAfter].join(',') || 'no listener'}).\n`)