mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 19:50:28 +00:00
954 lines
29 KiB
Rust
954 lines
29 KiB
Rust
use notify_rust::Notification;
|
|
|
|
pub fn send_notify(
|
|
summary: Option<String>,
|
|
body: Option<String>,
|
|
app_name: Option<String>,
|
|
app_id: Option<String>,
|
|
) -> anyhow::Result<()> {
|
|
let mut n = Notification::new();
|
|
if let Some(summary) = summary {
|
|
n.summary(&summary);
|
|
}
|
|
if let Some(body) = body {
|
|
n.body(&body);
|
|
}
|
|
if let Some(app_name) = app_name {
|
|
n.appname(&app_name);
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
if let Some(app_id) = app_id {
|
|
n.app_id(&app_id);
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
println!("send_notify (unix) appid: {:?}", app_id);
|
|
n.show()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get system memory size in GB
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_system_memory_size_gb() -> anyhow::Result<u64> {
|
|
use std::mem;
|
|
use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX};
|
|
|
|
unsafe {
|
|
let mut mem_status: MEMORYSTATUSEX = mem::zeroed();
|
|
mem_status.dwLength = mem::size_of::<MEMORYSTATUSEX>() as u32;
|
|
|
|
GlobalMemoryStatusEx(&mut mem_status)?;
|
|
|
|
// Convert bytes to GB
|
|
Ok(mem_status.ullTotalPhys / (1024 * 1024 * 1024))
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_system_memory_size_gb() -> anyhow::Result<u64> {
|
|
Ok(0)
|
|
}
|
|
|
|
/// Get number of logical processors
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_number_of_logical_processors() -> anyhow::Result<u32> {
|
|
use std::mem;
|
|
use windows::Win32::System::SystemInformation::{GetSystemInfo, SYSTEM_INFO};
|
|
|
|
unsafe {
|
|
let mut sys_info: SYSTEM_INFO = mem::zeroed();
|
|
GetSystemInfo(&mut sys_info);
|
|
Ok(sys_info.dwNumberOfProcessors)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_number_of_logical_processors() -> anyhow::Result<u32> {
|
|
Ok(0)
|
|
}
|
|
|
|
/// System information struct
|
|
#[derive(Debug, Clone)]
|
|
pub struct SystemInfo {
|
|
pub os_name: String,
|
|
pub cpu_name: String,
|
|
pub gpu_info: String,
|
|
pub disk_info: String,
|
|
}
|
|
|
|
/// Get all system information at once
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_system_info() -> anyhow::Result<SystemInfo> {
|
|
use serde::Deserialize;
|
|
use wmi::WMIConnection;
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(rename = "Caption")]
|
|
struct OsInfo {
|
|
#[serde(rename = "Caption")]
|
|
caption: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct CpuInfo {
|
|
#[serde(rename = "Name")]
|
|
name: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct GpuInfo {
|
|
#[serde(rename = "Name")]
|
|
name: Option<String>,
|
|
#[serde(rename = "AdapterRAM")]
|
|
adapter_ram: Option<u64>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct DiskInfo {
|
|
#[serde(rename = "MediaType")]
|
|
media_type: Option<String>,
|
|
#[serde(rename = "Model")]
|
|
model: Option<String>,
|
|
#[serde(rename = "Size")]
|
|
size: Option<u64>,
|
|
}
|
|
|
|
let wmi_con = WMIConnection::new()?;
|
|
|
|
// Get OS name using raw query
|
|
let os_name = match wmi_con.raw_query::<OsInfo>("SELECT Caption FROM Win32_OperatingSystem") {
|
|
Ok(results) => results
|
|
.first()
|
|
.and_then(|os| os.caption.clone())
|
|
.unwrap_or_default(),
|
|
Err(_) => String::new(),
|
|
};
|
|
|
|
// Get CPU name using raw query
|
|
let cpu_name = match wmi_con.raw_query::<CpuInfo>("SELECT Name FROM Win32_Processor") {
|
|
Ok(results) => results
|
|
.first()
|
|
.and_then(|cpu| cpu.name.clone())
|
|
.unwrap_or_default(),
|
|
Err(_) => String::new(),
|
|
};
|
|
|
|
// Get GPU info using raw query
|
|
let gpu_info =
|
|
match wmi_con.raw_query::<GpuInfo>("SELECT Name, AdapterRAM FROM Win32_VideoController") {
|
|
Ok(results) => results
|
|
.iter()
|
|
.filter_map(|gpu| {
|
|
gpu.name.as_ref().map(|name| {
|
|
let vram_gb = gpu.adapter_ram.unwrap_or(0) / (1024 * 1024 * 1024);
|
|
format!("{} ({} GB)", name, vram_gb)
|
|
})
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
Err(_) => String::new(),
|
|
};
|
|
|
|
// Get Disk info using raw query
|
|
let disk_info =
|
|
match wmi_con.raw_query::<DiskInfo>("SELECT MediaType, Model, Size FROM Win32_DiskDrive") {
|
|
Ok(results) => results
|
|
.iter()
|
|
.filter(|disk| disk.model.is_some())
|
|
.map(|disk| {
|
|
let size_gb = disk.size.unwrap_or(0) / (1024 * 1024 * 1024);
|
|
format!(
|
|
"{}\t{}\t{} GB",
|
|
disk.media_type.as_deref().unwrap_or(""),
|
|
disk.model.as_deref().unwrap_or(""),
|
|
size_gb
|
|
)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
Err(_) => String::new(),
|
|
};
|
|
|
|
Ok(SystemInfo {
|
|
os_name,
|
|
cpu_name,
|
|
gpu_info,
|
|
disk_info,
|
|
})
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_system_info() -> anyhow::Result<SystemInfo> {
|
|
Ok(SystemInfo {
|
|
os_name: String::new(),
|
|
cpu_name: String::new(),
|
|
gpu_info: String::new(),
|
|
disk_info: String::new(),
|
|
})
|
|
}
|
|
|
|
/// Get GPU info from registry (more accurate VRAM)
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_gpu_info_from_registry() -> anyhow::Result<String> {
|
|
use std::mem;
|
|
use windows::core::{HSTRING, PCWSTR};
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, RegQueryValueExW, HKEY_LOCAL_MACHINE, KEY_READ,
|
|
REG_VALUE_TYPE,
|
|
};
|
|
|
|
let mut result = Vec::new();
|
|
let base_path =
|
|
HSTRING::from(r"SYSTEM\ControlSet001\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}");
|
|
|
|
unsafe {
|
|
let mut hkey = std::mem::zeroed();
|
|
if RegOpenKeyExW(
|
|
HKEY_LOCAL_MACHINE,
|
|
PCWSTR(base_path.as_ptr()),
|
|
Some(0),
|
|
KEY_READ,
|
|
&mut hkey,
|
|
)
|
|
.is_err()
|
|
{
|
|
return Ok(String::new());
|
|
}
|
|
|
|
let mut index = 0u32;
|
|
let mut key_name = [0u16; 256];
|
|
|
|
loop {
|
|
let mut key_name_len = key_name.len() as u32;
|
|
if RegEnumKeyExW(
|
|
hkey,
|
|
index,
|
|
Some(windows::core::PWSTR::from_raw(key_name.as_mut_ptr())),
|
|
&mut key_name_len,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
.is_err()
|
|
{
|
|
break;
|
|
}
|
|
|
|
let subkey_name = String::from_utf16_lossy(&key_name[..key_name_len as usize]);
|
|
|
|
// Only process numbered subkeys (0000, 0001, etc.)
|
|
if subkey_name.chars().all(|c| c.is_ascii_digit()) {
|
|
let full_path = HSTRING::from(format!(
|
|
r"SYSTEM\ControlSet001\Control\Class\{{4d36e968-e325-11ce-bfc1-08002be10318}}\{}",
|
|
subkey_name
|
|
));
|
|
|
|
let mut subkey = mem::zeroed();
|
|
if RegOpenKeyExW(
|
|
HKEY_LOCAL_MACHINE,
|
|
PCWSTR(full_path.as_ptr()),
|
|
Some(0),
|
|
KEY_READ,
|
|
&mut subkey,
|
|
)
|
|
.is_ok()
|
|
{
|
|
// Read adapter string
|
|
let adapter_name = HSTRING::from("HardwareInformation.AdapterString");
|
|
let mut adapter_buffer = [0u16; 512];
|
|
let mut adapter_size = (adapter_buffer.len() * 2) as u32;
|
|
let mut adapter_type = REG_VALUE_TYPE::default();
|
|
|
|
let adapter_string = if RegQueryValueExW(
|
|
subkey,
|
|
PCWSTR(adapter_name.as_ptr()),
|
|
None,
|
|
Some(&mut adapter_type),
|
|
Some(adapter_buffer.as_mut_ptr() as *mut u8),
|
|
Some(&mut adapter_size),
|
|
)
|
|
.is_ok()
|
|
{
|
|
let len = (adapter_size as usize / 2).saturating_sub(1);
|
|
String::from_utf16_lossy(&adapter_buffer[..len])
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
// Read memory size
|
|
let mem_name = HSTRING::from("HardwareInformation.qwMemorySize");
|
|
let mut mem_value: u64 = 0;
|
|
let mut mem_size = std::mem::size_of::<u64>() as u32;
|
|
let mut mem_type = REG_VALUE_TYPE::default();
|
|
|
|
let vram_gb = if RegQueryValueExW(
|
|
subkey,
|
|
PCWSTR(mem_name.as_ptr()),
|
|
None,
|
|
Some(&mut mem_type),
|
|
Some(&mut mem_value as *mut u64 as *mut u8),
|
|
Some(&mut mem_size),
|
|
)
|
|
.is_ok()
|
|
{
|
|
mem_value / (1024 * 1024 * 1024)
|
|
} else {
|
|
0
|
|
};
|
|
|
|
if !adapter_string.is_empty() {
|
|
result.push(format!("Model: {}\nVRAM (GB): {}", adapter_string, vram_gb));
|
|
}
|
|
|
|
let _ = RegCloseKey(subkey);
|
|
}
|
|
}
|
|
|
|
index += 1;
|
|
}
|
|
|
|
let _ = RegCloseKey(hkey);
|
|
}
|
|
|
|
Ok(result.join("\n\n"))
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_gpu_info_from_registry() -> anyhow::Result<String> {
|
|
Ok(String::new())
|
|
}
|
|
|
|
/// Resolve shortcut (.lnk) file to get target path
|
|
#[cfg(target_os = "windows")]
|
|
pub fn resolve_shortcut(lnk_path: String) -> anyhow::Result<String> {
|
|
use windows::core::{Interface, HSTRING};
|
|
use windows::Win32::System::Com::IPersistFile;
|
|
use windows::Win32::System::Com::{
|
|
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
|
|
COINIT_APARTMENTTHREADED, STGM_READ,
|
|
};
|
|
use windows::Win32::UI::Shell::{IShellLinkW, ShellLink};
|
|
|
|
unsafe {
|
|
// Initialize COM
|
|
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
|
|
|
|
let result = (|| -> anyhow::Result<String> {
|
|
// Create ShellLink instance
|
|
let shell_link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
|
|
|
|
// Get IPersistFile interface
|
|
let persist_file: IPersistFile = shell_link.cast()?;
|
|
|
|
// Load the shortcut file
|
|
let lnk_path_w = HSTRING::from(&lnk_path);
|
|
persist_file.Load(windows::core::PCWSTR(lnk_path_w.as_ptr()), STGM_READ)?;
|
|
|
|
// Get target path
|
|
let mut path_buffer = [0u16; 260];
|
|
shell_link.GetPath(&mut path_buffer, std::ptr::null_mut(), 0)?;
|
|
|
|
let path = String::from_utf16_lossy(
|
|
&path_buffer[..path_buffer
|
|
.iter()
|
|
.position(|&c| c == 0)
|
|
.unwrap_or(path_buffer.len())],
|
|
);
|
|
|
|
Ok(path)
|
|
})();
|
|
|
|
CoUninitialize();
|
|
result
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn resolve_shortcut(_lnk_path: String) -> anyhow::Result<String> {
|
|
Ok(String::new())
|
|
}
|
|
|
|
/// Open file explorer and select file/folder
|
|
#[cfg(target_os = "windows")]
|
|
pub fn open_dir_with_explorer(path: &str, is_file: bool) -> anyhow::Result<()> {
|
|
use std::process::Command;
|
|
|
|
if is_file {
|
|
Command::new("explorer.exe")
|
|
.args(["/select,", path])
|
|
.spawn()?;
|
|
} else {
|
|
Command::new("explorer.exe")
|
|
.args(["/select,", path])
|
|
.spawn()?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn open_dir_with_explorer(path: &str, is_file: bool) -> anyhow::Result<()> {
|
|
println!(
|
|
"open_dir_with_explorer (unix): {} is_file={}",
|
|
path, is_file
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub fn set_foreground_window(window_name: &str) -> anyhow::Result<bool> {
|
|
use windows::core::{HSTRING, PCWSTR};
|
|
use windows::Win32::Foundation::HWND;
|
|
use windows::Win32::UI::WindowsAndMessaging;
|
|
let window_name_p: PCWSTR = PCWSTR(HSTRING::from(window_name).as_ptr());
|
|
let h = unsafe { WindowsAndMessaging::FindWindowW(PCWSTR::null(), window_name_p)? };
|
|
if h == HWND::default() {
|
|
return Ok(false);
|
|
}
|
|
let sr = unsafe { WindowsAndMessaging::ShowWindow(h, WindowsAndMessaging::SW_RESTORE) };
|
|
if !sr.as_bool() {
|
|
return Ok(false);
|
|
}
|
|
let r = unsafe { WindowsAndMessaging::SetForegroundWindow(h) };
|
|
Ok(r.as_bool())
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn set_foreground_window(window_name: &str) -> anyhow::Result<bool> {
|
|
println!("set_foreground_window (unix): {}", window_name);
|
|
return Ok(false);
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ProcessInfo {
|
|
pub pid: u32,
|
|
pub name: String,
|
|
pub path: String,
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_process_pid_by_name(process_name: &str) -> anyhow::Result<i32> {
|
|
// 保持向后兼容:返回第一个匹配进程的 PID
|
|
let processes = get_process_list_by_name(process_name)?;
|
|
if let Some(first) = processes.first() {
|
|
Ok(first.pid as i32)
|
|
} else {
|
|
Ok(-1)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_process_pid_by_name(process_name: &str) -> anyhow::Result<i32> {
|
|
println!("get_process_pid_by_name (unix): {}", process_name);
|
|
Ok(-1)
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_process_list_by_name(process_name: &str) -> anyhow::Result<Vec<ProcessInfo>> {
|
|
use std::mem;
|
|
use windows::Win32::Foundation::CloseHandle;
|
|
use windows::Win32::System::Diagnostics::ToolHelp::{
|
|
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
|
TH32CS_SNAPPROCESS,
|
|
};
|
|
|
|
let mut result = Vec::new();
|
|
let search_lower = process_name.to_lowercase();
|
|
|
|
unsafe {
|
|
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)?;
|
|
if snapshot.is_invalid() {
|
|
return Ok(result);
|
|
}
|
|
|
|
let mut process_entry: PROCESSENTRY32W = mem::zeroed();
|
|
process_entry.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
|
|
|
|
if Process32FirstW(snapshot, &mut process_entry).is_err() {
|
|
let _ = CloseHandle(snapshot);
|
|
return Ok(result);
|
|
}
|
|
|
|
loop {
|
|
// 将 WCHAR 数组转换为 String
|
|
let exe_file = String::from_utf16_lossy(
|
|
&process_entry.szExeFile[..process_entry
|
|
.szExeFile
|
|
.iter()
|
|
.position(|&c| c == 0)
|
|
.unwrap_or(process_entry.szExeFile.len())],
|
|
);
|
|
|
|
// 支持部分匹配(不区分大小写)
|
|
if exe_file.to_lowercase().contains(&search_lower) {
|
|
let pid = process_entry.th32ProcessID;
|
|
|
|
// 获取完整路径
|
|
let full_path = get_process_path(pid).unwrap_or_default();
|
|
|
|
result.push(ProcessInfo {
|
|
pid,
|
|
name: exe_file,
|
|
path: full_path,
|
|
});
|
|
}
|
|
|
|
if Process32NextW(snapshot, &mut process_entry).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let _ = CloseHandle(snapshot);
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn get_process_path(pid: u32) -> Option<String> {
|
|
use windows::core::PWSTR;
|
|
use windows::Win32::Foundation::{CloseHandle, MAX_PATH};
|
|
use windows::Win32::System::Threading::{
|
|
OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32,
|
|
PROCESS_QUERY_LIMITED_INFORMATION,
|
|
};
|
|
|
|
unsafe {
|
|
if let Ok(h_process) = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
|
|
if !h_process.is_invalid() {
|
|
let mut path_buffer = [0u16; MAX_PATH as usize];
|
|
let mut path_len = path_buffer.len() as u32;
|
|
|
|
let result = if QueryFullProcessImageNameW(
|
|
h_process,
|
|
PROCESS_NAME_WIN32,
|
|
PWSTR::from_raw(path_buffer.as_mut_ptr()),
|
|
&mut path_len,
|
|
)
|
|
.is_ok()
|
|
&& path_len > 0
|
|
{
|
|
Some(String::from_utf16_lossy(&path_buffer[..path_len as usize]))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let _ = CloseHandle(h_process);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_process_list_by_name(process_name: &str) -> anyhow::Result<Vec<ProcessInfo>> {
|
|
println!("get_process_list_by_name (unix): {}", process_name);
|
|
Ok(Vec::new())
|
|
}
|
|
|
|
/// Kill processes by name
|
|
#[cfg(target_os = "windows")]
|
|
pub fn kill_process_by_name(process_name: &str) -> anyhow::Result<u32> {
|
|
use windows::Win32::Foundation::CloseHandle;
|
|
use windows::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
|
|
|
|
let processes = get_process_list_by_name(process_name)?;
|
|
let mut killed_count = 0u32;
|
|
|
|
for process in processes {
|
|
unsafe {
|
|
if let Ok(h_process) = OpenProcess(PROCESS_TERMINATE, false, process.pid) {
|
|
if !h_process.is_invalid() {
|
|
if TerminateProcess(h_process, 0).is_ok() {
|
|
killed_count += 1;
|
|
}
|
|
let _ = CloseHandle(h_process);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(killed_count)
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn kill_process_by_name(process_name: &str) -> anyhow::Result<u32> {
|
|
println!("kill_process_by_name (unix): {}", process_name);
|
|
Ok(0)
|
|
}
|
|
|
|
/// Get disk physical sector size for performance
|
|
#[cfg(target_os = "windows")]
|
|
pub fn get_disk_physical_sector_size(drive_letter: &str) -> anyhow::Result<u32> {
|
|
use std::mem;
|
|
use windows::core::HSTRING;
|
|
use windows::Win32::Foundation::CloseHandle;
|
|
use windows::Win32::Storage::FileSystem::{
|
|
CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
|
};
|
|
use windows::Win32::System::Ioctl::IOCTL_STORAGE_QUERY_PROPERTY;
|
|
use windows::Win32::System::IO::DeviceIoControl;
|
|
|
|
// STORAGE_PROPERTY_QUERY structure
|
|
#[repr(C)]
|
|
struct StoragePropertyQuery {
|
|
property_id: u32,
|
|
query_type: u32,
|
|
additional_parameters: [u8; 1],
|
|
}
|
|
|
|
// STORAGE_ACCESS_ALIGNMENT_DESCRIPTOR structure
|
|
#[repr(C)]
|
|
struct StorageAccessAlignmentDescriptor {
|
|
version: u32,
|
|
size: u32,
|
|
bytes_per_cache_line: u32,
|
|
bytes_offset_for_cache_alignment: u32,
|
|
bytes_per_logical_sector: u32,
|
|
bytes_per_physical_sector: u32,
|
|
bytes_offset_for_sector_alignment: u32,
|
|
}
|
|
|
|
let drive_path = format!(r"\\.\{}:", drive_letter.chars().next().unwrap_or('C'));
|
|
let drive_path_w = HSTRING::from(&drive_path);
|
|
|
|
unsafe {
|
|
let handle = CreateFileW(
|
|
&drive_path_w,
|
|
0, // No access needed, just query
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
None,
|
|
OPEN_EXISTING,
|
|
windows::Win32::Storage::FileSystem::FILE_FLAGS_AND_ATTRIBUTES(0),
|
|
None,
|
|
)?;
|
|
|
|
if handle.is_invalid() {
|
|
return Err(anyhow::anyhow!("Failed to open drive"));
|
|
}
|
|
|
|
// StorageAccessAlignmentProperty = 6
|
|
let query = StoragePropertyQuery {
|
|
property_id: 6,
|
|
query_type: 0, // PropertyStandardQuery
|
|
additional_parameters: [0],
|
|
};
|
|
|
|
let mut descriptor: StorageAccessAlignmentDescriptor = mem::zeroed();
|
|
let mut bytes_returned: u32 = 0;
|
|
|
|
let result = DeviceIoControl(
|
|
handle,
|
|
IOCTL_STORAGE_QUERY_PROPERTY,
|
|
Some(&query as *const _ as *const std::ffi::c_void),
|
|
mem::size_of::<StoragePropertyQuery>() as u32,
|
|
Some(&mut descriptor as *mut _ as *mut std::ffi::c_void),
|
|
mem::size_of::<StorageAccessAlignmentDescriptor>() as u32,
|
|
Some(&mut bytes_returned),
|
|
None,
|
|
);
|
|
|
|
let _ = CloseHandle(handle);
|
|
|
|
if result.is_ok() {
|
|
Ok(descriptor.bytes_per_physical_sector)
|
|
} else {
|
|
Err(anyhow::anyhow!("DeviceIoControl failed"))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn get_disk_physical_sector_size(drive_letter: &str) -> anyhow::Result<u32> {
|
|
println!("get_disk_physical_sector_size (unix): {}", drive_letter);
|
|
Ok(0)
|
|
}
|
|
|
|
/// Create a desktop shortcut
|
|
#[cfg(target_os = "windows")]
|
|
pub fn create_desktop_shortcut(target_path: &str, shortcut_name: &str) -> anyhow::Result<()> {
|
|
use windows::core::{Interface, BSTR, HSTRING};
|
|
use windows::Win32::System::Com::IPersistFile;
|
|
use windows::Win32::System::Com::{
|
|
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
|
|
COINIT_APARTMENTTHREADED,
|
|
};
|
|
use windows::Win32::UI::Shell::{
|
|
FOLDERID_Desktop, IShellLinkW, SHGetKnownFolderPath, ShellLink,
|
|
};
|
|
|
|
unsafe {
|
|
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
|
|
|
|
let result = (|| -> anyhow::Result<()> {
|
|
// Get desktop path
|
|
let desktop_path = SHGetKnownFolderPath(
|
|
&FOLDERID_Desktop,
|
|
windows::Win32::UI::Shell::KNOWN_FOLDER_FLAG(0),
|
|
None,
|
|
)?;
|
|
let desktop_str = desktop_path.to_string()?;
|
|
|
|
// Create ShellLink instance
|
|
let shell_link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
|
|
|
|
// Set target path
|
|
let target_w = HSTRING::from(target_path);
|
|
shell_link.SetPath(&target_w)?;
|
|
|
|
// Get IPersistFile interface
|
|
let persist_file: IPersistFile = shell_link.cast()?;
|
|
|
|
// Create shortcut file path
|
|
let shortcut_path = format!("{}\\{}", desktop_str, shortcut_name);
|
|
let shortcut_w = BSTR::from(&shortcut_path);
|
|
|
|
// Save shortcut
|
|
persist_file.Save(windows::core::PCWSTR(shortcut_w.as_ptr()), true)?;
|
|
|
|
Ok(())
|
|
})();
|
|
|
|
CoUninitialize();
|
|
result
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn create_desktop_shortcut(target_path: &str, shortcut_name: &str) -> anyhow::Result<()> {
|
|
println!(
|
|
"create_desktop_shortcut (unix): {} -> {}",
|
|
target_path, shortcut_name
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Run a program with admin privileges (UAC)
|
|
#[cfg(target_os = "windows")]
|
|
pub fn run_as_admin(program: &str, args: &str) -> anyhow::Result<()> {
|
|
use windows::core::HSTRING;
|
|
use windows::Win32::UI::Shell::ShellExecuteW;
|
|
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
|
|
|
|
let operation = HSTRING::from("runas");
|
|
let file = HSTRING::from(program);
|
|
let parameters = HSTRING::from(args);
|
|
|
|
unsafe {
|
|
let result = ShellExecuteW(None, &operation, &file, ¶meters, None, SW_SHOWNORMAL);
|
|
|
|
// ShellExecuteW returns a value > 32 on success
|
|
if result.0 as usize > 32 {
|
|
Ok(())
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"ShellExecuteW failed with code: {}",
|
|
result.0 as usize
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn run_as_admin(program: &str, args: &str) -> anyhow::Result<()> {
|
|
println!("run_as_admin (unix): {} {}", program, args);
|
|
Ok(())
|
|
}
|
|
|
|
/// Start a program (without waiting)
|
|
#[cfg(target_os = "windows")]
|
|
pub fn start_process(program: &str, args: Vec<String>) -> anyhow::Result<()> {
|
|
use std::process::Command;
|
|
|
|
Command::new(program).args(&args).spawn()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn start_process(program: &str, args: Vec<String>) -> anyhow::Result<()> {
|
|
println!("start_process (unix): {} {:?}", program, args);
|
|
Ok(())
|
|
}
|
|
|
|
// ============== NVME Patch Functions ==============
|
|
#[cfg(target_os = "windows")]
|
|
const NVME_REGISTRY_PATH: &str = r"SYSTEM\CurrentControlSet\Services\stornvme\Parameters\Device";
|
|
#[cfg(target_os = "windows")]
|
|
const NVME_VALUE_NAME: &str = "ForcedPhysicalSectorSizeInBytes";
|
|
|
|
/// Check if NVME patch is applied
|
|
#[cfg(target_os = "windows")]
|
|
pub fn check_nvme_patch_status() -> anyhow::Result<bool> {
|
|
use windows::core::{HSTRING, PCWSTR};
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_LOCAL_MACHINE, KEY_READ, REG_VALUE_TYPE,
|
|
};
|
|
|
|
unsafe {
|
|
let path = HSTRING::from(NVME_REGISTRY_PATH);
|
|
let mut hkey = std::mem::zeroed();
|
|
|
|
// Try to open the registry key
|
|
if RegOpenKeyExW(
|
|
HKEY_LOCAL_MACHINE,
|
|
PCWSTR(path.as_ptr()),
|
|
Some(0),
|
|
KEY_READ,
|
|
&mut hkey,
|
|
)
|
|
.is_err()
|
|
{
|
|
return Ok(false);
|
|
}
|
|
|
|
// Query the value
|
|
let value_name = HSTRING::from(NVME_VALUE_NAME);
|
|
let mut buffer = [0u8; 1024];
|
|
let mut size = buffer.len() as u32;
|
|
let mut value_type = REG_VALUE_TYPE::default();
|
|
|
|
let result = if RegQueryValueExW(
|
|
hkey,
|
|
PCWSTR(value_name.as_ptr()),
|
|
None,
|
|
Some(&mut value_type),
|
|
Some(buffer.as_mut_ptr()),
|
|
Some(&mut size),
|
|
)
|
|
.is_ok()
|
|
{
|
|
// Check if the value contains "* 4095"
|
|
// REG_MULTI_SZ is stored as null-terminated wide strings
|
|
let data = String::from_utf16_lossy(std::slice::from_raw_parts(
|
|
buffer.as_ptr() as *const u16,
|
|
size as usize / 2,
|
|
));
|
|
data.contains("* 4095")
|
|
} else {
|
|
false
|
|
};
|
|
|
|
let _ = RegCloseKey(hkey);
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn check_nvme_patch_status() -> anyhow::Result<bool> {
|
|
Ok(false)
|
|
}
|
|
|
|
/// Add NVME patch to registry
|
|
#[cfg(target_os = "windows")]
|
|
pub fn add_nvme_patch() -> anyhow::Result<()> {
|
|
use windows::core::{HSTRING, PCWSTR};
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegOpenKeyExW, RegSetValueExW, HKEY_LOCAL_MACHINE, KEY_WRITE, REG_MULTI_SZ,
|
|
};
|
|
|
|
unsafe {
|
|
let path = HSTRING::from(NVME_REGISTRY_PATH);
|
|
let mut hkey = std::mem::zeroed();
|
|
|
|
// Open the registry key with write access
|
|
let open_result = RegOpenKeyExW(
|
|
HKEY_LOCAL_MACHINE,
|
|
PCWSTR(path.as_ptr()),
|
|
Some(0),
|
|
KEY_WRITE,
|
|
&mut hkey,
|
|
);
|
|
if open_result.is_err() {
|
|
return Err(anyhow::anyhow!(
|
|
"Failed to open registry key: {:?}",
|
|
open_result
|
|
));
|
|
}
|
|
|
|
// Prepare the value: "* 4095" as REG_MULTI_SZ (double null terminated)
|
|
let value_str = "* 4095\0\0";
|
|
let value_wide: Vec<u16> = value_str.encode_utf16().collect();
|
|
let value_name = HSTRING::from(NVME_VALUE_NAME);
|
|
|
|
let result = RegSetValueExW(
|
|
hkey,
|
|
PCWSTR(value_name.as_ptr()),
|
|
Some(0),
|
|
REG_MULTI_SZ,
|
|
Some(std::slice::from_raw_parts(
|
|
value_wide.as_ptr() as *const u8,
|
|
value_wide.len() * 2,
|
|
)),
|
|
);
|
|
|
|
let _ = RegCloseKey(hkey);
|
|
|
|
if result.is_err() {
|
|
return Err(anyhow::anyhow!(
|
|
"Failed to set registry value: {:?}",
|
|
result
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn add_nvme_patch() -> anyhow::Result<()> {
|
|
Err(anyhow::anyhow!("NVME patch is only supported on Windows"))
|
|
}
|
|
|
|
/// Remove NVME patch from registry
|
|
#[cfg(target_os = "windows")]
|
|
pub fn remove_nvme_patch() -> anyhow::Result<()> {
|
|
use windows::core::{HSTRING, PCWSTR};
|
|
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
|
use windows::Win32::System::Registry::{
|
|
RegCloseKey, RegDeleteValueW, RegOpenKeyExW, HKEY_LOCAL_MACHINE, KEY_WRITE,
|
|
};
|
|
|
|
unsafe {
|
|
let path = HSTRING::from(NVME_REGISTRY_PATH);
|
|
let mut hkey = std::mem::zeroed();
|
|
|
|
// Open the registry key with write access
|
|
let open_result = RegOpenKeyExW(
|
|
HKEY_LOCAL_MACHINE,
|
|
PCWSTR(path.as_ptr()),
|
|
Some(0),
|
|
KEY_WRITE,
|
|
&mut hkey,
|
|
);
|
|
if open_result.is_err() {
|
|
return Err(anyhow::anyhow!(
|
|
"Failed to open registry key: {:?}",
|
|
open_result
|
|
));
|
|
}
|
|
|
|
let value_name = HSTRING::from(NVME_VALUE_NAME);
|
|
|
|
let result = RegDeleteValueW(hkey, PCWSTR(value_name.as_ptr()));
|
|
|
|
let _ = RegCloseKey(hkey);
|
|
|
|
// It's OK if the value doesn't exist
|
|
if result.is_err() && result != ERROR_FILE_NOT_FOUND {
|
|
return Err(anyhow::anyhow!(
|
|
"Failed to delete registry value: {:?}",
|
|
result
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
pub fn remove_nvme_patch() -> anyhow::Result<()> {
|
|
Err(anyhow::anyhow!("NVME patch is only supported on Windows"))
|
|
}
|