use anyhow::{Context, Result}; use std::path::PathBuf;
use std::io::{self, Write};
use std::process::Command;
use std::env;
use log::info;
use is_terminal::IsTerminal;
#[cfg(target_os = "windows")]
use std::fs::File;
#[cfg(target_os = "windows")]
use std::ffi::{OsStr, OsString};
#[cfg(target_os = "windows")]
use std::os::windows::ffi::OsStrExt;
#[cfg(target_os = "windows")]
use std::os::windows::ffi::OsStringExt;
#[cfg(target_os = "windows")]
use winapi::um::shellapi::ShellExecuteW;
#[cfg(target_os = "windows")]
use winapi::um::winuser::SW_SHOWNORMAL;
#[cfg(target_os = "windows")]
use winapi::um::fileapi::GetTempPathW;
#[cfg(not(target_os = "windows"))]
use std::process::Stdio;
use crate::ui::{output_format, theme};
use crate::commands::cleansh::info_msg;
use crate::ui::theme::ThemeMap;
#[cfg(target_os = "windows")]
fn to_wide_string(s: &str) -> Vec<u16> {
OsStr::new(s).encode_wide().chain(Some(0)).collect()
}
#[cfg(target_os = "windows")]
fn is_elevated() -> bool {
unsafe {
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken};
use winapi::um::securitybaseapi::GetTokenInformation;
use winapi::um::winnt::{TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY};
let mut token_handle = std::ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle) == 0 {
return false;
}
let mut elevation: TOKEN_ELEVATION = std::mem::zeroed();
let mut size: u32 = 0;
let success = GetTokenInformation(
token_handle,
TokenElevation,
&mut elevation as *mut _ as winapi::shared::minwindef::LPVOID,
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut size,
) != 0;
CloseHandle(token_handle);
success && elevation.TokenIsElevated != 0
}
}
pub fn elevate_and_run_uninstall(yes_flag: bool, theme_map: &ThemeMap) -> Result<()> {
info!("Starting cleansh uninstall operation.");
let stderr_supports_color = io::stderr().is_terminal();
if !yes_flag {
info_msg("WARNING: This will uninstall Cleansh and remove its associated data.", theme_map);
output_format::print_message(
&mut io::stderr(),
"Are you sure you want to proceed? (y/N): ",
theme_map,
Some(theme::ThemeEntry::Prompt),
stderr_supports_color,
)?;
io::stderr().flush()?;
let mut confirmation = String::new();
io::stdin().read_line(&mut confirmation)
.context("Failed to read confirmation input.")?;
if confirmation.trim().to_lowercase() != "y" {
info_msg("Uninstallation cancelled.", theme_map);
return Ok(());
}
}
let current_exe_path = env::current_exe()
.context("Failed to determine current executable path.")?;
let app_state_path = std::env::var("CLEANSH_STATE_FILE_OVERRIDE_FOR_TESTS")
.map(PathBuf::from)
.unwrap_or_else(|_| {
if let Some(dir) = dirs::data_dir() {
dir.join("cleansh").join("state.json")
} else {
env::current_dir().expect("Failed to get current dir").join("cleansh_state.json")
}
});
let app_state_dir = app_state_path.parent().unwrap_or(&PathBuf::from(".")).to_path_buf();
info_msg("Preparing removal script...", theme_map);
#[cfg(target_os = "windows")]
{
uninstall_windows(current_exe_path, app_state_path, app_state_dir, theme_map)?;
}
#[cfg(not(target_os = "windows"))]
{
uninstall_unix(current_exe_path, app_state_path, app_state_dir, theme_map)?;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn uninstall_windows(exe_path: PathBuf, state_file: PathBuf, state_dir: PathBuf, theme_map: &ThemeMap) -> Result<()> {
let mut temp_path_buf = vec![0u16; 260];
let temp_path_len = unsafe { GetTempPathW(temp_path_buf.len() as u32, temp_path_buf.as_mut_ptr()) };
let temp_dir = PathBuf::from(OsString::from_wide(&temp_path_buf[0..temp_path_len as usize]));
let script_path = temp_dir.join(format!("cleansh_nuke_{}.ps1", std::process::id()));
let current_pid = std::process::id();
let script_content = format!(
r#"
$pidToWait = {}
$exePath = "{}"
$stateFile = "{}"
$stateDir = "{}"
Write-Host "Waiting for Cleansh (PID: $pidToWait) to exit..." -ForegroundColor Cyan
try {{
Wait-Process -Id $pidToWait -Timeout 10 -ErrorAction SilentlyContinue
}} catch {{
# Process might already be gone
}}
# Ensure file handle release
Start-Sleep -Seconds 1
Write-Host "Deleting executable..." -ForegroundColor Yellow
$deleted = $false
for ($i=0; $i -lt 10; $i++) {{
try {{
if (Test-Path $exePath) {{
Remove-Item -Path $exePath -Force -ErrorAction Stop
$deleted = $true
break
}} else {{
$deleted = $true # Already gone
break
}}
}} catch {{
Write-Host "Attempt $($i+1): Locked... retrying..."
Start-Sleep -Seconds 1
}}
}}
if (-not $deleted) {{
Write-Host "Could not delete executable. It may be locked." -ForegroundColor Red
Write-Host "Please delete manually: $exePath"
}} else {{
Write-Host "Executable deleted." -ForegroundColor Green
}}
# Clean Configs
if (Test-Path $stateFile) {{ Remove-Item -Path $stateFile -Force; Write-Host "State file removed." }}
if (Test-Path $stateDir) {{
# Only remove if empty or force if desired. Here we recurse.
Remove-Item -Path $stateDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "Config directory removed."
}}
Write-Host "Uninstallation Complete." -ForegroundColor Green
Start-Sleep -Seconds 3
# Self-destruct script
Remove-Item -Path $MyInvocation.MyCommand.Path -Force
"#,
current_pid,
exe_path.to_string_lossy(),
state_file.to_string_lossy(),
state_dir.to_string_lossy()
);
let mut file = File::create(&script_path).context("Failed to create temporary uninstall script")?;
file.write_all(script_content.as_bytes())?;
let script_path_str = script_path.to_string_lossy().to_string();
if is_elevated() {
info_msg("Running cleanup script...", theme_map);
Command::new("powershell.exe")
.args(&["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script_path_str])
.spawn()
.context("Failed to spawn cleanup script")?;
} else {
info_msg("Requesting administrative privileges to complete removal...", theme_map);
let operation = to_wide_string("runas");
let filename = to_wide_string("powershell.exe");
let args = to_wide_string(&format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", script_path_str));
let res = unsafe {
ShellExecuteW(
std::ptr::null_mut(),
operation.as_ptr(),
filename.as_ptr(),
args.as_ptr(),
std::ptr::null(),
SW_SHOWNORMAL,
)
};
if (res as usize) <= 32 {
return Err(anyhow::anyhow!("Failed to launch uninstaller (ShellExecute error: {})", res as usize));
}
}
std::process::exit(0);
}
#[cfg(not(target_os = "windows"))]
fn uninstall_unix(exe_path: PathBuf, state_file: PathBuf, state_dir: PathBuf, theme_map: &ThemeMap) -> Result<()> {
let bash_script = format!(
r#"
sleep 1
rm -f "{}"
rm -f "{}"
rm -rf "{}"
echo "CleanSH uninstalled."
"#,
exe_path.to_string_lossy(),
state_file.to_string_lossy(),
state_dir.to_string_lossy()
);
Command::new("sh")
.arg("-c")
.arg(bash_script)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("Failed to spawn uninstall script")?;
info_msg("Uninstallation scheduled. Exiting...", theme_map);
std::process::exit(0);
}