serve_windows.go 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. //go:build windows && !noserver
  2. package cmd
  3. import (
  4. "fmt"
  5. "sync"
  6. "golang.org/x/sys/windows/svc"
  7. "heckel.io/ntfy/v2/log"
  8. "heckel.io/ntfy/v2/server"
  9. )
  10. const serviceName = "ntfy"
  11. // sigHandlerConfigReload is a no-op on Windows since SIGHUP is not available.
  12. // Windows users can restart the service to reload configuration.
  13. func sigHandlerConfigReload(config string) {
  14. log.Debug("Config hot-reload via SIGHUP is not supported on Windows")
  15. // On Windows, we simply don't set up any signal handler for config reload.
  16. // Users must restart the service/process to reload configuration.
  17. }
  18. // runAsWindowsService runs the ntfy server as a Windows service
  19. func runAsWindowsService(conf *server.Config) error {
  20. return svc.Run(serviceName, &ntfyService{conf: conf})
  21. }
  22. // ntfyService implements the svc.Handler interface
  23. type ntfyService struct {
  24. conf *server.Config
  25. server *server.Server
  26. mu sync.Mutex
  27. }
  28. // Execute is the main entry point for the Windows service
  29. func (s *ntfyService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
  30. const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
  31. status <- svc.Status{State: svc.StartPending}
  32. // Create and start the server
  33. var err error
  34. s.mu.Lock()
  35. s.server, err = server.New(s.conf)
  36. s.mu.Unlock()
  37. if err != nil {
  38. log.Error("Failed to create server: %s", err.Error())
  39. return true, 1
  40. }
  41. // Start server in a goroutine
  42. serverErrChan := make(chan error, 1)
  43. go func() {
  44. serverErrChan <- s.server.Run()
  45. }()
  46. status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
  47. log.Info("Windows service started")
  48. for {
  49. select {
  50. case err := <-serverErrChan:
  51. if err != nil {
  52. log.Error("Server error: %s", err.Error())
  53. return true, 1
  54. }
  55. return false, 0
  56. case req := <-requests:
  57. switch req.Cmd {
  58. case svc.Interrogate:
  59. status <- req.CurrentStatus
  60. case svc.Stop, svc.Shutdown:
  61. log.Info("Windows service stopping...")
  62. status <- svc.Status{State: svc.StopPending}
  63. s.mu.Lock()
  64. if s.server != nil {
  65. s.server.Stop()
  66. }
  67. s.mu.Unlock()
  68. return false, 0
  69. default:
  70. log.Warn("Unexpected service control request: %d", req.Cmd)
  71. }
  72. }
  73. }
  74. }
  75. // maybeRunAsService checks if the process is running as a Windows service,
  76. // and if so, runs the server as a service. Returns true if it ran as a service.
  77. func maybeRunAsService(conf *server.Config) (bool, error) {
  78. isService, err := svc.IsWindowsService()
  79. if err != nil {
  80. return false, fmt.Errorf("failed to detect Windows service mode: %w", err)
  81. }
  82. if !isService {
  83. return false, nil
  84. }
  85. log.Info("Running as Windows service")
  86. if err := runAsWindowsService(conf); err != nil {
  87. return true, fmt.Errorf("failed to run as Windows service: %w", err)
  88. }
  89. return true, nil
  90. }