From 8b63d4988a30617a28a8c49088d82b1ae235d477 Mon Sep 17 00:00:00 2001 From: luytan Date: Tue, 28 Apr 2026 16:51:36 +0200 Subject: [PATCH 01/12] feat: allow root to communicate with the daemon --- assets/com.github.opengamingcollective.cardwire.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/com.github.opengamingcollective.cardwire.conf b/assets/com.github.opengamingcollective.cardwire.conf index ac0b9a8..d10fa7c 100644 --- a/assets/com.github.opengamingcollective.cardwire.conf +++ b/assets/com.github.opengamingcollective.cardwire.conf @@ -4,6 +4,8 @@ + + From 8fc11641adbdaca483c2657f94cd89fc392273e8 Mon Sep 17 00:00:00 2001 From: Luytan Date: Tue, 28 Apr 2026 17:02:46 +0200 Subject: [PATCH 02/12] ci: introduce ci-vm with 2, 3 and 15 gpus (#6) * ci: rename nix vm from test-vm to ci-vm * ci: introduce ci-vm with 2; 3 and 15 gpus * ci: update ci-vm.yml for the new vms --- .github/workflows/ci-vm.yml | 24 +++++ .github/workflows/test-vm.yml | 19 ---- flake.nix | 10 +- nix/ci-15gpu.nix | 123 ++++++++++++++++++++++ nix/{integration-test.nix => ci-2gpu.nix} | 19 +++- nix/ci-3gpu.nix | 97 +++++++++++++++++ 6 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/ci-vm.yml delete mode 100644 .github/workflows/test-vm.yml create mode 100644 nix/ci-15gpu.nix rename nix/{integration-test.nix => ci-2gpu.nix} (68%) create mode 100644 nix/ci-3gpu.nix diff --git a/.github/workflows/ci-vm.yml b/.github/workflows/ci-vm.yml new file mode 100644 index 0000000..ae711b7 --- /dev/null +++ b/.github/workflows/ci-vm.yml @@ -0,0 +1,24 @@ +name: Integration Tests in a Nix VM +on: + pull_request: + types: [opened, labeled] + branches: + - main + - dev + paths: + - 'crates/**' + - 'workflows/**' + - 'nix/**' +jobs: + run-test: + if: ${{ github.event.label.name == 'run-vm-test' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: cachix/install-nix-action@v31 + - name: Run Integration Tests with 2 GPUs + run: nix build -L .#checks.x86_64-linux.vm-ci-2gpu + - name: Run Integration Tests with 3 GPUs + run: nix build -L .#checks.x86_64-linux.vm-ci-3gpu + - name: Run Integration Tests with 15 GPUs + run: nix build -L .#checks.x86_64-linux.vm-ci-15gpu diff --git a/.github/workflows/test-vm.yml b/.github/workflows/test-vm.yml deleted file mode 100644 index 2ab89a9..0000000 --- a/.github/workflows/test-vm.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: test-vm -on: - pull_request: - types: [opened, labeled] - branches: - - main - paths: - - 'crates/**' - - 'workflows/**' - - 'nix/**' -jobs: - run-test: - if: ${{ github.event.label.name == 'run-vm-test' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: cachix/install-nix-action@v31 - - name: Run Integration Tests - run: nix build -L .#checks.x86_64-linux.vm-test diff --git a/flake.nix b/flake.nix index 24dd6de..eb94d66 100644 --- a/flake.nix +++ b/flake.nix @@ -70,7 +70,15 @@ } ); checks = forAllSystems (system: { - vm-test = import ./nix/integration-test.nix { + vm-ci-2gpu = import ./nix/ci-2gpu.nix { + inherit pkgs system self; + lib = nixpkgs.lib; + }; + vm-ci-3gpu = import ./nix/ci-3gpu.nix { + inherit pkgs system self; + lib = nixpkgs.lib; + }; + vm-ci-15gpu = import ./nix/ci-15gpu.nix { inherit pkgs system self; lib = nixpkgs.lib; }; diff --git a/nix/ci-15gpu.nix b/nix/ci-15gpu.nix new file mode 100644 index 0000000..bafa7b5 --- /dev/null +++ b/nix/ci-15gpu.nix @@ -0,0 +1,123 @@ +{ + pkgs, + system, + self, + lib, +}: +(pkgs system).testers.runNixOSTest { + name = "cardwire-test"; + nodes.machine = + { + config, + lib, + ... + }: + { + imports = [ + self.nixosModules.default + ./vm-configuration.nix + ]; + + virtualisation = { + memorySize = 1024; + graphics = false; + diskImage = null; + qemu.options = [ + "-machine q35,accel=kvm,kernel-irqchip=split" + "-device intel-iommu,intremap=on,device-iotlb=on" + "-vga none" + "-device virtio-gpu-pci,id=igpu,max_outputs=2" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + ]; + }; + networking.useDHCP = false; + networking.interfaces = lib.mkForce { }; + }; + + testScript = '' + machine.start() + machine.wait_for_unit("default.target") + with subtest("Wait for boot and services"): + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("dbus.service") + machine.wait_for_unit("cardwired.service") + + with subtest("Check for DRM Devices"): + # Check the DRM devices + t.assertIn("33", machine.succeed("ls -a /dev/dri | wc -l"), "Missing DRMs, must be 31") + + with subtest("Ensure cardwire is started and dbus works"): + machine.wait_until_succeeds("su - john -c 'cardwire help'") + + with subtest("Ensure files are present"): + machine.succeed("cat /etc/cardwire/cardwire.toml") + machine.succeed("cat /var/lib/cardwire/gpu_state.json") + machine.succeed("cat /var/lib/cardwire/mode.json") + + with subtest("Check if cardwire found all gpus"): + t.assertIn("17", machine.succeed("cardwire list | wc -l"), "Must be 17 (15 GPUs + 2 headers)") + + with subtest("Try to switch to integrated and hybrid"): + t.assertIn("Couldn't set mode to Integrated, the mode require exactly 2 GPUs", machine.succeed("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + t.assertIn("Couldn't set mode to Hybrid, the mode require exactly 2 GPUs", machine.succeed("cardwire set hybrid 2>&1"), "Mode has been switched to hybrid") + + with subtest("Set to manual, and block 14 gpus"): + machine.succeed("cardwire set manual") + t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + + for x in range(1, 15): + machine.succeed(f'cardwire gpu {x} --block') + + with subtest("Check cardwire to see if 14 gpus got blocked"): + t.assertIn("14", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only 13 or less got blocked") + + for x in range(1, 15): + cardid = 0 + x + renderid = 128 + x + machine.succeed(f'cardwire gpu {x} --block') + machine.fail(f": < /dev/dri/renderD{renderid}") + machine.fail(f": < /dev/dri/card{cardid}") + machine.succeed(": < /dev/dri/renderD128") + machine.succeed(": < /dev/dri/card0") + + with subtest("Check gpu_state.json to see if two gpus got blocked"): + t.assertIn("14", machine.succeed("cat /var/lib/cardwire/gpu_state.json|grep true|wc -l"), "Only 13 or less got blocked") + + + with subtest("Restart cardwire and check if gpu states got re-applied"): + machine.wait_until_succeeds("systemctl stop cardwired.service") + + for x in range(0, 15): + cardid = 0 + x + renderid = 128 + x + machine.succeed(f": < /dev/dri/renderD{renderid}") + machine.succeed(f": < /dev/dri/card{cardid}") + + machine.wait_until_succeeds("systemctl start cardwired.service") + + t.assertIn("14", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only 13 or less got blocked") + + for x in range(1, 15): + cardid = 0 + x + renderid = 128 + x + machine.succeed(f'cardwire gpu {x} --block') + machine.fail(f": < /dev/dri/renderD{renderid}") + machine.fail(f": < /dev/dri/card{cardid}") + machine.succeed(": < /dev/dri/renderD128") + machine.succeed(": < /dev/dri/card0") + ''; + +} diff --git a/nix/integration-test.nix b/nix/ci-2gpu.nix similarity index 68% rename from nix/integration-test.nix rename to nix/ci-2gpu.nix index 439dda6..58a3875 100644 --- a/nix/integration-test.nix +++ b/nix/ci-2gpu.nix @@ -49,17 +49,28 @@ t.assertIn("renderD129", machine.succeed("ls -a /dev/dri"), "Missing DRM") t.assertIn("card1", machine.succeed("ls -a /dev/dri"), "Missing DRM") - with subtest("Ensure cardwire is started"): + with subtest("Ensure cardwire is started and dbus works"): machine.wait_until_succeeds("su - john -c 'cardwire help'") + with subtest("Ensure files are present"): + machine.succeed("cat /etc/cardwire/cardwire.toml") + machine.succeed("cat /var/lib/cardwire/gpu_state.json") + machine.succeed("cat /var/lib/cardwire/mode.json") + with subtest("Switch to Integrated mode"): # Check if cardwire detect both video card - t.assertIn("renderD128", machine.succeed("su - john -c 'cardwire list'"), "Missing RenderD128 in cardwire") + t.assertIn("renderD128", machine.succeed("cardwire list"), "Missing RenderD128 in cardwire") machine.succeed("test -e /dev/dri/renderD129") - t.assertIn("Mode has been set to integrated", machine.succeed("su - john -c 'cardwire set integrated'"), "Couldn't set to integrated mode") + t.assertIn("Mode has been set to integrated", machine.succeed("cardwire set integrated"), "Couldn't set to integrated mode") machine.fail(": < /dev/dri/renderD129") + t.assertIn("Integrated", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") + with subtest("Switchback to hybrid mode"): - t.assertIn("Mode has been set to hybrid", machine.succeed("su - john -c 'cardwire set hybrid'"), "Couldn't set to hybrid mode") + t.assertIn("Mode has been set to hybrid", machine.succeed("cardwire set hybrid"), "Couldn't set to hybrid mode") machine.succeed(": < /dev/dri/renderD129") + t.assertIn("Hybrid", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") + + with subtest("Try to block default gpu"): + t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") ''; } diff --git a/nix/ci-3gpu.nix b/nix/ci-3gpu.nix new file mode 100644 index 0000000..5c89a41 --- /dev/null +++ b/nix/ci-3gpu.nix @@ -0,0 +1,97 @@ +{ + pkgs, + system, + self, + lib, +}: +(pkgs system).testers.runNixOSTest { + name = "cardwire-test"; + nodes.machine = + { + config, + lib, + ... + }: + { + imports = [ + self.nixosModules.default + ./vm-configuration.nix + ]; + + virtualisation = { + memorySize = 1024; + graphics = false; + diskImage = null; + qemu.options = [ + "-machine q35,accel=kvm,kernel-irqchip=split" + "-device intel-iommu,intremap=on,device-iotlb=on" + "-vga none" + "-device virtio-gpu-pci,id=igpu,max_outputs=2" + "-device virtio-gpu-pci,max_outputs=1" + "-device virtio-gpu-pci,max_outputs=1" + ]; + }; + networking.useDHCP = false; + networking.interfaces = lib.mkForce { }; + }; + + testScript = '' + machine.start() + machine.wait_for_unit("default.target") + with subtest("Wait for boot and services"): + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("dbus.service") + machine.wait_for_unit("cardwired.service") + + with subtest("Check for DRM Devices"): + # Check the DRM devices + t.assertIn("renderD128", machine.succeed("ls -a /dev/dri"), "Missing DRM") + t.assertIn("card0", machine.succeed("ls -a /dev/dri"), "Missing DRM") + t.assertIn("renderD129", machine.succeed("ls -a /dev/dri"), "Missing DRM") + t.assertIn("card1", machine.succeed("ls -a /dev/dri"), "Missing DRM") + t.assertIn("renderD130", machine.succeed("ls -a /dev/dri"), "Missing DRM") + t.assertIn("card2", machine.succeed("ls -a /dev/dri"), "Missing DRM") + + with subtest("Ensure cardwire is started and dbus works"): + machine.wait_until_succeeds("su - john -c 'cardwire help'") + + with subtest("Try to switch to integrated and hybrid"): + t.assertIn("Couldn't set mode to Integrated, the mode require exactly 2 GPUs", machine.succeed("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + t.assertIn("Couldn't set mode to Hybrid, the mode require exactly 2 GPUs", machine.succeed("cardwire set hybrid 2>&1"), "Mode has been switched to hybrid") + + with subtest("Set to manual, and block two gpus"): + machine.succeed("cardwire set manual") + machine.succeed("cardwire gpu 1 --block") + machine.succeed("cardwire gpu 2 --block") + t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + + + with subtest("Check gpu_state.json to see if two gpus got blocked"): + t.assertIn("2", machine.succeed("cat /var/lib/cardwire/gpu_state.json|grep true|wc -l"), "Only one or less GPU got blocked") + + with subtest("Check cardwire to see if two gpus got blocked"): + t.assertIn("2", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only one or less GPU got blocked") + machine.fail(": < /dev/dri/renderD129") + machine.fail(": < /dev/dri/renderD130") + machine.fail(": < /dev/dri/card1") + machine.fail(": < /dev/dri/card2") + + with subtest("Restart cardwire and check if gpu states got re-applied"): + machine.wait_until_succeeds("systemctl stop cardwired.service") + + machine.succeed(": < /dev/dri/renderD129") + machine.succeed(": < /dev/dri/renderD130") + machine.succeed(": < /dev/dri/card1") + machine.succeed(": < /dev/dri/card2") + + machine.wait_until_succeeds("systemctl start cardwired.service") + + t.assertIn("2", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only one or less GPU got blocked") + + machine.fail(": < /dev/dri/renderD129") + machine.fail(": < /dev/dri/renderD130") + machine.fail(": < /dev/dri/card1") + machine.fail(": < /dev/dri/card2") + ''; + +} From 6e73baaf57d0ea12f1690906cff8338fd81addd2 Mon Sep 17 00:00:00 2001 From: Luytan Date: Thu, 30 Apr 2026 20:54:33 +0200 Subject: [PATCH 03/12] Rework cardwire's CLI and Daemon's dbus (#11) * refactor: changed nvidia_minor to be an Option, with None if the gpu isnt nvidia * feat: new cardwire list + remade daemon dbus * daemon dbus now return json instead of preset message * cardwire cli now has --full and --json * cardwire_core gpu model has an blocked option now * nvidia_minor default was changed to None instead of 99 * fix: split list device into a gpu version and a pci one * fix: removed json strin from dbus result, added a new list func for pci, changed some HashMap to BTreeMap * fix: removed json parsing from cardwire cli * fix: added a specialized dbus struct for pci/gpu, that allows to remove the Option<> from the dbus message, * fix: forgot to remove println! debug from cardwire cli * feat: Mode is now a property * fix: set cli mode to use property * fix: None option should be empty instead of Null * feat: changed Mode property to be a u32 * fix: cardwire-cli now use u32 for Mode property * ci: fix cardwire set assert * feat: changed cardwire cli default display * ci: changed "on" to "true" --- Cargo.lock | 5 +- Cargo.toml | 2 +- crates/cardwire-cli/Cargo.toml | 4 +- crates/cardwire-cli/src/args.rs | 11 +- crates/cardwire-cli/src/dbus.rs | 19 ++-- crates/cardwire-cli/src/display.rs | 121 +++++++++++++++++++++ crates/cardwire-cli/src/main.rs | 86 +++++++++------ crates/cardwire-cli/src/output.rs | 65 ----------- crates/cardwire-core/Cargo.toml | 4 +- crates/cardwire-core/src/gpu/discover.rs | 18 +-- crates/cardwire-core/src/gpu/ebpf.rs | 8 +- crates/cardwire-core/src/gpu/mod.rs | 2 +- crates/cardwire-core/src/gpu/models.rs | 25 +++-- crates/cardwire-core/src/pci/mod.rs | 2 +- crates/cardwire-core/src/pci/models.rs | 15 ++- crates/cardwire-core/src/pci/pci_device.rs | 14 ++- crates/cardwire-daemon/src/config.rs | 12 +- crates/cardwire-daemon/src/dbus.rs | 87 ++++++++++----- crates/cardwire-daemon/src/models.rs | 28 +++-- nix/ci-15gpu.nix | 4 +- nix/ci-2gpu.nix | 4 +- nix/ci-3gpu.nix | 4 +- 22 files changed, 350 insertions(+), 190 deletions(-) create mode 100644 crates/cardwire-cli/src/display.rs delete mode 100644 crates/cardwire-cli/src/output.rs diff --git a/Cargo.lock b/Cargo.lock index 00c1993..6e39716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,9 +307,10 @@ name = "cardwire-cli" version = "0.5.0" dependencies = [ "anyhow", - "cardwire-core", "clap", "clap_complete", + "serde", + "serde_json", "tokio", "zbus", ] @@ -320,7 +321,9 @@ version = "0.5.0" dependencies = [ "cardwire-ebpf", "log", + "serde", "thiserror 2.0.18", + "zbus", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 701b2f0..34142f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/crates/cardwire-cli/Cargo.toml b/crates/cardwire-cli/Cargo.toml index 5937ff8..3b425fb 100644 --- a/crates/cardwire-cli/Cargo.toml +++ b/crates/cardwire-cli/Cargo.toml @@ -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" diff --git a/crates/cardwire-cli/src/args.rs b/crates/cardwire-cli/src/args.rs index 8110382..5dc4b00 100644 --- a/crates/cardwire-cli/src/args.rs +++ b/crates/cardwire-cli/src/args.rs @@ -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 { diff --git a/crates/cardwire-cli/src/dbus.rs b/crates/cardwire-cli/src/dbus.rs index 92bcee5..45e33d1 100644 --- a/crates/cardwire-cli/src/dbus.rs +++ b/crates/cardwire-cli/src/dbus.rs @@ -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>, @@ -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 { - self.proxy.call("GetMode", &()).await + pub async fn get_mode(&self) -> zbus::Result { + self.proxy.get_property("Mode").await } - pub async fn list_gpus(&self) -> zbus::Result> { - self.proxy.call("ListGpus", &()).await + pub async fn list_devices(&self) -> zbus::Result> { + self.proxy.call("ListDevices", &()).await + } + pub async fn list_devices_pci(&self) -> zbus::Result> { + self.proxy.call("ListDevicesPci", &()).await } pub async fn set_gpu_block(&self, id: u32, blocked: bool) -> zbus::Result<()> { diff --git a/crates/cardwire-cli/src/display.rs b/crates/cardwire-cli/src/display.rs new file mode 100644 index 0000000..b9f3571 --- /dev/null +++ b/crates/cardwire-cli/src/display.rs @@ -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, 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) -> 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) { + 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!( + "{: { - 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), @@ -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:?}"), + } +} diff --git a/crates/cardwire-cli/src/output.rs b/crates/cardwire-cli/src/output.rs deleted file mode 100644 index 2bcab39..0000000 --- a/crates/cardwire-cli/src/output.rs +++ /dev/null @@ -1,65 +0,0 @@ -use cardwire_core::gpu::GpuRow; - -pub fn print_gpu_table(rows: &[GpuRow]) { - let mut id_w = 2usize; - let mut name_w = 4usize; - let mut pci_w = 3usize; - let mut render_w = 6usize; - let default_w = 7usize; - let blocked_w = 7usize; - - // Calculate widths - for (id, name, pci, render, _, _) in rows { - id_w = id_w.max(id.to_string().len()); - name_w = name_w.max(name.len()); - pci_w = pci_w.max(pci.len()); - // Full render string is "renderD" + device number - let render_full = format!("renderD{}", render); - render_w = render_w.max(render_full.len()); - } - - // Header - println!( - "{:) -> io::Result> { +pub fn read_gpu(pci_devices: &BTreeMap) -> io::Result> { let gpus: Vec = pci_devices .values() .filter(|device| { @@ -32,10 +34,10 @@ pub fn read_gpu(pci_devices: &HashMap) -> io::Result io::Result { let nvidia: bool = device.vendor_id.as_deref() == Some("0x10de"); - let nvidia_minor: u32 = if nvidia { - nvidia_get_minor(&device.pci_address).unwrap_or(99) + let nvidia_minor: Option = if nvidia { + nvidia_get_minor(&device.pci_address) } else { - 99 + None }; Ok(Gpu { @@ -146,7 +148,7 @@ fn nvidia_get_minor(pci_address: &str) -> Option { .ok() } /// Method from kwin -pub fn check_default_drm_class(gpu_list: &mut HashMap) -> io::Result<()> { +pub fn check_default_drm_class(gpu_list: &mut BTreeMap) -> io::Result<()> { let class_path = Path::new("/sys/class/drm"); let mut drm_entries = Vec::new(); if class_path.exists() { @@ -238,11 +240,13 @@ pub fn check_default_drm_class(gpu_list: &mut HashMap) -> io::Result for gpu in gpu_list.values_mut() { if gpu.id == *default.0.unwrap() as u32 { gpu.default = Some(true); + } else { + gpu.default = Some(false); } } // Default GPU gets ID 0, rest ordered by PCI address - let mut gpus: Vec = gpu_list.drain().map(|(_, gpu)| gpu).collect(); + let mut gpus: Vec = std::mem::take(gpu_list).into_values().collect(); gpus.sort_by(|a, b| b.default.cmp(&a.default).then(a.pci.cmp(&b.pci))); *gpu_list = gpus .into_iter() diff --git a/crates/cardwire-core/src/gpu/ebpf.rs b/crates/cardwire-core/src/gpu/ebpf.rs index 01b1e0e..6da1a75 100644 --- a/crates/cardwire-core/src/gpu/ebpf.rs +++ b/crates/cardwire-core/src/gpu/ebpf.rs @@ -33,9 +33,11 @@ pub fn is_gpu_blocked(blocker: &GpuBlocker, gpu: &Gpu) -> GpuResult { .is_render_blocked(render_id) .map_err(map_gpu_error)? && if gpu.nvidia { + // unwrap because it should be Some if it's an nvidia gpu, if not it's a bug and should + // be reported blocker .inner - .is_nvidia_blocked(*gpu.nvidia_minor()) + .is_nvidia_blocked(gpu.nvidia_minor().unwrap()) .map_err(map_gpu_error)? } else { true @@ -50,7 +52,7 @@ pub fn block_gpu(blocker: &mut GpuBlocker, gpu: &Gpu, block: bool) -> GpuResult< blocker.inner.block_render(render_id)?; blocker.inner.block_pci(gpu.pci_address())?; if gpu.nvidia { - blocker.inner.block_nvidia(*gpu.nvidia_minor())? + blocker.inner.block_nvidia(gpu.nvidia_minor().unwrap())? } Ok(()) } else { @@ -58,7 +60,7 @@ pub fn block_gpu(blocker: &mut GpuBlocker, gpu: &Gpu, block: bool) -> GpuResult< blocker.inner.unblock_render(render_id)?; blocker.inner.unblock_pci(gpu.pci_address())?; if gpu.nvidia { - blocker.inner.unblock_nvidia(*gpu.nvidia_minor())? + blocker.inner.unblock_nvidia(gpu.nvidia_minor().unwrap())? } Ok(()) } diff --git a/crates/cardwire-core/src/gpu/mod.rs b/crates/cardwire-core/src/gpu/mod.rs index 7e37379..95780a2 100644 --- a/crates/cardwire-core/src/gpu/mod.rs +++ b/crates/cardwire-core/src/gpu/mod.rs @@ -6,4 +6,4 @@ mod models; pub use discover::{check_default_drm_class, read_gpu}; pub use ebpf::{GpuBlocker, block_gpu, is_gpu_blocked}; pub use errors::GpuResult; -pub use models::{Gpu, GpuRow}; +pub use models::{DbusGpuDevice, Gpu}; diff --git a/crates/cardwire-core/src/gpu/models.rs b/crates/cardwire-core/src/gpu/models.rs index af2bda2..1600e54 100644 --- a/crates/cardwire-core/src/gpu/models.rs +++ b/crates/cardwire-core/src/gpu/models.rs @@ -1,4 +1,4 @@ -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type)] pub struct Gpu { pub id: u32, pub name: String, @@ -7,9 +7,8 @@ pub struct Gpu { pub card: u32, pub default: Option, pub nvidia: bool, - pub nvidia_minor: u32, + pub nvidia_minor: Option, } - impl Gpu { pub fn pci_address(&self) -> &str { &self.pci @@ -34,13 +33,23 @@ impl Gpu { pub fn card_node(&self) -> &u32 { &self.card } - pub fn is_nvidia(&self) -> &bool { - &self.nvidia + pub fn is_nvidia(&self) -> bool { + self.nvidia } - pub fn nvidia_minor(&self) -> &u32 { + pub fn nvidia_minor(&self) -> &Option { &self.nvidia_minor } } -// GpuRow for display -pub type GpuRow = (u32, String, String, String, bool, bool); +#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type)] +pub struct DbusGpuDevice { + pub id: u32, + pub name: String, + pub pci: String, + pub render: u32, + pub card: u32, + pub default: bool, + pub blocked: bool, + pub nvidia: bool, + pub nvidia_minor: String, +} diff --git a/crates/cardwire-core/src/pci/mod.rs b/crates/cardwire-core/src/pci/mod.rs index b3244a7..85fbea9 100644 --- a/crates/cardwire-core/src/pci/mod.rs +++ b/crates/cardwire-core/src/pci/mod.rs @@ -5,5 +5,5 @@ mod pci_device; pub use errors::IommuError; pub use iommu::{is_iommu_enabled, read_iommu_groups}; -pub use models::{IommuGroup, PciDevice}; +pub use models::{DbusPciDevice, IommuGroup, PciDevice}; pub use pci_device::read_pci_devices; diff --git a/crates/cardwire-core/src/pci/models.rs b/crates/cardwire-core/src/pci/models.rs index db23e54..94e4cf5 100644 --- a/crates/cardwire-core/src/pci/models.rs +++ b/crates/cardwire-core/src/pci/models.rs @@ -1,4 +1,4 @@ -#[derive(Clone)] +#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type)] pub struct PciDevice { pub pci_address: String, pub iommu_group: Option, @@ -10,6 +10,19 @@ pub struct PciDevice { pub class: Option, } +#[derive(Clone, serde::Serialize, serde::Deserialize, zbus::zvariant::Type)] +pub struct DbusPciDevice { + pub pci_address: String, + // Strings to be able to put nothing + pub iommu_group: String, + pub vendor_id: String, + pub device_id: String, + pub vendor_name: String, + pub device_name: String, + pub driver: String, + pub class: String, +} + pub struct IommuGroup { pub id: usize, pub devices: Vec, diff --git a/crates/cardwire-core/src/pci/pci_device.rs b/crates/cardwire-core/src/pci/pci_device.rs index 9f654a3..af9a283 100644 --- a/crates/cardwire-core/src/pci/pci_device.rs +++ b/crates/cardwire-core/src/pci/pci_device.rs @@ -1,8 +1,10 @@ use crate::pci::{IommuError, PciDevice, is_iommu_enabled, read_iommu_groups}; use log::{error, info, warn}; -use std::{collections::HashMap, fs, fs::File, io, io::BufRead, path::Path}; +use std::{ + collections::{BTreeMap, HashMap}, fs, fs::File, io, io::BufRead, path::Path +}; -pub fn read_pci_devices() -> Result, IommuError> { +pub fn read_pci_devices() -> Result, IommuError> { match is_iommu_enabled() { true => { info!("IOMMU detected, reading pci devices using iommu dir"); @@ -15,13 +17,13 @@ pub fn read_pci_devices() -> Result, IommuError> { } } -fn read_pci_devices_using_iommu() -> Result, IommuError> { +fn read_pci_devices_using_iommu() -> Result, IommuError> { let iommu_groups = read_iommu_groups()?; let pci_names = load_pci_name_db(Path::new("/usr/share/hwdata/pci.ids")).unwrap_or_else(|e| { warn!("Failed to load PCI name DB: {}", e); PciNameDb::default() }); - let mut devices_map = HashMap::new(); + let mut devices_map = BTreeMap::new(); for (group_id, group) in iommu_groups { // read "device" folder, look at each PCI ADDRESS for pci_address in group.devices { @@ -57,13 +59,13 @@ fn read_pci_devices_using_iommu() -> Result, IommuErr } Ok(devices_map) } -fn read_pci_devices_using_sysfs() -> Result, IommuError> { +fn read_pci_devices_using_sysfs() -> Result, IommuError> { let sysfs = Path::new("/sys/bus/pci/devices"); let pci_names = load_pci_name_db(Path::new("/usr/share/hwdata/pci.ids")).unwrap_or_else(|e| { warn!("Failed to load PCI name DB: {}", e); PciNameDb::default() }); - let mut devices_map = HashMap::new(); + let mut devices_map = BTreeMap::new(); let sysfs_dir = fs::read_dir(sysfs).map_err(|e| { error!("Failed to read sysfs PCI devices at {:?}: {}", sysfs, e); IommuError::Io(e) diff --git a/crates/cardwire-daemon/src/config.rs b/crates/cardwire-daemon/src/config.rs index cc534ce..ecbdc54 100644 --- a/crates/cardwire-daemon/src/config.rs +++ b/crates/cardwire-daemon/src/config.rs @@ -3,7 +3,7 @@ use anyhow::{Context, Ok}; use cardwire_core::gpu::{Gpu, GpuBlocker, is_gpu_blocked}; use log::{info, warn}; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs, io}; +use std::{collections::BTreeMap, fs, io}; const CONFIG_PATH: &str = "/etc/cardwire"; const STATE_PATH: &str = "/var/lib/cardwire"; @@ -54,7 +54,7 @@ impl CardwireConfig { // This is the easiest way i found to have a good looking json, might change later #[derive(Serialize, Deserialize)] pub struct CardwireGpuState { - gpu: HashMap, + gpu: BTreeMap, } // A gpu contain a pci and a blocked state, // TODO: A more precise way to identify a GPU, but not dangerous since cardwire does not block @@ -79,14 +79,14 @@ impl CardwireGpuState { Ok(gpu_state) } // Parse directly into CardwireGpuState - fn parse_gpu_state(state_file: &str) -> anyhow::Result> { + fn parse_gpu_state(state_file: &str) -> anyhow::Result> { if !(fs::exists(state_file)?) { Self::create_default_state().context("Could not create default gpu_state.json")?; } let gpu_state = fs::read_to_string(state_file) .with_context(|| format!("Could not read file {}", state_file))?; - let content: HashMap = + let content: BTreeMap = serde_json::from_str(&gpu_state).context("Could not parse string into json")?; Ok(content) } @@ -98,7 +98,7 @@ impl CardwireGpuState { /// Save the new state into the daemon and to the gpu_state.json file pub async fn save_state( &mut self, - gpu_list: &HashMap, + gpu_list: &BTreeMap, blocker: &GpuBlocker, ) -> anyhow::Result<()> { // Prevent overwriting default config if it's not replaceable @@ -196,7 +196,7 @@ fn create_default_file(kind: FileKind) -> anyhow::Result<()> { .context("could not create default folder for gpu_state.json")?; // Default gpu_state for cardwire // TODO: Move to default trait? - let mut defaut_hash: HashMap = HashMap::new(); + let mut defaut_hash: BTreeMap = BTreeMap::new(); let _ = defaut_hash.insert("Null".to_string(), CardwireGpuUnit { block: false }); let default_gpu_state = serde_json::to_string_pretty(&defaut_hash)?; // write diff --git a/crates/cardwire-daemon/src/dbus.rs b/crates/cardwire-daemon/src/dbus.rs index e8d089e..d069a01 100644 --- a/crates/cardwire-daemon/src/dbus.rs +++ b/crates/cardwire-daemon/src/dbus.rs @@ -1,5 +1,9 @@ +use std::collections::BTreeMap; + use crate::models::{Daemon, Modes}; -use cardwire_core::gpu::{GpuRow, block_gpu, is_gpu_blocked}; +use cardwire_core::{ + gpu::{DbusGpuDevice, block_gpu, is_gpu_blocked}, pci::DbusPciDevice +}; use log::{error, info, warn}; use zbus::{fdo, interface}; @@ -8,7 +12,8 @@ impl Daemon { /* Set the mode */ - pub(crate) async fn set_mode(&self, mode: String) -> fdo::Result<()> { + #[zbus(property)] + pub(crate) async fn set_mode(&self, mode: u32) -> fdo::Result<()> { // Valide inputs and turn into a Modes let mode = Modes::parse(&mode)?; // Get current_config lock @@ -70,10 +75,14 @@ impl Daemon { info!("Switched to {}", mode); Ok(()) } - - pub(crate) async fn get_mode(&self) -> String { + #[zbus(property)] + pub(crate) async fn mode(&self) -> fdo::Result { let current_mode = self.state.mode_state.read().await; - format!("Current Mode: {}", current_mode.mode()) + match current_mode.mode() { + Modes::Integrated => Ok(0), + Modes::Hybrid => Ok(1), + Modes::Manual => Ok(2), + } } pub(crate) async fn set_gpu_block(&self, gpu_id: u32, block: bool) -> fdo::Result<()> { @@ -111,32 +120,52 @@ impl Daemon { Ok(()) } - pub(crate) async fn list_gpus(&self) -> Vec { - //self.list_gpu_rows().await - let mut rows = Vec::with_capacity(self.state.gpu_list.len()); + pub(crate) async fn list_devices(&self) -> fdo::Result> { let blocker = self.state.ebpf_blocker.read().await; - for gpu in self.state.gpu_list.values() { - let blocked: bool = match is_gpu_blocked(&blocker, gpu) { - Ok(b) => b, - Err(e) => { - error!( - "Couldn't check gpu's lock state for {}: {}", - gpu.pci_address(), - e - ); - false - } + let list = self.state.gpu_list.clone(); + let mut dbus_list: BTreeMap = BTreeMap::new(); + for (id, gpu) in list { + let temp_gpu = DbusGpuDevice { + id: gpu.id, + pci: gpu.pci.clone(), + render: gpu.render, + name: gpu.name.clone(), + card: gpu.card, + default: gpu.default.unwrap_or(false), + blocked: is_gpu_blocked(&blocker, &gpu).unwrap_or(false), + nvidia: gpu.is_nvidia(), + nvidia_minor: if gpu.nvidia_minor().is_some() { + gpu.nvidia_minor().unwrap().to_string() + } else { + "".to_string() + }, }; - rows.push(( - gpu.id(), - gpu.name().to_string(), - gpu.pci_address().to_string(), - gpu.render_node().to_string(), - gpu.is_default(), - blocked, - )); + dbus_list.insert(id, temp_gpu); } - rows.sort_by_key(|row| row.0); - rows + Ok(dbus_list) + } + + pub(crate) async fn list_devices_pci(&self) -> fdo::Result> { + let pci_list = &self.state.pci_devices; + let mut dbus_list: BTreeMap = BTreeMap::new(); + for (id, pci) in pci_list { + let temp_pci = DbusPciDevice { + pci_address: pci.pci_address.clone(), + iommu_group: if let Some(iommu) = pci.iommu_group { + iommu.to_string() + } else { + "".to_string() + }, + vendor_id: pci.vendor_id.clone().unwrap_or("".to_string()), + device_id: pci.device_id.clone().unwrap_or("".to_string()), + vendor_name: pci.vendor_name.clone().unwrap_or("".to_string()), + device_name: pci.device_name.clone().unwrap_or("".to_string()), + driver: pci.driver.clone().unwrap_or("".to_string()), + class: pci.class.clone().unwrap_or("".to_string()), + }; + dbus_list.insert(id.clone(), temp_pci); + } + + Ok(dbus_list) } } diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index fc54d16..3c231d4 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -5,7 +5,7 @@ use cardwire_core::{ }; use log::warn; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt}; +use std::{collections::BTreeMap, fmt}; use tokio::sync::RwLock; use zbus::fdo::Error; @@ -28,11 +28,11 @@ impl fmt::Display for Modes { } impl Modes { - pub fn parse(input: &str) -> zbus::fdo::Result { - match input.to_ascii_lowercase().as_str() { - "integrated" => Ok(Self::Integrated), - "hybrid" => Ok(Self::Hybrid), - "manual" => Ok(Self::Manual), + pub fn parse(input: &u32) -> zbus::fdo::Result { + match input { + 0 => Ok(Self::Integrated), + 1 => Ok(Self::Hybrid), + 2 => Ok(Self::Manual), unknown => Err(Error::InvalidArgs(format!( "unknown mode: {unknown} \n expected integrated|hybrid|manual" ))), @@ -44,10 +44,10 @@ pub struct DaemonState { pub config: RwLock, pub gpu_state: RwLock, pub mode_state: RwLock, - pub gpu_list: HashMap, + pub gpu_list: BTreeMap, pub ebpf_blocker: RwLock, // for future uses, related to vfio - pub _pci_devices: HashMap, + pub pci_devices: BTreeMap, pub _iommu: bool, } impl DaemonState { @@ -92,7 +92,7 @@ impl Daemon { config: RwLock::new(config), gpu_state: RwLock::new(gpu_state), mode_state: RwLock::new(mode_state), - _pci_devices: pci_devices, + pci_devices, _iommu: iommu, gpu_list, ebpf_blocker: RwLock::new(ebpf_blocker), @@ -109,9 +109,15 @@ impl Daemon { drop(blocker); drop(config); // Apply mode - let mode_to_apply = mode.mode().to_string(); + let mode_to_apply = mode.mode(); drop(mode); - self.set_mode(mode_to_apply).await?; + let mode_to_apply: usize = match mode_to_apply { + Modes::Integrated => 0, + Modes::Hybrid => 1, + Modes::Manual => 2, + }; + + self.set_mode(mode_to_apply as u32).await?; Ok(()) } diff --git a/nix/ci-15gpu.nix b/nix/ci-15gpu.nix index bafa7b5..17560d4 100644 --- a/nix/ci-15gpu.nix +++ b/nix/ci-15gpu.nix @@ -82,7 +82,7 @@ machine.succeed(f'cardwire gpu {x} --block') with subtest("Check cardwire to see if 14 gpus got blocked"): - t.assertIn("14", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only 13 or less got blocked") + t.assertIn("14", machine.succeed("cardwire list|grep 'true'|wc -l"), "Only 13 or less got blocked") for x in range(1, 15): cardid = 0 + x @@ -108,7 +108,7 @@ machine.wait_until_succeeds("systemctl start cardwired.service") - t.assertIn("14", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only 13 or less got blocked") + t.assertIn("14", machine.succeed("cardwire list|grep 'true'|wc -l"), "Only 13 or less got blocked") for x in range(1, 15): cardid = 0 + x diff --git a/nix/ci-2gpu.nix b/nix/ci-2gpu.nix index 58a3875..79c557c 100644 --- a/nix/ci-2gpu.nix +++ b/nix/ci-2gpu.nix @@ -61,12 +61,12 @@ # Check if cardwire detect both video card t.assertIn("renderD128", machine.succeed("cardwire list"), "Missing RenderD128 in cardwire") machine.succeed("test -e /dev/dri/renderD129") - t.assertIn("Mode has been set to integrated", machine.succeed("cardwire set integrated"), "Couldn't set to integrated mode") + t.assertIn("Mode has been set to Integrated", machine.succeed("cardwire set integrated"), "Couldn't set to integrated mode") machine.fail(": < /dev/dri/renderD129") t.assertIn("Integrated", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") with subtest("Switchback to hybrid mode"): - t.assertIn("Mode has been set to hybrid", machine.succeed("cardwire set hybrid"), "Couldn't set to hybrid mode") + t.assertIn("Mode has been set to Hybrid", machine.succeed("cardwire set hybrid"), "Couldn't set to hybrid mode") machine.succeed(": < /dev/dri/renderD129") t.assertIn("Hybrid", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") diff --git a/nix/ci-3gpu.nix b/nix/ci-3gpu.nix index 5c89a41..a44dc93 100644 --- a/nix/ci-3gpu.nix +++ b/nix/ci-3gpu.nix @@ -70,7 +70,7 @@ t.assertIn("2", machine.succeed("cat /var/lib/cardwire/gpu_state.json|grep true|wc -l"), "Only one or less GPU got blocked") with subtest("Check cardwire to see if two gpus got blocked"): - t.assertIn("2", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only one or less GPU got blocked") + t.assertIn("2", machine.succeed("cardwire list|grep 'true'|wc -l"), "Only one or less GPU got blocked") machine.fail(": < /dev/dri/renderD129") machine.fail(": < /dev/dri/renderD130") machine.fail(": < /dev/dri/card1") @@ -86,7 +86,7 @@ machine.wait_until_succeeds("systemctl start cardwired.service") - t.assertIn("2", machine.succeed("cardwire list|grep 'on'|wc -l"), "Only one or less GPU got blocked") + t.assertIn("2", machine.succeed("cardwire list|grep 'true'|wc -l"), "Only one or less GPU got blocked") machine.fail(": < /dev/dri/renderD129") machine.fail(": < /dev/dri/renderD130") From 04f907e83e676b7070bf6e8f16012b680bcf6425 Mon Sep 17 00:00:00 2001 From: Luytan Date: Fri, 1 May 2026 12:19:13 +0200 Subject: [PATCH 04/12] Implement a ebf map for blocked files + blocked nvidia files (#13) * feat(ebpf): replace the static 'config' string compare with a map check, that allow to add more files from the daemon into the ebpf block * feat(ebpf): implemented file map for nvidia * fix: dereference is nvidia from rebase --- crates/cardwire-core/src/gpu/ebpf.rs | 10 ++++++++ crates/cardwire-daemon/src/models.rs | 27 +++++++++++++++++++++ crates/cardwire-ebpf/src/bpf.c | 29 ++++++++++++++++++---- crates/cardwire-ebpf/src/lib.rs | 36 ++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/crates/cardwire-core/src/gpu/ebpf.rs b/crates/cardwire-core/src/gpu/ebpf.rs index 6da1a75..a196879 100644 --- a/crates/cardwire-core/src/gpu/ebpf.rs +++ b/crates/cardwire-core/src/gpu/ebpf.rs @@ -16,6 +16,16 @@ impl GpuBlocker { self.inner.set_vulkan_block(block).map_err(map_gpu_error)?; Ok(()) } + pub fn set_file_block(&mut self, file: &str) -> GpuResult<()> { + self.inner.set_file_block(file).map_err(map_gpu_error)?; + Ok(()) + } + pub fn set_nvidia_file_block(&mut self, file: &str) -> GpuResult<()> { + self.inner + .set_nvidia_file_block(file) + .map_err(map_gpu_error)?; + Ok(()) + } } pub fn is_gpu_blocked(blocker: &GpuBlocker, gpu: &Gpu) -> GpuResult { diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index 3c231d4..79e4b4f 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -9,6 +9,20 @@ use std::{collections::BTreeMap, fmt}; use tokio::sync::RwLock; use zbus::fdo::Error; +const BLOCKED_PCI_FILES: &[&str] = &[ + "config", + "current_link_speed", + "max_link_speed", + "max_link_width", + "current_link_width", +]; +// Files that get blocked when the vulkan block is on +const BLOCKED_NVIDIA_FILES: &[&str] = &[ + "libGLX_nvidia.so.0", + "nvidia_icd.json", + "nvidia_icd.x86_64.json", +]; + #[derive(Deserialize, Serialize, PartialEq, zbus::zvariant::Type, Clone, Copy, Default)] pub enum Modes { Integrated, @@ -105,6 +119,19 @@ impl Daemon { let mut blocker = self.state.ebpf_blocker.write().await; // Apply vulkan block blocker.set_vulkan_block(config.block_nvidia_vulkan())?; + + // Apply file blocks + for file in BLOCKED_PCI_FILES { + blocker.set_file_block(file)?; + } + for gpu in self.state.gpu_list.values() { + if gpu.is_nvidia() { + for file in BLOCKED_NVIDIA_FILES { + blocker.set_nvidia_file_block(file)?; + } + break; + } + } // Dropping the locks prevent set_mode being stuck drop(blocker); drop(config); diff --git a/crates/cardwire-ebpf/src/bpf.c b/crates/cardwire-ebpf/src/bpf.c index 671405b..b891117 100644 --- a/crates/cardwire-ebpf/src/bpf.c +++ b/crates/cardwire-ebpf/src/bpf.c @@ -82,6 +82,13 @@ struct { __type(value, __u8); } BLOCKED_PCI SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1024); + __type(key, char[30]); + __type(value, __u8); +} BLOCKED_PCI_FILES SEC(".maps"); + struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); @@ -89,6 +96,13 @@ struct { __type(value, __u8); } SETTINGS SEC(".maps"); +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1024); + __type(key, char[30]); + __type(value, __u8); +} BLOCKED_NVIDIA_FILES SEC(".maps"); + /* Safely read and compare kernel qstr */ static __always_inline int qstr_eq(struct qstr q, const char *name, __u32 len) { @@ -176,11 +190,18 @@ static __always_inline int is_blocked_device(struct dentry *d) } } struct qstr q = BPF_CORE_READ(d, d_name); + // ignore long files + if (!q.name || q.len > 30) { + return 0; + } + char buf[32] = {}; + if (bpf_core_read_str(buf, sizeof(buf), q.name) < 0) { + return 0; + } // Blocks vulkan nvidia_icd, it's dangerous and will only work if one nvidia gpu is blocked __u32 block_vulkan_key = 0; if (bpf_map_lookup_elem(&SETTINGS, &block_vulkan_key)) { - if (qstr_eq(q, "nvidia_icd.json", 15) || - qstr_eq(q, "nvidia_icd.x86_64.json", 22)) { + if (bpf_map_lookup_elem(&BLOCKED_PCI_FILES, buf)) { __u32 id0 = 0, id1 = 1; if (bpf_map_lookup_elem(&BLOCKED_NVIDIAID, &id0) && !bpf_map_lookup_elem(&BLOCKED_NVIDIAID, &id1)) { @@ -188,8 +209,8 @@ static __always_inline int is_blocked_device(struct dentry *d) } } } - - if (qstr_eq(q, "config", 6)) { + // PCI Part + if (bpf_map_lookup_elem(&BLOCKED_PCI_FILES, buf)) { char pci_addr[16] = {}; if (get_pci_addr(d, pci_addr, sizeof(pci_addr)) != 0) { return 0; diff --git a/crates/cardwire-ebpf/src/lib.rs b/crates/cardwire-ebpf/src/lib.rs index 4585d69..46a43ee 100644 --- a/crates/cardwire-ebpf/src/lib.rs +++ b/crates/cardwire-ebpf/src/lib.rs @@ -62,14 +62,26 @@ impl EbpfBlocker { Ok(Self { ebpf }) } + /// turn a pci string into a u8 array with a fixed 16 size fn pci_key(pci: &str) -> [u8; 16] { let mut key = [0u8; 16]; let bytes = pci.as_bytes(); + // leave one byte for terminator let len = bytes.len().min(15); key[..len].copy_from_slice(&bytes[..len]); key[len] = 0; key } + /// turn a file string into a u8 array with a fixed 30 size + fn file_key(file: &str) -> [u8; 30] { + let mut key = [0u8; 30]; + let bytes = file.as_bytes(); + // leave one byte for terminator + let len = bytes.len().min(29); + key[..len].copy_from_slice(&bytes[..len]); + key[len] = 0; + key + } fn missing_entity(kind: &str, name: &str) -> CardwireEbpfError { CardwireEbpfError::missing_entity(kind, name) @@ -258,4 +270,28 @@ impl EbpfBlocker { } Ok(()) } + + pub fn set_nvidia_file_block(&mut self, file: &str) -> CardwireEbpfResult<()> { + let mut map: HashMap<_, [u8; 30], u8> = HashMap::try_from( + self.ebpf + .map_mut("BLOCKED_NVIDIA_FILES") + .ok_or_else(|| Self::missing_entity("map", "BLOCKED_PCI"))?, + ) + .map_err(CardwireEbpfError::aya)?; + let key = Self::file_key(file); + map.insert(key, 1, 0).map_err(CardwireEbpfError::aya)?; + Ok(()) + } + + pub fn set_file_block(&mut self, file: &str) -> CardwireEbpfResult<()> { + let mut map: HashMap<_, [u8; 30], u8> = HashMap::try_from( + self.ebpf + .map_mut("BLOCKED_PCI_FILES") + .ok_or_else(|| Self::missing_entity("map", "BLOCKED_PCI"))?, + ) + .map_err(CardwireEbpfError::aya)?; + let key = Self::file_key(file); + map.insert(key, 1, 0).map_err(CardwireEbpfError::aya)?; + Ok(()) + } } From e38859a9af44951884a0ad39097f0509389aed17 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 2 May 2026 09:26:23 +0200 Subject: [PATCH 05/12] fix: compare to the right list in the nvidia part of the bpf code --- crates/cardwire-ebpf/src/bpf.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-ebpf/src/bpf.c b/crates/cardwire-ebpf/src/bpf.c index b891117..cecc9b9 100644 --- a/crates/cardwire-ebpf/src/bpf.c +++ b/crates/cardwire-ebpf/src/bpf.c @@ -201,7 +201,7 @@ static __always_inline int is_blocked_device(struct dentry *d) // Blocks vulkan nvidia_icd, it's dangerous and will only work if one nvidia gpu is blocked __u32 block_vulkan_key = 0; if (bpf_map_lookup_elem(&SETTINGS, &block_vulkan_key)) { - if (bpf_map_lookup_elem(&BLOCKED_PCI_FILES, buf)) { + if (bpf_map_lookup_elem(&BLOCKED_NVIDIA_FILES, buf)) { __u32 id0 = 0, id1 = 1; if (bpf_map_lookup_elem(&BLOCKED_NVIDIAID, &id0) && !bpf_map_lookup_elem(&BLOCKED_NVIDIAID, &id1)) { @@ -267,4 +267,4 @@ int BPF_PROG(inode_getattr, const struct path *path) { struct dentry *d = BPF_CORE_READ(path, dentry); return is_blocked_device(d); -} \ No newline at end of file +} From d3d55b34ad94cd4733a9d386f7723eb91d3deec3 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 17:20:50 +0200 Subject: [PATCH 06/12] docs: added some file head comments --- crates/cardwire-core/src/gpu/discover.rs | 1 + crates/cardwire-core/src/gpu/ebpf.rs | 1 + crates/cardwire-daemon/src/config.rs | 2 ++ crates/cardwire-daemon/src/daemon.rs | 1 + crates/cardwire-daemon/src/dbus.rs | 1 + crates/cardwire-daemon/src/models.rs | 1 + crates/cardwire-ebpf/src/errors.rs | 1 + crates/cardwire-ebpf/src/lib.rs | 1 + 8 files changed, 9 insertions(+) diff --git a/crates/cardwire-core/src/gpu/discover.rs b/crates/cardwire-core/src/gpu/discover.rs index 32b51cb..f2d44e7 100644 --- a/crates/cardwire-core/src/gpu/discover.rs +++ b/crates/cardwire-core/src/gpu/discover.rs @@ -1,3 +1,4 @@ +//! Read a pci list and return a list of gpu use crate::{gpu::models::Gpu, pci::PciDevice}; use log::{info, warn}; use std::{ diff --git a/crates/cardwire-core/src/gpu/ebpf.rs b/crates/cardwire-core/src/gpu/ebpf.rs index a196879..be187c6 100644 --- a/crates/cardwire-core/src/gpu/ebpf.rs +++ b/crates/cardwire-core/src/gpu/ebpf.rs @@ -1,3 +1,4 @@ +//! this is a middleman between the daemon and the ebpf library use crate::gpu::{GpuResult, errors::GpuError, models::Gpu}; use cardwire_ebpf::EbpfBlocker; diff --git a/crates/cardwire-daemon/src/config.rs b/crates/cardwire-daemon/src/config.rs index ecbdc54..30444e0 100644 --- a/crates/cardwire-daemon/src/config.rs +++ b/crates/cardwire-daemon/src/config.rs @@ -1,3 +1,5 @@ +//! helper to manage cardwired configs, include the user config .toml, and the .json states like +//! gpu, mode or pci use crate::models::Modes; use anyhow::{Context, Ok}; use cardwire_core::gpu::{Gpu, GpuBlocker, is_gpu_blocked}; diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index abd67dd..024fdf7 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -1,3 +1,4 @@ +//! entry point of cardwired mod config; mod dbus; mod models; diff --git a/crates/cardwire-daemon/src/dbus.rs b/crates/cardwire-daemon/src/dbus.rs index d069a01..a8e0d99 100644 --- a/crates/cardwire-daemon/src/dbus.rs +++ b/crates/cardwire-daemon/src/dbus.rs @@ -1,3 +1,4 @@ +//! handle the dbus part of cardwired use std::collections::BTreeMap; use crate::models::{Daemon, Modes}; diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index 79e4b4f..e885693 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -1,3 +1,4 @@ +//! where the struct and impl are declared use crate::config::{CardwireConfig, CardwireGpuState, CardwireModeState}; use anyhow::{Context, Result}; use cardwire_core::{ diff --git a/crates/cardwire-ebpf/src/errors.rs b/crates/cardwire-ebpf/src/errors.rs index cb9296e..345431d 100644 --- a/crates/cardwire-ebpf/src/errors.rs +++ b/crates/cardwire-ebpf/src/errors.rs @@ -1,3 +1,4 @@ +//! custom errors for cardwire-ebpf use std::{fmt, io}; use thiserror::Error; diff --git a/crates/cardwire-ebpf/src/lib.rs b/crates/cardwire-ebpf/src/lib.rs index 46a43ee..7e56751 100644 --- a/crates/cardwire-ebpf/src/lib.rs +++ b/crates/cardwire-ebpf/src/lib.rs @@ -1,3 +1,4 @@ +//! main lib code of cardwire-ebpf mod errors; pub use crate::errors::{CardwireEbpfError, CardwireEbpfResult}; From 974f4cc1702543e7ca20c52779452fe12ddad33b Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 17:35:05 +0200 Subject: [PATCH 07/12] fix: prevent single gpu block on hybrid/integrated --- crates/cardwire-daemon/src/dbus.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/cardwire-daemon/src/dbus.rs b/crates/cardwire-daemon/src/dbus.rs index a8e0d99..c228f2b 100644 --- a/crates/cardwire-daemon/src/dbus.rs +++ b/crates/cardwire-daemon/src/dbus.rs @@ -17,9 +17,10 @@ impl Daemon { pub(crate) async fn set_mode(&self, mode: u32) -> fdo::Result<()> { // Valide inputs and turn into a Modes let mode = Modes::parse(&mode)?; - // Get current_config lock let mut current_mode = self.state.mode_state.write().await; - + if let Err(e) = current_mode.save_state(mode).await { + warn!("mode couldn't be saved to config: {e}"); + } let mut blocker = self.state.ebpf_blocker.write().await; match mode { @@ -70,9 +71,6 @@ impl Daemon { } } } - if let Err(e) = current_mode.save_state(mode).await { - warn!("mode couldn't be saved to config: {e}"); - } info!("Switched to {}", mode); Ok(()) } @@ -87,6 +85,12 @@ impl Daemon { } pub(crate) async fn set_gpu_block(&self, gpu_id: u32, block: bool) -> fdo::Result<()> { + let mode = self.state.mode_state.read().await; + if mode.mode() != Modes::Manual { + return Err(fdo::Error::AccessDenied( + "Per GPU block is only available on manual mode".to_string(), + )); + } let mut blocker = self.state.ebpf_blocker.write().await; let mut gpu_state = self.state.gpu_state.write().await; let gpu = self From 65cc92a42d7b1af106eafc2e1233f742caea0196 Mon Sep 17 00:00:00 2001 From: Luytan Date: Sun, 3 May 2026 19:56:28 +0200 Subject: [PATCH 08/12] Implement an auto switch mode on battery event (#14) * feat: started implementation of a upower battery listener for auto_switching mode * fix: replace println by info! * fix: on_battery is a property not a signal * fix: on_battery returns a bool not a string * chore: move battery listen to model * feat: add auto_battery_switch to config, and default configuration * feat: add mode switching on battery change * fix: set set_mode to zbus property * ci: rename to new error message * feat: add upower to packaging --- Cargo.lock | 12 +++++++ Cargo.toml | 1 + crates/cardwire-daemon/Cargo.toml | 2 ++ crates/cardwire-daemon/src/config.rs | 23 +++++++++---- crates/cardwire-daemon/src/daemon.rs | 1 + crates/cardwire-daemon/src/listeners.rs | 45 +++++++++++++++++++++++++ crates/cardwire-daemon/src/models.rs | 7 ++-- nix/ci-2gpu.nix | 2 +- nix/default.nix | 1 + packages/arch-linux/cardwire-PKGBUILD | 2 +- packages/fedora/cardwire.spec | 3 ++ 11 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 crates/cardwire-daemon/src/listeners.rs diff --git a/Cargo.lock b/Cargo.lock index 6e39716..f8aa1d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", + "tokio-stream", "toml", "zbus", ] @@ -1430,6 +1431,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "1.1.2+spec-1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 34142f1..20d93eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ env_logger = "0.11" toml = "1.0.3" aya = "0.13.1" serde_json = "1.0.149" +tokio-stream = "0.1.18" [profile.dev] panic = "abort" diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index 91b5d76..419ef0d 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -20,6 +20,8 @@ anyhow.workspace = true env_logger.workspace = true toml.workspace = true serde_json.workspace = true +tokio-stream.workspace = true + [[bin]] name = "cardwired" path = "src/daemon.rs" diff --git a/crates/cardwire-daemon/src/config.rs b/crates/cardwire-daemon/src/config.rs index 30444e0..8a6d508 100644 --- a/crates/cardwire-daemon/src/config.rs +++ b/crates/cardwire-daemon/src/config.rs @@ -16,13 +16,22 @@ enum FileKind { ModeState, PciState, } -// TODO: Handle fs error for tomorrow -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] +#[serde(default)] pub struct CardwireConfig { auto_apply_gpu_state: bool, block_nvidia_vulkan: bool, + battery_auto_switch: bool, +} +impl Default for CardwireConfig { + fn default() -> Self { + CardwireConfig { + auto_apply_gpu_state: true, + block_nvidia_vulkan: false, + battery_auto_switch: false, + } + } } - impl CardwireConfig { /// Read TOML config file and return it's settings as a struct // TODO: Error handling on std::fs @@ -51,6 +60,9 @@ impl CardwireConfig { pub fn auto_apply_gpu_state(&self) -> bool { self.auto_apply_gpu_state } + pub fn battery_auto_switch(&self) -> bool { + self.battery_auto_switch + } } // This is the easiest way i found to have a good looking json, might change later @@ -186,10 +198,7 @@ fn create_default_file(kind: FileKind) -> anyhow::Result<()> { .context("could not create default folder for cardwire.toml")?; // Default config for cardwire // TODO: Move to default trait? - let default_config = toml::to_string_pretty(&CardwireConfig { - auto_apply_gpu_state: true, - block_nvidia_vulkan: false, - })?; + let default_config = toml::to_string_pretty(&CardwireConfig::default())?; // write fs::write(format!("{}/cardwire.toml", CONFIG_PATH), default_config) } diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 024fdf7..fa9a767 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -1,6 +1,7 @@ //! entry point of cardwired mod config; mod dbus; +mod listeners; mod models; use crate::models::Daemon; diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs new file mode 100644 index 0000000..daf05d1 --- /dev/null +++ b/crates/cardwire-daemon/src/listeners.rs @@ -0,0 +1,45 @@ +//! Used to listen to other dbus interface, mainly for auto battery switch and display detection + +use log::info; +use tokio_stream::StreamExt; +use zbus::{Connection, Result, proxy}; + +#[proxy( + interface = "org.freedesktop.UPower", + default_service = "org.freedesktop.UPower", + default_path = "/org/freedesktop/UPower" +)] +trait UPower { + #[zbus(property)] + fn on_battery(&self) -> Result; +} +#[proxy( + interface = "com.github.opengamingcollective.cardwire", + default_service = "com.github.opengamingcollective.cardwire", + default_path = "/com/github/opengamingcollective/cardwire" +)] +trait Cardwire { + #[zbus(property)] + fn set_mode(&self, mode: u32) -> Result<()>; +} + +pub async fn watch_battery_status() -> zbus::Result<()> { + let connection = Connection::system().await?; + let upower_proxy = UPowerProxy::new(&connection).await?; + + let cardwire = CardwireProxy::new(&connection).await?; + info!("Started listening to on_battery property"); + let mut battery_stream = upower_proxy.receive_on_battery_changed().await; + + while let Some(msg) = battery_stream.next().await { + if let Ok(state) = msg.get().await { + info!("battery event detected: {:?}", state); + match state { + true => cardwire.set_mode(0).await?, + false => cardwire.set_mode(1).await?, + }; + } + } + + Ok(()) +} diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index e885693..2e2c8d6 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -144,9 +144,12 @@ impl Daemon { Modes::Hybrid => 1, Modes::Manual => 2, }; - self.set_mode(mode_to_apply as u32).await?; - + // get config lock again + let config = self.state.config.read().await; + if config.battery_auto_switch() { + tokio::task::spawn(crate::listeners::watch_battery_status()); + } Ok(()) } } diff --git a/nix/ci-2gpu.nix b/nix/ci-2gpu.nix index 79c557c..c93b3a1 100644 --- a/nix/ci-2gpu.nix +++ b/nix/ci-2gpu.nix @@ -71,6 +71,6 @@ t.assertIn("Hybrid", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") with subtest("Try to block default gpu"): - t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + t.assertIn("Per GPU block is only available on manual mode", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") ''; } diff --git a/nix/default.nix b/nix/default.nix index 9eb07a5..006dc46 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -27,6 +27,7 @@ in ]; runtimeDeps = [ pkgs.hwdata + pkgs.upower ]; doCheck = false; doInstallCheck = true; diff --git a/packages/arch-linux/cardwire-PKGBUILD b/packages/arch-linux/cardwire-PKGBUILD index 64089a3..e6348a8 100644 --- a/packages/arch-linux/cardwire-PKGBUILD +++ b/packages/arch-linux/cardwire-PKGBUILD @@ -9,7 +9,7 @@ pkgdesc='GPU manager for Linux using eBPF LSM hooks' arch=('x86_64') url='https://github.com/OpenGamingCollective/cardwire' license=('GPL3') -depends=('hwdata' 'dbus' 'systemd') +depends=('hwdata' 'dbus' 'systemd' 'upower') makedepends=('libbpf' 'rust' 'clang') source=("https://github.com/OpenGamingCollective/cardwire/archive/refs/tags/v$pkgver.tar.gz") sha256sums=('37882d4d0d431c3ff48e24bd47cea03b7847080623254d8bdb4af9846134d700') diff --git a/packages/fedora/cardwire.spec b/packages/fedora/cardwire.spec index 3f869cd..17d9da6 100644 --- a/packages/fedora/cardwire.spec +++ b/packages/fedora/cardwire.spec @@ -13,6 +13,9 @@ BuildRequires: libbpf-devel BuildRequires: make BuildRequires: systemd-rpm-macros +Requires: hwdata +Requires: upower + %description Cardwire is a GPU manager for Linux that uses eBPF LSM hooks to block or unblock access to GPU device nodes. From 056266c2d5828125d3865d7f46d9f67e3ef858f5 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 20:19:04 +0200 Subject: [PATCH 09/12] docs: add dbus api to wiki --- docs/dev/dbus.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/dev/dbus.md diff --git a/docs/dev/dbus.md b/docs/dev/dbus.md new file mode 100644 index 0000000..fb23282 --- /dev/null +++ b/docs/dev/dbus.md @@ -0,0 +1,80 @@ +# DBUS + +## Service + +- **Bus Name:** `com.github.opengamingcollective.cardwire` +- **Object Path:** `/com/github/opengamingcollective/cardwire` +- **Interface:** `com.github.opengamingcollective.cardwire` + +## Methods + +### SetGpuBlock + +Set the block state for a specific GPU. Only available when `Mode` is set to `Manual`. The default GPU cannot be blocked + +**Inputs:** + +- gpu_id (in): `u` — The GPU identifier (`id` field) +- block (in): `b` — `true` to block, `false` to unblock + +**Outputs:** None + +### ListDevices + +List all detected GPU devices + +**Inputs:** None + +**Outputs:** + +- (out): `a{t(ussuubbbs)}` — Array of dictionary + +**GPU Struct `(ussuubbbs)` fields:** + +- `id`: `u` — GPU identifier +- `name`: `s` — GPU name +- `pci`: `s` — PCI address (e.g. `0000:01:00.0`) +- `render`: `u` — DRM render node minor number +- `card`: `u` — DRM card node minor number +- `default`: `b` — Whether this is the default display GPU +- `blocked`: `b` — Whether the GPU is currently blocked by the daemon +- `nvidia`: `b` — Whether the GPU is an NVIDIA device +- `nvidia_minor`: `s` — NVIDIA driver minor number (empty string if not applicable) + +### ListDevicesPci + +List all detected PCI devices + +**Inputs:** None + +**Outputs:** + +- (out): `a{s(ssssssss)}` — Array of dictionary + +**PCI Struct `(ssssssss)` fields:** + +- `pci_address`: `s` — PCI address (e.g. `0000:01:00.0`) +- `iommu_group`: `s` — IOMMU group number (empty string if none) +- `vendor_id`: `s` — PCI vendor ID (empty string if unknown) +- `device_id`: `s` — PCI device ID (empty string if unknown) +- `vendor_name`: `s` — Vendor name (empty string if unknown) +- `device_name`: `s` — Device name (empty string if unknown) +- `driver`: `s` — Kernel driver in use (empty string if unknown) +- `class`: `s` — PCI class (empty string if unknown) + +## Properties + +### Mode + +Controls the global GPU blocking mode + +- **Type:** `u` (uint32) +- **Access:** Read/Write +- **Emits:** `PropertiesChanged` on change + +**Values:** + +- `0` — Integrated: Block the dGPU. Requires exactly 2 GPUs +- `1` — Hybrid: Unblock the dGPU. Requires exactly 2 GPUs +- `2` — Manual: Allow per-GPU blocking via `SetGpuBlock`. Applies saved GPU state if `auto_apply` is enabled + From 0d67a214f0863118f8c49132fc25375070568ecc Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 20:19:38 +0200 Subject: [PATCH 10/12] docs: add the rest + formatting --- docs/SUMMARY.md | 4 ++++ docs/dev/dbus.md | 53 ++++++++++++++++++++++++------------------------ 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 5e9694d..906fe5c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -9,3 +9,7 @@ # Diagnostics - [Troubleshooting](troubleshooting.md) + +# Development + +- [DBus](dev/dbus.md) \ No newline at end of file diff --git a/docs/dev/dbus.md b/docs/dev/dbus.md index fb23282..e502a78 100644 --- a/docs/dev/dbus.md +++ b/docs/dev/dbus.md @@ -10,12 +10,14 @@ ### SetGpuBlock -Set the block state for a specific GPU. Only available when `Mode` is set to `Manual`. The default GPU cannot be blocked +Set the block state for a specific GPU. Only available when `Mode` is set to `Manual` + +The default GPU cannot be blocked **Inputs:** -- gpu_id (in): `u` — The GPU identifier (`id` field) -- block (in): `b` — `true` to block, `false` to unblock +- gpu_id (in): `u` -- The GPU identifier (`id` field) +- block (in): `b` -- `true` to block, `false` to unblock **Outputs:** None @@ -27,19 +29,19 @@ List all detected GPU devices **Outputs:** -- (out): `a{t(ussuubbbs)}` — Array of dictionary +- (out): `a{t(ussuubbbs)}` **GPU Struct `(ussuubbbs)` fields:** -- `id`: `u` — GPU identifier -- `name`: `s` — GPU name -- `pci`: `s` — PCI address (e.g. `0000:01:00.0`) -- `render`: `u` — DRM render node minor number -- `card`: `u` — DRM card node minor number -- `default`: `b` — Whether this is the default display GPU -- `blocked`: `b` — Whether the GPU is currently blocked by the daemon -- `nvidia`: `b` — Whether the GPU is an NVIDIA device -- `nvidia_minor`: `s` — NVIDIA driver minor number (empty string if not applicable) +- `id`: `u` -- GPU identifier +- `name`: `s` -- GPU name +- `pci`: `s` -- PCI address (e.g. `0000:01:00.0`) +- `render`: `u` -- DRM render node minor number +- `card`: `u` -- DRM card node minor number +- `default`: `b` -- Whether this is the default display GPU +- `blocked`: `b` -- Whether the GPU is currently blocked by the daemon +- `nvidia`: `b` -- Whether the GPU is an NVIDIA device +- `nvidia_minor`: `s` -- NVIDIA driver minor number (empty string if not applicable) ### ListDevicesPci @@ -49,18 +51,18 @@ List all detected PCI devices **Outputs:** -- (out): `a{s(ssssssss)}` — Array of dictionary +- (out): `a{s(ssssssss)}` **PCI Struct `(ssssssss)` fields:** -- `pci_address`: `s` — PCI address (e.g. `0000:01:00.0`) -- `iommu_group`: `s` — IOMMU group number (empty string if none) -- `vendor_id`: `s` — PCI vendor ID (empty string if unknown) -- `device_id`: `s` — PCI device ID (empty string if unknown) -- `vendor_name`: `s` — Vendor name (empty string if unknown) -- `device_name`: `s` — Device name (empty string if unknown) -- `driver`: `s` — Kernel driver in use (empty string if unknown) -- `class`: `s` — PCI class (empty string if unknown) +- `pci_address`: `s` -- PCI address (e.g. `0000:01:00.0`) +- `iommu_group`: `s` -- IOMMU group number (empty string if none) +- `vendor_id`: `s` -- PCI vendor ID (empty string if unknown) +- `device_id`: `s` -- PCI device ID (empty string if unknown) +- `vendor_name`: `s` -- Vendor name (empty string if unknown) +- `device_name`: `s` -- Device name (empty string if unknown) +- `driver`: `s` -- Kernel driver in use (empty string if unknown) +- `class`: `s` -- PCI class (empty string if unknown) ## Properties @@ -74,7 +76,6 @@ Controls the global GPU blocking mode **Values:** -- `0` — Integrated: Block the dGPU. Requires exactly 2 GPUs -- `1` — Hybrid: Unblock the dGPU. Requires exactly 2 GPUs -- `2` — Manual: Allow per-GPU blocking via `SetGpuBlock`. Applies saved GPU state if `auto_apply` is enabled - +- `0` -- Integrated: Block the dGPU. Requires exactly 2 GPUs +- `1` -- Hybrid: Unblock the dGPU. Requires exactly 2 GPUs +- `2` -- Manual: Allow per-GPU blocking via `SetGpuBlock`, applies saved GPU state if `auto_apply` is enabled From f942bc6153c8a1f07dd147b2ee5aef87d12c4460 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 20:34:25 +0200 Subject: [PATCH 11/12] docs: update config file --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4fb6dd1..238cee2 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ The daemon reads its configuration from `/etc/cardwire/cardwire.toml`. # /etc/cardwire/cardwire.toml auto_apply_gpu_state = true block_nvidia_vulkan = false +battery_auto_switch = false ``` `block_nvidia_vulkan` is an experimental feature that blocks the nvidia's vulkan icd, must be used with caution From accbfe7c3ba706ee2a00dd26b76e65409cff7ccd Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 20:36:26 +0200 Subject: [PATCH 12/12] chore: bump version to v0.6.0 --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- packages/arch-linux/cardwire-PKGBUILD | 2 +- packages/fedora/cardwire.spec | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8aa1d7..287ee2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,7 +304,7 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cardwire-cli" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "clap", @@ -317,7 +317,7 @@ dependencies = [ [[package]] name = "cardwire-core" -version = "0.5.0" +version = "0.6.0" dependencies = [ "cardwire-ebpf", "log", @@ -328,7 +328,7 @@ dependencies = [ [[package]] name = "cardwire-daemon" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "cardwire-core", @@ -346,7 +346,7 @@ dependencies = [ [[package]] name = "cardwire-ebpf" -version = "0.5.0" +version = "0.6.0" dependencies = [ "aya", "thiserror 2.0.18", diff --git a/Cargo.toml b/Cargo.toml index 20d93eb..f87ad9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.5.0" +version = "0.6.0" authors = ["luytan "] repository = "https://github.com/OpenGamingCollective/cardwire" edition = "2024" diff --git a/packages/arch-linux/cardwire-PKGBUILD b/packages/arch-linux/cardwire-PKGBUILD index e6348a8..5d08b13 100644 --- a/packages/arch-linux/cardwire-PKGBUILD +++ b/packages/arch-linux/cardwire-PKGBUILD @@ -3,7 +3,7 @@ pkgbase=cardwire pkgname=cardwire -pkgver=0.5.0 +pkgver=0.6.0 pkgrel=1 pkgdesc='GPU manager for Linux using eBPF LSM hooks' arch=('x86_64') diff --git a/packages/fedora/cardwire.spec b/packages/fedora/cardwire.spec index 17d9da6..42f51b7 100644 --- a/packages/fedora/cardwire.spec +++ b/packages/fedora/cardwire.spec @@ -1,5 +1,5 @@ Name: cardwire -Version: 0.5.0 +Version: 0.6.0 Release: 1%{?dist} Summary: A GPU manager for Linux using eBPF LSM hooks License: GPL-3.0