eva_common/
services.rs

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            &registry::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(&registry::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(&registry::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(&registry::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(&registry::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(&registry::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(&registry::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/// Initial properties for services
124#[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, // microseconds
651    #[serde(default = "default_bus_queue_size")]
652    queue_size: usize,
653    // deprecated field, as BUS/RT RPC uses timeout as a ping interval
654    #[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
685/// info-structure only, can be used by clients for auto-completion
686pub 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/// Returned by all services on "info" RPC command
717#[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/// Used by services to announce their status (for "*")
751#[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/// Used by services and the core to notify about its state
772#[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}