From 3ddc3d6e956a0b94d9f77fea6b20abb8213458e5 Mon Sep 17 00:00:00 2001 From: Adrian Benavides Date: Mon, 18 Sep 2023 15:17:47 +0200 Subject: [PATCH] feat(rust): add menu items to manually connect/disconnect to an invited service --- .../ockam_app/src/invitations/commands.rs | 93 ++++++++++++++---- .../ockam/ockam_app/src/invitations/state.rs | 33 +++++-- .../ockam_app/src/invitations/tray_menu.rs | 95 ++++++++++++++----- 3 files changed, 169 insertions(+), 52 deletions(-) diff --git a/implementations/rust/ockam/ockam_app/src/invitations/commands.rs b/implementations/rust/ockam/ockam_app/src/invitations/commands.rs index 56375c3ce04..683827ed1c4 100644 --- a/implementations/rust/ockam/ockam_app/src/invitations/commands.rs +++ b/implementations/rust/ockam/ockam_app/src/invitations/commands.rs @@ -15,7 +15,7 @@ use ockam_api::nodes::models::portal::InletStatus; use crate::app::{AppState, PROJECT_NAME}; use crate::cli::cli_bin; -use crate::invitations::state::{InvitationState, TcpInlet}; +use crate::invitations::state::{Inlet, InvitationState}; use crate::projects::commands::{create_enrollment_ticket, SyncAdminProjectsState}; use crate::shared_service::relay::RELAY_NAME; @@ -153,7 +153,7 @@ async fn refresh_inlets( let cli_state = app_state.state().await; let cli_bin = cli_bin()?; - let mut inlets_socket_addrs = vec![]; + let mut running_inlets = vec![]; for invitation in &invitations_state.accepted.invitations { match InletDataFromInvitation::new( &cli_state, @@ -161,13 +161,12 @@ async fn refresh_inlets( &invitations_state.accepted.inlets, ) { Ok(i) => match i { - Some(i) => { + Some(mut i) => { if !i.enabled { debug!(node = %i.local_node_name, "TCP inlet is disabled by the user, skipping"); continue; } - let mut inlet_is_running = false; debug!(node = %i.local_node_name, "Checking node status"); if let Ok(node) = cli_state.nodes.get(&i.local_node_name) { if node.is_running() { @@ -191,21 +190,16 @@ async fn refresh_inlets( { trace!(output = ?String::from_utf8_lossy(&cmd.stdout), "TCP inlet status"); let inlet: InletStatus = serde_json::from_slice(&cmd.stdout)?; - let inlet_socket_addr = SocketAddr::from_str(&inlet.bind_addr)?; - inlet_is_running = true; debug!( at = ?inlet.bind_addr, alias = inlet.alias, "TCP inlet running" ); - inlets_socket_addrs - .push((invitation.invitation.id.clone(), inlet_socket_addr)); + running_inlets.push((invitation.invitation.id.clone(), i)); + continue; } } } - if inlet_is_running { - continue; - } debug!(node = %i.local_node_name, "Deleting node"); let _ = duct::cmd!( &cli_bin, @@ -219,9 +213,9 @@ async fn refresh_inlets( .stdout_capture() .run(); match create_inlet(&i).await { - Ok(inlet_socket_addr) => { - inlets_socket_addrs - .push((invitation.invitation.id.clone(), inlet_socket_addr)); + Ok(socket_addr) => { + i.socket_addr = Some(socket_addr); + running_inlets.push((invitation.invitation.id.clone(), i)); } Err(err) => { warn!(%err, node = %i.local_node_name, "Failed to create TCP inlet for accepted invitation"); @@ -237,11 +231,11 @@ async fn refresh_inlets( } } } - for (invitation_id, inlet_socket_addr) in inlets_socket_addrs { + for (invitation_id, i) in running_inlets { invitations_state .accepted .inlets - .insert(invitation_id, TcpInlet::new(inlet_socket_addr)); + .insert(invitation_id, Inlet::new(i)?); } info!("Inlets refreshed"); Ok(()) @@ -318,13 +312,70 @@ async fn create_inlet(inlet_data: &InletDataFromInvitation) -> crate::Result( + app: AppHandle, + invitation_id: &str, +) -> crate::Result<()> { + let invitation_state: State<'_, SyncInvitationsState> = app.state(); + let mut writer = invitation_state.write().await; + if let Some(inlet) = writer.accepted.inlets.get_mut(invitation_id) { + if !inlet.enabled { + debug!(node = %inlet.node_name, alias = %inlet.alias, "TCP inlet was already disconnected"); + return Ok(()); + } + inlet.disable(); + let local_node_name = &inlet.node_name; + let alias = &inlet.alias; + debug!(node = %local_node_name, %alias, "Deleting TCP inlet"); + let _ = duct::cmd!( + &cli_bin()?, + "--no-input", + "tcp-inlet", + "delete", + alias, + "--at", + local_node_name, + "--yes" + ) + .stderr_null() + .stdout_capture() + .run() + .map_err( + |e| warn!(%e, node = %local_node_name, alias = %alias, "Failed to delete TCP inlet"), + ); + info!( + node = %local_node_name, %alias, + "Disconnected TCP inlet for accepted invitation" + ); + } + Ok(()) +} + +pub(crate) async fn enable_tcp_inlet( + app: AppHandle, + invitation_id: &str, +) -> crate::Result<()> { + let invitation_state: State<'_, SyncInvitationsState> = app.state(); + let mut writer = invitation_state.write().await; + if let Some(inlet) = writer.accepted.inlets.get_mut(invitation_id) { + if inlet.enabled { + debug!(node = %inlet.node_name, alias = %inlet.alias, "TCP inlet was already enabled"); + return Ok(()); + } + inlet.enable(); + app.trigger_global(super::events::REFRESH_INVITATIONS, None); + info!(node = %inlet.node_name, alias = %inlet.alias, "Enabled TCP inlet"); + } + Ok(()) +} + #[derive(Debug)] -struct InletDataFromInvitation { +pub(crate) struct InletDataFromInvitation { pub enabled: bool, pub local_node_name: String, pub service_name: String, @@ -337,7 +388,7 @@ impl InletDataFromInvitation { pub fn new( cli_state: &CliState, invitation: &InvitationWithAccess, - inlets: &HashMap, + inlets: &HashMap, ) -> crate::Result> { match &invitation.service_access_details { Some(d) => { @@ -468,7 +519,9 @@ mod tests { // Validate the inlet data, with prior inlet data inlets.insert( "invitation_id".to_string(), - TcpInlet { + Inlet { + node_name: "local_node_name".to_string(), + alias: "alias".to_string(), socket_addr: "127.0.0.1:1000".parse().unwrap(), enabled: true, }, diff --git a/implementations/rust/ockam/ockam_app/src/invitations/state.rs b/implementations/rust/ockam/ockam_app/src/invitations/state.rs index 749bd77df87..5a34b184420 100644 --- a/implementations/rust/ockam/ockam_app/src/invitations/state.rs +++ b/implementations/rust/ockam/ockam_app/src/invitations/state.rs @@ -9,6 +9,9 @@ use ockam_api::cloud::share::{ InvitationList, InvitationWithAccess, ReceivedInvitation, SentInvitation, }; +use crate::invitations::commands::InletDataFromInvitation; +use crate::{error::Error, Result}; + #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct InvitationState { #[serde(default)] @@ -34,21 +37,37 @@ pub struct AcceptedInvitations { /// Inlets for accepted invitations, keyed by invitation id. #[serde(default)] - pub(crate) inlets: HashMap, + pub(crate) inlets: HashMap, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct TcpInlet { +pub(crate) struct Inlet { + pub(crate) node_name: String, + pub(crate) alias: String, pub(crate) socket_addr: SocketAddr, pub(crate) enabled: bool, } -impl TcpInlet { - pub fn new(socket_addr: SocketAddr) -> Self { - Self { +impl Inlet { + pub(crate) fn new(data: InletDataFromInvitation) -> Result { + let socket_addr = match data.socket_addr { + Some(addr) => addr, + None => return Err(Error::App("Socket address should be set".to_string())), + }; + Ok(Self { + node_name: data.local_node_name, + alias: data.service_name, socket_addr, - enabled: true, - } + enabled: data.enabled, + }) + } + + pub(crate) fn disable(&mut self) { + self.enabled = false; + } + + pub(crate) fn enable(&mut self) { + self.enabled = true; } } diff --git a/implementations/rust/ockam/ockam_app/src/invitations/tray_menu.rs b/implementations/rust/ockam/ockam_app/src/invitations/tray_menu.rs index 1f62fe73204..bdbd7b76c75 100644 --- a/implementations/rust/ockam/ockam_app/src/invitations/tray_menu.rs +++ b/implementations/rust/ockam/ockam_app/src/invitations/tray_menu.rs @@ -1,7 +1,6 @@ use arboard::Clipboard; use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; use std::collections::HashMap; -use std::net::SocketAddr; use tauri::async_runtime::spawn; use tauri::menu::{ IconMenuItemBuilder, MenuBuilder, MenuEvent, MenuItemBuilder, NativeIcon, Submenu, @@ -15,7 +14,7 @@ use ockam_api::cloud::share::{ReceivedInvitation, SentInvitation, ServiceAccessD use super::state::SyncInvitationsState; use crate::app::AppState; -use crate::invitations::state::AcceptedInvitations; +use crate::invitations::state::{AcceptedInvitations, Inlet}; pub const INVITATIONS_WINDOW_ID: &str = "invitations_creation"; @@ -171,12 +170,7 @@ fn add_accepted_menus( submenu_builder = invitations .into_iter() .map(|(invitation_id, access_details, inlet)| { - accepted_invite_menu( - app_handle, - invitation_id, - access_details, - inlet.map(|i| &i.socket_addr), - ) + accepted_invite_menu(app_handle, invitation_id, access_details, inlet) }) .fold(submenu_builder, |menu, submenu| menu.item(&submenu)); submenus.push( @@ -190,32 +184,57 @@ fn add_accepted_menus( fn accepted_invite_menu( app_handle: &AppHandle, - _invitation_id: &str, + invitation_id: &str, access_details: &ServiceAccessDetails, - inlet_socket_addr: Option<&SocketAddr>, + inlet: Option<&Inlet>, ) -> Submenu { let service_name = access_details .service_name() .unwrap_or_else(|_| "Unknown service name".to_string()); let mut submenu_builder = SubmenuBuilder::new(app_handle, &service_name); - submenu_builder = match inlet_socket_addr { - Some(s) => submenu_builder.items(&[ - &IconMenuItemBuilder::new(format!("Available at: {s}")) - .enabled(false) - .native_icon(NativeIcon::StatusAvailable) - .build(app_handle), - &IconMenuItemBuilder::with_id( - format!("invitation-accepted-copy-{s}"), - format!("Copy {s}"), - ) - .icon(Icon::Raw( - include_bytes!("../../icons/clipboard2.png").to_vec(), - )) - .build(app_handle), - ]), + submenu_builder = match &inlet { + Some(i) => { + let socket_addr = i.socket_addr; + if i.enabled { + submenu_builder.items(&[ + &IconMenuItemBuilder::new(format!("Available at: {socket_addr}")) + .enabled(false) + .native_icon(NativeIcon::StatusAvailable) + .build(app_handle), + &IconMenuItemBuilder::with_id( + format!("invitation-accepted-copy-{socket_addr}"), + format!("Copy {socket_addr}"), + ) + .icon(Icon::Raw( + include_bytes!("../../icons/clipboard2.png").to_vec(), + )) + .build(app_handle), + &IconMenuItemBuilder::with_id( + format!("invitation-accepted-disconnect-{invitation_id}"), + "Disconnect", + ) + .icon(Icon::Raw(include_bytes!("../../icons/power.png").to_vec())) + .build(app_handle), + ]) + } else { + submenu_builder.items(&[ + &IconMenuItemBuilder::new("Not connected") + .native_icon(NativeIcon::StatusUnavailable) + .enabled(false) + .build(app_handle), + &IconMenuItemBuilder::with_id( + format!("invitation-accepted-connect-{invitation_id}"), + "Connect", + ) + .icon(Icon::Raw(include_bytes!("../../icons/power.png").to_vec())) + .build(app_handle), + ]) + } + } None => submenu_builder.item( &IconMenuItemBuilder::new("Not connected") .native_icon(NativeIcon::StatusUnavailable) + .enabled(false) .build(app_handle), ), }; @@ -245,6 +264,8 @@ fn dispatch_click_event(app: &AppHandle, id: &str) -> tauri::Resu ["create", "for", outlet_socket_addr] => on_create(app, outlet_socket_addr), ["received", "accept", id] => on_accept(app, id), ["accepted", "copy", socket_address] => on_copy(app, socket_address), + ["accepted", "disconnect", id] => on_disconnect(app, id), + ["accepted", "connect", id] => on_connect(app, id), other => { warn!(?other, "unexpected menu ID"); Ok(()) @@ -312,3 +333,27 @@ fn on_copy(_app: &AppHandle, socket_address: &str) -> tauri::Resu } Ok(()) } + +fn on_disconnect(app: &AppHandle, invitation_id: &str) -> tauri::Result<()> { + debug!(%invitation_id, "Invite on_disconnect clicked"); + let app = app.clone(); + let invitation_id = invitation_id.to_string(); + spawn(async move { + let _ = super::commands::disconnect_tcp_inlet(app, &invitation_id) + .await + .map_err(|e| error!(%e, "Failed to disconnect TCP inlet")); + }); + Ok(()) +} + +fn on_connect(app: &AppHandle, invitation_id: &str) -> tauri::Result<()> { + debug!(%invitation_id, "Invite on_connect clicked"); + let app = app.clone(); + let invitation_id = invitation_id.to_string(); + spawn(async move { + let _ = super::commands::enable_tcp_inlet(app, &invitation_id) + .await + .map_err(|e| error!(%e, "Failed to re-enable TCP inlet")); + }); + Ok(()) +}