dashboardSupervisor.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  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. it('does not launch a competing process while a system service is being restarted', async () => {
  48. const f = supervisor()
  49. const resume = f.instance.suspendChecks()
  50. await f.instance.checkNow()
  51. expect(f.launches).toEqual([])
  52. resume(); resume()
  53. await f.instance.checkNow()
  54. expect(f.launches).toHaveLength(1)
  55. })
  56. it('also suspends a check that was already waiting for its listener probe', async () => {
  57. let probeDone!: (running: boolean) => void
  58. const launch = vi.fn()
  59. const instance = new DashboardSupervisor({ launch, probe: () => new Promise(done => { probeDone = done }) })
  60. const pending = instance.checkNow()
  61. const resume = instance.suspendChecks()
  62. probeDone(false); await pending
  63. expect(launch).not.toHaveBeenCalled()
  64. resume()
  65. })
  66. })
  67. function managedSupervisor(initiallyRunning=false, readyTimeoutMs=1000) {
  68. let running=initiallyRunning
  69. const children:ChildProcess[]=[]
  70. const spawnManaged=vi.fn(()=>{
  71. const child=Object.assign(new EventEmitter(),{pid:10000+children.length,exitCode:null as number|null,signalCode:null as NodeJS.Signals|null,
  72. kill:vi.fn((signal:NodeJS.Signals)=>{running=false;child.signalCode=signal;child.emit('exit',null,signal);return true})}) as unknown as ChildProcess
  73. children.push(child)
  74. return child
  75. })
  76. const instance=new DashboardSupervisor({managed:true,spawnManaged,probe:async()=>running,readyTimeoutMs,log:()=>{}})
  77. return {instance,spawnManaged,children,listening:(value=true)=>{running=value}}
  78. }
  79. it('never adopts or restarts an externally started Dashboard',async()=>{
  80. const f=managedSupervisor(true)
  81. await f.instance.checkNow()
  82. expect(f.instance.canRestart).toBe(false)
  83. await expect(f.instance.restart()).rejects.toThrow('外部服务管理')
  84. expect(f.spawnManaged).not.toHaveBeenCalled()
  85. expect(supervisor(true).instance.canRestart).toBe(false)
  86. })
  87. it('restarts only its owned child, serializes requests and waits for the replacement listener',async()=>{
  88. const f=managedSupervisor()
  89. await f.instance.checkNow();f.listening()
  90. expect(f.instance.canRestart).toBe(true)
  91. let completed=false
  92. const pending=f.instance.restart().then(()=>{completed=true})
  93. await vi.waitFor(()=>expect(f.children).toHaveLength(2))
  94. expect(f.children[0]!.kill).toHaveBeenCalledWith('SIGTERM')
  95. expect(completed).toBe(false)
  96. await expect(f.instance.restart()).rejects.toThrow('正在重启')
  97. await f.instance.checkNow()
  98. expect(f.spawnManaged).toHaveBeenCalledTimes(2)
  99. f.listening();await pending
  100. expect(completed).toBe(true)
  101. expect(f.instance.canRestart).toBe(true)
  102. f.instance.stop();await f.instance.stopOwned()
  103. })
  104. it('reports startup failure and timeout instead of claiming a successful restart',async()=>{
  105. const failed=managedSupervisor()
  106. await failed.instance.checkNow()
  107. const pending=failed.instance.restart()
  108. const rejected=expect(pending).rejects.toThrow('启动失败')
  109. await vi.waitFor(()=>expect(failed.children).toHaveLength(2))
  110. failed.children[1]!.emit('error',new Error('spawn failed'))
  111. await rejected
  112. expect(failed.instance.canRestart).toBe(false)
  113. const timeout=managedSupervisor(false,10)
  114. await timeout.instance.checkNow()
  115. await expect(timeout.instance.restart()).rejects.toThrow('规定时间')
  116. timeout.instance.stop();await timeout.instance.stopOwned()
  117. })
  118. it('does not relaunch an owned Dashboard when shutdown starts during restart',async()=>{
  119. const f=managedSupervisor()
  120. await f.instance.checkNow()
  121. f.children[0]!.kill=vi.fn(()=>{f.instance.stop();f.children[0]!.signalCode='SIGTERM';f.children[0]!.emit('exit');return true})
  122. await expect(f.instance.restart()).rejects.toThrow('启动失败')
  123. expect(f.spawnManaged).toHaveBeenCalledTimes(1)
  124. expect(f.instance.canRestart).toBe(false)
  125. })