1use crate::registry;
2use crate::Value;
3use crate::{EResult, Error};
4use busrt::rpc::{self, RpcClient, RpcHandlers};
5#[cfg(all(feature = "openssl3", feature = "fips"))]
6use once_cell::sync::OnceCell;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::ffi::CString;
10use std::fmt;
11#[cfg(feature = "extended-value")]
12use std::path::Path;
13use std::sync::atomic;
14use std::sync::Arc;
15use std::time::Duration;
16
17pub const SERVICE_CONFIG_VERSION: u16 = 4;
18
19pub const SERVICE_PAYLOAD_PING: u8 = 0;
20pub const SERVICE_PAYLOAD_INITIAL: u8 = 1;
21
22#[cfg(all(feature = "openssl3", feature = "fips"))]
23#[allow(dead_code)]
24static FIPS_LOADED: OnceCell<()> = OnceCell::new();
25
26#[cfg(any(
27 feature = "openssl-vendored",
28 feature = "openssl-no-fips",
29 not(feature = "fips")
30))]
31pub fn enable_fips() -> EResult<()> {
32 Err(Error::failed(
33 "FIPS can not be enabled, consider using a native OS distribution",
34 ))
35}
36
37#[cfg(not(any(feature = "openssl-vendored", feature = "openssl-no-fips")))]
38#[cfg(feature = "fips")]
39pub fn enable_fips() -> EResult<()> {
40 #[cfg(feature = "openssl3")]
41 {
42 FIPS_LOADED
43 .set(())
44 .map_err(|_| Error::core("FIPS provided already loaded"))?;
45 std::mem::forget(openssl::provider::Provider::load(None, "fips")?);
46 }
47 #[cfg(not(feature = "openssl3"))]
48 openssl::fips::enable(true)?;
49 Ok(())
50}
51
52pub struct Registry {
53 id: String,
54 rpc: Arc<RpcClient>,
55}
56
57impl Registry {
58 #[inline]
59 pub async fn key_set<V>(&self, key: &str, value: V) -> EResult<Value>
60 where
61 V: Serialize,
62 {
63 registry::key_set(
64 ®istry::format_svc_data_subkey(&self.id),
65 key,
66 value,
67 &self.rpc,
68 )
69 .await
70 }
71 #[inline]
72 pub async fn key_get(&self, key: &str) -> EResult<Value> {
73 registry::key_get(®istry::format_svc_data_subkey(&self.id), key, &self.rpc).await
74 }
75 #[inline]
76 pub async fn key_userdata_get(&self, key: &str) -> EResult<Value> {
77 registry::key_get(registry::R_USER_DATA, key, &self.rpc).await
78 }
79 #[inline]
80 pub async fn key_increment(&self, key: &str) -> EResult<i64> {
81 registry::key_increment(®istry::format_svc_data_subkey(&self.id), key, &self.rpc).await
82 }
83
84 #[inline]
85 pub async fn key_decrement(&self, key: &str) -> EResult<i64> {
86 registry::key_decrement(®istry::format_svc_data_subkey(&self.id), key, &self.rpc).await
87 }
88 #[inline]
89 pub async fn key_get_recursive(&self, key: &str) -> EResult<Vec<(String, Value)>> {
90 registry::key_get_recursive(®istry::format_svc_data_subkey(&self.id), key, &self.rpc)
91 .await
92 }
93 #[inline]
94 pub async fn key_delete(&self, key: &str) -> EResult<Value> {
95 registry::key_delete(®istry::format_svc_data_subkey(&self.id), key, &self.rpc).await
96 }
97 #[inline]
98 pub async fn key_delete_recursive(&self, key: &str) -> EResult<Value> {
99 registry::key_delete_recursive(®istry::format_svc_data_subkey(&self.id), key, &self.rpc)
100 .await
101 }
102}
103
104#[inline]
105fn default_workers() -> u32 {
106 1
107}
108
109#[derive(Default, Clone, Debug, Serialize, Deserialize)]
110pub struct RealtimeConfig {
111 #[serde(default)]
112 pub priority: Option<i32>,
113 #[serde(default)]
114 pub cpu_ids: Vec<usize>,
115 #[serde(default)]
116 pub prealloc_heap: Option<usize>,
117}
118
119fn default_restart_delay() -> Duration {
120 Duration::from_secs(2)
121}
122
123#[derive(Debug, Serialize, Deserialize)]
125pub struct Initial {
126 #[serde(rename = "version")]
127 config_version: u16,
128 system_name: String,
129 id: String,
130 command: String,
131 #[serde(default)]
132 prepare_command: Option<String>,
133 data_path: String,
134 timeout: Timeout,
135 core: CoreInfo,
136 bus: BusConfig,
137 #[serde(default)]
138 realtime: RealtimeConfig,
139 #[serde(default)]
140 config: Option<Value>,
141 #[serde(default = "default_workers")]
142 workers: u32,
143 #[serde(default)]
144 user: Option<String>,
145 #[serde(default)]
146 react_to_fail: bool,
147 #[serde(
148 serialize_with = "crate::tools::serialize_atomic_bool",
149 deserialize_with = "crate::tools::deserialize_atomic_bool"
150 )]
151 fail_mode: atomic::AtomicBool,
152 #[serde(default)]
153 fips: bool,
154 #[serde(default)]
155 call_tracing: bool,
156 #[serde(
157 default = "default_restart_delay",
158 deserialize_with = "crate::tools::de_float_as_duration"
159 )]
160 restart_delay: Duration,
161}
162
163impl Initial {
164 #[allow(clippy::too_many_arguments)]
165 pub fn new(
166 id: &str,
167 system_name: &str,
168 command: &str,
169 prepare_command: Option<&str>,
170 data_path: &str,
171 timeout: &Timeout,
172 core_info: CoreInfo,
173 bus: BusConfig,
174 config: Option<&Value>,
175 workers: u32,
176 user: Option<&str>,
177 react_to_fail: bool,
178 fips: bool,
179 call_tracing: bool,
180 ) -> Self {
181 Self {
182 config_version: SERVICE_CONFIG_VERSION,
183 system_name: system_name.to_owned(),
184 id: id.to_owned(),
185 command: command.to_owned(),
186 prepare_command: prepare_command.map(ToOwned::to_owned),
187 data_path: data_path.to_owned(),
188 timeout: timeout.clone(),
189 core: core_info,
190 bus,
191 realtime: <_>::default(),
192 config: config.cloned(),
193 workers,
194 user: user.map(ToOwned::to_owned),
195 react_to_fail,
196 fail_mode: atomic::AtomicBool::new(false),
197 fips,
198 call_tracing,
199 restart_delay: default_restart_delay(),
200 }
201 }
202 pub fn with_realtime(mut self, realtime: RealtimeConfig) -> Self {
203 self.realtime = realtime;
204 self
205 }
206 #[inline]
207 pub fn init(&self) -> EResult<()> {
208 #[cfg(feature = "openssl-no-fips")]
209 if self.fips {
210 return Err(Error::not_implemented(
211 "no FIPS 140 support, disable FIPS or switch to native package",
212 ));
213 }
214 if self.fips {
215 enable_fips()?;
216 }
217 Ok(())
218 }
219 #[inline]
220 pub fn config_version(&self) -> u16 {
221 self.config_version
222 }
223 #[inline]
224 pub fn system_name(&self) -> &str {
225 &self.system_name
226 }
227 #[inline]
228 pub fn id(&self) -> &str {
229 &self.id
230 }
231 #[inline]
232 pub fn command(&self) -> &str {
233 &self.command
234 }
235 pub fn realtime(&self) -> &RealtimeConfig {
236 &self.realtime
237 }
238 #[inline]
239 pub fn prepare_command(&self) -> Option<&str> {
240 self.prepare_command.as_deref()
241 }
242 #[inline]
243 pub fn user(&self) -> Option<&str> {
244 self.user.as_deref()
245 }
246 pub fn set_user(&mut self, user: Option<&str>) {
247 self.user = user.map(ToOwned::to_owned);
248 }
249 pub fn set_id(&mut self, id: &str) {
250 id.clone_into(&mut self.id);
251 }
252 #[inline]
253 pub fn data_path(&self) -> Option<&str> {
254 if let Some(ref user) = self.user {
255 if user == "nobody" {
256 return None;
257 }
258 }
259 Some(&self.data_path)
260 }
261 #[inline]
262 pub fn planned_data_path(&self) -> &str {
263 &self.data_path
264 }
265 pub fn set_data_path(&mut self, path: &str) {
266 path.clone_into(&mut self.data_path);
267 }
268 #[inline]
269 pub fn timeout(&self) -> Duration {
270 self.timeout
271 .default
272 .map_or(crate::DEFAULT_TIMEOUT, Duration::from_secs_f64)
273 }
274 #[inline]
275 pub fn startup_timeout(&self) -> Duration {
276 self.timeout
277 .startup
278 .map_or_else(|| self.timeout(), Duration::from_secs_f64)
279 }
280 #[inline]
281 pub fn shutdown_timeout(&self) -> Duration {
282 self.timeout
283 .shutdown
284 .map_or_else(|| self.timeout(), Duration::from_secs_f64)
285 }
286 #[inline]
287 pub fn bus_timeout(&self) -> Duration {
288 self.bus
289 .timeout
290 .map_or_else(|| self.timeout(), Duration::from_secs_f64)
291 }
292 #[inline]
293 pub fn eva_build(&self) -> u64 {
294 self.core.build
295 }
296 #[inline]
297 pub fn eva_version(&self) -> &str {
298 &self.core.version
299 }
300 #[inline]
301 pub fn eapi_version(&self) -> u16 {
302 self.core.eapi_verion
303 }
304 #[inline]
305 pub fn eva_dir(&self) -> &str {
306 &self.core.path
307 }
308 #[inline]
309 pub fn eva_log_level(&self) -> u8 {
310 self.core.log_level
311 }
312 #[inline]
313 pub fn core_active(&self) -> bool {
314 self.core.active
315 }
316 #[inline]
317 pub fn call_tracing(&self) -> bool {
318 self.call_tracing
319 }
320 #[inline]
321 pub fn restart_delay(&self) -> Duration {
322 self.restart_delay
323 }
324 #[inline]
325 pub fn eva_log_level_filter(&self) -> log::LevelFilter {
326 match self.core.log_level {
327 crate::LOG_LEVEL_TRACE => log::LevelFilter::Trace,
328 crate::LOG_LEVEL_DEBUG => log::LevelFilter::Debug,
329 crate::LOG_LEVEL_WARN => log::LevelFilter::Warn,
330 crate::LOG_LEVEL_ERROR => log::LevelFilter::Error,
331 crate::LOG_LEVEL_OFF => log::LevelFilter::Off,
332 _ => log::LevelFilter::Info,
333 }
334 }
335 #[inline]
336 pub fn bus_config(&self) -> EResult<busrt::ipc::Config> {
337 if self.bus.tp == "native" {
338 Ok(busrt::ipc::Config::new(&self.bus.path, &self.id)
339 .buf_size(self.bus.buf_size)
340 .buf_ttl(Duration::from_micros(self.bus.buf_ttl))
341 .queue_size(self.bus.queue_size)
342 .timeout(self.bus_timeout()))
343 } else {
344 Err(Error::not_implemented(format!(
345 "bus type {} is not supported",
346 self.bus.tp
347 )))
348 }
349 }
350 #[inline]
351 pub fn bus_config_for_sub(&self, sub_id: &str) -> EResult<busrt::ipc::Config> {
352 if self.bus.tp == "native" {
353 Ok(
354 busrt::ipc::Config::new(&self.bus.path, &format!("{}::{}", self.id, sub_id))
355 .buf_size(self.bus.buf_size)
356 .buf_ttl(Duration::from_micros(self.bus.buf_ttl))
357 .queue_size(self.bus.queue_size)
358 .timeout(self.bus_timeout()),
359 )
360 } else {
361 Err(Error::not_implemented(format!(
362 "bus type {} is not supported",
363 self.bus.tp
364 )))
365 }
366 }
367 pub fn set_bus_path(&mut self, path: &str) {
368 path.clone_into(&mut self.bus.path);
369 }
370 #[inline]
371 pub fn bus_path(&self) -> &str {
372 &self.bus.path
373 }
374 #[inline]
375 pub fn config(&self) -> Option<&Value> {
376 self.config.as_ref()
377 }
378 #[cfg(feature = "extended-value")]
379 #[inline]
380 pub async fn extend_config(&mut self, timeout: Duration, base: &Path) -> EResult<()> {
381 self.config = if let Some(config) = self.config.take() {
382 Some(config.extend(timeout, base).await?)
383 } else {
384 None
385 };
386 Ok(())
387 }
388 #[inline]
389 pub fn workers(&self) -> u32 {
390 self.workers
391 }
392 #[inline]
393 pub fn bus_queue_size(&self) -> usize {
394 self.bus.queue_size
395 }
396 #[inline]
397 pub fn take_config(&mut self) -> Option<Value> {
398 self.config.take()
399 }
400 #[inline]
401 pub async fn init_rpc<R>(&self, handlers: R) -> EResult<Arc<RpcClient>>
402 where
403 R: RpcHandlers + Send + Sync + 'static,
404 {
405 self.init_rpc_opts(handlers, rpc::Options::default()).await
406 }
407 #[inline]
408 pub async fn init_rpc_blocking<R>(&self, handlers: R) -> EResult<Arc<RpcClient>>
409 where
410 R: RpcHandlers + Send + Sync + 'static,
411 {
412 self.init_rpc_opts(
413 handlers,
414 rpc::Options::new()
415 .blocking_notifications()
416 .blocking_frames(),
417 )
418 .await
419 }
420 #[inline]
421 pub async fn init_rpc_blocking_with_secondary<R>(
422 &self,
423 handlers: R,
424 ) -> EResult<(Arc<RpcClient>, Arc<RpcClient>)>
425 where
426 R: RpcHandlers + Send + Sync + 'static,
427 {
428 let bus = self.init_bus_client().await?;
429 let bus_secondary = bus.register_secondary().await?;
430 let opts = rpc::Options::new()
431 .blocking_notifications()
432 .blocking_frames();
433 let rpc = Arc::new(RpcClient::create(bus, handlers, opts.clone()));
434 let rpc_secondary = Arc::new(RpcClient::create0(bus_secondary, opts));
435 Ok((rpc, rpc_secondary))
436 }
437 pub async fn init_rpc_opts<R>(&self, handlers: R, opts: rpc::Options) -> EResult<Arc<RpcClient>>
438 where
439 R: RpcHandlers + Send + Sync + 'static,
440 {
441 let bus = self.init_bus_client().await?;
442 let rpc = RpcClient::create(bus, handlers, opts);
443 Ok(Arc::new(rpc))
444 }
445 pub async fn init_bus_client(&self) -> EResult<busrt::ipc::Client> {
446 let bus = tokio::time::timeout(
447 self.bus_timeout(),
448 busrt::ipc::Client::connect(&self.bus_config()?),
449 )
450 .await??;
451 Ok(bus)
452 }
453 pub async fn init_bus_client_sub(&self, sub_id: &str) -> EResult<busrt::ipc::Client> {
454 let bus = tokio::time::timeout(
455 self.bus_timeout(),
456 busrt::ipc::Client::connect(&self.bus_config_for_sub(sub_id)?),
457 )
458 .await??;
459 Ok(bus)
460 }
461 #[inline]
462 pub fn init_registry(&self, rpc: &Arc<RpcClient>) -> Registry {
463 Registry {
464 id: self.id.clone(),
465 rpc: rpc.clone(),
466 }
467 }
468 #[inline]
469 pub fn can_rtf(&self) -> bool {
470 self.react_to_fail
471 }
472 #[inline]
473 pub fn is_mode_normal(&self) -> bool {
474 !self.fail_mode.load(atomic::Ordering::SeqCst)
475 }
476 #[inline]
477 pub fn is_mode_rtf(&self) -> bool {
478 self.fail_mode.load(atomic::Ordering::SeqCst)
479 }
480 #[inline]
481 pub fn set_fail_mode(&self, mode: bool) {
482 self.fail_mode.store(mode, atomic::Ordering::SeqCst);
483 }
484 #[cfg(not(target_os = "windows"))]
485 #[inline]
486 pub fn drop_privileges(&self) -> EResult<()> {
487 if let Some(ref user) = self.user {
488 if !user.is_empty() {
489 let u = get_system_user(user)?;
490 if nix::unistd::getuid() != u.uid {
491 let c_user = CString::new(user.as_str()).map_err(|e| {
492 Error::failed(format!("Failed to parse user {}: {}", user, e))
493 })?;
494
495 let groups = nix::unistd::getgrouplist(&c_user, u.gid).map_err(|e| {
496 Error::failed(format!("Failed to get groups for user {}: {}", user, e))
497 })?;
498 nix::unistd::setgroups(&groups).map_err(|e| {
499 Error::failed(format!(
500 "Failed to switch the process groups for user {}: {}",
501 user, e
502 ))
503 })?;
504 nix::unistd::setgid(u.gid).map_err(|e| {
505 Error::failed(format!(
506 "Failed to switch the process group for user {}: {}",
507 user, e
508 ))
509 })?;
510 nix::unistd::setuid(u.uid).map_err(|e| {
511 Error::failed(format!(
512 "Failed to switch the process user to {}: {}",
513 user, e
514 ))
515 })?;
516 }
517 }
518 }
519 Ok(())
520 }
521 pub fn into_legacy_compat(mut self) -> Self {
522 self.data_path = self.data_path().unwrap_or_default().to_owned();
523 let user = self.user.take().unwrap_or_default();
524 self.user.replace(user);
525 let timeout = self
526 .timeout
527 .default
528 .unwrap_or(crate::DEFAULT_TIMEOUT.as_secs_f64());
529 self.timeout.default.replace(timeout);
530 if self.timeout.startup.is_none() {
531 self.timeout.startup.replace(timeout);
532 }
533 if self.timeout.shutdown.is_none() {
534 self.timeout.shutdown.replace(timeout);
535 }
536 let config = self
537 .take_config()
538 .unwrap_or_else(|| Value::Map(<_>::default()));
539 self.config.replace(config);
540 self
541 }
542}
543
544#[cfg(not(target_os = "windows"))]
545pub fn get_system_user(user: &str) -> EResult<nix::unistd::User> {
546 let u = nix::unistd::User::from_name(user)
547 .map_err(|e| Error::failed(format!("failed to get the system user {}: {}", user, e)))?
548 .ok_or_else(|| Error::failed(format!("Failed to locate the system user {}", user)))?;
549 Ok(u)
550}
551
552#[cfg(not(target_os = "windows"))]
553pub fn get_system_group(group: &str) -> EResult<nix::unistd::Group> {
554 let g = nix::unistd::Group::from_name(group)
555 .map_err(|e| Error::failed(format!("failed to get the system group {}: {}", group, e)))?
556 .ok_or_else(|| Error::failed(format!("Failed to locate the system group {}", group)))?;
557 Ok(g)
558}
559
560#[derive(Debug, Serialize, Deserialize, Clone, Default)]
561pub struct Timeout {
562 startup: Option<f64>,
563 shutdown: Option<f64>,
564 default: Option<f64>,
565}
566
567impl Timeout {
568 pub fn offer(&mut self, timeout: f64) {
569 if self.startup.is_none() {
570 self.startup.replace(timeout);
571 }
572 if self.shutdown.is_none() {
573 self.shutdown.replace(timeout);
574 }
575 if self.default.is_none() {
576 self.default.replace(timeout);
577 }
578 }
579 pub fn get(&self) -> Option<Duration> {
580 self.default.map(Duration::from_secs_f64)
581 }
582 pub fn startup(&self) -> Option<Duration> {
583 self.startup.map(Duration::from_secs_f64)
584 }
585 pub fn shutdown(&self) -> Option<Duration> {
586 self.shutdown.map(Duration::from_secs_f64)
587 }
588}
589
590#[derive(Debug, Serialize, Deserialize)]
591pub struct CoreInfo {
592 build: u64,
593 version: String,
594 eapi_verion: u16,
595 path: String,
596 log_level: u8,
597 active: bool,
598}
599
600impl CoreInfo {
601 pub fn new(
602 build: u64,
603 version: &str,
604 eapi_verion: u16,
605 path: &str,
606 log_level: u8,
607 active: bool,
608 ) -> Self {
609 Self {
610 build,
611 version: version.to_owned(),
612 eapi_verion,
613 path: path.to_owned(),
614 log_level,
615 active,
616 }
617 }
618}
619
620#[inline]
621fn default_bus_type() -> String {
622 "native".to_owned()
623}
624
625#[inline]
626fn default_bus_buf_size() -> usize {
627 busrt::DEFAULT_BUF_SIZE
628}
629
630#[allow(clippy::cast_possible_truncation)]
631#[inline]
632fn default_bus_buf_ttl() -> u64 {
633 busrt::DEFAULT_BUF_TTL.as_micros() as u64
634}
635
636#[inline]
637fn default_bus_queue_size() -> usize {
638 busrt::DEFAULT_QUEUE_SIZE
639}
640
641#[derive(Debug, Clone, Deserialize, Serialize)]
642pub struct BusConfig {
643 #[serde(rename = "type", default = "default_bus_type")]
644 tp: String,
645 path: String,
646 timeout: Option<f64>,
647 #[serde(default = "default_bus_buf_size")]
648 buf_size: usize,
649 #[serde(default = "default_bus_buf_ttl")]
650 buf_ttl: u64, #[serde(default = "default_bus_queue_size")]
652 queue_size: usize,
653 #[serde(rename = "ping_interval", skip_serializing, default)]
655 _ping_interval: f64,
656}
657
658impl BusConfig {
659 pub fn path(&self) -> &str {
660 &self.path
661 }
662 pub fn set_path(&mut self, path: &str) {
663 path.clone_into(&mut self.path);
664 }
665 pub fn offer_timeout(&mut self, timeout: f64) {
666 if self.timeout.is_none() {
667 self.timeout.replace(timeout);
668 }
669 }
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct MethodParamInfo {
674 #[serde(default)]
675 pub required: bool,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct MethodInfo {
680 #[serde(default)]
681 pub description: String,
682 pub params: HashMap<String, MethodParamInfo>,
683}
684
685pub struct ServiceMethod {
687 pub name: String,
688 pub description: String,
689 pub params: HashMap<String, MethodParamInfo>,
690}
691
692impl ServiceMethod {
693 pub fn new(name: &str) -> Self {
694 Self {
695 name: name.to_owned(),
696 description: String::new(),
697 params: <_>::default(),
698 }
699 }
700 pub fn description(mut self, desc: &str) -> Self {
701 desc.clone_into(&mut self.description);
702 self
703 }
704 pub fn required(mut self, name: &str) -> Self {
705 self.params
706 .insert(name.to_owned(), MethodParamInfo { required: true });
707 self
708 }
709 pub fn optional(mut self, name: &str) -> Self {
710 self.params
711 .insert(name.to_owned(), MethodParamInfo { required: false });
712 self
713 }
714}
715
716#[derive(Serialize, Deserialize, Debug, Clone)]
718pub struct ServiceInfo {
719 #[serde(default)]
720 pub author: String,
721 #[serde(default)]
722 pub version: String,
723 #[serde(default)]
724 pub description: String,
725 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
726 pub methods: HashMap<String, MethodInfo>,
727}
728
729impl ServiceInfo {
730 pub fn new(author: &str, version: &str, description: &str) -> Self {
731 Self {
732 author: author.to_owned(),
733 version: version.to_owned(),
734 description: description.to_owned(),
735 methods: <_>::default(),
736 }
737 }
738 #[inline]
739 pub fn add_method(&mut self, method: ServiceMethod) {
740 self.methods.insert(
741 method.name,
742 MethodInfo {
743 description: method.description,
744 params: method.params,
745 },
746 );
747 }
748}
749
750#[derive(Serialize, Deserialize)]
752pub struct ServiceStatusBroadcastEvent {
753 pub status: ServiceStatusBroadcast,
754}
755
756impl ServiceStatusBroadcastEvent {
757 #[inline]
758 pub fn ready() -> Self {
759 Self {
760 status: ServiceStatusBroadcast::Ready,
761 }
762 }
763 #[inline]
764 pub fn terminating() -> Self {
765 Self {
766 status: ServiceStatusBroadcast::Terminating,
767 }
768 }
769}
770
771#[derive(Serialize, Deserialize)]
773#[serde(rename_all = "lowercase")]
774#[repr(u8)]
775pub enum ServiceStatusBroadcast {
776 Starting = 0,
777 Ready = 1,
778 Terminating = 0xef,
779 Unknown = 0xff,
780}
781
782impl fmt::Display for ServiceStatusBroadcast {
783 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784 write!(
785 f,
786 "{}",
787 match self {
788 ServiceStatusBroadcast::Starting => "starting",
789 ServiceStatusBroadcast::Ready => "ready",
790 ServiceStatusBroadcast::Terminating => "terminating",
791 ServiceStatusBroadcast::Unknown => "unknown",
792 }
793 )
794 }
795}