systemd.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /*
  2. Copyright 2025 Psiphon Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package udsipc
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "log/slog"
  19. "net"
  20. "os"
  21. "strconv"
  22. "sync"
  23. )
  24. // SystemdManager handles systemd detection and integration.
  25. type SystemdManager struct {
  26. listener net.Listener
  27. notifyConn net.Conn
  28. runtimeDir string
  29. stateDir string
  30. closeOnce sync.Once
  31. isSystemd bool
  32. }
  33. // NewSystemdManager creates a new systemd manager, setting up all systemd resources once.
  34. func NewSystemdManager() (*SystemdManager, error) {
  35. manager := &SystemdManager{
  36. runtimeDir: os.Getenv("RUNTIME_DIRECTORY"),
  37. stateDir: os.Getenv("STATE_DIRECTORY"),
  38. }
  39. listenFds := os.Getenv("LISTEN_FDS")
  40. notifySocket := os.Getenv("NOTIFY_SOCKET")
  41. manager.isSystemd = manager.runtimeDir != "" ||
  42. listenFds != "" ||
  43. notifySocket != ""
  44. if !manager.isSystemd {
  45. return manager, nil
  46. }
  47. // Set up socket activation listener if available.
  48. if listenFds != "" {
  49. listener, err := manager.setupSocketActivation(listenFds)
  50. if err != nil {
  51. return nil, fmt.Errorf("failed to setup socket activation: %w", err)
  52. }
  53. manager.listener = listener
  54. }
  55. // Set up notify connection if available.
  56. if notifySocket != "" {
  57. conn, err := manager.setupNotifyConnection(notifySocket)
  58. if err != nil {
  59. return nil, fmt.Errorf("failed to setup notify connection: %w", err)
  60. }
  61. manager.notifyConn = conn
  62. }
  63. return manager, nil
  64. }
  65. // setupSocketActivation configures the systemd-provided socket listener.
  66. func (s *SystemdManager) setupSocketActivation(listenFdsStr string) (net.Listener, error) {
  67. // Validate LISTEN_PID matches current process.
  68. if listenPidStr := os.Getenv("LISTEN_PID"); listenPidStr != "" {
  69. listenPid, err := strconv.Atoi(listenPidStr)
  70. if err != nil {
  71. return nil, fmt.Errorf("invalid LISTEN_PID: %w", err)
  72. }
  73. if listenPid != os.Getpid() {
  74. return nil, fmt.Errorf("LISTEN_PID %d does not match current PID %d", listenPid, os.Getpid())
  75. }
  76. }
  77. listenFds, err := strconv.Atoi(listenFdsStr)
  78. if err != nil {
  79. return nil, fmt.Errorf("invalid LISTEN_FDS: %w", err)
  80. }
  81. if listenFds != 1 {
  82. return nil, fmt.Errorf("expected 1 socket, got %d", listenFds)
  83. }
  84. // nolint: mnd // Systemd passes file descriptor numbers starting at 3.
  85. file := os.NewFile(uintptr(3), "systemd-socket")
  86. if file == nil {
  87. return nil, errors.New("failed to create file from systemd fd")
  88. }
  89. listener, err := net.FileListener(file)
  90. if err != nil {
  91. _ = file.Close()
  92. return nil, fmt.Errorf("failed to create listener from systemd fd: %w", err)
  93. }
  94. // Close the file (listener now owns the fd).
  95. _ = file.Close()
  96. // Clean up environment variables (so potential child processes don't inherit them).
  97. _ = os.Unsetenv("LISTEN_FDS")
  98. _ = os.Unsetenv("LISTEN_PID")
  99. return listener, nil
  100. }
  101. // setupNotifyConnection configures the systemd notify connection.
  102. func (s *SystemdManager) setupNotifyConnection(notifySocket string) (net.Conn, error) {
  103. conn, err := net.Dial("unixgram", notifySocket) // nolint: noctx
  104. if err != nil {
  105. return nil, fmt.Errorf("failed to connect to systemd notify socket: %w", err)
  106. }
  107. return conn, nil
  108. }
  109. // IsSystemd returns true if running under systemd.
  110. func (s *SystemdManager) IsSystemd() bool {
  111. return s.isSystemd
  112. }
  113. // GetRuntimeDir returns the systemd runtime directory (empty if not).
  114. func (s *SystemdManager) GetRuntimeDir() string {
  115. return s.runtimeDir
  116. }
  117. // GetStateDir returns the systemd state directory (empty if not).
  118. func (s *SystemdManager) GetStateDir() string {
  119. return s.stateDir
  120. }
  121. // GetSystemdListener returns the pre-configured systemd listener (nil if not available).
  122. func (s *SystemdManager) GetSystemdListener() net.Listener {
  123. return s.listener
  124. }
  125. // NotifyReady sends a ready notification to systemd (nil if not available).
  126. func (s *SystemdManager) NotifyReady() error {
  127. if s.notifyConn == nil {
  128. return nil
  129. }
  130. _, err := s.notifyConn.Write([]byte("READY=1"))
  131. if err != nil {
  132. return fmt.Errorf("failed to send ready notification: %w", err)
  133. }
  134. return nil
  135. }
  136. // NotifyStopping sends a stopping notification to systemd (nil if not available).
  137. func (s *SystemdManager) NotifyStopping() error {
  138. if s.notifyConn == nil {
  139. return nil
  140. }
  141. _, err := s.notifyConn.Write([]byte("STOPPING=1"))
  142. if err != nil {
  143. return fmt.Errorf("failed to send stopping notification: %w", err)
  144. }
  145. return nil
  146. }
  147. // NotifyStatus sends a status message to systemd (nil if not available).
  148. func (s *SystemdManager) NotifyStatus(status string) error {
  149. if s.notifyConn == nil {
  150. return nil
  151. }
  152. message := fmt.Sprintf("STATUS=%s", status)
  153. _, err := s.notifyConn.Write([]byte(message))
  154. if err != nil {
  155. return fmt.Errorf("failed to send status notification: %w", err)
  156. }
  157. return nil
  158. }
  159. // Close cleans up systemd resources and notifies systemd of intended shutdown. Subsequent calls return nil.
  160. func (s *SystemdManager) Close() error {
  161. var err error
  162. s.closeOnce.Do(func() {
  163. // If we aren't running under systemd, Close should just be a no-op with no error.
  164. if !s.isSystemd {
  165. return
  166. }
  167. if stopErr := s.NotifyStopping(); stopErr != nil {
  168. slog.LogAttrs(context.Background(), slog.LevelError, "failed to notify systemd stopping", slog.Any("error", stopErr))
  169. }
  170. if s.listener != nil {
  171. err = s.listener.Close()
  172. }
  173. if s.notifyConn != nil {
  174. err = errors.Join(err, s.notifyConn.Close())
  175. }
  176. })
  177. return err
  178. }