scaffold workspace with gartop, gartopctl, gartop-ipc crates
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
b9657a952fe83d3f16fc89bda1cc08ced71b76e3- Tree
7eb3cd2
b9657a9
b9657a952fe83d3f16fc89bda1cc08ced71b76e37eb3cd2| Status | File | + | - |
|---|---|---|---|
| A |
.gitignore
|
15 | 0 |
| A |
Cargo.toml
|
49 | 0 |
| A |
gartop-ipc/Cargo.toml
|
11 | 0 |
| A |
gartop-ipc/src/lib.rs
|
189 | 0 |
| A |
gartop/Cargo.toml
|
32 | 0 |
| A |
gartop/src/collector/mod.rs
|
5 | 0 |
| A |
gartop/src/config.rs
|
98 | 0 |
| A |
gartop/src/daemon/mod.rs
|
10 | 0 |
| A |
gartop/src/error.rs
|
27 | 0 |
| A |
gartop/src/gui/mod.rs
|
10 | 0 |
| A |
gartop/src/ipc/mod.rs
|
6 | 0 |
| A |
gartop/src/main.rs
|
71 | 0 |
| A |
gartopctl/Cargo.toml
|
19 | 0 |
| A |
gartopctl/src/main.rs
|
121 | 0 |
.gitignoreadded@@ -0,0 +1,15 @@ | ||
| 1 | +# Build artifacts | |
| 2 | +/target/ | |
| 3 | +Cargo.lock | |
| 4 | + | |
| 5 | +# IDE | |
| 6 | +.idea/ | |
| 7 | +.vscode/ | |
| 8 | +*.swp | |
| 9 | +*.swo | |
| 10 | + | |
| 11 | +# Documentation and planning | |
| 12 | +/docs/ | |
| 13 | + | |
| 14 | +# Claude | |
| 15 | +.claude/ | |
Cargo.tomladded@@ -0,0 +1,49 @@ | ||
| 1 | +[workspace] | |
| 2 | +resolver = "2" | |
| 3 | +members = ["gartop", "gartopctl", "gartop-ipc"] | |
| 4 | + | |
| 5 | +[workspace.package] | |
| 6 | +version = "0.1.0" | |
| 7 | +edition = "2024" | |
| 8 | +authors = ["mfwolffe"] | |
| 9 | +license = "MIT" | |
| 10 | + | |
| 11 | +[workspace.dependencies] | |
| 12 | +# Shared IPC types | |
| 13 | +gartop-ipc = { path = "gartop-ipc" } | |
| 14 | + | |
| 15 | +# gartk toolkit | |
| 16 | +gartk-core = { path = "../gartk/gartk-core" } | |
| 17 | +gartk-x11 = { path = "../gartk/gartk-x11" } | |
| 18 | +gartk-render = { path = "../gartk/gartk-render" } | |
| 19 | + | |
| 20 | +# X11 | |
| 21 | +x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr"] } | |
| 22 | + | |
| 23 | +# Cairo rendering | |
| 24 | +cairo-rs = { version = "0.20", features = ["png"] } | |
| 25 | + | |
| 26 | +# System data | |
| 27 | +procfs = "0.18" | |
| 28 | + | |
| 29 | +# Async runtime | |
| 30 | +tokio = { version = "1", features = ["full", "signal"] } | |
| 31 | + | |
| 32 | +# Serialization | |
| 33 | +serde = { version = "1", features = ["derive"] } | |
| 34 | +serde_json = "1" | |
| 35 | +toml = "0.8" | |
| 36 | + | |
| 37 | +# CLI | |
| 38 | +clap = { version = "4", features = ["derive"] } | |
| 39 | + | |
| 40 | +# Logging/Errors | |
| 41 | +tracing = "0.1" | |
| 42 | +tracing-subscriber = { version = "0.3", features = ["env-filter"] } | |
| 43 | +thiserror = "2" | |
| 44 | +anyhow = "1" | |
| 45 | + | |
| 46 | +# Utilities | |
| 47 | +dirs = "5" | |
| 48 | +nix = { version = "0.29", features = ["signal"] } | |
| 49 | +users = "0.11" | |
gartop-ipc/Cargo.tomladded@@ -0,0 +1,11 @@ | ||
| 1 | +[package] | |
| 2 | +name = "gartop-ipc" | |
| 3 | +version.workspace = true | |
| 4 | +edition.workspace = true | |
| 5 | +authors.workspace = true | |
| 6 | +license.workspace = true | |
| 7 | +description = "IPC protocol types for gartop system monitor" | |
| 8 | + | |
| 9 | +[dependencies] | |
| 10 | +serde.workspace = true | |
| 11 | +serde_json.workspace = true | |
gartop-ipc/src/lib.rsadded@@ -0,0 +1,189 @@ | ||
| 1 | +//! Shared IPC protocol types for gartop. | |
| 2 | +//! | |
| 3 | +//! This crate defines the request/response types used for communication | |
| 4 | +//! between gartop daemon and clients (gartopctl, GUI, garbar). | |
| 5 | + | |
| 6 | +use serde::{Deserialize, Serialize}; | |
| 7 | +use std::path::PathBuf; | |
| 8 | + | |
| 9 | +/// IPC commands sent to the gartop daemon. | |
| 10 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 11 | +#[serde(tag = "command", rename_all = "snake_case")] | |
| 12 | +pub enum Command { | |
| 13 | + /// Get daemon status. | |
| 14 | + Status, | |
| 15 | + /// Get current CPU stats. | |
| 16 | + GetCpu, | |
| 17 | + /// Get current memory stats. | |
| 18 | + GetMemory, | |
| 19 | + /// Get CPU history. | |
| 20 | + GetCpuHistory { | |
| 21 | + #[serde(default)] | |
| 22 | + count: Option<usize>, | |
| 23 | + }, | |
| 24 | + /// Get memory history. | |
| 25 | + GetMemoryHistory { | |
| 26 | + #[serde(default)] | |
| 27 | + count: Option<usize>, | |
| 28 | + }, | |
| 29 | + /// Get running processes. | |
| 30 | + GetProcesses { | |
| 31 | + #[serde(default)] | |
| 32 | + sort_by: Option<SortField>, | |
| 33 | + #[serde(default)] | |
| 34 | + limit: Option<usize>, | |
| 35 | + }, | |
| 36 | + /// Kill a process by PID. | |
| 37 | + KillProcess { | |
| 38 | + pid: i32, | |
| 39 | + #[serde(default)] | |
| 40 | + signal: Option<i32>, | |
| 41 | + }, | |
| 42 | + /// Subscribe to real-time updates. | |
| 43 | + Subscribe { events: Vec<String> }, | |
| 44 | + /// Unsubscribe from events. | |
| 45 | + Unsubscribe { events: Vec<String> }, | |
| 46 | + /// Reload configuration. | |
| 47 | + Reload, | |
| 48 | + /// Quit the daemon. | |
| 49 | + Quit, | |
| 50 | +} | |
| 51 | + | |
| 52 | +/// Sort field for process list. | |
| 53 | +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] | |
| 54 | +#[serde(rename_all = "snake_case")] | |
| 55 | +pub enum SortField { | |
| 56 | + #[default] | |
| 57 | + Cpu, | |
| 58 | + Memory, | |
| 59 | + Pid, | |
| 60 | + Name, | |
| 61 | +} | |
| 62 | + | |
| 63 | +/// IPC response from gartop daemon. | |
| 64 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 65 | +pub struct Response { | |
| 66 | + pub success: bool, | |
| 67 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 68 | + pub data: Option<serde_json::Value>, | |
| 69 | + #[serde(skip_serializing_if = "Option::is_none")] | |
| 70 | + pub error: Option<String>, | |
| 71 | +} | |
| 72 | + | |
| 73 | +impl Response { | |
| 74 | + /// Create a successful response with no data. | |
| 75 | + pub fn ok() -> Self { | |
| 76 | + Self { | |
| 77 | + success: true, | |
| 78 | + data: None, | |
| 79 | + error: None, | |
| 80 | + } | |
| 81 | + } | |
| 82 | + | |
| 83 | + /// Create a successful response with data. | |
| 84 | + pub fn ok_with_data(data: impl Serialize) -> Self { | |
| 85 | + Self { | |
| 86 | + success: true, | |
| 87 | + data: serde_json::to_value(data).ok(), | |
| 88 | + error: None, | |
| 89 | + } | |
| 90 | + } | |
| 91 | + | |
| 92 | + /// Create an error response. | |
| 93 | + pub fn err(message: impl Into<String>) -> Self { | |
| 94 | + Self { | |
| 95 | + success: false, | |
| 96 | + data: None, | |
| 97 | + error: Some(message.into()), | |
| 98 | + } | |
| 99 | + } | |
| 100 | +} | |
| 101 | + | |
| 102 | +/// CPU usage statistics. | |
| 103 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 104 | +pub struct CpuStats { | |
| 105 | + /// Overall CPU usage percentage (0-100). | |
| 106 | + pub usage_percent: f64, | |
| 107 | + /// Per-core usage percentages. | |
| 108 | + pub per_core: Vec<f64>, | |
| 109 | + /// Number of cores. | |
| 110 | + pub core_count: usize, | |
| 111 | + /// Timestamp (milliseconds since epoch). | |
| 112 | + pub timestamp: u64, | |
| 113 | +} | |
| 114 | + | |
| 115 | +/// Memory usage statistics. | |
| 116 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 117 | +pub struct MemoryStats { | |
| 118 | + /// Total memory in bytes. | |
| 119 | + pub total: u64, | |
| 120 | + /// Used memory in bytes (excluding cache/buffers). | |
| 121 | + pub used: u64, | |
| 122 | + /// Free memory in bytes. | |
| 123 | + pub free: u64, | |
| 124 | + /// Available memory in bytes. | |
| 125 | + pub available: u64, | |
| 126 | + /// Swap total in bytes. | |
| 127 | + pub swap_total: u64, | |
| 128 | + /// Swap used in bytes. | |
| 129 | + pub swap_used: u64, | |
| 130 | + /// Usage percentage (0-100). | |
| 131 | + pub usage_percent: f64, | |
| 132 | + /// Timestamp (milliseconds since epoch). | |
| 133 | + pub timestamp: u64, | |
| 134 | +} | |
| 135 | + | |
| 136 | +/// Process information. | |
| 137 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 138 | +pub struct ProcessInfo { | |
| 139 | + /// Process ID. | |
| 140 | + pub pid: i32, | |
| 141 | + /// Process name. | |
| 142 | + pub name: String, | |
| 143 | + /// Command line. | |
| 144 | + pub cmdline: String, | |
| 145 | + /// CPU usage percentage. | |
| 146 | + pub cpu_percent: f64, | |
| 147 | + /// Memory usage percentage. | |
| 148 | + pub memory_percent: f64, | |
| 149 | + /// Resident set size in bytes. | |
| 150 | + pub rss: u64, | |
| 151 | + /// Virtual memory size in bytes. | |
| 152 | + pub vsize: u64, | |
| 153 | + /// Process state. | |
| 154 | + pub state: String, | |
| 155 | + /// User owning the process. | |
| 156 | + pub user: String, | |
| 157 | +} | |
| 158 | + | |
| 159 | +/// Events broadcast to subscribers. | |
| 160 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 161 | +#[serde(tag = "event", rename_all = "snake_case")] | |
| 162 | +pub enum Event { | |
| 163 | + /// CPU stats updated. | |
| 164 | + CpuUpdate(CpuStats), | |
| 165 | + /// Memory stats updated. | |
| 166 | + MemoryUpdate(MemoryStats), | |
| 167 | + /// Process list updated. | |
| 168 | + ProcessUpdate { processes: Vec<ProcessInfo> }, | |
| 169 | +} | |
| 170 | + | |
| 171 | +/// Daemon status information. | |
| 172 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 173 | +pub struct StatusInfo { | |
| 174 | + /// Daemon version. | |
| 175 | + pub version: String, | |
| 176 | + /// Uptime in seconds. | |
| 177 | + pub uptime_secs: u64, | |
| 178 | + /// Sample interval in milliseconds. | |
| 179 | + pub sample_interval_ms: u64, | |
| 180 | + /// History buffer size. | |
| 181 | + pub history_size: usize, | |
| 182 | +} | |
| 183 | + | |
| 184 | +/// Get the default socket path for gartop IPC. | |
| 185 | +pub fn socket_path() -> PathBuf { | |
| 186 | + let runtime_dir = | |
| 187 | + std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()); | |
| 188 | + PathBuf::from(runtime_dir).join("gartop.sock") | |
| 189 | +} | |
gartop/Cargo.tomladded@@ -0,0 +1,32 @@ | ||
| 1 | +[package] | |
| 2 | +name = "gartop" | |
| 3 | +version.workspace = true | |
| 4 | +edition.workspace = true | |
| 5 | +authors.workspace = true | |
| 6 | +license.workspace = true | |
| 7 | +description = "System monitor daemon for gar desktop" | |
| 8 | + | |
| 9 | +[[bin]] | |
| 10 | +name = "gartop" | |
| 11 | +path = "src/main.rs" | |
| 12 | + | |
| 13 | +[dependencies] | |
| 14 | +gartop-ipc.workspace = true | |
| 15 | +gartk-core.workspace = true | |
| 16 | +gartk-x11.workspace = true | |
| 17 | +gartk-render.workspace = true | |
| 18 | +x11rb.workspace = true | |
| 19 | +cairo-rs.workspace = true | |
| 20 | +procfs.workspace = true | |
| 21 | +tokio.workspace = true | |
| 22 | +serde.workspace = true | |
| 23 | +serde_json.workspace = true | |
| 24 | +toml.workspace = true | |
| 25 | +clap.workspace = true | |
| 26 | +tracing.workspace = true | |
| 27 | +tracing-subscriber.workspace = true | |
| 28 | +thiserror.workspace = true | |
| 29 | +anyhow.workspace = true | |
| 30 | +dirs.workspace = true | |
| 31 | +nix.workspace = true | |
| 32 | +users.workspace = true | |
gartop/src/collector/mod.rsadded@@ -0,0 +1,5 @@ | ||
| 1 | +//! System data collectors | |
| 2 | +//! | |
| 3 | +//! Collects CPU, memory, and process data from procfs. | |
| 4 | + | |
| 5 | +// TODO: Sprint 2 - implement collectors | |
gartop/src/config.rsadded@@ -0,0 +1,98 @@ | ||
| 1 | +//! Configuration for gartop | |
| 2 | + | |
| 3 | +use anyhow::Result; | |
| 4 | +use serde::{Deserialize, Serialize}; | |
| 5 | +use std::path::PathBuf; | |
| 6 | + | |
| 7 | +/// Main configuration. | |
| 8 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 9 | +#[serde(default)] | |
| 10 | +pub struct Config { | |
| 11 | + pub daemon: DaemonConfig, | |
| 12 | + pub gui: GuiConfig, | |
| 13 | +} | |
| 14 | + | |
| 15 | +/// Daemon configuration. | |
| 16 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 17 | +#[serde(default)] | |
| 18 | +pub struct DaemonConfig { | |
| 19 | + /// Sample interval in milliseconds. | |
| 20 | + pub sample_interval_ms: u64, | |
| 21 | + /// History buffer size (number of samples to keep). | |
| 22 | + pub history_size: usize, | |
| 23 | + /// Maximum number of processes to track. | |
| 24 | + pub max_processes: usize, | |
| 25 | +} | |
| 26 | + | |
| 27 | +/// GUI configuration. | |
| 28 | +#[derive(Debug, Clone, Serialize, Deserialize)] | |
| 29 | +#[serde(default)] | |
| 30 | +pub struct GuiConfig { | |
| 31 | + /// Initial window width. | |
| 32 | + pub width: u32, | |
| 33 | + /// Initial window height. | |
| 34 | + pub height: u32, | |
| 35 | + /// Refresh rate in FPS. | |
| 36 | + pub refresh_rate: u32, | |
| 37 | + /// Show legend on graphs. | |
| 38 | + pub show_legend: bool, | |
| 39 | + /// Font family for UI. | |
| 40 | + pub font_family: String, | |
| 41 | + /// Font size. | |
| 42 | + pub font_size: f64, | |
| 43 | +} | |
| 44 | + | |
| 45 | +impl Default for Config { | |
| 46 | + fn default() -> Self { | |
| 47 | + Self { | |
| 48 | + daemon: DaemonConfig::default(), | |
| 49 | + gui: GuiConfig::default(), | |
| 50 | + } | |
| 51 | + } | |
| 52 | +} | |
| 53 | + | |
| 54 | +impl Default for DaemonConfig { | |
| 55 | + fn default() -> Self { | |
| 56 | + Self { | |
| 57 | + sample_interval_ms: 1000, | |
| 58 | + history_size: 300, | |
| 59 | + max_processes: 100, | |
| 60 | + } | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +impl Default for GuiConfig { | |
| 65 | + fn default() -> Self { | |
| 66 | + Self { | |
| 67 | + width: 600, | |
| 68 | + height: 500, | |
| 69 | + refresh_rate: 30, | |
| 70 | + show_legend: true, | |
| 71 | + font_family: "sans-serif".to_string(), | |
| 72 | + font_size: 12.0, | |
| 73 | + } | |
| 74 | + } | |
| 75 | +} | |
| 76 | + | |
| 77 | +impl Config { | |
| 78 | + /// Load configuration from file. | |
| 79 | + pub fn load(path: Option<&str>) -> Result<Self> { | |
| 80 | + let config_path = path.map(PathBuf::from).or_else(Self::default_path); | |
| 81 | + | |
| 82 | + if let Some(path) = config_path { | |
| 83 | + if path.exists() { | |
| 84 | + let content = std::fs::read_to_string(&path)?; | |
| 85 | + let config: Config = toml::from_str(&content)?; | |
| 86 | + tracing::info!("Loaded config from {}", path.display()); | |
| 87 | + return Ok(config); | |
| 88 | + } | |
| 89 | + } | |
| 90 | + | |
| 91 | + Ok(Self::default()) | |
| 92 | + } | |
| 93 | + | |
| 94 | + /// Get default configuration path. | |
| 95 | + fn default_path() -> Option<PathBuf> { | |
| 96 | + dirs::config_dir().map(|d| d.join("gartop").join("config.toml")) | |
| 97 | + } | |
| 98 | +} | |
gartop/src/daemon/mod.rsadded@@ -0,0 +1,10 @@ | ||
| 1 | +//! Daemon mode implementation | |
| 2 | + | |
| 3 | +use anyhow::Result; | |
| 4 | + | |
| 5 | +/// Run the gartop daemon. | |
| 6 | +pub async fn run(_config_path: Option<String>, _foreground: bool) -> Result<()> { | |
| 7 | + // TODO: Sprint 2/3 - implement daemon | |
| 8 | + tracing::info!("Daemon not yet implemented"); | |
| 9 | + Ok(()) | |
| 10 | +} | |
gartop/src/error.rsadded@@ -0,0 +1,27 @@ | ||
| 1 | +//! Error types for gartop | |
| 2 | + | |
| 3 | +use thiserror::Error; | |
| 4 | + | |
| 5 | +/// Library errors | |
| 6 | +#[derive(Debug, Error)] | |
| 7 | +pub enum Error { | |
| 8 | + #[error("procfs error: {0}")] | |
| 9 | + Procfs(#[from] procfs::ProcError), | |
| 10 | + | |
| 11 | + #[error("IO error: {0}")] | |
| 12 | + Io(#[from] std::io::Error), | |
| 13 | + | |
| 14 | + #[error("IPC error: {0}")] | |
| 15 | + Ipc(String), | |
| 16 | + | |
| 17 | + #[error("process not found: {0}")] | |
| 18 | + ProcessNotFound(i32), | |
| 19 | + | |
| 20 | + #[error("permission denied: {0}")] | |
| 21 | + PermissionDenied(String), | |
| 22 | + | |
| 23 | + #[error("config error: {0}")] | |
| 24 | + Config(String), | |
| 25 | +} | |
| 26 | + | |
| 27 | +pub type Result<T> = std::result::Result<T, Error>; | |
gartop/src/gui/mod.rsadded@@ -0,0 +1,10 @@ | ||
| 1 | +//! GUI implementation using gartk | |
| 2 | + | |
| 3 | +use anyhow::Result; | |
| 4 | + | |
| 5 | +/// Run the gartop GUI. | |
| 6 | +pub async fn run() -> Result<()> { | |
| 7 | + // TODO: Sprint 4 - implement GUI | |
| 8 | + tracing::info!("GUI not yet implemented"); | |
| 9 | + Ok(()) | |
| 10 | +} | |
gartop/src/ipc/mod.rsadded@@ -0,0 +1,6 @@ | ||
| 1 | +//! IPC server and client handling | |
| 2 | + | |
| 3 | +// Re-export from gartop-ipc | |
| 4 | +pub use gartop_ipc::{Command, Event, Response}; | |
| 5 | + | |
| 6 | +// TODO: Sprint 3 - implement server and client | |
gartop/src/main.rsadded@@ -0,0 +1,71 @@ | ||
| 1 | +//! gartop - System monitor for gar desktop | |
| 2 | +//! | |
| 3 | +//! A daemon-based system monitor with real-time CPU, memory, and process | |
| 4 | +//! monitoring. Uses gartk for GUI rendering. | |
| 5 | + | |
| 6 | +use anyhow::Result; | |
| 7 | +use clap::{Parser, Subcommand}; | |
| 8 | +use tracing_subscriber::EnvFilter; | |
| 9 | + | |
| 10 | +mod collector; | |
| 11 | +mod config; | |
| 12 | +mod daemon; | |
| 13 | +mod error; | |
| 14 | +mod gui; | |
| 15 | +mod ipc; | |
| 16 | + | |
| 17 | +/// gartop - System monitor for gar desktop | |
| 18 | +#[derive(Parser)] | |
| 19 | +#[command(name = "gartop")] | |
| 20 | +#[command(about = "System monitor for gar desktop", long_about = None)] | |
| 21 | +struct Cli { | |
| 22 | + #[command(subcommand)] | |
| 23 | + command: Option<Commands>, | |
| 24 | + | |
| 25 | + /// Run in foreground (don't daemonize) | |
| 26 | + #[arg(short, long)] | |
| 27 | + foreground: bool, | |
| 28 | + | |
| 29 | + /// Configuration file path | |
| 30 | + #[arg(short, long)] | |
| 31 | + config: Option<String>, | |
| 32 | +} | |
| 33 | + | |
| 34 | +#[derive(Subcommand)] | |
| 35 | +enum Commands { | |
| 36 | + /// Start the daemon (default) | |
| 37 | + Daemon { | |
| 38 | + /// Run in foreground | |
| 39 | + #[arg(short, long)] | |
| 40 | + foreground: bool, | |
| 41 | + }, | |
| 42 | + /// Open the GUI window (connects to daemon) | |
| 43 | + Gui, | |
| 44 | +} | |
| 45 | + | |
| 46 | +#[tokio::main] | |
| 47 | +async fn main() -> Result<()> { | |
| 48 | + tracing_subscriber::fmt() | |
| 49 | + .with_env_filter( | |
| 50 | + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), | |
| 51 | + ) | |
| 52 | + .init(); | |
| 53 | + | |
| 54 | + let cli = Cli::parse(); | |
| 55 | + | |
| 56 | + match cli.command { | |
| 57 | + Some(Commands::Daemon { foreground }) => { | |
| 58 | + tracing::info!("Starting gartop daemon"); | |
| 59 | + daemon::run(cli.config, foreground || cli.foreground).await | |
| 60 | + } | |
| 61 | + Some(Commands::Gui) => { | |
| 62 | + tracing::info!("Starting gartop GUI"); | |
| 63 | + gui::run().await | |
| 64 | + } | |
| 65 | + None => { | |
| 66 | + // Default: start daemon | |
| 67 | + tracing::info!("Starting gartop daemon"); | |
| 68 | + daemon::run(cli.config, cli.foreground).await | |
| 69 | + } | |
| 70 | + } | |
| 71 | +} | |
gartopctl/Cargo.tomladded@@ -0,0 +1,19 @@ | ||
| 1 | +[package] | |
| 2 | +name = "gartopctl" | |
| 3 | +version.workspace = true | |
| 4 | +edition.workspace = true | |
| 5 | +authors.workspace = true | |
| 6 | +license.workspace = true | |
| 7 | +description = "Control CLI for gartop daemon" | |
| 8 | + | |
| 9 | +[[bin]] | |
| 10 | +name = "gartopctl" | |
| 11 | +path = "src/main.rs" | |
| 12 | + | |
| 13 | +[dependencies] | |
| 14 | +gartop-ipc.workspace = true | |
| 15 | +serde.workspace = true | |
| 16 | +serde_json.workspace = true | |
| 17 | +clap.workspace = true | |
| 18 | +anyhow.workspace = true | |
| 19 | +dirs.workspace = true | |
gartopctl/src/main.rsadded@@ -0,0 +1,121 @@ | ||
| 1 | +//! gartopctl - Control CLI for gartop daemon | |
| 2 | + | |
| 3 | +use anyhow::{Context, Result}; | |
| 4 | +use clap::{Parser, Subcommand}; | |
| 5 | +use gartop_ipc::{Command, Response, SortField}; | |
| 6 | +use std::io::{BufRead, BufReader, Write}; | |
| 7 | +use std::os::unix::net::UnixStream; | |
| 8 | + | |
| 9 | +#[derive(Parser)] | |
| 10 | +#[command(name = "gartopctl")] | |
| 11 | +#[command(about = "Control the gartop daemon", long_about = None)] | |
| 12 | +struct Cli { | |
| 13 | + #[command(subcommand)] | |
| 14 | + command: Commands, | |
| 15 | +} | |
| 16 | + | |
| 17 | +#[derive(Subcommand)] | |
| 18 | +enum Commands { | |
| 19 | + /// Get daemon status | |
| 20 | + Status, | |
| 21 | + /// Get current CPU stats | |
| 22 | + Cpu, | |
| 23 | + /// Get current memory stats | |
| 24 | + Memory, | |
| 25 | + /// Get CPU usage history | |
| 26 | + CpuHistory { | |
| 27 | + /// Number of samples | |
| 28 | + #[arg(short, long)] | |
| 29 | + count: Option<usize>, | |
| 30 | + }, | |
| 31 | + /// Get memory usage history | |
| 32 | + MemoryHistory { | |
| 33 | + /// Number of samples | |
| 34 | + #[arg(short, long)] | |
| 35 | + count: Option<usize>, | |
| 36 | + }, | |
| 37 | + /// List running processes | |
| 38 | + Processes { | |
| 39 | + /// Sort by field (cpu, memory, pid, name) | |
| 40 | + #[arg(short, long, default_value = "cpu")] | |
| 41 | + sort: String, | |
| 42 | + /// Limit results | |
| 43 | + #[arg(short, long)] | |
| 44 | + limit: Option<usize>, | |
| 45 | + }, | |
| 46 | + /// Kill a process | |
| 47 | + Kill { | |
| 48 | + /// Process ID | |
| 49 | + pid: i32, | |
| 50 | + /// Signal number (default: 15 SIGTERM) | |
| 51 | + #[arg(short, long)] | |
| 52 | + signal: Option<i32>, | |
| 53 | + }, | |
| 54 | + /// Reload configuration | |
| 55 | + Reload, | |
| 56 | + /// Stop the daemon | |
| 57 | + Quit, | |
| 58 | +} | |
| 59 | + | |
| 60 | +fn main() -> Result<()> { | |
| 61 | + let cli = Cli::parse(); | |
| 62 | + | |
| 63 | + let cmd = match cli.command { | |
| 64 | + Commands::Status => Command::Status, | |
| 65 | + Commands::Cpu => Command::GetCpu, | |
| 66 | + Commands::Memory => Command::GetMemory, | |
| 67 | + Commands::CpuHistory { count } => Command::GetCpuHistory { count }, | |
| 68 | + Commands::MemoryHistory { count } => Command::GetMemoryHistory { count }, | |
| 69 | + Commands::Processes { sort, limit } => { | |
| 70 | + let sort_by = match sort.as_str() { | |
| 71 | + "memory" | "mem" => Some(SortField::Memory), | |
| 72 | + "pid" => Some(SortField::Pid), | |
| 73 | + "name" => Some(SortField::Name), | |
| 74 | + _ => Some(SortField::Cpu), | |
| 75 | + }; | |
| 76 | + Command::GetProcesses { sort_by, limit } | |
| 77 | + } | |
| 78 | + Commands::Kill { pid, signal } => Command::KillProcess { pid, signal }, | |
| 79 | + Commands::Reload => Command::Reload, | |
| 80 | + Commands::Quit => Command::Quit, | |
| 81 | + }; | |
| 82 | + | |
| 83 | + let response = send_command(&cmd)?; | |
| 84 | + | |
| 85 | + if response.success { | |
| 86 | + if let Some(data) = response.data { | |
| 87 | + println!("{}", serde_json::to_string_pretty(&data)?); | |
| 88 | + } else { | |
| 89 | + println!("OK"); | |
| 90 | + } | |
| 91 | + } else { | |
| 92 | + eprintln!("Error: {}", response.error.unwrap_or_default()); | |
| 93 | + std::process::exit(1); | |
| 94 | + } | |
| 95 | + | |
| 96 | + Ok(()) | |
| 97 | +} | |
| 98 | + | |
| 99 | +fn send_command(cmd: &Command) -> Result<Response> { | |
| 100 | + let socket_path = gartop_ipc::socket_path(); | |
| 101 | + | |
| 102 | + if !socket_path.exists() { | |
| 103 | + anyhow::bail!("gartop daemon not running (socket not found at {})", socket_path.display()); | |
| 104 | + } | |
| 105 | + | |
| 106 | + let mut stream = UnixStream::connect(&socket_path) | |
| 107 | + .with_context(|| "Failed to connect to gartop daemon")?; | |
| 108 | + | |
| 109 | + // Send command as JSON line | |
| 110 | + let json = serde_json::to_string(cmd)?; | |
| 111 | + writeln!(stream, "{}", json)?; | |
| 112 | + stream.flush()?; | |
| 113 | + | |
| 114 | + // Read response | |
| 115 | + let mut reader = BufReader::new(stream); | |
| 116 | + let mut line = String::new(); | |
| 117 | + reader.read_line(&mut line)?; | |
| 118 | + | |
| 119 | + let response: Response = serde_json::from_str(&line)?; | |
| 120 | + Ok(response) | |
| 121 | +} | |