Skip to content

Commit 03f87b7

Browse files
faratechclaude
andcommitted
Fix auto-update, installer, and UI bugs (v0.1.1)
- Fix auto-update applying to wrong exe when run from non-installed location - Skip background update check after applying update to prevent double download - Remove unnecessary UAC elevation for install/update (LOCALAPPDATA is user-writable) - Fix process info dialog background color inconsistency - Fetch exe path on-demand for process info dialog - Exclude System Idle Process (PID 0) from CPU utilization - Improve process info dialog layout and styling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent eab12cd commit 03f87b7

8 files changed

Lines changed: 100 additions & 189 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "htop-win"
3-
version = "0.1.0"
3+
version = "0.1.1"
44
edition = "2024"
55
description = "A Windows clone of htop - interactive process viewer"
66
authors = ["htop-win contributors"]
@@ -27,7 +27,6 @@ windows = { version = "0.61", features = [
2727
"Win32_NetworkManagement_IpHelper",
2828
"Win32_NetworkManagement_Ndis",
2929
"Win32_UI_Shell",
30-
"Win32_UI_WindowsAndMessaging",
3130
"Win32_System_Com",
3231
"Wdk_System_SystemInformation",
3332
] }

src/app.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,14 @@ impl App {
798798
let (io_read, io_write) = crate::system::get_process_io_counters(proc.pid);
799799
proc_copy.io_read_bytes = io_read;
800800
proc_copy.io_write_bytes = io_write;
801+
// Query exe path on-demand if not already available
802+
if proc_copy.exe_path.is_empty() {
803+
let exe_path = crate::system::get_process_exe_path(proc.pid);
804+
if !exe_path.is_empty() {
805+
proc_copy.exe_path = exe_path.clone();
806+
proc_copy.command = exe_path;
807+
}
808+
}
801809
self.process_info_target = Some(proc_copy);
802810
self.view_mode = ViewMode::ProcessInfo;
803811
}

src/installer.rs

Lines changed: 10 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,6 @@
33
use std::fs;
44
use std::path::PathBuf;
55

6-
#[cfg(windows)]
7-
use windows::core::{PCWSTR, w};
8-
#[cfg(windows)]
9-
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND};
10-
#[cfg(windows)]
11-
use windows::Win32::Security::{GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY};
12-
#[cfg(windows)]
13-
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
14-
#[cfg(windows)]
15-
use windows::Win32::UI::Shell::ShellExecuteW;
16-
#[cfg(windows)]
17-
use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
18-
19-
/// Check if running as administrator
20-
#[cfg(windows)]
21-
pub fn is_admin() -> bool {
22-
unsafe {
23-
let mut token = HANDLE::default();
24-
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token).is_ok() {
25-
let mut elevation = TOKEN_ELEVATION::default();
26-
let mut size = 0u32;
27-
let result = GetTokenInformation(
28-
token,
29-
TokenElevation,
30-
Some(&mut elevation as *mut _ as *mut _),
31-
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
32-
&mut size,
33-
);
34-
let _ = CloseHandle(token);
35-
result.is_ok() && elevation.TokenIsElevated != 0
36-
} else {
37-
false
38-
}
39-
}
40-
}
41-
42-
#[cfg(not(windows))]
43-
pub fn is_admin() -> bool {
44-
false
45-
}
46-
47-
/// Re-launch the current process with UAC elevation
48-
#[cfg(windows)]
49-
pub fn elevate_with_args(args: &str) -> Result<(), Box<dyn std::error::Error>> {
50-
let exe_path = std::env::current_exe()?;
51-
let exe_path_wide: Vec<u16> = exe_path
52-
.to_string_lossy()
53-
.encode_utf16()
54-
.chain(std::iter::once(0))
55-
.collect();
56-
let args_wide: Vec<u16> = format!("{}\0", args).encode_utf16().collect();
57-
58-
let result = unsafe {
59-
ShellExecuteW(
60-
Some(HWND::default()),
61-
w!("runas"),
62-
PCWSTR(exe_path_wide.as_ptr()),
63-
PCWSTR(args_wide.as_ptr()),
64-
PCWSTR::null(),
65-
SW_SHOWNORMAL,
66-
)
67-
};
68-
69-
// ShellExecuteW returns > 32 on success
70-
if result.0 as usize > 32 {
71-
Ok(())
72-
} else {
73-
Err("Failed to elevate privileges".into())
74-
}
75-
}
76-
77-
#[cfg(not(windows))]
78-
pub fn elevate_with_args(_args: &str) -> Result<(), Box<dyn std::error::Error>> {
79-
Err("UAC elevation is only supported on Windows".into())
80-
}
81-
826
/// Get the installation path for htop
837
pub fn get_install_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
848
let local_app_data = std::env::var("LOCALAPPDATA")?;
@@ -109,17 +33,8 @@ pub fn get_installed_version() -> Option<String> {
10933
}
11034

