Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ license = "GPL-3.0"

[workspace.dependencies]
tokio = { version = "1.52.1", features = ["full"] }
zbus = { version = "5.15.0", features = ["tokio"] }
zbus = { version = "5.15.0", features = ["tokio", "option-as-array"] }
clap = { version = "4.6.1", features = ["derive"] }
config = { version = "0.15.19", features = ["toml"] }
serde = { version = "1.0.228", features = ["derive"] }
Expand Down
4 changes: 3 additions & 1 deletion crates/cardwire-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ license.workspace = true
description = "CLI for cardwire GPU management"

[dependencies]
cardwire-core = { path = "../cardwire-core" }
tokio.workspace = true
zbus.workspace = true
clap.workspace = true
clap_complete.workspace = true
anyhow.workspace = true
serde_json.workspace = true
serde.workspace = true

[[bin]]
name = "cardwire"
path = "src/main.rs"
11 changes: 10 additions & 1 deletion crates/cardwire-cli/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
use clap::{ArgAction, Args as ClapArgs, Parser, Subcommand, ValueEnum};
use clap_complete::Shell;
use std::fmt;
#[derive(Clone, Debug, ValueEnum)]
pub enum CliMode {
Integrated,
Hybrid,
Manual,
}

impl fmt::Display for CliMode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CliMode::Integrated => write!(f, "Integrated"),
CliMode::Hybrid => write!(f, "Hybrid"),
CliMode::Manual => write!(f, "Manual"),
}
}
}
#[derive(Parser)]
#[command(version, about)]
pub struct Args {
Expand Down
19 changes: 12 additions & 7 deletions crates/cardwire-cli/src/dbus.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use cardwire_core::gpu::GpuRow;
use crate::display::{GpuDevice, PciDevice};
use std::collections::BTreeMap;

use zbus::{Proxy, connection::Connection};
pub struct DaemonClient<'a> {
proxy: Proxy<'a>,
Expand All @@ -17,16 +19,19 @@ impl<'a> DaemonClient<'a> {
Ok(Self { proxy })
}

pub async fn set_mode(&self, mode: &String) -> zbus::Result<()> {
self.proxy.call("SetMode", &(mode,)).await
pub async fn set_mode(&self, mode: &u32) -> zbus::fdo::Result<()> {
self.proxy.set_property("Mode", mode).await
}

pub async fn get_mode(&self) -> zbus::Result<String> {
self.proxy.call("GetMode", &()).await
pub async fn get_mode(&self) -> zbus::Result<u32> {
self.proxy.get_property("Mode").await
}

pub async fn list_gpus(&self) -> zbus::Result<Vec<GpuRow>> {
self.proxy.call("ListGpus", &()).await
pub async fn list_devices(&self) -> zbus::Result<BTreeMap<usize, GpuDevice>> {
self.proxy.call("ListDevices", &()).await
}
pub async fn list_devices_pci(&self) -> zbus::Result<BTreeMap<String, PciDevice>> {
self.proxy.call("ListDevicesPci", &()).await
}

pub async fn set_gpu_block(&self, id: u32, blocked: bool) -> zbus::Result<()> {
Expand Down
121 changes: 121 additions & 0 deletions crates/cardwire-cli/src/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! The purpose of this file is to format the received String from daemon into a displayable format
//! for the user

use std::collections::BTreeMap;

use anyhow::{Ok, Result};
// Define the struct here instead of importing from cardwire_core,
// I want cardwire-cli to be independent of the rest of cardwire
// This allow other dev to make their own client for cardwire
// Here the struct are used to parse the json
#[derive(serde::Deserialize, serde::Serialize, zbus::zvariant::Type, Debug)]
pub struct GpuDevice {
id: u32,
name: String,
pci: String,
render: u32,
card: u32,
default: bool,
blocked: bool,
nvidia: bool,
nvidia_minor: String,
}
#[derive(serde::Deserialize, serde::Serialize, zbus::zvariant::Type)]
pub struct PciDevice {
pci_address: String,
iommu_group: String,
vendor_id: String,
device_id: String,
vendor_name: String,
device_name: String,
driver: String,
class: String,
}

/// Take a Map and print it
pub fn print_devices(gpu_list: BTreeMap<usize, GpuDevice>, is_json: bool) -> Result<()> {
if is_json {
println!("{}", serde_json::to_string_pretty(&gpu_list)?);
} else {
pretty_print_gpu(gpu_list);
};

Ok(())
}
/// Take a Map and print it
pub fn print_devices_pci(pci_list: BTreeMap<String, PciDevice>) -> Result<()> {
println!("{}", serde_json::to_string_pretty(&pci_list)?);
Ok(())
}
/// Take a Map and print it into a good looking table
fn pretty_print_gpu(gpu_list: BTreeMap<usize, GpuDevice>) {
let mut id_w = 2usize;
let mut name_w = 4usize;
let mut pci_w = 3usize;
let mut render_w = 6usize;
let mut card_w = 4usize;
let default_w = 7usize;
let blocked_w = 7usize;

// Calculate widths
for (id, gpu) in &gpu_list {
id_w = id_w.max(*id);
name_w = name_w.max(gpu.name.len());
pci_w = pci_w.max(gpu.pci.len());
// Full render string is "renderD" + device number
let render_full = format!("renderD{}", gpu.render);
render_w = render_w.max(render_full.len());
let card_full = format!("card{}", gpu.card);
card_w = card_w.max(card_full.len());
}

// Header
println!(
"{:<id_w$} {:<name_w$} {:<pci_w$} {:<render_w$} {:<card_w$} {:<default_w$} {:<blocked_w$}",
"ID",
"NAME",
"PCI",
"RENDER",
"CARD",
"DEFAULT",
"BLOCKED",
id_w = id_w,
name_w = name_w,
pci_w = pci_w,
render_w = render_w,
card_w = card_w,
default_w = default_w,
blocked_w = blocked_w,
);
println!(
"{} {} {} {} {} {} {}",
"-".repeat(id_w),
"-".repeat(name_w),
"-".repeat(pci_w),
"-".repeat(render_w),
"-".repeat(card_w),
"-".repeat(default_w),
"-".repeat(blocked_w),
);
for (_, gpu) in gpu_list {
let render_full = format!("renderD{}", gpu.render);
let card_full = format!("card{}", gpu.card);
println!(
"{:<id_w$} {:<name_w$} {:<pci_w$} {:<render_w$} {:<card_w$} {:<default_w$} {:<blocked_w$}",
gpu.id,
gpu.name,
gpu.pci,
render_full,
card_full,
if gpu.default { "(*)" } else { "( )" },
gpu.blocked,
id_w = id_w,
name_w = name_w,
pci_w = pci_w,
render_w = render_w,
card_w = card_w,
default_w = default_w,
blocked_w = blocked_w,
);
}
}
86 changes: 52 additions & 34 deletions crates/cardwire-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,72 @@
mod args;
mod dbus;
mod output;
mod display;
use args::{Args, CliMode, Commands};
use clap::{CommandFactory, Parser};
use dbus::DaemonClient;

const BIN_NAME: &str = "cardwire";
use crate::display::{print_devices, print_devices_pci};

fn handle_error(err: zbus::Error) {
match err {
zbus::Error::MethodError(name, description, _) => {
eprintln!("{}", description.unwrap_or_else(|| name.to_string()))
}
zbus::Error::FDO(fdo_err) => match *fdo_err {
zbus::fdo::Error::ServiceUnknown(content) => {
eprint!("error: {} \n is the service up?", content)
}
other => eprintln!("FDO error: {}", other),
},
e => eprintln!("error: {e:?}"),
}
}
const BIN_NAME: &str = "cardwire";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
/*
Handle completion before connecting to dbus
*/

// Handle completion before connecting to dbus
if let Commands::Completion { shell } = args.command {
let mut cmd = Args::command();
clap_complete::generate(shell, &mut cmd, BIN_NAME, &mut std::io::stdout());
return Ok(());
}
// Now connect
let connection: zbus::Connection = zbus::connection::Builder::system()?.build().await?;
let client: DaemonClient<'_> = DaemonClient::connect(&connection).await?;

match args.command {
Commands::Set { mode } => {
let mode_string = match mode {
CliMode::Integrated => "integrated".to_string(),
CliMode::Hybrid => "hybrid".to_string(),
CliMode::Manual => "manual".to_string(),
let mode_u32 = match mode {
CliMode::Integrated => 0,
CliMode::Hybrid => 1,
CliMode::Manual => 2,
};

match client.set_mode(&mode_string).await {
Ok(_) => println!("Mode has been set to {}", mode_string),
Err(e) => handle_error(e),
match client.set_mode(&mode_u32).await {
Ok(_) => println!("Mode has been set to {}", mode),
Err(e) => handle_error(e.into()),
};
}
Commands::Get => {
match client.get_mode().await {
Ok(response) => println!("{}", response),
Ok(response) => {
let response: CliMode = match response {
0 => CliMode::Integrated,
1 => CliMode::Hybrid,
2 => CliMode::Manual,
// shouldn't happen
_ => CliMode::Manual,
};
println!("Current Mode: {}", response)
}
Err(e) => handle_error(e),
};
}
Commands::List { full: _, json: _ } => match client.list_gpus().await {
Ok(mut response) => {
response.sort_by_key(|row| row.0);
output::print_gpu_table(&response);
Commands::List { full, json } => {
if full {
match client.list_devices_pci().await {
Ok(response) => {
print_devices_pci(response)?;
}
Err(e) => handle_error(e),
}
} else {
match client.list_devices().await {
Ok(response) => {
print_devices(response, json)?;
}
Err(e) => handle_error(e),
}
}
Err(e) => handle_error(e),
},
}
Commands::Gpu { id, action } => {
match client.set_gpu_block(id, action.block).await {
Ok(_) => println!("Mode has been set to {} on GPU {}", action.block, id),
Expand All @@ -74,3 +78,17 @@ async fn main() -> anyhow::Result<()> {

Ok(())
}
fn handle_error(err: zbus::Error) {
match err {
zbus::Error::MethodError(name, description, _) => {
eprintln!("{}", description.unwrap_or_else(|| name.to_string()))
}
zbus::Error::FDO(fdo_err) => match *fdo_err {
zbus::fdo::Error::ServiceUnknown(content) => {
eprint!("error: {} \n is the service up?", content)
}
other => eprintln!("FDO error: {}", other),
},
e => eprintln!("error: {e:?}"),
}
}
Loading