Add machine deletion API with uninstall command support
- Add AdminCommand message to protobuf (uninstall, restart, update) - Add DELETE /api/machines/:agent_id endpoint with options: - ?uninstall=true - send uninstall command to online agent - ?export=true - return session history before deletion - Add GET /api/machines/:agent_id/history endpoint for history export - Add GET /api/machines endpoint to list all machines - Handle AdminCommand in agent session handler - Handle ADMIN_UNINSTALL error in agent main loop to trigger uninstall 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -416,9 +416,23 @@ async fn run_agent(config: config::Config) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if error_msg.contains("ADMIN_UNINSTALL") {
|
||||||
|
info!("Uninstall command received from server - uninstalling");
|
||||||
|
if let Err(e) = startup::uninstall() {
|
||||||
|
warn!("Uninstall failed: {}", e);
|
||||||
|
}
|
||||||
|
show_message_box("GuruConnect Removed", "This computer has been removed from remote management.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if error_msg.contains("ADMIN_RESTART") {
|
||||||
|
info!("Restart command received - will reconnect");
|
||||||
|
// Don't exit, just let the loop continue to reconnect
|
||||||
|
} else {
|
||||||
error!("Session error: {}", e);
|
error!("Session error: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_msg = e.to_string();
|
let error_msg = e.to_string();
|
||||||
|
|
||||||
|
|||||||
@@ -452,6 +452,30 @@ impl SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(message::Payload::AdminCommand(cmd)) => {
|
||||||
|
use crate::proto::AdminCommandType;
|
||||||
|
tracing::info!("Admin command received: {:?} - {}", cmd.command, cmd.reason);
|
||||||
|
|
||||||
|
match AdminCommandType::try_from(cmd.command).ok() {
|
||||||
|
Some(AdminCommandType::AdminUninstall) => {
|
||||||
|
tracing::warn!("Uninstall command received from server");
|
||||||
|
// Return special error to trigger uninstall in main loop
|
||||||
|
return Err(anyhow::anyhow!("ADMIN_UNINSTALL: {}", cmd.reason));
|
||||||
|
}
|
||||||
|
Some(AdminCommandType::AdminRestart) => {
|
||||||
|
tracing::info!("Restart command received from server");
|
||||||
|
// For now, just disconnect - the auto-restart logic will handle it
|
||||||
|
return Err(anyhow::anyhow!("ADMIN_RESTART: {}", cmd.reason));
|
||||||
|
}
|
||||||
|
Some(AdminCommandType::AdminUpdate) => {
|
||||||
|
tracing::info!("Update command received (not implemented)");
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing::warn!("Unknown admin command: {}", cmd.command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some(message::Payload::Disconnect(disc)) => {
|
Some(message::Payload::Disconnect(disc)) => {
|
||||||
tracing::info!("Disconnect requested: {}", disc.reason);
|
tracing::info!("Disconnect requested: {}", disc.reason);
|
||||||
if disc.reason.contains("cancelled") {
|
if disc.reason.contains("cancelled") {
|
||||||
|
|||||||
@@ -278,6 +278,18 @@ message AgentStatus {
|
|||||||
bool is_streaming = 6;
|
bool is_streaming = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server commands agent to uninstall itself
|
||||||
|
message AdminCommand {
|
||||||
|
AdminCommandType command = 1;
|
||||||
|
string reason = 2; // Why the command was issued
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AdminCommandType {
|
||||||
|
ADMIN_UNINSTALL = 0; // Uninstall agent and remove from startup
|
||||||
|
ADMIN_RESTART = 1; // Restart the agent process
|
||||||
|
ADMIN_UPDATE = 2; // Download and install update (future)
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Top-Level Message Wrapper
|
// Top-Level Message Wrapper
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -320,5 +332,8 @@ message Message {
|
|||||||
|
|
||||||
// Chat
|
// Chat
|
||||||
ChatMessage chat_message = 60;
|
ChatMessage chat_message = 60;
|
||||||
|
|
||||||
|
// Admin commands (server -> agent)
|
||||||
|
AdminCommand admin_command = 70;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
//! REST API endpoints
|
//! REST API endpoints
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State, Query},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::session::SessionManager;
|
use crate::session::SessionManager;
|
||||||
|
use crate::db;
|
||||||
|
|
||||||
/// Viewer info returned by API
|
/// Viewer info returned by API
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -88,3 +89,120 @@ pub async fn get_session(
|
|||||||
|
|
||||||
Ok(Json(SessionInfo::from(session)))
|
Ok(Json(SessionInfo::from(session)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Machine API Types
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Machine info returned by API
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct MachineInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub agent_id: String,
|
||||||
|
pub hostname: String,
|
||||||
|
pub os_version: Option<String>,
|
||||||
|
pub is_elevated: bool,
|
||||||
|
pub is_persistent: bool,
|
||||||
|
pub first_seen: String,
|
||||||
|
pub last_seen: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<db::machines::Machine> for MachineInfo {
|
||||||
|
fn from(m: db::machines::Machine) -> Self {
|
||||||
|
Self {
|
||||||
|
id: m.id.to_string(),
|
||||||
|
agent_id: m.agent_id,
|
||||||
|
hostname: m.hostname,
|
||||||
|
os_version: m.os_version,
|
||||||
|
is_elevated: m.is_elevated,
|
||||||
|
is_persistent: m.is_persistent,
|
||||||
|
first_seen: m.first_seen.to_rfc3339(),
|
||||||
|
last_seen: m.last_seen.to_rfc3339(),
|
||||||
|
status: m.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Session record for history
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct SessionRecord {
|
||||||
|
pub id: String,
|
||||||
|
pub started_at: String,
|
||||||
|
pub ended_at: Option<String>,
|
||||||
|
pub duration_secs: Option<i32>,
|
||||||
|
pub is_support_session: bool,
|
||||||
|
pub support_code: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<db::sessions::DbSession> for SessionRecord {
|
||||||
|
fn from(s: db::sessions::DbSession) -> Self {
|
||||||
|
Self {
|
||||||
|
id: s.id.to_string(),
|
||||||
|
started_at: s.started_at.to_rfc3339(),
|
||||||
|
ended_at: s.ended_at.map(|t| t.to_rfc3339()),
|
||||||
|
duration_secs: s.duration_secs,
|
||||||
|
is_support_session: s.is_support_session,
|
||||||
|
support_code: s.support_code,
|
||||||
|
status: s.status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event record for history
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct EventRecord {
|
||||||
|
pub id: i64,
|
||||||
|
pub session_id: String,
|
||||||
|
pub event_type: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub viewer_id: Option<String>,
|
||||||
|
pub viewer_name: Option<String>,
|
||||||
|
pub details: Option<serde_json::Value>,
|
||||||
|
pub ip_address: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<db::events::SessionEvent> for EventRecord {
|
||||||
|
fn from(e: db::events::SessionEvent) -> Self {
|
||||||
|
Self {
|
||||||
|
id: e.id,
|
||||||
|
session_id: e.session_id.to_string(),
|
||||||
|
event_type: e.event_type,
|
||||||
|
timestamp: e.timestamp.to_rfc3339(),
|
||||||
|
viewer_id: e.viewer_id,
|
||||||
|
viewer_name: e.viewer_name,
|
||||||
|
details: e.details,
|
||||||
|
ip_address: e.ip_address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full machine history (for export)
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct MachineHistory {
|
||||||
|
pub machine: MachineInfo,
|
||||||
|
pub sessions: Vec<SessionRecord>,
|
||||||
|
pub events: Vec<EventRecord>,
|
||||||
|
pub exported_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query parameters for machine deletion
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct DeleteMachineParams {
|
||||||
|
/// If true, send uninstall command to agent (if online)
|
||||||
|
#[serde(default)]
|
||||||
|
pub uninstall: bool,
|
||||||
|
/// If true, include history in response before deletion
|
||||||
|
#[serde(default)]
|
||||||
|
pub export: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response for machine deletion
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DeleteMachineResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub message: String,
|
||||||
|
pub uninstall_sent: bool,
|
||||||
|
pub history: Option<MachineHistory>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,3 +105,22 @@ pub async fn get_events_by_type(
|
|||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all events for a machine (by joining through sessions)
|
||||||
|
pub async fn get_events_for_machine(
|
||||||
|
pool: &PgPool,
|
||||||
|
machine_id: Uuid,
|
||||||
|
) -> Result<Vec<SessionEvent>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, SessionEvent>(
|
||||||
|
r#"
|
||||||
|
SELECT e.id, e.session_id, e.event_type, e.timestamp, e.viewer_id, e.viewer_name, e.details, e.ip_address::text as ip_address
|
||||||
|
FROM connect_session_events e
|
||||||
|
JOIN connect_sessions s ON e.session_id = s.id
|
||||||
|
WHERE s.machine_id = $1
|
||||||
|
ORDER BY e.timestamp DESC
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.bind(machine_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,3 +96,16 @@ pub async fn get_recent_sessions(
|
|||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all sessions for a machine (for history export)
|
||||||
|
pub async fn get_sessions_for_machine(
|
||||||
|
pool: &PgPool,
|
||||||
|
machine_id: Uuid,
|
||||||
|
) -> Result<Vec<DbSession>, sqlx::Error> {
|
||||||
|
sqlx::query_as::<_, DbSession>(
|
||||||
|
"SELECT * FROM connect_sessions WHERE machine_id = $1 ORDER BY started_at DESC"
|
||||||
|
)
|
||||||
|
.bind(machine_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ pub mod proto {
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
routing::{get, post},
|
routing::{get, post, delete},
|
||||||
extract::{Path, State, Json},
|
extract::{Path, State, Json, Query},
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
};
|
};
|
||||||
@@ -122,7 +122,13 @@ async fn main() -> Result<()> {
|
|||||||
// REST API - Sessions
|
// REST API - Sessions
|
||||||
.route("/api/sessions", get(list_sessions))
|
.route("/api/sessions", get(list_sessions))
|
||||||
.route("/api/sessions/:id", get(get_session))
|
.route("/api/sessions/:id", get(get_session))
|
||||||
.route("/api/sessions/:id", axum::routing::delete(disconnect_session))
|
.route("/api/sessions/:id", delete(disconnect_session))
|
||||||
|
|
||||||
|
// REST API - Machines
|
||||||
|
.route("/api/machines", get(list_machines))
|
||||||
|
.route("/api/machines/:agent_id", get(get_machine))
|
||||||
|
.route("/api/machines/:agent_id", delete(delete_machine))
|
||||||
|
.route("/api/machines/:agent_id/history", get(get_machine_history))
|
||||||
|
|
||||||
// HTML page routes (clean URLs)
|
// HTML page routes (clean URLs)
|
||||||
.route("/login", get(serve_login))
|
.route("/login", get(serve_login))
|
||||||
@@ -237,6 +243,129 @@ async fn disconnect_session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Machine API handlers
|
||||||
|
|
||||||
|
async fn list_machines(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<Vec<api::MachineInfo>>, (StatusCode, &'static str)> {
|
||||||
|
let db = state.db.as_ref()
|
||||||
|
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
|
||||||
|
|
||||||
|
let machines = db::machines::get_all_machines(db.pool()).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
|
||||||
|
|
||||||
|
Ok(Json(machines.into_iter().map(api::MachineInfo::from).collect()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_machine(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(agent_id): Path<String>,
|
||||||
|
) -> Result<Json<api::MachineInfo>, (StatusCode, &'static str)> {
|
||||||
|
let db = state.db.as_ref()
|
||||||
|
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
|
||||||
|
|
||||||
|
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
|
||||||
|
.ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
|
||||||
|
|
||||||
|
Ok(Json(api::MachineInfo::from(machine)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_machine_history(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(agent_id): Path<String>,
|
||||||
|
) -> Result<Json<api::MachineHistory>, (StatusCode, &'static str)> {
|
||||||
|
let db = state.db.as_ref()
|
||||||
|
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
|
||||||
|
|
||||||
|
// Get machine
|
||||||
|
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
|
||||||
|
.ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
|
||||||
|
|
||||||
|
// Get sessions for this machine
|
||||||
|
let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
|
||||||
|
|
||||||
|
// Get events for this machine
|
||||||
|
let events = db::events::get_events_for_machine(db.pool(), machine.id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
|
||||||
|
|
||||||
|
let history = api::MachineHistory {
|
||||||
|
machine: api::MachineInfo::from(machine),
|
||||||
|
sessions: sessions.into_iter().map(api::SessionRecord::from).collect(),
|
||||||
|
events: events.into_iter().map(api::EventRecord::from).collect(),
|
||||||
|
exported_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(history))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_machine(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(agent_id): Path<String>,
|
||||||
|
Query(params): Query<api::DeleteMachineParams>,
|
||||||
|
) -> Result<Json<api::DeleteMachineResponse>, (StatusCode, &'static str)> {
|
||||||
|
let db = state.db.as_ref()
|
||||||
|
.ok_or((StatusCode::SERVICE_UNAVAILABLE, "Database not available"))?;
|
||||||
|
|
||||||
|
// Get machine first
|
||||||
|
let machine = db::machines::get_machine_by_agent_id(db.pool(), &agent_id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?
|
||||||
|
.ok_or((StatusCode::NOT_FOUND, "Machine not found"))?;
|
||||||
|
|
||||||
|
// Export history if requested
|
||||||
|
let history = if params.export {
|
||||||
|
let sessions = db::sessions::get_sessions_for_machine(db.pool(), machine.id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
|
||||||
|
let events = db::events::get_events_for_machine(db.pool(), machine.id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error"))?;
|
||||||
|
|
||||||
|
Some(api::MachineHistory {
|
||||||
|
machine: api::MachineInfo::from(machine.clone()),
|
||||||
|
sessions: sessions.into_iter().map(api::SessionRecord::from).collect(),
|
||||||
|
events: events.into_iter().map(api::EventRecord::from).collect(),
|
||||||
|
exported_at: chrono::Utc::now().to_rfc3339(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send uninstall command if requested and agent is online
|
||||||
|
let mut uninstall_sent = false;
|
||||||
|
if params.uninstall {
|
||||||
|
// Find session for this agent
|
||||||
|
if let Some(session) = state.sessions.get_session_by_agent(&agent_id).await {
|
||||||
|
if session.is_online {
|
||||||
|
uninstall_sent = state.sessions.send_admin_command(
|
||||||
|
session.id,
|
||||||
|
proto::AdminCommandType::AdminUninstall,
|
||||||
|
"Deleted by administrator",
|
||||||
|
).await;
|
||||||
|
if uninstall_sent {
|
||||||
|
info!("Sent uninstall command to agent {}", agent_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from session manager
|
||||||
|
state.sessions.remove_agent(&agent_id).await;
|
||||||
|
|
||||||
|
// Delete from database (cascades to sessions and events)
|
||||||
|
db::machines::delete_machine(db.pool(), &agent_id).await
|
||||||
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to delete machine"))?;
|
||||||
|
|
||||||
|
info!("Deleted machine {} (uninstall_sent: {})", agent_id, uninstall_sent);
|
||||||
|
|
||||||
|
Ok(Json(api::DeleteMachineResponse {
|
||||||
|
success: true,
|
||||||
|
message: format!("Machine {} deleted", machine.hostname),
|
||||||
|
uninstall_sent,
|
||||||
|
history,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// Static page handlers
|
// Static page handlers
|
||||||
async fn serve_login() -> impl IntoResponse {
|
async fn serve_login() -> impl IntoResponse {
|
||||||
match tokio::fs::read_to_string("static/login.html").await {
|
match tokio::fs::read_to_string("static/login.html").await {
|
||||||
|
|||||||
@@ -383,6 +383,48 @@ impl SessionManager {
|
|||||||
let sessions = self.sessions.read().await;
|
let sessions = self.sessions.read().await;
|
||||||
sessions.values().map(|s| s.info.clone()).collect()
|
sessions.values().map(|s| s.info.clone()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send an admin command to an agent (uninstall, restart, etc.)
|
||||||
|
/// Returns true if the message was sent successfully
|
||||||
|
pub async fn send_admin_command(&self, session_id: SessionId, command: crate::proto::AdminCommandType, reason: &str) -> bool {
|
||||||
|
let sessions = self.sessions.read().await;
|
||||||
|
if let Some(session_data) = sessions.get(&session_id) {
|
||||||
|
if !session_data.info.is_online {
|
||||||
|
tracing::warn!("Cannot send admin command to offline agent");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::proto;
|
||||||
|
use prost::Message;
|
||||||
|
|
||||||
|
let admin_cmd = proto::Message {
|
||||||
|
payload: Some(proto::message::Payload::AdminCommand(proto::AdminCommand {
|
||||||
|
command: command as i32,
|
||||||
|
reason: reason.to_string(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
if admin_cmd.encode(&mut buf).is_ok() {
|
||||||
|
if session_data.input_tx.send(buf).await.is_ok() {
|
||||||
|
tracing::info!("Sent admin command {:?} to session {}", command, session_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an agent/machine from the session manager (for deletion)
|
||||||
|
/// Returns the agent_id if found
|
||||||
|
pub async fn remove_agent(&self, agent_id: &str) -> Option<SessionId> {
|
||||||
|
let agents = self.agents.read().await;
|
||||||
|
let session_id = agents.get(agent_id).copied()?;
|
||||||
|
drop(agents);
|
||||||
|
|
||||||
|
self.remove_session(session_id).await;
|
||||||
|
Some(session_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SessionManager {
|
impl Default for SessionManager {
|
||||||
|
|||||||
Reference in New Issue
Block a user