index.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { existsSync } from 'node:fs'
  2. import { resolve } from 'node:path'
  3. import send from 'koa-send'
  4. import serve from 'koa-static'
  5. import { createApplication, createNodeServer } from './app.js'
  6. import { loadServerConfig } from './config.js'
  7. import { DashboardSupervisor } from './dashboardSupervisor.js'
  8. import { isLocalAuthorizationTarget } from './loopbackAuthorization.js'
  9. import { acquireServiceInstance } from './serviceInstance.js'
  10. import { readReleaseManifest } from './releases.js'
  11. const config = loadServerConfig()
  12. const instance = acquireServiceInstance(config.home, readReleaseManifest(resolve(process.cwd(), 'release.json')).webVersion, process.env.HERMES_YAOYAO_DESKTOP === '1')
  13. process.once('exit', () => instance.release())
  14. const runtime = createApplication({
  15. config,
  16. })
  17. const dashboardSupervisor = config.superviseDashboard
  18. && isLocalAuthorizationTarget(config.upstream) && config.upstream.hostname === '127.0.0.1'
  19. && config.upstream.protocol === 'http:' && config.upstream.port === '9119'
  20. ? new DashboardSupervisor({ managed: process.env.HERMES_YAOYAO_DESKTOP === '1' })
  21. : undefined
  22. const nodeRuntime = createNodeServer(runtime)
  23. let closeFrontend = async (): Promise<void> => undefined
  24. runtime.app.use(instance.middleware(shutdown))
  25. if (runtime.config.production) {
  26. const dist = resolve(process.env.HERMES_YAOYAO_STATIC_DIR || resolve(process.cwd(), 'dist'))
  27. runtime.app.use(serve(dist, { index: 'index.html' }))
  28. runtime.app.use(async (ctx) => {
  29. if (ctx.method !== 'GET' || !ctx.accepts('html') || !existsSync(resolve(dist, 'index.html'))) {
  30. ctx.status = 404
  31. return
  32. }
  33. await send(ctx, 'index.html', { root: dist })
  34. })
  35. } else {
  36. const { createServer } = await import('vite')
  37. const vite = await createServer({
  38. server: {
  39. middlewareMode: true,
  40. ws: { server: nodeRuntime.server },
  41. },
  42. appType: 'spa',
  43. })
  44. closeFrontend = () => vite.close()
  45. runtime.app.use(async (ctx) => {
  46. const handled = await new Promise<boolean>((resolvePromise, reject) => {
  47. const finish = () => {
  48. cleanup()
  49. resolvePromise(true)
  50. }
  51. const cleanup = () => {
  52. ctx.res.off('finish', finish)
  53. ctx.res.off('close', finish)
  54. }
  55. ctx.res.once('finish', finish)
  56. ctx.res.once('close', finish)
  57. vite.middlewares(ctx.req, ctx.res, (error?: unknown) => {
  58. cleanup()
  59. if (error) reject(error)
  60. else resolvePromise(false)
  61. })
  62. })
  63. if (handled) ctx.respond = false
  64. else ctx.status = 404
  65. })
  66. }
  67. nodeRuntime.server.once('error', (error: NodeJS.ErrnoException) => {
  68. if (error.code === 'EADDRINUSE') {
  69. console.error(`夭夭 Web could not start: ${runtime.config.host}:${runtime.config.port} is already in use`)
  70. } else {
  71. console.error(`夭夭 Web could not start: ${error.message}`)
  72. }
  73. void shutdown().finally(() => { process.exitCode = 1 })
  74. })
  75. nodeRuntime.server.listen(runtime.config.port, runtime.config.host, () => {
  76. const protocol = runtime.config.tlsCert ? 'https' : 'http'
  77. console.log(`夭夭 Web listening on ${protocol}://${runtime.config.host}:${runtime.config.port}`)
  78. instance.publish(`${protocol}://${runtime.config.host}:${runtime.config.port}`)
  79. if (runtime.config.insecureLan) {
  80. console.warn('Warning: trusted-LAN HTTP mode is enabled; credentials are not encrypted in transit.')
  81. }
  82. dashboardSupervisor?.start()
  83. })
  84. let closing = false
  85. async function shutdown(): Promise<void> {
  86. if (closing) return
  87. closing = true
  88. try {
  89. dashboardSupervisor?.stop()
  90. await closeFrontend()
  91. await nodeRuntime.close()
  92. await dashboardSupervisor?.stopOwned()
  93. } finally {
  94. instance.release()
  95. process.exitCode = 0
  96. }
  97. }
  98. process.once('SIGINT', () => { void shutdown() })
  99. process.once('SIGTERM', () => { void shutdown() })