v0.2.5 : info enrichi (statut, versions, dependances verifiees) + self-uninstall tolerant (fichiers verrouilles / lecture seule, balayage des dossiers vides) + agent deepseek-harness au catalogue
This commit is contained in:
+177
-10
@@ -9,6 +9,10 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(windows)]
|
||||
const MOVEFILE_DELAY_UNTIL_REBOOT: u32 = 0x4;
|
||||
#[cfg(windows)]
|
||||
const FILE_ATTRIBUTE_READONLY: u32 = 0x1;
|
||||
#[cfg(windows)]
|
||||
const INVALID_FILE_ATTRIBUTES: u32 = 0xFFFF_FFFF;
|
||||
|
||||
#[cfg(windows)]
|
||||
#[link(name = "Kernel32")]
|
||||
@@ -18,6 +22,8 @@ extern "system" {
|
||||
lpNewFileName: *const u16,
|
||||
dwFlags: u32,
|
||||
) -> i32;
|
||||
fn GetFileAttributesW(lpFileName: *const u16) -> u32;
|
||||
fn SetFileAttributesW(lpFileName: *const u16, dwFileAttributes: u32) -> i32;
|
||||
}
|
||||
|
||||
pub fn run(app: &App) -> Result<i32> {
|
||||
@@ -67,30 +73,150 @@ pub fn run(app: &App) -> Result<i32> {
|
||||
// Remove everything first; any message logged *after* deleting the
|
||||
// state directory would recreate the log folder, so the removal
|
||||
// itself is silent and the report below goes to plain stdout.
|
||||
// The removal is tolerant: read-only attributes are cleared and files
|
||||
// locked by other processes (for example the running am.exe when it
|
||||
// lives inside the data directory) are scheduled for deletion at the
|
||||
// next reboot instead of failing the whole uninstall.
|
||||
let mut locked: Vec<PathBuf> = Vec::new();
|
||||
for (_, path) in &roots {
|
||||
std::fs::remove_dir_all(path)
|
||||
.with_context(|| format!("cannot remove {}", path.display()))?;
|
||||
remove_tree(path, &mut locked);
|
||||
}
|
||||
for (_, path) in &roots {
|
||||
println!("{} {}", "removed:".green(), path.display());
|
||||
if !path.exists() {
|
||||
println!("{} {}", "removed:".green(), path.display());
|
||||
}
|
||||
}
|
||||
if !locked.is_empty() {
|
||||
let scheduled = schedule_locked(&locked);
|
||||
if scheduled.len() == locked.len() {
|
||||
println!(
|
||||
"{} {} entrée(s) verrouillée(s) — suppression planifiée au prochain redémarrage",
|
||||
"locked:".yellow(),
|
||||
locked.len()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} {} entrée(s) verrouillée(s), {} planifiée(s) au prochain redémarrage",
|
||||
"locked:".yellow(),
|
||||
locked.len(),
|
||||
scheduled.len()
|
||||
);
|
||||
for path in &locked {
|
||||
if !scheduled.contains(path) {
|
||||
eprintln!(
|
||||
"{} {} (supprimez-le manuellement ou relancez en administrateur)",
|
||||
"kept:".red(),
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Delete the executable itself.
|
||||
// 3. Delete the executable itself, then sweep the directories left
|
||||
// empty behind it.
|
||||
let exe = std::env::current_exe()
|
||||
.with_context(|| "cannot locate the agent-manager executable")?;
|
||||
remove_self(&exe)?;
|
||||
let cleanup_roots: Vec<PathBuf> = roots.iter().map(|(_, p)| p.clone()).collect();
|
||||
remove_self(&exe, &cleanup_roots)?;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Recursively remove a tree, tolerating locked or read-only entries.
|
||||
/// Paths that could not be removed are collected into `locked` so the
|
||||
/// caller can schedule their deletion at the next reboot.
|
||||
fn remove_tree(path: &Path, locked: &mut Vec<PathBuf>) {
|
||||
let meta = match std::fs::symlink_metadata(path) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return,
|
||||
};
|
||||
if meta.is_dir() {
|
||||
if let Ok(entries) = std::fs::read_dir(path) {
|
||||
for entry in entries.flatten() {
|
||||
remove_tree(&entry.path(), locked);
|
||||
}
|
||||
}
|
||||
clear_readonly(path);
|
||||
if std::fs::remove_dir(path).is_err() {
|
||||
locked.push(path.to_path_buf());
|
||||
}
|
||||
} else {
|
||||
clear_readonly(path);
|
||||
if std::fs::remove_file(path).is_err() {
|
||||
locked.push(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the read-only attribute (a common cause of "Access denied" on
|
||||
/// Windows, e.g. files extracted by Expand-Archive).
|
||||
#[cfg(windows)]
|
||||
fn clear_readonly(path: &Path) {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let wide: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
unsafe {
|
||||
let attrs = GetFileAttributesW(wide.as_ptr());
|
||||
if attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_READONLY) != 0 {
|
||||
SetFileAttributesW(wide.as_ptr(), attrs & !FILE_ATTRIBUTE_READONLY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn clear_readonly(_path: &Path) {}
|
||||
|
||||
/// Schedule every locked path for deletion at the next reboot (Windows).
|
||||
/// Returns the paths that could be scheduled.
|
||||
#[cfg(windows)]
|
||||
fn schedule_locked(locked: &[PathBuf]) -> Vec<PathBuf> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let mut scheduled = Vec::new();
|
||||
for path in locked {
|
||||
let wide: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let ok = unsafe {
|
||||
MoveFileExW(wide.as_ptr(), std::ptr::null(), MOVEFILE_DELAY_UNTIL_REBOOT) != 0
|
||||
};
|
||||
if ok {
|
||||
scheduled.push(path.clone());
|
||||
}
|
||||
}
|
||||
scheduled
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn schedule_locked(_locked: &[PathBuf]) -> Vec<PathBuf> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Delete the running executable. On Unix the file can be unlinked while it
|
||||
/// runs; on Windows a detached cmd re-tries the deletion right after this
|
||||
/// process exits (the file is locked while we run).
|
||||
fn remove_self(exe: &Path) -> Result<()> {
|
||||
/// runs; on Windows a hidden cmd re-tries the deletion right after this
|
||||
/// process exits (the file is locked while we run). Empty directories left
|
||||
/// behind under the removed roots are swept afterwards.
|
||||
fn remove_self(exe: &Path, roots: &[PathBuf]) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::fs::remove_file(exe)
|
||||
.with_context(|| format!("cannot remove {}", exe.display()))?;
|
||||
// Sweep empty directories upward, but never above a removed root.
|
||||
let mut dir = exe.parent();
|
||||
while let Some(d) = dir {
|
||||
if !roots.iter().any(|r| d.starts_with(r)) {
|
||||
break;
|
||||
}
|
||||
if std::fs::remove_dir(d).is_err() {
|
||||
break;
|
||||
}
|
||||
dir = d.parent();
|
||||
}
|
||||
println!(
|
||||
"{} {}",
|
||||
"agent-manager removed:".green(),
|
||||
@@ -103,10 +229,20 @@ fn remove_self(exe: &Path) -> Result<()> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
// Immediate deletion: a hidden cmd retries for a few seconds, because
|
||||
// the executable is locked while we are still running.
|
||||
let script = format!(
|
||||
"for /L %i in (1,1,20) do @(del /f /q \"{0}\" >nul 2>&1 & if not exist \"{0}\" exit /b 0 & ping -n 2 127.0.0.1 >nul)",
|
||||
// Once the file is gone, the now-empty directories (parent, then the
|
||||
// removed roots) are swept — rd /q only ever removes empty dirs.
|
||||
let mut script = format!(
|
||||
"for /L %i in (1,1,20) do @if exist \"{0}\" (del /f /q \"{0}\" >nul 2>&1 & ping -n 2 127.0.0.1 >nul)",
|
||||
exe.display()
|
||||
);
|
||||
if let Some(parent) = exe.parent() {
|
||||
if roots.iter().any(|r| parent.starts_with(r)) {
|
||||
script.push_str(&format!(" & rd /q \"{}\" >nul 2>&1", parent.display()));
|
||||
}
|
||||
}
|
||||
for root in roots {
|
||||
script.push_str(&format!(" & rd /q \"{}\" >nul 2>&1", root.display()));
|
||||
}
|
||||
let spawned = std::process::Command::new("cmd")
|
||||
.args(["/C", &script])
|
||||
.spawn()
|
||||
@@ -142,3 +278,34 @@ fn remove_self(exe: &Path) -> Result<()> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn removes_readonly_tree() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let sub = dir.path().join("sub");
|
||||
std::fs::create_dir_all(&sub).unwrap();
|
||||
let file = sub.join("readonly.txt");
|
||||
std::fs::write(&file, "x").unwrap();
|
||||
// Read-only files are a classic cause of "Access denied" on Windows.
|
||||
let mut perms = std::fs::metadata(&file).unwrap().permissions();
|
||||
#[allow(clippy::permissions_set_readonly_false)]
|
||||
perms.set_readonly(true);
|
||||
std::fs::set_permissions(&file, perms).unwrap();
|
||||
|
||||
let mut locked: Vec<PathBuf> = Vec::new();
|
||||
remove_tree(dir.path(), &mut locked);
|
||||
assert!(!dir.path().exists(), "tree should be fully removed");
|
||||
assert!(locked.is_empty(), "unexpected locked paths: {locked:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tolerates_missing_tree() {
|
||||
let mut locked: Vec<PathBuf> = Vec::new();
|
||||
remove_tree(Path::new("/nonexistent/agent-manager-xyz"), &mut locked);
|
||||
assert!(locked.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user