feat: initialize full microservices architecture including backend-api, edge-agent, infrastructure backup, and supporting modules
Some checks failed
Build, MSI & Deploy / Build Docker images (push) Failing after 1m46s
Build, MSI & Deploy / Deploy to Dokploy (push) Has been skipped
Build, MSI & Deploy / Build Edge Agent Binaries (push) Failing after 1m11s
Build, MSI & Deploy / Build Edge Agent MSI (push) Has been skipped
Build, MSI & Deploy / Send notification (push) Successful in 0s
Some checks failed
Build, MSI & Deploy / Build Docker images (push) Failing after 1m46s
Build, MSI & Deploy / Deploy to Dokploy (push) Has been skipped
Build, MSI & Deploy / Build Edge Agent Binaries (push) Failing after 1m11s
Build, MSI & Deploy / Build Edge Agent MSI (push) Has been skipped
Build, MSI & Deploy / Send notification (push) Successful in 0s
This commit is contained in:
@@ -24,10 +24,19 @@ mod app {
|
||||
|
||||
const PIPE_NAME: &str = r"\\.\pipe\omnioil-edge-agent";
|
||||
|
||||
// ─── Colores branding OmniOil ──────────────────────────────────────────────
|
||||
const BRAND_GOLD: [u8; 3] = [212, 160, 23]; // #D4A017 — dorado llama
|
||||
const BRAND_RED: [u8; 3] = [231, 76, 60]; // #E74C3C — rojo desconectado
|
||||
const BRAND_BG: [u8; 3] = [26, 26, 46]; // #1A1A2E — fondo oscuro
|
||||
|
||||
struct EdgeTrayApp {
|
||||
tray_icon: Option<TrayIcon>,
|
||||
quit_id: MenuId,
|
||||
test_id: MenuId,
|
||||
test_connectivity_id: MenuId,
|
||||
restart_service_id: MenuId,
|
||||
open_logs_id: MenuId,
|
||||
open_install_id: MenuId,
|
||||
about_id: MenuId,
|
||||
status_label: MenuItem,
|
||||
command_tx: tokio::sync::mpsc::Sender<AgentCommand>,
|
||||
status_rx: tokio::sync::mpsc::Receiver<IpcMessage>,
|
||||
@@ -36,6 +45,7 @@ mod app {
|
||||
animation_tick: usize,
|
||||
last_tick_time: std::time::Instant,
|
||||
connected_mqtt: bool,
|
||||
exe_dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for EdgeTrayApp {
|
||||
@@ -125,7 +135,10 @@ mod app {
|
||||
}
|
||||
}
|
||||
IpcMessage::Notification { title, message } => {
|
||||
println!("[NOTIFICATION] {}: {}", title, message);
|
||||
self.status_label
|
||||
.set_text(format!("{}: {}", title, message));
|
||||
// Restaurar al estado después de 5 segundos
|
||||
// (el próximo StatusUpdate lo restaurará automáticamente)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -148,10 +161,28 @@ mod app {
|
||||
if event.id == self.quit_id {
|
||||
self.tray_icon.take();
|
||||
event_loop.exit();
|
||||
} else if event.id == self.test_id {
|
||||
let _ = self.command_tx.try_send(AgentCommand::Notify(
|
||||
"¡Hola desde el Tray Icon!".to_string(),
|
||||
));
|
||||
} else if event.id == self.test_connectivity_id {
|
||||
let _ = self.command_tx.try_send(AgentCommand::CheckConnectivity);
|
||||
} else if event.id == self.restart_service_id {
|
||||
let _ = self.command_tx.try_send(AgentCommand::RestartService);
|
||||
} else if event.id == self.open_logs_id {
|
||||
let logs_dir = self.exe_dir.join("logs");
|
||||
let _ = std::process::Command::new("explorer.exe")
|
||||
.arg(&logs_dir)
|
||||
.spawn();
|
||||
} else if event.id == self.open_install_id {
|
||||
let _ = std::process::Command::new("explorer.exe")
|
||||
.arg(&self.exe_dir)
|
||||
.spawn();
|
||||
} else if event.id == self.about_id {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let status = if self.connected_mqtt {
|
||||
"MQTT: Conectado"
|
||||
} else {
|
||||
"MQTT: Desconectado"
|
||||
};
|
||||
self.status_label
|
||||
.set_text(format!("OmniOil Edge Agent v{} — {}", version, status));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,20 +192,41 @@ mod app {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
|
||||
// Obtener directorio del ejecutable para acciones locales
|
||||
let exe_dir = std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
|
||||
// Setup Tray Menu
|
||||
let tray_menu = Menu::new();
|
||||
let status_label = MenuItem::new("Estado: Conectando...", false, None);
|
||||
let test_i = MenuItem::new("Probar Notificación", true, None);
|
||||
let test_connectivity_i = MenuItem::new("Probar Conexión", true, None);
|
||||
let restart_service_i = MenuItem::new("Reiniciar Servicio", true, None);
|
||||
let open_logs_i = MenuItem::new("Abrir Logs", true, None);
|
||||
let open_install_i = MenuItem::new("Abrir Carpeta Instalación", true, None);
|
||||
let about_i = MenuItem::new("Acerca de...", true, None);
|
||||
let quit_i = MenuItem::new("Salir", true, None);
|
||||
|
||||
let quit_id = quit_i.id().clone();
|
||||
let test_id = test_i.id().clone();
|
||||
let test_connectivity_id = test_connectivity_i.id().clone();
|
||||
let restart_service_id = restart_service_i.id().clone();
|
||||
let open_logs_id = open_logs_i.id().clone();
|
||||
let open_install_id = open_install_i.id().clone();
|
||||
let about_id = about_i.id().clone();
|
||||
|
||||
tray_menu
|
||||
.append_items(&[
|
||||
&status_label,
|
||||
&test_i,
|
||||
&PredefinedMenuItem::separator(),
|
||||
&test_connectivity_i,
|
||||
&restart_service_i,
|
||||
&PredefinedMenuItem::separator(),
|
||||
&open_logs_i,
|
||||
&open_install_i,
|
||||
&PredefinedMenuItem::separator(),
|
||||
&about_i,
|
||||
&quit_i,
|
||||
])
|
||||
.unwrap();
|
||||
@@ -253,7 +305,11 @@ mod app {
|
||||
let mut app = EdgeTrayApp {
|
||||
tray_icon: Some(tray_icon),
|
||||
quit_id,
|
||||
test_id,
|
||||
test_connectivity_id,
|
||||
restart_service_id,
|
||||
open_logs_id,
|
||||
open_install_id,
|
||||
about_id,
|
||||
status_label,
|
||||
command_tx,
|
||||
status_rx,
|
||||
@@ -261,6 +317,7 @@ mod app {
|
||||
animation_tick: 0,
|
||||
last_tick_time: std::time::Instant::now(),
|
||||
connected_mqtt: false,
|
||||
exe_dir,
|
||||
};
|
||||
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
@@ -278,7 +335,7 @@ mod app {
|
||||
}
|
||||
}
|
||||
|
||||
// Generar icono de estado elegante de 16x16 en memoria
|
||||
// Generar icono de estado elegante de 16x16 en memoria (branding OmniOil)
|
||||
let width = 16;
|
||||
let height = 16;
|
||||
let mut rgba = vec![0u8; (width * height * 4) as usize];
|
||||
@@ -290,23 +347,23 @@ mod app {
|
||||
let is_dot = x >= 11 && x <= 14 && y >= 11 && y <= 14;
|
||||
if is_dot {
|
||||
if connected {
|
||||
// Punto Verde (Conectado)
|
||||
rgba[idx] = 46;
|
||||
rgba[idx + 1] = 204;
|
||||
rgba[idx + 2] = 113;
|
||||
// Punto Dorado OmniOil (Conectado)
|
||||
rgba[idx] = BRAND_GOLD[0];
|
||||
rgba[idx + 1] = BRAND_GOLD[1];
|
||||
rgba[idx + 2] = BRAND_GOLD[2];
|
||||
rgba[idx + 3] = 255;
|
||||
} else {
|
||||
// Punto Rojo (Desconectado)
|
||||
rgba[idx] = 231;
|
||||
rgba[idx + 1] = 76;
|
||||
rgba[idx + 2] = 60;
|
||||
rgba[idx] = BRAND_RED[0];
|
||||
rgba[idx + 1] = BRAND_RED[1];
|
||||
rgba[idx + 2] = BRAND_RED[2];
|
||||
rgba[idx + 3] = 255;
|
||||
}
|
||||
} else {
|
||||
// Fondo gris slate de tema oscuro moderno
|
||||
rgba[idx] = 44;
|
||||
rgba[idx + 1] = 62;
|
||||
rgba[idx + 2] = 80;
|
||||
// Fondo oscuro consistente con branding
|
||||
rgba[idx] = BRAND_BG[0];
|
||||
rgba[idx + 1] = BRAND_BG[1];
|
||||
rgba[idx + 2] = BRAND_BG[2];
|
||||
rgba[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
@@ -336,16 +393,16 @@ mod app {
|
||||
let dist_sq = dx * dx + dy * dy;
|
||||
|
||||
if dist_sq <= 1 {
|
||||
// Punto naranja brillante de animación
|
||||
rgba[idx] = 255;
|
||||
rgba[idx + 1] = 165;
|
||||
rgba[idx + 2] = 0;
|
||||
// Punto dorado brillante de animación (branding OmniOil)
|
||||
rgba[idx] = BRAND_GOLD[0];
|
||||
rgba[idx + 1] = BRAND_GOLD[1];
|
||||
rgba[idx + 2] = BRAND_GOLD[2];
|
||||
rgba[idx + 3] = 255;
|
||||
} else {
|
||||
// Fondo azul oscuro moderno
|
||||
rgba[idx] = 0;
|
||||
rgba[idx + 1] = 50;
|
||||
rgba[idx + 2] = 100;
|
||||
// Fondo oscuro consistente
|
||||
rgba[idx] = BRAND_BG[0];
|
||||
rgba[idx + 1] = BRAND_BG[1];
|
||||
rgba[idx + 2] = BRAND_BG[2];
|
||||
rgba[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,10 +433,7 @@ async fn run_device_collector(spec: DeviceTaskSpec) {
|
||||
let _ = storage.save(&topic, &payload_str).await;
|
||||
} else {
|
||||
let _ = reporter
|
||||
.info(
|
||||
"MQTT",
|
||||
&format!("✅ Data sent for {}", device.name),
|
||||
)
|
||||
.info("MQTT", &format!("✅ Data sent for {}", device.name))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -444,17 +441,13 @@ async fn run_device_collector(spec: DeviceTaskSpec) {
|
||||
let _ = reporter
|
||||
.warn(
|
||||
"COLLECTOR",
|
||||
&format!(
|
||||
"⚠️ No values returned for device {}",
|
||||
device.name
|
||||
),
|
||||
&format!("⚠️ No values returned for device {}", device.name),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg =
|
||||
format!("❌ Error reading from device {}: {}", device.name, e);
|
||||
let msg = format!("❌ Error reading from device {}: {}", device.name, e);
|
||||
tracing::error!("{}", msg);
|
||||
let _ = reporter.error("COLLECTOR", &msg).await;
|
||||
let _ = driver.disconnect().await;
|
||||
|
||||
@@ -53,11 +53,7 @@ impl DeadbandFilter {
|
||||
}
|
||||
Some(entry) => {
|
||||
let change_pct = if entry.last_value == 0.0 {
|
||||
if new_val == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
100.0
|
||||
}
|
||||
if new_val == 0.0 { 0.0 } else { 100.0 }
|
||||
} else {
|
||||
((new_val - entry.last_value) / entry.last_value.abs() * 100.0).abs()
|
||||
};
|
||||
|
||||
@@ -27,4 +27,10 @@ pub enum IpcMessage {
|
||||
pub enum AgentCommand {
|
||||
Restart,
|
||||
Notify(String),
|
||||
/// Detiene y reinicia el servicio Windows OmniOilEdgeAgent via SCM.
|
||||
RestartService,
|
||||
/// Envía un mensaje de prueba al servidor vía MQTT (log reporter) para
|
||||
/// verificar conectividad end-to-end. La latencia es ~RTT MQTT (100-500ms
|
||||
/// local, 1-3s internet).
|
||||
CheckConnectivity,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
#[cfg(windows)]
|
||||
use tokio::net::windows::named_pipe::ServerOptions;
|
||||
|
||||
pub async fn start_ipc_server(receiver: broadcast::Receiver<IpcMessage>) -> anyhow::Result<()> {
|
||||
pub async fn start_ipc_server(
|
||||
receiver: broadcast::Receiver<IpcMessage>,
|
||||
tray_cmd_tx: tokio::sync::mpsc::Sender<AgentCommand>,
|
||||
) -> anyhow::Result<()> {
|
||||
let pipe_name = r"\\.\pipe\omnioil-edge-agent";
|
||||
|
||||
tracing::info!("📡 IPC Server (Named Pipes) listening on {}...", pipe_name);
|
||||
@@ -48,8 +51,9 @@ pub async fn start_ipc_server(receiver: broadcast::Receiver<IpcMessage>) -> anyh
|
||||
}
|
||||
|
||||
let receiver_clone = receiver.resubscribe();
|
||||
let cmd_tx = tray_cmd_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_client(server, receiver_clone).await {
|
||||
if let Err(e) = handle_client(server, receiver_clone, cmd_tx).await {
|
||||
tracing::warn!("⚠️ IPC client disconnected: {}", e);
|
||||
}
|
||||
});
|
||||
@@ -59,6 +63,7 @@ pub async fn start_ipc_server(receiver: broadcast::Receiver<IpcMessage>) -> anyh
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = &receiver;
|
||||
let _ = &tray_cmd_tx;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
|
||||
}
|
||||
}
|
||||
@@ -75,8 +80,8 @@ fn create_pipe_server(
|
||||
use std::ptr::addr_of_mut;
|
||||
use windows_sys::Win32::Foundation::{FALSE, TRUE};
|
||||
use windows_sys::Win32::Security::{
|
||||
InitializeSecurityDescriptor, SetSecurityDescriptorDacl, SECURITY_ATTRIBUTES,
|
||||
SECURITY_DESCRIPTOR,
|
||||
InitializeSecurityDescriptor, SECURITY_ATTRIBUTES, SECURITY_DESCRIPTOR,
|
||||
SetSecurityDescriptorDacl,
|
||||
};
|
||||
// SECURITY_DESCRIPTOR_REVISION siempre vale 1 según el Windows SDK; la
|
||||
// constante fue eliminada en versiones nuevas de windows-sys.
|
||||
@@ -123,6 +128,7 @@ fn create_pipe_server(
|
||||
async fn handle_client(
|
||||
server: tokio::net::windows::named_pipe::NamedPipeServer,
|
||||
mut receiver: broadcast::Receiver<IpcMessage>,
|
||||
tray_cmd_tx: tokio::sync::mpsc::Sender<AgentCommand>,
|
||||
) -> anyhow::Result<()> {
|
||||
let (read_half, mut write_half) = tokio::io::split(server);
|
||||
let mut reader = BufReader::new(read_half);
|
||||
@@ -147,13 +153,21 @@ async fn handle_client(
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) => {
|
||||
if let Ok(IpcMessage::Command(cmd)) = serde_json::from_str(&line) {
|
||||
match cmd {
|
||||
match &cmd {
|
||||
AgentCommand::Restart => tracing::warn!("🔄 Restart requested!"),
|
||||
AgentCommand::Notify(msg) => {
|
||||
let resp = IpcMessage::Notification { title: "Agent".into(), message: msg };
|
||||
let resp = IpcMessage::Notification { title: "Agent".into(), message: msg.clone() };
|
||||
let _ = write_half.write_all((serde_json::to_string(&resp)? + "\n").as_bytes()).await;
|
||||
}
|
||||
AgentCommand::RestartService => {
|
||||
tracing::info!("🔄 RestartService requested from tray icon");
|
||||
}
|
||||
AgentCommand::CheckConnectivity => {
|
||||
tracing::info!("📡 CheckConnectivity requested from tray icon");
|
||||
}
|
||||
}
|
||||
// Reenviar comandos que requieren acción del agente principal
|
||||
let _ = tray_cmd_tx.send(cmd).await;
|
||||
}
|
||||
line.clear();
|
||||
}
|
||||
|
||||
@@ -107,19 +107,19 @@ fn main() -> Result<()> {
|
||||
let opts = rumqttc::MqttOptions::new("crash-reporter", mqtt_host, port);
|
||||
let (client, mut eventloop) = rumqttc::AsyncClient::new(opts, 4);
|
||||
// Dar 2s para conectar y enviar
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
async {
|
||||
let _ = eventloop.poll().await; // ConnAck
|
||||
let _ = client.publish(
|
||||
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
let _ = eventloop.poll().await; // ConnAck
|
||||
let _ = client
|
||||
.publish(
|
||||
&topic,
|
||||
rumqttc::QoS::AtMostOnce,
|
||||
false,
|
||||
serde_json::to_vec(&log_payload).unwrap_or_default(),
|
||||
).await;
|
||||
let _ = eventloop.poll().await; // flush
|
||||
}
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
let _ = eventloop.poll().await; // flush
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -300,8 +300,11 @@ pub async fn run_agent(
|
||||
let (ipc_tx, _) = tokio::sync::broadcast::channel::<ipc_protocol::IpcMessage>(32);
|
||||
let ipc_rx = ipc_tx.subscribe();
|
||||
|
||||
// Canal para comandos desde el tray icon hacia el agente principal
|
||||
let (tray_cmd_tx, mut tray_cmd_rx) = mpsc::channel::<ipc_protocol::AgentCommand>(16);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = ipc_server::start_ipc_server(ipc_rx).await;
|
||||
let _ = ipc_server::start_ipc_server(ipc_rx, tray_cmd_tx).await;
|
||||
});
|
||||
|
||||
// ─── Limpieza de logs locales ───────────────────────────────────────────────
|
||||
@@ -556,6 +559,42 @@ pub async fn run_agent(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(tray_cmd) = tray_cmd_rx.recv() => {
|
||||
match tray_cmd {
|
||||
ipc_protocol::AgentCommand::RestartService => {
|
||||
log_reporter.info("TRAY", "Restart service requested from tray icon").await;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Detener el servicio — esto terminará el proceso actual.
|
||||
// El Service Control Manager lo reiniciará automáticamente
|
||||
// gracias a la política de recuperación configurada.
|
||||
let _ = std::process::Command::new("sc")
|
||||
.arg("stop")
|
||||
.arg("OmniOilEdgeAgent")
|
||||
.output();
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
tracing::warn!("RestartService not supported on non-Windows");
|
||||
}
|
||||
}
|
||||
ipc_protocol::AgentCommand::CheckConnectivity => {
|
||||
log_reporter.info("CONNECTIVITY_TEST", "Test message from tray icon — connectivity OK").await;
|
||||
let _ = ipc_tx.send(ipc_protocol::IpcMessage::Notification {
|
||||
title: "Conectividad".to_string(),
|
||||
message: "Conexión verificada — el agente está comunicando con el servidor.".to_string(),
|
||||
});
|
||||
}
|
||||
ipc_protocol::AgentCommand::Notify(msg) => {
|
||||
log_reporter.info("TRAY", &format!("Test notification: {}", msg)).await;
|
||||
let _ = ipc_tx.send(ipc_protocol::IpcMessage::Notification {
|
||||
title: "Agent".to_string(),
|
||||
message: msg,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(15)) => {
|
||||
// Despierta periódicamente el bucle para actualizar el heartbeat
|
||||
// del watchdog y demostrar que el runtime de Tokio sigue activo.
|
||||
|
||||
@@ -53,8 +53,8 @@ impl StoreAndForward {
|
||||
// Salt aleatorio por dispositivo — persistir junto al DB
|
||||
let salt_path = db_path.with_extension("salt");
|
||||
let salt: [u8; 16] = if salt_path.exists() {
|
||||
let existing = std::fs::read(&salt_path)
|
||||
.context("Failed to read existing salt file")?;
|
||||
let existing =
|
||||
std::fs::read(&salt_path).context("Failed to read existing salt file")?;
|
||||
existing.try_into().unwrap_or_else(|_| {
|
||||
tracing::warn!("Salt file corrupt, generating new salt");
|
||||
let new: [u8; 16] = rand::random();
|
||||
@@ -63,8 +63,7 @@ impl StoreAndForward {
|
||||
})
|
||||
} else {
|
||||
let new: [u8; 16] = rand::random();
|
||||
std::fs::write(&salt_path, &new)
|
||||
.context("Failed to write salt file")?;
|
||||
std::fs::write(&salt_path, &new).context("Failed to write salt file")?;
|
||||
new
|
||||
};
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
//! credenciales almacenadas en el agente. La URL es temporal (TTL 30 min).
|
||||
//! - El agente nunca expone ningún puerto HTTP.
|
||||
|
||||
use crate::ipc_protocol::IpcMessage;
|
||||
use anyhow::{Context, Result, bail};
|
||||
use semver::Version;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing::{info, warn};
|
||||
use crate::ipc_protocol::IpcMessage;
|
||||
|
||||
/// Aplica una actualización OTA a partir de los datos recibidos vía MQTT.
|
||||
///
|
||||
@@ -91,7 +91,8 @@ pub async fn apply_ota_from_command(
|
||||
.build()
|
||||
.context("Error al construir cliente HTTP para descarga OTA")?;
|
||||
|
||||
let content = match download_asset(&client, download_url, target_version, ipc_tx.as_ref()).await {
|
||||
let content = match download_asset(&client, download_url, target_version, ipc_tx.as_ref()).await
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
if let Some(ref tx) = ipc_tx {
|
||||
|
||||
Reference in New Issue
Block a user