Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bfb0dda39 | ||
|
|
ae32ce9c8d | ||
|
|
11c3cec99d | ||
|
|
f69b74ab73 | ||
|
|
bdf1312dd1 | ||
|
|
2544f44309 | ||
|
|
34a57007d1 | ||
|
|
3684c5b071 | ||
|
|
8faccc117a | ||
|
|
92806e2843 | ||
|
|
bdc95801c3 | ||
|
|
2efa7a64f1 | ||
|
|
c8561bf4ff | ||
|
|
f202103a06 | ||
|
|
e0a589d441 | ||
|
|
151f5ad303 | ||
|
|
92298ddce8 | ||
|
|
c223fea5f4 | ||
|
|
80899602e5 | ||
|
|
21defdb70a | ||
|
|
90965f13b9 | ||
|
|
30a89fe2fe | ||
|
|
c7c06f09b8 | ||
|
|
e4c3646fe0 | ||
|
|
9e5b618f19 | ||
|
|
59dabcf6f4 | ||
|
|
c03abaadc5 | ||
|
|
972618a8e3 | ||
|
|
74aa1ff262 | ||
|
|
0219f1c8ce | ||
|
|
191afd2f12 | ||
|
|
02f8ff4c33 | ||
|
|
75c3a39597 | ||
|
|
f5f73a4990 | ||
|
|
c018aeda57 | ||
|
|
73e1edfd97 | ||
|
|
4e46604cfd | ||
|
|
b6c0ca0b9b | ||
|
|
94aed29728 | ||
|
|
d12d0a0916 | ||
|
|
30dadec637 | ||
|
|
dba3995608 | ||
|
|
c8a688f44f | ||
|
|
7de76d0365 | ||
|
|
055fdcd500 | ||
|
|
934aeda6aa | ||
|
|
8947bd643a | ||
|
|
5ac2aed516 | ||
|
|
4020f65e28 | ||
|
|
d52b3992e3 | ||
|
|
01d2e148a0 | ||
|
|
0dc00ec5c9 | ||
|
|
319ca54300 | ||
|
|
f12134a9c8 | ||
|
|
86ac9b556c | ||
|
|
c564259a19 | ||
|
|
46cbcb5b4b | ||
|
|
899d4c831f | ||
|
|
3590f693d3 | ||
|
|
e98346fad5 | ||
|
|
b0fdc0d58e | ||
|
|
c20a5d3a06 |
No files matched your search
@@ -0,0 +1,110 @@
|
||||
name: Release Smoke Test
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish (e.g. 0.3.4)'
|
||||
required: true
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
jobs:
|
||||
smoke-test:
|
||||
name: Smoke Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Run Clippy Lints
|
||||
run: cargo clippy -- -D warnings
|
||||
- name: Build release
|
||||
run: cargo build --release --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Smoke test — binary runs
|
||||
run: ./target/release/lapctl --version
|
||||
- name: Upload release artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lapctl-${{ github.ref_name }}
|
||||
path: ./target/release/lapctl
|
||||
- name: Publish to crates.io
|
||||
continue-on-error: true
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish
|
||||
publish-aur:
|
||||
name: Publish to AUR
|
||||
needs: smoke-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version || github.ref_name }}
|
||||
- name: Download release binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: lapctl-${{ github.ref_name }}
|
||||
path: .
|
||||
- name: Create tarball with binary
|
||||
run: |
|
||||
tar czf "lapctl-${{ steps.version.outputs.version }}-x86_64.tar.gz" \
|
||||
lapctl LICENSE
|
||||
- name: Generate PKGBUILD with cargo-aur
|
||||
run: |
|
||||
cargo install cargo-aur
|
||||
mkdir -p aur-pkg
|
||||
cargo aur --output aur-pkg
|
||||
- name: Update PKGBUILD checksums
|
||||
run: |
|
||||
SHA=$(sha256sum "lapctl-${{ steps.version.outputs.version }}-x86_64.tar.gz" | cut -d' ' -f1)
|
||||
sed -i "s/sha256sums=([^)]*)/sha256sums=('$SHA')/" aur-pkg/PKGBUILD
|
||||
- name: Generate .SRCINFO
|
||||
run: |
|
||||
docker run --rm -v "$(pwd)/aur-pkg:/build" archlinux:latest \
|
||||
bash -c "
|
||||
useradd -m builder
|
||||
chown -R builder:builder /build
|
||||
cd /build && su builder -c 'makepkg --printsrcinfo > .SRCINFO'
|
||||
"
|
||||
- name: Push to AUR
|
||||
uses: KSXGitHub/github-actions-deploy-aur@v4.1.3
|
||||
with:
|
||||
pkgname: lapctl-bin
|
||||
pkgbuild: aur-pkg/PKGBUILD
|
||||
assets: aur-pkg/.SRCINFO
|
||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
ssh_keyscan_types: rsa,ecdsa,ed25519
|
||||
commit_username: ${{ github.actor }}
|
||||
commit_email: ${{ github.actor }}@users.noreply.github.com
|
||||
allow_empty_commits: true
|
||||
force_push: true
|
||||
post_process: |
|
||||
git add -A && git stash && git checkout master && git checkout stash -- . && git stash drop
|
||||
- name: Upload tarball to release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
TAG="v${{ steps.version.outputs.version }}"
|
||||
gh release create "$TAG" --title "$TAG" --notes "" 2>/dev/null || true
|
||||
gh release upload "$TAG" \
|
||||
"lapctl-${{ steps.version.outputs.version }}-x86_64.tar.gz" --clobber
|
||||
+1
-3
@@ -23,6 +23,4 @@ target
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
./lapctl.py
|
||||
lapctl.py
|
||||
/target
|
||||
Generated
+1132
-4
File diff suppressed because it is too large.
Load diff
+10
-1
@@ -1,7 +1,11 @@
|
||||
[package]
|
||||
name = "lapctl"
|
||||
version = "0.2.0"
|
||||
version = "0.3.5"
|
||||
edition = "2024"
|
||||
authors = ["S4NKALP"]
|
||||
description = "CLI tool for managing laptop hardware features on Linux"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/S4NKALP/lapctl"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
@@ -12,3 +16,8 @@ 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"
|
||||
ctrlc = "3.4"
|
||||
fs2 = "0.4"
|
||||
@@ -12,21 +12,18 @@
|
||||
|
||||
### 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 for speed and simplicity, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). A background daemon (**lapctld**) handles all privileged operations via a secure D-Bus interface, allowing you to control your laptop completely **without sudo**.
|
||||
|
||||
---
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Graphics Switching**: Effortlessly toggle between Integrated, NVIDIA, and Hybrid modes. Optimize for battery life on the go or raw performance at your desk.
|
||||
- **Graphics Switching**: Effortlessly toggle between Integrated, NVIDIA, and Hybrid modes. Wayland is auto detected; no `--wayland` flag needed. Optimize for battery life on the go or raw performance at your desk.
|
||||
- **Battery Health**: Modern batteries hate being at 100% all the time. Set custom charge limits (like 80%) to significantly extend your battery's lifespan.
|
||||
- **Power Tuning**: Switch through performance profiles or set hard CPU power (TDP) limits in Watts to keep things cool or let them loose.
|
||||
- **Intelligent Cooling**: Force your fans into Performance, Balanced, or Quiet modes (supporting ASUS and Lenovo laptops).
|
||||
- **Display Refresh Rate**: Easily query available refresh rates and change your active display's Hz on-the-fly (100% native Rust Wayland implementation using `zwlr_output_manager_v1` for wlroots compositors like Sway and Hyprland).
|
||||
- **Display Refresh Rate**: Easily query available refresh rates and change your active display's Hz on the fly (100% native Rust Wayland implementation using `zwlr_output_manager_v1` for wlroots compositors like Sway and Hyprland).
|
||||
- **Touchpad Toggle**: Quickly enable or disable your touchpad from the terminal when using an external mouse.
|
||||
- **Sleep Inhibitor**: Running a long compile or a critical download? Use the inhibitor to stop your laptop from falling asleep mid task.
|
||||
- **Instant Status**: Get a bird's eye view of your hardware state, battery health, and current limits with one simple command.
|
||||
|
||||
---
|
||||
@@ -35,23 +32,38 @@ Built with performance and simplicity in mind, it talks directly to your system'
|
||||
|
||||
**lapctl** is built with Rust. Ensure you have the [Rust toolchain](https://rustup.rs/) installed.
|
||||
|
||||
#### Option 1: Using `just` (Recommended)
|
||||
If you have [just](https://github.com/casey/just) installed:
|
||||
#### Option 1: Arch Linux (AUR)
|
||||
|
||||
```bash
|
||||
just install
|
||||
yay -S lapctl-bin
|
||||
sudo lapctl install-rules
|
||||
```
|
||||
|
||||
#### Option 2: Using `cargo`
|
||||
#### Option 2: crates.io
|
||||
|
||||
```bash
|
||||
cargo install --path .
|
||||
cargo install lapctl
|
||||
sudo lapctl install-rules
|
||||
```
|
||||
|
||||
#### Option 3: From source (using `just`)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/S4NKALP/lapctl.git
|
||||
cd lapctl
|
||||
just install
|
||||
sudo lapctl install-rules
|
||||
```
|
||||
|
||||
_The `install-rules` command automatically sets up the D-Bus policy and starts the `lapctld` background service._
|
||||
|
||||
#### Requirements
|
||||
- **systemd**: For sleep inhibitor (`systemd-inhibit`)
|
||||
- **GPU Switching (Optional)**: `xrandr` and `nvidia-settings` are strictly required **ONLY** when using the `lapctl gpu` command on X11 (to route proprietary NVIDIA Optimus drivers).
|
||||
|
||||
- **GPU Switching (Optional)**: `xrandr` and `nvidia-settings` are strictly required **ONLY** when using the `lapctl gpu` command on X11 (to route proprietary NVIDIA Optimus drivers). Wayland is auto detected so no extra flags are needed.
|
||||
- **Wayland Display**: Built entirely natively using `wayland-client` and `wayland-protocols-wlr` (no `wlr-randr` required!)
|
||||
|
||||
#### Limitations
|
||||
|
||||
- **GNOME / KDE Plasma (Wayland)**: The display refresh rate feature relies heavily on the `zwlr_output_manager_v1` protocol. This protocol is exclusive to wlroots-based compositors (like Sway and Hyprland). GNOME and KDE use their own disparate internal display protocols, meaning this feature will **not work** out-of-the-box on those Desktop Environments.
|
||||
|
||||
---
|
||||
@@ -59,11 +71,17 @@ cargo install --path .
|
||||
### Quick Start Guide
|
||||
|
||||
```bash
|
||||
# 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
|
||||
lapctl gpu nvidia # High performance
|
||||
lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode
|
||||
lapctl gpu integrated # Max battery
|
||||
lapctl gpu hybrid # Best of both worlds
|
||||
lapctl gpu nvidia # High performance
|
||||
lapctl gpu nvidia --no-reboot # Switch without rebooting (Ctrl+C safe)
|
||||
lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode
|
||||
|
||||
# Prolong battery life
|
||||
lapctl battery limit 80
|
||||
@@ -86,10 +104,6 @@ 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
|
||||
|
||||
# Check everything
|
||||
lapctl status
|
||||
```
|
||||
@@ -106,13 +120,19 @@ 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
|
||||
│
|
||||
└── tests/ # Robust integration & unit tests
|
||||
```
|
||||
|
||||
**Safety features:**
|
||||
- **Concurrency lock** -- Exclusive file lock (`/var/lock/lapctl.lock`) prevents concurrent GPU switch commands from corrupting state
|
||||
- **Interrupt safety** -- If `--no-reboot` GPU switching is interrupted (Ctrl+C, crash), the display manager is automatically restarted via a RAII guard
|
||||
- **Atomic writes** -- Cache file is written atomically (temp file + rename) to prevent partial/corrupt state on crash
|
||||
|
||||
---
|
||||
|
||||
### Development & Connection
|
||||
|
||||
+20
-16
@@ -44,6 +44,10 @@ pub enum Commands {
|
||||
},
|
||||
/// Hardware status
|
||||
Status,
|
||||
/// Show system temperatures
|
||||
Temperature,
|
||||
/// Show fan speeds
|
||||
Fan,
|
||||
/// Install udev rules for rootless operation
|
||||
InstallRules,
|
||||
/// Touchpad management
|
||||
@@ -51,28 +55,20 @@ pub enum Commands {
|
||||
#[command(subcommand)]
|
||||
command: TouchpadCommands,
|
||||
},
|
||||
/// Inhibit system sleep/suspend
|
||||
Inhibit {
|
||||
/// The command to run while inhibiting (optional)
|
||||
command: Vec<String>,
|
||||
/// Why the system is being inhibited
|
||||
#[arg(long, default_value = "lapctl inhibiting sleep")]
|
||||
why: String,
|
||||
/// Who is inhibiting the system
|
||||
#[arg(long, default_value = "lapctl")]
|
||||
who: String,
|
||||
/// Run the inhibitor in the background (daemon mode)
|
||||
#[arg(long)]
|
||||
daemon: bool,
|
||||
},
|
||||
/// Start the lapctl background daemon (requires root)
|
||||
Daemon,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
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 +77,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 +98,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,
|
||||
@@ -116,6 +118,8 @@ pub enum GpuCommands {
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
command: Vec<String>,
|
||||
},
|
||||
/// Show GPU power state and consumption
|
||||
Power,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
|
||||
+127
-61
@@ -2,6 +2,56 @@ 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 is_ideapad() -> bool {
|
||||
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
|
||||
}
|
||||
|
||||
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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => conn,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let res = match command {
|
||||
BatteryCommands::Limit { percent } => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_battery_limit(*percent as u32),
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
matches!(res, Ok(Ok(_)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn execute(command: &BatteryCommands) {
|
||||
match command {
|
||||
@@ -11,9 +61,51 @@ pub fn execute(command: &BatteryCommands) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("Setting battery charge limit to {}%", percent);
|
||||
if is_ideapad() {
|
||||
if *percent < 100 {
|
||||
println!("Setting battery charge limit (Lenovo Conservation Mode ~60%)...");
|
||||
} else {
|
||||
println!("Disabling battery charge limit (charging to 100%)...");
|
||||
}
|
||||
} else {
|
||||
println!("Setting battery charge limit to {}%...", percent);
|
||||
}
|
||||
}
|
||||
BatteryCommands::Status => {}
|
||||
}
|
||||
|
||||
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
|
||||
return;
|
||||
}
|
||||
|
||||
execute_local(command);
|
||||
}
|
||||
|
||||
pub fn execute_local(command: &BatteryCommands) {
|
||||
match command {
|
||||
BatteryCommands::Limit { percent } => {
|
||||
let mut success_count = 0;
|
||||
|
||||
// Ideapad fallback - search for conservation_mode in ideapad_acpi driver directories
|
||||
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") {
|
||||
for entry in drivers.flatten() {
|
||||
let conservation_path = entry.path().join("conservation_mode");
|
||||
if conservation_path.exists() {
|
||||
let val = if *percent < 100 { "1" } else { "0" };
|
||||
match fs::write(&conservation_path, val) {
|
||||
Ok(_) => {
|
||||
success_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to write to {}: {}", conservation_path.display(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only try standard thresholds if we haven't already succeeded with Ideapad fallback
|
||||
// or if we want to ensure all bases are covered. For now, let's do both but prioritize the message.
|
||||
let sys_class_power = Path::new("/sys/class/power_supply");
|
||||
|
||||
if let Ok(entries) = fs::read_dir(sys_class_power) {
|
||||
@@ -24,6 +116,10 @@ pub fn execute(command: &BatteryCommands) {
|
||||
if name_str.starts_with("BAT") {
|
||||
let threshold_path = entry.path().join("charge_control_end_threshold");
|
||||
if threshold_path.exists() {
|
||||
println!(
|
||||
"Setting battery charge limit for {} to {}%",
|
||||
name_str, percent
|
||||
);
|
||||
match fs::write(&threshold_path, percent.to_string()) {
|
||||
Ok(_) => {
|
||||
info!("Successfully set limit for {}", name_str);
|
||||
@@ -33,49 +129,6 @@ pub fn execute(command: &BatteryCommands) {
|
||||
debug!("Failed to write to {}: {}", threshold_path.display(), e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ideapad fallback
|
||||
let mut ideapad_found = false;
|
||||
if let Ok(ideapad_entries) =
|
||||
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
|
||||
{
|
||||
for ideapad_entry in ideapad_entries.flatten() {
|
||||
let ideapad_path = ideapad_entry.path();
|
||||
let conservation_path = ideapad_path.join("conservation_mode");
|
||||
if conservation_path.exists() {
|
||||
ideapad_found = true;
|
||||
let val = if *percent < 100 { "1" } else { "0" };
|
||||
match fs::write(&conservation_path, val) {
|
||||
Ok(_) => {
|
||||
if *percent < 100 {
|
||||
println!(
|
||||
"WARNING: Your laptop (Lenovo Ideapad) does NOT support custom charge limits (like {}%).",
|
||||
percent
|
||||
);
|
||||
println!(
|
||||
"Instead, lapctl has enabled Lenovo's built-in 'Conservation Mode' via the Ideapad ACPI."
|
||||
);
|
||||
println!(
|
||||
"This mode is hard-coded into your laptop's firmware to stop charging at ~60%."
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Disabled Conservation Mode (charging to 100%)."
|
||||
);
|
||||
}
|
||||
success_count += 1;
|
||||
}
|
||||
Err(e) => debug!(
|
||||
"Failed to write to conservation_mode: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ideapad_found {
|
||||
debug!("limit not supported for {} (missing thresholds)", name_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +143,6 @@ pub fn execute(command: &BatteryCommands) {
|
||||
}
|
||||
}
|
||||
BatteryCommands::Status => {
|
||||
// Re-use logic from `lapctl status` to show battery-specific details:
|
||||
let sys_class_power = Path::new("/sys/class/power_supply");
|
||||
if let Ok(entries) = fs::read_dir(sys_class_power) {
|
||||
for entry in entries.flatten() {
|
||||
@@ -107,25 +159,39 @@ pub fn execute(command: &BatteryCommands) {
|
||||
println!(" Capacity: {}%", capacity.trim());
|
||||
println!(" Status: {}", status.trim());
|
||||
|
||||
if let Ok(limit) =
|
||||
fs::read_to_string(bat_path.join("charge_control_end_threshold"))
|
||||
// Check for Ideapad conservation mode first using robust detection
|
||||
let mut limit_shown = false;
|
||||
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
|
||||
{
|
||||
println!(" Charge Limit: {}%", limit.trim());
|
||||
} else {
|
||||
// Ideapad fallback display
|
||||
if let Ok(ideapad_entries) =
|
||||
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
|
||||
for entry in drivers.flatten() {
|
||||
let conservation_path = entry.path().join("conservation_mode");
|
||||
if conservation_path.exists()
|
||||
&& let Ok(mode) = fs::read_to_string(&conservation_path)
|
||||
{
|
||||
if mode.trim() == "1" {
|
||||
println!(" Charge Limit: Conservation Mode (~60%)");
|
||||
limit_shown = true;
|
||||
} else if mode.trim() == "0" {
|
||||
// On Ideapads 0 means 100% or off.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !limit_shown {
|
||||
if let Ok(limit) =
|
||||
fs::read_to_string(bat_path.join("charge_control_end_threshold"))
|
||||
{
|
||||
for ideapad_entry in ideapad_entries.flatten() {
|
||||
let conservation_path =
|
||||
ideapad_entry.path().join("conservation_mode");
|
||||
if conservation_path.exists()
|
||||
&& let Ok(mode) = fs::read_to_string(&conservation_path)
|
||||
{
|
||||
if mode.trim() == "1" {
|
||||
println!(" Charge Limit: Conservation Mode (~60%)");
|
||||
} else {
|
||||
println!(" Charge Limit: {}%", limit.trim());
|
||||
} else {
|
||||
// Re-check if it's an ideapad and we haven't shown a limit (likely 100%)
|
||||
if let Ok(drivers) =
|
||||
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
|
||||
{
|
||||
for entry in drivers.flatten() {
|
||||
if entry.path().join("conservation_mode").exists() {
|
||||
println!(" Charge Limit: 100%");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,65 @@ 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => conn,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let res = match command {
|
||||
CoolingCommands::Performance => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_cooling_profile("performance".to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
CoolingCommands::Balanced => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_cooling_profile("balanced".to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
CoolingCommands::Quiet => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_cooling_profile("quiet".to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
matches!(res, Ok(Ok(_)))
|
||||
})
|
||||
}
|
||||
|
||||
fn set_ideapad_fan_mode(mode: &str) -> bool {
|
||||
let mut success = false;
|
||||
@@ -38,6 +97,21 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
|
||||
}
|
||||
|
||||
pub fn execute(command: &CoolingCommands) {
|
||||
let desc = match command {
|
||||
CoolingCommands::Performance => "Performance",
|
||||
CoolingCommands::Balanced => "Balanced",
|
||||
CoolingCommands::Quiet => "Quiet",
|
||||
};
|
||||
println!("Setting thermal/cooling profile to {}", desc);
|
||||
|
||||
if try_call_daemon(command) {
|
||||
return;
|
||||
}
|
||||
|
||||
execute_local(command);
|
||||
}
|
||||
|
||||
pub fn execute_local(command: &CoolingCommands) {
|
||||
let (ideapad_mode, asus_mode, desc) = match command {
|
||||
CoolingCommands::Performance => ("1", "1", "Performance"),
|
||||
CoolingCommands::Balanced => ("0", "0", "Balanced"),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn execute() {
|
||||
println!("--- Fan Speed ---");
|
||||
let mut found = false;
|
||||
|
||||
// Read fan speeds from hwmon
|
||||
let hwmon_dir = Path::new("/sys/class/hwmon");
|
||||
if hwmon_dir.exists()
|
||||
&& let Ok(entries) = fs::read_dir(hwmon_dir)
|
||||
{
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name_path = path.join("name");
|
||||
let Ok(name) = fs::read_to_string(&name_path) else {
|
||||
continue;
|
||||
};
|
||||
let hwmon_name = name.trim().to_string();
|
||||
|
||||
let mut idx = 1;
|
||||
loop {
|
||||
let input_path = path.join(format!("fan{}_input", idx));
|
||||
if !input_path.exists() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(raw) = fs::read_to_string(&input_path) {
|
||||
let rpm: u64 = raw.trim().parse().unwrap_or(0);
|
||||
|
||||
let label = path.join(format!("fan{}_label", idx));
|
||||
let label_str = fs::read_to_string(&label)
|
||||
.map(|l| l.trim().to_string())
|
||||
.unwrap_or_else(|_| format!("Fan {}", idx));
|
||||
|
||||
let display_name = match hwmon_name.as_str() {
|
||||
"acpi_fan" => "CPU Fan",
|
||||
other => other,
|
||||
};
|
||||
|
||||
if rpm == 0 {
|
||||
println!(" {} ({}): Stopped", display_name, label_str);
|
||||
} else {
|
||||
println!(" {} ({}): {} RPM", display_name, label_str, rpm);
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show fan policy from thermal cooling devices
|
||||
let thermal_dir = Path::new("/sys/class/thermal");
|
||||
if thermal_dir.exists()
|
||||
&& let Ok(entries) = fs::read_dir(thermal_dir)
|
||||
{
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
if !name_str.starts_with("cooling_device") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type_path = entry.path().join("type");
|
||||
let state_path = entry.path().join("cur_state");
|
||||
let max_state_path = entry.path().join("max_state");
|
||||
|
||||
if let (Ok(type_content), Ok(state), Ok(max_state)) = (
|
||||
fs::read_to_string(&type_path),
|
||||
fs::read_to_string(&state_path),
|
||||
fs::read_to_string(&max_state_path),
|
||||
) {
|
||||
let zone_type = type_content.trim();
|
||||
let cur_state: u64 = state.trim().parse().unwrap_or(0);
|
||||
let max: u64 = max_state.trim().parse().unwrap_or(0);
|
||||
|
||||
if zone_type == "Fan" {
|
||||
let policy = if cur_state == 0 {
|
||||
"Off"
|
||||
} else if cur_state >= max {
|
||||
"Full Speed"
|
||||
} else {
|
||||
"Variable"
|
||||
};
|
||||
println!(
|
||||
" {} ({}): Level {}/{} ({})",
|
||||
name_str, zone_type, cur_state, max, policy
|
||||
);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
println!(" No fan sensors found.");
|
||||
}
|
||||
}
|
||||
+570
-70
@@ -1,13 +1,171 @@
|
||||
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_display_manager, rebuild_initramfs};
|
||||
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 fs2::FileExt;
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use zbus::Connection;
|
||||
use zbus::proxy;
|
||||
|
||||
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn setup_signal_handler() {
|
||||
if let Err(e) = ctrlc::set_handler(|| {
|
||||
INTERRUPTED.store(true, Ordering::SeqCst);
|
||||
}) {
|
||||
log::debug!("Could not set Ctrl+C handler (may already be set): {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
struct DmGuard {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
impl Drop for DmGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(ref name) = self.name {
|
||||
info!("Restarting display manager: {}", name);
|
||||
if let Err(e) = manage_service(name, "start") {
|
||||
error!("Failed to restart display manager '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LOCK_PATH: &str = "/var/lock/lapctl.lock";
|
||||
|
||||
struct LockGuard;
|
||||
|
||||
impl LockGuard {
|
||||
fn acquire() -> Result<Self, String> {
|
||||
let file = fs::File::create(LOCK_PATH)
|
||||
.map_err(|e| format!("Failed to create lock file: {}", e))?;
|
||||
file.try_lock_exclusive()
|
||||
.map_err(|e| format!("Another GPU switch operation is already in progress: {}", e))?;
|
||||
info!("Acquired exclusive lock");
|
||||
Ok(LockGuard)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = fs::remove_file(LOCK_PATH) {
|
||||
log::debug!("Failed to remove lock file: {}", e);
|
||||
}
|
||||
info!("Released lock");
|
||||
}
|
||||
}
|
||||
|
||||
#[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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => conn,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let res = match cmd {
|
||||
GpuCommands::Integrated { no_reboot } => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
proxy.switch_gpu_integrated(*no_reboot),
|
||||
)
|
||||
.await
|
||||
}
|
||||
GpuCommands::Hybrid {
|
||||
rtd3,
|
||||
use_nvidia_current,
|
||||
no_reboot,
|
||||
} => {
|
||||
let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1);
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
proxy.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot),
|
||||
)
|
||||
.await
|
||||
}
|
||||
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);
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
proxy.switch_gpu_nvidia(
|
||||
dm_val,
|
||||
*force_comp,
|
||||
coolbits_val,
|
||||
*use_nvidia_current,
|
||||
*wayland,
|
||||
*no_reboot,
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
matches!(res, Ok(Ok(_)))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_wayland_session() -> bool {
|
||||
std::env::var("WAYLAND_DISPLAY").is_ok()
|
||||
|| std::env::var("XDG_SESSION_TYPE")
|
||||
.map(|v| v == "wayland")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
|
||||
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
|
||||
@@ -189,6 +347,42 @@ EndSection
|
||||
)
|
||||
}
|
||||
|
||||
pub fn xorg_modesetting(bus_id: &str) -> String {
|
||||
format!(
|
||||
r#"# Automatically generated by lapctl
|
||||
|
||||
Section "ServerLayout"
|
||||
Identifier "layout"
|
||||
Screen 0 "nvidia"
|
||||
Inactive "modesetting"
|
||||
EndSection
|
||||
|
||||
Section "Device"
|
||||
Identifier "nvidia"
|
||||
Driver "nvidia"
|
||||
BusID "{}"
|
||||
EndSection
|
||||
|
||||
Section "Screen"
|
||||
Identifier "nvidia"
|
||||
Device "nvidia"
|
||||
Option "AllowEmptyInitialConfiguration"
|
||||
EndSection
|
||||
|
||||
Section "Device"
|
||||
Identifier "modesetting"
|
||||
Driver "modesetting"
|
||||
EndSection
|
||||
|
||||
Section "Screen"
|
||||
Identifier "modesetting"
|
||||
Device "modesetting"
|
||||
EndSection
|
||||
"#,
|
||||
bus_id
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_xrandr_script(igpu_vendor: Option<&String>) -> String {
|
||||
let source_name = match igpu_vendor.map(|s| s.as_str()) {
|
||||
Some("intel") => "modesetting".to_string(),
|
||||
@@ -258,21 +452,33 @@ 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);
|
||||
|
||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent()
|
||||
&& let Err(e) = fs::create_dir_all(parent)
|
||||
{
|
||||
log::debug!("Failed to create cache directory: {}", e);
|
||||
}
|
||||
|
||||
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);
|
||||
if let Ok(json) = serde_json::to_string_pretty(&cache) {
|
||||
let tmp_path = format!("{}.tmp", CACHE_FILE_PATH);
|
||||
if fs::write(&tmp_path, &json).is_ok() {
|
||||
if fs::rename(&tmp_path, CACHE_FILE_PATH).is_ok() {
|
||||
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
|
||||
} else {
|
||||
if let Err(e) = fs::remove_file(&tmp_path) {
|
||||
log::debug!("Failed to remove stale temp cache file: {}", e);
|
||||
}
|
||||
log::debug!("Failed to rename temp cache file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,10 +496,92 @@ fn read_cache_file() -> Result<Cache, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_integrated() {
|
||||
fn prepare_no_reboot(dm_flag: Option<String>) -> Result<DmGuard, String> {
|
||||
setup_signal_handler();
|
||||
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(DmGuard {
|
||||
name: 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(DmGuard { name: None })
|
||||
}
|
||||
|
||||
fn switch_integrated(no_reboot: bool) {
|
||||
assert_root();
|
||||
let _lock = match LockGuard::acquire() {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
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 _guard = if no_reboot {
|
||||
let guard = match prepare_no_reboot(None) {
|
||||
Ok(g) => g,
|
||||
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 {
|
||||
if let Err(e) = Command::new("modprobe").args(["-r", module]).status() {
|
||||
log::debug!("Failed to run modprobe -r {}: {}", module, e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
guard
|
||||
} else {
|
||||
DmGuard { name: None }
|
||||
};
|
||||
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
let mut dis_cmd = Command::new("systemctl");
|
||||
dis_cmd.args(["disable", "nvidia-persistenced.service"]);
|
||||
@@ -303,37 +591,65 @@ fn switch_integrated() {
|
||||
}
|
||||
match dis_cmd.status() {
|
||||
Ok(s) if s.success() => println!("Successfully disabled nvidia-persistenced.service"),
|
||||
_ => error!("An error ocurred while disabling service"),
|
||||
Ok(s) => error!(
|
||||
"nvidia-persistenced.service disable failed with exit code: {}",
|
||||
s
|
||||
),
|
||||
Err(e) => error!("Failed to run systemctl: {}", e),
|
||||
}
|
||||
|
||||
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
|
||||
create_cache_file();
|
||||
if no_reboot {
|
||||
println!("Operation completed successfully");
|
||||
} 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();
|
||||
let _lock = match LockGuard::acquire() {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
println!("Switching to hybrid mode");
|
||||
println!(
|
||||
"Enable PCI-Express Runtime D3 (RTD3) Power Management: {}",
|
||||
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 _guard = if no_reboot {
|
||||
let guard = match prepare_no_reboot(None) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = kill_gpu_processes() {
|
||||
error!("Failed to kill GPU processes: {}", e);
|
||||
}
|
||||
|
||||
guard
|
||||
} else {
|
||||
DmGuard { name: None }
|
||||
};
|
||||
|
||||
// persistenced enable
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
let mut enable_cmd = Command::new("systemctl");
|
||||
@@ -344,7 +660,11 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
|
||||
}
|
||||
match enable_cmd.status() {
|
||||
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
||||
_ => error!("An error ocurred while enabling service"),
|
||||
Ok(s) => error!(
|
||||
"nvidia-persistenced.service enable failed with exit code: {}",
|
||||
s
|
||||
),
|
||||
Err(e) => error!("Failed to run systemctl: {}", e),
|
||||
}
|
||||
|
||||
if let Some(val) = rtd3 {
|
||||
@@ -370,9 +690,13 @@ 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");
|
||||
} else {
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_nvidia(
|
||||
@@ -381,26 +705,28 @@ fn switch_nvidia(
|
||||
coolbits: Option<u32>,
|
||||
use_nvidia_current: bool,
|
||||
wayland: bool,
|
||||
no_reboot: bool,
|
||||
) {
|
||||
assert_root();
|
||||
let _lock = match LockGuard::acquire() {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let wayland = wayland || is_wayland_session();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -440,6 +766,8 @@ fn switch_nvidia(
|
||||
} else if vendor == "amd" {
|
||||
create_file(XORG_PATH, &xorg_amd(&pci_bus), false);
|
||||
}
|
||||
} else {
|
||||
create_file(XORG_PATH, &xorg_modesetting(&pci_bus), false);
|
||||
}
|
||||
|
||||
let mut extra_xorg = EXTRA_XORG_CONTENT.to_string();
|
||||
@@ -454,7 +782,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,49 +808,209 @@ fn switch_nvidia(
|
||||
}
|
||||
}
|
||||
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
let _guard = if no_reboot {
|
||||
let guard = match prepare_no_reboot(dm.clone()) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = kill_gpu_processes() {
|
||||
error!("Failed to kill GPU processes: {}", e);
|
||||
}
|
||||
|
||||
guard
|
||||
} else {
|
||||
DmGuard { name: None }
|
||||
};
|
||||
|
||||
// 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"),
|
||||
Ok(s) => error!(
|
||||
"nvidia-persistenced.service enable failed with exit code: {}",
|
||||
s
|
||||
),
|
||||
Err(e) => error!("Failed to run systemctl: {}", e),
|
||||
}
|
||||
|
||||
if no_reboot {
|
||||
println!("Operation completed successfully");
|
||||
} else {
|
||||
rebuild_initramfs();
|
||||
println!("Operation completed successfully");
|
||||
println!("Please reboot your computer for changes to take effect!");
|
||||
}
|
||||
}
|
||||
|
||||
fn run_on_dgpu(command: &[String]) {
|
||||
if command.is_empty() {
|
||||
error!("No command provided to run.");
|
||||
error!("No command specified.");
|
||||
return;
|
||||
}
|
||||
|
||||
if get_current_mode() != "hybrid" {
|
||||
log::warn!(
|
||||
"Running on dGPU is typically only effective in Hybrid mode. \
|
||||
Your current mode is '{}'. The application may fail to start or \
|
||||
not use the NVIDIA GPU.",
|
||||
get_current_mode()
|
||||
);
|
||||
error!("GPU run is only supported in Hybrid mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
log::info!("Launching '{}' on the discrete GPU...", command.join(" "));
|
||||
let env_vars = vec![
|
||||
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
|
||||
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
|
||||
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
|
||||
];
|
||||
|
||||
let mut child = Command::new(&command[0]);
|
||||
let mut cmd = Command::new(&command[0]);
|
||||
if command.len() > 1 {
|
||||
child.args(&command[1..]);
|
||||
cmd.args(&command[1..]);
|
||||
}
|
||||
|
||||
// Set PRIME offload variables natively
|
||||
child.env("__NV_PRIME_RENDER_OFFLOAD", "1");
|
||||
child.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
|
||||
child.env("__VK_LAYER_NV_optimus", "NVIDIA_only");
|
||||
for (key, value) in &env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
match child.status() {
|
||||
info!("Running '{}' on discrete GPU", command.join(" "));
|
||||
|
||||
match cmd.status() {
|
||||
Ok(status) => {
|
||||
log::info!("Application exited with: {}", status);
|
||||
if !status.success() {
|
||||
error!("Command exited with status: {}", status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to launch application: {}", e);
|
||||
error!("Failed to execute command: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_gpu_power_state() {
|
||||
println!("--- GPU Power State ---");
|
||||
|
||||
// Read PCI power state from sysfs
|
||||
let pci_base = Path::new("/sys/bus/pci/devices");
|
||||
if let Ok(entries) = fs::read_dir(pci_base) {
|
||||
for entry in entries.flatten() {
|
||||
let vendor_path = entry.path().join("vendor");
|
||||
let class_path = entry.path().join("class");
|
||||
|
||||
let Ok(vendor) = fs::read_to_string(&vendor_path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(class) = fs::read_to_string(&class_path) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// NVIDIA vendor = 0x10de, VGA class starts with 0x0300
|
||||
if vendor.trim() == "0x10de" && class.trim().starts_with("0x0300") {
|
||||
let pci_addr = entry.file_name().to_string_lossy().to_string();
|
||||
println!(" Device: NVIDIA ({})", pci_addr);
|
||||
|
||||
// Power state
|
||||
let runtime_status = entry.path().join("power/runtime_status");
|
||||
if let Ok(status) = fs::read_to_string(&runtime_status) {
|
||||
println!(" Runtime Status: {}", status.trim());
|
||||
}
|
||||
|
||||
// Power control
|
||||
let power_control = entry.path().join("power/control");
|
||||
if let Ok(control) = fs::read_to_string(&power_control) {
|
||||
println!(" Power Control: {}", control.trim());
|
||||
}
|
||||
|
||||
// Current power state (D0 = fully on, D3 = off)
|
||||
let current_state = entry.path().join("power_state");
|
||||
if let Ok(state) = fs::read_to_string(¤t_state) {
|
||||
println!(" PCI Power State: {}", state.trim());
|
||||
}
|
||||
|
||||
// Max power state
|
||||
let max_state = entry.path().join("max_power_state");
|
||||
if let Ok(state) = fs::read_to_string(&max_state) {
|
||||
println!(" Max Power State: {}", state.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NVIDIA power info via nvidia-smi
|
||||
match std::process::Command::new("nvidia-smi")
|
||||
.args([
|
||||
"--query-gpu=power.draw,power.limit,power.default_limit,power.max_limit,clocks.current.graphics,clocks.max.graphics",
|
||||
"--format=csv,noheader",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split(", ").collect();
|
||||
if parts.len() >= 5 {
|
||||
println!();
|
||||
println!(" Power Draw: {}", parts[0].trim());
|
||||
println!(" Power Limit: {}", parts[1].trim());
|
||||
println!(" Default Limit: {}", parts[2].trim());
|
||||
println!(" Max Limit: {}", parts[3].trim());
|
||||
println!(" Current Clock: {}", parts[4].trim());
|
||||
if parts.len() > 5 {
|
||||
println!(" Max Clock: {}", parts[5].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::debug!("nvidia-smi not available or GPU not powered on");
|
||||
println!(" (nvidia-smi not available - GPU may be powered off)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute(cmd: &GpuCommands) {
|
||||
let cmd = match cmd {
|
||||
GpuCommands::Nvidia {
|
||||
wayland: false,
|
||||
dm,
|
||||
force_comp,
|
||||
coolbits,
|
||||
use_nvidia_current,
|
||||
no_reboot,
|
||||
} if is_wayland_session() => {
|
||||
println!("Auto-detected Wayland session");
|
||||
GpuCommands::Nvidia {
|
||||
wayland: true,
|
||||
dm: dm.clone(),
|
||||
force_comp: *force_comp,
|
||||
coolbits: *coolbits,
|
||||
use_nvidia_current: *use_nvidia_current,
|
||||
no_reboot: *no_reboot,
|
||||
}
|
||||
}
|
||||
_ => cmd.clone(),
|
||||
};
|
||||
|
||||
match &cmd {
|
||||
GpuCommands::Integrated { .. } => println!("Switching to Integrated GPU mode..."),
|
||||
GpuCommands::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."),
|
||||
GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if try_call_daemon(&cmd) {
|
||||
return;
|
||||
}
|
||||
|
||||
execute_local(&cmd);
|
||||
}
|
||||
|
||||
pub fn execute_local(cmd: &GpuCommands) {
|
||||
match cmd {
|
||||
GpuCommands::Query => {
|
||||
let mode = get_current_mode();
|
||||
@@ -535,9 +1023,13 @@ pub fn execute(cmd: &GpuCommands) {
|
||||
GpuCommands::CacheDelete => {
|
||||
assert_root();
|
||||
if Path::new(CACHE_FILE_PATH).exists() {
|
||||
let _ = fs::remove_file(CACHE_FILE_PATH);
|
||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
|
||||
let _ = fs::remove_dir(parent);
|
||||
if let Err(e) = fs::remove_file(CACHE_FILE_PATH) {
|
||||
error!("Failed to remove cache file: {}", e);
|
||||
}
|
||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent()
|
||||
&& let Err(e) = fs::remove_dir(parent)
|
||||
{
|
||||
log::debug!("Failed to remove cache directory: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,9 +1049,13 @@ pub fn execute(cmd: &GpuCommands) {
|
||||
|
||||
cleanup();
|
||||
if Path::new(CACHE_FILE_PATH).exists() {
|
||||
let _ = fs::remove_file(CACHE_FILE_PATH);
|
||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
|
||||
let _ = fs::remove_dir(parent);
|
||||
if let Err(e) = fs::remove_file(CACHE_FILE_PATH) {
|
||||
error!("Failed to remove cache file: {}", e);
|
||||
}
|
||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent()
|
||||
&& let Err(e) = fs::remove_dir(parent)
|
||||
{
|
||||
log::debug!("Failed to remove cache directory: {}", e);
|
||||
}
|
||||
}
|
||||
rebuild_initramfs();
|
||||
@@ -570,24 +1066,28 @@ 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),
|
||||
GpuCommands::Power => show_gpu_power_state(),
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
use log::{error, info};
|
||||
use std::process::Command;
|
||||
|
||||
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() {
|
||||
if !daemon {
|
||||
info!("No command provided. Inhibiting sleep indefinitely. Press Ctrl+C to stop.");
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"Running command while inhibiting sleep: {}",
|
||||
command_args.join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
if daemon {
|
||||
match Command::new("systemd-inhibit").args(&args).spawn() {
|
||||
Ok(child) => {
|
||||
info!("Inhibitor started in background with PID: {}", child.id());
|
||||
info!("To stop it, use: pkill -P {} systemd-inhibit", child.id());
|
||||
// In background mode, we just let the child run and exit lapctl
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to spawn systemd-inhibit: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match Command::new("systemd-inhibit").args(&args).status() {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
error!("systemd-inhibit exited with status: {}", status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to execute systemd-inhibit: {}", e);
|
||||
eprintln!(
|
||||
"Error: make sure 'systemd-inhibit' is installed and available in your PATH."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_args(command_args: &[String], why: &str, who: &str) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--why".to_string(),
|
||||
why.to_string(),
|
||||
"--who".to_string(),
|
||||
who.to_string(),
|
||||
"--what".to_string(),
|
||||
"sleep:idle".to_string(),
|
||||
];
|
||||
|
||||
if command_args.is_empty() {
|
||||
args.push("sleep".to_string());
|
||||
args.push("infinity".to_string());
|
||||
} else {
|
||||
args.extend(command_args.iter().cloned());
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_args_no_command() {
|
||||
let args = build_args(&[], "testing", "lapctl");
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"--why",
|
||||
"testing",
|
||||
"--who",
|
||||
"lapctl",
|
||||
"--what",
|
||||
"sleep:idle",
|
||||
"sleep",
|
||||
"infinity"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_args_with_command() {
|
||||
let command_args = vec!["ls".to_string(), "-l".to_string()];
|
||||
let args = build_args(&command_args, "testing", "lapctl");
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"--why",
|
||||
"testing",
|
||||
"--who",
|
||||
"lapctl",
|
||||
"--what",
|
||||
"sleep:idle",
|
||||
"ls",
|
||||
"-l"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+131
-51
@@ -1,75 +1,155 @@
|
||||
use crate::utils::system::{assert_root, create_file};
|
||||
use log::info;
|
||||
use log::{error, 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.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">{EXE_PATH}</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={EXE_PATH} 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...");
|
||||
let current_exe = match std::env::current_exe() {
|
||||
Ok(exe) => exe,
|
||||
Err(e) => {
|
||||
error!("Failed to get current executable path: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let exe_path = match current_exe.to_str() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
error!("Executable path contains invalid UTF-8");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
println!("Found lapctl at: {}", exe_path);
|
||||
println!("Installing lapctl configuration with dynamic paths...");
|
||||
|
||||
let dbus_policy_path = "/etc/dbus-1/system.d/org.lapctl.conf";
|
||||
create_file(dbus_policy_path, DBUS_POLICY, false);
|
||||
|
||||
let systemd_content = SYSTEMD_SERVICE.replace("{EXE_PATH}", exe_path);
|
||||
let service_path = "/etc/systemd/system/lapctld.service";
|
||||
create_file(service_path, &systemd_content, 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_content = POLKIT_POLICY.replace("{EXE_PATH}", exe_path);
|
||||
let polkit_policy_path = "/usr/share/polkit-1/actions/org.lapctl.policy";
|
||||
create_file(polkit_policy_path, &polkit_content, false);
|
||||
|
||||
let _ = Command::new("udevadm")
|
||||
.args(["control", "--reload-rules"])
|
||||
.status();
|
||||
let polkit_rules_path = "/etc/polkit-1/rules.d/10-lapctl.rules";
|
||||
create_file(polkit_rules_path, POLKIT_RULES, false);
|
||||
|
||||
let _ = Command::new("udevadm").args(["trigger"]).status();
|
||||
println!("Successfully installed configuration files.");
|
||||
println!("Activating background daemon (lapctld)...");
|
||||
|
||||
// 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();
|
||||
// Reload systemd to recognize the new service
|
||||
if let Err(e) = Command::new("systemctl").arg("daemon-reload").status() {
|
||||
error!("Failed to run systemctl daemon-reload: {}", e);
|
||||
println!("You may need to run 'systemctl daemon-reload' manually.");
|
||||
}
|
||||
|
||||
info!("Udev reload and immediate ACL application complete.");
|
||||
println!(
|
||||
"Installation successful! You can now use lapctl power, cooling, and battery commands without sudo."
|
||||
);
|
||||
// Enable and start the daemon
|
||||
let status = Command::new("systemctl")
|
||||
.args(["enable", "--now", "lapctld"])
|
||||
.status();
|
||||
|
||||
let daemon_ok = match status {
|
||||
Ok(s) if s.success() => {
|
||||
info!("lapctld daemon enabled and started.");
|
||||
true
|
||||
}
|
||||
Ok(s) => {
|
||||
log::error!(
|
||||
"systemctl enable --now lapctld failed with exit code: {}",
|
||||
s
|
||||
);
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to run systemctl: {}", e);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if daemon_ok {
|
||||
println!("Installation successful! The lapctld daemon is now running.");
|
||||
} else {
|
||||
println!("Installation completed (but daemon may not be running).");
|
||||
println!("Try starting it manually with: systemctl start lapctld");
|
||||
}
|
||||
println!("You can now use all lapctl commands from any location.");
|
||||
}
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
pub mod battery;
|
||||
pub mod cooling;
|
||||
pub mod display;
|
||||
pub mod fan;
|
||||
pub mod gpu;
|
||||
pub mod inhibit;
|
||||
pub mod install_rules;
|
||||
pub mod power;
|
||||
pub mod status;
|
||||
pub mod temperature;
|
||||
pub mod touchpad;
|
||||
+109
-5
@@ -2,6 +2,73 @@ 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => conn,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let res = match command {
|
||||
PowerCommands::Performance => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_power_profile("performance".to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
PowerCommands::Balanced => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_power_profile("balanced".to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
PowerCommands::BatterySave => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_power_profile("battery-save".to_string()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
PowerCommands::LimitTdp { watts } => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_tdp_limit(*watts),
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
matches!(res, Ok(Ok(_)))
|
||||
})
|
||||
}
|
||||
|
||||
fn set_cpu_governor(governor: &str) {
|
||||
let cpu_dir = Path::new("/sys/devices/system/cpu");
|
||||
@@ -142,6 +209,10 @@ fn set_amd_hwmon_limit(watts: u32) -> bool {
|
||||
}
|
||||
success
|
||||
}
|
||||
fn is_ideapad() -> bool {
|
||||
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
|
||||
}
|
||||
|
||||
fn set_platform_profile(profile: &str) {
|
||||
let platform_profile_path = Path::new("/sys/firmware/acpi/platform_profile");
|
||||
if platform_profile_path.exists() {
|
||||
@@ -155,26 +226,59 @@ fn set_platform_profile(profile: &str) {
|
||||
}
|
||||
|
||||
pub fn execute(command: &PowerCommands) {
|
||||
match command {
|
||||
PowerCommands::Performance => println!("Setting power profile to Performance"),
|
||||
PowerCommands::Balanced => println!("Setting power profile to Balanced"),
|
||||
PowerCommands::BatterySave => println!("Setting power profile to Battery Saver"),
|
||||
PowerCommands::LimitTdp { watts } => {
|
||||
println!("Setting CPU Package TDP Limit to {} Watts...", watts)
|
||||
}
|
||||
}
|
||||
|
||||
if try_call_daemon(command) {
|
||||
return;
|
||||
}
|
||||
|
||||
execute_local(command);
|
||||
}
|
||||
|
||||
pub fn execute_local(command: &PowerCommands) {
|
||||
match command {
|
||||
PowerCommands::Performance => {
|
||||
println!("Setting power profile to Performance");
|
||||
set_platform_profile("performance");
|
||||
if !is_ideapad() {
|
||||
set_platform_profile("performance");
|
||||
} else {
|
||||
println!(
|
||||
"Skipping platform_profile (decoupled from fan control on Ideapad). Use 'lapctl cooling' for fan speed."
|
||||
);
|
||||
}
|
||||
set_cpu_governor("performance");
|
||||
set_energy_performance_preference("performance");
|
||||
println!("Operation completed successfully.");
|
||||
}
|
||||
PowerCommands::Balanced => {
|
||||
println!("Setting power profile to Balanced");
|
||||
set_platform_profile("balanced");
|
||||
// Schedutil is the modern balanced default. Fallback isn't critical since cpufreq
|
||||
// usually rejects invalid profiles, and standard Intel/AMD systems use EPP now anyway.
|
||||
if !is_ideapad() {
|
||||
set_platform_profile("balanced");
|
||||
} else {
|
||||
println!(
|
||||
"Skipping platform_profile (decoupled from fan control on Ideapad). Use 'lapctl cooling' for fan speed."
|
||||
);
|
||||
}
|
||||
set_cpu_governor("schedutil");
|
||||
set_energy_performance_preference("balance_performance");
|
||||
println!("Operation completed successfully.");
|
||||
}
|
||||
PowerCommands::BatterySave => {
|
||||
println!("Setting power profile to Battery Saver");
|
||||
set_platform_profile("low-power");
|
||||
if !is_ideapad() {
|
||||
set_platform_profile("low-power");
|
||||
} else {
|
||||
println!(
|
||||
"Skipping platform_profile (decoupled from fan control on Ideapad). Use 'lapctl cooling' for fan speed."
|
||||
);
|
||||
}
|
||||
set_cpu_governor("powersave");
|
||||
set_energy_performance_preference("power");
|
||||
println!("Operation completed successfully.");
|
||||
|
||||
@@ -181,6 +181,38 @@ pub fn execute() {
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if let Ok(temp_output) = fs::read_to_string("/sys/class/hwmon/hwmon5/temp1_input")
|
||||
&& let Ok(millideg) = temp_output.trim().parse::<i64>()
|
||||
{
|
||||
println!("CPU Temp: {:.1}°C", millideg as f64 / 1000.0);
|
||||
}
|
||||
|
||||
// Fan Speed
|
||||
if let Ok(fan_output) = fs::read_to_string("/sys/class/hwmon/hwmon1/fan1_input")
|
||||
&& let Ok(rpm) = fan_output.trim().parse::<u64>()
|
||||
{
|
||||
println!("Fan Speed: {} RPM", rpm);
|
||||
}
|
||||
|
||||
// GPU Power State
|
||||
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||
.args([
|
||||
"--query-gpu=power.draw,temperature.gpu",
|
||||
"--format=csv,noheader",
|
||||
])
|
||||
.output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(line) = stdout.lines().next() {
|
||||
let parts: Vec<&str> = line.split(", ").collect();
|
||||
if parts.len() >= 2 {
|
||||
println!("GPU: {} @ {}°C (NVIDIA)", parts[0].trim(), parts[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display Status
|
||||
let displays = display::get_active_display_info();
|
||||
if displays.is_empty() {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use log::debug;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn execute() {
|
||||
println!("--- Temperature ---");
|
||||
let mut found = false;
|
||||
|
||||
// Read all hwmon temperature sensors
|
||||
let hwmon_dir = Path::new("/sys/class/hwmon");
|
||||
if hwmon_dir.exists()
|
||||
&& let Ok(entries) = fs::read_dir(hwmon_dir)
|
||||
{
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name_path = path.join("name");
|
||||
let Ok(name) = fs::read_to_string(&name_path) else {
|
||||
continue;
|
||||
};
|
||||
let hwmon_name = name.trim().to_string();
|
||||
|
||||
let mut idx = 1;
|
||||
loop {
|
||||
let input_path = path.join(format!("temp{}_input", idx));
|
||||
if !input_path.exists() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(raw) = fs::read_to_string(&input_path) {
|
||||
let millideg: i64 = raw.trim().parse().unwrap_or(0);
|
||||
let celsius = millideg as f64 / 1000.0;
|
||||
|
||||
let label = path.join(format!("temp{}_label", idx));
|
||||
let label_str = fs::read_to_string(&label)
|
||||
.map(|l| l.trim().to_string())
|
||||
.unwrap_or_else(|_| format!("Sensor {}", idx));
|
||||
|
||||
let display_name = match hwmon_name.as_str() {
|
||||
"k10temp" => "CPU",
|
||||
"acpitz" => "ACPI Thermal Zone",
|
||||
"nvme" => "NVMe SSD",
|
||||
"mt7921_phy0" => "WiFi",
|
||||
other => other,
|
||||
};
|
||||
|
||||
println!(" {} ({}): {:.1}°C", display_name, label_str, celsius);
|
||||
found = true;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NVIDIA GPU temperature via nvidia-smi
|
||||
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||
.args([
|
||||
"--query-gpu=temperature.gpu,power.draw,clocks.current.graphics",
|
||||
"--format=csv,noheader",
|
||||
])
|
||||
.output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split(", ").collect();
|
||||
if let Some(temp) = parts.first()
|
||||
&& let Ok(temp_c) = temp.trim().parse::<f64>()
|
||||
{
|
||||
let info = if parts.len() >= 3 {
|
||||
format!(" [{} @ {}]", parts[1].trim(), parts[2].trim())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
println!(" GPU (NVIDIA): {:.0}°C{}", temp_c, info);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("nvidia-smi not available, skipping GPU temperature");
|
||||
}
|
||||
|
||||
// Read thermal zone temperatures
|
||||
let thermal_dir = Path::new("/sys/class/thermal");
|
||||
if thermal_dir.exists()
|
||||
&& let Ok(entries) = fs::read_dir(thermal_dir)
|
||||
{
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
if !name_str.starts_with("thermal_zone") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type_path = entry.path().join("type");
|
||||
let temp_path = entry.path().join("temp");
|
||||
|
||||
if let (Ok(type_content), Ok(temp_content)) = (
|
||||
fs::read_to_string(&type_path),
|
||||
fs::read_to_string(&temp_path),
|
||||
) {
|
||||
let zone_type = type_content.trim();
|
||||
let millideg: i64 = temp_content.trim().parse().unwrap_or(0);
|
||||
let celsius = millideg as f64 / 1000.0;
|
||||
|
||||
// Skip if we already showed this from hwmon
|
||||
if zone_type == "x86_pkg_temp" || zone_type == "k10temp" || zone_type == "acpitz" {
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(" {} ({}): {:.1}°C", name_str, zone_type, celsius);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
println!(" No temperature sensors found.");
|
||||
}
|
||||
}
|
||||
+102
-37
@@ -1,54 +1,119 @@
|
||||
use crate::cli::TouchpadCommands;
|
||||
use log::{error, info};
|
||||
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_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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(conn)) => conn,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
let proxy = match LapctlProxy::new(&connection).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
let res = match command {
|
||||
TouchpadCommands::Enable => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_touchpad_inhibition(false),
|
||||
)
|
||||
.await
|
||||
}
|
||||
TouchpadCommands::Disable => {
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
proxy.set_touchpad_inhibition(true),
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
matches!(res, Ok(Ok(_)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn execute(command: &TouchpadCommands) {
|
||||
let sys_class_input = Path::new("/sys/class/input");
|
||||
if !sys_class_input.exists() {
|
||||
match command {
|
||||
TouchpadCommands::Enable => println!("Enabling touchpad..."),
|
||||
TouchpadCommands::Disable => println!("Disabling touchpad..."),
|
||||
}
|
||||
|
||||
if try_call_daemon(command) {
|
||||
return;
|
||||
}
|
||||
|
||||
execute_local(command);
|
||||
}
|
||||
|
||||
pub fn execute_local(command: &TouchpadCommands) {
|
||||
match command {
|
||||
TouchpadCommands::Enable => set_touchpad_inhibition(false),
|
||||
TouchpadCommands::Disable => set_touchpad_inhibition(true),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_touchpad_inhibition(inhibited: bool) {
|
||||
let dev_input = Path::new("/sys/class/input");
|
||||
if !dev_input.exists() {
|
||||
error!("Could not find /sys/class/input. Is this a Linux system?");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut found_touchpads = Vec::new();
|
||||
|
||||
if let Ok(entries) = fs::read_dir(sys_class_input) {
|
||||
if let Ok(entries) = fs::read_dir(dev_input) {
|
||||
let mut found = false;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name_path = path.join("name");
|
||||
let inhibited_path = path.join("inhibited");
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
|
||||
if name_path.exists()
|
||||
&& inhibited_path.exists()
|
||||
&& let Ok(name) = fs::read_to_string(&name_path)
|
||||
&& name.to_lowercase().contains("touchpad")
|
||||
{
|
||||
found_touchpads.push((name.trim().to_string(), inhibited_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found_touchpads.is_empty() {
|
||||
error!("No touchpad devices found with 'inhibited' support in /sys/class/input.");
|
||||
return;
|
||||
}
|
||||
|
||||
match command {
|
||||
TouchpadCommands::Enable => {
|
||||
for (name, path) in found_touchpads {
|
||||
match fs::write(&path, "0") {
|
||||
Ok(_) => info!("Enabled touchpad: {}", name),
|
||||
Err(e) => error!("Failed to enable touchpad {}: {}", name, e),
|
||||
if name_str.starts_with("input") {
|
||||
let name_file = entry.path().join("name");
|
||||
if let Ok(content) = fs::read_to_string(name_file) {
|
||||
let content_lower = content.to_lowercase();
|
||||
if content_lower.contains("touchpad") || content_lower.contains("trackpad") {
|
||||
found = true;
|
||||
let inhibited_file = entry.path().join("inhibited");
|
||||
if inhibited_file.exists() {
|
||||
let val = if inhibited { "1" } else { "0" };
|
||||
match fs::write(&inhibited_file, val) {
|
||||
Ok(_) => info!("Set inhibit={} for {}", inhibited, name_str),
|
||||
Err(e) => {
|
||||
debug!("Failed to write to {}: {}", inhibited_file.display(), e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TouchpadCommands::Disable => {
|
||||
for (name, path) in found_touchpads {
|
||||
match fs::write(&path, "1") {
|
||||
Ok(_) => info!("Disabled touchpad: {}", name),
|
||||
Err(e) => error!("Failed to disable touchpad {}: {}", name, e),
|
||||
}
|
||||
}
|
||||
if found {
|
||||
println!("Operation completed successfully.");
|
||||
} else {
|
||||
error!("No touchpad device found in /sys/class/input.");
|
||||
}
|
||||
} else {
|
||||
error!("Failed to read directory /sys/class/input.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
use crate::cli::GpuCommands;
|
||||
use crate::commands::gpu;
|
||||
use zbus::interface;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LapctlInterface {}
|
||||
|
||||
#[interface(name = "org.lapctl1")]
|
||||
impl LapctlInterface {
|
||||
pub async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::fdo::Result<()> {
|
||||
gpu::execute_local(&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_local(&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_local(&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<()> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(format!(
|
||||
"Invalid battery limit: {}. Must be between 1 and 100.",
|
||||
percent
|
||||
)));
|
||||
}
|
||||
crate::commands::battery::execute_local(&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_local(&cmd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_tdp_limit(&self, watts: u32) -> zbus::fdo::Result<()> {
|
||||
crate::commands::power::execute_local(&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_local(&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_local(&cmd);
|
||||
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;
|
||||
}
|
||||
}
|
||||
+90
-10
@@ -21,30 +21,42 @@ pub fn get_current_mode() -> String {
|
||||
}
|
||||
|
||||
pub fn get_nvidia_gpu_pci_bus() -> String {
|
||||
let output = Command::new("lspci")
|
||||
.output()
|
||||
.expect("Failed to execute lspci");
|
||||
let output = match Command::new("lspci").output() {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
error!("Failed to execute lspci: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
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_bus_id_raw = line.split_whitespace().next().unwrap();
|
||||
let pci_bus_id_raw = match line.split_whitespace().next() {
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
let pci_bus_id = pci_bus_id_raw.replace("0000:", "");
|
||||
info!("Found Nvidia GPU at {}", pci_bus_id);
|
||||
|
||||
let parts: Vec<&str> = pci_bus_id.split(':').collect();
|
||||
let bus = parts[0];
|
||||
if parts.len() < 2 {
|
||||
warn!("Unexpected PCI bus ID format: {}", pci_bus_id);
|
||||
continue;
|
||||
}
|
||||
let device_fun: Vec<&str> = parts[1].split('.').collect();
|
||||
let device = device_fun[0];
|
||||
let function = device_fun[1];
|
||||
if device_fun.len() < 2 {
|
||||
warn!("Unexpected PCI device.function format: {}", parts[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
return format!(
|
||||
"PCI:{}:{}:{}",
|
||||
u32::from_str_radix(bus, 16).unwrap_or(0),
|
||||
u32::from_str_radix(device, 16).unwrap_or(0),
|
||||
u32::from_str_radix(function, 16).unwrap_or(0)
|
||||
u32::from_str_radix(parts[0], 16).unwrap_or(0),
|
||||
u32::from_str_radix(device_fun[0], 16).unwrap_or(0),
|
||||
u32::from_str_radix(device_fun[1], 16).unwrap_or(0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +66,74 @@ 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().ok()?;
|
||||
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()?;
|
||||
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
|
||||
if let Err(e) = Command::new("fuser").args(["-k", "/dev/nvidia*"]).output() {
|
||||
log::warn!("fuser failed (may not be installed): {}", e);
|
||||
}
|
||||
|
||||
// Also kill specific nvidia services if they are running
|
||||
if let Err(e) = Command::new("systemctl")
|
||||
.args(["stop", "nvidia-persistenced.service"])
|
||||
.output()
|
||||
{
|
||||
log::debug!("Failed to stop nvidia-persistenced: {}", e);
|
||||
}
|
||||
|
||||
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;
|
||||
+17
-6
@@ -28,13 +28,24 @@ fn main() {
|
||||
Commands::Cooling { command } => commands::cooling::execute(command),
|
||||
Commands::Display { command } => commands::display::execute(command),
|
||||
Commands::Status => commands::status::execute(),
|
||||
Commands::Temperature => commands::temperature::execute(),
|
||||
Commands::Fan => commands::fan::execute(),
|
||||
Commands::InstallRules => commands::install_rules::execute(),
|
||||
Commands::Touchpad { command } => commands::touchpad::execute(command),
|
||||
Commands::Inhibit {
|
||||
command,
|
||||
why,
|
||||
who,
|
||||
daemon,
|
||||
} => commands::inhibit::execute(command, why, who, *daemon),
|
||||
Commands::Daemon => {
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create tokio runtime: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
rt.block_on(async {
|
||||
if let Err(e) = lapctl::daemon::run().await {
|
||||
eprintln!("Daemon error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+131
-28
@@ -9,8 +9,29 @@ 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),
|
||||
Ok(s) => {
|
||||
error!("Privilege escalation failed with exit code: {}", s);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Privilege escalation failed: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"This operation requires root privileges and could not determine executable path."
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +53,11 @@ pub fn create_file(path: &str, content: &str, executable: bool) {
|
||||
|
||||
if executable && let Ok(mut perms) = fs::metadata(path_obj).map(|m| m.permissions()) {
|
||||
perms.set_mode(perms.mode() | 0o111);
|
||||
let _ = fs::set_permissions(path_obj, perms);
|
||||
info!("Added execution privilege to file {}", path);
|
||||
if let Err(e) = fs::set_permissions(path_obj, perms) {
|
||||
error!("Failed to set executable permission on '{}': {}", path, e);
|
||||
} else {
|
||||
info!("Added execution privilege to file {}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -43,19 +67,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();
|
||||
|
||||
@@ -76,20 +155,6 @@ pub fn rebuild_initramfs() {
|
||||
command = vec!["mkinitcpio", "-P"];
|
||||
}
|
||||
|
||||
if let Ok(which) = Command::new("which").arg("systemd-inhibit").output()
|
||||
&& which.status.success()
|
||||
{
|
||||
let mut new_cmd = vec![
|
||||
"systemd-inhibit",
|
||||
"--who=lapctl",
|
||||
"--why",
|
||||
"Rebuilding initramfs",
|
||||
"--",
|
||||
];
|
||||
new_cmd.extend(command.iter());
|
||||
command = new_cmd;
|
||||
}
|
||||
|
||||
if !command.is_empty() {
|
||||
println!("Rebuilding the initramfs...");
|
||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||
@@ -106,9 +171,47 @@ pub fn rebuild_initramfs() {
|
||||
Ok(status) if status.success() => {
|
||||
println!("Successfully rebuilt the initramfs!");
|
||||
}
|
||||
_ => {
|
||||
error!("An error occurred while rebuilding the initramfs");
|
||||
Ok(status) => {
|
||||
error!("initramfs rebuild failed with exit code: {}", status);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to run initramfs rebuild command: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Unknown distribution: initramfs was not rebuilt. You may need to rebuild it manually before rebooting."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(()),
|
||||
Ok(s) => Err(format!(
|
||||
"systemctl {} {} failed with exit code: {}",
|
||||
action, name, s
|
||||
)),
|
||||
Err(e) => Err(format!(
|
||||
"Failed to run systemctl {} {}: {}",
|
||||
action, name, e
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
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(()),
|
||||
Ok(s) => Err(format!(
|
||||
"loginctl terminate-session {} failed with exit code: {}",
|
||||
id, s
|
||||
)),
|
||||
Err(e) => Err(format!("Failed to run loginctl: {}", e)),
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use clap::Parser;
|
||||
use lapctl::cli::{Cli, Commands};
|
||||
|
||||
#[test]
|
||||
fn test_inhibit_cli_parsing() {
|
||||
let args = vec!["lapctl", "inhibit", "sleep", "10", "--why", "testing"];
|
||||
let cli = Cli::parse_from(args);
|
||||
|
||||
if let Commands::Inhibit {
|
||||
command,
|
||||
why,
|
||||
who,
|
||||
daemon,
|
||||
} = cli.command
|
||||
{
|
||||
assert_eq!(command, vec!["sleep".to_string(), "10".to_string()]);
|
||||
assert_eq!(why, "testing");
|
||||
assert_eq!(who, "lapctl"); // default value
|
||||
assert!(!daemon);
|
||||
} else {
|
||||
panic!("Command should be Inhibit");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inhibit_daemon_parsing() {
|
||||
let args = vec!["lapctl", "inhibit", "--daemon"];
|
||||
let cli = Cli::parse_from(args);
|
||||
|
||||
if let Commands::Inhibit {
|
||||
command,
|
||||
why,
|
||||
who,
|
||||
daemon,
|
||||
} = cli.command
|
||||
{
|
||||
assert!(command.is_empty());
|
||||
assert_eq!(why, "lapctl inhibiting sleep"); // default value
|
||||
assert!(daemon);
|
||||
} else {
|
||||
panic!("Command should be Inhibit");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user