Add VPN configuration tools and agent documentation
Created comprehensive VPN setup tooling for Peaceful Spirit L2TP/IPsec connection and enhanced agent documentation framework. VPN Configuration (PST-NW-VPN): - Setup-PST-L2TP-VPN.ps1: Automated L2TP/IPsec setup with split-tunnel and DNS - Connect-PST-VPN.ps1: Connection helper with PPP adapter detection, DNS (192.168.0.2), and route config (192.168.0.0/24) - Connect-PST-VPN-Standalone.ps1: Self-contained connection script for remote deployment - Fix-PST-VPN-Auth.ps1: Authentication troubleshooting for CHAP/MSChapv2 - Diagnose-VPN-Interface.ps1: Comprehensive VPN interface and routing diagnostic - Quick-Test-VPN.ps1: Fast connectivity verification (DNS/router/routes) - Add-PST-VPN-Route-Manual.ps1: Manual route configuration helper - vpn-connect.bat, vpn-disconnect.bat: Simple batch file shortcuts - OpenVPN config files (Windows-compatible, abandoned for L2TP) Key VPN Implementation Details: - L2TP creates PPP adapter with connection name as interface description - UniFi auto-configures DNS (192.168.0.2) but requires manual route to 192.168.0.0/24 - Split-tunnel enabled (only remote traffic through VPN) - All-user connection for pre-login auto-connect via scheduled task - Authentication: CHAP + MSChapv2 for UniFi compatibility Agent Documentation: - AGENT_QUICK_REFERENCE.md: Quick reference for all specialized agents - documentation-squire.md: Documentation and task management specialist agent - Updated all agent markdown files with standardized formatting Project Organization: - Moved conversation logs to dedicated directories (guru-connect-conversation-logs, guru-rmm-conversation-logs) - Cleaned up old session JSONL files from projects/msp-tools/ - Added guru-connect infrastructure (agent, dashboard, proto, scripts, .gitea workflows) - Added guru-rmm server components and deployment configs Technical Notes: - VPN IP pool: 192.168.4.x (client gets 192.168.4.6) - Remote network: 192.168.0.0/24 (router at 192.168.0.10) - PSK: rrClvnmUeXEFo90Ol+z7tfsAZHeSK6w7 - Credentials: pst-admin / 24Hearts$ Files: 15 VPN scripts, 2 agent docs, conversation log reorganization, guru-connect/guru-rmm infrastructure additions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
299
projects/msp-tools/guru-rmm/agent/src/transport/mod.rs
Normal file
299
projects/msp-tools/guru-rmm/agent/src/transport/mod.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
//! Transport layer for agent-server communication
|
||||
//!
|
||||
//! Handles WebSocket connection to the GuruRMM server with:
|
||||
//! - Auto-reconnection on disconnect
|
||||
//! - Authentication via API key
|
||||
//! - Sending metrics and receiving commands
|
||||
//! - Heartbeat to maintain connection
|
||||
|
||||
mod websocket;
|
||||
|
||||
pub use websocket::WebSocketClient;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Messages sent from agent to server
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AgentMessage {
|
||||
/// Authentication message (sent on connect)
|
||||
Auth(AuthPayload),
|
||||
|
||||
/// Metrics report
|
||||
Metrics(crate::metrics::SystemMetrics),
|
||||
|
||||
/// Network state update (sent on connect and when interfaces change)
|
||||
NetworkState(crate::metrics::NetworkState),
|
||||
|
||||
/// Command execution result
|
||||
CommandResult(CommandResultPayload),
|
||||
|
||||
/// Watchdog event (service stopped, restarted, etc.)
|
||||
WatchdogEvent(WatchdogEventPayload),
|
||||
|
||||
/// Update result (success, failure, rollback)
|
||||
UpdateResult(UpdateResultPayload),
|
||||
|
||||
/// Heartbeat to keep connection alive
|
||||
Heartbeat,
|
||||
}
|
||||
|
||||
/// Authentication payload
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthPayload {
|
||||
/// API key for this agent (or site)
|
||||
pub api_key: String,
|
||||
|
||||
/// Unique device identifier (hardware-derived)
|
||||
pub device_id: String,
|
||||
|
||||
/// Hostname of this machine
|
||||
pub hostname: String,
|
||||
|
||||
/// Operating system type
|
||||
pub os_type: String,
|
||||
|
||||
/// Operating system version
|
||||
pub os_version: String,
|
||||
|
||||
/// Agent version
|
||||
pub agent_version: String,
|
||||
|
||||
/// Architecture (amd64, arm64, etc.)
|
||||
#[serde(default = "default_arch")]
|
||||
pub architecture: String,
|
||||
|
||||
/// Previous version if reconnecting after update
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub previous_version: Option<String>,
|
||||
|
||||
/// Update ID if reconnecting after update
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pending_update_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
fn default_arch() -> String {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{ "amd64".to_string() }
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{ "arm64".to_string() }
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||
{ "unknown".to_string() }
|
||||
}
|
||||
|
||||
/// Command execution result payload
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandResultPayload {
|
||||
/// Command ID (from the server)
|
||||
pub command_id: Uuid,
|
||||
|
||||
/// Exit code (0 = success)
|
||||
pub exit_code: i32,
|
||||
|
||||
/// Standard output
|
||||
pub stdout: String,
|
||||
|
||||
/// Standard error
|
||||
pub stderr: String,
|
||||
|
||||
/// Execution duration in milliseconds
|
||||
pub duration_ms: u64,
|
||||
}
|
||||
|
||||
/// Watchdog event payload
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatchdogEventPayload {
|
||||
/// Service or process name
|
||||
pub name: String,
|
||||
|
||||
/// Event type
|
||||
pub event: WatchdogEvent,
|
||||
|
||||
/// Additional details
|
||||
pub details: Option<String>,
|
||||
}
|
||||
|
||||
/// Types of watchdog events
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WatchdogEvent {
|
||||
/// Service/process was found stopped
|
||||
Stopped,
|
||||
|
||||
/// Service/process was restarted by the agent
|
||||
Restarted,
|
||||
|
||||
/// Restart attempt failed
|
||||
RestartFailed,
|
||||
|
||||
/// Max restart attempts reached
|
||||
MaxRestartsReached,
|
||||
|
||||
/// Service/process recovered on its own
|
||||
Recovered,
|
||||
}
|
||||
|
||||
/// Messages sent from server to agent
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "payload")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ServerMessage {
|
||||
/// Authentication acknowledgment
|
||||
AuthAck(AuthAckPayload),
|
||||
|
||||
/// Command to execute
|
||||
Command(CommandPayload),
|
||||
|
||||
/// Configuration update
|
||||
ConfigUpdate(ConfigUpdatePayload),
|
||||
|
||||
/// Agent update command
|
||||
Update(UpdatePayload),
|
||||
|
||||
/// Acknowledgment of received message
|
||||
Ack { message_id: Option<String> },
|
||||
|
||||
/// Error message
|
||||
Error { code: String, message: String },
|
||||
}
|
||||
|
||||
/// Authentication acknowledgment payload
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthAckPayload {
|
||||
/// Whether authentication was successful
|
||||
pub success: bool,
|
||||
|
||||
/// Agent ID assigned by server
|
||||
pub agent_id: Option<Uuid>,
|
||||
|
||||
/// Error message if authentication failed
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Command payload from server
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandPayload {
|
||||
/// Unique command ID
|
||||
pub id: Uuid,
|
||||
|
||||
/// Type of command
|
||||
pub command_type: CommandType,
|
||||
|
||||
/// Command text to execute
|
||||
pub command: String,
|
||||
|
||||
/// Optional timeout in seconds
|
||||
pub timeout_seconds: Option<u64>,
|
||||
|
||||
/// Whether to run as elevated/admin
|
||||
pub elevated: bool,
|
||||
}
|
||||
|
||||
/// Types of commands
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommandType {
|
||||
/// Shell command (cmd on Windows, bash on Unix)
|
||||
Shell,
|
||||
|
||||
/// PowerShell command (Windows)
|
||||
PowerShell,
|
||||
|
||||
/// Python script
|
||||
Python,
|
||||
|
||||
/// Raw script (requires interpreter path)
|
||||
Script { interpreter: String },
|
||||
}
|
||||
|
||||
/// Configuration update payload
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigUpdatePayload {
|
||||
/// New metrics interval (if changed)
|
||||
pub metrics_interval_seconds: Option<u64>,
|
||||
|
||||
/// Updated watchdog config
|
||||
pub watchdog: Option<WatchdogConfigUpdate>,
|
||||
}
|
||||
|
||||
/// Watchdog configuration update
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatchdogConfigUpdate {
|
||||
/// Enable/disable watchdog
|
||||
pub enabled: Option<bool>,
|
||||
|
||||
/// Check interval
|
||||
pub check_interval_seconds: Option<u64>,
|
||||
|
||||
// Services and processes would be included here for remote config updates
|
||||
}
|
||||
|
||||
/// Update command payload from server
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdatePayload {
|
||||
/// Unique update ID for tracking
|
||||
pub update_id: Uuid,
|
||||
|
||||
/// Target version to update to
|
||||
pub target_version: String,
|
||||
|
||||
/// Download URL for the new binary
|
||||
pub download_url: String,
|
||||
|
||||
/// SHA256 checksum of the binary
|
||||
pub checksum_sha256: String,
|
||||
|
||||
/// Whether to force update (skip version check)
|
||||
#[serde(default)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// Update result payload sent back to server
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateResultPayload {
|
||||
/// Update ID (from the server)
|
||||
pub update_id: Uuid,
|
||||
|
||||
/// Update status
|
||||
pub status: UpdateStatus,
|
||||
|
||||
/// Old version before update
|
||||
pub old_version: String,
|
||||
|
||||
/// New version after update (if successful)
|
||||
pub new_version: Option<String>,
|
||||
|
||||
/// Error message if failed
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Update status codes
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UpdateStatus {
|
||||
/// Update starting
|
||||
Starting,
|
||||
|
||||
/// Downloading new binary
|
||||
Downloading,
|
||||
|
||||
/// Download complete, verifying
|
||||
Verifying,
|
||||
|
||||
/// Installing (replacing binary)
|
||||
Installing,
|
||||
|
||||
/// Restarting service
|
||||
Restarting,
|
||||
|
||||
/// Update completed successfully
|
||||
Completed,
|
||||
|
||||
/// Update failed
|
||||
Failed,
|
||||
|
||||
/// Rolled back to previous version
|
||||
RolledBack,
|
||||
}
|
||||
439
projects/msp-tools/guru-rmm/agent/src/transport/websocket.rs
Normal file
439
projects/msp-tools/guru-rmm/agent/src/transport/websocket.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
//! WebSocket client for server communication
|
||||
//!
|
||||
//! Handles the WebSocket connection lifecycle including:
|
||||
//! - Connection establishment
|
||||
//! - Authentication handshake
|
||||
//! - Message sending/receiving
|
||||
//! - Heartbeat maintenance
|
||||
//! - Command handling
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{interval, timeout};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::{AgentMessage, AuthPayload, CommandPayload, ServerMessage, UpdatePayload, UpdateResultPayload, UpdateStatus};
|
||||
use crate::metrics::NetworkState;
|
||||
use crate::updater::{AgentUpdater, UpdaterConfig};
|
||||
use crate::AppState;
|
||||
|
||||
/// WebSocket client for communicating with the GuruRMM server
|
||||
pub struct WebSocketClient;
|
||||
|
||||
impl WebSocketClient {
|
||||
/// Connect to the server and run the message loop
|
||||
///
|
||||
/// This function will return when the connection is closed or an error occurs.
|
||||
/// The caller should handle reconnection logic.
|
||||
pub async fn connect_and_run(state: Arc<AppState>) -> Result<()> {
|
||||
let url = &state.config.server.url;
|
||||
|
||||
// Connect to WebSocket server
|
||||
info!("Connecting to {}", url);
|
||||
let (ws_stream, response) = connect_async(url)
|
||||
.await
|
||||
.context("Failed to connect to WebSocket server")?;
|
||||
|
||||
info!(
|
||||
"WebSocket connected (HTTP status: {})",
|
||||
response.status()
|
||||
);
|
||||
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Check for pending update (from previous update attempt)
|
||||
let updater_config = UpdaterConfig::default();
|
||||
let pending_update = AgentUpdater::load_pending_update(&updater_config).await;
|
||||
|
||||
// If we have pending update info, we just restarted after an update
|
||||
let (previous_version, pending_update_id) = if let Some(ref info) = pending_update {
|
||||
info!(
|
||||
"Found pending update info: {} -> {} (id: {})",
|
||||
info.old_version, info.target_version, info.update_id
|
||||
);
|
||||
(Some(info.old_version.clone()), Some(info.update_id))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Send authentication message
|
||||
let auth_msg = AgentMessage::Auth(AuthPayload {
|
||||
api_key: state.config.server.api_key.clone(),
|
||||
device_id: crate::device_id::get_device_id(),
|
||||
hostname: state.config.get_hostname(),
|
||||
os_type: std::env::consts::OS.to_string(),
|
||||
os_version: sysinfo::System::os_version().unwrap_or_else(|| "unknown".to_string()),
|
||||
agent_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
architecture: Self::get_architecture().to_string(),
|
||||
previous_version,
|
||||
pending_update_id,
|
||||
});
|
||||
|
||||
let auth_json = serde_json::to_string(&auth_msg)?;
|
||||
write.send(Message::Text(auth_json)).await?;
|
||||
debug!("Sent authentication message");
|
||||
|
||||
// Wait for auth response with timeout
|
||||
let auth_response = timeout(Duration::from_secs(10), read.next())
|
||||
.await
|
||||
.context("Authentication timeout")?
|
||||
.ok_or_else(|| anyhow::anyhow!("Connection closed before auth response"))?
|
||||
.context("Failed to receive auth response")?;
|
||||
|
||||
// Parse auth response
|
||||
if let Message::Text(text) = auth_response {
|
||||
let server_msg: ServerMessage =
|
||||
serde_json::from_str(&text).context("Failed to parse auth response")?;
|
||||
|
||||
match server_msg {
|
||||
ServerMessage::AuthAck(ack) => {
|
||||
if ack.success {
|
||||
info!("Authentication successful, agent_id: {:?}", ack.agent_id);
|
||||
*state.connected.write().await = true;
|
||||
|
||||
// Send initial network state immediately after auth
|
||||
let network_state = NetworkState::collect();
|
||||
info!(
|
||||
"Sending initial network state ({} interfaces)",
|
||||
network_state.interfaces.len()
|
||||
);
|
||||
let network_msg = AgentMessage::NetworkState(network_state);
|
||||
let network_json = serde_json::to_string(&network_msg)?;
|
||||
write.send(Message::Text(network_json)).await?;
|
||||
} else {
|
||||
error!("Authentication failed: {:?}", ack.error);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Authentication failed: {}",
|
||||
ack.error.unwrap_or_else(|| "Unknown error".to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
ServerMessage::Error { code, message } => {
|
||||
error!("Server error during auth: {} - {}", code, message);
|
||||
return Err(anyhow::anyhow!("Server error: {} - {}", code, message));
|
||||
}
|
||||
_ => {
|
||||
warn!("Unexpected message during auth: {:?}", server_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create channel for outgoing messages
|
||||
let (tx, mut rx) = mpsc::channel::<AgentMessage>(100);
|
||||
|
||||
// Spawn metrics sender task
|
||||
let metrics_tx = tx.clone();
|
||||
let metrics_state = Arc::clone(&state);
|
||||
let metrics_interval = state.config.metrics.interval_seconds;
|
||||
|
||||
let metrics_task = tokio::spawn(async move {
|
||||
let mut timer = interval(Duration::from_secs(metrics_interval));
|
||||
|
||||
loop {
|
||||
timer.tick().await;
|
||||
|
||||
let metrics = metrics_state.metrics_collector.collect().await;
|
||||
if metrics_tx.send(AgentMessage::Metrics(metrics)).await.is_err() {
|
||||
debug!("Metrics channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn network state monitor task (checks for changes every 30 seconds)
|
||||
let network_tx = tx.clone();
|
||||
let network_task = tokio::spawn(async move {
|
||||
// Check for network changes every 30 seconds
|
||||
let mut timer = interval(Duration::from_secs(30));
|
||||
let mut last_state = NetworkState::collect();
|
||||
|
||||
loop {
|
||||
timer.tick().await;
|
||||
|
||||
let current_state = NetworkState::collect();
|
||||
if current_state.has_changed(&last_state) {
|
||||
info!(
|
||||
"Network state changed (hash: {} -> {}), sending update",
|
||||
last_state.state_hash, current_state.state_hash
|
||||
);
|
||||
|
||||
// Log the changes for debugging
|
||||
for iface in ¤t_state.interfaces {
|
||||
debug!(
|
||||
" Interface {}: IPv4={:?}",
|
||||
iface.name, iface.ipv4_addresses
|
||||
);
|
||||
}
|
||||
|
||||
if network_tx
|
||||
.send(AgentMessage::NetworkState(current_state.clone()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
debug!("Network channel closed");
|
||||
break;
|
||||
}
|
||||
last_state = current_state;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn heartbeat task
|
||||
let heartbeat_tx = tx.clone();
|
||||
let heartbeat_task = tokio::spawn(async move {
|
||||
let mut timer = interval(Duration::from_secs(30));
|
||||
|
||||
loop {
|
||||
timer.tick().await;
|
||||
|
||||
if heartbeat_tx.send(AgentMessage::Heartbeat).await.is_err() {
|
||||
debug!("Heartbeat channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Main message loop
|
||||
let result: Result<()> = loop {
|
||||
tokio::select! {
|
||||
// Handle outgoing messages
|
||||
Some(msg) = rx.recv() => {
|
||||
let json = serde_json::to_string(&msg)?;
|
||||
if let Err(e) = write.send(Message::Text(json)).await {
|
||||
break Err(e.into());
|
||||
}
|
||||
|
||||
match &msg {
|
||||
AgentMessage::Metrics(m) => {
|
||||
debug!("Sent metrics: CPU={:.1}%", m.cpu_percent);
|
||||
}
|
||||
AgentMessage::NetworkState(n) => {
|
||||
debug!("Sent network state: {} interfaces, hash={}",
|
||||
n.interfaces.len(), n.state_hash);
|
||||
}
|
||||
AgentMessage::Heartbeat => {
|
||||
debug!("Sent heartbeat");
|
||||
}
|
||||
_ => {
|
||||
debug!("Sent message: {:?}", std::mem::discriminant(&msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming messages
|
||||
Some(msg_result) = read.next() => {
|
||||
match msg_result {
|
||||
Ok(Message::Text(text)) => {
|
||||
if let Err(e) = Self::handle_server_message(&text, &tx).await {
|
||||
error!("Error handling message: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(Message::Ping(data)) => {
|
||||
if let Err(e) = write.send(Message::Pong(data)).await {
|
||||
break Err(e.into());
|
||||
}
|
||||
}
|
||||
Ok(Message::Pong(_)) => {
|
||||
debug!("Received pong");
|
||||
}
|
||||
Ok(Message::Close(frame)) => {
|
||||
info!("Server closed connection: {:?}", frame);
|
||||
break Ok(());
|
||||
}
|
||||
Ok(Message::Binary(_)) => {
|
||||
warn!("Received unexpected binary message");
|
||||
}
|
||||
Ok(Message::Frame(_)) => {
|
||||
// Raw frame, usually not seen
|
||||
}
|
||||
Err(e) => {
|
||||
error!("WebSocket error: {}", e);
|
||||
break Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection timeout (no activity)
|
||||
_ = tokio::time::sleep(Duration::from_secs(90)) => {
|
||||
warn!("Connection timeout, no activity for 90 seconds");
|
||||
break Err(anyhow::anyhow!("Connection timeout"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
metrics_task.abort();
|
||||
network_task.abort();
|
||||
heartbeat_task.abort();
|
||||
*state.connected.write().await = false;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Handle a message received from the server
|
||||
async fn handle_server_message(
|
||||
text: &str,
|
||||
tx: &mpsc::Sender<AgentMessage>,
|
||||
) -> Result<()> {
|
||||
let msg: ServerMessage =
|
||||
serde_json::from_str(text).context("Failed to parse server message")?;
|
||||
|
||||
match msg {
|
||||
ServerMessage::Command(cmd) => {
|
||||
info!("Received command: {:?} (id: {})", cmd.command_type, cmd.id);
|
||||
Self::execute_command(cmd, tx.clone()).await;
|
||||
}
|
||||
ServerMessage::ConfigUpdate(update) => {
|
||||
info!("Received config update: {:?}", update);
|
||||
// Config updates will be handled in a future phase
|
||||
}
|
||||
ServerMessage::Ack { message_id } => {
|
||||
debug!("Received ack for message: {:?}", message_id);
|
||||
}
|
||||
ServerMessage::AuthAck(_) => {
|
||||
// Already handled during initial auth
|
||||
}
|
||||
ServerMessage::Error { code, message } => {
|
||||
error!("Server error: {} - {}", code, message);
|
||||
}
|
||||
ServerMessage::Update(payload) => {
|
||||
info!(
|
||||
"Received update command: {} -> {} (id: {})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
payload.target_version,
|
||||
payload.update_id
|
||||
);
|
||||
Self::handle_update(payload, tx.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle an update command from the server
|
||||
async fn handle_update(payload: UpdatePayload, tx: mpsc::Sender<AgentMessage>) {
|
||||
// Send starting status
|
||||
let starting_result = UpdateResultPayload {
|
||||
update_id: payload.update_id,
|
||||
status: UpdateStatus::Starting,
|
||||
old_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
new_version: None,
|
||||
error: None,
|
||||
};
|
||||
let _ = tx.send(AgentMessage::UpdateResult(starting_result)).await;
|
||||
|
||||
// Spawn update in background (it will restart the service)
|
||||
tokio::spawn(async move {
|
||||
let config = UpdaterConfig::default();
|
||||
let updater = AgentUpdater::new(config);
|
||||
let result = updater.perform_update(payload).await;
|
||||
|
||||
// If we reach here, the update failed (successful update restarts the process)
|
||||
let _ = tx.send(AgentMessage::UpdateResult(result)).await;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the current architecture
|
||||
fn get_architecture() -> &'static str {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{ "amd64" }
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{ "arm64" }
|
||||
#[cfg(target_arch = "x86")]
|
||||
{ "386" }
|
||||
#[cfg(target_arch = "arm")]
|
||||
{ "arm" }
|
||||
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "x86", target_arch = "arm")))]
|
||||
{ "unknown" }
|
||||
}
|
||||
|
||||
/// Execute a command received from the server
|
||||
async fn execute_command(cmd: CommandPayload, tx: mpsc::Sender<AgentMessage>) {
|
||||
let command_id = cmd.id;
|
||||
|
||||
// Spawn command execution in background
|
||||
tokio::spawn(async move {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let result = Self::run_command(&cmd).await;
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
let (exit_code, stdout, stderr) = match result {
|
||||
Ok((code, out, err)) => (code, out, err),
|
||||
Err(e) => (-1, String::new(), format!("Execution error: {}", e)),
|
||||
};
|
||||
|
||||
let result_msg = AgentMessage::CommandResult(super::CommandResultPayload {
|
||||
command_id,
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
duration_ms,
|
||||
});
|
||||
|
||||
if tx.send(result_msg).await.is_err() {
|
||||
error!("Failed to send command result");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Run a command and capture output
|
||||
async fn run_command(cmd: &CommandPayload) -> Result<(i32, String, String)> {
|
||||
use tokio::process::Command;
|
||||
|
||||
let timeout_secs = cmd.timeout_seconds.unwrap_or(300); // 5 minute default
|
||||
|
||||
let mut command = match &cmd.command_type {
|
||||
super::CommandType::Shell => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut c = Command::new("cmd");
|
||||
c.args(["/C", &cmd.command]);
|
||||
c
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut c = Command::new("sh");
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
}
|
||||
super::CommandType::PowerShell => {
|
||||
let mut c = Command::new("powershell");
|
||||
c.args(["-NoProfile", "-NonInteractive", "-Command", &cmd.command]);
|
||||
c
|
||||
}
|
||||
super::CommandType::Python => {
|
||||
let mut c = Command::new("python");
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
super::CommandType::Script { interpreter } => {
|
||||
let mut c = Command::new(interpreter);
|
||||
c.args(["-c", &cmd.command]);
|
||||
c
|
||||
}
|
||||
};
|
||||
|
||||
// Capture output
|
||||
command.stdout(std::process::Stdio::piped());
|
||||
command.stderr(std::process::Stdio::piped());
|
||||
|
||||
// Execute with timeout
|
||||
let output = timeout(Duration::from_secs(timeout_secs), command.output())
|
||||
.await
|
||||
.context("Command timeout")?
|
||||
.context("Failed to execute command")?;
|
||||
|
||||
let exit_code = output.status.code().unwrap_or(-1);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user