1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//! Display the status of a service
//!
//! Right now only `systemd` is supported.
//!
//! # Configuration
//!
//! Key | Values | Default
//! ----|--------|--------
//! `driver` | Which init system is running the service. Available drivers are: `"systemd"` | `"systemd"`
//! `service` | The name of the service | **Required**
//! `active_format` | A string to customise the output of this block. See below for available placeholders. | `" $service active "`
//! `inactive_format` | A string to customise the output of this block. See below for available placeholders. | `" $service inactive "`
//! `active_state` | A valid [`State`] | [`State::Idle`]
//! `inactive_state` | A valid [`State`]  | [`State::Critical`]
//!
//! Placeholder    | Value                     | Type   | Unit
//! ---------------|---------------------------|--------|-----
//! `service`      | The name of the service   | Text   | -
//!
//! # Example
//!
//! Example using an icon:
//!
//! ```toml
//! [[block]]
//! block = "service_status"
//! service = "cups"
//! active_format = " ^icon_tea "
//! inactive_format = " no ^icon_tea "
//! ```
//!
//! Example overriding the default `inactive_state`:
//!
//! ```toml
//! [[block]]
//! block = "service_status"
//! service = "shadow"
//! active_format = ""
//! inactive_format = " Integrity of password and group files failed "
//! inactive_state = "Warning"
//! ```
//!

use super::prelude::*;
use zbus::proxy::PropertyStream;

#[derive(Deserialize, Debug, Default)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
    pub driver: DriverType,
    pub service: String,
    pub active_format: FormatConfig,
    pub inactive_format: FormatConfig,
    pub active_state: Option<State>,
    pub inactive_state: Option<State>,
}

#[derive(Deserialize, Debug, SmartDefault)]
#[serde(rename_all = "snake_case")]
pub enum DriverType {
    #[default]
    Systemd,
}

pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
    let active_format = config.active_format.with_default(" $service active ")?;
    let inactive_format = config.inactive_format.with_default(" $service inactive ")?;

    let active_state = config.active_state.unwrap_or(State::Idle);
    let inactive_state = config.inactive_state.unwrap_or(State::Critical);

    let mut driver: Box<dyn Driver> = match config.driver {
        DriverType::Systemd => Box::new(SystemdDriver::new(config.service.clone()).await?),
    };

    loop {
        let service_active_state = driver.is_active().await?;

        let mut widget = Widget::new();

        if service_active_state {
            widget.state = active_state;
            widget.set_format(active_format.clone());
        } else {
            widget.state = inactive_state;
            widget.set_format(inactive_format.clone());
        };

        widget.set_values(map! {
            "service" =>Value::text(config.service.clone()),
        });

        api.set_widget(widget)?;

        driver.wait_for_change().await?;
    }
}

#[async_trait]
trait Driver {
    async fn is_active(&self) -> Result<bool>;
    async fn wait_for_change(&mut self) -> Result<()>;
}

struct SystemdDriver {
    proxy: UnitProxy<'static>,
    active_state_changed: PropertyStream<'static, String>,
}

impl SystemdDriver {
    async fn new(service: String) -> Result<Self> {
        let dbus_conn = new_system_dbus_connection().await?;

        if !service.is_ascii() {
            return Err(Error::new(format!(
                "service name \"{service}\" must only contain ASCII characters"
            )));
        }
        let encoded_service = format!("{service}.service")
            // For each byte...
            .bytes()
            .map(|b| {
                if b.is_ascii_alphanumeric() {
                    // Just use the character as a string
                    char::from(b).to_string()
                } else {
                    // Otherwise use the hex representation of the byte preceded by an underscore
                    format!("_{b:02x}")
                }
            })
            .collect::<String>();

        let path = format!("/org/freedesktop/systemd1/unit/{encoded_service}");

        let proxy = UnitProxy::builder(&dbus_conn)
            .path(path)
            .error("Could not set path")?
            .build()
            .await
            .error("Failed to create UnitProxy")?;

        Ok(Self {
            active_state_changed: proxy.receive_active_state_changed().await,
            proxy,
        })
    }
}

#[async_trait]
impl Driver for SystemdDriver {
    async fn is_active(&self) -> Result<bool> {
        self.proxy
            .active_state()
            .await
            .error("Could not get active_state")
            .map(|state| state == "active")
    }

    async fn wait_for_change(&mut self) -> Result<()> {
        self.active_state_changed.next().await;
        Ok(())
    }
}

#[zbus::proxy(
    interface = "org.freedesktop.systemd1.Unit",
    default_service = "org.freedesktop.systemd1"
)]
trait Unit {
    #[zbus(property)]
    fn active_state(&self) -> zbus::Result<String>;
}