/ crates / distrox-cli / src / systemd.rs
systemd.rs
  1  #[derive(Debug)]
  2  pub struct ProcessState {
  3      pub span: tracing::Span,
  4  }
  5  
  6  impl ProcessState {
  7      pub fn set_starting(&self) {
  8          tracing::debug!(parent: &self.span, status = "starting", "Setting service status");
  9          if let Err(error) = notify(&[NotifyState::Status("starting")]) {
 10              tracing::error!(parent: &self.span, ?error, "Failed to notify systemd of state change");
 11          } else {
 12              tracing::info!(
 13                  parent: &self.span,
 14                  status = "starting",
 15                  "Successfully notified systemd of service status"
 16              );
 17          }
 18      }
 19  
 20      pub fn set_running(&self) {
 21          tracing::debug!(parent: &self.span, status = "ready", "Setting service status");
 22          if let Err(error) = notify(&[NotifyState::Ready]) {
 23              tracing::error!(parent: &self.span, ?error, "Failed to notify systemd of state change");
 24          } else {
 25              tracing::info!(parent: &self.span,
 26                  status = "ready",
 27                  "Successfully notified systemd of service status"
 28              );
 29          }
 30      }
 31  
 32      pub fn set_failed(&self) {
 33          tracing::debug!(parent: &self.span, status = "failed,stopping", "Setting service status");
 34          if let Err(error) = notify(&[NotifyState::Status("failed"), NotifyState::Stopping]) {
 35              tracing::error!(parent: &self.span, ?error, "Failed to notify systemd of state change");
 36          } else {
 37              tracing::info!(
 38                  parent: &self.span,
 39                  status = "failed,stopping",
 40                  "Successfully notified systemd of service status"
 41              );
 42          }
 43      }
 44  
 45      pub fn set_cancelled(&self) {
 46          tracing::debug!(parent: &self.span, status = "ECANCELED", "Setting service status");
 47          // verified highly scientificly by looking up https://docs.rs/nix/latest/nix/type.Error.html#variant.ECANCELED
 48          let ecanceled = 125;
 49  
 50          if let Err(error) = notify(&[NotifyState::Errno(ecanceled)]) {
 51              tracing::error!(parent: &self.span, ?error, "Failed to notify systemd of state change");
 52          } else {
 53              tracing::info!(
 54                  parent: &self.span,
 55                  status = "ECANCELED",
 56                  "Successfully notified systemd of service status"
 57              );
 58          }
 59      }
 60  
 61      pub fn set_finished(&self) {
 62          tracing::debug!(parent: &self.span, status = "stopping", "Setting service status");
 63          if let Err(error) = notify(&[NotifyState::Stopping]) {
 64              tracing::error!(parent: &self.span, ?error, "Failed to notify systemd of state change");
 65          } else {
 66              tracing::info!(
 67                  parent: &self.span,
 68                  status = "stopping",
 69                  "Successfully notified systemd of service status"
 70              );
 71          }
 72      }
 73  }
 74  
 75  /// Daemon notification for the service manager.
 76  #[derive(Clone, Debug)]
 77  #[allow(dead_code)] // TODO: Delete unused variants?
 78  enum NotifyState<'a> {
 79      /// Service startup is finished.
 80      Ready,
 81  
 82      /// Service is reloading its configuration.
 83      ///
 84      /// On systemd v253 and newer, this message MUST be followed by a
 85      /// [`NotifyState::MonotonicUsec`] notification, or the reload will fail
 86      /// and the service will be terminated.
 87      Reloading,
 88  
 89      /// Service is stopping.
 90      Stopping,
 91  
 92      /// Free-form status message for the service manager.
 93      Status(&'a str),
 94  
 95      /// Service has failed with an `errno`-style error code, e.g. `2` for `ENOENT`.
 96      Errno(u32),
 97  
 98      /// Service has failed with a D-Bus-style error code, e.g. `org.freedesktop.DBus.Error.TimedOut`.
 99      BusError(&'a str),
100  
101      /// Main process ID (PID) of the service, in case it wasn't started directly by the service manager.
102      MainPid(u32),
103  
104      /// Tells the service manager to update the watchdog timestamp.
105      Watchdog,
106  
107      /// Tells the service manager to trigger a watchdog failure.
108      WatchdogTrigger,
109  
110      /// Resets the configured watchdog value.
111      WatchdogUsec(u32),
112  
113      /// Tells the service manager to extend the service timeout.
114      ExtendTimeoutUsec(u32),
115  
116      /// Tells the service manager to store attached file descriptors.
117      FdStore,
118  
119      /// Tells the service manager to remove stored file descriptors.
120      FdStoreRemove,
121  
122      /// Tells the service manager to use this name for the attached file descriptor.
123      FdName(&'a str),
124  
125      /// Notify systemd of the current monotonic time in microseconds.
126      /// You can construct this value by calling [`NotifyState::monotonic_usec_now()`].
127      MonotonicUsec(i128),
128  
129      /// Custom state.
130      Custom(&'a str),
131  }
132  
133  impl std::fmt::Display for NotifyState<'_> {
134      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135          match self {
136              NotifyState::Ready => write!(f, "READY=1"),
137              NotifyState::Reloading => write!(f, "RELOADING=1"),
138              NotifyState::Stopping => write!(f, "STOPPING=1"),
139              NotifyState::Status(msg) => write!(f, "STATUS={msg}"),
140              NotifyState::Errno(err) => write!(f, "ERRNO={err}"),
141              NotifyState::BusError(addr) => write!(f, "BUSERROR={addr}"),
142              NotifyState::MainPid(pid) => write!(f, "MAINPID={pid}"),
143              NotifyState::Watchdog => write!(f, "WATCHDOG=1"),
144              NotifyState::WatchdogTrigger => write!(f, "WATCHDOG=trigger"),
145              NotifyState::WatchdogUsec(usec) => write!(f, "WATCHDOG_USEC={usec}"),
146              NotifyState::ExtendTimeoutUsec(usec) => write!(f, "EXTEND_TIMEOUT_USEC={usec}"),
147              NotifyState::FdStore => write!(f, "FDSTORE=1"),
148              NotifyState::FdStoreRemove => write!(f, "FDSTOREREMOVE=1"),
149              NotifyState::FdName(name) => write!(f, "FDNAME={name}"),
150              NotifyState::MonotonicUsec(usec) => write!(f, "MONOTONIC_USEC={usec}"),
151              NotifyState::Custom(state) => write!(f, "{state}"),
152          }
153      }
154  }
155  
156  fn connect_notify_socket() -> std::io::Result<Option<std::os::unix::net::UnixDatagram>> {
157      let Some(socket_path) = std::env::var_os("NOTIFY_SOCKET") else {
158          return Ok(None);
159      };
160  
161      let sock = std::os::unix::net::UnixDatagram::unbound()?;
162  
163      sock.connect(socket_path)?;
164  
165      Ok(Some(sock))
166  }
167  
168  fn notify(state: &[NotifyState]) -> std::io::Result<()> {
169      use std::fmt::Write;
170  
171      let mut msg = String::new();
172  
173      let Some(sock) = connect_notify_socket()? else {
174          return Ok(());
175      };
176  
177      for s in state {
178          let _ = writeln!(msg, "{s}");
179      }
180  
181      let len = sock.send(msg.as_bytes())?;
182  
183      if len != msg.len() {
184          Err(std::io::Error::new(
185              std::io::ErrorKind::WriteZero,
186              "incomplete write",
187          ))
188      } else {
189          Ok(())
190      }
191  }