pikadick/commands/
system.rs

1use crate::checks::ENABLED_CHECK;
2use anyhow::Context as _;
3use heim::units::{
4    frequency::{
5        gigahertz,
6        hertz,
7    },
8    Frequency,
9};
10use serenity::{
11    builder::{
12        CreateEmbed,
13        CreateEmbedFooter,
14        CreateMessage,
15    },
16    framework::standard::{
17        macros::command,
18        Args,
19        CommandResult,
20    },
21    model::{
22        colour::Colour,
23        prelude::*,
24    },
25    prelude::*,
26};
27use std::time::{
28    Duration,
29    Instant,
30};
31use systemstat::{
32    platform::common::Platform,
33    System,
34};
35use time::format_description::well_known::Rfc2822;
36use tracing::warn;
37use uom::{
38    fmt::DisplayStyle,
39    si::f32::Frequency as FrequencyF32,
40};
41
42const BYTES_IN_GB_F64: f64 = 1_000_000_000_f64;
43
44fn fmt_cpu_frequency(freq: Frequency) -> String {
45    let fmt_args = FrequencyF32::format_args(gigahertz, DisplayStyle::Abbreviation);
46    let freq = FrequencyF32::new::<hertz>(freq.get::<hertz>() as f32);
47
48    format!("{:.2}", fmt_args.with(freq))
49}
50
51async fn get_cpu_usage() -> Result<f32, heim::Error> {
52    let start = heim::cpu::usage().await?;
53    tokio::time::sleep(Duration::from_secs(1)).await;
54    let end = heim::cpu::usage().await?;
55
56    Ok((end - start).get::<heim::units::ratio::percent>())
57}
58
59#[command]
60#[description("Get System Stats")]
61#[bucket("system")]
62#[checks(Enabled)]
63async fn system(ctx: &Context, msg: &Message, _args: Args) -> CommandResult {
64    let start = Instant::now();
65
66    let profile_avatar_url = ctx.cache.current_user().avatar_url();
67
68    // Start Legacy data gathering
69    let sys = System::new();
70
71    let cpu_temp = match sys.cpu_temp() {
72        Ok(cpu_temp) => Some(cpu_temp),
73        Err(error) => {
74            warn!("Failed to get cpu temp: {error}");
75            None
76        }
77    };
78
79    // End Legacy data gathering
80    let cache_context = pikadick_system_info::CacheContext::new();
81
82    let boot_time = match cache_context
83        .get_boot_time()
84        .context("failed to get boot time")
85        .map(time::OffsetDateTime::from)
86        .and_then(|boot_time| {
87            boot_time
88                .format(&Rfc2822)
89                .context("failed to format boot time date")
90        }) {
91        Ok(boot_time) => Some(boot_time),
92        Err(error) => {
93            warn!("{error:?}");
94            None
95        }
96    };
97
98    let uptime = match cache_context.get_uptime().context("failed to get uptime") {
99        Ok(uptime) => Some(uptime),
100        Err(error) => {
101            warn!("{error:?}");
102            None
103        }
104    };
105
106    let hostname = match cache_context
107        .get_hostname()
108        .context("failed to get hostname")
109    {
110        Ok(hostname) => Some(hostname),
111        Err(error) => {
112            warn!("{error:?}");
113            None
114        }
115    };
116
117    let architecture = match cache_context
118        .get_architecture()
119        .context("failed to get architecture")
120    {
121        Ok(architecture) => Some(
122            architecture
123                .map(|architecture| architecture.as_str())
124                .unwrap_or("unknown"),
125        ),
126        Err(error) => {
127            warn!("{error:?}");
128            None
129        }
130    };
131
132    let system_name = match cache_context
133        .get_system_name()
134        .context("failed to get system name")
135    {
136        Ok(system_name) => system_name,
137        Err(error) => {
138            warn!("{error:?}");
139            None
140        }
141    };
142
143    let system_version = match cache_context
144        .get_system_version()
145        .context("failed to get system version")
146    {
147        Ok(system_name) => Some(system_name),
148        Err(error) => {
149            warn!("{error:?}");
150            None
151        }
152    };
153
154    let total_memory = match cache_context
155        .get_total_memory()
156        .context("failed to get total memory")
157    {
158        Ok(memory) => Some(memory),
159        Err(error) => {
160            warn!("{error:?}");
161            None
162        }
163    };
164
165    let available_memory = match cache_context
166        .get_available_memory()
167        .context("failed to get available memory")
168    {
169        Ok(memory) => Some(memory),
170        Err(error) => {
171            warn!("{error:?}");
172            None
173        }
174    };
175
176    let total_swap = match cache_context
177        .get_total_swap()
178        .context("failed to get total swap")
179    {
180        Ok(memory) => Some(memory),
181        Err(error) => {
182            warn!("{error:?}");
183            None
184        }
185    };
186
187    let available_swap = match cache_context
188        .get_available_swap()
189        .context("failed to get available swap")
190    {
191        Ok(memory) => Some(memory),
192        Err(error) => {
193            warn!("{error:?}");
194            None
195        }
196    };
197
198    let cpu_frequency = match heim::cpu::frequency().await {
199        Ok(cpu_frequency) => Some(cpu_frequency),
200        Err(error) => {
201            warn!("Failed to get cpu frequency: {error}");
202            None
203        }
204    };
205
206    let cpu_logical_count = match heim::cpu::logical_count().await {
207        Ok(cpu_logical_count) => Some(cpu_logical_count),
208        Err(error) => {
209            warn!("Failed to get logical cpu count: {error}");
210            None
211        }
212    };
213
214    let cpu_physical_count = match heim::cpu::physical_count().await {
215        Ok(cpu_physical_count) => cpu_physical_count, // This returns an option, so we return it here to flatten it.
216        Err(error) => {
217            warn!("Failed to get physical cpu count: {error}");
218            None
219        }
220    };
221
222    let virtualization = heim::virt::detect().await;
223
224    let cpu_usage = match get_cpu_usage().await {
225        Ok(usage) => Some(usage),
226        Err(error) => {
227            warn!("Failed to get cpu usage: {error}");
228            None
229        }
230    };
231
232    let data_retrieval_time = Instant::now() - start;
233
234    // Start WIP
235
236    // Reports Cpu time since boot.
237    // let cpu_time = heim::cpu::time().await.unwrap();
238
239    // Reports some cpu stats
240    // let cpu_stats = heim::cpu::stats().await.unwrap();
241
242    // Reports temps from all sensors
243    // let temperatures = heim::sensors::temperatures().collect::<Vec<_>>().await;
244
245    // End WIP
246
247    let mut embed_builder = CreateEmbed::new()
248        .title("System Status")
249        .color(Colour::from_rgb(255, 0, 0));
250    if let Some(icon) = profile_avatar_url {
251        embed_builder = embed_builder.thumbnail(icon);
252    }
253
254    if let Some(hostname) = hostname {
255        embed_builder = embed_builder.field("Hostname", hostname, true);
256    }
257
258    if let Some(system_name) = system_name {
259        embed_builder = embed_builder.field("OS", system_name, true);
260    }
261
262    if let Some(system_version) = system_version {
263        embed_builder = embed_builder.field("OS Version", system_version, true);
264    }
265
266    if let Some(architecture) = architecture {
267        embed_builder = embed_builder.field("Architecture", architecture, true);
268    }
269
270    if let Some(boot_time) = boot_time {
271        embed_builder = embed_builder.field("Boot Time", boot_time, true);
272    }
273
274    if let Some(uptime) = uptime {
275        let raw_secs = uptime.as_secs();
276
277        let days = raw_secs / (60 * 60 * 24);
278        let hours = (raw_secs % (60 * 60 * 24)) / (60 * 60);
279        let minutes = (raw_secs % (60 * 60)) / 60;
280        let seconds = raw_secs % 60;
281
282        let mut value = String::with_capacity(64);
283        if days != 0 {
284            value.push_str(itoa::Buffer::new().format(days));
285            value.push_str(" day");
286            if days > 1 {
287                value.push('s');
288            }
289        }
290
291        if hours != 0 {
292            value.push(' ');
293
294            value.push_str(itoa::Buffer::new().format(hours));
295            value.push_str(" hour");
296            if hours > 1 {
297                value.push('s');
298            }
299        }
300
301        if minutes != 0 {
302            value.push(' ');
303
304            value.push_str(itoa::Buffer::new().format(minutes));
305            value.push_str(" minute");
306            if minutes > 1 {
307                value.push('s');
308            }
309        }
310
311        if seconds != 0 {
312            value.push(' ');
313
314            value.push_str(itoa::Buffer::new().format(seconds));
315            value.push_str(" second");
316            if seconds > 1 {
317                value.push('s');
318            }
319        }
320
321        embed_builder = embed_builder.field("Uptime", value, true);
322    }
323
324    // Currently reports incorrectly on Windows
325    if let Some(cpu_frequency) = cpu_frequency {
326        embed_builder =
327            embed_builder.field("Cpu Freq", fmt_cpu_frequency(cpu_frequency.current()), true);
328
329        if let Some(min_cpu_frequency) = cpu_frequency.min() {
330            embed_builder =
331                embed_builder.field("Min Cpu Freq", fmt_cpu_frequency(min_cpu_frequency), true);
332        }
333
334        if let Some(max_cpu_frequency) = cpu_frequency.max() {
335            embed_builder =
336                embed_builder.field("Max Cpu Freq", fmt_cpu_frequency(max_cpu_frequency), true);
337        }
338    }
339
340    match (cpu_logical_count, cpu_physical_count) {
341        (Some(logical_count), Some(physical_count)) => {
342            embed_builder = embed_builder.field(
343                "Cpu Core Count",
344                format!("{logical_count} logical, {physical_count} physical"),
345                true,
346            );
347        }
348        (Some(logical_count), None) => {
349            embed_builder =
350                embed_builder.field("Cpu Core Count", format!("{logical_count} logical"), true);
351        }
352        (None, Some(physical_count)) => {
353            embed_builder =
354                embed_builder.field("Cpu Core Count", format!("{physical_count} physical"), true);
355        }
356        (None, None) => {}
357    }
358
359    if let (Some(total_memory), Some(available_memory)) = (total_memory, available_memory) {
360        embed_builder = embed_builder.field(
361            "Memory Usage",
362            format!(
363                "{:.2} GB / {:.2} GB",
364                (total_memory - available_memory) as f64 / BYTES_IN_GB_F64,
365                total_memory as f64 / BYTES_IN_GB_F64,
366            ),
367            true,
368        );
369    }
370
371    if let (Some(total_swap), Some(available_swap)) = (total_swap, available_swap) {
372        embed_builder = embed_builder.field(
373            "Swap Usage",
374            format!(
375                "{:.2} GB / {:.2} GB",
376                (total_swap - available_swap) as f64 / BYTES_IN_GB_F64,
377                total_swap as f64 / BYTES_IN_GB_F64,
378            ),
379            true,
380        );
381    }
382
383    let virtualization_field = match virtualization.as_ref() {
384        Some(virtualization) => virtualization.as_str(),
385        None => "None",
386    };
387    embed_builder = embed_builder.field("Virtualization", virtualization_field, true);
388
389    if let (Some(cpu_usage), Some(cpu_logical_count)) = (cpu_usage, cpu_logical_count) {
390        embed_builder = embed_builder.field(
391            "Cpu Usage",
392            format!("{:.2}%", cpu_usage / (cpu_logical_count as f32)),
393            true,
394        );
395    }
396
397    /////////////////////////////////////////////////////////////////////////////////////
398    // Legacy (These functions from systemstat have no direct replacement in heim yet) //
399    /////////////////////////////////////////////////////////////////////////////////////
400
401    // This does not work on Windows
402    // TODO: This can probably be replaced with temprature readings from heim.
403    // It doesn't support Windows, but this never worked there anyways as Windows has no simple way to get temps
404    if let Some(cpu_temp) = cpu_temp {
405        embed_builder = embed_builder.field("Cpu Temp", format!("{cpu_temp} °C"), true);
406    }
407
408    let footer = CreateEmbedFooter::new(format!(
409        "Retrieved system data in {:.2} second(s)",
410        data_retrieval_time.as_secs_f32()
411    ));
412
413    embed_builder = embed_builder.footer(footer);
414
415    let message_builder = CreateMessage::new().embed(embed_builder);
416    msg.channel_id
417        .send_message(&ctx.http, message_builder)
418        .await?;
419
420    Ok(())
421}