Skip to content

Commit

Permalink
feat(rust): add menu items to manually connect/disconnect to an invit…
Browse files Browse the repository at this point in the history
…ed service
  • Loading branch information
adrianbenavides committed Sep 19, 2023
1 parent 74b9623 commit 3ddc3d6
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 52 deletions.
93 changes: 73 additions & 20 deletions implementations/rust/ockam/ockam_app/src/invitations/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -153,21 +153,20 @@ 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,
invitation,
&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() {
Expand All @@ -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,
Expand All @@ -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");
Expand All @@ -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(())
Expand Down Expand Up @@ -318,13 +312,70 @@ async fn create_inlet(inlet_data: &InletDataFromInvitation) -> crate::Result<Soc
info!(
from = from_str,
to = service_route,
"Created tcp-inlet for accepted invitation"
"Created TCP inlet for accepted invitation"
);
Ok(from)
}

pub(crate) async fn disconnect_tcp_inlet<R: Runtime>(
app: AppHandle<R>,
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<R: Runtime>(
app: AppHandle<R>,
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,
Expand All @@ -337,7 +388,7 @@ impl InletDataFromInvitation {
pub fn new(
cli_state: &CliState,
invitation: &InvitationWithAccess,
inlets: &HashMap<String, TcpInlet>,
inlets: &HashMap<String, Inlet>,
) -> crate::Result<Option<Self>> {
match &invitation.service_access_details {
Some(d) => {
Expand Down Expand Up @@ -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,
},
Expand Down
33 changes: 26 additions & 7 deletions implementations/rust/ockam/ockam_app/src/invitations/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -34,21 +37,37 @@ pub struct AcceptedInvitations {

/// Inlets for accepted invitations, keyed by invitation id.
#[serde(default)]
pub(crate) inlets: HashMap<String, TcpInlet>,
pub(crate) inlets: HashMap<String, Inlet>,
}

#[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<Self> {
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;
}
}

Expand Down
95 changes: 70 additions & 25 deletions implementations/rust/ockam/ockam_app/src/invitations/tray_menu.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";

Expand Down Expand Up @@ -171,12 +170,7 @@ fn add_accepted_menus<R: Runtime>(
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(
Expand All @@ -190,32 +184,57 @@ fn add_accepted_menus<R: Runtime>(

fn accepted_invite_menu<R: Runtime>(
app_handle: &AppHandle<R>,
_invitation_id: &str,
invitation_id: &str,
access_details: &ServiceAccessDetails,
inlet_socket_addr: Option<&SocketAddr>,
inlet: Option<&Inlet>,
) -> Submenu<R> {
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),
),
};
Expand Down Expand Up @@ -245,6 +264,8 @@ fn dispatch_click_event<R: Runtime>(app: &AppHandle<R>, 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(())
Expand Down Expand Up @@ -312,3 +333,27 @@ fn on_copy<R: Runtime>(_app: &AppHandle<R>, socket_address: &str) -> tauri::Resu
}
Ok(())
}

fn on_disconnect<R: Runtime>(app: &AppHandle<R>, 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<R: Runtime>(app: &AppHandle<R>, 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(())
}

0 comments on commit 3ddc3d6

Please sign in to comment.