11135
/// Install htop-win to a PATH directory so it can be run from anywhere
36+
/// Installs to %LOCALAPPDATA%\Microsoft\WindowsApps which is user-writable and already in PATH
11237
pub fn install_to_path(force: bool) -> Result<(), Box<dyn std::error::Error>> {
113-
if !is_admin() {
114-
// Re-launch with UAC elevation
115-
println!("Requesting administrator privileges...");
116-
let args = if force { "--install --force" } else { "--install" };
117-
elevate_with_args(args)?;
118-
println!("Elevated process launched. Check that window for results.");
119-
return Ok(());
120-
}
121-
122-
// We're running as admin - do the installation
12338
let current_exe = std::env::current_exe()?;
12439
let current_version = env!("CARGO_PKG_VERSION");
12540
let target_path = get_install_path()?;
@@ -245,7 +160,6 @@ fn download_file(url: &str, dest: &std::path::Path) -> Result<(), Box<dyn std::e
245160
fn cleanup_temp_files() {
246161
let temp_dir = std::env::temp_dir();
247162
let _ = fs::remove_file(temp_dir.join("htop-win-update.exe"));
248-
let _ = fs::remove_file(temp_dir.join("htop-win-update-path.txt"));
249163
}
250164

251165
/// Update htop-win from GitHub releases
@@ -281,19 +195,7 @@ pub fn update_from_github(force: bool) -> Result<(), Box<dyn std::error::Error>>
281195

282196
println!("Download complete. Installing...");
283197

284-
// Need admin to install to WindowsApps
285-
if !is_admin() {
286-
// Copy temp file path to a location the elevated process can access
287-
let update_marker = temp_dir.join("htop-win-update-path.txt");
288-
fs::write(&update_marker, temp_file.to_string_lossy().as_bytes())?;
289-
290-
println!("Requesting administrator privileges...");
291-
elevate_with_args("--install-update")?;
292-
println!("Elevated process launched. Check that window for results.");
293-
return Ok(());
294-
}
295-
296-
// We're admin - do the actual install
198+
// Install directly - %LOCALAPPDATA%\Microsoft\WindowsApps is user-writable
297199
do_install_update(&temp_file)
298200
}
299201

@@ -340,24 +242,6 @@ pub fn do_install_update(update_file: &std::path::Path) -> Result<(), Box<dyn st
340242
Ok(())
341243
}
342244

