dashboardSupervisor.test.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { describe, expect, it, vi } from 'vitest'
  2. import { EventEmitter } from 'node:events'
  3. import type { ChildProcess } from 'node:child_process'
  4. import { DashboardSupervisor } from '../../src/server/dashboardSupervisor.js'
  5. function supervisor(initiallyRunning = false) {
  6. let running = initiallyRunning
  7. const calls: string[][] = [], launches: string[][] = [], probes: string[] = []
  8. const instance = new DashboardSupervisor({
  9. run(args) {
  10. calls.push([...args])
  11. if (args[0] === 'dashboard' && args[1] === '--stop') running = false
  12. return ''
  13. },
  14. launch(args) { launches.push([...args]); running = true },
  15. probe: async (host, port) => { probes.push(host + ':' + port); return running },
  16. log: () => undefined,
  17. })
  18. return { instance, calls, launches, probes }
  19. }
  20. describe('DashboardSupervisor', () => {
  21. it('starts a missing service on loopback without reading or writing authentication config', async () => {
  22. const f = supervisor()
  23. await f.instance.checkNow()
  24. expect(f.calls).toEqual([])
  25. expect(f.launches).toEqual([['dashboard', '--host', '127.0.0.1']])
  26. await f.instance.checkNow()
  27. expect(f.launches).toHaveLength(1)
  28. })
  29. it('leaves an existing service and its authentication/binding untouched', async () => {
  30. const f = supervisor(true)
  31. await f.instance.checkNow(); await f.instance.checkNow()
  32. expect(f.calls).toEqual([])
  33. expect(f.launches).toEqual([])
  34. expect(new Set(f.probes)).toEqual(new Set(['127.0.0.1:9119']))
  35. })
  36. it('only restarts on an explicit request, without configuring any credentials', async () => {
  37. const f = supervisor(true)
  38. await f.instance.restart()
  39. expect(f.calls).toEqual([['dashboard', '--stop']])
  40. expect(f.launches).toEqual([['dashboard', '--host', '127.0.0.1']])
  41. })
  42. it('coalesces concurrent checks', async () => {
  43. const f = supervisor()
  44. await Promise.all([f.instance.checkNow(), f.instance.checkNow()])
  45. expect(f.launches).toHaveLength(1)
  46. })
  47. })
  48. function managedSupervisor(initiallyRunning=false, readyTimeoutMs=1000) {
  49. let running=initiallyRunning
  50. const children:ChildProcess[]=[]
  51. const spawnManaged=vi.fn(()=>{
  52. const child=Object.assign(new EventEmitter(),{pid:10000+children.length,exitCode:null as number|null,signalCode:null as NodeJS.Signals|null,
  53. kill:vi.fn((signal:NodeJS.Signals)=>{running=false;child.signalCode=signal;child.emit('exit',null,signal);return true})}) as unknown as ChildProcess
  54. children.push(child)
  55. return child
  56. })
  57. const instance=new DashboardSupervisor({managed:true,spawnManaged,probe:async()=>running,readyTimeoutMs,log:()=>{}})
  58. return {instance,spawnManaged,children,listening:(value=true)=>{running=value}}
  59. }
  60. it('never adopts or restarts an externally started Dashboard',async()=>{
  61. const f=managedSupervisor(true)
  62. await f.instance.checkNow()
  63. expect(f.instance.canRestart).toBe(false)
  64. await expect(f.instance.restart()).rejects.toThrow('外部服务管理')
  65. expect(f.spawnManaged).not.toHaveBeenCalled()
  66. expect(supervisor(true).instance.canRestart).toBe(false)
  67. })
  68. it('restarts only its owned child, serializes requests and waits for the replacement listener',async()=>{
  69. const f=managedSupervisor()
  70. await f.instance.checkNow();f.listening()
  71. expect(f.instance.canRestart).toBe(true)
  72. let completed=false
  73. const pending=f.instance.restart().then(()=>{completed=true})
  74. await vi.waitFor(()=>expect(f.children).toHaveLength(2))
  75. expect(f.children[0]!.kill).toHaveBeenCalledWith('SIGTERM')
  76. expect(completed).toBe(false)
  77. await expect(f.instance.restart()).rejects.toThrow('正在重启')
  78. await f.instance.checkNow()
  79. expect(f.spawnManaged).toHaveBeenCalledTimes(2)
  80. f.listening();await pending
  81. expect(completed).toBe(true)
  82. expect(f.instance.canRestart).toBe(true)
  83. f.instance.stop();await f.instance.stopOwned()
  84. })
  85. it('reports startup failure and timeout instead of claiming a successful restart',async()=>{
  86. const failed=managedSupervisor()
  87. await failed.instance.checkNow()
  88. const pending=failed.instance.restart()
  89. const rejected=expect(pending).rejects.toThrow('启动失败')
  90. await vi.waitFor(()=>expect(failed.children).toHaveLength(2))
  91. failed.children[1]!.emit('error',new Error('spawn failed'))
  92. await rejected
  93. expect(failed.instance.canRestart).toBe(false)
  94. const timeout=managedSupervisor(false,10)
  95. await timeout.instance.checkNow()
  96. await expect(timeout.instance.restart()).rejects.toThrow('规定时间')
  97. timeout.instance.stop();await timeout.instance.stopOwned()
  98. })
  99. it('does not relaunch an owned Dashboard when shutdown starts during restart',async()=>{
  100. const f=managedSupervisor()
  101. await f.instance.checkNow()
  102. f.children[0]!.kill=vi.fn(()=>{f.instance.stop();f.children[0]!.signalCode='SIGTERM';f.children[0]!.emit('exit');return true})
  103. await expect(f.instance.restart()).rejects.toThrow('启动失败')
  104. expect(f.spawnManaged).toHaveBeenCalledTimes(1)
  105. expect(f.instance.canRestart).toBe(false)
  106. })