Add disconnect/uninstall for persistent sessions

- Server: Add DELETE /api/sessions/:id endpoint to disconnect agents
- Server: SessionManager.disconnect_session() sends Disconnect message
- Agent: Handle ADMIN_DISCONNECT to trigger uninstall
- Agent: Add startup::uninstall() to remove from startup and schedule exe deletion
- Dashboard: Add Disconnect button in Access tab machine details
- Dashboard: Add Chat button for persistent sessions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-28 16:53:29 -07:00
parent aa03a87c7c
commit 4b29dbe6c8
7 changed files with 131 additions and 2 deletions

View File

@@ -73,6 +73,7 @@ windows = { version = "0.58", features = [
"Win32_System_Threading",
"Win32_System_Registry",
"Win32_Security",
"Win32_Storage_FileSystem",
]}
# Windows service support

View File

@@ -249,6 +249,19 @@ async fn run_agent(config: config::Config) -> Result<()> {
return Ok(());
}
// Check if this is an admin disconnect (uninstall)
if error_msg.contains("ADMIN_DISCONNECT") {
info!("Session was disconnected by administrator - uninstalling");
if let Err(e) = startup::uninstall() {
warn!("Uninstall failed: {}", e);
}
show_message_box(
"Remote Session Ended",
"The remote support session has been ended by the administrator.\n\nThe agent will be removed from this computer.",
);
return Ok(());
}
error!("Session error: {}", e);
}
}

View File

@@ -340,10 +340,14 @@ impl SessionManager {
Some(message::Payload::Disconnect(disc)) => {
tracing::info!("Disconnect requested: {}", disc.reason);
// Check if this is a cancellation
// Check if this is a cancellation (support session)
if disc.reason.contains("cancelled") {
return Err(anyhow::anyhow!("SESSION_CANCELLED: {}", disc.reason));
}
// Check if this is an admin disconnect (persistent session)
if disc.reason.contains("administrator") || disc.reason.contains("Disconnected") {
return Err(anyhow::anyhow!("ADMIN_DISCONNECT: {}", disc.reason));
}
return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
}

View File

@@ -134,6 +134,49 @@ pub fn remove_from_startup() -> Result<()> {
Ok(())
}
/// Full uninstall: remove from startup and delete the executable
#[cfg(windows)]
pub fn uninstall() -> Result<()> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_DELAY_UNTIL_REBOOT};
info!("Uninstalling agent");
// First remove from startup
let _ = remove_from_startup();
// Get the path to the current executable
let exe_path = std::env::current_exe()?;
let exe_path_str = exe_path.to_string_lossy();
info!("Scheduling deletion of: {}", exe_path_str);
// Convert path to wide string
let exe_wide: Vec<u16> = OsStr::new(&*exe_path_str)
.encode_wide()
.chain(std::iter::once(0))
.collect();
// Schedule the file for deletion on next reboot
// This is necessary because the executable is currently running
unsafe {
let result = MoveFileExW(
PCWSTR(exe_wide.as_ptr()),
PCWSTR::null(),
MOVEFILE_DELAY_UNTIL_REBOOT,
);
if result.is_err() {
warn!("Failed to schedule file deletion: {:?}. File may need manual removal.", result);
} else {
info!("Executable scheduled for deletion on reboot");
}
}
Ok(())
}
#[cfg(not(windows))]
pub fn add_to_startup() -> Result<()> {
warn!("Startup persistence not implemented for this platform");
@@ -144,3 +187,9 @@ pub fn add_to_startup() -> Result<()> {
pub fn remove_from_startup() -> Result<()> {
Ok(())
}
#[cfg(not(windows))]
pub fn uninstall() -> Result<()> {
warn!("Uninstall not implemented for this platform");
Ok(())
}