Merge pull request #3 from S4NKALP/dev
This commit was merged in pull request #3.
This commit is contained in:
18 files changed
+2010
-112
No files matched your search
Generated
+1029
File diff suppressed because it is too large.
Load diff
@@ -12,3 +12,6 @@ env_logger = "0.11"
|
||||
regex = "1.10"
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||
zbus = { version = "5.5.0", features = ["tokio"] }
|
||||
tokio = { version = "1.21", features = ["full"] }
|
||||
futures-util = "0.3"
|
||||
@@ -12,9 +12,7 @@
|
||||
|
||||
### Why lapctl?
|
||||
|
||||
Modern Linux laptops often have great hardware that goes underutilized or requires heavy, bloated background services to manage. **lapctl** changes that.
|
||||
|
||||
Built with performance and simplicity in mind, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). No background daemons (unless you want them), no heavy RAM usage just pure control from your terminal.
|
||||
Built with performance and simplicity in mind, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). A high-performance background daemon (**lapctld**) handles all privileged operations via a secure D-Bus interface, allowing you to control your laptop completely **without sudo**.
|
||||
|
||||
---
|
||||
|
||||
@@ -52,6 +50,8 @@ just install
|
||||
sudo lapctl install-rules
|
||||
```
|
||||
|
||||
*The `install-rules` command automatically sets up the D-Bus policy and starts the `lapctld` background service.*
|
||||
|
||||
#### Option 3: Using `cargo`
|
||||
|
||||
```bash
|
||||
@@ -74,9 +74,11 @@ sudo lapctl install-rules
|
||||
### Quick Start Guide
|
||||
|
||||
```bash
|
||||
# Install udev rules (Required for rootless control if not using AUR)
|
||||
# Install D-Bus policy & start daemon (Required for rootless control)
|
||||
sudo lapctl install-rules
|
||||
|
||||
# From now on, NO sudo is required!
|
||||
|
||||
# Manage your GPU
|
||||
lapctl gpu integrated # Max battery
|
||||
lapctl gpu hybrid # Best of both worlds
|
||||
@@ -104,9 +106,10 @@ lapctl touchpad enable
|
||||
lapctl display rates
|
||||
lapctl display set-rate 144
|
||||
|
||||
# Keep it awake
|
||||
lapctl inhibit --daemon # Run in background
|
||||
lapctl inhibit -- why "Critical update" ./long-task.sh
|
||||
# Keep it awake (requires lapctld)
|
||||
lapctl inhibit --daemon # Stay awake persistently (via daemon)
|
||||
lapctl inhibit --stop # Allow sleep again
|
||||
lapctl inhibit --why "Keeping awake" -- sleep 60 # One-shot (local)
|
||||
|
||||
# Check everything
|
||||
lapctl status
|
||||
@@ -124,7 +127,8 @@ lapctl
|
||||
├── src/
|
||||
│ ├── main.rs # The entry point
|
||||
│ ├── cli.rs # Command definition & parsing
|
||||
│ ├── commands/ # Feature logic (GPU, Battery, etc.)
|
||||
│ ├── daemon/ # lapctld (D-Bus interface & logic)
|
||||
│ ├── commands/ # CLI feature logic & proxies
|
||||
│ ├── hardware/ # Hardware specific drivers (NVIDIA, etc.)
|
||||
│ └── utils/ # System helpers
|
||||
│
|
||||
|
||||
+16
-1
@@ -64,7 +64,12 @@ pub enum Commands {
|
||||
/// Run the inhibitor in the background (daemon mode)
|
||||
#[arg(long)]
|
||||
daemon: bool,
|
||||
/// Stop any active persistent inhibition managed by the daemon
|
||||
#[arg(long)]
|
||||
stop: bool,
|
||||
},
|
||||
/// Start the lapctl background daemon (requires root)
|
||||
Daemon,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
@@ -72,7 +77,11 @@ pub enum GpuCommands {
|
||||
/// Query the current graphics mode
|
||||
Query,
|
||||
/// Switch to integrated graphics
|
||||
Integrated,
|
||||
Integrated {
|
||||
/// Switch without rebooting (experimental)
|
||||
#[arg(long)]
|
||||
no_reboot: bool,
|
||||
},
|
||||
/// Switch to hybrid graphics
|
||||
Hybrid {
|
||||
/// Setup PCI-Express Runtime D3 (RTD3) Power Management on Hybrid mode (0, 1, 2, 3)
|
||||
@@ -81,6 +90,9 @@ pub enum GpuCommands {
|
||||
/// Use nvidia-current instead of nvidia for kernel modules
|
||||
#[arg(long)]
|
||||
use_nvidia_current: bool,
|
||||
/// Switch without rebooting (experimental)
|
||||
#[arg(long)]
|
||||
no_reboot: bool,
|
||||
},
|
||||
/// Switch to NVIDIA graphics
|
||||
Nvidia {
|
||||
@@ -99,6 +111,9 @@ pub enum GpuCommands {
|
||||
/// Set up Nvidia mode for Wayland (skips Xorg configuration)
|
||||
#[arg(long)]
|
||||
wayland: bool,
|
||||
/// Switch without rebooting (experimental)
|
||||
#[arg(long)]
|
||||
no_reboot: bool,
|
||||
},
|
||||
/// Revert changes made by lapctl
|
||||
Reset,
|
||||
|
||||
@@ -2,8 +2,49 @@ use crate::cli::BatteryCommands;
|
||||
use log::{debug, error, info};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.lapctl1",
|
||||
default_service = "org.lapctl",
|
||||
default_path = "/org/lapctl"
|
||||
)]
|
||||
trait Lapctl {
|
||||
async fn set_battery_limit(&self, percent: u32) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
fn try_call_daemon(command: &BatteryCommands) -> bool {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match command {
|
||||
BatteryCommands::Limit { percent } => {
|
||||
proxy.set_battery_limit(*percent as u32).await.is_ok()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn execute(command: &BatteryCommands) {
|
||||
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
|
||||
println!("Request handled by lapctld daemon.");
|
||||
return;
|
||||
}
|
||||
|
||||
match command {
|
||||
BatteryCommands::Limit { percent } => {
|
||||
if !(&1..=&100).contains(&percent) {
|
||||
|
||||
@@ -2,6 +2,47 @@ use crate::cli::CoolingCommands;
|
||||
use log::{debug, info};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.lapctl1",
|
||||
default_service = "org.lapctl",
|
||||
default_path = "/org/lapctl"
|
||||
)]
|
||||
trait Lapctl {
|
||||
async fn set_cooling_profile(&self, profile: String) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
fn try_call_daemon(command: &CoolingCommands) -> bool {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match command {
|
||||
CoolingCommands::Performance => proxy
|
||||
.set_cooling_profile("performance".to_string())
|
||||
.await
|
||||
.is_ok(),
|
||||
CoolingCommands::Balanced => proxy
|
||||
.set_cooling_profile("balanced".to_string())
|
||||
.await
|
||||
.is_ok(),
|
||||
CoolingCommands::Quiet => proxy.set_cooling_profile("quiet".to_string()).await.is_ok(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_ideapad_fan_mode(mode: &str) -> bool {
|
||||
let mut success = false;
|
||||
@@ -38,6 +79,11 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn execute(command: &CoolingCommands) {
|
||||
if try_call_daemon(command) {
|
||||
println!("Request handled by lapctld daemon.");
|
||||
return;
|
||||
}
|
||||
|
||||
let (ideapad_mode, asus_mode, desc) = match command {
|
||||
CoolingCommands::Performance => ("1", "1", "Performance"),
|
||||
CoolingCommands::Balanced => ("0", "0", "Balanced"),
|
||||
|
||||
+257
-40
@@ -1,13 +1,101 @@
|
||||
use crate::cli::GpuCommands;
|
||||
use crate::hardware::gpu::{
|
||||
get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_bus,
|
||||
get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_addr,
|
||||
get_nvidia_gpu_pci_bus, kill_gpu_processes, remove_gpu, rescan_pci, unbind_gpu,
|
||||
};
|
||||
use crate::utils::system::{
|
||||
assert_root, create_file, get_active_graphical_sessions, get_display_manager,
|
||||
is_service_active, manage_service, rebuild_initramfs, terminate_session,
|
||||
};
|
||||
use crate::utils::system::{assert_root, create_file, get_display_manager, rebuild_initramfs};
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.lapctl1",
|
||||
default_service = "org.lapctl",
|
||||
default_path = "/org/lapctl"
|
||||
)]
|
||||
trait Lapctl {
|
||||
async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::Result<()>;
|
||||
async fn switch_gpu_hybrid(
|
||||
&self,
|
||||
rtd3: i32,
|
||||
use_nvidia_current: bool,
|
||||
no_reboot: bool,
|
||||
) -> zbus::Result<()>;
|
||||
async fn switch_gpu_nvidia(
|
||||
&self,
|
||||
dm: String,
|
||||
force_comp: bool,
|
||||
coolbits: i32,
|
||||
use_nvidia_current: bool,
|
||||
wayland: bool,
|
||||
no_reboot: bool,
|
||||
) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
fn try_call_daemon(cmd: &GpuCommands) -> bool {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match cmd {
|
||||
GpuCommands::Integrated { no_reboot } => {
|
||||
proxy.switch_gpu_integrated(*no_reboot).await.is_ok()
|
||||
}
|
||||
GpuCommands::Hybrid {
|
||||
rtd3,
|
||||
use_nvidia_current,
|
||||
no_reboot,
|
||||
} => {
|
||||
let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1);
|
||||
proxy
|
||||
.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
GpuCommands::Nvidia {
|
||||
dm,
|
||||
force_comp,
|
||||
coolbits,
|
||||
use_nvidia_current,
|
||||
wayland,
|
||||
no_reboot,
|
||||
} => {
|
||||
let dm_val = dm.clone().unwrap_or_default();
|
||||
let coolbits_val = coolbits.map(|v| v as i32).unwrap_or(-1);
|
||||
proxy
|
||||
.switch_gpu_nvidia(
|
||||
dm_val,
|
||||
*force_comp,
|
||||
coolbits_val,
|
||||
*use_nvidia_current,
|
||||
*wayland,
|
||||
*no_reboot,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
|
||||
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
|
||||
@@ -258,10 +346,12 @@ fn create_cache_obj(bus_id: String) -> Cache {
|
||||
}
|
||||
|
||||
fn create_cache_file() {
|
||||
if get_current_mode() != "hybrid" {
|
||||
error!("--cache-create requires that the system be in the hybrid Optimus mode");
|
||||
let pci_addr = get_nvidia_gpu_pci_addr();
|
||||
if pci_addr.is_none() {
|
||||
log::debug!("NVIDIA GPU not found, skipping cache creation");
|
||||
return;
|
||||
}
|
||||
|
||||
let bus_id = get_nvidia_gpu_pci_bus();
|
||||
let cache = create_cache_obj(bus_id);
|
||||
|
||||
@@ -272,7 +362,7 @@ fn create_cache_file() {
|
||||
if let Ok(json) = serde_json::to_string_pretty(&cache)
|
||||
&& fs::write(CACHE_FILE_PATH, json).is_ok()
|
||||
{
|
||||
log::debug!("Created file {}", CACHE_FILE_PATH);
|
||||
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,10 +380,84 @@ fn read_cache_file() -> Result<Cache, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_integrated() {
|
||||
fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String> {
|
||||
let dm_name = dm_flag.or_else(get_display_manager);
|
||||
|
||||
if let Some(ref name) = dm_name
|
||||
&& is_service_active(name)
|
||||
{
|
||||
info!("Stopping display manager: {}", name);
|
||||
manage_service(name, "stop")?;
|
||||
return Ok(Some(name.clone()));
|
||||
}
|
||||
|
||||
// If no active DM found, check for graphical sessions
|
||||
let sessions = get_active_graphical_sessions();
|
||||
if !sessions.is_empty() {
|
||||
println!("No active display manager found, but graphical sessions are running.");
|
||||
println!("Terminating graphical sessions for no-reboot GPU switching...");
|
||||
for session_id in sessions {
|
||||
if let Err(e) = terminate_session(&session_id) {
|
||||
error!("Failed to terminate session {}: {}", session_id, e);
|
||||
}
|
||||
}
|
||||
// Give some time for sessions to terminate
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn finalize_no_reboot(stopped_dm: Option<String>) {
|
||||
if let Some(name) = stopped_dm {
|
||||
info!("Starting display manager: {}", name);
|
||||
let _ = manage_service(&name, "start");
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_integrated(no_reboot: bool) {
|
||||
assert_root();
|
||||
println!("Switching to integrated mode");
|
||||
|
||||
if !Path::new(CACHE_FILE_PATH).exists() {
|
||||
create_cache_file();
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
|
||||
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
|
||||
|
||||
let mut stopped_dm = None;
|
||||
if no_reboot {
|
||||
match prepare_no_reboot(None) {
|
||||
Ok(dm) => stopped_dm = dm,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = kill_gpu_processes() {
|
||||
error!("Failed to kill GPU processes: {}", e);
|
||||
}
|
||||
|
||||
// Try to unload modules
|
||||
let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"];
|
||||
for module in modules {
|
||||
let _ = Command::new("modprobe").args(["-r", module]).status();
|
||||
}
|
||||
|
||||
if let Some(pci_addr) = get_nvidia_gpu_pci_addr() {
|
||||
if let Err(e) = unbind_gpu(&pci_addr) {
|
||||
error!("Failed to unbind GPU: {}", e);
|
||||
}
|
||||
if let Err(e) = remove_gpu(&pci_addr) {
|
||||
error!("Failed to remove GPU: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
let mut dis_cmd = Command::new("systemctl");
|
||||
dis_cmd.args(["disable", "nvidia-persistenced.service"]);
|
||||
@@ -306,21 +470,17 @@ fn switch_integrated() {
|
||||
_ => error!("An error ocurred while disabling service"),
|
||||
}
|
||||
|
||||
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
|
||||
create_cache_file();
|
||||
if no_reboot {
|
||||
println!("Operation completed successfully");
|
||||
finalize_no_reboot(stopped_dm);
|
||||
} else {
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
|
||||
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
|
||||
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
}
|
||||
|
||||
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
|
||||
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
|
||||
assert_root();
|
||||
println!("Switching to hybrid mode");
|
||||
println!(
|
||||
@@ -328,12 +488,31 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
|
||||
rtd3.is_some()
|
||||
);
|
||||
|
||||
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
|
||||
if no_reboot && let Err(e) = rescan_pci() {
|
||||
error!("Failed to rescan PCI bus: {}", e);
|
||||
}
|
||||
|
||||
if !Path::new(CACHE_FILE_PATH).exists() {
|
||||
create_cache_file();
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
let mut stopped_dm = None;
|
||||
if no_reboot {
|
||||
match prepare_no_reboot(None) {
|
||||
Ok(dm) => stopped_dm = dm,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = kill_gpu_processes() {
|
||||
error!("Failed to kill GPU processes: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// persistenced enable
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
let mut enable_cmd = Command::new("systemctl");
|
||||
@@ -370,9 +549,14 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
|
||||
create_file(MODESET_PATH, &modeset_content, false);
|
||||
}
|
||||
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
if no_reboot {
|
||||
println!("Operation completed successfully");
|
||||
finalize_no_reboot(stopped_dm);
|
||||
} else {
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_nvidia(
|
||||
@@ -381,26 +565,18 @@ fn switch_nvidia(
|
||||
coolbits: Option<u32>,
|
||||
use_nvidia_current: bool,
|
||||
wayland: bool,
|
||||
no_reboot: bool,
|
||||
) {
|
||||
assert_root();
|
||||
println!("Switching to nvidia mode");
|
||||
println!("Enable ForceCompositionPipeline: {}", force_comp);
|
||||
println!("Enable Coolbits: {}", coolbits.is_some());
|
||||
|
||||
// persistenced enable
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
let mut enable_cmd = Command::new("systemctl");
|
||||
enable_cmd.args(["enable", "nvidia-persistenced.service"]);
|
||||
if !is_debug {
|
||||
enable_cmd.stdout(std::process::Stdio::null());
|
||||
enable_cmd.stderr(std::process::Stdio::null());
|
||||
}
|
||||
match enable_cmd.status() {
|
||||
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
||||
_ => error!("An error ocurred while enabling service"),
|
||||
if no_reboot && let Err(e) = rescan_pci() {
|
||||
error!("Failed to rescan PCI bus: {}", e);
|
||||
}
|
||||
|
||||
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
|
||||
if !Path::new(CACHE_FILE_PATH).exists() {
|
||||
create_cache_file();
|
||||
}
|
||||
|
||||
@@ -454,7 +630,7 @@ fn switch_nvidia(
|
||||
create_file(EXTRA_XORG_PATH, &extra_xorg, false);
|
||||
}
|
||||
|
||||
let display_manager = dm.or_else(get_display_manager);
|
||||
let display_manager = dm.clone().or_else(get_display_manager);
|
||||
|
||||
if let Some(cdm) = display_manager {
|
||||
if cdm == "sddm" {
|
||||
@@ -480,9 +656,42 @@ fn switch_nvidia(
|
||||
}
|
||||
}
|
||||
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
let mut stopped_dm = None;
|
||||
if no_reboot {
|
||||
match prepare_no_reboot(dm.clone()) {
|
||||
Ok(dm_name) => stopped_dm = dm_name,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = kill_gpu_processes() {
|
||||
error!("Failed to kill GPU processes: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// persistenced enable
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
let mut enable_cmd = Command::new("systemctl");
|
||||
enable_cmd.args(["enable", "nvidia-persistenced.service"]);
|
||||
if !is_debug {
|
||||
enable_cmd.stdout(std::process::Stdio::null());
|
||||
enable_cmd.stderr(std::process::Stdio::null());
|
||||
}
|
||||
match enable_cmd.status() {
|
||||
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
||||
_ => error!("An error ocurred while enabling service"),
|
||||
}
|
||||
|
||||
if no_reboot {
|
||||
println!("Operation completed successfully");
|
||||
finalize_no_reboot(stopped_dm);
|
||||
} else {
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
}
|
||||
}
|
||||
|
||||
fn run_on_dgpu(command: &[String]) {
|
||||
@@ -523,6 +732,11 @@ fn run_on_dgpu(command: &[String]) {
|
||||
}
|
||||
|
||||
pub fn execute(cmd: &GpuCommands) {
|
||||
if try_call_daemon(cmd) {
|
||||
println!("Request handled by lapctld daemon.");
|
||||
return;
|
||||
}
|
||||
|
||||
match cmd {
|
||||
GpuCommands::Query => {
|
||||
let mode = get_current_mode();
|
||||
@@ -570,23 +784,26 @@ pub fn execute(cmd: &GpuCommands) {
|
||||
create_file(SDDM_XSETUP_PATH, SDDM_XSETUP_CONTENT, true);
|
||||
println!("Operation completed successfully");
|
||||
}
|
||||
GpuCommands::Integrated => switch_integrated(),
|
||||
GpuCommands::Integrated { no_reboot } => switch_integrated(*no_reboot),
|
||||
GpuCommands::Hybrid {
|
||||
rtd3,
|
||||
use_nvidia_current,
|
||||
} => switch_hybrid(*rtd3, *use_nvidia_current),
|
||||
no_reboot,
|
||||
} => switch_hybrid(*rtd3, *use_nvidia_current, *no_reboot),
|
||||
GpuCommands::Nvidia {
|
||||
dm,
|
||||
force_comp,
|
||||
coolbits,
|
||||
use_nvidia_current,
|
||||
wayland,
|
||||
no_reboot,
|
||||
} => switch_nvidia(
|
||||
dm.clone(),
|
||||
*force_comp,
|
||||
*coolbits,
|
||||
*use_nvidia_current,
|
||||
*wayland,
|
||||
*no_reboot,
|
||||
),
|
||||
GpuCommands::Run { command } => run_on_dgpu(command),
|
||||
}
|
||||
|
||||
+57
-1
@@ -1,7 +1,63 @@
|
||||
use log::{error, info};
|
||||
use std::process::Command;
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.lapctl1",
|
||||
default_service = "org.lapctl",
|
||||
default_path = "/org/lapctl"
|
||||
)]
|
||||
trait Lapctl {
|
||||
async fn set_system_inhibition(
|
||||
&self,
|
||||
active: bool,
|
||||
why: String,
|
||||
who: String,
|
||||
) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
fn try_call_daemon_inhibit(active: bool, why: String, who: String) -> bool {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
proxy.set_system_inhibition(active, why, who).await.is_ok()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool, stop: bool) {
|
||||
if stop {
|
||||
if try_call_daemon_inhibit(false, why.to_string(), who.to_string()) {
|
||||
println!("Persistent system inhibition deactivated via lapctld.");
|
||||
} else {
|
||||
error!("Failed to deactivate inhibition via daemon. Is lapctld running?");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if daemon && command_args.is_empty() {
|
||||
if try_call_daemon_inhibit(true, why.to_string(), who.to_string()) {
|
||||
println!("Persistent system inhibition activated via lapctld.");
|
||||
println!("The system will stay awake until you run: lapctl inhibit --stop");
|
||||
return;
|
||||
} else {
|
||||
error!("Failed to activate inhibition via daemon. Is lapctld running?");
|
||||
// Fallback to local systemd-inhibit if daemon call fails
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool) {
|
||||
let args = build_args(command_args, why, who);
|
||||
|
||||
if command_args.is_empty() {
|
||||
|
||||
@@ -2,74 +2,118 @@ use crate::utils::system::{assert_root, create_file};
|
||||
use log::info;
|
||||
use std::process::Command;
|
||||
|
||||
const LAPCTL_UDEV_RULES: &str = r#"# /etc/udev/rules.d/99-lapctl.rules
|
||||
# Automatically generated by lapctl
|
||||
# This file grants read/write permissions to all local users
|
||||
# for the SysFS endpoints required by lapctl's power, cooling, and battery commands.
|
||||
// No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
|
||||
const LAPCTL_UDEV_RULES: &str = "# lapctl udev rules (placeholder)\n";
|
||||
|
||||
# ACPI Platform Profiles & CPU Scaling
|
||||
ACTION=="add", SUBSYSTEM=="firmware", RUN+="/bin/sh -c 'chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null'"
|
||||
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null'"
|
||||
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null'"
|
||||
const POLKIT_POLICY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC
|
||||
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||
<policyconfig>
|
||||
<vendor>lapctl</vendor>
|
||||
<vendor_url>https://github.com/S4NKALP/lapctl</vendor_url>
|
||||
<action id="org.s4nkalp.lapctl.execute">
|
||||
<description>Elevated execution for lapctl</description>
|
||||
<message>Authentication is required to perform privileged lapctl operations</message>
|
||||
<defaults>
|
||||
<allow_any>auth_admin</allow_any>
|
||||
<allow_inactive>auth_admin</allow_inactive>
|
||||
<allow_active>yes</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/lapctl</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.allow_gui">false</annotate>
|
||||
</action>
|
||||
</policyconfig>
|
||||
"#;
|
||||
|
||||
# Battery Charging Limits
|
||||
ACTION=="add", SUBSYSTEM=="power_supply", RUN+="/bin/sh -c 'chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null'"
|
||||
const POLKIT_RULES: &str = r#"/* /etc/polkit-1/rules.d/10-lapctl.rules
|
||||
* Automatically generated by lapctl
|
||||
* This rule allows users in the 'wheel' or 'sudo' groups to
|
||||
* perform privileged lapctl operations via pkexec without a password.
|
||||
*/
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (action.id == "org.freedesktop.policykit.exec" &&
|
||||
action.lookup("program").indexOf("lapctl") !== -1 &&
|
||||
(subject.isInGroup("wheel") || subject.isInGroup("sudo"))) {
|
||||
return polkit.Result.YES;
|
||||
}
|
||||
});
|
||||
"#;
|
||||
|
||||
# Lenovo Ideapad Thermal and Conservation Mode
|
||||
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null'"
|
||||
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null'"
|
||||
const DBUS_POLICY: &str = r#"<!DOCTYPE busconfig PUBLIC
|
||||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="org.lapctl"/>
|
||||
<allow send_destination="org.lapctl"/>
|
||||
<allow receive_sender="org.lapctl"/>
|
||||
</policy>
|
||||
<policy group="wheel">
|
||||
<allow send_destination="org.lapctl"/>
|
||||
<allow receive_sender="org.lapctl"/>
|
||||
</policy>
|
||||
<policy group="sudo">
|
||||
<allow send_destination="org.lapctl"/>
|
||||
<allow receive_sender="org.lapctl"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
"#;
|
||||
|
||||
# ASUS WMI Thermal Policy
|
||||
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null'"
|
||||
const SYSTEMD_SERVICE: &str = r#"[Unit]
|
||||
Description=lapctl hardware management daemon
|
||||
After=dbus.service
|
||||
|
||||
# Intel RAPL TDP Control
|
||||
ACTION=="add", SUBSYSTEM=="powercap", RUN+="/bin/sh -c 'chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null'"
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/lapctl daemon
|
||||
Restart=on-failure
|
||||
|
||||
# AMD HWMon Power Capping
|
||||
ACTION=="add", SUBSYSTEM=="hwmon", RUN+="/bin/sh -c 'find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null'"
|
||||
|
||||
# Input Device Inhibition (Touchpad)
|
||||
ACTION=="add", SUBSYSTEM=="input", RUN+="/bin/sh -c 'find /sys/class/input -name \"inhibited\" -exec chmod a+w {} + 2>/dev/null'"
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"#;
|
||||
|
||||
pub fn execute() {
|
||||
assert_root();
|
||||
|
||||
println!("Installing lapctl udev rules for rootless hardware configuration...");
|
||||
println!("Installing lapctl configuration for daemon communication...");
|
||||
|
||||
let dbus_policy_path = "/etc/dbus-1/system.d/org.lapctl.conf";
|
||||
create_file(dbus_policy_path, DBUS_POLICY, false);
|
||||
|
||||
let service_path = "/etc/systemd/system/lapctld.service";
|
||||
create_file(service_path, SYSTEMD_SERVICE, false);
|
||||
|
||||
// Also install the udev rules file (even if empty now) to avoid errors if it was previously there
|
||||
let rules_path = "/etc/udev/rules.d/99-lapctl.rules";
|
||||
create_file(rules_path, LAPCTL_UDEV_RULES, false);
|
||||
|
||||
println!("Successfully wrote rules to {}", rules_path);
|
||||
println!("Loading rules and applying immediate session permissions...");
|
||||
// Polkit fallbacks for rootless execution without the daemon
|
||||
let polkit_policy_path = "/usr/share/polkit-1/actions/org.s4nkalp.lapctl.policy";
|
||||
create_file(polkit_policy_path, POLKIT_POLICY, false);
|
||||
|
||||
let _ = Command::new("udevadm")
|
||||
.args(["control", "--reload-rules"])
|
||||
let polkit_rules_path = "/etc/polkit-1/rules.d/10-lapctl.rules";
|
||||
create_file(polkit_rules_path, POLKIT_RULES, false);
|
||||
|
||||
println!("Successfully installed configuration files.");
|
||||
println!("Activating background daemon (lapctld)...");
|
||||
|
||||
// Reload systemd to recognize the new service
|
||||
let _ = Command::new("systemctl").arg("daemon-reload").status();
|
||||
|
||||
// Enable and start the daemon
|
||||
let status = Command::new("systemctl")
|
||||
.args(["enable", "--now", "lapctld"])
|
||||
.status();
|
||||
|
||||
let _ = Command::new("udevadm").args(["trigger"]).status();
|
||||
|
||||
// The udevadm trigger often fails to catch already-initialized ACPI nodes without a reboot,
|
||||
// so we manually apply the permissions for the current session right now.
|
||||
let commands = vec![
|
||||
"chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null",
|
||||
"chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null",
|
||||
"chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null",
|
||||
"chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null",
|
||||
"chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null",
|
||||
"chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null",
|
||||
"chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null",
|
||||
"chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null",
|
||||
"find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null",
|
||||
"find /sys/class/input -name \"inhibited\" -exec chmod a+w {} + 2>/dev/null",
|
||||
];
|
||||
|
||||
for cmd in commands {
|
||||
let _ = Command::new("sh").arg("-c").arg(cmd).status();
|
||||
if let Ok(s) = status {
|
||||
if s.success() {
|
||||
info!("lapctld daemon enabled and started.");
|
||||
} else {
|
||||
log::error!("Failed to enable/start lapctld daemon.");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Udev reload and immediate ACL application complete.");
|
||||
println!(
|
||||
"Installation successful! You can now use lapctl power, cooling, and battery commands without sudo."
|
||||
);
|
||||
println!("Installation successful! The lapctld daemon is now running.");
|
||||
println!("You can now use all lapctl commands without sudo.");
|
||||
}
|
||||
@@ -2,6 +2,52 @@ use crate::cli::PowerCommands;
|
||||
use log::{error, info};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.lapctl1",
|
||||
default_service = "org.lapctl",
|
||||
default_path = "/org/lapctl"
|
||||
)]
|
||||
trait Lapctl {
|
||||
async fn set_power_profile(&self, profile: String) -> zbus::Result<()>;
|
||||
async fn set_tdp_limit(&self, watts: u32) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
fn try_call_daemon(command: &PowerCommands) -> bool {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match command {
|
||||
PowerCommands::Performance => proxy
|
||||
.set_power_profile("performance".to_string())
|
||||
.await
|
||||
.is_ok(),
|
||||
PowerCommands::Balanced => proxy
|
||||
.set_power_profile("balanced".to_string())
|
||||
.await
|
||||
.is_ok(),
|
||||
PowerCommands::BatterySave => proxy
|
||||
.set_power_profile("battery-save".to_string())
|
||||
.await
|
||||
.is_ok(),
|
||||
PowerCommands::LimitTdp { watts } => proxy.set_tdp_limit(*watts).await.is_ok(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn set_cpu_governor(governor: &str) {
|
||||
let cpu_dir = Path::new("/sys/devices/system/cpu");
|
||||
@@ -155,6 +201,11 @@ fn set_platform_profile(profile: &str) {
|
||||
}
|
||||
|
||||
pub fn execute(command: &PowerCommands) {
|
||||
if try_call_daemon(command) {
|
||||
println!("Request handled by lapctld daemon.");
|
||||
return;
|
||||
}
|
||||
|
||||
match command {
|
||||
PowerCommands::Performance => {
|
||||
println!("Setting power profile to Performance");
|
||||
|
||||
@@ -2,8 +2,47 @@ use crate::cli::TouchpadCommands;
|
||||
use log::{error, info};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
#[proxy(
|
||||
interface = "org.lapctl1",
|
||||
default_service = "org.lapctl",
|
||||
default_path = "/org/lapctl"
|
||||
)]
|
||||
trait Lapctl {
|
||||
async fn set_touchpad_inhibition(&self, inhibited: bool) -> zbus::Result<()>;
|
||||
}
|
||||
|
||||
fn try_call_daemon(command: &TouchpadCommands) -> bool {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
rt.block_on(async {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(conn) => conn,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
match command {
|
||||
TouchpadCommands::Enable => proxy.set_touchpad_inhibition(false).await.is_ok(),
|
||||
TouchpadCommands::Disable => proxy.set_touchpad_inhibition(true).await.is_ok(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn execute(command: &TouchpadCommands) {
|
||||
if try_call_daemon(command) {
|
||||
println!("Request handled by lapctld daemon.");
|
||||
return;
|
||||
}
|
||||
|
||||
let sys_class_input = Path::new("/sys/class/input");
|
||||
if !sys_class_input.exists() {
|
||||
error!("Could not find /sys/class/input. Is this a Linux system?");
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
use crate::cli::GpuCommands;
|
||||
use crate::commands::gpu;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use zbus::interface;
|
||||
use zbus::zvariant::OwnedFd;
|
||||
|
||||
pub struct LapctlInterface {
|
||||
inhibit_fd: Arc<Mutex<Option<OwnedFd>>>,
|
||||
}
|
||||
|
||||
impl Default for LapctlInterface {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inhibit_fd: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "org.lapctl1")]
|
||||
impl LapctlInterface {
|
||||
pub async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::fdo::Result<()> {
|
||||
gpu::execute(&GpuCommands::Integrated { no_reboot });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn switch_gpu_hybrid(
|
||||
&self,
|
||||
rtd3: i32,
|
||||
use_nvidia_current: bool,
|
||||
no_reboot: bool,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let rtd3_opt = if rtd3 < 0 { None } else { Some(rtd3 as u8) };
|
||||
gpu::execute(&GpuCommands::Hybrid {
|
||||
rtd3: rtd3_opt,
|
||||
use_nvidia_current,
|
||||
no_reboot,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn switch_gpu_nvidia(
|
||||
&self,
|
||||
dm: String,
|
||||
force_comp: bool,
|
||||
coolbits: i32,
|
||||
use_nvidia_current: bool,
|
||||
wayland: bool,
|
||||
no_reboot: bool,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let dm_opt = if dm.is_empty() { None } else { Some(dm) };
|
||||
let coolbits_opt = if coolbits < 0 {
|
||||
None
|
||||
} else {
|
||||
Some(coolbits as u32)
|
||||
};
|
||||
gpu::execute(&GpuCommands::Nvidia {
|
||||
dm: dm_opt,
|
||||
force_comp,
|
||||
coolbits: coolbits_opt,
|
||||
use_nvidia_current,
|
||||
wayland,
|
||||
no_reboot,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_battery_limit(&self, percent: u32) -> zbus::fdo::Result<()> {
|
||||
crate::commands::battery::execute(&crate::cli::BatteryCommands::Limit {
|
||||
percent: percent as u8,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_power_profile(&self, profile: String) -> zbus::fdo::Result<()> {
|
||||
let cmd = match profile.as_str() {
|
||||
"performance" => crate::cli::PowerCommands::Performance,
|
||||
"balanced" => crate::cli::PowerCommands::Balanced,
|
||||
"battery-save" => crate::cli::PowerCommands::BatterySave,
|
||||
_ => {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(format!(
|
||||
"Invalid power profile: {}",
|
||||
profile
|
||||
)));
|
||||
}
|
||||
};
|
||||
crate::commands::power::execute(&cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_tdp_limit(&self, watts: u32) -> zbus::fdo::Result<()> {
|
||||
crate::commands::power::execute(&crate::cli::PowerCommands::LimitTdp { watts });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_cooling_profile(&self, profile: String) -> zbus::fdo::Result<()> {
|
||||
let cmd = match profile.as_str() {
|
||||
"performance" => crate::cli::CoolingCommands::Performance,
|
||||
"balanced" => crate::cli::CoolingCommands::Balanced,
|
||||
"quiet" => crate::cli::CoolingCommands::Quiet,
|
||||
_ => {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(format!(
|
||||
"Invalid cooling profile: {}",
|
||||
profile
|
||||
)));
|
||||
}
|
||||
};
|
||||
crate::commands::cooling::execute(&cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_touchpad_inhibition(&self, inhibited: bool) -> zbus::fdo::Result<()> {
|
||||
let cmd = if inhibited {
|
||||
crate::cli::TouchpadCommands::Disable
|
||||
} else {
|
||||
crate::cli::TouchpadCommands::Enable
|
||||
};
|
||||
crate::commands::touchpad::execute(&cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_system_inhibition(
|
||||
&self,
|
||||
active: bool,
|
||||
why: String,
|
||||
who: String,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let mut fd_lock = self.inhibit_fd.lock().await;
|
||||
|
||||
if active {
|
||||
if fd_lock.is_some() {
|
||||
return Ok(()); // Already inhibiting
|
||||
}
|
||||
|
||||
let conn = zbus::Connection::system().await.map_err(|e| {
|
||||
zbus::fdo::Error::Failed(format!("Failed to connect to system bus: {}", e))
|
||||
})?;
|
||||
|
||||
let msg = conn
|
||||
.call_method(
|
||||
Some("org.freedesktop.login1"),
|
||||
"/org/freedesktop/login1",
|
||||
Some("org.freedesktop.login1.Manager"),
|
||||
"Inhibit",
|
||||
&("sleep:idle", who, why, "block"),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to inhibit: {}", e)))?;
|
||||
|
||||
let fd: OwnedFd = msg
|
||||
.body()
|
||||
.deserialize()
|
||||
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to parse FD: {}", e)))?;
|
||||
|
||||
*fd_lock = Some(fd);
|
||||
log::info!("System inhibition activated via lapctld.");
|
||||
} else {
|
||||
*fd_lock = None;
|
||||
log::info!("System inhibition deactivated via lapctld.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use log::info;
|
||||
use std::error::Error;
|
||||
use zbus::connection::Builder;
|
||||
|
||||
mod dbus_iface;
|
||||
|
||||
pub async fn run() -> Result<(), Box<dyn Error>> {
|
||||
info!("Starting lapctld daemon...");
|
||||
let _connection = Builder::system()?
|
||||
.name("org.lapctl")?
|
||||
.serve_at("/org/lapctl", dbus_iface::LapctlInterface::default())?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
info!("Daemon is running on the system bus.");
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,71 @@ pub fn get_nvidia_gpu_pci_bus() -> String {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
pub fn get_nvidia_gpu_pci_addr() -> Option<String> {
|
||||
let output = Command::new("lspci")
|
||||
.output()
|
||||
.expect("Failed to execute lspci");
|
||||
let lspci_output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
for line in lspci_output.lines() {
|
||||
if line.contains("NVIDIA")
|
||||
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
|
||||
{
|
||||
let pci_addr = line.split_whitespace().next().unwrap();
|
||||
if pci_addr.contains(':') {
|
||||
if pci_addr.chars().filter(|&c| c == ':').count() == 1 {
|
||||
return Some(format!("0000:{}", pci_addr));
|
||||
}
|
||||
return Some(pci_addr.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn unbind_gpu(pci_addr: &str) -> Result<(), String> {
|
||||
let driver_path = Path::new("/sys/bus/pci/devices")
|
||||
.join(pci_addr)
|
||||
.join("driver");
|
||||
if driver_path.exists() {
|
||||
let unbind_path = driver_path.join("unbind");
|
||||
info!("Unbinding GPU at {} via {:?}", pci_addr, unbind_path);
|
||||
std::fs::write(unbind_path, pci_addr).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_gpu(pci_addr: &str) -> Result<(), String> {
|
||||
let remove_path = Path::new("/sys/bus/pci/devices")
|
||||
.join(pci_addr)
|
||||
.join("remove");
|
||||
if remove_path.exists() {
|
||||
info!("Removing GPU at {} via {:?}", pci_addr, remove_path);
|
||||
std::fs::write(remove_path, "1").map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rescan_pci() -> Result<(), String> {
|
||||
let rescan_path = Path::new("/sys/bus/pci/rescan");
|
||||
info!("Rescanning PCI bus via {:?}", rescan_path);
|
||||
std::fs::write(rescan_path, "1").map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn kill_gpu_processes() -> Result<(), String> {
|
||||
info!("Killing processes using NVIDIA GPU...");
|
||||
// Try using fuser first as it is more likely to be available and easy to use
|
||||
let _ = Command::new("fuser").args(["-k", "/dev/nvidia*"]).output();
|
||||
|
||||
// Also kill specific nvidia services if they are running
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["stop", "nvidia-persistenced.service"])
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_igpu_vendor() -> Option<String> {
|
||||
let output = Command::new("lspci").output().ok()?;
|
||||
let lspci_output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod cli;
|
||||
pub mod commands;
|
||||
pub mod daemon;
|
||||
pub mod hardware;
|
||||
pub mod utils;
|
||||
+11
-1
@@ -35,6 +35,16 @@ fn main() {
|
||||
why,
|
||||
who,
|
||||
daemon,
|
||||
} => commands::inhibit::execute(command, why, who, *daemon),
|
||||
stop,
|
||||
} => commands::inhibit::execute(command, why, who, *daemon, *stop),
|
||||
Commands::Daemon => {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
if let Err(e) = lapctl::daemon::run().await {
|
||||
eprintln!("Daemon error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-10
@@ -9,8 +9,25 @@ pub fn assert_root() {
|
||||
if let Ok(output) = output {
|
||||
let is_root = String::from_utf8_lossy(&output.stdout).trim() == "0";
|
||||
if !is_root {
|
||||
error!("This operation requires root privileges");
|
||||
std::process::exit(1);
|
||||
// Try to re-exec with pkexec
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
info!("Not running as root. Attempting to elevate privileges via pkexec...");
|
||||
let status = Command::new("pkexec").arg(exe).args(&args).status();
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => std::process::exit(0),
|
||||
_ => {
|
||||
error!("Privilege escalation failed or was cancelled.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"This operation requires root privileges and could not determine executable path."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,19 +60,74 @@ pub fn create_file(path: &str, content: &str, executable: bool) {
|
||||
}
|
||||
|
||||
pub fn get_display_manager() -> Option<String> {
|
||||
if let Ok(content) = fs::read_to_string("/etc/systemd/system/display-manager.service") {
|
||||
let re = regex::Regex::new(r"ExecStart=(.+)\n").unwrap();
|
||||
if let Some(cap) = re.captures(&content) {
|
||||
let path = cap.get(1).unwrap().as_str();
|
||||
let dm = Path::new(path).file_name()?.to_string_lossy().to_string();
|
||||
info!("Found {} Display Manager", dm);
|
||||
return Some(dm);
|
||||
}
|
||||
let output = Command::new("systemctl")
|
||||
.args(["show", "-p", "FragmentPath", "display-manager.service"])
|
||||
.output()
|
||||
.ok()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(line) = stdout.lines().next()
|
||||
&& let Some(path_str) = line.strip_prefix("FragmentPath=")
|
||||
&& !path_str.is_empty()
|
||||
&& path_str != "/dev/null"
|
||||
{
|
||||
let dm = Path::new(path_str)
|
||||
.file_name()?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
info!("Found {} Display Manager", dm);
|
||||
return Some(dm);
|
||||
}
|
||||
warn!("Display Manager detection is not available");
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_service_active(name: &str) -> bool {
|
||||
let output = Command::new("systemctl").args(["is-active", name]).output();
|
||||
if let Ok(output) = output {
|
||||
return String::from_utf8_lossy(&output.stdout).trim() == "active";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_active_graphical_sessions() -> Vec<String> {
|
||||
let mut sessions = Vec::new();
|
||||
let output = Command::new("loginctl")
|
||||
.args(["list-sessions", "--no-legend"])
|
||||
.output();
|
||||
|
||||
if let Ok(output) = output {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if let Some(id) = parts.first() {
|
||||
let show_output = Command::new("loginctl")
|
||||
.args(["show-session", id, "-p", "Type", "-p", "State"])
|
||||
.output();
|
||||
if let Ok(show_output) = show_output {
|
||||
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
|
||||
let mut is_graphical = false;
|
||||
let mut is_active = false;
|
||||
for show_line in show_stdout.lines() {
|
||||
if let Some(t) = show_line.strip_prefix("Type=") {
|
||||
if t == "x11" || t == "wayland" {
|
||||
is_graphical = true;
|
||||
}
|
||||
} else if let Some(s) = show_line.strip_prefix("State=")
|
||||
&& (s == "active" || s == "online")
|
||||
{
|
||||
is_active = true;
|
||||
}
|
||||
}
|
||||
if is_graphical && is_active {
|
||||
sessions.push(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sessions
|
||||
}
|
||||
|
||||
pub fn rebuild_initramfs() {
|
||||
let mut command: Vec<&str> = Vec::new();
|
||||
|
||||
@@ -112,3 +184,23 @@ pub fn rebuild_initramfs() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn manage_service(name: &str, action: &str) -> Result<(), String> {
|
||||
info!("Performing {} on service {}", action, name);
|
||||
let mut cmd = Command::new("systemctl");
|
||||
cmd.args([action, name]);
|
||||
match cmd.status() {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
_ => Err(format!("Failed to {} service {}", action, name)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn terminate_session(id: &str) -> Result<(), String> {
|
||||
info!("Terminating session {}", id);
|
||||
let mut cmd = Command::new("loginctl");
|
||||
cmd.args(["terminate-session", id]);
|
||||
match cmd.status() {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
_ => Err(format!("Failed to terminate session {}", id)),
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -11,6 +11,7 @@ fn test_inhibit_cli_parsing() {
|
||||
why,
|
||||
who,
|
||||
daemon,
|
||||
stop: _,
|
||||
} = cli.command
|
||||
{
|
||||
assert_eq!(command, vec!["sleep".to_string(), "10".to_string()]);
|
||||
@@ -30,8 +31,9 @@ fn test_inhibit_daemon_parsing() {
|
||||
if let Commands::Inhibit {
|
||||
command,
|
||||
why,
|
||||
who,
|
||||
who: _,
|
||||
daemon,
|
||||
stop: _,
|
||||
} = cli.command
|
||||
{
|
||||
assert!(command.is_empty());
|
||||
|
||||
Reference in new issue
Block a user