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