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