343-
/// Complete an update installation (called when elevated with --install-update)
344-
pub fn complete_update_install() -> Result<(), Box<dyn std::error::Error>> {
345-
let temp_dir = std::env::temp_dir();
346-
let update_marker = temp_dir.join("htop-win-update-path.txt");
347-
348-
let update_path = fs::read_to_string(&update_marker)?;
349-
let update_file = PathBuf::from(update_path.trim());
350-
351-
// Clean up marker file
352-
let _ = fs::remove_file(&update_marker);
353-
354-
if !update_file.exists() {
355-
return Err("Update file not found".into());
356-
}
357-
358-
do_install_update(&update_file)
359-
}
360-
361245
/// Update status for background updates
362246
#[derive(Clone)]
363247
pub enum UpdateStatus {
@@ -418,24 +302,20 @@ pub fn apply_pending_update() -> bool {
418302
let temp_dir = std::env::temp_dir();
419303
let update_file = temp_dir.join("htop-win-update.exe");
420304

305+
// Get the currently running executable - this is what we need to update
306+
let current_exe = match std::env::current_exe() {
307+
Ok(p) => p,
308+
Err(_) => return false,
309+
};
310+
421311
if !update_file.exists() {
422312
// Clean up any old backup files from previous updates
423-
let install_path = match get_install_path() {
424-
Ok(p) => p,
425-
Err(_) => return false,
426-
};
427-
let backup_path = install_path.with_extension("exe.old");
313+
let backup_path = current_exe.with_extension("exe.old");
428314
let _ = fs::remove_file(&backup_path);
429315
return false;
430316
}
431317

432-
let install_path = match get_install_path() {
433-
Ok(p) => p,
434-
Err(_) => {
435-
let _ = fs::remove_file(&update_file);
436-
return false;
437-
}
438-
};
318+
let install_path = current_exe;
439319

440320
// If install path doesn't exist, just copy directly
441321
if !install_path.exists() {

src/main.rs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ struct Args {
4141
inefficient: bool,
4242
install: bool,
4343
update: bool,
44-
install_update: bool,
4544
force: bool,
4645
}
4746

@@ -121,9 +120,6 @@ fn parse_args() -> Result<Args, lexopt::Error> {
121120
Long("update") => {
122121
args.update = true;
123122
}
124-
Long("install-update") => {
125-
args.install_update = true;
126-
}
127123
Long("force") | Short('f') => {
128124
args.force = true;
129125
}
@@ -333,17 +329,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
333329
return Ok(());
334330
}
335331

336-
if args.install_update {
337-
// Called from elevated process to complete update installation
338-
if let Err(e) = installer::complete_update_install() {
339-
eprintln!("Update installation failed: {}", e);
340-
std::process::exit(1);
341-
}
342-
return Ok(());
343-
}
344-
345332
// Apply any pending update before starting (downloaded in previous session)
346-
installer::apply_pending_update();
333+
let update_just_applied = installer::apply_pending_update();
347334

348335
// Enable Efficiency Mode by default (reduces CPU usage via EcoQoS)
349336
if !args.inefficient {
@@ -449,8 +436,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
449436
// Create benchmark stats if in benchmark mode
450437
let mut bench_stats = benchmark_mode.map(|_| BenchmarkStats::new());
451438

452-
// Spawn background update check
453-
let update_rx = installer::spawn_update_check();
439+
// Spawn background update check (skip if we just applied an update, since
440+
// the running binary is still the old version and would re-download)
441+
let update_rx = if update_just_applied {
442+
// Create a dummy channel that never sends anything
443+
let (_, rx) = std::sync::mpsc::channel();
444+
rx
445+
} else {
446+
installer::spawn_update_check()
447+
};
454448

455449
// Run the main loop
456450
let result = run_app(&mut terminal, &mut app, &config, bench_stats.as_mut(), update_rx);

src/system/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ mod process;
99
pub use cpu::CpuInfo;
1010
pub use memory::{format_bytes, MemoryInfo};
1111
pub use process::{
12-
enable_debug_privilege, enrich_processes, get_process_affinity, get_process_io_counters,
13-
kill_process, set_efficiency_mode, set_priority_class, set_process_affinity, ProcessInfo,
12+
enable_debug_privilege, enrich_processes, get_process_affinity, get_process_exe_path,
13+
get_process_io_counters, kill_process, set_efficiency_mode, set_priority_class,
14+
set_process_affinity, ProcessInfo,
1415
};
1516
#[cfg(windows)]
1617
pub use native::{query_all_processes, calculate_cpu_percentages};

src/system/native.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,12 @@ pub fn calculate_cpu_percentages(
190190
let cache_snapshot = CACHE.snapshot();
191191

192192
for proc in processes.iter() {
193+
// System Idle Process (PID 0) represents idle CPU time, not actual work
194+
if proc.pid == 0 {
195+
cpu_percentages.insert(0, 0.0);
196+
continue;
197+
}
198+
193199
let total_time = proc.kernel_time + proc.user_time;
194200

195201
let cpu_percent = if let Some(entry) = cache_snapshot.get(&proc.pid) {

src/system/process.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,23 @@ pub fn get_process_io_counters(_pid: u32) -> (u64, u64) {
317317
(0, 0)
318318
}
319319

320+
/// Get executable path for a specific process (on-demand, for ProcessInfo dialog)
321+
#[cfg(windows)]
322+
pub fn get_process_exe_path(pid: u32) -> String {
323+
let handle = match open_process_query(pid) {
324+
Some(h) => h,
325+
None => return String::new(),
326+
};
327+
let result = query_exe_path(handle);
328+
unsafe { let _ = CloseHandle(handle); }
329+
result
330+
}
331+
332+
#[cfg(not(windows))]
333+
pub fn get_process_exe_path(_pid: u32) -> String {
334+
String::new()
335+
}
336+
320337
/// Enriched data from Windows API for visible processes
321338
#[cfg(windows)]
322339
struct EnrichedProcessData {

0 commit comments

Comments
 (0)