initial commit
53
.eslintrc.cjs
Normal file
@ -0,0 +1,53 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"prettier"
|
||||
],
|
||||
plugins: ["svelte3", "@typescript-eslint"],
|
||||
overrides: [{ files: ["*.svelte"], processor: "svelte3/svelte3" }],
|
||||
settings: {
|
||||
"svelte3/typescript": () => require("typescript")
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaVersion: 2022
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
Data: true,
|
||||
SummaryData: true,
|
||||
CPUData: true,
|
||||
MemData: true,
|
||||
NetData: true,
|
||||
DiskData: true,
|
||||
TempData: true,
|
||||
IOData: true,
|
||||
BatteryData: true,
|
||||
Process: true
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
camelcase: ["error", { properties: "never", ignoreDestructuring: true }],
|
||||
eqeqeq: "error",
|
||||
"func-names": "error",
|
||||
"import/order": "error",
|
||||
"import/no-unresolved": "off",
|
||||
"no-unused-vars": ["error", { varsIgnorePattern: "^_", argsIgnorePattern: "^_" }],
|
||||
"no-var": "error",
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-const": "off",
|
||||
"require-await": "error"
|
||||
}
|
||||
}
|
||||
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
/src-tauri
|
||||
|
||||
package-lock.json
|
||||
8
.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
1
README.md
Normal file
@ -0,0 +1 @@
|
||||
A work-in-progress system monitoring GUI inspired by Conky Seamod.
|
||||
14
index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + Svelte + TS</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
3814
package-lock.json
generated
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "toerings",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"pretty": "prettier --write --plugin-search-dir . .",
|
||||
"lint": "eslint --ignore-path .prettierignore .",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"path-browserify": "^1.0.1",
|
||||
"uplot": "^1.6.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.0",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/node": "^18.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"colord": "^2.9.3",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-awesome-color-picker": "^2.4.1",
|
||||
"svelte-check": "^3.0.0",
|
||||
"svelte-preprocess": "^5.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
0
public/.gitkeep
Normal file
BIN
public/example1.png
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
public/example2.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/example3.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
6
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
log.txt
|
||||
|
||||
4107
src-tauri/Cargo.lock
generated
Normal file
74
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "toerings"
|
||||
version = "0.0.0"
|
||||
description = "A spiritual port of Conky SeaMod to Tauri."
|
||||
authors = ["Andrew Carlson <2yinyang2@gmail.com>"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/acarl005/toerings"
|
||||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_millis = "0.1.1"
|
||||
tauri = { version = "1.2", features = ["macos-private-api", "shell-open", "window-set-size", "window-start-dragging"] }
|
||||
sysinfo = "0.27.7"
|
||||
anyhow = "1.0.68"
|
||||
backtrace = "0.3.67"
|
||||
cfg-if = "1.0.0"
|
||||
futures = "0.3.25"
|
||||
futures-timer = "3.0.2"
|
||||
fxhash = "0.2.1"
|
||||
once_cell = "1.17.0"
|
||||
itertools = "0.10.5"
|
||||
thiserror = "1.0.38"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1.1"
|
||||
local-ip-address = "0.5.1"
|
||||
time = { version = "0.3.9", features = ["formatting", "macros"] }
|
||||
fern = { version = "0.6.1", optional = true }
|
||||
log = { version = "0.4.17", optional = true }
|
||||
starship-battery = { version = "0.7.9", optional = true }
|
||||
nvml-wrapper = { version = "0.8.0", optional = true }
|
||||
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.139"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
heim = { git = "https://github.com/heim-rs/heim", features = ["cpu", "disk", "memory", "net", "sensors"] }
|
||||
procfs = { version = "0.14.2", default-features = false }
|
||||
smol = "1.2.5"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
heim = { git = "https://github.com/heim-rs/heim", features = ["cpu", "disk", "memory", "net"] }
|
||||
mach2 = "0.4.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
heim = { git = "https://github.com/heim-rs/heim", features = ["cpu", "disk", "memory"] }
|
||||
windows = { version = "0.44.0", features = ["Win32_System_Threading", "Win32_Foundation"] }
|
||||
winapi = "0.3.9"
|
||||
|
||||
[target.'cfg(target_os = "freebsd")'.dependencies]
|
||||
serde_json = { version = "1.0.82" }
|
||||
sysctl = { version = "0.5.2", optional = true }
|
||||
filedescriptor = "0.8.2"
|
||||
|
||||
[features]
|
||||
battery = ["starship-battery"]
|
||||
gpu = ["nvidia"]
|
||||
nvidia = ["nvml-wrapper"]
|
||||
zfs = ["sysctl"]
|
||||
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = ["custom-protocol", "fern", "log"]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
3
src-tauri/icons/icon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" fill="transparent" stroke-linecap="round">
|
||||
<path d="M60,115 A55,55 0 1,1 115,60" stroke="black" stroke-width="10px" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 190 B |
BIN
src-tauri/icons/logo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
465
src-tauri/src/data_harvester.rs
Normal file
@ -0,0 +1,465 @@
|
||||
//! This is the main file to house data collection functions.
|
||||
|
||||
use std::{
|
||||
net::IpAddr,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures::join;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
use serde::Serialize;
|
||||
#[cfg(feature = "battery")]
|
||||
use starship_battery::{Battery, Manager};
|
||||
|
||||
use sysinfo::{System, SystemExt};
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
pub mod nvidia;
|
||||
|
||||
#[cfg(feature = "battery")]
|
||||
pub mod batteries;
|
||||
pub mod cpu;
|
||||
pub mod disks;
|
||||
pub mod memory;
|
||||
pub mod network;
|
||||
pub mod processes;
|
||||
pub mod temperature;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Data {
|
||||
#[serde(with = "serde_millis")]
|
||||
pub last_collection_time: Instant,
|
||||
pub cpu: Option<cpu::CpuHarvest>,
|
||||
pub load_avg: Option<cpu::LoadAvgHarvest>,
|
||||
pub memory: Option<memory::MemHarvest>,
|
||||
pub swap: Option<memory::MemHarvest>,
|
||||
pub temperature_sensors: Option<Vec<temperature::TempHarvest>>,
|
||||
pub network: Option<network::NetworkHarvest>,
|
||||
pub list_of_processes: Option<Vec<processes::ProcessHarvest>>,
|
||||
pub disks: Option<Vec<disks::DiskHarvest>>,
|
||||
pub io: Option<disks::IoHarvest>,
|
||||
#[serde(with = "humantime_serde")]
|
||||
#[serde(default)]
|
||||
pub uptime: Duration,
|
||||
pub hostname: Option<String>,
|
||||
pub kernel_name: Option<String>,
|
||||
pub kernel_version: Option<String>,
|
||||
pub os_version: Option<String>,
|
||||
pub local_ip: Option<IpAddr>,
|
||||
#[cfg(feature = "battery")]
|
||||
pub list_of_batteries: Option<Vec<batteries::BatteryHarvest>>,
|
||||
#[cfg(feature = "zfs")]
|
||||
pub arc: Option<memory::MemHarvest>,
|
||||
#[cfg(feature = "gpu")]
|
||||
pub gpu: Option<Vec<(String, memory::MemHarvest)>>,
|
||||
}
|
||||
|
||||
impl Default for Data {
|
||||
fn default() -> Self {
|
||||
Data {
|
||||
last_collection_time: Instant::now(),
|
||||
cpu: None,
|
||||
load_avg: None,
|
||||
memory: None,
|
||||
swap: None,
|
||||
temperature_sensors: None,
|
||||
list_of_processes: None,
|
||||
disks: None,
|
||||
io: None,
|
||||
network: None,
|
||||
uptime: Duration::ZERO,
|
||||
hostname: None,
|
||||
kernel_name: None,
|
||||
kernel_version: None,
|
||||
os_version: None,
|
||||
local_ip: None,
|
||||
#[cfg(feature = "battery")]
|
||||
list_of_batteries: None,
|
||||
#[cfg(feature = "zfs")]
|
||||
arc: None,
|
||||
#[cfg(feature = "gpu")]
|
||||
gpu: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.io = None;
|
||||
self.temperature_sensors = None;
|
||||
self.list_of_processes = None;
|
||||
self.disks = None;
|
||||
self.memory = None;
|
||||
self.swap = None;
|
||||
self.cpu = None;
|
||||
self.load_avg = None;
|
||||
|
||||
if let Some(network) = &mut self.network {
|
||||
network.first_run_cleanup();
|
||||
}
|
||||
#[cfg(feature = "zfs")]
|
||||
{
|
||||
self.arc = None;
|
||||
}
|
||||
#[cfg(feature = "gpu")]
|
||||
{
|
||||
self.gpu = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DataCollector {
|
||||
pub data: Data,
|
||||
sys: System,
|
||||
previous_cpu_times: Vec<(cpu::PastCpuWork, cpu::PastCpuTotal)>,
|
||||
previous_average_cpu_time: Option<(cpu::PastCpuWork, cpu::PastCpuTotal)>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pid_mapping: FxHashMap<crate::Pid, processes::PrevProcDetails>,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_idle: f64,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_non_idle: f64,
|
||||
mem_total_kb: u64,
|
||||
use_current_cpu_total: bool,
|
||||
unnormalized_cpu: bool,
|
||||
last_collection_time: Instant,
|
||||
total_rx: u64,
|
||||
total_tx: u64,
|
||||
show_average_cpu: bool,
|
||||
#[cfg(feature = "battery")]
|
||||
battery_manager: Option<Manager>,
|
||||
#[cfg(feature = "battery")]
|
||||
battery_list: Option<Vec<Battery>>,
|
||||
#[cfg(target_family = "unix")]
|
||||
user_table: self::processes::UserTable,
|
||||
}
|
||||
|
||||
impl DataCollector {
|
||||
pub fn new() -> Self {
|
||||
DataCollector {
|
||||
data: Data::default(),
|
||||
sys: System::new_with_specifics(sysinfo::RefreshKind::new()),
|
||||
previous_cpu_times: vec![],
|
||||
previous_average_cpu_time: None,
|
||||
#[cfg(target_os = "linux")]
|
||||
pid_mapping: FxHashMap::default(),
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_idle: 0_f64,
|
||||
#[cfg(target_os = "linux")]
|
||||
prev_non_idle: 0_f64,
|
||||
mem_total_kb: 0,
|
||||
use_current_cpu_total: false,
|
||||
unnormalized_cpu: false,
|
||||
last_collection_time: Instant::now(),
|
||||
total_rx: 0,
|
||||
total_tx: 0,
|
||||
show_average_cpu: false,
|
||||
#[cfg(feature = "battery")]
|
||||
battery_manager: None,
|
||||
#[cfg(feature = "battery")]
|
||||
battery_list: None,
|
||||
#[cfg(target_family = "unix")]
|
||||
user_table: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
futures::executor::block_on(self.initialize_memory_size());
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
self.sys.refresh_memory();
|
||||
self.mem_total_kb = self.sys.total_memory();
|
||||
|
||||
// TODO: Would be good to get this and network list running on a timer instead...?
|
||||
// Refresh components list once...
|
||||
self.sys.refresh_components_list();
|
||||
|
||||
// Refresh network list once...
|
||||
if cfg!(target_os = "windows") {
|
||||
self.sys.refresh_networks_list();
|
||||
}
|
||||
|
||||
self.sys.refresh_cpu();
|
||||
|
||||
// Refresh disk list once...
|
||||
if cfg!(target_os = "freebsd") {
|
||||
self.sys.refresh_disks_list();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "battery")]
|
||||
{
|
||||
if let Ok(battery_manager) = Manager::new() {
|
||||
if let Ok(batteries) = battery_manager.batteries() {
|
||||
let battery_list: Vec<Battery> = batteries.filter_map(Result::ok).collect();
|
||||
if !battery_list.is_empty() {
|
||||
self.battery_list = Some(battery_list);
|
||||
self.battery_manager = Some(battery_manager);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
futures::executor::block_on(self.update_data());
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
|
||||
self.data.cleanup();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn initialize_memory_size(&mut self) {
|
||||
self.mem_total_kb = if let Ok(mem) = heim::memory::memory().await {
|
||||
mem.total().get::<heim::units::information::kilobyte>()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn update_data(&mut self) {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
self.sys.refresh_cpu();
|
||||
self.sys.refresh_processes();
|
||||
self.sys.refresh_components();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.sys.refresh_networks();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
self.sys.refresh_disks();
|
||||
self.sys.refresh_memory();
|
||||
}
|
||||
}
|
||||
|
||||
let current_instant = std::time::Instant::now();
|
||||
|
||||
// CPU
|
||||
#[cfg(not(target_os = "freebsd"))]
|
||||
{
|
||||
if let Ok(cpu_data) = cpu::get_cpu_data_list(
|
||||
self.show_average_cpu,
|
||||
&mut self.previous_cpu_times,
|
||||
&mut self.previous_average_cpu_time,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.data.cpu = Some(cpu_data);
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
if let Ok(cpu_data) = cpu::get_cpu_data_list(
|
||||
&self.sys,
|
||||
self.show_average_cpu,
|
||||
&mut self.previous_cpu_times,
|
||||
&mut self.previous_average_cpu_time,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.data.cpu = Some(cpu_data);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
// Load Average
|
||||
if let Ok(load_avg_data) = cpu::get_load_avg().await {
|
||||
self.data.load_avg = Some(load_avg_data);
|
||||
}
|
||||
}
|
||||
|
||||
// Batteries
|
||||
#[cfg(feature = "battery")]
|
||||
{
|
||||
if let Some(battery_manager) = &self.battery_manager {
|
||||
if let Some(battery_list) = &mut self.battery_list {
|
||||
self.data.list_of_batteries =
|
||||
Some(batteries::refresh_batteries(battery_manager, battery_list));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut process_list) = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Must do this here since we otherwise have to make `get_process_data` async.
|
||||
use self::processes::CpuUsageStrategy;
|
||||
|
||||
let normalize_cpu = if self.unnormalized_cpu {
|
||||
heim::cpu::logical_count()
|
||||
.await
|
||||
.map(|v| CpuUsageStrategy::NonNormalized(v as f64))
|
||||
.unwrap_or(CpuUsageStrategy::Normalized)
|
||||
} else {
|
||||
CpuUsageStrategy::Normalized
|
||||
};
|
||||
|
||||
processes::get_process_data(
|
||||
&mut self.prev_idle,
|
||||
&mut self.prev_non_idle,
|
||||
&mut self.pid_mapping,
|
||||
self.use_current_cpu_total,
|
||||
normalize_cpu,
|
||||
current_instant
|
||||
.duration_since(self.last_collection_time)
|
||||
.as_secs(),
|
||||
self.mem_total_kb,
|
||||
&mut self.user_table,
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
#[cfg(target_family = "unix")]
|
||||
{
|
||||
processes::get_process_data(
|
||||
&self.sys,
|
||||
self.use_current_cpu_total,
|
||||
self.unnormalized_cpu,
|
||||
self.mem_total_kb,
|
||||
&mut self.user_table,
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
{
|
||||
processes::get_process_data(
|
||||
&self.sys,
|
||||
self.use_current_cpu_total,
|
||||
self.unnormalized_cpu,
|
||||
self.mem_total_kb,
|
||||
)
|
||||
}
|
||||
}
|
||||
} {
|
||||
// NB: To avoid duplicate sorts on rerenders/events, we sort the processes by PID here.
|
||||
// We also want to avoid re-sorting *again* later on if we're sorting by PID, since we already
|
||||
// did it here!
|
||||
process_list.sort_unstable_by_key(|p| p.pid);
|
||||
self.data.list_of_processes = Some(process_list);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
if let Ok(data) = temperature::get_temperature_data(&self.sys) {
|
||||
self.data.temperature_sensors = data;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(data) = temperature::get_temperature_data() {
|
||||
self.data.temperature_sensors = data;
|
||||
}
|
||||
}
|
||||
|
||||
let network_data_fut = {
|
||||
#[cfg(any(target_os = "windows", target_os = "freebsd"))]
|
||||
{
|
||||
network::get_network_data(
|
||||
&self.sys,
|
||||
self.last_collection_time,
|
||||
&mut self.total_rx,
|
||||
&mut self.total_tx,
|
||||
current_instant,
|
||||
)
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "freebsd")))]
|
||||
{
|
||||
network::get_network_data(
|
||||
self.last_collection_time,
|
||||
&mut self.total_rx,
|
||||
&mut self.total_tx,
|
||||
current_instant,
|
||||
)
|
||||
}
|
||||
};
|
||||
let mem_data_fut = {
|
||||
#[cfg(not(target_os = "freebsd"))]
|
||||
{
|
||||
memory::get_mem_data()
|
||||
}
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
memory::get_mem_data(&self.sys)
|
||||
}
|
||||
};
|
||||
let disk_data_fut = disks::get_disk_usage();
|
||||
let disk_io_usage_fut = disks::get_io_usage();
|
||||
|
||||
let (net_data, mem_res, disk_res, io_res) = join!(
|
||||
network_data_fut,
|
||||
mem_data_fut,
|
||||
disk_data_fut,
|
||||
disk_io_usage_fut,
|
||||
);
|
||||
|
||||
if let Ok(net_data) = net_data {
|
||||
if let Some(net_data) = &net_data {
|
||||
self.total_rx = net_data.total_rx;
|
||||
self.total_tx = net_data.total_tx;
|
||||
}
|
||||
self.data.network = net_data;
|
||||
}
|
||||
|
||||
if let Ok(memory) = mem_res.ram {
|
||||
self.data.memory = memory;
|
||||
}
|
||||
|
||||
if let Ok(swap) = mem_res.swap {
|
||||
self.data.swap = swap;
|
||||
}
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
if let Ok(arc) = mem_res.arc {
|
||||
self.data.arc = arc;
|
||||
}
|
||||
|
||||
#[cfg(feature = "gpu")]
|
||||
if let Ok(gpu) = mem_res.gpus {
|
||||
self.data.gpu = gpu;
|
||||
}
|
||||
|
||||
if let Ok(disks) = disk_res {
|
||||
self.data.disks = disks;
|
||||
}
|
||||
|
||||
if let Ok(io) = io_res {
|
||||
self.data.io = io;
|
||||
}
|
||||
|
||||
self.data.uptime = Duration::from_secs(self.sys.uptime());
|
||||
self.data.hostname = self.sys.host_name();
|
||||
self.data.kernel_name = self.sys.name();
|
||||
self.data.kernel_version = self.sys.kernel_version();
|
||||
self.data.os_version = self.sys.long_os_version();
|
||||
self.data.local_ip = local_ip_address::local_ip().ok();
|
||||
|
||||
// Update time
|
||||
self.data.last_collection_time = current_instant;
|
||||
self.last_collection_time = current_instant;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
/// Deserialize [libxo](https://www.freebsd.org/cgi/man.cgi?query=libxo&apropos=0&sektion=0&manpath=FreeBSD+13.1-RELEASE+and+Ports&arch=default&format=html) JSON data
|
||||
fn deserialize_xo<T>(key: &str, data: &[u8]) -> Result<T, std::io::Error>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let mut value: serde_json::Value = serde_json::from_slice(data)?;
|
||||
value
|
||||
.as_object_mut()
|
||||
.and_then(|map| map.remove(key))
|
||||
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "key not found"))
|
||||
.and_then(|val| serde_json::from_value(val).map_err(|err| err.into()))
|
||||
}
|
||||
10
src-tauri/src/data_harvester/batteries.rs
Normal file
@ -0,0 +1,10 @@
|
||||
//! Data collection for batteries.
|
||||
//!
|
||||
//! For Linux, macOS, Windows, FreeBSD, Dragonfly, and iOS, this is handled by the battery crate.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "ios"))] {
|
||||
pub mod battery;
|
||||
pub use self::battery::*;
|
||||
}
|
||||
}
|
||||
49
src-tauri/src/data_harvester/batteries/battery.rs
Normal file
@ -0,0 +1,49 @@
|
||||
//! Uses the battery crate from svartalf.
|
||||
//! Covers battery usage for:
|
||||
//! - Linux 2.6.39+
|
||||
//! - MacOS 10.10+
|
||||
//! - iOS
|
||||
//! - Windows 7+
|
||||
//! - FreeBSD
|
||||
//! - DragonFlyBSD
|
||||
//!
|
||||
//! For more information, refer to the [starship_battery](https://github.com/starship/rust-battery) repo/docs.
|
||||
|
||||
use starship_battery::{
|
||||
units::{power::watt, ratio::percent, time::second},
|
||||
Battery, Manager,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BatteryHarvest {
|
||||
pub charge_percent: f64,
|
||||
pub secs_until_full: Option<i64>,
|
||||
pub secs_until_empty: Option<i64>,
|
||||
pub power_consumption_rate_watts: f64,
|
||||
pub health_percent: f64,
|
||||
}
|
||||
|
||||
pub fn refresh_batteries(manager: &Manager, batteries: &mut [Battery]) -> Vec<BatteryHarvest> {
|
||||
batteries
|
||||
.iter_mut()
|
||||
.filter_map(|battery| {
|
||||
if manager.refresh(battery).is_ok() {
|
||||
Some(BatteryHarvest {
|
||||
secs_until_full: {
|
||||
let optional_time = battery.time_to_full();
|
||||
optional_time.map(|time| f64::from(time.get::<second>()) as i64)
|
||||
},
|
||||
secs_until_empty: {
|
||||
let optional_time = battery.time_to_empty();
|
||||
optional_time.map(|time| f64::from(time.get::<second>()) as i64)
|
||||
},
|
||||
charge_percent: f64::from(battery.state_of_charge().get::<percent>()),
|
||||
power_consumption_rate_watts: f64::from(battery.energy_rate().get::<watt>()),
|
||||
health_percent: f64::from(battery.state_of_health().get::<percent>()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
38
src-tauri/src/data_harvester/cpu.rs
Normal file
@ -0,0 +1,38 @@
|
||||
//! Data collection for CPU usage and load average.
|
||||
//!
|
||||
//! For CPU usage, Linux, macOS, and Windows are handled by Heim, FreeBSD by sysinfo.
|
||||
//!
|
||||
//! For load average, macOS and Linux are supported through Heim, FreeBSD by sysinfo.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
} else if #[cfg(target_os = "freebsd")] {
|
||||
pub mod sysinfo;
|
||||
pub use self::sysinfo::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub type LoadAvgHarvest = [f32; 3];
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub enum CpuDataType {
|
||||
Avg,
|
||||
Cpu(usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CpuData {
|
||||
pub data_type: CpuDataType,
|
||||
pub cpu_usage: f64,
|
||||
}
|
||||
|
||||
pub type CpuHarvest = Vec<CpuData>;
|
||||
|
||||
pub type PastCpuWork = f64;
|
||||
pub type PastCpuTotal = f64;
|
||||
|
||||
pub type Point = (f64, f64);
|
||||
159
src-tauri/src/data_harvester/cpu/heim.rs
Normal file
@ -0,0 +1,159 @@
|
||||
//! CPU stats through heim.
|
||||
//! Supports macOS, Linux, and Windows.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use linux::*;
|
||||
} else if #[cfg(any(target_os = "macos", target_os = "windows"))] {
|
||||
pub mod windows_macos;
|
||||
pub use windows_macos::*;
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
pub mod unix;
|
||||
pub use unix::*;
|
||||
}
|
||||
}
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::data_harvester::cpu::{
|
||||
CpuData, CpuDataType, CpuHarvest, PastCpuTotal, PastCpuWork, Point,
|
||||
};
|
||||
|
||||
pub async fn get_cpu_data_list(
|
||||
show_average_cpu: bool,
|
||||
previous_cpu_times: &mut Vec<(PastCpuWork, PastCpuTotal)>,
|
||||
previous_average_cpu_time: &mut Option<(PastCpuWork, PastCpuTotal)>,
|
||||
) -> crate::error::Result<CpuHarvest> {
|
||||
fn calculate_cpu_usage_percentage(
|
||||
(previous_working_time, previous_total_time): Point,
|
||||
(current_working_time, current_total_time): Point,
|
||||
) -> f64 {
|
||||
((if current_working_time > previous_working_time {
|
||||
current_working_time - previous_working_time
|
||||
} else {
|
||||
0.0
|
||||
}) * 100.0)
|
||||
/ (if current_total_time > previous_total_time {
|
||||
current_total_time - previous_total_time
|
||||
} else {
|
||||
1.0
|
||||
})
|
||||
}
|
||||
|
||||
// Get all CPU times...
|
||||
let cpu_times = heim::cpu::times().await?;
|
||||
futures::pin_mut!(cpu_times);
|
||||
|
||||
let mut cpu_deque: VecDeque<CpuData> = if previous_cpu_times.is_empty() {
|
||||
// Must initialize ourselves. Use a very quick timeout to calculate an initial.
|
||||
futures_timer::Delay::new(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
let second_cpu_times = heim::cpu::times().await?;
|
||||
futures::pin_mut!(second_cpu_times);
|
||||
|
||||
let mut new_cpu_times: Vec<(PastCpuWork, PastCpuTotal)> = Vec::new();
|
||||
let mut cpu_deque: VecDeque<CpuData> = VecDeque::new();
|
||||
let mut collected_zip = cpu_times.zip(second_cpu_times).enumerate(); // Gotta move it here, can't on while line.
|
||||
|
||||
while let Some((itx, (past, present))) = collected_zip.next().await {
|
||||
if let (Ok(past), Ok(present)) = (past, present) {
|
||||
let present_times = convert_cpu_times(&present);
|
||||
new_cpu_times.push(present_times);
|
||||
cpu_deque.push_back(CpuData {
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: calculate_cpu_usage_percentage(
|
||||
convert_cpu_times(&past),
|
||||
present_times,
|
||||
),
|
||||
});
|
||||
} else {
|
||||
new_cpu_times.push((0.0, 0.0));
|
||||
cpu_deque.push_back(CpuData {
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: 0.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
*previous_cpu_times = new_cpu_times;
|
||||
cpu_deque
|
||||
} else {
|
||||
let (new_cpu_times, cpu_deque): (Vec<(PastCpuWork, PastCpuTotal)>, VecDeque<CpuData>) =
|
||||
cpu_times
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.iter()
|
||||
.zip(&*previous_cpu_times)
|
||||
.enumerate()
|
||||
.map(|(itx, (current_cpu, (past_cpu_work, past_cpu_total)))| {
|
||||
if let Ok(cpu_time) = current_cpu {
|
||||
let present_times = convert_cpu_times(cpu_time);
|
||||
|
||||
(
|
||||
present_times,
|
||||
CpuData {
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: calculate_cpu_usage_percentage(
|
||||
(*past_cpu_work, *past_cpu_total),
|
||||
present_times,
|
||||
),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
(*past_cpu_work, *past_cpu_total),
|
||||
CpuData {
|
||||
data_type: CpuDataType::Cpu(itx),
|
||||
cpu_usage: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.unzip();
|
||||
|
||||
*previous_cpu_times = new_cpu_times;
|
||||
cpu_deque
|
||||
};
|
||||
|
||||
// Get average CPU if needed... and slap it at the top
|
||||
if show_average_cpu {
|
||||
let cpu_time = heim::cpu::time().await?;
|
||||
|
||||
let (cpu_usage, new_average_cpu_time) = if let Some((past_cpu_work, past_cpu_total)) =
|
||||
previous_average_cpu_time
|
||||
{
|
||||
let present_times = convert_cpu_times(&cpu_time);
|
||||
(
|
||||
calculate_cpu_usage_percentage((*past_cpu_work, *past_cpu_total), present_times),
|
||||
present_times,
|
||||
)
|
||||
} else {
|
||||
// Again, we need to do a quick timeout...
|
||||
futures_timer::Delay::new(std::time::Duration::from_millis(100)).await;
|
||||
let second_cpu_time = heim::cpu::time().await?;
|
||||
|
||||
let present_times = convert_cpu_times(&second_cpu_time);
|
||||
(
|
||||
calculate_cpu_usage_percentage(convert_cpu_times(&cpu_time), present_times),
|
||||
present_times,
|
||||
)
|
||||
};
|
||||
|
||||
*previous_average_cpu_time = Some(new_average_cpu_time);
|
||||
cpu_deque.push_front(CpuData {
|
||||
data_type: CpuDataType::Avg,
|
||||
cpu_usage,
|
||||
})
|
||||
}
|
||||
|
||||
// Ok(Vec::from(cpu_deque.drain(0..3).collect::<Vec<_>>())) // For artificially limiting the CPU results
|
||||
|
||||
Ok(Vec::from(cpu_deque))
|
||||
}
|
||||
19
src-tauri/src/data_harvester/cpu/heim/linux.rs
Normal file
@ -0,0 +1,19 @@
|
||||
//! Linux-specific functions regarding CPU usage.
|
||||
|
||||
use heim::cpu::os::linux::CpuTimeExt;
|
||||
|
||||
use crate::data_harvester::cpu::Point;
|
||||
|
||||
pub fn convert_cpu_times(cpu_time: &heim::cpu::CpuTime) -> Point {
|
||||
let working_time: f64 = (cpu_time.user()
|
||||
+ cpu_time.nice()
|
||||
+ cpu_time.system()
|
||||
+ cpu_time.irq()
|
||||
+ cpu_time.soft_irq()
|
||||
+ cpu_time.steal())
|
||||
.get::<heim::units::time::second>();
|
||||
(
|
||||
working_time,
|
||||
working_time + (cpu_time.idle() + cpu_time.io_wait()).get::<heim::units::time::second>(),
|
||||
)
|
||||
}
|
||||
13
src-tauri/src/data_harvester/cpu/heim/unix.rs
Normal file
@ -0,0 +1,13 @@
|
||||
//! Unix-specific functions regarding CPU usage.
|
||||
|
||||
use crate::data_harvester::cpu::LoadAvgHarvest;
|
||||
|
||||
pub async fn get_load_avg() -> crate::error::Result<LoadAvgHarvest> {
|
||||
let (one, five, fifteen) = heim::cpu::os::unix::loadavg().await?;
|
||||
|
||||
Ok([
|
||||
one.get::<heim::units::ratio::ratio>(),
|
||||
five.get::<heim::units::ratio::ratio>(),
|
||||
fifteen.get::<heim::units::ratio::ratio>(),
|
||||
])
|
||||
}
|
||||
12
src-tauri/src/data_harvester/cpu/heim/windows_macos.rs
Normal file
@ -0,0 +1,12 @@
|
||||
//! Windows and macOS-specific functions regarding CPU usage.
|
||||
|
||||
use crate::data_harvester::cpu::Point;
|
||||
|
||||
pub fn convert_cpu_times(cpu_time: &heim::cpu::CpuTime) -> Point {
|
||||
let working_time: f64 =
|
||||
(cpu_time.user() + cpu_time.system()).get::<heim::units::time::second>();
|
||||
(
|
||||
working_time,
|
||||
working_time + cpu_time.idle().get::<heim::units::time::second>(),
|
||||
)
|
||||
}
|
||||
44
src-tauri/src/data_harvester/cpu/sysinfo.rs
Normal file
@ -0,0 +1,44 @@
|
||||
//! CPU stats through sysinfo.
|
||||
//! Supports FreeBSD.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use sysinfo::{CpuExt, LoadAvg, System, SystemExt};
|
||||
|
||||
use super::{CpuData, CpuDataType, CpuHarvest, PastCpuTotal, PastCpuWork};
|
||||
use crate::data_harvester::cpu::LoadAvgHarvest;
|
||||
|
||||
pub async fn get_cpu_data_list(
|
||||
sys: &sysinfo::System,
|
||||
show_average_cpu: bool,
|
||||
_previous_cpu_times: &mut [(PastCpuWork, PastCpuTotal)],
|
||||
_previous_average_cpu_time: &mut Option<(PastCpuWork, PastCpuTotal)>,
|
||||
) -> crate::error::Result<CpuHarvest> {
|
||||
let mut cpu_deque: VecDeque<_> = sys
|
||||
.cpus()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, cpu)| CpuData {
|
||||
data_type: CpuDataType::Cpu(i),
|
||||
cpu_usage: cpu.cpu_usage() as f64,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if show_average_cpu {
|
||||
let cpu = sys.global_cpu_info();
|
||||
|
||||
cpu_deque.push_front(CpuData {
|
||||
data_type: CpuDataType::Avg,
|
||||
cpu_usage: cpu.cpu_usage() as f64,
|
||||
})
|
||||
}
|
||||
|
||||
Ok(Vec::from(cpu_deque))
|
||||
}
|
||||
|
||||
pub async fn get_load_avg() -> crate::error::Result<LoadAvgHarvest> {
|
||||
let sys = System::new();
|
||||
let LoadAvg { one, five, fifteen } = sys.load_average();
|
||||
|
||||
Ok([one as f32, five as f32, fifteen as f32])
|
||||
}
|
||||
33
src-tauri/src/data_harvester/disks.rs
Normal file
@ -0,0 +1,33 @@
|
||||
//! Data collection for disks (IO, usage, space, etc.).
|
||||
//!
|
||||
//! For Linux, macOS, and Windows, this is handled by heim. For FreeBSD there is a custom
|
||||
//! implementation.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
} else if #[cfg(target_os = "freebsd")] {
|
||||
pub mod freebsd;
|
||||
pub use self::freebsd::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct DiskHarvest {
|
||||
pub name: String,
|
||||
pub mount_point: String,
|
||||
pub free_space: Option<u64>,
|
||||
pub used_space: Option<u64>,
|
||||
pub total_space: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct IoData {
|
||||
pub read_bytes: u64,
|
||||
pub write_bytes: u64,
|
||||
}
|
||||
|
||||
pub type IoHarvest = std::collections::HashMap<String, Option<IoData>>;
|
||||
107
src-tauri/src/data_harvester/disks/freebsd.rs
Normal file
@ -0,0 +1,107 @@
|
||||
//! Disk stats for FreeBSD.
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{DiskHarvest, IoHarvest};
|
||||
use crate::app::Filter;
|
||||
use crate::data_harvester::deserialize_xo;
|
||||
|
||||
#[derive(Deserialize, Debug, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct StorageSystemInformation {
|
||||
filesystem: Vec<FileSystem>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct FileSystem {
|
||||
name: String,
|
||||
total_blocks: u64,
|
||||
used_blocks: u64,
|
||||
available_blocks: u64,
|
||||
mounted_on: String,
|
||||
}
|
||||
|
||||
pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result<Option<IoHarvest>> {
|
||||
if !actually_get {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let io_harvest = get_disk_info().map(|storage_system_information| {
|
||||
storage_system_information
|
||||
.filesystem
|
||||
.into_iter()
|
||||
.map(|disk| (disk.name, None))
|
||||
.collect()
|
||||
})?;
|
||||
Ok(Some(io_harvest))
|
||||
}
|
||||
|
||||
pub async fn get_disk_usage(
|
||||
actually_get: bool,
|
||||
disk_filter: &Option<Filter>,
|
||||
mount_filter: &Option<Filter>,
|
||||
) -> crate::utils::error::Result<Option<Vec<DiskHarvest>>> {
|
||||
if !actually_get {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let vec_disks: Vec<DiskHarvest> = get_disk_info().map(|storage_system_information| {
|
||||
storage_system_information
|
||||
.filesystem
|
||||
.into_iter()
|
||||
.filter_map(|disk| {
|
||||
// Precedence ordering in the case where name and mount filters disagree, "allow"
|
||||
// takes precedence over "deny".
|
||||
//
|
||||
// For implementation, we do this as follows:
|
||||
//
|
||||
// 1. Is the entry allowed through any filter? That is, does it match an entry in a
|
||||
// filter where `is_list_ignored` is `false`? If so, we always keep this entry.
|
||||
// 2. Is the entry denied through any filter? That is, does it match an entry in a
|
||||
// filter where `is_list_ignored` is `true`? If so, we always deny this entry.
|
||||
// 3. Anything else is allowed.
|
||||
let filter_check_map =
|
||||
[(disk_filter, &disk.name), (mount_filter, &disk.mounted_on)];
|
||||
if matches_allow_list(filter_check_map.as_slice())
|
||||
|| !matches_ignore_list(filter_check_map.as_slice())
|
||||
{
|
||||
Some(DiskHarvest {
|
||||
free_space: Some(disk.available_blocks * 1024),
|
||||
used_space: Some(disk.used_blocks * 1024),
|
||||
total_space: Some(disk.total_blocks * 1024),
|
||||
mount_point: disk.mounted_on,
|
||||
name: disk.name,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
|
||||
Ok(Some(vec_disks))
|
||||
}
|
||||
|
||||
fn matches_allow_list(filter_check_map: &[(&Option<Filter>, &String)]) -> bool {
|
||||
filter_check_map.iter().any(|(filter, text)| match filter {
|
||||
Some(f) if !f.is_list_ignored => f.list.iter().any(|r| r.is_match(text)),
|
||||
Some(_) | None => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn matches_ignore_list(filter_check_map: &[(&Option<Filter>, &String)]) -> bool {
|
||||
filter_check_map.iter().any(|(filter, text)| match filter {
|
||||
Some(f) if f.is_list_ignored => f.list.iter().any(|r| r.is_match(text)),
|
||||
Some(_) | None => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_disk_info() -> io::Result<StorageSystemInformation> {
|
||||
let output = std::process::Command::new("df")
|
||||
.args(["--libxo", "json", "-k", "-t", "ufs,msdosfs,zfs"])
|
||||
.output()?;
|
||||
deserialize_xo("storage-system-information", &output.stdout)
|
||||
}
|
||||
83
src-tauri/src/data_harvester/disks/heim.rs
Normal file
@ -0,0 +1,83 @@
|
||||
//! Disk stats through heim.
|
||||
//! Supports macOS, Linux, and Windows.
|
||||
|
||||
use crate::data_harvester::disks::{DiskHarvest, IoData, IoHarvest};
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use linux::*;
|
||||
} else if #[cfg(any(target_os = "macos", target_os = "windows"))] {
|
||||
pub mod windows_macos;
|
||||
pub use windows_macos::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_io_usage() -> crate::utils::error::Result<Option<IoHarvest>> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut io_hash: std::collections::HashMap<String, Option<IoData>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
let counter_stream = heim::disk::io_counters().await?;
|
||||
futures::pin_mut!(counter_stream);
|
||||
|
||||
while let Some(io) = counter_stream.next().await {
|
||||
if let Ok(io) = io {
|
||||
let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable");
|
||||
|
||||
io_hash.insert(
|
||||
mount_point.to_string(),
|
||||
Some(IoData {
|
||||
read_bytes: io.read_bytes().get::<heim::units::information::byte>(),
|
||||
write_bytes: io.write_bytes().get::<heim::units::information::byte>(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(io_hash))
|
||||
}
|
||||
|
||||
pub async fn get_disk_usage() -> crate::utils::error::Result<Option<Vec<DiskHarvest>>> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let mut vec_disks: Vec<DiskHarvest> = Vec::new();
|
||||
let partitions_stream = heim::disk::partitions_physical().await?;
|
||||
futures::pin_mut!(partitions_stream);
|
||||
|
||||
while let Some(part) = partitions_stream.next().await {
|
||||
if let Ok(partition) = part {
|
||||
let name = get_device_name(&partition);
|
||||
|
||||
let mount_point = (partition
|
||||
.mount_point()
|
||||
.to_str()
|
||||
.unwrap_or("Name Unavailable"))
|
||||
.to_string();
|
||||
|
||||
// The usage line can fail in some cases (for example, if you use Void Linux + LUKS,
|
||||
// see https://github.com/ClementTsang/bottom/issues/419 for details). As such, check
|
||||
// it like this instead.
|
||||
if let Ok(usage) = heim::disk::usage(partition.mount_point()).await {
|
||||
vec_disks.push(DiskHarvest {
|
||||
free_space: Some(usage.free().get::<heim::units::information::byte>()),
|
||||
used_space: Some(usage.used().get::<heim::units::information::byte>()),
|
||||
total_space: Some(usage.total().get::<heim::units::information::byte>()),
|
||||
mount_point,
|
||||
name,
|
||||
});
|
||||
} else {
|
||||
vec_disks.push(DiskHarvest {
|
||||
free_space: None,
|
||||
used_space: None,
|
||||
total_space: None,
|
||||
mount_point,
|
||||
name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(vec_disks))
|
||||
}
|
||||
34
src-tauri/src/data_harvester/disks/heim/linux.rs
Normal file
@ -0,0 +1,34 @@
|
||||
//! Linux-specific things for Heim disk data collection.
|
||||
|
||||
use heim::disk::Partition;
|
||||
|
||||
pub fn get_device_name(partition: &Partition) -> String {
|
||||
if let Some(device) = partition.device() {
|
||||
// See if this disk is actually mounted elsewhere on Linux...
|
||||
// This is a workaround to properly map I/O in some cases (i.e. disk encryption), see
|
||||
// https://github.com/ClementTsang/bottom/issues/419
|
||||
if let Ok(path) = std::fs::read_link(device) {
|
||||
if path.is_absolute() {
|
||||
path.into_os_string()
|
||||
} else {
|
||||
let mut combined_path = std::path::PathBuf::new();
|
||||
combined_path.push(device);
|
||||
combined_path.pop(); // Pop the current file...
|
||||
combined_path.push(path);
|
||||
|
||||
if let Ok(canon_path) = std::fs::canonicalize(combined_path) {
|
||||
// Resolve the local path into an absolute one...
|
||||
canon_path.into_os_string()
|
||||
} else {
|
||||
device.to_os_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
device.to_os_string()
|
||||
}
|
||||
.into_string()
|
||||
.unwrap_or_else(|_| "Name Unavailable".to_string())
|
||||
} else {
|
||||
"Name Unavailable".to_string()
|
||||
}
|
||||
}
|
||||
14
src-tauri/src/data_harvester/disks/heim/windows_macos.rs
Normal file
@ -0,0 +1,14 @@
|
||||
//! macOS and Windows-specific things for Heim disk data collection.
|
||||
|
||||
use heim::disk::Partition;
|
||||
|
||||
pub fn get_device_name(partition: &Partition) -> String {
|
||||
if let Some(device) = partition.device() {
|
||||
device
|
||||
.to_os_string()
|
||||
.into_string()
|
||||
.unwrap_or_else(|_| "Name Unavailable".to_string())
|
||||
} else {
|
||||
"Name Unavailable".to_string()
|
||||
}
|
||||
}
|
||||
10
src-tauri/src/data_harvester/memory.rs
Normal file
@ -0,0 +1,10 @@
|
||||
//! Data collection for memory.
|
||||
//!
|
||||
//! For Linux, macOS, and Windows, this is handled by Heim. On FreeBSD it is handled by sysinfo.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod general;
|
||||
pub use self::general::*;
|
||||
}
|
||||
}
|
||||
28
src-tauri/src/data_harvester/memory/general.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use serde::Serialize;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
} else if #[cfg(target_os = "freebsd")] {
|
||||
pub mod sysinfo;
|
||||
pub use self::sysinfo::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct MemHarvest {
|
||||
pub mem_total_in_kib: u64,
|
||||
pub mem_used_in_kib: u64,
|
||||
pub use_percent: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MemCollect {
|
||||
pub ram: crate::utils::error::Result<Option<MemHarvest>>,
|
||||
pub swap: crate::utils::error::Result<Option<MemHarvest>>,
|
||||
#[cfg(feature = "zfs")]
|
||||
pub arc: crate::utils::error::Result<Option<MemHarvest>>,
|
||||
#[cfg(feature = "gpu")]
|
||||
pub gpus: crate::utils::error::Result<Option<Vec<(String, MemHarvest)>>>,
|
||||
}
|
||||
279
src-tauri/src/data_harvester/memory/general/heim.rs
Normal file
@ -0,0 +1,279 @@
|
||||
//! Data collection for memory via heim.
|
||||
|
||||
use crate::data_harvester::memory::{MemCollect, MemHarvest};
|
||||
|
||||
pub async fn get_mem_data() -> MemCollect {
|
||||
MemCollect {
|
||||
ram: get_ram_data().await,
|
||||
swap: get_swap_data().await,
|
||||
#[cfg(feature = "zfs")]
|
||||
arc: get_arc_data().await,
|
||||
#[cfg(feature = "gpu")]
|
||||
gpus: get_gpu_data().await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_ram_data() -> crate::utils::error::Result<Option<MemHarvest>> {
|
||||
let (mem_total_in_kib, mem_used_in_kib) = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// TODO: [OPT] is this efficient?
|
||||
use smol::fs::read_to_string;
|
||||
let meminfo = read_to_string("/proc/meminfo").await?;
|
||||
|
||||
// All values are in KiB by default.
|
||||
let mut mem_total = 0;
|
||||
let mut cached = 0;
|
||||
let mut s_reclaimable = 0;
|
||||
let mut shmem = 0;
|
||||
let mut buffers = 0;
|
||||
let mut mem_free = 0;
|
||||
|
||||
let mut keys_read: u8 = 0;
|
||||
const TOTAL_KEYS_NEEDED: u8 = 6;
|
||||
|
||||
for line in meminfo.lines() {
|
||||
if let Some((label, value)) = line.split_once(':') {
|
||||
let to_write = match label {
|
||||
"MemTotal" => &mut mem_total,
|
||||
"MemFree" => &mut mem_free,
|
||||
"Buffers" => &mut buffers,
|
||||
"Cached" => &mut cached,
|
||||
"Shmem" => &mut shmem,
|
||||
"SReclaimable" => &mut s_reclaimable,
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((number, _unit)) = value.trim_start().split_once(' ') {
|
||||
// Parse the value, remember it's in KiB!
|
||||
if let Ok(number) = number.parse::<u64>() {
|
||||
*to_write = number;
|
||||
|
||||
// We only need a few keys, so we can bail early.
|
||||
keys_read += 1;
|
||||
if keys_read == TOTAL_KEYS_NEEDED {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Let's preface this by saying that memory usage calculations are... not straightforward.
|
||||
// There are conflicting implementations everywhere.
|
||||
//
|
||||
// Now that we've added this preface (mainly for future reference), the current implementation below for usage
|
||||
// is based on htop's calculation formula. See
|
||||
// https://github.com/htop-dev/htop/blob/976c6123f41492aaf613b9d172eef1842fb7b0a3/linux/LinuxProcessList.c#L1584
|
||||
// for implementation details as of writing.
|
||||
//
|
||||
// Another implementation, commonly used in other things, is to skip the shmem part of the calculation,
|
||||
// which matches gopsutil and stuff like free.
|
||||
|
||||
let total = mem_total;
|
||||
let cached_mem = cached + s_reclaimable - shmem;
|
||||
let used_diff = mem_free + cached_mem + buffers;
|
||||
let used = if total >= used_diff {
|
||||
total - used_diff
|
||||
} else {
|
||||
total - mem_free
|
||||
};
|
||||
|
||||
(total, used)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let memory = heim::memory::memory().await?;
|
||||
|
||||
use heim::memory::os::macos::MemoryExt;
|
||||
use heim::units::information::kibibyte;
|
||||
(
|
||||
memory.total().get::<kibibyte>(),
|
||||
memory.active().get::<kibibyte>() + memory.wire().get::<kibibyte>(),
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let memory = heim::memory::memory().await?;
|
||||
|
||||
use heim::units::information::kibibyte;
|
||||
let mem_total_in_kib = memory.total().get::<kibibyte>();
|
||||
(
|
||||
mem_total_in_kib,
|
||||
mem_total_in_kib - memory.available().get::<kibibyte>(),
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
let mut s = System::new();
|
||||
s.refresh_memory();
|
||||
(s.total_memory(), s.used_memory())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_swap_data() -> crate::utils::error::Result<Option<MemHarvest>> {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
|
||||
let memory = heim::memory::swap().await?;
|
||||
#[cfg(target_os = "freebsd")]
|
||||
let mut memory = System::new();
|
||||
|
||||
let (mem_total_in_kib, mem_used_in_kib) = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Similar story to above - heim parses this information incorrectly as far as I can tell, so kilobytes = kibibytes here.
|
||||
use heim::units::information::kilobyte;
|
||||
(
|
||||
memory.total().get::<kilobyte>(),
|
||||
memory.used().get::<kilobyte>(),
|
||||
)
|
||||
}
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
{
|
||||
use heim::units::information::kibibyte;
|
||||
(
|
||||
memory.total().get::<kibibyte>(),
|
||||
memory.used().get::<kibibyte>(),
|
||||
)
|
||||
}
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
memory.refresh_memory();
|
||||
(memory.total_swap(), memory.used_swap())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
pub async fn get_arc_data() -> crate::utils::error::Result<Option<MemHarvest>> {
|
||||
let (mem_total_in_kib, mem_used_in_kib) = {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mut mem_arc = 0;
|
||||
let mut mem_total = 0;
|
||||
let mut zfs_keys_read: u8 = 0;
|
||||
const ZFS_KEYS_NEEDED: u8 = 2;
|
||||
use smol::fs::read_to_string;
|
||||
let arcinfo = read_to_string("/proc/spl/kstat/zfs/arcstats").await?;
|
||||
for line in arcinfo.lines() {
|
||||
if let Some((label, value)) = line.split_once(' ') {
|
||||
let to_write = match label {
|
||||
"size" => &mut mem_arc,
|
||||
"memory_all_bytes" => &mut mem_total,
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((_type, number)) = value.trim_start().rsplit_once(' ') {
|
||||
// Parse the value, remember it's in bytes!
|
||||
if let Ok(number) = number.parse::<u64>() {
|
||||
*to_write = number;
|
||||
// We only need a few keys, so we can bail early.
|
||||
zfs_keys_read += 1;
|
||||
if zfs_keys_read == ZFS_KEYS_NEEDED {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(mem_total / 1024, mem_arc / 1024)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
use sysctl::Sysctl;
|
||||
if let (Ok(mem_arc_value), Ok(mem_sys_value)) = (
|
||||
sysctl::Ctl::new("kstat.zfs.misc.arcstats.size"),
|
||||
sysctl::Ctl::new("hw.physmem"),
|
||||
) {
|
||||
if let (Ok(sysctl::CtlValue::U64(arc)), Ok(sysctl::CtlValue::Ulong(mem))) =
|
||||
(mem_arc_value.value(), mem_sys_value.value())
|
||||
{
|
||||
(mem / 1024, arc / 1024)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
(0, 0)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
(0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
pub async fn get_gpu_data() -> crate::utils::error::Result<Option<Vec<(String, MemHarvest)>>> {
|
||||
use crate::data_harvester::nvidia::NVML_DATA;
|
||||
if let Ok(nvml) = &*NVML_DATA {
|
||||
if let Ok(ngpu) = nvml.device_count() {
|
||||
let mut results = Vec::with_capacity(ngpu as usize);
|
||||
for i in 0..ngpu {
|
||||
if let Ok(device) = nvml.device_by_index(i) {
|
||||
if let (Ok(name), Ok(mem)) = (device.name(), device.memory_info()) {
|
||||
// add device memory in bytes
|
||||
let mem_total_in_kib = mem.total / 1024;
|
||||
let mem_used_in_kib = mem.used / 1024;
|
||||
results.push((
|
||||
name,
|
||||
MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(results))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
128
src-tauri/src/data_harvester/memory/general/sysinfo.rs
Normal file
@ -0,0 +1,128 @@
|
||||
//! Data collection for memory via sysinfo.
|
||||
|
||||
use sysinfo::{System, SystemExt};
|
||||
|
||||
use crate::data_harvester::memory::{MemCollect, MemHarvest};
|
||||
|
||||
pub async fn get_mem_data(sys: &System, actually_get: bool, _get_gpu: bool) -> MemCollect {
|
||||
if !actually_get {
|
||||
MemCollect {
|
||||
ram: Ok(None),
|
||||
swap: Ok(None),
|
||||
#[cfg(feature = "zfs")]
|
||||
arc: Ok(None),
|
||||
#[cfg(feature = "gpu")]
|
||||
gpus: Ok(None),
|
||||
}
|
||||
} else {
|
||||
MemCollect {
|
||||
ram: get_ram_data(sys).await,
|
||||
swap: get_swap_data(sys).await,
|
||||
#[cfg(feature = "zfs")]
|
||||
arc: get_arc_data().await,
|
||||
#[cfg(feature = "gpu")]
|
||||
gpus: if _get_gpu {
|
||||
get_gpu_data().await
|
||||
} else {
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_ram_data(sys: &System) -> crate::utils::error::Result<Option<MemHarvest>> {
|
||||
let (mem_total_in_kib, mem_used_in_kib) = (sys.total_memory() / 1024, sys.used_memory() / 1024);
|
||||
|
||||
Ok(Some(MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_swap_data(sys: &System) -> crate::utils::error::Result<Option<MemHarvest>> {
|
||||
let (mem_total_in_kib, mem_used_in_kib) = (sys.total_swap() / 1024, sys.used_swap() / 1024);
|
||||
|
||||
Ok(Some(MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "zfs")]
|
||||
pub async fn get_arc_data() -> crate::utils::error::Result<Option<MemHarvest>> {
|
||||
let (mem_total_in_kib, mem_used_in_kib) = {
|
||||
#[cfg(target_os = "freebsd")]
|
||||
{
|
||||
use sysctl::Sysctl;
|
||||
if let (Ok(mem_arc_value), Ok(mem_sys_value)) = (
|
||||
sysctl::Ctl::new("kstat.zfs.misc.arcstats.size"),
|
||||
sysctl::Ctl::new("hw.physmem"),
|
||||
) {
|
||||
if let (Ok(sysctl::CtlValue::U64(arc)), Ok(sysctl::CtlValue::Ulong(mem))) =
|
||||
(mem_arc_value.value(), mem_sys_value.value())
|
||||
{
|
||||
(mem / 1024, arc / 1024)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Some(MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
pub async fn get_gpu_data() -> crate::utils::error::Result<Option<Vec<(String, MemHarvest)>>> {
|
||||
use crate::data_harvester::nvidia::NVML_DATA;
|
||||
if let Ok(nvml) = &*NVML_DATA {
|
||||
if let Ok(ngpu) = nvml.device_count() {
|
||||
let mut results = Vec::with_capacity(ngpu as usize);
|
||||
for i in 0..ngpu {
|
||||
if let Ok(device) = nvml.device_by_index(i) {
|
||||
if let (Ok(name), Ok(mem)) = (device.name(), device.memory_info()) {
|
||||
// add device memory in bytes
|
||||
let mem_total_in_kib = mem.total / 1024;
|
||||
let mem_used_in_kib = mem.used / 1024;
|
||||
results.push((
|
||||
name,
|
||||
MemHarvest {
|
||||
mem_total_in_kib,
|
||||
mem_used_in_kib,
|
||||
use_percent: if mem_total_in_kib == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0)
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(results))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
32
src-tauri/src/data_harvester/network.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! Data collection for network usage/IO.
|
||||
//!
|
||||
//! For Linux and macOS, this is handled by Heim.
|
||||
//! For Windows, this is handled by sysinfo.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "macos"))] {
|
||||
pub mod heim;
|
||||
pub use self::heim::*;
|
||||
} else if #[cfg(any(target_os = "freebsd", target_os = "windows"))] {
|
||||
pub mod sysinfo;
|
||||
pub use self::sysinfo::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Serialize)]
|
||||
/// All units in bits.
|
||||
pub struct NetworkHarvest {
|
||||
pub rx: u64,
|
||||
pub tx: u64,
|
||||
pub total_rx: u64,
|
||||
pub total_tx: u64,
|
||||
}
|
||||
|
||||
impl NetworkHarvest {
|
||||
pub fn first_run_cleanup(&mut self) {
|
||||
self.rx = 0;
|
||||
self.tx = 0;
|
||||
}
|
||||
}
|
||||
51
src-tauri/src/data_harvester/network/heim.rs
Normal file
@ -0,0 +1,51 @@
|
||||
//! Gets network data via heim.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use super::NetworkHarvest;
|
||||
|
||||
// TODO: Eventually make it so that this thing also takes individual usage into account, so we can show per-interface!
|
||||
pub async fn get_network_data(
|
||||
prev_net_access_time: Instant,
|
||||
prev_net_rx: &mut u64,
|
||||
prev_net_tx: &mut u64,
|
||||
curr_time: Instant,
|
||||
) -> crate::utils::error::Result<Option<NetworkHarvest>> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let io_data = heim::net::io_counters().await?;
|
||||
futures::pin_mut!(io_data);
|
||||
let mut total_rx: u64 = 0;
|
||||
let mut total_tx: u64 = 0;
|
||||
|
||||
while let Some(io) = io_data.next().await {
|
||||
if let Ok(io) = io {
|
||||
// TODO: Use bytes as the default instead, perhaps?
|
||||
// Since you might have to do a double conversion (bytes -> bits -> bytes) in some cases;
|
||||
// but if you stick to bytes, then in the bytes, case, you do no conversion, and in the bits case,
|
||||
// you only do one conversion...
|
||||
total_rx += io.bytes_recv().get::<heim::units::information::bit>();
|
||||
total_tx += io.bytes_sent().get::<heim::units::information::bit>();
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64();
|
||||
|
||||
let (rx, tx) = if elapsed_time == 0.0 {
|
||||
(0, 0)
|
||||
} else {
|
||||
(
|
||||
((total_rx.saturating_sub(*prev_net_rx)) as f64 / elapsed_time) as u64,
|
||||
((total_tx.saturating_sub(*prev_net_tx)) as f64 / elapsed_time) as u64,
|
||||
)
|
||||
};
|
||||
|
||||
*prev_net_rx = total_rx;
|
||||
*prev_net_tx = total_tx;
|
||||
Ok(Some(NetworkHarvest {
|
||||
rx,
|
||||
tx,
|
||||
total_rx,
|
||||
total_tx,
|
||||
}))
|
||||
}
|
||||
44
src-tauri/src/data_harvester/network/sysinfo.rs
Normal file
@ -0,0 +1,44 @@
|
||||
//! Gets network data via sysinfo.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use super::NetworkHarvest;
|
||||
|
||||
pub async fn get_network_data(
|
||||
sys: &sysinfo::System,
|
||||
prev_net_access_time: Instant,
|
||||
prev_net_rx: &mut u64,
|
||||
prev_net_tx: &mut u64,
|
||||
curr_time: Instant,
|
||||
) -> crate::utils::error::Result<Option<NetworkHarvest>> {
|
||||
use sysinfo::{NetworkExt, SystemExt};
|
||||
|
||||
let mut total_rx: u64 = 0;
|
||||
let mut total_tx: u64 = 0;
|
||||
|
||||
let networks = sys.networks();
|
||||
for (name, network) in networks {
|
||||
total_rx += network.total_received() * 8;
|
||||
total_tx += network.total_transmitted() * 8;
|
||||
}
|
||||
|
||||
let elapsed_time = curr_time.duration_since(prev_net_access_time).as_secs_f64();
|
||||
|
||||
let (rx, tx) = if elapsed_time == 0.0 {
|
||||
(0, 0)
|
||||
} else {
|
||||
(
|
||||
((total_rx.saturating_sub(*prev_net_rx)) as f64 / elapsed_time) as u64,
|
||||
((total_tx.saturating_sub(*prev_net_tx)) as f64 / elapsed_time) as u64,
|
||||
)
|
||||
};
|
||||
|
||||
*prev_net_rx = total_rx;
|
||||
*prev_net_tx = total_tx;
|
||||
Ok(Some(NetworkHarvest {
|
||||
rx,
|
||||
tx,
|
||||
total_rx,
|
||||
total_tx,
|
||||
}))
|
||||
}
|
||||
3
src-tauri/src/data_harvester/nvidia.rs
Normal file
@ -0,0 +1,3 @@
|
||||
use nvml_wrapper::{error::NvmlError, Nvml};
|
||||
use once_cell::sync::Lazy;
|
||||
pub static NVML_DATA: Lazy<Result<Nvml, NvmlError>> = Lazy::new(Nvml::init);
|
||||
83
src-tauri/src/data_harvester/processes.rs
Normal file
@ -0,0 +1,83 @@
|
||||
//! Data collection for processes.
|
||||
//!
|
||||
//! For Linux, this is handled by a custom set of functions.
|
||||
//! For Windows and macOS, this is handled by sysinfo.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use self::linux::*;
|
||||
} else if #[cfg(target_os = "macos")] {
|
||||
pub mod macos;
|
||||
mod macos_freebsd;
|
||||
pub use self::macos::*;
|
||||
} else if #[cfg(target_os = "windows")] {
|
||||
pub mod windows;
|
||||
pub use self::windows::*;
|
||||
} else if #[cfg(target_os = "freebsd")] {
|
||||
pub mod freebsd;
|
||||
mod macos_freebsd;
|
||||
pub use self::freebsd::*;
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
pub mod unix;
|
||||
pub use self::unix::*;
|
||||
}
|
||||
}
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Pid;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct ProcessHarvest {
|
||||
/// The pid of the process.
|
||||
pub pid: Pid,
|
||||
|
||||
/// The parent PID of the process. Remember, parent_pid 0 is root.
|
||||
pub parent_pid: Option<Pid>,
|
||||
|
||||
/// CPU usage as a percentage.
|
||||
pub cpu_usage_percent: f64,
|
||||
|
||||
/// Memory usage as a percentage.
|
||||
pub mem_usage_percent: f64,
|
||||
|
||||
/// Memory usage as bytes.
|
||||
pub mem_usage_bytes: u64,
|
||||
|
||||
/// The name of the process.
|
||||
pub name: String,
|
||||
|
||||
/// The exact command for the process.
|
||||
pub command: String,
|
||||
|
||||
/// Bytes read per second.
|
||||
pub read_bytes_per_sec: u64,
|
||||
|
||||
/// Bytes written per second.
|
||||
pub write_bytes_per_sec: u64,
|
||||
|
||||
/// The total number of bytes read by the process.
|
||||
pub total_read_bytes: u64,
|
||||
|
||||
/// The total number of bytes written by the process.
|
||||
pub total_write_bytes: u64,
|
||||
|
||||
/// The current state of the process (e.g. zombie, asleep)
|
||||
pub process_state: (String, char),
|
||||
|
||||
/// This is the *effective* user ID of the process. This is only used on Unix platforms.
|
||||
#[cfg(target_family = "unix")]
|
||||
pub uid: Option<libc::uid_t>,
|
||||
|
||||
/// This is the process' user. This is only used on Unix platforms.
|
||||
#[cfg(target_family = "unix")]
|
||||
pub user: std::borrow::Cow<'static, str>,
|
||||
// TODO: Additional fields
|
||||
// pub rss_kb: u64,
|
||||
// pub virt_kb: u64,
|
||||
}
|
||||
76
src-tauri/src/data_harvester/processes/freebsd.rs
Normal file
@ -0,0 +1,76 @@
|
||||
//! Process data collection for FreeBSD. Uses sysinfo.
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use sysinfo::System;
|
||||
|
||||
use super::ProcessHarvest;
|
||||
use crate::data_harvester::deserialize_xo;
|
||||
use crate::data_harvester::processes::UserTable;
|
||||
|
||||
#[derive(Deserialize, Debug, Default)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct ProcessInformation {
|
||||
process: Vec<ProcessRow>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct ProcessRow {
|
||||
#[serde(deserialize_with = "pid")]
|
||||
pid: i32,
|
||||
#[serde(deserialize_with = "percent_cpu")]
|
||||
percent_cpu: f64,
|
||||
}
|
||||
|
||||
pub fn get_process_data(
|
||||
sys: &System,
|
||||
use_current_cpu_total: bool,
|
||||
unnormalized_cpu: bool,
|
||||
mem_total_kb: u64,
|
||||
user_table: &mut UserTable,
|
||||
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
||||
super::macos_freebsd::get_process_data(
|
||||
sys,
|
||||
use_current_cpu_total,
|
||||
unnormalized_cpu,
|
||||
mem_total_kb,
|
||||
user_table,
|
||||
get_freebsd_process_cpu_usage,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_freebsd_process_cpu_usage(pids: &[i32]) -> io::Result<std::collections::HashMap<i32, f64>> {
|
||||
if pids.is_empty() {
|
||||
return Ok(std::collections::HashMap::new());
|
||||
}
|
||||
|
||||
let output = std::process::Command::new("ps")
|
||||
.args(["--libxo", "json", "-o", "pid,pcpu", "-p"])
|
||||
.args(pids.iter().map(i32::to_string))
|
||||
.output()?;
|
||||
deserialize_xo("process-information", &output.stdout).map(|process_info: ProcessInformation| {
|
||||
process_info
|
||||
.process
|
||||
.into_iter()
|
||||
.map(|row| (row.pid, row.percent_cpu))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn pid<'de, D>(deserializer: D) -> Result<i32, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn percent_cpu<'de, D>(deserializer: D) -> Result<f64, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
357
src-tauri/src/data_harvester/processes/linux.rs
Normal file
@ -0,0 +1,357 @@
|
||||
//! Process data collection for Linux.
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
|
||||
use fxhash::{FxHashMap, FxHashSet};
|
||||
use procfs::process::{Process, Stat};
|
||||
use sysinfo::ProcessStatus;
|
||||
|
||||
use super::{ProcessHarvest, UserTable};
|
||||
use crate::data_harvester::cpu::Point;
|
||||
use crate::utils::error::{self, ToeError};
|
||||
use crate::Pid;
|
||||
|
||||
/// Maximum character length of a /proc/<PID>/stat process name.
|
||||
/// If it's equal or greater, then we instead refer to the command for the name.
|
||||
const MAX_STAT_NAME_LEN: usize = 15;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PrevProcDetails {
|
||||
total_read_bytes: u64,
|
||||
total_write_bytes: u64,
|
||||
cpu_time: u64,
|
||||
}
|
||||
|
||||
fn calculate_idle_values(line: &str) -> Point {
|
||||
/// Converts a `Option<&str>` value to an f64. If it fails to parse or is `None`, then it will return `0_f64`.
|
||||
fn str_to_f64(val: Option<&str>) -> f64 {
|
||||
val.and_then(|v| v.parse::<f64>().ok()).unwrap_or(0_f64)
|
||||
}
|
||||
|
||||
let mut val = line.split_whitespace();
|
||||
let user = str_to_f64(val.next());
|
||||
let nice: f64 = str_to_f64(val.next());
|
||||
let system: f64 = str_to_f64(val.next());
|
||||
let idle: f64 = str_to_f64(val.next());
|
||||
let iowait: f64 = str_to_f64(val.next());
|
||||
let irq: f64 = str_to_f64(val.next());
|
||||
let softirq: f64 = str_to_f64(val.next());
|
||||
let steal: f64 = str_to_f64(val.next());
|
||||
|
||||
// Note we do not get guest/guest_nice, as they are calculated as part of user/nice respectively
|
||||
// See https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c
|
||||
|
||||
let idle = idle + iowait;
|
||||
let non_idle = user + nice + system + irq + softirq + steal;
|
||||
|
||||
(idle, non_idle)
|
||||
}
|
||||
|
||||
struct CpuUsage {
|
||||
/// Difference between the total delta and the idle delta.
|
||||
cpu_usage: f64,
|
||||
|
||||
/// Overall CPU usage as a fraction.
|
||||
cpu_fraction: f64,
|
||||
}
|
||||
|
||||
fn cpu_usage_calculation(prev_idle: &mut f64, prev_non_idle: &mut f64) -> error::Result<CpuUsage> {
|
||||
let (idle, non_idle) = {
|
||||
// From SO answer: https://stackoverflow.com/a/23376195
|
||||
let mut reader = BufReader::new(File::open("/proc/stat")?);
|
||||
let mut first_line = String::new();
|
||||
reader.read_line(&mut first_line)?;
|
||||
|
||||
calculate_idle_values(&first_line)
|
||||
};
|
||||
|
||||
let total = idle + non_idle;
|
||||
let prev_total = *prev_idle + *prev_non_idle;
|
||||
|
||||
let total_delta = total - prev_total;
|
||||
let idle_delta = idle - *prev_idle;
|
||||
|
||||
*prev_idle = idle;
|
||||
*prev_non_idle = non_idle;
|
||||
|
||||
// TODO: Should these return errors instead?
|
||||
let cpu_usage = if total_delta - idle_delta != 0.0 {
|
||||
total_delta - idle_delta
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let cpu_fraction = if total_delta != 0.0 {
|
||||
cpu_usage / total_delta
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Ok(CpuUsage {
|
||||
cpu_usage,
|
||||
cpu_fraction,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the usage and a new set of process times.
|
||||
///
|
||||
/// NB: cpu_fraction should be represented WITHOUT the x100 factor!
|
||||
fn get_linux_cpu_usage(
|
||||
stat: &Stat,
|
||||
cpu_usage: f64,
|
||||
cpu_fraction: f64,
|
||||
prev_proc_times: u64,
|
||||
use_current_cpu_total: bool,
|
||||
) -> (f64, u64) {
|
||||
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
|
||||
let new_proc_times = stat.utime + stat.stime;
|
||||
let diff = (new_proc_times - prev_proc_times) as f64; // No try_from for u64 -> f64... oh well.
|
||||
|
||||
if cpu_usage == 0.0 {
|
||||
(0.0, new_proc_times)
|
||||
} else if use_current_cpu_total {
|
||||
((diff / cpu_usage) * 100.0, new_proc_times)
|
||||
} else {
|
||||
((diff / cpu_usage) * 100.0 * cpu_fraction, new_proc_times)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_proc(
|
||||
prev_proc: &PrevProcDetails,
|
||||
process: &Process,
|
||||
cpu_usage: f64,
|
||||
cpu_fraction: f64,
|
||||
use_current_cpu_total: bool,
|
||||
time_difference_in_secs: u64,
|
||||
mem_total_kb: u64,
|
||||
user_table: &mut UserTable,
|
||||
) -> error::Result<(ProcessHarvest, u64)> {
|
||||
let stat = process.stat()?;
|
||||
let (command, name) = {
|
||||
let truncated_name = stat.comm.as_str();
|
||||
if let Ok(cmdline) = process.cmdline() {
|
||||
if cmdline.is_empty() {
|
||||
(format!("[{}]", truncated_name), truncated_name.to_string())
|
||||
} else {
|
||||
(
|
||||
cmdline.join(" "),
|
||||
if truncated_name.len() >= MAX_STAT_NAME_LEN {
|
||||
if let Some(first_part) = cmdline.first() {
|
||||
// We're only interested in the executable part... not the file path.
|
||||
// That's for command.
|
||||
first_part
|
||||
.rsplit_once('/')
|
||||
.map(|(_prefix, suffix)| suffix)
|
||||
.unwrap_or(truncated_name)
|
||||
.to_string()
|
||||
} else {
|
||||
truncated_name.to_string()
|
||||
}
|
||||
} else {
|
||||
truncated_name.to_string()
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(truncated_name.to_string(), truncated_name.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
let process_state_char = stat.state;
|
||||
let process_state = (
|
||||
ProcessStatus::from(process_state_char).to_string(),
|
||||
process_state_char,
|
||||
);
|
||||
let (cpu_usage_percent, new_process_times) = get_linux_cpu_usage(
|
||||
&stat,
|
||||
cpu_usage,
|
||||
cpu_fraction,
|
||||
prev_proc.cpu_time,
|
||||
use_current_cpu_total,
|
||||
);
|
||||
let parent_pid = Some(stat.ppid);
|
||||
let mem_usage_bytes = stat.rss_bytes()?;
|
||||
let mem_usage_kb = mem_usage_bytes / 1024;
|
||||
let mem_usage_percent = mem_usage_kb as f64 / mem_total_kb as f64 * 100.0;
|
||||
|
||||
// This can fail if permission is denied!
|
||||
let (total_read_bytes, total_write_bytes, read_bytes_per_sec, write_bytes_per_sec) =
|
||||
if let Ok(io) = process.io() {
|
||||
let total_read_bytes = io.read_bytes;
|
||||
let total_write_bytes = io.write_bytes;
|
||||
let prev_total_read_bytes = prev_proc.total_read_bytes;
|
||||
let prev_total_write_bytes = prev_proc.total_write_bytes;
|
||||
|
||||
let read_bytes_per_sec = total_read_bytes
|
||||
.saturating_sub(prev_total_read_bytes)
|
||||
.checked_div(time_difference_in_secs)
|
||||
.unwrap_or(0);
|
||||
|
||||
let write_bytes_per_sec = total_write_bytes
|
||||
.saturating_sub(prev_total_write_bytes)
|
||||
.checked_div(time_difference_in_secs)
|
||||
.unwrap_or(0);
|
||||
|
||||
(
|
||||
total_read_bytes,
|
||||
total_write_bytes,
|
||||
read_bytes_per_sec,
|
||||
write_bytes_per_sec,
|
||||
)
|
||||
} else {
|
||||
(0, 0, 0, 0)
|
||||
};
|
||||
|
||||
let uid = process.uid()?;
|
||||
|
||||
Ok((
|
||||
ProcessHarvest {
|
||||
pid: process.pid,
|
||||
parent_pid,
|
||||
cpu_usage_percent,
|
||||
mem_usage_percent,
|
||||
mem_usage_bytes,
|
||||
name,
|
||||
command,
|
||||
read_bytes_per_sec,
|
||||
write_bytes_per_sec,
|
||||
total_read_bytes,
|
||||
total_write_bytes,
|
||||
process_state,
|
||||
uid: Some(uid),
|
||||
user: user_table
|
||||
.get_uid_to_username_mapping(uid)
|
||||
.map(Into::into)
|
||||
.unwrap_or_else(|_| "N/A".into()),
|
||||
},
|
||||
new_process_times,
|
||||
))
|
||||
}
|
||||
|
||||
/// How to calculate CPU usage.
|
||||
pub enum CpuUsageStrategy {
|
||||
/// Normalized means the displayed usage percentage is divided over the number of CPU cores.
|
||||
///
|
||||
/// For example, if the "overall" usage over the entire system is 105%, and there are 5 cores, then
|
||||
/// the displayed percentage is 21%.
|
||||
Normalized,
|
||||
|
||||
/// Non-normalized means that the overall usage over the entire system is shown, without dividing
|
||||
/// over the number of cores.
|
||||
NonNormalized(f64),
|
||||
}
|
||||
|
||||
pub fn get_process_data(
|
||||
prev_idle: &mut f64,
|
||||
prev_non_idle: &mut f64,
|
||||
pid_mapping: &mut FxHashMap<Pid, PrevProcDetails>,
|
||||
use_current_cpu_total: bool,
|
||||
normalization: CpuUsageStrategy,
|
||||
time_difference_in_secs: u64,
|
||||
mem_total_kb: u64,
|
||||
user_table: &mut UserTable,
|
||||
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
||||
// TODO: [PROC THREADS] Add threads
|
||||
|
||||
if let Ok(CpuUsage {
|
||||
mut cpu_usage,
|
||||
cpu_fraction,
|
||||
}) = cpu_usage_calculation(prev_idle, prev_non_idle)
|
||||
{
|
||||
if let CpuUsageStrategy::NonNormalized(num_cores) = normalization {
|
||||
// Note we *divide* here because the later calculation divides `cpu_usage` - in effect,
|
||||
// multiplying over the number of cores.
|
||||
cpu_usage /= num_cores;
|
||||
}
|
||||
|
||||
let mut pids_to_clear: FxHashSet<Pid> = pid_mapping.keys().cloned().collect();
|
||||
|
||||
let process_vector: Vec<ProcessHarvest> = std::fs::read_dir("/proc")?
|
||||
.filter_map(|dir| {
|
||||
if let Ok(dir) = dir {
|
||||
if let Ok(pid) = dir.file_name().to_string_lossy().trim().parse::<Pid>() {
|
||||
let Ok(process) = Process::new(pid) else {
|
||||
return None;
|
||||
};
|
||||
let prev_proc_details = pid_mapping.entry(pid).or_default();
|
||||
|
||||
if let Ok((process_harvest, new_process_times)) = read_proc(
|
||||
prev_proc_details,
|
||||
&process,
|
||||
cpu_usage,
|
||||
cpu_fraction,
|
||||
use_current_cpu_total,
|
||||
time_difference_in_secs,
|
||||
mem_total_kb,
|
||||
user_table,
|
||||
) {
|
||||
prev_proc_details.cpu_time = new_process_times;
|
||||
prev_proc_details.total_read_bytes = process_harvest.total_read_bytes;
|
||||
prev_proc_details.total_write_bytes = process_harvest.total_write_bytes;
|
||||
|
||||
pids_to_clear.remove(&pid);
|
||||
return Some(process_harvest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
pids_to_clear.iter().for_each(|pid| {
|
||||
pid_mapping.remove(pid);
|
||||
});
|
||||
|
||||
Ok(process_vector)
|
||||
} else {
|
||||
Err(ToeError::GenericError(
|
||||
"Could not calculate CPU usage.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_proc_cpu_parse() {
|
||||
assert_eq!(
|
||||
(100_f64, 200_f64),
|
||||
calculate_idle_values("100 0 100 100"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 4 values"
|
||||
);
|
||||
assert_eq!(
|
||||
(120_f64, 200_f64),
|
||||
calculate_idle_values("100 0 100 100 20"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 5 values"
|
||||
);
|
||||
assert_eq!(
|
||||
(120_f64, 230_f64),
|
||||
calculate_idle_values("100 0 100 100 20 30"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 6 values"
|
||||
);
|
||||
assert_eq!(
|
||||
(120_f64, 270_f64),
|
||||
calculate_idle_values("100 0 100 100 20 30 40"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 7 values"
|
||||
);
|
||||
assert_eq!(
|
||||
(120_f64, 320_f64),
|
||||
calculate_idle_values("100 0 100 100 20 30 40 50"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 8 values"
|
||||
);
|
||||
assert_eq!(
|
||||
(120_f64, 320_f64),
|
||||
calculate_idle_values("100 0 100 100 20 30 40 50 100"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 9 values"
|
||||
);
|
||||
assert_eq!(
|
||||
(120_f64, 320_f64),
|
||||
calculate_idle_values("100 0 100 100 20 30 40 50 100 200"),
|
||||
"Failed to properly calculate idle/non-idle for /proc/stat CPU with 10 values"
|
||||
);
|
||||
}
|
||||
}
|
||||
61
src-tauri/src/data_harvester/processes/macos.rs
Normal file
@ -0,0 +1,61 @@
|
||||
//! Process data collection for macOS. Uses sysinfo and custom bindings.
|
||||
|
||||
use sysinfo::System;
|
||||
|
||||
use super::ProcessHarvest;
|
||||
use crate::{data_harvester::processes::UserTable, Pid};
|
||||
mod sysctl_bindings;
|
||||
|
||||
pub fn get_process_data(
|
||||
sys: &System,
|
||||
use_current_cpu_total: bool,
|
||||
unnormalized_cpu: bool,
|
||||
mem_total_kb: u64,
|
||||
user_table: &mut UserTable,
|
||||
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
||||
super::macos_freebsd::get_process_data(
|
||||
sys,
|
||||
use_current_cpu_total,
|
||||
unnormalized_cpu,
|
||||
mem_total_kb,
|
||||
user_table,
|
||||
get_macos_process_cpu_usage,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn fallback_macos_ppid(pid: Pid) -> Option<Pid> {
|
||||
sysctl_bindings::kinfo_process(pid)
|
||||
.map(|kinfo| kinfo.kp_eproc.e_ppid)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn get_macos_process_cpu_usage(
|
||||
pids: &[Pid],
|
||||
) -> std::io::Result<std::collections::HashMap<i32, f64>> {
|
||||
use itertools::Itertools;
|
||||
let output = std::process::Command::new("ps")
|
||||
.args(["-o", "pid=,pcpu=", "-p"])
|
||||
.arg(
|
||||
// Has to look like this since otherwise, it you hit a `unstable_name_collisions` warning.
|
||||
Itertools::intersperse(pids.iter().map(i32::to_string), ",".to_string())
|
||||
.collect::<String>(),
|
||||
)
|
||||
.output()?;
|
||||
let mut result = std::collections::HashMap::new();
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.split_whitespace()
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.for_each(|chunk| {
|
||||
let chunk: Vec<&str> = chunk.collect();
|
||||
if chunk.len() != 2 {
|
||||
panic!("Unexpected `ps` output");
|
||||
}
|
||||
let pid = chunk[0].parse();
|
||||
let usage = chunk[1].parse();
|
||||
if let (Ok(pid), Ok(usage)) = (pid, usage) {
|
||||
result.insert(pid, usage);
|
||||
}
|
||||
});
|
||||
Ok(result)
|
||||
}
|
||||
323
src-tauri/src/data_harvester/processes/macos/sysctl_bindings.rs
Normal file
@ -0,0 +1,323 @@
|
||||
//! Partial bindings from Apple's open source code for getting process information.
|
||||
//! Some of this is based on [heim's binding implementation](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs).
|
||||
|
||||
use std::mem;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use libc::{
|
||||
boolean_t, c_char, c_long, c_short, c_uchar, c_ushort, c_void, dev_t, gid_t, itimerval, pid_t,
|
||||
rusage, sigset_t, timeval, uid_t, xucred, CTL_KERN, KERN_PROC, KERN_PROC_PID, MAXCOMLEN,
|
||||
};
|
||||
use mach2::vm_types::user_addr_t;
|
||||
|
||||
use crate::Pid;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct kinfo_proc {
|
||||
pub kp_proc: extern_proc,
|
||||
pub kp_eproc: eproc,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct p_st1 {
|
||||
/// Doubly-linked run/sleep queue.
|
||||
p_forw: user_addr_t,
|
||||
p_back: user_addr_t,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
pub union p_un {
|
||||
pub p_st1: p_st1,
|
||||
|
||||
/// process start time
|
||||
pub p_starttime: timeval,
|
||||
}
|
||||
|
||||
/// Exported fields for kern sysctl. See
|
||||
/// [`proc.h`](https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/proc.h)
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct extern_proc {
|
||||
pub p_un: p_un,
|
||||
|
||||
/// Address space.
|
||||
pub p_vmspace: *mut vmspace,
|
||||
|
||||
/// Signal actions, state (PROC ONLY). Should point to
|
||||
/// a `sigacts` but we don't really seem to need this.
|
||||
pub p_sigacts: user_addr_t,
|
||||
|
||||
/// P_* flags.
|
||||
pub p_flag: i32,
|
||||
|
||||
/// S* process status.
|
||||
pub p_stat: c_char,
|
||||
|
||||
/// Process identifier.
|
||||
pub p_pid: pid_t,
|
||||
|
||||
/// Save parent pid during ptrace.
|
||||
pub p_oppid: pid_t,
|
||||
|
||||
/// Sideways return value from fdopen.
|
||||
pub p_dupfd: i32,
|
||||
|
||||
/// where user stack was allocated
|
||||
pub user_stack: caddr_t,
|
||||
|
||||
/// Which thread is exiting?
|
||||
pub exit_thread: *mut c_void,
|
||||
|
||||
/// allow to debug
|
||||
pub p_debugger: i32,
|
||||
|
||||
/// indication to suspend
|
||||
pub sigwait: boolean_t,
|
||||
|
||||
/// Time averaged value of p_cpticks.
|
||||
pub p_estcpu: u32,
|
||||
|
||||
/// Ticks of cpu time.
|
||||
pub p_cpticks: i32,
|
||||
|
||||
/// %cpu for this process during p_swtime
|
||||
pub p_pctcpu: fixpt_t,
|
||||
|
||||
/// Sleep address.
|
||||
pub p_wchan: *mut c_void,
|
||||
|
||||
/// Reason for sleep.
|
||||
pub p_wmesg: *mut c_char,
|
||||
|
||||
/// Time swapped in or out.
|
||||
pub p_swtime: u32,
|
||||
|
||||
/// Time since last blocked.
|
||||
pub p_slptime: u32,
|
||||
|
||||
/// Alarm timer.
|
||||
pub p_realtimer: itimerval,
|
||||
|
||||
/// Real time.
|
||||
pub p_rtime: timeval,
|
||||
|
||||
/// Statclock hit in user mode.
|
||||
pub p_uticks: u64,
|
||||
|
||||
/// Statclock hits in system mode.
|
||||
pub p_sticks: u64,
|
||||
|
||||
/// Statclock hits processing intr.
|
||||
pub p_iticks: u64,
|
||||
|
||||
/// Kernel trace points.
|
||||
pub p_traceflag: i32,
|
||||
|
||||
/// Trace to vnode. Originally a pointer to a struct of vnode.
|
||||
pub p_tracep: *mut c_void,
|
||||
|
||||
/// DEPRECATED.
|
||||
pub p_siglist: i32,
|
||||
|
||||
/// Vnode of executable. Originally a pointer to a struct of vnode.
|
||||
pub p_textvp: *mut c_void,
|
||||
|
||||
/// If non-zero, don't swap.
|
||||
pub p_holdcnt: i32,
|
||||
|
||||
/// DEPRECATED.
|
||||
pub p_sigmask: sigset_t,
|
||||
|
||||
/// Signals being ignored.
|
||||
pub p_sigignore: sigset_t,
|
||||
|
||||
/// Signals being caught by user.
|
||||
pub p_sigcatch: sigset_t,
|
||||
|
||||
/// Process priority.
|
||||
pub p_priority: c_uchar,
|
||||
|
||||
/// User-priority based on p_cpu and p_nice.
|
||||
pub p_usrpri: c_uchar,
|
||||
|
||||
/// Process "nice" value.
|
||||
pub p_nice: c_char,
|
||||
|
||||
pub p_comm: [c_char; MAXCOMLEN + 1],
|
||||
|
||||
/// Pointer to process group. Originally a pointer to a `pgrp`.
|
||||
pub p_pgrp: *mut c_void,
|
||||
|
||||
/// Kernel virtual addr of u-area (PROC ONLY). Originally a pointer to a `user`.
|
||||
pub p_addr: *mut c_void,
|
||||
|
||||
/// Exit status for wait; also stop signal.
|
||||
pub p_xstat: c_ushort,
|
||||
|
||||
/// Accounting flags.
|
||||
pub p_acflag: c_ushort,
|
||||
|
||||
/// Exit information. XXX
|
||||
pub p_ru: *mut rusage,
|
||||
}
|
||||
|
||||
const WMESGLEN: usize = 7;
|
||||
const COMAPT_MAXLOGNAME: usize = 12;
|
||||
|
||||
/// See `_caddr_t.h`.
|
||||
#[allow(non_camel_case_types)]
|
||||
type caddr_t = *const libc::c_char;
|
||||
|
||||
/// See `types.h`.
|
||||
#[allow(non_camel_case_types)]
|
||||
type segsz_t = i32;
|
||||
|
||||
/// See `types.h`.
|
||||
#[allow(non_camel_case_types)]
|
||||
type fixpt_t = u32;
|
||||
|
||||
/// See [`proc.h`](https://opensource.apple.com/source/xnu/xnu-201/bsd/sys/proc.h)
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct pcred {
|
||||
pub pc_lock: [c_char; 72],
|
||||
pub pc_ucred: *mut xucred,
|
||||
pub p_ruid: uid_t,
|
||||
pub p_svuid: uid_t,
|
||||
pub p_rgid: gid_t,
|
||||
pub p_svgid: gid_t,
|
||||
pub p_refcnt: i32,
|
||||
}
|
||||
|
||||
/// See `vm.h`.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct vmspace {
|
||||
pub dummy: i32,
|
||||
pub dummy2: caddr_t,
|
||||
pub dummy3: [i32; 5],
|
||||
pub dummy4: [caddr_t; 3],
|
||||
}
|
||||
|
||||
/// See [`sysctl.h`](https://opensource.apple.com/source/xnu/xnu-344/bsd/sys/sysctl.h).
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(C)]
|
||||
pub(crate) struct eproc {
|
||||
/// Address of proc. We just cheat and use a c_void pointer since we aren't using this.
|
||||
pub e_paddr: *mut c_void,
|
||||
|
||||
/// Session pointer. We just cheat and use a c_void pointer since we aren't using this.
|
||||
pub e_sess: *mut c_void,
|
||||
|
||||
/// Process credentials
|
||||
pub e_pcred: pcred,
|
||||
|
||||
/// Current credentials
|
||||
pub e_ucred: xucred,
|
||||
|
||||
/// Address space
|
||||
pub e_vm: vmspace,
|
||||
|
||||
/// Parent process ID
|
||||
pub e_ppid: pid_t,
|
||||
|
||||
/// Process group ID
|
||||
pub e_pgid: pid_t,
|
||||
|
||||
/// Job control counter
|
||||
pub e_jobc: c_short,
|
||||
|
||||
/// Controlling tty dev
|
||||
pub e_tdev: dev_t,
|
||||
|
||||
/// tty process group id
|
||||
pub e_tpgid: pid_t,
|
||||
|
||||
/// tty session pointer. We just cheat and use a c_void pointer since we aren't using this.
|
||||
pub e_tsess: *mut c_void,
|
||||
|
||||
/// wchan message
|
||||
pub e_wmesg: [c_char; WMESGLEN + 1],
|
||||
|
||||
/// text size
|
||||
pub e_xsize: segsz_t,
|
||||
|
||||
/// text rss
|
||||
pub e_xrssize: c_short,
|
||||
|
||||
/// text references
|
||||
pub e_xccount: c_short,
|
||||
|
||||
pub e_xswrss: c_short,
|
||||
|
||||
pub e_flag: c_long,
|
||||
|
||||
/// short setlogin() name
|
||||
pub e_login: [c_char; COMAPT_MAXLOGNAME],
|
||||
|
||||
pub e_spare: [c_long; 4],
|
||||
}
|
||||
|
||||
/// Obtains the [`kinfo_proc`] given a process PID.
|
||||
///
|
||||
/// From [heim](https://github.com/heim-rs/heim/blob/master/heim-process/src/sys/macos/bindings/process.rs#L235).
|
||||
pub(crate) fn kinfo_process(pid: Pid) -> Result<kinfo_proc> {
|
||||
let mut name: [i32; 4] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid];
|
||||
let mut size = mem::size_of::<kinfo_proc>();
|
||||
let mut info = mem::MaybeUninit::<kinfo_proc>::uninit();
|
||||
|
||||
let result = unsafe {
|
||||
libc::sysctl(
|
||||
name.as_mut_ptr(),
|
||||
4,
|
||||
info.as_mut_ptr() as *mut libc::c_void,
|
||||
&mut size,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
if result < 0 {
|
||||
bail!("failed to get process for pid {pid}");
|
||||
}
|
||||
|
||||
// sysctl succeeds but size is zero, happens when process has gone away
|
||||
if size == 0 {
|
||||
bail!("failed to get process for pid {pid}");
|
||||
}
|
||||
|
||||
unsafe { Ok(info.assume_init()) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::mem;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A quick test to ensure that things are sized correctly.
|
||||
#[test]
|
||||
fn test_struct_sizes() {
|
||||
assert_eq!(mem::size_of::<p_st1>(), 16);
|
||||
assert_eq!(mem::align_of::<p_st1>(), 8);
|
||||
|
||||
assert_eq!(mem::size_of::<pcred>(), 104);
|
||||
assert_eq!(mem::align_of::<pcred>(), 8);
|
||||
|
||||
assert_eq!(mem::size_of::<vmspace>(), 64);
|
||||
assert_eq!(mem::align_of::<vmspace>(), 8);
|
||||
|
||||
assert_eq!(mem::size_of::<extern_proc>(), 296);
|
||||
assert_eq!(mem::align_of::<extern_proc>(), 8);
|
||||
|
||||
assert_eq!(mem::size_of::<eproc>(), 376);
|
||||
assert_eq!(mem::align_of::<eproc>(), 8);
|
||||
|
||||
assert_eq!(mem::size_of::<kinfo_proc>(), 672);
|
||||
assert_eq!(mem::align_of::<kinfo_proc>(), 8);
|
||||
}
|
||||
}
|
||||
147
src-tauri/src/data_harvester/processes/macos_freebsd.rs
Normal file
@ -0,0 +1,147 @@
|
||||
//! Shared process data harvesting code from macOS and FreeBSD via sysinfo.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
use sysinfo::{CpuExt, PidExt, ProcessExt, ProcessStatus, System, SystemExt};
|
||||
|
||||
use super::ProcessHarvest;
|
||||
use crate::{data_harvester::processes::UserTable, utils::error::Result, Pid};
|
||||
|
||||
pub fn get_process_data<F>(
|
||||
sys: &System,
|
||||
use_current_cpu_total: bool,
|
||||
unnormalized_cpu: bool,
|
||||
mem_total_kb: u64,
|
||||
user_table: &mut UserTable,
|
||||
backup_cpu_proc_usage: F,
|
||||
) -> Result<Vec<ProcessHarvest>>
|
||||
where
|
||||
F: Fn(&[Pid]) -> io::Result<HashMap<Pid, f64>>,
|
||||
{
|
||||
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
|
||||
let process_hashmap = sys.processes();
|
||||
let cpu_usage = sys.global_cpu_info().cpu_usage() as f64 / 100.0;
|
||||
let num_processors = sys.cpus().len() as f64;
|
||||
|
||||
for process_val in process_hashmap.values() {
|
||||
let name = if process_val.name().is_empty() {
|
||||
let process_cmd = process_val.cmd();
|
||||
if process_cmd.len() > 1 {
|
||||
process_cmd[0].clone()
|
||||
} else {
|
||||
let process_exe = process_val.exe().file_stem();
|
||||
if let Some(exe) = process_exe {
|
||||
let process_exe_opt = exe.to_str();
|
||||
if let Some(exe_name) = process_exe_opt {
|
||||
exe_name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
process_val.name().to_string()
|
||||
};
|
||||
let command = {
|
||||
let command = process_val.cmd().join(" ");
|
||||
if command.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
command
|
||||
}
|
||||
};
|
||||
|
||||
let pcu = {
|
||||
let usage = process_val.cpu_usage() as f64;
|
||||
if unnormalized_cpu || num_processors == 0.0 {
|
||||
usage
|
||||
} else {
|
||||
usage / num_processors
|
||||
}
|
||||
};
|
||||
let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 {
|
||||
pcu / cpu_usage
|
||||
} else {
|
||||
pcu
|
||||
};
|
||||
|
||||
let disk_usage = process_val.disk_usage();
|
||||
let process_state = {
|
||||
let ps = process_val.status();
|
||||
(ps.to_string(), convert_process_status_to_char(ps))
|
||||
};
|
||||
let uid = process_val.user_id().map(|u| **u);
|
||||
let pid = process_val.pid().as_u32() as Pid;
|
||||
process_vector.push(ProcessHarvest {
|
||||
pid,
|
||||
parent_pid: {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
process_val
|
||||
.parent()
|
||||
.map(|p| p.as_u32() as _)
|
||||
.or_else(|| super::fallback_macos_ppid(pid))
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
process_val.parent().map(|p| p.as_u32() as _)
|
||||
}
|
||||
},
|
||||
name,
|
||||
command,
|
||||
mem_usage_percent: if mem_total_kb > 0 {
|
||||
process_val.memory() as f64 * 100.0 / mem_total_kb as f64
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
mem_usage_bytes: process_val.memory(),
|
||||
cpu_usage_percent: process_cpu_usage,
|
||||
read_bytes_per_sec: disk_usage.read_bytes,
|
||||
write_bytes_per_sec: disk_usage.written_bytes,
|
||||
total_read_bytes: disk_usage.total_read_bytes,
|
||||
total_write_bytes: disk_usage.total_written_bytes,
|
||||
process_state,
|
||||
uid,
|
||||
user: uid
|
||||
.and_then(|uid| {
|
||||
user_table
|
||||
.get_uid_to_username_mapping(uid)
|
||||
.map(Into::into)
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| "N/A".into()),
|
||||
});
|
||||
}
|
||||
|
||||
let unknown_state = ProcessStatus::Unknown(0).to_string();
|
||||
let cpu_usage_unknown_pids: Vec<Pid> = process_vector
|
||||
.iter()
|
||||
.filter(|process| process.process_state.0 == unknown_state)
|
||||
.map(|process| process.pid)
|
||||
.collect();
|
||||
let cpu_usages = backup_cpu_proc_usage(&cpu_usage_unknown_pids)?;
|
||||
for process in &mut process_vector {
|
||||
if cpu_usages.contains_key(&process.pid) {
|
||||
process.cpu_usage_percent = if unnormalized_cpu || num_processors == 0.0 {
|
||||
*cpu_usages.get(&process.pid).unwrap()
|
||||
} else {
|
||||
*cpu_usages.get(&process.pid).unwrap() / num_processors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(process_vector)
|
||||
}
|
||||
|
||||
fn convert_process_status_to_char(status: ProcessStatus) -> char {
|
||||
match status {
|
||||
ProcessStatus::Run => 'R',
|
||||
ProcessStatus::Sleep => 'S',
|
||||
ProcessStatus::Idle => 'D',
|
||||
ProcessStatus::Zombie => 'Z',
|
||||
_ => '?',
|
||||
}
|
||||
}
|
||||
32
src-tauri/src/data_harvester/processes/unix.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! Unix-specific parts of process collection.
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
|
||||
use crate::utils::error;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct UserTable {
|
||||
pub uid_user_mapping: FxHashMap<libc::uid_t, String>,
|
||||
}
|
||||
|
||||
impl UserTable {
|
||||
pub fn get_uid_to_username_mapping(&mut self, uid: libc::uid_t) -> error::Result<String> {
|
||||
if let Some(user) = self.uid_user_mapping.get(&uid) {
|
||||
Ok(user.clone())
|
||||
} else {
|
||||
// SAFETY: getpwuid returns a null pointer if no passwd entry is found for the uid
|
||||
let passwd = unsafe { libc::getpwuid(uid) };
|
||||
|
||||
if passwd.is_null() {
|
||||
return Err(error::ToeError::QueryError("Missing passwd".into()));
|
||||
}
|
||||
|
||||
let username = unsafe { std::ffi::CStr::from_ptr((*passwd).pw_name) }
|
||||
.to_str()?
|
||||
.to_string();
|
||||
self.uid_user_mapping.insert(uid, username.clone());
|
||||
|
||||
Ok(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src-tauri/src/data_harvester/processes/windows.rs
Normal file
@ -0,0 +1,85 @@
|
||||
//! Process data collection for Windows. Uses sysinfo.
|
||||
|
||||
use sysinfo::{CpuExt, PidExt, ProcessExt, System, SystemExt};
|
||||
|
||||
use super::ProcessHarvest;
|
||||
|
||||
pub fn get_process_data(
|
||||
sys: &System,
|
||||
use_current_cpu_total: bool,
|
||||
unnormalized_cpu: bool,
|
||||
mem_total_kb: u64,
|
||||
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
||||
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
|
||||
let process_hashmap = sys.processes();
|
||||
let cpu_usage = sys.global_cpu_info().cpu_usage() as f64 / 100.0;
|
||||
let num_processors = sys.cpus().len();
|
||||
|
||||
for process_val in process_hashmap.values() {
|
||||
let name = if process_val.name().is_empty() {
|
||||
let process_cmd = process_val.cmd();
|
||||
if process_cmd.len() > 1 {
|
||||
process_cmd[0].clone()
|
||||
} else {
|
||||
let process_exe = process_val.exe().file_stem();
|
||||
if let Some(exe) = process_exe {
|
||||
let process_exe_opt = exe.to_str();
|
||||
if let Some(exe_name) = process_exe_opt {
|
||||
exe_name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
process_val.name().to_string()
|
||||
};
|
||||
let command = {
|
||||
let command = process_val.cmd().join(" ");
|
||||
if command.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
command
|
||||
}
|
||||
};
|
||||
|
||||
let pcu = {
|
||||
let usage = process_val.cpu_usage() as f64;
|
||||
if unnormalized_cpu || num_processors == 0 {
|
||||
usage
|
||||
} else {
|
||||
usage / (num_processors as f64)
|
||||
}
|
||||
};
|
||||
let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 {
|
||||
pcu / cpu_usage
|
||||
} else {
|
||||
pcu
|
||||
};
|
||||
|
||||
let disk_usage = process_val.disk_usage();
|
||||
let process_state = (process_val.status().to_string(), 'R');
|
||||
process_vector.push(ProcessHarvest {
|
||||
pid: process_val.pid().as_u32() as _,
|
||||
parent_pid: process_val.parent().map(|p| p.as_u32() as _),
|
||||
name,
|
||||
command,
|
||||
mem_usage_percent: if mem_total_kb > 0 {
|
||||
process_val.memory() as f64 * 100.0 / mem_total_kb as f64
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
mem_usage_bytes: process_val.memory(),
|
||||
cpu_usage_percent: process_cpu_usage,
|
||||
read_bytes_per_sec: disk_usage.read_bytes,
|
||||
write_bytes_per_sec: disk_usage.written_bytes,
|
||||
total_read_bytes: disk_usage.total_read_bytes,
|
||||
total_write_bytes: disk_usage.total_written_bytes,
|
||||
process_state,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(process_vector)
|
||||
}
|
||||
25
src-tauri/src/data_harvester/temperature.rs
Normal file
@ -0,0 +1,25 @@
|
||||
//! Data collection for temperature metrics.
|
||||
//!
|
||||
//! For Linux and macOS, this is handled by Heim.
|
||||
//! For Windows, this is handled by sysinfo.
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub mod linux;
|
||||
pub use self::linux::*;
|
||||
} else if #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "windows"))] {
|
||||
pub mod sysinfo;
|
||||
pub use self::sysinfo::*;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
pub mod nvidia;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize)]
|
||||
pub struct TempHarvest {
|
||||
pub name: String,
|
||||
pub temperature: f32,
|
||||
}
|
||||
230
src-tauri/src/data_harvester/temperature/linux.rs
Normal file
@ -0,0 +1,230 @@
|
||||
//! Gets temperature sensor data for Linux platforms.
|
||||
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use super::TempHarvest;
|
||||
|
||||
/// Get temperature sensors from the linux sysfs interface `/sys/class/hwmon`.
|
||||
/// See [here](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-hwmon) for
|
||||
/// details.
|
||||
///
|
||||
/// This method will return `0` as the temperature for devices, such as GPUs,
|
||||
/// that support power management features and power themselves off.
|
||||
///
|
||||
/// Specifically, in laptops with iGPUs and dGPUs, if the dGPU is capable of
|
||||
/// entering ACPI D3cold, reading the temperature sensors will wake it,
|
||||
/// and keep it awake, wasting power.
|
||||
///
|
||||
/// For such devices, this method will only query the sensors *only* if
|
||||
/// the device is already in ACPI D0. This has the notable issue that
|
||||
/// once this happens, the device will be *kept* on through the sensor
|
||||
/// reading, and not be able to re-enter ACPI D3cold.
|
||||
fn get_from_hwmon() -> Result<Vec<TempHarvest>> {
|
||||
let mut temperature_vec: Vec<TempHarvest> = vec![];
|
||||
let path = Path::new("/sys/class/hwmon");
|
||||
|
||||
// NOTE: Technically none of this is async, *but* sysfs is in memory,
|
||||
// so in theory none of this should block if we're slightly careful.
|
||||
// Of note is that reading the temperature sensors of a device that has
|
||||
// `/sys/class/hwmon/hwmon*/device/power_state` == `D3cold` will
|
||||
// wake the device up, and will block until it initializes.
|
||||
//
|
||||
// Reading the `hwmon*/device/power_state` or `hwmon*/temp*_label` properties
|
||||
// will not wake the device, and thus not block,
|
||||
// and meaning no sensors have to be hidden depending on `power_state`
|
||||
//
|
||||
// It would probably be more ideal to use a proper async runtime..
|
||||
for entry in path.read_dir()? {
|
||||
let file = entry?;
|
||||
let mut file_path = file.path();
|
||||
|
||||
// hwmon includes many sensors, we only want ones with at least one temperature sensor
|
||||
// Reading this file will wake the device, but we're only checking existence.
|
||||
if !file_path.join("temp1_input").exists() {
|
||||
// Note we also check for a `device` subdirectory (e.g. `/sys/class/hwmon/hwmon*/device/`).
|
||||
// This is needed for CentOS, which adds this extra `/device` directory. See:
|
||||
// - https://github.com/nicolargo/glances/issues/1060
|
||||
// - https://github.com/giampaolo/psutil/issues/971
|
||||
// - https://github.com/giampaolo/psutil/blob/642438375e685403b4cd60b0c0e25b80dd5a813d/psutil/_pslinux.py#L1316
|
||||
//
|
||||
// If it does match, then add the `device/` directory to the path.
|
||||
if file_path.join("device/temp1_input").exists() {
|
||||
file_path.push("device");
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let hwmon_name = file_path.join("name");
|
||||
let hwmon_name = Some(fs::read_to_string(hwmon_name)?);
|
||||
|
||||
// Whether the temperature should *actually* be read during enumeration
|
||||
// Set to false if the device is in ACPI D3cold.
|
||||
let should_read_temp = {
|
||||
// Documented at https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-power_state
|
||||
let device = file_path.join("device");
|
||||
let power_state = device.join("power_state");
|
||||
if power_state.exists() {
|
||||
let state = fs::read_to_string(power_state)?;
|
||||
let state = state.trim();
|
||||
// The zenpower3 kernel module (incorrectly?) reports "unknown"
|
||||
// causing this check to fail and temperatures to appear as zero
|
||||
// instead of having the file not exist..
|
||||
// their self-hosted git instance has disabled sign up,
|
||||
// so this bug cant be reported either.
|
||||
state == "D0" || state == "unknown"
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
// Enumerate the devices temperature sensors
|
||||
for entry in file_path.read_dir()? {
|
||||
let file = entry?;
|
||||
let name = file.file_name();
|
||||
// This should always be ASCII
|
||||
let name = name
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("temperature device filenames should be ASCII"))?;
|
||||
// We only want temperature sensors, skip others early
|
||||
if !(name.starts_with("temp") && name.ends_with("input")) {
|
||||
continue;
|
||||
}
|
||||
let temp = file.path();
|
||||
let temp_label = file_path.join(name.replace("input", "label"));
|
||||
let temp_label = fs::read_to_string(temp_label).ok();
|
||||
|
||||
// Do some messing around to get a more sensible name for sensors
|
||||
//
|
||||
// - For GPUs, this will use the kernel device name, ex `card0`
|
||||
// - For nvme drives, this will also use the kernel name, ex `nvme0`.
|
||||
// This is found differently than for GPUs
|
||||
// - For whatever acpitz is, on my machine this is now `thermal_zone0`.
|
||||
// - For k10temp, this will still be k10temp, but it has to be handled special.
|
||||
let human_hwmon_name = {
|
||||
let device = file_path.join("device");
|
||||
// This will exist for GPUs but not others, this is how
|
||||
// we find their kernel name
|
||||
let drm = device.join("drm");
|
||||
if drm.exists() {
|
||||
// This should never actually be empty
|
||||
let mut gpu = None;
|
||||
for card in drm.read_dir()? {
|
||||
let card = card?;
|
||||
let name = card.file_name().to_str().unwrap_or_default().to_owned();
|
||||
if name.starts_with("card") {
|
||||
if let Some(hwmon_name) = hwmon_name.as_ref() {
|
||||
gpu = Some(format!("{} ({})", name, hwmon_name.trim()));
|
||||
} else {
|
||||
gpu = Some(name)
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
gpu
|
||||
} else {
|
||||
// This little mess is to account for stuff like k10temp
|
||||
// This is needed because the `device` symlink
|
||||
// points to `nvme*` for nvme drives, but to PCI buses for anything else
|
||||
// If the first character is alphabetic,
|
||||
// its an actual name like k10temp or nvme0, not a PCI bus
|
||||
let link = fs::read_link(device)?
|
||||
.file_name()
|
||||
.map(|f| f.to_str().unwrap_or_default().to_owned())
|
||||
.unwrap();
|
||||
if link.as_bytes()[0].is_ascii_alphabetic() {
|
||||
if let Some(hwmon_name) = hwmon_name.as_ref() {
|
||||
Some(format!("{} ({})", link, hwmon_name.trim()))
|
||||
} else {
|
||||
Some(link)
|
||||
}
|
||||
} else {
|
||||
hwmon_name.clone()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let name = match (&human_hwmon_name, &temp_label) {
|
||||
(Some(name), Some(label)) => format!("{}: {}", name.trim(), label.trim()),
|
||||
(None, Some(label)) => label.to_string(),
|
||||
(Some(name), None) => name.to_string(),
|
||||
(None, None) => String::default(),
|
||||
};
|
||||
|
||||
let temp = if should_read_temp {
|
||||
if let Ok(temp) = fs::read_to_string(temp) {
|
||||
let temp = temp.trim_end().parse::<f32>().map_err(|e| {
|
||||
crate::utils::error::ToeError::ConversionError(e.to_string())
|
||||
})?;
|
||||
temp / 1_000.0
|
||||
} else {
|
||||
// For some devices (e.g. iwlwifi), this file becomes empty when the device
|
||||
// is disabled. In this case we skip the device.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
temperature_vec.push(TempHarvest {
|
||||
name,
|
||||
temperature: temp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(temperature_vec)
|
||||
}
|
||||
|
||||
/// Gets data from `/sys/class/thermal/thermal_zone*`. This should only be used if
|
||||
/// [`get_from_hwmon`] doesn't return anything. See
|
||||
/// [here](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-thermal) for details.
|
||||
fn get_from_thermal_zone() -> Result<Vec<TempHarvest>> {
|
||||
let mut temperatures = vec![];
|
||||
let path = Path::new("/sys/class/thermal");
|
||||
for entry in path.read_dir()? {
|
||||
let file = entry?;
|
||||
if file
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with("thermal_zone")
|
||||
{
|
||||
let file_path = file.path();
|
||||
let name_path = file_path.join("type");
|
||||
|
||||
let name = fs::read_to_string(name_path)?.trim_end().to_string();
|
||||
|
||||
let temp_path = file_path.join("temp");
|
||||
let temp = fs::read_to_string(temp_path)?
|
||||
.trim_end()
|
||||
.parse::<f32>()
|
||||
.map_err(|e| crate::utils::error::ToeError::ConversionError(e.to_string()))?
|
||||
/ 1_000.0;
|
||||
temperatures.push(TempHarvest {
|
||||
name,
|
||||
temperature: temp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(temperatures)
|
||||
}
|
||||
|
||||
/// Gets temperature sensors and data.
|
||||
pub fn get_temperature_data() -> Result<Option<Vec<TempHarvest>>> {
|
||||
let mut temperature_vec: Vec<TempHarvest> = get_from_hwmon()?;
|
||||
|
||||
if temperature_vec.is_empty() {
|
||||
// If it's empty, fall back to checking `thermal_zone*`.
|
||||
temperature_vec = get_from_thermal_zone()?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
{
|
||||
super::nvidia::add_nvidia_data(&mut temperature_vec)?;
|
||||
}
|
||||
|
||||
Ok(Some(temperature_vec))
|
||||
}
|
||||
26
src-tauri/src/data_harvester/temperature/nvidia.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use nvml_wrapper::enum_wrappers::device::TemperatureSensor;
|
||||
|
||||
use super::TempHarvest;
|
||||
use crate::data_harvester::nvidia::NVML_DATA;
|
||||
|
||||
pub fn add_nvidia_data(
|
||||
temperature_vec: &mut Vec<TempHarvest>,
|
||||
temp_type: &TemperatureType,
|
||||
) -> crate::utils::error::Result<()> {
|
||||
if let Ok(nvml) = &*NVML_DATA {
|
||||
if let Ok(ngpu) = nvml.device_count() {
|
||||
for i in 0..ngpu {
|
||||
if let Ok(device) = nvml.device_by_index(i) {
|
||||
if let (Ok(name), Ok(temperature)) =
|
||||
(device.name(), device.temperature(TemperatureSensor::Gpu))
|
||||
{
|
||||
let temperature = temperature as f32;
|
||||
temperature_vec.push(TempHarvest { name, temperature });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
28
src-tauri/src/data_harvester/temperature/sysinfo.rs
Normal file
@ -0,0 +1,28 @@
|
||||
//! Gets temperature data via sysinfo.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use super::TempHarvest;
|
||||
|
||||
pub fn get_temperature_data(sys: &sysinfo::System) -> Result<Option<Vec<TempHarvest>>> {
|
||||
use sysinfo::{ComponentExt, SystemExt};
|
||||
|
||||
let mut temperature_vec: Vec<TempHarvest> = Vec::new();
|
||||
|
||||
let sensor_data = sys.components();
|
||||
for component in sensor_data {
|
||||
let name = component.label().to_string();
|
||||
|
||||
temperature_vec.push(TempHarvest {
|
||||
name,
|
||||
temperature: component.temperature(),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "nvidia")]
|
||||
{
|
||||
super::nvidia::add_nvidia_data(&mut temperature_vec, temp_type, filter)?;
|
||||
}
|
||||
|
||||
Ok(Some(temperature_vec))
|
||||
}
|
||||
64
src-tauri/src/main.rs
Normal file
@ -0,0 +1,64 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
mod data_harvester;
|
||||
mod utils;
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::utils::error;
|
||||
use data_harvester::{Data, DataCollector};
|
||||
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu};
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
pub type Pid = usize;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub type Pid = libc::pid_t;
|
||||
|
||||
#[tauri::command]
|
||||
fn collect_data(data_state: tauri::State<Mutex<DataCollector>>) -> Data {
|
||||
futures::executor::block_on(data_state.lock().unwrap().update_data());
|
||||
data_state.lock().unwrap().data.clone()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut data_state = DataCollector::new();
|
||||
data_state.init();
|
||||
|
||||
let preferences = CustomMenuItem::new("preferences", "Open Preferences").accelerator("cmd+,");
|
||||
let submenu = Submenu::new(
|
||||
"Menu",
|
||||
Menu::new()
|
||||
.add_item(preferences)
|
||||
.add_native_item(MenuItem::SelectAll)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Quit)
|
||||
.add_native_item(MenuItem::About(
|
||||
"ToeRings".to_string(),
|
||||
AboutMetadata::new()
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.authors(
|
||||
env!("CARGO_PKG_AUTHORS")
|
||||
.split(":")
|
||||
.map(String::from)
|
||||
.collect(),
|
||||
)
|
||||
.license("MIT"),
|
||||
)),
|
||||
);
|
||||
let menu = Menu::new().add_submenu(submenu);
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(Mutex::new(data_state))
|
||||
.menu(menu)
|
||||
.on_menu_event(|event| match event.menu_item_id() {
|
||||
"preferences" => event.window().emit("openPreferences", ()).unwrap(),
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![collect_data])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
107
src-tauri/src/utils/error.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use std::{borrow::Cow, result};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use procfs::ProcError;
|
||||
|
||||
/// A type alias for handling errors related to Bottom.
|
||||
pub type Result<T> = result::Result<T, ToeError>;
|
||||
|
||||
/// An error that can occur while Bottom runs.
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum ToeError {
|
||||
/// An error when there is an IO exception.
|
||||
#[error("IO exception, {0}")]
|
||||
InvalidIo(String),
|
||||
/// An error when the heim library encounters a problem.
|
||||
#[error("Error caused by Heim, {0}")]
|
||||
InvalidHeim(String),
|
||||
/// An error when the Crossterm library encounters a problem.
|
||||
#[error("Error caused by Crossterm, {0}")]
|
||||
CrosstermError(String),
|
||||
/// An error to represent generic errors.
|
||||
#[error("Error, {0}")]
|
||||
GenericError(String),
|
||||
/// An error to represent errors with fern.
|
||||
#[error("Fern error, {0}")]
|
||||
FernError(String),
|
||||
/// An error to represent errors with the config.
|
||||
#[error("Configuration file error, {0}")]
|
||||
ConfigError(String),
|
||||
/// An error to represent errors with converting between data types.
|
||||
#[error("Conversion error, {0}")]
|
||||
ConversionError(String),
|
||||
/// An error to represent errors with querying.
|
||||
#[error("Query error, {0}")]
|
||||
QueryError(Cow<'static, str>),
|
||||
/// An error that just signifies something minor went wrong; no message.
|
||||
#[error("Minor error.")]
|
||||
MinorError,
|
||||
/// An error to represent errors with procfs
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error("Procfs error, {0}")]
|
||||
ProcfsError(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ToeError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
ToeError::InvalidIo(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "freebsd"))]
|
||||
impl From<heim::Error> for ToeError {
|
||||
fn from(err: heim::Error) -> Self {
|
||||
ToeError::InvalidHeim(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for ToeError {
|
||||
fn from(err: std::num::ParseIntError) -> Self {
|
||||
ToeError::ConfigError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::String> for ToeError {
|
||||
fn from(err: std::string::String) -> Self {
|
||||
ToeError::GenericError(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fern")]
|
||||
impl From<fern::InitError> for ToeError {
|
||||
fn from(err: fern::InitError) -> Self {
|
||||
ToeError::FernError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::str::Utf8Error> for ToeError {
|
||||
fn from(err: std::str::Utf8Error) -> Self {
|
||||
ToeError::ConversionError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for ToeError {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
ToeError::ConversionError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl From<ProcError> for ToeError {
|
||||
fn from(err: ProcError) -> Self {
|
||||
match err {
|
||||
ProcError::PermissionDenied(p) => {
|
||||
ToeError::ProcfsError(format!("Permission denied for {:?}", p))
|
||||
}
|
||||
ProcError::NotFound(p) => ToeError::ProcfsError(format!("{:?} not found", p)),
|
||||
ProcError::Incomplete(p) => ToeError::ProcfsError(format!("{:?} incomplete", p)),
|
||||
ProcError::Io(e, p) => ToeError::ProcfsError(format!("io error: {:?} for {:?}", e, p)),
|
||||
ProcError::Other(s) => ToeError::ProcfsError(format!("Other procfs error: {}", s)),
|
||||
ProcError::InternalError(e) => {
|
||||
ToeError::ProcfsError(format!("procfs internal error: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src-tauri/src/utils/logging.rs
Normal file
@ -0,0 +1,31 @@
|
||||
#[cfg(feature = "fern")]
|
||||
pub fn init_logger(
|
||||
min_level: log::LevelFilter,
|
||||
debug_file_name: &std::ffi::OsStr,
|
||||
) -> Result<(), fern::InitError> {
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
// Note we aren't using local time since it only works on single-threaded processes.
|
||||
// If that ever does get patched in again, enable the "local-offset" feature.
|
||||
let offset = time::OffsetDateTime::now_utc();
|
||||
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
offset
|
||||
.format(&time::macros::format_description!(
|
||||
// The weird "[[[" is because we need to escape a bracket ("[[") to show one "[".
|
||||
// See https://time-rs.github.io/book/api/format-description.html
|
||||
"[[[year]-[month]-[day]][[[hour]:[minute]:[second][subsecond digits:9]]"
|
||||
))
|
||||
.unwrap(),
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(min_level)
|
||||
.chain(fern::log_file(debug_file_name)?)
|
||||
.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
2
src-tauri/src/utils/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod error;
|
||||
pub mod logging;
|
||||
79
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"package": {
|
||||
"productName": "ToeRings",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"window": {
|
||||
"startDragging": true,
|
||||
"setSize": true
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "io.toerings",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"macOSPrivateApi": true,
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 850,
|
||||
"width": 325,
|
||||
"x": 30,
|
||||
"y": 50,
|
||||
"resizable": true,
|
||||
"title": "toerings",
|
||||
"decorations": false,
|
||||
"transparent": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
172
src/App.svelte
Normal file
@ -0,0 +1,172 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/tauri"
|
||||
import { appWindow, LogicalSize } from "@tauri-apps/api/window"
|
||||
import { listen } from "@tauri-apps/api/event"
|
||||
import { sum, pick } from "lodash-es"
|
||||
import { onMount } from "svelte"
|
||||
import "uplot/dist/uPlot.min.css"
|
||||
|
||||
import { styleVars } from "./lib/actions"
|
||||
import {
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
titleColor,
|
||||
accentColor,
|
||||
fontFamily
|
||||
} from "./lib/stores"
|
||||
import { sleep, saturatedPush } from "./lib/utils"
|
||||
import SummaryWidget from "./components/SummaryWidget.svelte"
|
||||
import CPUWidget from "./components/CPUWidget.svelte"
|
||||
import MemWidget from "./components/MemWidget.svelte"
|
||||
import DiskWidget from "./components/DiskWidget.svelte"
|
||||
import NetWidget from "./components/NetWidget.svelte"
|
||||
import Preferences from "./components/Preferences.svelte"
|
||||
|
||||
let preferencesVisible = false
|
||||
|
||||
onMount(() => {
|
||||
const unlisten = listen("openPreferences", () => {
|
||||
preferencesVisible = true
|
||||
})
|
||||
return unlisten
|
||||
})
|
||||
|
||||
$: if (preferencesVisible) {
|
||||
appWindow.setSize(new LogicalSize(650, 850))
|
||||
} else {
|
||||
appWindow.setSize(new LogicalSize(325, 850))
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
preferencesVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
const graphXLimit = 60
|
||||
|
||||
let summaryData: SummaryData = {
|
||||
uptime: "0s",
|
||||
hostname: null,
|
||||
kernel_name: null,
|
||||
kernel_version: null,
|
||||
os_version: null
|
||||
}
|
||||
|
||||
let cpuData = {
|
||||
perCoreUtil: [],
|
||||
cpuLoads: Array(60).fill(0)
|
||||
}
|
||||
let processList: Array<Process> = []
|
||||
let tempData: Array<TempData> = []
|
||||
let memData = {
|
||||
ram: {
|
||||
usage: {
|
||||
mem_total_in_kib: 0,
|
||||
mem_used_in_kib: 0,
|
||||
use_percent: null
|
||||
},
|
||||
percentages: Array(60).fill(0)
|
||||
},
|
||||
swap: {
|
||||
usage: {
|
||||
mem_total_in_kib: 0,
|
||||
mem_used_in_kib: 0,
|
||||
use_percent: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let diskData: Array<DiskData> = []
|
||||
let ioData = Array(60).fill({ read: 0, write: 0 })
|
||||
|
||||
let networkData = {
|
||||
rx: Array(60).fill(0),
|
||||
tx: Array(60).fill(0)
|
||||
}
|
||||
|
||||
let localIp = ""
|
||||
|
||||
let lastDataCollection = 0
|
||||
|
||||
async function collectData() {
|
||||
lastDataCollection = Date.now()
|
||||
const data: Data = await invoke("collect_data")
|
||||
processData(data)
|
||||
const sleepMs = Math.max(lastDataCollection + 1000 - Date.now(), 0)
|
||||
await sleep(sleepMs)
|
||||
collectData()
|
||||
}
|
||||
|
||||
function processData(data: Data) {
|
||||
summaryData = pick(data, ["uptime", "hostname", "kernel_name", "kernel_version", "os_version"])
|
||||
processList = data.list_of_processes
|
||||
|
||||
cpuData.perCoreUtil = data.cpu.map(cpu => cpu.cpu_usage)
|
||||
saturatedPush(cpuData.cpuLoads, sum(cpuData.perCoreUtil) / 100, graphXLimit)
|
||||
cpuData = cpuData
|
||||
|
||||
tempData = data.temperature_sensors
|
||||
|
||||
memData.ram.usage = data.memory
|
||||
saturatedPush(memData.ram.percentages, data.memory.use_percent, graphXLimit)
|
||||
memData.swap.usage = data.swap
|
||||
|
||||
diskData = data.disks
|
||||
|
||||
const ioDataPoint = {
|
||||
read: sum(data.list_of_processes.map(process => process.read_bytes_per_sec)),
|
||||
write: sum(data.list_of_processes.map(process => process.write_bytes_per_sec))
|
||||
}
|
||||
saturatedPush(ioData, ioDataPoint, graphXLimit)
|
||||
ioData = ioData
|
||||
|
||||
saturatedPush(networkData.rx, data.network.rx, graphXLimit)
|
||||
saturatedPush(networkData.tx, data.network.tx, graphXLimit)
|
||||
networkData = networkData
|
||||
|
||||
localIp = data.local_ip
|
||||
}
|
||||
|
||||
collectData()
|
||||
|
||||
$: cssVars = {
|
||||
foregroundColor: $foregroundColor.toHslString(),
|
||||
backgroundColor: $backgroundColor.toHslString(),
|
||||
titleColor: $titleColor.toHslString(),
|
||||
accentColor: $accentColor.toHslString(),
|
||||
fontFamily: $fontFamily
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeydown} />
|
||||
|
||||
<div class="flex" use:styleVars={cssVars}>
|
||||
<main>
|
||||
<SummaryWidget {summaryData} />
|
||||
<CPUWidget {cpuData} {tempData} {processList} />
|
||||
<MemWidget {memData} {processList} />
|
||||
<DiskWidget {diskData} {ioData} {processList} />
|
||||
<NetWidget {networkData} {localIp} hostname={summaryData.hostname} />
|
||||
</main>
|
||||
|
||||
{#if preferencesVisible}
|
||||
<Preferences bind:preferencesVisible />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
background-color: var(--backgroundColor);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 325px;
|
||||
padding: 10px;
|
||||
max-height: 850px;
|
||||
overflow-y: hidden;
|
||||
font-family: var(--fontFamily);
|
||||
color: var(--foregroundColor);
|
||||
}
|
||||
</style>
|
||||
131
src/components/Arc.svelte
Normal file
@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
export let value = 0
|
||||
export let max = 100
|
||||
export let strokeColor = "rgba(255, 255, 255, 0.6)"
|
||||
export let strokeWidth = 8
|
||||
export let trackColor = "rgba(255, 255, 255, 0.2)"
|
||||
export let capColor = "white"
|
||||
export let size = 100
|
||||
export let label = null
|
||||
export let tooltip = null
|
||||
|
||||
import { styleVars } from "../lib/actions"
|
||||
|
||||
let showTooltip = false
|
||||
|
||||
const margin = 5
|
||||
$: sizeHalf = size / 2
|
||||
$: zero = [sizeHalf, size - margin]
|
||||
$: x = zero[0]
|
||||
$: y = zero[1]
|
||||
$: fullPath = `M${zero[0]},${zero[1]} A${sizeHalf - margin},${sizeHalf - margin} 0 1,1 ${
|
||||
zero[1]
|
||||
},${zero[0]}`
|
||||
|
||||
$: cssVars = {
|
||||
strokeColor,
|
||||
strokeWidth: `${strokeWidth}px`,
|
||||
trackColor,
|
||||
size: `${size}px`
|
||||
}
|
||||
|
||||
let arcPath = ""
|
||||
$: if (value <= 0) {
|
||||
arcPath = ""
|
||||
x = zero[0]
|
||||
y = zero[1]
|
||||
} else {
|
||||
const frac = Math.min(value / max, 1)
|
||||
const angle = ((3 * Math.PI) / 2) * frac
|
||||
x = sizeHalf + Math.cos(angle + Math.PI / 2) * (sizeHalf - margin)
|
||||
y = sizeHalf + Math.sin(angle + Math.PI / 2) * (sizeHalf - margin)
|
||||
const majorArc = Number(angle > Math.PI)
|
||||
arcPath = `M${zero[0]},${zero[1]} A${sizeHalf - margin},${
|
||||
sizeHalf - margin
|
||||
} 0 ${majorArc},1 ${x},${y}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:styleVars={cssVars} class="arc-wrap">
|
||||
<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d={fullPath} />
|
||||
<path d={arcPath} />
|
||||
<circle cx={x} cy={y} r={strokeWidth / 2} fill={capColor} />
|
||||
</svg>
|
||||
{#if label}
|
||||
{#if showTooltip && tooltip}
|
||||
<div class="tooltip">
|
||||
{@html tooltip}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="label"
|
||||
on:mouseenter={() => {
|
||||
showTooltip = true
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
showTooltip = false
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $$slots.default}
|
||||
<div>
|
||||
<div class="slot-parent">
|
||||
<slot {value} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
fill: transparent;
|
||||
position: absolute;
|
||||
stroke-linecap: round;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
path {
|
||||
stroke-width: var(--strokeWidth);
|
||||
stroke: var(--trackColor);
|
||||
}
|
||||
|
||||
path:nth-child(2) {
|
||||
stroke: var(--strokeColor);
|
||||
}
|
||||
|
||||
.arc-wrap {
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: calc(var(--size) / 2 + 7px);
|
||||
width: 45px;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.slot-parent {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: calc(var(--size) / 2 + 7px);
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
</style>
|
||||
65
src/components/ArcStack.svelte
Normal file
@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
export let arcs: Array<ArcSpec>
|
||||
export let size = 100
|
||||
export let strokeWidth = 8
|
||||
|
||||
interface ArcSpec {
|
||||
value: number
|
||||
max: number
|
||||
label?: string
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
import { styleVars } from "../lib/actions"
|
||||
import { arcTrackColor, arcCapColor, foregroundColor } from "../lib/stores"
|
||||
import Arc from "./Arc.svelte"
|
||||
|
||||
function computeGap(strokeWidth: number) {
|
||||
if (strokeWidth <= 5) {
|
||||
return 1
|
||||
}
|
||||
if (strokeWidth <= 10) {
|
||||
return 2
|
||||
}
|
||||
return 3
|
||||
}
|
||||
$: gap = computeGap(strokeWidth)
|
||||
$: levelWidth = (strokeWidth + gap) * 2
|
||||
$: cssVars = {
|
||||
size: `${size}px`
|
||||
}
|
||||
$: trackColor = $arcTrackColor.toHslString()
|
||||
$: strokeColor = $foregroundColor.alpha(0.6).toHslString()
|
||||
$: capColor = $arcCapColor.toHslString()
|
||||
</script>
|
||||
|
||||
<div class="arcs-container" use:styleVars={cssVars}>
|
||||
{#each arcs as arc, i}
|
||||
<div class="arc-wrap">
|
||||
<Arc
|
||||
{...arc}
|
||||
size={size - levelWidth * i}
|
||||
{strokeWidth}
|
||||
{strokeColor}
|
||||
{capColor}
|
||||
{trackColor}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.arcs-container {
|
||||
position: relative;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.arc-wrap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: fit-content;
|
||||
}
|
||||
</style>
|
||||
60
src/components/ArcWidget.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
export let attrs: Array<{ key: string; value: string }>
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="arc-wrap">
|
||||
<slot name="arcStack" />
|
||||
</div>
|
||||
<ul>
|
||||
{#each attrs as pair}
|
||||
<li>
|
||||
<div class="key">{pair.key}</div>
|
||||
<div class="value">{pair.value}</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="flowing-content">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.arc-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
ul {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
text-align: right;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.key {
|
||||
font-weight: bold;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.flowing-content {
|
||||
margin-top: 65px;
|
||||
margin-left: 75px;
|
||||
}
|
||||
</style>
|
||||
44
src/components/CPUWidget.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
interface CPUProps {
|
||||
perCoreUtil: Array<number>
|
||||
cpuLoads: Array<number>
|
||||
}
|
||||
|
||||
export let cpuData: CPUProps
|
||||
export let processList: Array<Process>
|
||||
export let tempData: Array<TempData>
|
||||
|
||||
import { mean } from "lodash-es"
|
||||
import { calcStrokeWidth } from "../lib/utils"
|
||||
import { foregroundColor } from "../lib/stores"
|
||||
import ArcStack from "./ArcStack.svelte"
|
||||
import ArcWidget from "./ArcWidget.svelte"
|
||||
import ProcessList from "./ProcessList.svelte"
|
||||
|
||||
$: arcs = cpuData.perCoreUtil.map(utilPercent => ({
|
||||
value: utilPercent,
|
||||
max: 100
|
||||
}))
|
||||
|
||||
$: plotData = {
|
||||
x: cpuData.cpuLoads.map((_, i) => i),
|
||||
y: cpuData.cpuLoads,
|
||||
color: $foregroundColor
|
||||
}
|
||||
|
||||
$: avgDieTemp = mean(tempData.map(sensor => sensor.temperature))
|
||||
$: attrs = [
|
||||
{ key: "CPU Temp:", value: `${avgDieTemp.toFixed(1)}°C` },
|
||||
{ key: "CPU Load:", value: cpuData.cpuLoads.at(-1).toFixed(2) }
|
||||
]
|
||||
$: cpuSortedProcesses = [...processList]
|
||||
.sort((a, b) => a.cpu_usage_percent < b.cpu_usage_percent)
|
||||
.slice(0, 5)
|
||||
</script>
|
||||
|
||||
<ArcWidget {attrs}>
|
||||
<ArcStack slot="arcStack" {arcs} size={120} strokeWidth={calcStrokeWidth(arcs.length)} />
|
||||
<ProcessList slot="content" title="CPU" plotDatas={[plotData]} processList={cpuSortedProcesses}>
|
||||
<span slot="processVal" let:process>{process.cpu_usage_percent.toFixed(2)}%</span>
|
||||
</ProcessList>
|
||||
</ArcWidget>
|
||||
70
src/components/DiskWidget.svelte
Normal file
@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
export let diskData: Array<DiskData>
|
||||
export let ioData: Array<{ read: number; write: number }>
|
||||
export let processList: Array<Process>
|
||||
|
||||
import { basename } from "path"
|
||||
import { uniqBy } from "lodash-es"
|
||||
|
||||
import { toMetric, calcStrokeWidth } from "../lib/utils"
|
||||
import { foregroundColor } from "../lib/stores"
|
||||
import ArcStack from "./ArcStack.svelte"
|
||||
import ArcWidget from "./ArcWidget.svelte"
|
||||
import ProcessList from "./ProcessList.svelte"
|
||||
|
||||
function formatPath(filepath: string): string {
|
||||
if (filepath === "/") {
|
||||
return "/"
|
||||
}
|
||||
return basename(filepath)
|
||||
}
|
||||
|
||||
$: pathSortedDisks = uniqBy(
|
||||
[...diskData].sort((a, b) => a.mount_point > b.mount_point),
|
||||
disk => `${disk.free_space},${disk.used_space},${disk.total_space}`
|
||||
)
|
||||
|
||||
$: ioSortedProcesses = [...processList]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
a.write_bytes_per_sec + a.read_bytes_per_sec < b.write_bytes_per_sec + b.read_bytes_per_sec
|
||||
)
|
||||
.slice(0, 5)
|
||||
|
||||
$: arcs = pathSortedDisks.slice(0, 4).map(disk => ({
|
||||
label: formatPath(disk.mount_point),
|
||||
value: disk.used_space,
|
||||
max: disk.total_space,
|
||||
tooltip: `${disk.name}<br/>${disk.mount_point}<br/>${(
|
||||
(disk.used_space / disk.total_space) *
|
||||
100
|
||||
).toFixed(1)}% used`
|
||||
}))
|
||||
|
||||
$: attrs = [
|
||||
{ key: "Read:", value: toMetric(ioData.at(-1).read) },
|
||||
{ key: "Write:", value: toMetric(ioData.at(-1).write) }
|
||||
]
|
||||
|
||||
$: plotDatas = [
|
||||
{
|
||||
x: ioData.map((_, i) => i),
|
||||
y: ioData.map(point => point.read),
|
||||
color: $foregroundColor
|
||||
},
|
||||
{
|
||||
x: ioData.map((_, i) => i),
|
||||
y: ioData.map(point => point.write),
|
||||
color: $foregroundColor
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<ArcWidget {attrs}>
|
||||
<ArcStack slot="arcStack" {arcs} size={120} strokeWidth={calcStrokeWidth(arcs.length)} />
|
||||
<ProcessList slot="content" title="Disk" {plotDatas} processList={ioSortedProcesses}>
|
||||
<span slot="processVal" let:process>
|
||||
r:{toMetric(process.read_bytes_per_sec, 0)}, w:{toMetric(process.write_bytes_per_sec, 0)}
|
||||
</span>
|
||||
</ProcessList>
|
||||
</ArcWidget>
|
||||
50
src/components/MemWidget.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
export let memData: {
|
||||
ram: { usage: MemData; percentages: Array<number> }
|
||||
swap: { usage: MemData }
|
||||
}
|
||||
export let processList: Array<Process>
|
||||
|
||||
import { toMetric, calcStrokeWidth } from "../lib/utils"
|
||||
import { foregroundColor } from "../lib/stores"
|
||||
import ArcStack from "./ArcStack.svelte"
|
||||
import ArcWidget from "./ArcWidget.svelte"
|
||||
import ProcessList from "./ProcessList.svelte"
|
||||
|
||||
$: arcs = [
|
||||
{
|
||||
value: memData.ram.usage.mem_used_in_kib,
|
||||
max: memData.ram.usage.mem_total_in_kib,
|
||||
label: "RAM"
|
||||
},
|
||||
{
|
||||
value: memData.swap.usage.mem_used_in_kib,
|
||||
max: memData.swap.usage.mem_total_in_kib,
|
||||
label: "Swap"
|
||||
}
|
||||
]
|
||||
|
||||
$: attrs = [
|
||||
{
|
||||
key: "Available:",
|
||||
value: toMetric(memData.ram.usage.mem_total_in_kib * 1024)
|
||||
},
|
||||
{ key: "Used:", value: toMetric(memData.ram.usage.mem_used_in_kib * 1024) }
|
||||
]
|
||||
$: plotData = {
|
||||
x: memData.ram.percentages.map((_, i) => i),
|
||||
y: memData.ram.percentages,
|
||||
color: $foregroundColor
|
||||
}
|
||||
|
||||
$: memSortedProcesses = [...processList]
|
||||
.sort((a, b) => a.mem_usage_bytes < b.mem_usage_bytes)
|
||||
.slice(0, 5)
|
||||
</script>
|
||||
|
||||
<ArcWidget {attrs}>
|
||||
<ArcStack slot="arcStack" {arcs} size={120} strokeWidth={calcStrokeWidth(arcs.length)} />
|
||||
<ProcessList slot="content" title="Mem" plotDatas={[plotData]} processList={memSortedProcesses}>
|
||||
<span slot="processVal" let:process>{toMetric(process.mem_usage_bytes)}</span>
|
||||
</ProcessList>
|
||||
</ArcWidget>
|
||||
76
src/components/NetWidget.svelte
Normal file
@ -0,0 +1,76 @@
|
||||
<script>
|
||||
export let networkData
|
||||
export let localIp = null
|
||||
export let hostname
|
||||
|
||||
import { calcStrokeWidth, toMetric } from "../lib/utils"
|
||||
import { uPlotAction } from "../lib/actions"
|
||||
import { titleColor, accentColor } from "../lib/stores"
|
||||
import ArcWidget from "./ArcWidget.svelte"
|
||||
import ArcStack from "./ArcStack.svelte"
|
||||
|
||||
$: attrs = [
|
||||
{ key: "Hostname:", value: hostname },
|
||||
{ key: "Local IP:", value: localIp }
|
||||
]
|
||||
|
||||
$: arcs = [
|
||||
{ label: "Down", value: networkData.rx.at(-1), max: 100 * 1024 ** 2 },
|
||||
{ label: "Up", value: networkData.tx.at(-1), max: 100 * 1024 ** 2 }
|
||||
]
|
||||
|
||||
$: inPlotData = {
|
||||
x: networkData.rx.map((_, i) => i),
|
||||
y: networkData.rx,
|
||||
color: $accentColor
|
||||
}
|
||||
$: outPlotData = {
|
||||
x: networkData.tx.map((_, i) => i),
|
||||
y: networkData.tx,
|
||||
color: $titleColor
|
||||
}
|
||||
</script>
|
||||
|
||||
<ArcWidget {attrs}>
|
||||
<ArcStack slot="arcStack" {arcs} size={120} strokeWidth={calcStrokeWidth(arcs.length)} />
|
||||
<div slot="content">
|
||||
<div class="flex">
|
||||
<h2>Net</h2>
|
||||
<div class="plot-container">
|
||||
<div use:uPlotAction={inPlotData} />
|
||||
<p class="caption">
|
||||
Down: {toMetric(networkData.rx.at(-1))}
|
||||
</p>
|
||||
<div use:uPlotAction={outPlotData} />
|
||||
<p class="caption">
|
||||
Up: {toMetric(networkData.tx.at(-1))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ArcWidget>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding-right: 5px;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
color: var(--titleColor);
|
||||
}
|
||||
|
||||
.plot-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.caption {
|
||||
text-align: right;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
141
src/components/Preferences.svelte
Normal file
@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
export let preferencesVisible
|
||||
|
||||
import ColorPicker from "svelte-awesome-color-picker"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
import {
|
||||
foregroundColor,
|
||||
backgroundColor,
|
||||
titleColor,
|
||||
accentColor,
|
||||
fontFamily,
|
||||
arcTrackColor,
|
||||
arcCapColor
|
||||
} from "../lib/stores"
|
||||
|
||||
function closePreferences() {
|
||||
preferencesVisible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside>
|
||||
<h1>Preferences</h1>
|
||||
|
||||
<button on:click={closePreferences} aria-label="close">✕</button>
|
||||
<div class="picker">
|
||||
<ColorPicker
|
||||
rgb={get(foregroundColor).toRgb()}
|
||||
label="Foreground color"
|
||||
isAlpha={false}
|
||||
on:input={e => foregroundColor.set(e.detail.color)}
|
||||
/>
|
||||
</div>
|
||||
<div class="picker">
|
||||
<ColorPicker
|
||||
rgb={get(backgroundColor).toRgb()}
|
||||
label="Background color"
|
||||
on:input={e => backgroundColor.set(e.detail.color)}
|
||||
/>
|
||||
</div>
|
||||
<div class="picker">
|
||||
<ColorPicker
|
||||
rgb={get(titleColor).toRgb()}
|
||||
label="Title color"
|
||||
isAlpha={false}
|
||||
on:input={e => titleColor.set(e.detail.color)}
|
||||
/>
|
||||
</div>
|
||||
<div class="picker">
|
||||
<ColorPicker
|
||||
rgb={get(accentColor).toRgb()}
|
||||
label="Accent color"
|
||||
isAlpha={false}
|
||||
on:input={e => accentColor.set(e.detail.color)}
|
||||
/>
|
||||
</div>
|
||||
<div class="picker">
|
||||
<ColorPicker
|
||||
rgb={get(arcTrackColor).toRgb()}
|
||||
label="Arc track color"
|
||||
isAlpha={true}
|
||||
on:input={e => arcTrackColor.set(e.detail.color)}
|
||||
/>
|
||||
</div>
|
||||
<div class="picker">
|
||||
<ColorPicker
|
||||
rgb={get(arcCapColor).toRgb()}
|
||||
label="Arc cap color"
|
||||
isAlpha={true}
|
||||
on:input={e => arcCapColor.set(e.detail.color)}
|
||||
/>
|
||||
</div>
|
||||
<form on:submit|preventDefault={() => {}}>
|
||||
<fieldset>
|
||||
<div>
|
||||
<label for="font-family">Font</label>
|
||||
<input
|
||||
type="text"
|
||||
id="font-family"
|
||||
value={get(fontFamily)}
|
||||
on:change={e => fontFamily.set(e.target.value)}
|
||||
placeholder="Avenir, Arial"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
aside {
|
||||
position: relative;
|
||||
width: 325px;
|
||||
background-color: white;
|
||||
color: #2b3e51;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
margin-bottom: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #cfd9db;
|
||||
background-color: #ffffff;
|
||||
border-radius: 0.25em;
|
||||
box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #2c3e50;
|
||||
box-shadow: 0 0 5px rgb(44 151 222 / 20%);
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
border: none;
|
||||
background-color: white;
|
||||
font-size: 30px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
</style>
|
||||
105
src/components/ProcessList.svelte
Normal file
@ -0,0 +1,105 @@
|
||||
<script lang="ts">
|
||||
export let title: string
|
||||
export let plotDatas: Array<{ x: Array<number>; y: Array<number> }>
|
||||
export let processList: Array<Process>
|
||||
|
||||
import type { Colord } from "colord"
|
||||
import { uPlotAction, styleVars } from "../lib/actions"
|
||||
import { foregroundColor } from "../lib/stores"
|
||||
|
||||
function attenuateLightness(color: Colord): Colord {
|
||||
const lightness = color.toHsl().l / 100 // 0 to 1
|
||||
const delta = lightness - 0.5
|
||||
return delta >= 0 ? color.darken(delta / 2) : color.lighten(delta / -2)
|
||||
}
|
||||
|
||||
function halveSaturation(color: Colord): Colord {
|
||||
const hsl = color.toHsl()
|
||||
return color.desaturate(hsl.s / 2 / 100)
|
||||
}
|
||||
|
||||
$: cssVars = {
|
||||
mutedForegroundColor: attenuateLightness(halveSaturation($foregroundColor)).toHslString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<h2>{title}</h2>
|
||||
<div class="plot-container">
|
||||
{#each plotDatas as plotData, i}
|
||||
{#if i === 0}
|
||||
<div use:uPlotAction={plotData} />
|
||||
{:else}
|
||||
<div class="plot-wrap">
|
||||
<div use:uPlotAction={plotData} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<ul class="processes" use:styleVars={cssVars}>
|
||||
{#each processList as process}
|
||||
<li>
|
||||
<div class="key">{process.name}</div>
|
||||
<div class="value">
|
||||
<slot name="processVal" {process} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding-right: 5px;
|
||||
width: 48px;
|
||||
text-align: right;
|
||||
color: var(--titleColor);
|
||||
}
|
||||
|
||||
.plot-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.plot-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.processes {
|
||||
margin: 0;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.processes li {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.processes li:first-child {
|
||||
color: var(--accentColor);
|
||||
}
|
||||
|
||||
.processes li:nth-child(n + 3) {
|
||||
color: var(--mutedForegroundColor);
|
||||
}
|
||||
|
||||
.key {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.value {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
44
src/components/SummaryWidget.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
export let summaryData: SummaryData
|
||||
</script>
|
||||
|
||||
<header data-tauri-drag-region>
|
||||
<h1>System</h1>
|
||||
<div class="divider" />
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li><strong>Uptime:</strong> {summaryData.uptime}</li>
|
||||
<li><strong>OS Version:</strong> {summaryData.os_version}</li>
|
||||
<li>
|
||||
<strong>Kernel:</strong>
|
||||
{summaryData.kernel_name}
|
||||
{summaryData.kernel_version}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
header {
|
||||
display: flex;
|
||||
padding: 5px 10px 0 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
flex-grow: 1;
|
||||
border-top: 1px solid var(--foregroundColor);
|
||||
height: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
</style>
|
||||
60
src/lib/actions.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import uPlot from "uplot"
|
||||
import type { Options } from "uplot"
|
||||
import { colord, Colord } from "colord"
|
||||
|
||||
export function styleVars(node: HTMLElement, props: Record<string, any>) {
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
node.style.setProperty(`--${key}`, value)
|
||||
})
|
||||
|
||||
return {
|
||||
update(newProps: Record<string, any>) {
|
||||
Object.entries(newProps).forEach(([key, value]) => {
|
||||
node.style.setProperty(`--${key}`, value)
|
||||
delete props[key]
|
||||
})
|
||||
|
||||
Object.keys(props).forEach(name => node.style.removeProperty(`--${name}`))
|
||||
props = newProps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlotData {
|
||||
x: Array<number>
|
||||
y: Array<number>
|
||||
color?: Colord
|
||||
}
|
||||
|
||||
function makePlotOptions(color: Colord): Options {
|
||||
const stroke = color.alpha(0.7).lighten(0.2).toHslString()
|
||||
const fill = color.alpha(0.5).lighten(0.2).toHslString()
|
||||
return {
|
||||
width: 180,
|
||||
height: 15,
|
||||
pxAlign: false,
|
||||
cursor: { show: false },
|
||||
select: { show: false },
|
||||
legend: { show: false },
|
||||
scales: {
|
||||
x: { time: false }
|
||||
},
|
||||
axes: [{ show: false }, { show: false }],
|
||||
series: [{}, { stroke, fill }]
|
||||
}
|
||||
}
|
||||
|
||||
export function uPlotAction(node: HTMLElement, data: PlotData) {
|
||||
const { x, y, color } = data
|
||||
const options = makePlotOptions(color || colord("#fff"))
|
||||
const plot = new uPlot(options, [x, y], node)
|
||||
return {
|
||||
update(newData: PlotData) {
|
||||
plot.setData([newData.x, newData.y])
|
||||
plot.series[1].stroke = () =>
|
||||
(newData.color || colord("#fff")).alpha(0.7).lighten(0.2).toHslString()
|
||||
plot.series[1].fill = () =>
|
||||
(newData.color || colord("#fff")).alpha(0.5).lighten(0.2).toHslString()
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/lib/stores.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { writable } from "svelte/store"
|
||||
import { colord } from "colord"
|
||||
|
||||
export const foregroundColor = writable(colord("#ffffff"))
|
||||
export const backgroundColor = writable(colord("rgba(0, 0, 0, 0.5)"))
|
||||
export const titleColor = writable(colord("#00ff00"))
|
||||
export const accentColor = writable(colord("#ff00ff"))
|
||||
export const arcTrackColor = writable(colord("rgba(255, 255, 255, 0.2)"))
|
||||
export const arcCapColor = writable(colord("#ffffff"))
|
||||
export const fontFamily = writable("Inter, Avenir, Helvetica, Arial, sans-serif")
|
||||
45
src/lib/utils.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export function toMetric(bytes: number | null, precision = 1): string {
|
||||
if (!bytes) {
|
||||
return "0B"
|
||||
}
|
||||
const units = ["B", "kB", "MB", "GB", "TB", "PB", "EB"]
|
||||
let unitIndex = 0
|
||||
while (bytes > 1000) {
|
||||
bytes /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
return bytes.toFixed(precision) + units[unitIndex]
|
||||
}
|
||||
|
||||
export function calcStrokeWidth(numArcs: number): number {
|
||||
if (numArcs === 1) {
|
||||
return 12
|
||||
}
|
||||
if (numArcs < 4) {
|
||||
return 10
|
||||
}
|
||||
if (numArcs === 4) {
|
||||
return 8
|
||||
}
|
||||
if (numArcs === 5) {
|
||||
return 6
|
||||
}
|
||||
if (numArcs === 6) {
|
||||
return 5
|
||||
}
|
||||
if (numArcs < 9) {
|
||||
return 4
|
||||
}
|
||||
return 3
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
export function saturatedPush<T>(arr: Array<T>, element: T, limit: number) {
|
||||
arr.push(element)
|
||||
while (arr.length > limit) {
|
||||
arr.shift()
|
||||
}
|
||||
}
|
||||
8
src/main.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import "./style.css"
|
||||
import App from "./App.svelte"
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("app")
|
||||
})
|
||||
|
||||
export default app
|
||||
38
src/style.css
Normal file
@ -0,0 +1,38 @@
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
|
||||
color: white;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
[data-tauri-drag-region] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
[data-tauri-drag-region]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
93
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
/* eslint no-unused-vars: 0 */
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface CPUData {
|
||||
cpu_usage: number
|
||||
}
|
||||
|
||||
interface Process {
|
||||
name: string
|
||||
command: string
|
||||
pid: string
|
||||
parent_pid: string | null
|
||||
cpu_usage_percent: number
|
||||
mem_usage_percent: number
|
||||
mem_usage_bytes: number
|
||||
read_bytes_per_sec: number
|
||||
write_bytes_per_sec: number
|
||||
total_read_bytes: number
|
||||
total_write_bytes: number
|
||||
process_state: [string, string]
|
||||
uid: string | null
|
||||
user: string | null
|
||||
}
|
||||
|
||||
interface DiskData {
|
||||
name: string
|
||||
mount_point: string
|
||||
free_space: number | null
|
||||
used_space: number | null
|
||||
total_space: number | null
|
||||
}
|
||||
|
||||
interface MemData {
|
||||
mem_total_in_kib: number
|
||||
mem_used_in_kib: number
|
||||
use_percent: number | null
|
||||
}
|
||||
|
||||
interface NetData {
|
||||
rx: number
|
||||
tx: number
|
||||
total_rx: number
|
||||
total_tx: number
|
||||
}
|
||||
|
||||
interface TempData {
|
||||
name: string
|
||||
temperature: number
|
||||
}
|
||||
|
||||
interface BatteryData {
|
||||
charge_percent: number
|
||||
secs_until_full: number | null
|
||||
secs_until_empty: number | null
|
||||
power_consumption_rate_watts: number
|
||||
health_percent: number
|
||||
}
|
||||
|
||||
interface SummaryData {
|
||||
uptime: string
|
||||
hostname: string | null
|
||||
kernel_name: string | null
|
||||
kernel_version: string | null
|
||||
os_version: string | null
|
||||
}
|
||||
|
||||
interface IOData {
|
||||
read_bytes: number
|
||||
write_bytes: number
|
||||
}
|
||||
|
||||
interface Data {
|
||||
last_collection_time: number
|
||||
uptime: string
|
||||
hostname: string | null
|
||||
kernel_name: string | null
|
||||
kernel_version: string | null
|
||||
os_version: string | null
|
||||
list_of_processes: Array<Process>
|
||||
cpu: Array<CPUData>
|
||||
load_avg: Array<number>
|
||||
memory: MemData
|
||||
swap: MemData
|
||||
disks: Array<DiskData>
|
||||
io: Record<string, IOData>
|
||||
local_ip: string | null
|
||||
network: NetData
|
||||
temperature_sensors: Array<TempData>
|
||||
list_of_batteries?: Array<BatteryData>
|
||||
arc?: MemData
|
||||
gpu?: Array<[string, MemData]>
|
||||
}
|
||||
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
8
tsconfig.node.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
40
vite.config.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import sveltePreprocess from "svelte-preprocess"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte({
|
||||
preprocess: [
|
||||
sveltePreprocess({
|
||||
typescript: true
|
||||
})
|
||||
]
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
path: "path-browserify"
|
||||
}
|
||||
},
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true
|
||||
},
|
||||
// to make use of `TAURI_DEBUG` and other env variables
|
||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: process.env.TAURI_PLATFORM === "windows" ? "chrome105" : "safari13",
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG
|
||||
}
|
||||
})
|
||||