From 5b1977b1e7390ed2941cd233235b5177f104a2b7 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Tue, 20 Aug 2024 12:38:51 +0200 Subject: [PATCH 01/24] feat(launchpad): connection mode and port rage selection --- node-launchpad/.config/config.json5 | 6 + node-launchpad/src/action.rs | 9 +- node-launchpad/src/app.rs | 38 +- node-launchpad/src/components/options.rs | 134 +++++- node-launchpad/src/components/popup.rs | 2 + .../src/components/popup/change_drive.rs | 8 +- .../src/components/popup/connection_mode.rs | 395 ++++++++++++++++++ .../src/components/popup/port_range.rs | 0 node-launchpad/src/components/status.rs | 43 +- node-launchpad/src/config.rs | 21 +- node-launchpad/src/connection_mode.rs | 71 ++++ node-launchpad/src/lib.rs | 1 + node-launchpad/src/mode.rs | 2 + 13 files changed, 704 insertions(+), 26 deletions(-) create mode 100644 node-launchpad/src/components/popup/connection_mode.rs create mode 100644 node-launchpad/src/components/popup/port_range.rs create mode 100644 node-launchpad/src/connection_mode.rs diff --git a/node-launchpad/.config/config.json5 b/node-launchpad/.config/config.json5 index 8bbd2f356e..58db17d7bb 100644 --- a/node-launchpad/.config/config.json5 +++ b/node-launchpad/.config/config.json5 @@ -39,6 +39,12 @@ "": {"OptionsActions":"TriggerChangeDrive"}, "": {"OptionsActions":"TriggerChangeDrive"}, "": {"OptionsActions":"TriggerChangeDrive"}, + "": {"OptionsActions":"TriggerChangeConnectionMode"}, + "": {"OptionsActions":"TriggerChangeConnectionMode"}, + "": {"OptionsActions":"TriggerChangeConnectionMode"}, + "": {"OptionsActions":"TriggerChangePortRange"}, + "": {"OptionsActions":"TriggerChangePortRange"}, + "": {"OptionsActions":"TriggerChangePortRange"}, "": {"OptionsActions":"TriggerBetaProgramme"}, "": {"OptionsActions":"TriggerBetaProgramme"}, "": {"OptionsActions":"TriggerBetaProgramme"}, diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index e58402a462..043089c842 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -7,6 +7,7 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::{ + connection_mode::ConnectionMode, mode::{InputMode, Scene}, node_stats::NodeStats, }; @@ -22,9 +23,11 @@ pub enum Action { SwitchScene(Scene), SwitchInputMode(InputMode), + StoreStorageDrive(PathBuf, String), + StoreConnectionMode(ConnectionMode), + StorePortRange(u16, u16), StoreDiscordUserName(String), StoreNodesToStart(usize), - StoreStorageDrive(PathBuf, String), Tick, Render, @@ -61,9 +64,13 @@ pub enum OptionsActions { ResetNodes, TriggerChangeDrive, + TriggerChangeConnectionMode, + TriggerChangePortRange, TriggerBetaProgramme, TriggerResetNodes, TriggerAccessLogs, + UpdateConnectionMode(ConnectionMode), + UpdatePortRange(u16, u16), UpdateBetaProgrammeUsername(String), UpdateStorageDrive(PathBuf, String), } diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 09053998fd..6e1342e848 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -15,12 +15,13 @@ use crate::{ options::Options, popup::{ beta_programme::BetaProgramme, change_drive::ChangeDrivePopup, - manage_nodes::ManageNodes, reset_nodes::ResetNodesPopup, + connection_mode::ChangeConnectionModePopUp, manage_nodes::ManageNodes, + reset_nodes::ResetNodesPopup, }, - status::Status, Component, }, config::{get_launchpad_nodes_data_dir_path, AppData, Config}, + connection_mode::ConnectionMode, mode::{InputMode, Scene}, style::SPACE_CADET, system::{get_default_mount_point, get_primary_mount_point, get_primary_mount_point_name}, @@ -71,6 +72,11 @@ impl App { debug!("Data dir path for nodes: {data_dir_path:?}"); // App data validations + let connection_mode = app_data + .connection_mode + .unwrap_or(ConnectionMode::Automatic); + let port_from = app_data.port_from.unwrap_or(PORT_MIN); + let port_to = app_data.port_to.unwrap_or(PORT_MAX); let storage_mountpoint = app_data .storage_mountpoint .clone() @@ -87,12 +93,18 @@ impl App { peers_args, safenode_path, data_dir_path, + connection_mode, + Some(port_from), + Some(port_to), ) .await?; let options = Options::new( storage_mountpoint.clone(), storage_drive.clone(), app_data.discord_username.clone(), + connection_mode, + Some(port_from), + Some(port_to), ) .await?; let help = Help::new().await?; @@ -102,6 +114,8 @@ impl App { let discord_username_input = BetaProgramme::new(app_data.discord_username.clone()); let manage_nodes = ManageNodes::new(app_data.nodes_to_start, storage_mountpoint.clone())?; let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; + let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; + let beta_programme = BetaProgramme::new(app_data.discord_username.clone()); Ok(Self { config, @@ -115,9 +129,10 @@ impl App { Box::new(help), // Popups Box::new(change_drive), + Box::new(change_connection_mode), + Box::new(beta_programme), Box::new(reset_nodes), Box::new(manage_nodes), - Box::new(discord_username_input), ], should_quit: false, should_suspend: false, @@ -229,6 +244,23 @@ impl App { self.input_mode = mode; } // Storing Application Data + Action::StoreStorageDrive(ref drive_mountpoint, ref drive_name) => { + debug!("Storing storage drive: {drive_mountpoint:?}, {drive_name:?}"); + self.app_data.storage_mountpoint = Some(drive_mountpoint.clone()); + self.app_data.storage_drive = Some(drive_name.as_str().to_string()); + self.app_data.save(None)?; + } + Action::StoreConnectionMode(ref mode) => { + debug!("Storing connection mode: {mode:?}"); + self.app_data.connection_mode = Some(mode.clone()); + self.app_data.save(None)?; + } + Action::StorePortRange(from, to) => { + debug!("Storing port range: {from:?}, {to:?}"); + self.app_data.port_from = Some(from); + self.app_data.port_to = Some(to); + self.app_data.save(None)?; + } Action::StoreDiscordUserName(ref username) => { debug!("Storing discord username: {username:?}"); self.app_data.discord_username.clone_from(username); diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index e3dc97764a..a19be98b4b 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -15,6 +15,7 @@ use super::{header::SelectedMenuItem, Component}; use crate::{ action::{Action, OptionsActions}, components::header::Header, + connection_mode::ConnectionMode, mode::{InputMode, Scene}, style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE}, system, @@ -26,6 +27,10 @@ pub struct Options { pub storage_mountpoint: PathBuf, pub storage_drive: String, pub discord_username: String, + pub connection_mode: ConnectionMode, + pub port_edit: bool, + pub port_from: Option, + pub port_to: Option, pub active: bool, pub action_tx: Option>, } @@ -35,11 +40,18 @@ impl Options { storage_mountpoint: PathBuf, storage_drive: String, discord_username: String, + connection_mode: ConnectionMode, + port_from: Option, + port_to: Option, ) -> Result { Ok(Self { storage_mountpoint, storage_drive, discord_username, + connection_mode, + port_edit: false, + port_from, + port_to, active: false, action_tx: None, }) @@ -47,10 +59,6 @@ impl Options { } impl Component for Options { - fn init(&mut self, _area: Rect) -> Result<()> { - Ok(()) - } - fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> { if !self.active { return Ok(()); @@ -61,7 +69,7 @@ impl Component for Options { .constraints( [ Constraint::Length(1), - Constraint::Length(5), + Constraint::Length(9), Constraint::Length(5), Constraint::Length(5), Constraint::Length(5), @@ -76,7 +84,7 @@ impl Component for Options { // Storage Drive let block1 = Block::default() - .title(" Storage Drive ") + .title(" Device Options ") .title_style(Style::default().bold().fg(GHOST_WHITE)) .style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) @@ -86,8 +94,16 @@ impl Component for Options { Row::new(vec![ Cell::from(Span::raw(" ")), // Empty row for padding Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), ]), Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Storage Drive: ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), Cell::from( Line::from(vec![Span::styled( format!(" {} ", self.storage_drive), @@ -103,15 +119,82 @@ impl Component for Options { .alignment(Alignment::Right), ), ]), + Row::new(vec![ + Cell::from(Span::raw(" ")), // Empty row for padding + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + ]), + Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Connection Mode: ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![Span::styled( + format!(" {} ", self.connection_mode), + Style::default().fg(VIVID_SKY_BLUE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![ + Span::styled(" Change Mode ", Style::default().fg(VERY_LIGHT_AZURE)), + Span::styled(" [Ctrl+K] ", Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right), + ), + ]), + Row::new(vec![ + Cell::from(Span::raw(" ")), // Empty row for padding + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + ]), + Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Port Range: ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![Span::styled( + format!( + " {}-{} ", + self.port_from.unwrap_or(0), + self.port_to.unwrap_or(0) + ), + Style::default().fg(VIVID_SKY_BLUE), + )]) + .alignment(Alignment::Left), + ), + Cell::from( + Line::from(vec![ + Span::styled( + " Edit Port Range ", + Style::default().fg(VERY_LIGHT_AZURE), + ), + Span::styled(" [Ctrl+P] ", Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right), + ), + ]), + ], + &[ + Constraint::Length(18), + Constraint::Percentage(25), + Constraint::Fill(1), ], - &[Constraint::Percentage(50), Constraint::Percentage(50)], ) .block(block1) .style(Style::default().fg(GHOST_WHITE)); - // Beta Rewards Program — Discord Username + // Beta Rewards Program let block2 = Block::default() - .title(" Beta Rewards Program — Discord Username ") + .title(" Beta Rewards Program ") .title_style(Style::default().bold().fg(GHOST_WHITE)) .style(Style::default().fg(GHOST_WHITE)) .borders(Borders::ALL) @@ -122,8 +205,16 @@ impl Component for Options { // Empty row for padding Cell::from(Span::raw(" ")), Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), ]), Row::new(vec![ + Cell::from( + Line::from(vec![Span::styled( + " Discord Username: ", + Style::default().fg(LIGHT_PERIWINKLE), + )]) + .alignment(Alignment::Left), + ), Cell::from( Line::from(vec![Span::styled( format!(" {} ", self.discord_username), @@ -143,7 +234,11 @@ impl Component for Options { ), ]), ], - &[Constraint::Percentage(50), Constraint::Percentage(50)], + &[ + Constraint::Length(18), + Constraint::Percentage(25), + Constraint::Fill(1), + ], ) .block(block2) .style(Style::default().fg(GHOST_WHITE)); @@ -232,9 +327,11 @@ impl Component for Options { match action { Action::SwitchScene(scene) => match scene { Scene::Options + | Scene::ChangeDrivePopUp + | Scene::ChangeConnectionModePopUp + | Scene::ChangePortsPopUp | Scene::BetaProgrammePopUp - | Scene::ResetNodesPopUp - | Scene::ChangeDrivePopUp => { + | Scene::ResetNodesPopUp => { self.active = true; // make sure we're in navigation mode return Ok(Some(Action::SwitchInputMode(InputMode::Navigation))); @@ -249,6 +346,19 @@ impl Component for Options { self.storage_mountpoint = mountpoint; self.storage_drive = drive; } + OptionsActions::TriggerChangeConnectionMode => { + return Ok(Some(Action::SwitchScene(Scene::ChangeConnectionModePopUp))); + } + OptionsActions::UpdateConnectionMode(mode) => { + self.connection_mode = mode; + } + OptionsActions::TriggerChangePortRange => { + return Ok(Some(Action::SwitchScene(Scene::ChangePortsPopUp))); + } + OptionsActions::UpdatePortRange(from, to) => { + self.port_from = Some(from); + self.port_to = Some(to); + } OptionsActions::TriggerBetaProgramme => { return Ok(Some(Action::SwitchScene(Scene::BetaProgrammePopUp))); } diff --git a/node-launchpad/src/components/popup.rs b/node-launchpad/src/components/popup.rs index 11c2bf9a3d..703ff50eae 100644 --- a/node-launchpad/src/components/popup.rs +++ b/node-launchpad/src/components/popup.rs @@ -8,5 +8,7 @@ pub mod beta_programme; pub mod change_drive; +pub mod connection_mode; pub mod manage_nodes; +pub mod port_range; pub mod reset_nodes; diff --git a/node-launchpad/src/components/popup/change_drive.rs b/node-launchpad/src/components/popup/change_drive.rs index 7ae9ba1d51..02d2ae13e5 100644 --- a/node-launchpad/src/components/popup/change_drive.rs +++ b/node-launchpad/src/components/popup/change_drive.rs @@ -54,7 +54,7 @@ impl ChangeDrivePopup { pub fn new(storage_mountpoint: PathBuf) -> Result { let drives_and_space = system::get_list_of_available_drives_and_available_space()?; - let mut selected_drive: DriveItem = DriveItem::default(); + let mut selected_connection_mode: DriveItem = DriveItem::default(); // Create a vector of DriveItem from drives_and_space let drives_items: Vec = drives_and_space .iter() @@ -66,7 +66,7 @@ impl ChangeDrivePopup { mountpoint: mountpoint.clone(), size: size_str, status: if mountpoint == &storage_mountpoint { - selected_drive = DriveItem { + selected_connection_mode = DriveItem { name: drive_name.to_string(), mountpoint: mountpoint.clone(), size: size_str_cloned, @@ -89,8 +89,8 @@ impl ChangeDrivePopup { active: false, state: ChangeDriveState::Selection, items, - drive_selection: selected_drive.clone(), - drive_selection_initial_state: selected_drive.clone(), + drive_selection: selected_connection_mode.clone(), + drive_selection_initial_state: selected_connection_mode.clone(), can_select: false, }) } diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs new file mode 100644 index 0000000000..9b0c4d3d08 --- /dev/null +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -0,0 +1,395 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use std::default::Default; + +use super::super::utils::centered_rect_fixed; + +use color_eyre::Result; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Modifier, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph}, +}; +use strum::IntoEnumIterator; + +use crate::{ + action::{Action, OptionsActions}, + components::Component, + connection_mode::ConnectionMode, + mode::{InputMode, Scene}, + style::{ + clear_area, COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, + SPACE_CADET, VIVID_SKY_BLUE, + }, +}; + +#[derive(Default)] +pub struct ChangeConnectionModePopUp { + active: bool, + items: StatefulList, + connection_mode_selection: ConnectionModeItem, + connection_mode_initial_state: ConnectionModeItem, + can_select: bool, // If the user can select the connection mode +} + +impl ChangeConnectionModePopUp { + pub fn new(connection_mode: ConnectionMode) -> Result { + let mut selected_connection_mode: ConnectionModeItem = ConnectionModeItem::default(); + let connection_modes_items: Vec = ConnectionMode::iter() + .map(|connection_mode_item| ConnectionModeItem { + connection_mode: connection_mode_item.clone(), + status: if connection_mode == connection_mode_item { + selected_connection_mode = ConnectionModeItem { + connection_mode: connection_mode_item, + status: ConnectionModeStatus::Selected, + }; + ConnectionModeStatus::Selected + } else { + ConnectionModeStatus::NotSelected + }, + }) + .collect::>(); + debug!("Connection Mode in Config: {:?}", connection_mode); + let items = StatefulList::with_items(connection_modes_items); + Ok(Self { + active: false, + items, + connection_mode_selection: selected_connection_mode.clone(), + connection_mode_initial_state: selected_connection_mode.clone(), + can_select: false, + }) + } + + // --- Interactions with the List of modes --- + + /// Deselects all modes in the list of items + /// + fn deselect_all(&mut self) { + for item in &mut self.items.items { + item.status = ConnectionModeStatus::NotSelected; + } + } + /// Assigns to self.connection_mode_selection the selected connection mode in the list + /// + #[allow(dead_code)] + fn assign_connection_mode_selection(&mut self) { + self.deselect_all(); + if let Some(i) = self.items.state.selected() { + self.items.items[i].status = ConnectionModeStatus::Selected; + self.connection_mode_selection = self.items.items[i].clone(); + } + } + /// Highlights the connection mode that is currently selected in the list of items. + /// + fn select_connection_mode(&mut self) { + self.deselect_all(); + for (index, item) in self.items.items.iter_mut().enumerate() { + if item.connection_mode == self.connection_mode_selection.connection_mode { + item.status = ConnectionModeStatus::Selected; + self.items.state.select(Some(index)); + break; + } + } + } + /// Returns the highlighted connection mode in the list of items. + /// + fn return_selection(&mut self) -> ConnectionModeItem { + if let Some(i) = self.items.state.selected() { + return self.items.items[i].clone(); + } + ConnectionModeItem::default() + } +} + +impl Component for ChangeConnectionModePopUp { + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + if !self.active { + return Ok(vec![]); + } + let send_back: Vec = match key.code { + KeyCode::Enter => { + // We allow action if we have more than one connection mode and the action is not + // over the connection mode already selected + let connection_mode = self.return_selection(); + if connection_mode.connection_mode != self.connection_mode_selection.connection_mode + { + debug!( + "Got Enter and there's a new selection, storing value and switching to Options" + ); + debug!("Connection Mode selected: {:?}", connection_mode); + self.connection_mode_initial_state = self.connection_mode_selection.clone(); + self.assign_connection_mode_selection(); + vec![ + Action::StoreConnectionMode( + self.connection_mode_selection.connection_mode.clone(), + ), + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + connection_mode.connection_mode, + )), + Action::SwitchScene(Scene::Options), + ] + } else { + debug!("Got Enter, but no new selection. We should not do anything"); + vec![Action::SwitchScene(Scene::ChangeConnectionModePopUp)] + } + } + KeyCode::Esc => { + debug!("Got Esc, switching to Options"); + vec![Action::SwitchScene(Scene::Options)] + } + KeyCode::Up => { + if self.items.items.len() > 1 { + self.items.previous(); + let connection_mode = self.return_selection(); + self.can_select = connection_mode.connection_mode + != self.connection_mode_selection.connection_mode; + } + vec![] + } + KeyCode::Down => { + if self.items.items.len() > 1 { + self.items.next(); + let connection_mode = self.return_selection(); + self.can_select = connection_mode.connection_mode + != self.connection_mode_selection.connection_mode; + } + vec![] + } + _ => { + vec![] + } + }; + Ok(send_back) + } + + fn update(&mut self, action: Action) -> Result> { + let send_back = match action { + Action::SwitchScene(scene) => match scene { + Scene::ChangeConnectionModePopUp => { + self.active = true; + self.can_select = false; + self.select_connection_mode(); + Some(Action::SwitchInputMode(InputMode::Entry)) + } + _ => { + self.active = false; + None + } + }, + // Useful when the user has selected a connection mode but didn't confirm it + Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { + self.connection_mode_selection.connection_mode = connection_mode; + self.select_connection_mode(); + None + } + _ => None, + }; + Ok(send_back) + } + + fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { + if !self.active { + return Ok(()); + } + + let layer_zero = centered_rect_fixed(52, 15, area); + + let layer_one = Layout::new( + Direction::Vertical, + [ + // Padding from title to the table + Constraint::Length(1), + // Table + Constraint::Min(1), + // for the pop_up_border + Constraint::Length(1), + ], + ) + .split(layer_zero); + + let pop_up_border: Paragraph = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(" Connection Mode ") + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(VIVID_SKY_BLUE)) + .bg(DARK_GUNMETAL), + ); + clear_area(f, layer_zero); + + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the table + Constraint::Length(10), + // gap + Constraint::Length(3), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + // Connection Mode selector + let items: Vec = self + .items + .items + .iter() + .enumerate() + .map(|(i, connection_mode_item)| { + connection_mode_item.to_list_item(i, layer_two[0].width as usize) + }) + .collect(); + + let items = List::new(items) + .block(Block::default().padding(Padding::uniform(1))) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::REVERSED) + .fg(INDIGO), + ) + .highlight_spacing(HighlightSpacing::Always); + + f.render_stateful_widget(items, layer_two[0], &mut self.items.state); + + // Dash + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[1]); + + // Buttons + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[2]); + + let button_no = Line::from(vec![Span::styled( + "Cancel [Esc]", + Style::default().fg(LIGHT_PERIWINKLE), + )]); + + f.render_widget( + Paragraph::new(button_no) + .block(Block::default().padding(Padding::horizontal(2))) + .alignment(Alignment::Left), + buttons_layer[0], + ); + + let button_yes = Line::from(vec![ + Span::styled( + "Select ", + if self.can_select { + Style::default().fg(EUCALYPTUS) + } else { + Style::default().fg(COOL_GREY) + }, + ), + Span::styled("[Enter]", Style::default().fg(LIGHT_PERIWINKLE).bold()), + ]) + .alignment(Alignment::Right); + + f.render_widget( + Paragraph::new(button_yes) + .block(Block::default().padding(Padding::horizontal(2))) + .alignment(Alignment::Right), + buttons_layer[1], + ); + + // We render now so the borders are on top of the other widgets + f.render_widget(pop_up_border, layer_zero); + + Ok(()) + } +} + +#[derive(Default)] +struct StatefulList { + state: ListState, + items: Vec, + last_selected: Option, +} + +impl StatefulList { + fn with_items(items: Vec) -> Self { + StatefulList { + state: ListState::default(), + items, + last_selected: None, + } + } + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => self.last_selected.unwrap_or(0), + }; + self.state.select(Some(i)); + } + + fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => self.last_selected.unwrap_or(0), + }; + self.state.select(Some(i)); + } +} + +#[derive(Default, Debug, Copy, Clone)] +enum ConnectionModeStatus { + Selected, + #[default] + NotSelected, +} + +#[derive(Default, Debug, Clone)] +pub struct ConnectionModeItem { + connection_mode: ConnectionMode, + status: ConnectionModeStatus, +} + +impl ConnectionModeItem { + fn to_list_item(&self, _index: usize, _width: usize) -> ListItem { + let line = match self.status { + ConnectionModeStatus::NotSelected => Line::from(vec![ + Span::raw(" "), + Span::styled( + self.connection_mode.to_string(), + Style::default().fg(VIVID_SKY_BLUE), + ), + ]), + ConnectionModeStatus::Selected => Line::from(vec![ + Span::styled(" ►", Style::default().fg(EUCALYPTUS)), + Span::raw(" "), + Span::styled( + self.connection_mode.to_string(), + Style::default().fg(VIVID_SKY_BLUE), + ), + ]), + }; + + ListItem::new(line).style(Style::default().bg(SPACE_CADET)) + } +} diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 35418e3b4f..911727d8aa 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -14,6 +14,7 @@ use super::{ }; use crate::action::OptionsActions; use crate::config::get_launchpad_nodes_data_dir_path; +use crate::connection_mode::ConnectionMode; use crate::{ action::{Action, StatusActions}, config::Config, @@ -69,6 +70,12 @@ pub struct Status { safenode_path: Option, // Path where the node data is stored data_dir_path: PathBuf, + // Connection mode + connection_mode: ConnectionMode, + // Port from + port_from: Option, + // Port to + port_to: Option, } #[derive(Clone)] @@ -85,6 +92,9 @@ impl Status { peers_args: PeersArgs, safenode_path: Option, data_dir_path: PathBuf, + connection_mode: ConnectionMode, + port_from: Option, + port_to: Option, ) -> Result { let mut status = Self { peers_args, @@ -102,6 +112,9 @@ impl Status { discord_username: discord_username.to_string(), safenode_path, data_dir_path, + connection_mode, + port_from, + port_to, }; let now = Instant::now(); @@ -377,6 +390,9 @@ impl Component for Status { info!("Got action to reset nodes"); reset_nodes(action_sender, false); } + Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { + self.connection_mode = connection_mode; + } _ => {} } Ok(None) @@ -455,6 +471,22 @@ impl Component for Status { Cell::new(memory_use_val).fg(GHOST_WHITE), ]); + let connection_mode_string = match self.connection_mode { + ConnectionMode::HomeNetwork => "Home Network", + ConnectionMode::UPnP => "UPnP", + ConnectionMode::CustomPorts => &format!( + "Custom Ports {}-{}", + self.port_from.unwrap_or(0), + self.port_to.unwrap_or(0) + ), + ConnectionMode::Automatic => "Automatic", + }; + + let connection_mode_row = Row::new(vec![ + Cell::new("Connection".to_string()).fg(GHOST_WHITE), + Cell::new(connection_mode_string).fg(GHOST_WHITE), + ]); + // Combine "Nanos Earned" and "Discord Username" into a single row let discord_username_title = Span::styled( "Discord Username: ".to_string(), @@ -474,7 +506,7 @@ impl Component for Status { ) }; - let total_nanos_earned_and_discord = Row::new(vec![ + let total_nanos_earned_and_discord_row = Row::new(vec![ Cell::new("Nanos Earned".to_string()).fg(VIVID_SKY_BLUE), Cell::new(self.node_stats.forwarded_rewards.to_string()) .fg(VIVID_SKY_BLUE) @@ -487,14 +519,15 @@ impl Component for Status { let stats_rows = vec![ storage_allocated_row, - memory_use_row.bottom_margin(1), - total_nanos_earned_and_discord, + memory_use_row, + connection_mode_row, + total_nanos_earned_and_discord_row, ]; let stats_width = [Constraint::Length(5)]; let column_constraints = [ + Constraint::Length(23), Constraint::Percentage(25), - Constraint::Percentage(5), - Constraint::Percentage(70), + Constraint::Fill(1), ]; let stats_table = Table::new(stats_rows, stats_width) .block( diff --git a/node-launchpad/src/config.rs b/node-launchpad/src/config.rs index 3db61eb518..1b19ab96e2 100644 --- a/node-launchpad/src/config.rs +++ b/node-launchpad/src/config.rs @@ -6,6 +6,8 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. +use crate::connection_mode::ConnectionMode; +use crate::system; use crate::system::get_primary_mount_point; use crate::{action::Action, mode::Scene}; use color_eyre::eyre::{eyre, Result}; @@ -103,6 +105,9 @@ pub struct AppData { pub nodes_to_start: usize, pub storage_mountpoint: Option, pub storage_drive: Option, + pub connection_mode: Option, + pub port_from: Option, + pub port_to: Option, } impl Default for AppData { @@ -112,6 +117,9 @@ impl Default for AppData { nodes_to_start: 1, storage_mountpoint: None, storage_drive: None, + connection_mode: None, + port_from: None, + port_to: None, } } } @@ -133,9 +141,20 @@ impl AppData { let data = std::fs::read_to_string(&config_path) .map_err(|_| color_eyre::eyre::eyre!("Failed to read app data file"))?; - let app_data: AppData = serde_json::from_str(&data) + let mut app_data: AppData = serde_json::from_str(&data) .map_err(|_| color_eyre::eyre::eyre!("Failed to parse app data"))?; + if app_data.storage_mountpoint.is_none() || app_data.storage_drive.is_none() { + // If the storage drive is not set, set it to the default mount point + let drive_info = system::get_default_mount_point()?; + app_data.storage_drive = Some(drive_info.0); + app_data.storage_mountpoint = Some(drive_info.1); + debug!("Setting storage drive to {:?}", app_data.storage_mountpoint); + } + + if app_data.connection_mode.is_none() { + app_data.connection_mode = Some(ConnectionMode::default()); + } Ok(app_data) } diff --git a/node-launchpad/src/connection_mode.rs b/node-launchpad/src/connection_mode.rs new file mode 100644 index 0000000000..fd8bd1093b --- /dev/null +++ b/node-launchpad/src/connection_mode.rs @@ -0,0 +1,71 @@ +use std::fmt::{Display, Formatter, Result}; + +use serde::{Deserialize, Serialize}; +use strum::EnumIter; + +#[derive(Clone, Debug, Default, EnumIter)] +pub enum ConnectionMode { + #[default] + Automatic, + HomeNetwork, + UPnP, + CustomPorts, +} + +impl Display for ConnectionMode { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + ConnectionMode::HomeNetwork => write!(f, "Home Network"), + ConnectionMode::UPnP => write!(f, "UPnP"), + ConnectionMode::CustomPorts => write!(f, "Custom Ports"), + ConnectionMode::Automatic => write!(f, "Automatic"), + } + } +} + +impl PartialEq for ConnectionMode { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (ConnectionMode::HomeNetwork, ConnectionMode::HomeNetwork) + | (ConnectionMode::UPnP, ConnectionMode::UPnP) + | (ConnectionMode::CustomPorts, ConnectionMode::CustomPorts) + | (ConnectionMode::Automatic, ConnectionMode::Automatic) + ) + } +} + +impl Eq for ConnectionMode {} + +impl<'de> Deserialize<'de> for ConnectionMode { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "Home Network" => Ok(ConnectionMode::HomeNetwork), + "UPnP" => Ok(ConnectionMode::UPnP), + "Custom Ports" => Ok(ConnectionMode::CustomPorts), + "Automatic" => Ok(ConnectionMode::Automatic), + _ => Err(serde::de::Error::custom(format!( + "Invalid ConnectionMode: {s:?}" + ))), + } + } +} + +impl Serialize for ConnectionMode { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let s = match self { + ConnectionMode::HomeNetwork => "Home Network", + ConnectionMode::UPnP => "UPnP", + ConnectionMode::CustomPorts => "Custom Ports", + ConnectionMode::Automatic => "Automatic", + }; + serializer.serialize_str(s) + } +} diff --git a/node-launchpad/src/lib.rs b/node-launchpad/src/lib.rs index dc0ecc2e73..aa18661f27 100644 --- a/node-launchpad/src/lib.rs +++ b/node-launchpad/src/lib.rs @@ -10,6 +10,7 @@ pub mod action; pub mod app; pub mod components; pub mod config; +pub mod connection_mode; pub mod mode; pub mod node_mgmt; pub mod node_stats; diff --git a/node-launchpad/src/mode.rs b/node-launchpad/src/mode.rs index 641c433dfc..9a515383e5 100644 --- a/node-launchpad/src/mode.rs +++ b/node-launchpad/src/mode.rs @@ -15,6 +15,8 @@ pub enum Scene { Options, Help, ChangeDrivePopUp, + ChangeConnectionModePopUp, + ChangePortsPopUp, BetaProgrammePopUp, ManageNodesPopUp, ResetNodesPopUp, From ac4ad9105d8e8cf52566cfa9890f4d07fa120405 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 21 Aug 2024 11:21:36 +0200 Subject: [PATCH 02/24] feat(launchpad): port selection screen and user flow --- node-launchpad/src/app.rs | 4 +- node-launchpad/src/components/options.rs | 41 ++- .../src/components/popup/connection_mode.rs | 8 +- .../src/components/popup/port_range.rs | 341 ++++++++++++++++++ node-launchpad/src/connection_mode.rs | 16 +- 5 files changed, 381 insertions(+), 29 deletions(-) diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 6e1342e848..c0621eda10 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -16,7 +16,7 @@ use crate::{ popup::{ beta_programme::BetaProgramme, change_drive::ChangeDrivePopup, connection_mode::ChangeConnectionModePopUp, manage_nodes::ManageNodes, - reset_nodes::ResetNodesPopup, + port_range::PortRangePopUp, reset_nodes::ResetNodesPopup, }, Component, }, @@ -115,6 +115,7 @@ impl App { let manage_nodes = ManageNodes::new(app_data.nodes_to_start, storage_mountpoint.clone())?; let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; + let port_range = PortRangePopUp::new(connection_mode, port_from, port_to); let beta_programme = BetaProgramme::new(app_data.discord_username.clone()); Ok(Self { @@ -130,6 +131,7 @@ impl App { // Popups Box::new(change_drive), Box::new(change_connection_mode), + Box::new(port_range), Box::new(beta_programme), Box::new(reset_nodes), Box::new(manage_nodes), diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index a19be98b4b..cf5b8f5f3f 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -17,7 +17,9 @@ use crate::{ components::header::Header, connection_mode::ConnectionMode, mode::{InputMode, Scene}, - style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE}, + style::{ + COOL_GREY, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE, + }, system, }; use sn_node_manager::config::get_service_log_dir_path; @@ -161,23 +163,40 @@ impl Component for Options { .alignment(Alignment::Left), ), Cell::from( - Line::from(vec![Span::styled( - format!( - " {}-{} ", - self.port_from.unwrap_or(0), - self.port_to.unwrap_or(0) - ), - Style::default().fg(VIVID_SKY_BLUE), - )]) + Line::from(vec![ + if self.connection_mode == ConnectionMode::CustomPorts { + Span::styled( + format!( + " {}-{} ", + self.port_from.unwrap_or(0), + self.port_to.unwrap_or(0) + ), + Style::default().fg(VIVID_SKY_BLUE), + ) + } else { + Span::styled(" Auto ", Style::default().fg(COOL_GREY)) + }, + ]) .alignment(Alignment::Left), ), Cell::from( Line::from(vec![ Span::styled( " Edit Port Range ", - Style::default().fg(VERY_LIGHT_AZURE), + if self.connection_mode == ConnectionMode::CustomPorts { + Style::default().fg(VERY_LIGHT_AZURE) + } else { + Style::default().fg(COOL_GREY) + }, + ), + Span::styled( + " [Ctrl+P] ", + if self.connection_mode == ConnectionMode::CustomPorts { + Style::default().fg(GHOST_WHITE) + } else { + Style::default().fg(COOL_GREY) + }, ), - Span::styled(" [Ctrl+P] ", Style::default().fg(GHOST_WHITE)), ]) .alignment(Alignment::Right), ), diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs index 9b0c4d3d08..3e0b4b8b8f 100644 --- a/node-launchpad/src/components/popup/connection_mode.rs +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -132,9 +132,13 @@ impl Component for ChangeConnectionModePopUp { self.connection_mode_selection.connection_mode.clone(), ), Action::OptionsActions(OptionsActions::UpdateConnectionMode( - connection_mode.connection_mode, + connection_mode.clone().connection_mode, )), - Action::SwitchScene(Scene::Options), + if connection_mode.connection_mode == ConnectionMode::CustomPorts { + Action::SwitchScene(Scene::ChangePortsPopUp) + } else { + Action::SwitchScene(Scene::Options) + }, ] } else { debug!("Got Enter, but no new selection. We should not do anything"); diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index e69de29bb2..9ce7ae48f1 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -0,0 +1,341 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use super::super::utils::centered_rect_fixed; +use super::super::Component; +use crate::{ + action::{Action, OptionsActions}, + connection_mode::ConnectionMode, + mode::{InputMode, Scene}, + style::{clear_area, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, VIVID_SKY_BLUE}, +}; +use color_eyre::Result; +use crossterm::event::{Event, KeyCode, KeyEvent}; +use ratatui::{prelude::*, widgets::*}; +use tui_input::{backend::crossterm::EventHandler, Input}; + +const PORT_MAX: u16 = 65535; +const PORT_MIN: u16 = 1024; +const INPUT_SIZE: u16 = 5; +const INPUT_AREA: u16 = INPUT_SIZE + 2; // +2 for the left and right padding + +#[derive(PartialEq)] +enum FocusInput { + PortFrom, + PortTo, +} + +pub struct PortRangePopUp { + active: bool, + connection_mode: ConnectionMode, + port_from: Input, + port_to: Input, + port_from_old_value: u16, + port_to_old_value: u16, + focus: FocusInput, + can_save: bool, +} + +impl PortRangePopUp { + pub fn new(connection_mode: ConnectionMode, port_from: u16, port_to: u16) -> Self { + Self { + active: false, + connection_mode, + port_from: Input::default().with_value(port_from.to_string()), + port_to: Input::default().with_value(port_to.to_string()), + port_from_old_value: Default::default(), + port_to_old_value: Default::default(), + focus: FocusInput::PortFrom, + can_save: false, + } + } + + pub fn validate(&mut self) { + if self.port_from.value().is_empty() || self.port_to.value().is_empty() { + self.can_save = false; + } else { + let port_from: u16 = self.port_from.value().parse().unwrap_or_default(); + let port_to: u16 = self.port_to.value().parse().unwrap_or_default(); + self.can_save = (PORT_MIN..=PORT_MAX).contains(&port_from) + && (PORT_MIN..=PORT_MAX).contains(&port_to) + && port_from <= port_to; + } + } +} + +impl Component for PortRangePopUp { + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + if !self.active { + return Ok(vec![]); + } + // while in entry mode, keybinds are not captured, so gotta exit entry mode from here + let send_back = match key.code { + KeyCode::Enter => { + let port_from = self.port_from.value(); + let port_to = self.port_to.value(); + + if port_from.is_empty() || port_to.is_empty() || !self.can_save { + debug!("Got Enter, but port_from or port_to is empty, ignoring."); + return Ok(vec![]); + } + debug!("Got Enter, saving the ports and switching to Options Screen",); + vec![ + Action::StorePortRange( + self.port_from.value().parse().unwrap_or_default(), + self.port_to.value().parse().unwrap_or_default(), + ), + Action::OptionsActions(OptionsActions::UpdatePortRange( + self.port_from.value().parse().unwrap_or_default(), + self.port_to.value().parse().unwrap_or_default(), + )), + Action::SwitchScene(Scene::Options), + ] + } + KeyCode::Esc => { + debug!("Got Esc, restoring the old values and switching to actual screen"); + // reset to old value + self.port_from = self + .port_from + .clone() + .with_value(self.port_from_old_value.to_string()); + self.port_to = self + .port_to + .clone() + .with_value(self.port_to_old_value.to_string()); + vec![Action::SwitchScene(Scene::Options)] + } + KeyCode::Char(c) if !c.is_numeric() => vec![], + KeyCode::Tab => { + self.focus = if self.focus == FocusInput::PortFrom { + FocusInput::PortTo + } else { + FocusInput::PortFrom + }; + vec![] + } + KeyCode::Up => { + if self.focus == FocusInput::PortFrom + && self.port_from.value().parse::().unwrap_or_default() < PORT_MAX + { + self.port_from = self.port_from.clone().with_value( + (self.port_from.value().parse::().unwrap_or_default() + 1).to_string(), + ); + } else if self.focus == FocusInput::PortTo + && self.port_from.value().parse::().unwrap_or_default() > PORT_MIN + { + self.port_to = self.port_to.clone().with_value( + (self.port_to.value().parse::().unwrap_or_default() + 1).to_string(), + ); + } + self.validate(); + vec![] + } + KeyCode::Down => { + if self.focus == FocusInput::PortFrom + && self.port_from.value().parse::().unwrap_or_default() > PORT_MIN + { + self.port_from = self.port_from.clone().with_value( + (self.port_from.value().parse::().unwrap_or_default() - 1).to_string(), + ); + } else if self.focus == FocusInput::PortTo + && self.port_to.value().parse::().unwrap_or_default() < PORT_MAX + { + self.port_to = self.port_to.clone().with_value( + (self.port_to.value().parse::().unwrap_or_default() - 1).to_string(), + ); + } + self.validate(); + vec![] + } + KeyCode::Backspace => { + // if max limit reached, we should allow Backspace to work. + if self.focus == FocusInput::PortFrom { + self.port_from.handle_event(&Event::Key(key)); + } else if self.focus == FocusInput::PortTo { + self.port_to.handle_event(&Event::Key(key)); + } + self.validate(); + vec![] + } + _ => { + // if max limit reached, we should not allow any more inputs. + if self.focus == FocusInput::PortFrom + && self.port_from.value().len() < INPUT_SIZE as usize + { + self.port_from.handle_event(&Event::Key(key)); + } else if self.focus == FocusInput::PortTo + && self.port_to.value().len() < INPUT_SIZE as usize + { + self.port_to.handle_event(&Event::Key(key)); + } + + self.validate(); + vec![] + } + }; + Ok(send_back) + } + + fn update(&mut self, action: Action) -> Result> { + let send_back = match action { + Action::SwitchScene(scene) => match scene { + Scene::ChangePortsPopUp => { + if self.connection_mode == ConnectionMode::CustomPorts { + self.active = true; + self.validate(); + self.port_from_old_value = + self.port_from.value().parse().unwrap_or_default(); + self.port_to_old_value = self.port_to.value().parse().unwrap_or_default(); + // Set to InputMode::Entry as we want to handle everything within our handle_key_events + // so by default if this scene is active, we capture inputs. + Some(Action::SwitchInputMode(InputMode::Entry)) + } else { + self.active = false; + Some(Action::SwitchScene(Scene::Options)) + } + } + _ => { + self.active = false; + None + } + }, + // Useful when the user has selected a connection mode but didn't confirm it + Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { + self.connection_mode = connection_mode; + None + } + _ => None, + }; + Ok(send_back) + } + + fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { + if !self.active { + return Ok(()); + } + + let layer_zero = centered_rect_fixed(52, 15, area); + + let layer_one = Layout::new( + Direction::Vertical, + [ + // for the pop_up_border + Constraint::Length(2), + // for the input field + Constraint::Min(1), + // for the pop_up_border + Constraint::Length(1), + ], + ) + .split(layer_zero); + + // layer zero + let pop_up_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(" Custom Ports ") + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(VIVID_SKY_BLUE)), + ); + clear_area(f, layer_zero); + + // split into 4 parts, for the prompt, input, text, dash , and buttons + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the prompt text + Constraint::Length(3), + // for the input + Constraint::Length(2), + // for the text + Constraint::Length(3), + // gap + Constraint::Length(3), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + let prompt = Paragraph::new("Enter Port Number") + .bold() + .alignment(Alignment::Center); + + f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]); + + let spaces_from = " ".repeat((INPUT_AREA - 1) as usize - self.port_from.value().len()); + let spaces_to = " ".repeat((INPUT_AREA - 1) as usize - self.port_to.value().len()); + + let input_line = Line::from(vec![ + Span::styled( + format!("{}{} ", spaces_from, self.port_from.value()), + if self.focus == FocusInput::PortFrom { + Style::default() + .fg(VIVID_SKY_BLUE) + .bg(INDIGO) + .underlined() + .underline_color(VIVID_SKY_BLUE) + } else { + Style::default().fg(VIVID_SKY_BLUE) + }, + ), + Span::styled(" to ", Style::default().fg(GHOST_WHITE)), + Span::styled( + format!("{}{} ", spaces_to, self.port_to.value()), + if self.focus == FocusInput::PortTo { + Style::default() + .fg(VIVID_SKY_BLUE) + .bg(INDIGO) + .underlined() + .underline_color(VIVID_SKY_BLUE) + } else { + Style::default().fg(VIVID_SKY_BLUE) + }, + ), + ]) + .alignment(Alignment::Center); + + f.render_widget(input_line, layer_two[1]); + + let text = Paragraph::new("Choose the start of the port range. The range will then match the number of nodes on this device.") + .block(block::Block::default().padding(Padding::horizontal(2))) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + f.render_widget(text.fg(GHOST_WHITE), layer_two[2]); + + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[3]); + + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[4]); + + let button_no = Line::from(vec![Span::styled( + " Cancel [Esc]", + Style::default().fg(LIGHT_PERIWINKLE), + )]); + let button_yes_style = if self.can_save { + Style::default().fg(EUCALYPTUS) + } else { + Style::default().fg(LIGHT_PERIWINKLE) + }; + f.render_widget(button_no, buttons_layer[0]); + let button_yes = Line::from(vec![Span::styled( + "Save Port Range [Enter]", + button_yes_style, + )]); + f.render_widget(button_yes, buttons_layer[1]); + + f.render_widget(pop_up_border, layer_zero); + + Ok(()) + } +} diff --git a/node-launchpad/src/connection_mode.rs b/node-launchpad/src/connection_mode.rs index fd8bd1093b..51286d7b0a 100644 --- a/node-launchpad/src/connection_mode.rs +++ b/node-launchpad/src/connection_mode.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter, Result}; use serde::{Deserialize, Serialize}; use strum::EnumIter; -#[derive(Clone, Debug, Default, EnumIter)] +#[derive(Clone, Debug, Default, EnumIter, Eq, PartialEq)] pub enum ConnectionMode { #[default] Automatic, @@ -23,20 +23,6 @@ impl Display for ConnectionMode { } } -impl PartialEq for ConnectionMode { - fn eq(&self, other: &Self) -> bool { - matches!( - (self, other), - (ConnectionMode::HomeNetwork, ConnectionMode::HomeNetwork) - | (ConnectionMode::UPnP, ConnectionMode::UPnP) - | (ConnectionMode::CustomPorts, ConnectionMode::CustomPorts) - | (ConnectionMode::Automatic, ConnectionMode::Automatic) - ) - } -} - -impl Eq for ConnectionMode {} - impl<'de> Deserialize<'de> for ConnectionMode { fn deserialize(deserializer: D) -> std::result::Result where From 4166c7a035dba3fd04c68124b9e414f6f4efc154 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 21 Aug 2024 13:55:01 +0200 Subject: [PATCH 03/24] fix(launchpad): updating ports on status screen --- node-launchpad/src/components/status.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 911727d8aa..7350424b13 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -393,6 +393,10 @@ impl Component for Status { Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { self.connection_mode = connection_mode; } + Action::OptionsActions(OptionsActions::UpdatePortRange(port_from, port_to)) => { + self.port_from = Some(port_from); + self.port_to = Some(port_to); + } _ => {} } Ok(None) From 5e8bde8f8abb53f6e67ad1b7f53c68e392abc336 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 21 Aug 2024 13:55:28 +0200 Subject: [PATCH 04/24] fix(launchpad): beta programme popups restyling --- .../src/components/popup/beta_programme.rs | 111 ++++++++++++------ 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/node-launchpad/src/components/popup/beta_programme.rs b/node-launchpad/src/components/popup/beta_programme.rs index 18c8192faa..bcfbe45acd 100644 --- a/node-launchpad/src/components/popup/beta_programme.rs +++ b/node-launchpad/src/components/popup/beta_programme.rs @@ -11,7 +11,7 @@ use super::super::Component; use crate::{ action::{Action, OptionsActions}, mode::{InputMode, Scene}, - style::{clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE}, + style::{clear_area, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, VIVID_SKY_BLUE}, widgets::hyperlink::Hyperlink, }; use color_eyre::Result; @@ -19,6 +19,9 @@ use crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::{prelude::*, widgets::*}; use tui_input::{backend::crossterm::EventHandler, Input}; +const INPUT_SIZE_USERNAME: u16 = 32; // as per discord docs +const INPUT_AREA_USERNAME: u16 = INPUT_SIZE_USERNAME + 2; // +2 for the padding + pub struct BetaProgramme { /// Whether the component is active right now, capturing keystrokes + draw things. active: bool, @@ -199,9 +202,9 @@ impl Component for BetaProgramme { // for the prompt text Constraint::Length(3), // for the input - Constraint::Length(3), + Constraint::Length(1), // for the text - Constraint::Length(4), + Constraint::Length(6), // gap Constraint::Length(1), // for the buttons @@ -211,26 +214,24 @@ impl Component for BetaProgramme { .split(layer_one[1]); let prompt_text = Paragraph::new("Discord Username associated with this device:") + .block(Block::default()) .alignment(Alignment::Center) .fg(GHOST_WHITE); f.render_widget(prompt_text, layer_two[0]); - let input = Paragraph::new(self.discord_input_filed.value()) - .alignment(Alignment::Center) - .fg(VIVID_SKY_BLUE); - f.set_cursor( - // Put cursor past the end of the input text - layer_two[1].x - + (layer_two[1].width / 2) as u16 - + (self.discord_input_filed.value().len() / 2) as u16 - + if self.discord_input_filed.value().len() % 2 != 0 { - 1 - } else { - 0 - }, - layer_two[1].y, + let spaces = " ".repeat( + (INPUT_AREA_USERNAME - 1) as usize - self.discord_input_filed.value().len(), ); + let input = Paragraph::new(Span::styled( + format!("{}{} ", spaces, self.discord_input_filed.value()), + Style::default() + .fg(VIVID_SKY_BLUE) + .bg(INDIGO) + .underlined() + .underline_color(VIVID_SKY_BLUE), + )) + .alignment(Alignment::Center); f.render_widget(input, layer_two[1]); let text = Paragraph::new(Text::from(vec![ @@ -238,7 +239,11 @@ impl Component for BetaProgramme { Line::raw("and any Nanos left on this device will be lost."), ])) .alignment(Alignment::Center) - .block(Block::default().padding(Padding::horizontal(2))); + .block( + Block::default() + .padding(Padding::horizontal(2)) + .padding(Padding::top(2)), + ); f.render_widget(text.fg(GHOST_WHITE), layer_two[2]); @@ -276,7 +281,7 @@ impl Component for BetaProgramme { Direction::Vertical, [ // for the text - Constraint::Length(6), + Constraint::Length(7), // for the hypertext Constraint::Length(1), // gap @@ -287,8 +292,18 @@ impl Component for BetaProgramme { ) .split(layer_one[1]); - let text = Paragraph::new(" Earn a slice of millions of tokens created at\n the genesis of the Autonomi Network by running\n nodes to build and test the Beta.\n\n To continue in the beta Rewards Program you\n agree to the Terms and Conditions found here:"); + let text = Paragraph::new(vec![ + Line::from(Span::styled("Earn a slice of millions of tokens created at the genesis of the Autonomi Network by running nodes to build and test the Beta.",Style::default())), + Line::from(Span::styled("\n\n",Style::default())), + Line::from(Span::styled("To continue in the beta Rewards Program you agree to the Terms and Conditions found here:",Style::default())), + Line::from(Span::styled("\n\n",Style::default())), + ] + ) + .block(Block::default().padding(Padding::horizontal(2))) + .wrap(Wrap { trim: false }); + f.render_widget(text.fg(GHOST_WHITE), layer_two[0]); + let link = Hyperlink::new( Span::styled( " https://autonomi.com/beta/terms", @@ -336,7 +351,17 @@ impl Component for BetaProgramme { ) .split(layer_one[1]); - let text = Paragraph::new(" Terms and conditions not accepted\n Beta Rewards Program entry not approved\n You can still run nodes on the network, but\n you will not be part of the Beta Rewards\n Program.\n"); + let text = Paragraph::new(vec![ + Line::from(Span::styled("Terms and conditions not accepted.",Style::default())), + Line::from(Span::styled("\n\n",Style::default())), + Line::from(Span::styled("Beta Rewards Program entry not approved.",Style::default())), + Line::from(Span::styled("\n\n",Style::default())), + Line::from(Span::styled("You can still run nodes on the network, but you will not be part of the Beta Rewards Program.",Style::default())), + ] + ) + .block(Block::default().padding(Padding::horizontal(2))) + .wrap(Wrap { trim: false }); + f.render_widget(text.fg(GHOST_WHITE), layer_two[0]); let dash = Block::new() @@ -359,9 +384,9 @@ impl Component for BetaProgramme { // for the input Constraint::Length(2), // for the text - Constraint::Length(3), + Constraint::Length(5), // gap - Constraint::Length(3), + Constraint::Length(1), // for the buttons Constraint::Length(1), ], @@ -373,24 +398,34 @@ impl Component for BetaProgramme { f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]); - let input = Paragraph::new(self.discord_input_filed.value()) - .alignment(Alignment::Center) - .fg(VIVID_SKY_BLUE); - f.set_cursor( - // Put cursor past the end of the input text - layer_two[1].x - + (layer_two[1].width / 2) as u16 - + (self.discord_input_filed.value().len() / 2) as u16 - + if self.discord_input_filed.value().len() % 2 != 0 { - 1 - } else { - 0 - }, - layer_two[1].y, + let spaces = " ".repeat( + (INPUT_AREA_USERNAME - 1) as usize - self.discord_input_filed.value().len(), ); + let input = Paragraph::new(Span::styled( + format!("{}{} ", spaces, self.discord_input_filed.value()), + Style::default() + .fg(VIVID_SKY_BLUE) + .bg(INDIGO) + .underlined() + .underline_color(VIVID_SKY_BLUE), + )) + .alignment(Alignment::Center); f.render_widget(input, layer_two[1]); - let text = Paragraph::new(" Submit your username and track your progress on\n our Discord server. Note: your username may be\n different from your display name."); + let text = Paragraph::new(vec![ + Line::from(Span::styled( + "Submit your username and track your progress on our Discord server.", + Style::default(), + )), + Line::from(Span::styled("\n\n", Style::default())), + Line::from(Span::styled( + "Note: your username may be different from your display name.", + Style::default(), + )), + ]) + .block(Block::default().padding(Padding::horizontal(2))) + .wrap(Wrap { trim: false }); + f.render_widget(text.fg(GHOST_WHITE), layer_two[2]); let dash = Block::new() From d67b34443d84bd2f034f685f0124635a8d13a397 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 21 Aug 2024 17:30:50 +0200 Subject: [PATCH 05/24] feat(launchpad): more items in help screen --- node-launchpad/src/components/help.rs | 55 +++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/node-launchpad/src/components/help.rs b/node-launchpad/src/components/help.rs index 5e9b286b42..3bc293ad0a 100644 --- a/node-launchpad/src/components/help.rs +++ b/node-launchpad/src/components/help.rs @@ -49,7 +49,7 @@ impl Component for Help { .constraints(vec![ Constraint::Length(1), Constraint::Min(7), - Constraint::Max(9), + Constraint::Max(13), ]) .split(area); @@ -171,12 +171,12 @@ impl Component for Help { ])), Cell::from(Line::from(vec![ Span::styled("[Ctrl+S] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Start Nodes", Style::default().fg(EUCALYPTUS)), + Span::styled("Start All Nodes", Style::default().fg(EUCALYPTUS)), ])), Cell::from(Line::from(vec![ - Span::styled("[Ctrl+B] ", Style::default().fg(GHOST_WHITE)), + Span::styled("[Ctrl+K] ", Style::default().fg(GHOST_WHITE)), Span::styled( - "Edit Discord Username", + "Switch Connection Mode", Style::default().fg(VERY_LIGHT_AZURE), ), ])), @@ -194,11 +194,14 @@ impl Component for Help { ])), Cell::from(Line::from(vec![ Span::styled("[Ctrl+X] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Stop Nodes", Style::default().fg(EUCALYPTUS)), + Span::styled("Stop All Nodes", Style::default().fg(EUCALYPTUS)), ])), Cell::from(Line::from(vec![ - Span::styled("[Ctrl+L] ", Style::default().fg(GHOST_WHITE)), - Span::styled("Open Logs Folder", Style::default().fg(VERY_LIGHT_AZURE)), + Span::styled("[Ctrl+P] ", Style::default().fg(GHOST_WHITE)), + Span::styled( + "Edit Custom Port Range", + Style::default().fg(VERY_LIGHT_AZURE), + ), ])), ]), Row::new(vec![ @@ -216,7 +219,43 @@ impl Component for Help { Span::styled("[Ctrl+R] ", Style::default().fg(GHOST_WHITE)), Span::styled("Reset All Nodes", Style::default().fg(EUCALYPTUS)), ])), - Cell::from(""), + Cell::from(Line::from(vec![ + Span::styled("[Ctrl+B] ", Style::default().fg(GHOST_WHITE)), + Span::styled( + "Edit Discord Username", + Style::default().fg(VERY_LIGHT_AZURE), + ), + ])), + ]), + Row::new(vec![ + // Empty row for padding + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + ]), + Row::new(vec![ + // Empty row for padding + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + Cell::from(Line::from(vec![ + Span::styled("[Ctrl+L] ", Style::default().fg(GHOST_WHITE)), + Span::styled("Open Logs Folder", Style::default().fg(VERY_LIGHT_AZURE)), + ])), + ]), + Row::new(vec![ + // Empty row for padding + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + ]), + Row::new(vec![ + // Empty row for padding + Cell::from(Span::raw(" ")), + Cell::from(Span::raw(" ")), + Cell::from(Line::from(vec![ + Span::styled("[Ctrl+L] ", Style::default().fg(GHOST_WHITE)), + Span::styled("Open Logs Folder", Style::default().fg(VERY_LIGHT_AZURE)), + ])), ]), ]; From aa9b9e69dbd1b955bc196e6ada78ab1ebcdd587c Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 21 Aug 2024 17:31:12 +0200 Subject: [PATCH 06/24] feat(launchpad): reset nodes input styling --- .../src/components/popup/reset_nodes.rs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/node-launchpad/src/components/popup/reset_nodes.rs b/node-launchpad/src/components/popup/reset_nodes.rs index 7d83ef17bf..220f5e5fca 100644 --- a/node-launchpad/src/components/popup/reset_nodes.rs +++ b/node-launchpad/src/components/popup/reset_nodes.rs @@ -10,13 +10,16 @@ use super::super::{utils::centered_rect_fixed, Component}; use crate::{ action::{Action, OptionsActions}, mode::{InputMode, Scene}, - style::{clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE}, + style::{clear_area, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, VIVID_SKY_BLUE}, }; use color_eyre::Result; use crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::{prelude::*, widgets::*}; use tui_input::{backend::crossterm::EventHandler, Input}; +const INPUT_SIZE: u16 = 5; +const INPUT_AREA: u16 = INPUT_SIZE + 2; // +2 for the left and right padding + #[derive(Default)] pub struct ResetNodesPopup { /// Whether the component is active right now, capturing keystrokes + draw things. @@ -56,7 +59,7 @@ impl Component for ResetNodesPopup { } _ => { // max char limit - if self.confirmation_input_field.value().chars().count() < 10 { + if self.confirmation_input_field.value().chars().count() < INPUT_SIZE as usize { self.confirmation_input_field.handle_event(&Event::Key(key)); } vec![] @@ -145,21 +148,19 @@ impl Component for ResetNodesPopup { f.render_widget(prompt, layer_two[0]); - let input = Paragraph::new(self.confirmation_input_field.value()) - .alignment(Alignment::Center) - .fg(VIVID_SKY_BLUE); - f.set_cursor( - // Put cursor past the end of the input text - layer_two[1].x - + (layer_two[1].width / 2) as u16 - + (self.confirmation_input_field.value().len() / 2) as u16 - + if self.confirmation_input_field.value().len() % 2 != 0 { - 1 - } else { - 0 - }, - layer_two[1].y, - ); + let spaces = + " ".repeat((INPUT_AREA - 1) as usize - self.confirmation_input_field.value().len()); + + let input = Paragraph::new(Span::styled( + format!("{}{} ", spaces, self.confirmation_input_field.value()), + Style::default() + .fg(VIVID_SKY_BLUE) + .bg(INDIGO) + .underlined() + .underline_color(VIVID_SKY_BLUE), + )) + .alignment(Alignment::Center); + f.render_widget(input, layer_two[1]); let text = Paragraph::new("This will clear out all the nodes and all the stored data. You should still keep all your earned rewards.") From dcf490430c681d800133f49f329f074f8c997eb6 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 22 Aug 2024 11:52:24 +0200 Subject: [PATCH 07/24] feat(launchpad): better interaction when resetting nodes --- .../src/components/popup/reset_nodes.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/node-launchpad/src/components/popup/reset_nodes.rs b/node-launchpad/src/components/popup/reset_nodes.rs index 220f5e5fca..c021598278 100644 --- a/node-launchpad/src/components/popup/reset_nodes.rs +++ b/node-launchpad/src/components/popup/reset_nodes.rs @@ -25,6 +25,7 @@ pub struct ResetNodesPopup { /// Whether the component is active right now, capturing keystrokes + draw things. active: bool, confirmation_input_field: Input, + can_reset: bool, } impl Component for ResetNodesPopup { @@ -34,17 +35,14 @@ impl Component for ResetNodesPopup { } let send_back = match key.code { KeyCode::Enter => { - let input = self.confirmation_input_field.value().to_string(); - - if input.to_lowercase() == "reset" { + if self.can_reset { debug!("Got reset, sending Reset action and switching to Options"); vec![ Action::OptionsActions(OptionsActions::ResetNodes), Action::SwitchScene(Scene::Options), ] } else { - debug!("Got Enter, but RESET is not typed. Switching to Options"); - vec![Action::SwitchScene(Scene::Options)] + vec![] } } KeyCode::Esc => { @@ -55,6 +53,8 @@ impl Component for ResetNodesPopup { KeyCode::Backspace => { // if max limit reached, we should allow Backspace to work. self.confirmation_input_field.handle_event(&Event::Key(key)); + let input = self.confirmation_input_field.value().to_string(); + self.can_reset = input.to_lowercase() == "reset"; vec![] } _ => { @@ -62,6 +62,8 @@ impl Component for ResetNodesPopup { if self.confirmation_input_field.value().chars().count() < INPUT_SIZE as usize { self.confirmation_input_field.handle_event(&Event::Key(key)); } + let input = self.confirmation_input_field.value().to_string(); + self.can_reset = input.to_lowercase() == "reset"; vec![] } }; @@ -193,7 +195,11 @@ impl Component for ResetNodesPopup { let button_yes = Line::from(vec![Span::styled( "Reset Nodes [Enter]", - Style::default().fg(EUCALYPTUS), + if self.can_reset { + Style::default().fg(EUCALYPTUS) + } else { + Style::default().fg(LIGHT_PERIWINKLE) + }, )]) .alignment(Alignment::Right); From 3ec6006b9ba74446e7a5a10046c6a65cd7df0094 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 22 Aug 2024 11:53:10 +0200 Subject: [PATCH 08/24] feat(launchpad): connection modes integration with service --- node-launchpad/src/app.rs | 16 +- node-launchpad/src/components/status.rs | 231 ++++++++++++++---------- node-launchpad/src/node_mgmt.rs | 102 ++++++++--- 3 files changed, 218 insertions(+), 131 deletions(-) diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index c0621eda10..668f308308 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -18,6 +18,7 @@ use crate::{ connection_mode::ChangeConnectionModePopUp, manage_nodes::ManageNodes, port_range::PortRangePopUp, reset_nodes::ResetNodesPopup, }, + status::{Status, StatusConfig}, Component, }, config::{get_launchpad_nodes_data_dir_path, AppData, Config}, @@ -87,17 +88,18 @@ impl App { .unwrap_or(get_primary_mount_point_name()?); // Main Screens - let status = Status::new( - app_data.nodes_to_start, - &app_data.discord_username, + let status_config = StatusConfig { + allocated_disk_space: app_data.nodes_to_start, + discord_username: app_data.discord_username.clone(), peers_args, safenode_path, data_dir_path, connection_mode, - Some(port_from), - Some(port_to), - ) - .await?; + port_from: Some(port_from), + port_to: Some(port_to), + }; + + let status = Status::new(status_config).await?; let options = Options::new( storage_mountpoint.clone(), storage_drive.clone(), diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 7350424b13..99175c5a01 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -15,6 +15,7 @@ use super::{ use crate::action::OptionsActions; use crate::config::get_launchpad_nodes_data_dir_path; use crate::connection_mode::ConnectionMode; +use crate::node_mgmt::MaintainNodesArgs; use crate::{ action::{Action, StatusActions}, config::Config, @@ -28,6 +29,7 @@ use color_eyre::eyre::{OptionExt, Result}; use crossterm::event::KeyEvent; use ratatui::text::Span; use ratatui::{prelude::*, widgets::*}; +use sn_node_manager::add_services::config::PortRange; use sn_node_manager::config::get_node_registry_path; use sn_peers_acquisition::PeersArgs; use sn_service_management::{ @@ -85,19 +87,21 @@ pub enum LockRegistryState { ResettingNodes, } +pub struct StatusConfig { + pub allocated_disk_space: usize, + pub discord_username: String, + pub peers_args: PeersArgs, + pub safenode_path: Option, + pub data_dir_path: PathBuf, + pub connection_mode: ConnectionMode, + pub port_from: Option, + pub port_to: Option, +} + impl Status { - pub async fn new( - allocated_disk_space: usize, - discord_username: &str, - peers_args: PeersArgs, - safenode_path: Option, - data_dir_path: PathBuf, - connection_mode: ConnectionMode, - port_from: Option, - port_to: Option, - ) -> Result { + pub async fn new(config: StatusConfig) -> Result { let mut status = Self { - peers_args, + peers_args: config.peers_args, action_sender: Default::default(), config: Default::default(), active: true, @@ -106,15 +110,15 @@ impl Status { error_while_running_nat_detection: 0, node_stats: NodeStats::default(), node_stats_last_update: Instant::now(), - nodes_to_start: allocated_disk_space, + nodes_to_start: config.allocated_disk_space, node_table_state: Default::default(), lock_registry: None, - discord_username: discord_username.to_string(), - safenode_path, - data_dir_path, - connection_mode, - port_from, - port_to, + discord_username: config.discord_username, + safenode_path: config.safenode_path, + data_dir_path: config.data_dir_path, + connection_mode: config.connection_mode, + port_from: config.port_from, + port_to: config.port_to, }; let now = Instant::now(); @@ -284,100 +288,136 @@ impl Component for Status { if we_have_nodes && has_changed { self.lock_registry = Some(LockRegistryState::ResettingNodes); - info!("Resetting safenode services because the discord username was reset."); + info!("Resetting safenode services because the Discord Username was reset."); let action_sender = self.get_actions_sender()?; reset_nodes(action_sender, true); } } Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => { + self.lock_registry = Some(LockRegistryState::ResettingNodes); + info!("Resetting safenode services because the Storage Drive was changed."); let action_sender = self.get_actions_sender()?; reset_nodes(action_sender, false); self.data_dir_path = get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?; } - Action::StatusActions(status_action) => { - match status_action { - StatusActions::NodesStatsObtained(stats) => { - self.node_stats = stats; - } - StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => { - self.lock_registry = None; - self.load_node_registry_and_update_states()?; - } - StatusActions::ResetNodesCompleted { trigger_start_node } => { - self.lock_registry = None; - self.load_node_registry_and_update_states()?; + Action::StoreConnectionMode(connection_mode) => { + self.connection_mode = connection_mode; + self.lock_registry = Some(LockRegistryState::ResettingNodes); + info!("Resetting safenode services because the Connection Mode range was changed."); + let action_sender = self.get_actions_sender()?; + reset_nodes(action_sender, false); + } + Action::StorePortRange(port_from, port_range) => { + self.port_from = Some(port_from); + self.port_to = Some(port_range); + self.lock_registry = Some(LockRegistryState::ResettingNodes); + info!("Resetting safenode services because the Port Range was changed."); + let action_sender = self.get_actions_sender()?; + reset_nodes(action_sender, false); + } + Action::StatusActions(status_action) => match status_action { + StatusActions::NodesStatsObtained(stats) => { + self.node_stats = stats; + } + StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => { + self.lock_registry = None; + self.load_node_registry_and_update_states()?; + } + StatusActions::ResetNodesCompleted { trigger_start_node } => { + self.lock_registry = None; + self.load_node_registry_and_update_states()?; - if trigger_start_node { - debug!("Reset nodes completed. Triggering start nodes."); - return Ok(Some(Action::StatusActions(StatusActions::StartNodes))); - } - debug!("Reset nodes completed"); - } - StatusActions::SuccessfullyDetectedNatStatus => { - debug!("Successfully detected nat status, is_nat_status_determined set to true"); - self.is_nat_status_determined = true; - } - StatusActions::ErrorWhileRunningNatDetection => { - self.error_while_running_nat_detection += 1; - debug!( - "Error while running nat detection. Error count: {}", - self.error_while_running_nat_detection - ); - } - StatusActions::TriggerManageNodes => { - return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp))); - } - StatusActions::PreviousTableItem => { - self.select_previous_table_item(); + if trigger_start_node { + debug!("Reset nodes completed. Triggering start nodes."); + return Ok(Some(Action::StatusActions(StatusActions::StartNodes))); } - StatusActions::NextTableItem => { - self.select_next_table_item(); + debug!("Reset nodes completed"); + } + StatusActions::SuccessfullyDetectedNatStatus => { + debug!( + "Successfully detected nat status, is_nat_status_determined set to true" + ); + self.is_nat_status_determined = true; + } + StatusActions::ErrorWhileRunningNatDetection => { + self.error_while_running_nat_detection += 1; + debug!( + "Error while running nat detection. Error count: {}", + self.error_while_running_nat_detection + ); + } + StatusActions::TriggerManageNodes => { + return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp))); + } + StatusActions::PreviousTableItem => { + self.select_previous_table_item(); + } + StatusActions::NextTableItem => { + self.select_next_table_item(); + } + StatusActions::StartNodes => { + debug!("Got action to start nodes"); + if self.lock_registry.is_some() { + error!("Registry is locked. Cannot start node now."); + return Ok(None); } - StatusActions::StartNodes => { - debug!("Got action to start nodes"); - if self.lock_registry.is_some() { - error!("Registry is locked. Cannot start node now."); - return Ok(None); - } - if self.nodes_to_start == 0 { - info!("Nodes to start not set. Ask for input."); - return Ok(Some(Action::StatusActions( - StatusActions::TriggerManageNodes, - ))); - } - - self.lock_registry = Some(LockRegistryState::StartingNodes); - let action_sender = self.get_actions_sender()?; - info!("Running maintain node count: {:?}", self.nodes_to_start); - - maintain_n_running_nodes( - self.nodes_to_start as u16, - self.discord_username.clone(), - self.peers_args.clone(), - self.should_we_run_nat_detection(), - self.safenode_path.clone(), - Some(self.data_dir_path.clone()), - action_sender, - ); + if self.nodes_to_start == 0 { + info!("Nodes to start not set. Ask for input."); + return Ok(Some(Action::StatusActions( + StatusActions::TriggerManageNodes, + ))); } - StatusActions::StopNodes => { - debug!("Got action to stop nodes"); - if self.lock_registry.is_some() { - error!("Registry is locked. Cannot stop node now."); + + self.lock_registry = Some(LockRegistryState::StartingNodes); + let action_sender = self.get_actions_sender()?; + info!("Running maintain node count: {:?}", self.nodes_to_start); + + let port_range_str = format!( + "{}-{}", + self.port_from.unwrap_or(0), + self.port_to.unwrap_or(0) + ); + + let port_range = match PortRange::parse(&port_range_str) { + Ok(port_range) => port_range, + Err(err) => { + error!("When starting nodes, we got an error while parsing port range: {err:?}"); return Ok(None); } + }; + + let maintain_nodes_args = MaintainNodesArgs { + count: self.nodes_to_start as u16, + owner: self.discord_username.clone(), + peers_args: self.peers_args.clone(), + run_nat_detection: self.should_we_run_nat_detection() + && self.connection_mode == ConnectionMode::Automatic, + safenode_path: self.safenode_path.clone(), + data_dir_path: Some(self.data_dir_path.clone()), + action_sender, + connection_mode: self.connection_mode.clone(), + port_range: Some(port_range), + }; + + maintain_n_running_nodes(maintain_nodes_args); + } + StatusActions::StopNodes => { + debug!("Got action to stop nodes"); + if self.lock_registry.is_some() { + error!("Registry is locked. Cannot stop node now."); + return Ok(None); + } - let running_nodes = self.get_running_nodes(); - self.lock_registry = Some(LockRegistryState::StoppingNodes); - let action_sender = self.get_actions_sender()?; - info!("Stopping node service: {running_nodes:?}"); + let running_nodes = self.get_running_nodes(); + self.lock_registry = Some(LockRegistryState::StoppingNodes); + let action_sender = self.get_actions_sender()?; + info!("Stopping node service: {running_nodes:?}"); - stop_nodes(running_nodes, action_sender); - } + stop_nodes(running_nodes, action_sender); } - } + }, Action::OptionsActions(OptionsActions::ResetNodes) => { debug!("Got action to reset nodes"); if self.lock_registry.is_some() { @@ -390,13 +430,6 @@ impl Component for Status { info!("Got action to reset nodes"); reset_nodes(action_sender, false); } - Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { - self.connection_mode = connection_mode; - } - Action::OptionsActions(OptionsActions::UpdatePortRange(port_from, port_to)) => { - self.port_from = Some(port_from); - self.port_to = Some(port_to); - } _ => {} } Ok(None) diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index 1bfe76588b..f3c81b4de4 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -1,12 +1,14 @@ use std::path::PathBuf; -use sn_node_manager::VerbosityLevel; +use sn_node_manager::{add_services::config::PortRange, VerbosityLevel}; use sn_peers_acquisition::PeersArgs; use tokio::sync::mpsc::UnboundedSender; use crate::action::{Action, StatusActions}; use color_eyre::eyre::Result; +use crate::connection_mode::ConnectionMode; + pub fn stop_nodes(services: Vec, action_sender: UnboundedSender) { tokio::task::spawn_local(async move { if let Err(err) = @@ -24,20 +26,25 @@ pub fn stop_nodes(services: Vec, action_sender: UnboundedSender) }); } -pub fn maintain_n_running_nodes( - count: u16, - owner: String, - peers_args: PeersArgs, - run_nat_detection: bool, - safenode_path: Option, - data_dir_path: Option, - action_sender: UnboundedSender, -) { +pub struct MaintainNodesArgs { + pub count: u16, + pub owner: String, + pub peers_args: PeersArgs, + pub run_nat_detection: bool, + pub safenode_path: Option, + pub data_dir_path: Option, + pub action_sender: UnboundedSender, + pub connection_mode: ConnectionMode, + pub port_range: Option, +} + +pub fn maintain_n_running_nodes(args: MaintainNodesArgs) { tokio::task::spawn_local(async move { - if run_nat_detection { + if args.run_nat_detection { + info!("Running nat detection...."); if let Err(err) = run_nat_detection_process().await { error!("Error while running nat detection {err:?}. Registering the error."); - if let Err(err) = action_sender.send(Action::StatusActions( + if let Err(err) = args.action_sender.send(Action::StatusActions( StatusActions::ErrorWhileRunningNatDetection, )) { error!("Error while sending action: {err:?}"); @@ -47,28 +54,69 @@ pub fn maintain_n_running_nodes( } } - let owner = if owner.is_empty() { None } else { Some(owner) }; + let auto_set_nat_flags: bool = args.connection_mode == ConnectionMode::Automatic; + let upnp: bool = args.connection_mode == ConnectionMode::UPnP; + let home_network: bool = args.connection_mode == ConnectionMode::HomeNetwork; + let custom_ports: Option = if args.connection_mode == ConnectionMode::CustomPorts + { + match args.port_range { + Some(port_range) => { + debug!("Port range to run nodes: {port_range:?}"); + Some(port_range) + } + None => { + debug!("Port range not provided. Using default port range."); + None + } + } + } else { + None + }; + let owner = if args.owner.is_empty() { + None + } else { + Some(args.owner) + }; + + debug!("************"); + debug!( + "Maintaining {} running nodes with the following args:", + args.count + ); + debug!( + " owner: {:?}, peers_args: {:?}, safenode_path: {:?}", + owner, args.peers_args, args.safenode_path + ); + debug!( + " data_dir_path: {:?}, connection_mode: {:?}", + args.data_dir_path, args.connection_mode + ); + debug!( + " auto_set_nat_flags: {:?}, custom_ports: {:?}, upnp: {}, home_network: {}", + auto_set_nat_flags, custom_ports, upnp, home_network + ); + if let Err(err) = sn_node_manager::cmd::node::maintain_n_running_nodes( false, - true, + auto_set_nat_flags, 120, - count, - data_dir_path, + args.count, + args.data_dir_path, true, None, + home_network, false, - false, - None, None, None, None, + custom_ports, owner, - peers_args, + args.peers_args, None, None, - safenode_path, + args.safenode_path, None, - false, + upnp, None, None, VerbosityLevel::Minimal, @@ -76,12 +124,16 @@ pub fn maintain_n_running_nodes( ) .await { - error!("Error while maintaining {count:?} running nodes {err:?}"); + error!( + "Error while maintaining {:?} running nodes {err:?}", + args.count + ); } else { - info!("Maintained {count} running nodes successfully."); + info!("Maintained {} running nodes successfully.", args.count); } - if let Err(err) = - action_sender.send(Action::StatusActions(StatusActions::StartNodesCompleted)) + if let Err(err) = args + .action_sender + .send(Action::StatusActions(StatusActions::StartNodesCompleted)) { error!("Error while sending action: {err:?}"); } From 99fd59cf321cdfd4cfa41a784f663f0b01596de3 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 22 Aug 2024 16:44:26 +0200 Subject: [PATCH 09/24] feat(launchpad): port selection and execution of nodes --- node-launchpad/src/action.rs | 4 +- node-launchpad/src/components/options.rs | 8 +- .../src/components/popup/port_range.rs | 131 +++++++----------- node-launchpad/src/components/status.rs | 31 +++-- node-launchpad/src/config.rs | 4 +- 5 files changed, 78 insertions(+), 100 deletions(-) diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index 043089c842..8ab87cf0eb 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -25,7 +25,7 @@ pub enum Action { StoreStorageDrive(PathBuf, String), StoreConnectionMode(ConnectionMode), - StorePortRange(u16, u16), + StorePortRange(u32, u32), StoreDiscordUserName(String), StoreNodesToStart(usize), @@ -70,7 +70,7 @@ pub enum OptionsActions { TriggerResetNodes, TriggerAccessLogs, UpdateConnectionMode(ConnectionMode), - UpdatePortRange(u16, u16), + UpdatePortRange(u32, u32), UpdateBetaProgrammeUsername(String), UpdateStorageDrive(PathBuf, String), } diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index cf5b8f5f3f..d2605f1759 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -31,8 +31,8 @@ pub struct Options { pub discord_username: String, pub connection_mode: ConnectionMode, pub port_edit: bool, - pub port_from: Option, - pub port_to: Option, + pub port_from: Option, + pub port_to: Option, pub active: bool, pub action_tx: Option>, } @@ -43,8 +43,8 @@ impl Options { storage_drive: String, discord_username: String, connection_mode: ConnectionMode, - port_from: Option, - port_to: Option, + port_from: Option, + port_to: Option, ) -> Result { Ok(Self { storage_mountpoint, diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index 9ce7ae48f1..9b44116a08 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -19,30 +19,24 @@ use crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::{prelude::*, widgets::*}; use tui_input::{backend::crossterm::EventHandler, Input}; -const PORT_MAX: u16 = 65535; -const PORT_MIN: u16 = 1024; -const INPUT_SIZE: u16 = 5; -const INPUT_AREA: u16 = INPUT_SIZE + 2; // +2 for the left and right padding - -#[derive(PartialEq)] -enum FocusInput { - PortFrom, - PortTo, -} +pub const PORT_MAX: u32 = 65535; +pub const PORT_MIN: u32 = 1024; +pub const PORT_ALLOCATION: u32 = 49; // We count the port_from as well +const INPUT_SIZE: u32 = 5; +const INPUT_AREA: u32 = INPUT_SIZE + 2; // +2 for the left and right padding pub struct PortRangePopUp { active: bool, connection_mode: ConnectionMode, port_from: Input, port_to: Input, - port_from_old_value: u16, - port_to_old_value: u16, - focus: FocusInput, + port_from_old_value: u32, + port_to_old_value: u32, can_save: bool, } impl PortRangePopUp { - pub fn new(connection_mode: ConnectionMode, port_from: u16, port_to: u16) -> Self { + pub fn new(connection_mode: ConnectionMode, port_from: u32, port_to: u32) -> Self { Self { active: false, connection_mode, @@ -50,17 +44,16 @@ impl PortRangePopUp { port_to: Input::default().with_value(port_to.to_string()), port_from_old_value: Default::default(), port_to_old_value: Default::default(), - focus: FocusInput::PortFrom, can_save: false, } } pub fn validate(&mut self) { - if self.port_from.value().is_empty() || self.port_to.value().is_empty() { + if self.port_from.value().is_empty() { self.can_save = false; } else { - let port_from: u16 = self.port_from.value().parse().unwrap_or_default(); - let port_to: u16 = self.port_to.value().parse().unwrap_or_default(); + let port_from: u32 = self.port_from.value().parse().unwrap_or_default(); + let port_to: u32 = self.port_to.value().parse().unwrap_or_default(); self.can_save = (PORT_MIN..=PORT_MAX).contains(&port_from) && (PORT_MIN..=PORT_MAX).contains(&port_to) && port_from <= port_to; @@ -110,70 +103,58 @@ impl Component for PortRangePopUp { vec![Action::SwitchScene(Scene::Options)] } KeyCode::Char(c) if !c.is_numeric() => vec![], - KeyCode::Tab => { - self.focus = if self.focus == FocusInput::PortFrom { - FocusInput::PortTo - } else { - FocusInput::PortFrom - }; - vec![] - } KeyCode::Up => { - if self.focus == FocusInput::PortFrom - && self.port_from.value().parse::().unwrap_or_default() < PORT_MAX - { + if self.port_from.value().parse::().unwrap_or_default() < PORT_MAX { self.port_from = self.port_from.clone().with_value( - (self.port_from.value().parse::().unwrap_or_default() + 1).to_string(), - ); - } else if self.focus == FocusInput::PortTo - && self.port_from.value().parse::().unwrap_or_default() > PORT_MIN - { - self.port_to = self.port_to.clone().with_value( - (self.port_to.value().parse::().unwrap_or_default() + 1).to_string(), + (self.port_from.value().parse::().unwrap_or_default() + 1).to_string(), ); - } + let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); + if port_from_value + PORT_ALLOCATION <= PORT_MAX { + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + } else { + self.port_to = Input::default().with_value("-".to_string()); + } + }; self.validate(); vec![] } KeyCode::Down => { - if self.focus == FocusInput::PortFrom - && self.port_from.value().parse::().unwrap_or_default() > PORT_MIN - { + if self.port_from.value().parse::().unwrap_or_default() > 0 { self.port_from = self.port_from.clone().with_value( - (self.port_from.value().parse::().unwrap_or_default() - 1).to_string(), - ); - } else if self.focus == FocusInput::PortTo - && self.port_to.value().parse::().unwrap_or_default() < PORT_MAX - { - self.port_to = self.port_to.clone().with_value( - (self.port_to.value().parse::().unwrap_or_default() - 1).to_string(), + (self.port_from.value().parse::().unwrap_or_default() - 1).to_string(), ); - } + let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); + if port_from_value + PORT_ALLOCATION <= PORT_MAX { + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + } else { + self.port_to = Input::default().with_value("-".to_string()); + } + }; self.validate(); vec![] } KeyCode::Backspace => { - // if max limit reached, we should allow Backspace to work. - if self.focus == FocusInput::PortFrom { - self.port_from.handle_event(&Event::Key(key)); - } else if self.focus == FocusInput::PortTo { - self.port_to.handle_event(&Event::Key(key)); - } + self.port_from.handle_event(&Event::Key(key)); + let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); + self.port_to = + Input::default().with_value((port_from_value + PORT_ALLOCATION).to_string()); self.validate(); vec![] } _ => { // if max limit reached, we should not allow any more inputs. - if self.focus == FocusInput::PortFrom - && self.port_from.value().len() < INPUT_SIZE as usize - { + if self.port_from.value().len() < INPUT_SIZE as usize { self.port_from.handle_event(&Event::Key(key)); - } else if self.focus == FocusInput::PortTo - && self.port_to.value().len() < INPUT_SIZE as usize - { - self.port_to.handle_event(&Event::Key(key)); - } - + let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); + if port_from_value + PORT_ALLOCATION <= PORT_MAX { + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + } else { + self.port_to = Input::default().with_value("-".to_string()); + } + }; self.validate(); vec![] } @@ -275,28 +256,16 @@ impl Component for PortRangePopUp { let input_line = Line::from(vec![ Span::styled( format!("{}{} ", spaces_from, self.port_from.value()), - if self.focus == FocusInput::PortFrom { - Style::default() - .fg(VIVID_SKY_BLUE) - .bg(INDIGO) - .underlined() - .underline_color(VIVID_SKY_BLUE) - } else { - Style::default().fg(VIVID_SKY_BLUE) - }, + Style::default() + .fg(VIVID_SKY_BLUE) + .bg(INDIGO) + .underlined() + .underline_color(VIVID_SKY_BLUE), ), Span::styled(" to ", Style::default().fg(GHOST_WHITE)), Span::styled( format!("{}{} ", spaces_to, self.port_to.value()), - if self.focus == FocusInput::PortTo { - Style::default() - .fg(VIVID_SKY_BLUE) - .bg(INDIGO) - .underlined() - .underline_color(VIVID_SKY_BLUE) - } else { - Style::default().fg(VIVID_SKY_BLUE) - }, + Style::default().fg(VIVID_SKY_BLUE), ), ]) .alignment(Alignment::Center); diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 99175c5a01..aa1c56a04b 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -13,6 +13,7 @@ use super::{ Component, Frame, }; use crate::action::OptionsActions; +use crate::components::popup::port_range::{PORT_ALLOCATION, PORT_MAX, PORT_MIN}; use crate::config::get_launchpad_nodes_data_dir_path; use crate::connection_mode::ConnectionMode; use crate::node_mgmt::MaintainNodesArgs; @@ -75,9 +76,9 @@ pub struct Status { // Connection mode connection_mode: ConnectionMode, // Port from - port_from: Option, + port_from: Option, // Port to - port_to: Option, + port_to: Option, } #[derive(Clone)] @@ -94,8 +95,8 @@ pub struct StatusConfig { pub safenode_path: Option, pub data_dir_path: PathBuf, pub connection_mode: ConnectionMode, - pub port_from: Option, - pub port_to: Option, + pub port_from: Option, + pub port_to: Option, } impl Status { @@ -370,16 +371,23 @@ impl Component for Status { ))); } - self.lock_registry = Some(LockRegistryState::StartingNodes); - let action_sender = self.get_actions_sender()?; - info!("Running maintain node count: {:?}", self.nodes_to_start); + // Check if the port range is valid and we shorten the range based on the nodes to start + if self.port_from.unwrap_or(PORT_MIN) + self.nodes_to_start as u32 > PORT_MAX { + error!("Port range exceeds maximum port number. Cannot start nodes."); + //TODO: Give feedback to the user + return Ok(None); + } let port_range_str = format!( "{}-{}", - self.port_from.unwrap_or(0), - self.port_to.unwrap_or(0) + self.port_from.unwrap_or(PORT_MIN), + self.port_from.unwrap_or(PORT_MIN) - 1 + self.nodes_to_start as u32 ); + self.lock_registry = Some(LockRegistryState::StartingNodes); + let action_sender = self.get_actions_sender()?; + info!("Running maintain node count: {:?}", self.nodes_to_start); + let port_range = match PortRange::parse(&port_range_str) { Ok(port_range) => port_range, Err(err) => { @@ -401,6 +409,7 @@ impl Component for Status { port_range: Some(port_range), }; + //TODO: Handle errors and give feedback to the user maintain_n_running_nodes(maintain_nodes_args); } StatusActions::StopNodes => { @@ -513,8 +522,8 @@ impl Component for Status { ConnectionMode::UPnP => "UPnP", ConnectionMode::CustomPorts => &format!( "Custom Ports {}-{}", - self.port_from.unwrap_or(0), - self.port_to.unwrap_or(0) + self.port_from.unwrap_or(PORT_MIN), + self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION) ), ConnectionMode::Automatic => "Automatic", }; diff --git a/node-launchpad/src/config.rs b/node-launchpad/src/config.rs index 1b19ab96e2..59dfe3a975 100644 --- a/node-launchpad/src/config.rs +++ b/node-launchpad/src/config.rs @@ -106,8 +106,8 @@ pub struct AppData { pub storage_mountpoint: Option, pub storage_drive: Option, pub connection_mode: Option, - pub port_from: Option, - pub port_to: Option, + pub port_from: Option, + pub port_to: Option, } impl Default for AppData { From 12e91dc3b6bfa0fb5b7da258d1d4157c8b083325 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 22 Aug 2024 18:06:33 +0200 Subject: [PATCH 10/24] fix(launchpad): configuration for one node one port --- node-launchpad/src/components/status.rs | 30 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index aa1c56a04b..e307afd05f 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -371,22 +371,25 @@ impl Component for Status { ))); } + // Port calculation // Check if the port range is valid and we shorten the range based on the nodes to start - if self.port_from.unwrap_or(PORT_MIN) + self.nodes_to_start as u32 > PORT_MAX { + if self.port_from.unwrap_or(PORT_MIN) - 1 + self.nodes_to_start as u32 + > PORT_MAX + { error!("Port range exceeds maximum port number. Cannot start nodes."); //TODO: Give feedback to the user return Ok(None); } - let port_range_str = format!( - "{}-{}", - self.port_from.unwrap_or(PORT_MIN), - self.port_from.unwrap_or(PORT_MIN) - 1 + self.nodes_to_start as u32 - ); - - self.lock_registry = Some(LockRegistryState::StartingNodes); - let action_sender = self.get_actions_sender()?; - info!("Running maintain node count: {:?}", self.nodes_to_start); + let port_range_str = if self.nodes_to_start > 1 { + format!( + "{}-{}", + self.port_from.unwrap_or(PORT_MIN), + self.port_from.unwrap_or(PORT_MIN) - 1 + self.nodes_to_start as u32 + ) + } else { + format!("{}", self.port_from.unwrap_or(PORT_MIN)) + }; let port_range = match PortRange::parse(&port_range_str) { Ok(port_range) => port_range, @@ -396,6 +399,10 @@ impl Component for Status { } }; + self.lock_registry = Some(LockRegistryState::StartingNodes); + let action_sender = self.get_actions_sender()?; + info!("Running maintain node count: {:?}", self.nodes_to_start); + let maintain_nodes_args = MaintainNodesArgs { count: self.nodes_to_start as u16, owner: self.discord_username.clone(), @@ -404,12 +411,13 @@ impl Component for Status { && self.connection_mode == ConnectionMode::Automatic, safenode_path: self.safenode_path.clone(), data_dir_path: Some(self.data_dir_path.clone()), - action_sender, + action_sender: action_sender.clone(), connection_mode: self.connection_mode.clone(), port_range: Some(port_range), }; //TODO: Handle errors and give feedback to the user + stop_nodes(self.get_running_nodes(), action_sender.clone()); maintain_n_running_nodes(maintain_nodes_args); } StatusActions::StopNodes => { From 67760a3d9f51ba9ef1a92a50050afc9a05b68f9d Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 28 Aug 2024 10:09:43 +0200 Subject: [PATCH 11/24] feat(launchpad): error component --- node-launchpad/src/error.rs | 195 ++++++++++++++++++++++++++++++++++++ node-launchpad/src/lib.rs | 1 + node-launchpad/src/style.rs | 1 + 3 files changed, 197 insertions(+) create mode 100644 node-launchpad/src/error.rs diff --git a/node-launchpad/src/error.rs b/node-launchpad/src/error.rs new file mode 100644 index 0000000000..352bbfa8fd --- /dev/null +++ b/node-launchpad/src/error.rs @@ -0,0 +1,195 @@ +use crate::{ + components::utils::centered_rect_fixed, + style::{clear_area, EUCALYPTUS, GHOST_WHITE, RED}, + tui::Frame, +}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span}, + widgets::{Block, Borders, Padding, Paragraph, Wrap}, +}; + +/// Error popup is a popup that is used to display error messages to the user. +/// +/// It accepts a title, a message and a error message. +/// Handles key events to hide the popup (Enter and Esc keys). +/// +/// How to use: +/// 1. Create a new ErrorPopup member in your component. +/// 2. Show the error popup by calling the `show` method. +/// 3. Hide the error popup by calling the `hide` method. +/// 4. Check if the error popup is visible by calling the `is_visible` method. +/// 5. Draw the error popup by calling the `draw_error` method in your `draw` function. +/// 6. Handle the input for the error popup by calling the `handle_input` method. +/// +/// Example: +/// ```rust +/// use crate::error::ErrorPopup; +/// +/// pub struct MyComponent { +/// error_popup: Option, +/// } +/// +/// impl MyComponent { +/// pub fn new() -> Self { +/// Self { +/// error_popup: None, +/// } +/// } +/// } +/// +/// impl Component for MyComponent { +/// fn handle_key_events(&mut self, key: KeyEvent) -> Result> { +/// if let Some(error_popup) = &mut self.error_popup { +/// if error_popup.is_visible() { +/// error_popup.handle_input(key); +/// return Ok(vec![]); +/// } +/// } +/// // ... Your keys being handled here ... +/// } +/// fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { +/// if let Some(error_popup) = &self.error_popup { +/// if error_popup.is_visible() { +/// error_popup.draw_error(f, area); +/// return Ok(()); +/// } +/// } +/// // ... Your drawing code here ... +/// } +/// } +/// ``` +/// +/// How to trigger the error +/// +/// ```rust +/// self.error_popup = Some(ErrorPopup::new( +/// "Error".to_string(), +/// "This is a test error message".to_string(), +/// "raw message".to_string(), +/// )); +/// if let Some(error_popup) = &mut self.error_popup { +/// error_popup.show(); +/// } +/// ``` + +pub struct ErrorPopup { + visible: bool, + title: String, + message: String, + error_message: String, +} + +impl ErrorPopup { + pub fn new(title: String, message: String, error_message: String) -> Self { + Self { + visible: false, + title, + message, + error_message, + } + } + + pub fn draw_error(&self, f: &mut Frame, area: Rect) { + if !self.visible { + return; + } + + let layer_zero = centered_rect_fixed(52, 15, area); + + let layer_one = Layout::new( + Direction::Vertical, + [ + // for the pop_up_border + padding + Constraint::Length(2), + // for the text + Constraint::Min(1), + // for the pop_up_border + Constraint::Length(1), + ], + ) + .split(layer_zero); + + let pop_up_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(format!(" {} ", self.title)) + .title_style(Style::new().fg(RED)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(RED)), + ); + clear_area(f, layer_zero); + + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the message + Constraint::Length(5), + // for the error_message + Constraint::Length(5), + // gap + Constraint::Length(1), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + let prompt = Paragraph::new(self.message.clone()) + .block(Block::default().padding(Padding::horizontal(2))) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + + f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]); + + let text = Paragraph::new(self.error_message.clone()) + .block(Block::default().padding(Padding::horizontal(2))) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); + f.render_widget(text.fg(GHOST_WHITE), layer_two[1]); + + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[2]); + + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[3]); + let button_ok = Line::from(vec![ + Span::styled("OK ", Style::default().fg(EUCALYPTUS)), + Span::styled("[Enter] ", Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right); + + f.render_widget(button_ok, buttons_layer[1]); + + // We render now so the borders are on top of the other widgets + f.render_widget(pop_up_border, layer_zero); + } + + pub fn handle_input(&mut self, key: KeyEvent) -> bool { + if self.visible && (key.code == KeyCode::Esc || key.code == KeyCode::Enter) { + self.hide(); + true + } else { + false + } + } + + pub fn show(&mut self) { + debug!("Showing error popup"); + self.visible = true; + } + + pub fn hide(&mut self) { + debug!("Hiding error popup"); + self.visible = false; + } + + pub fn is_visible(&self) -> bool { + self.visible + } +} diff --git a/node-launchpad/src/lib.rs b/node-launchpad/src/lib.rs index aa18661f27..28e8535f42 100644 --- a/node-launchpad/src/lib.rs +++ b/node-launchpad/src/lib.rs @@ -11,6 +11,7 @@ pub mod app; pub mod components; pub mod config; pub mod connection_mode; +pub mod error; pub mod mode; pub mod node_mgmt; pub mod node_stats; diff --git a/node-launchpad/src/style.rs b/node-launchpad/src/style.rs index e4ae3c0c07..10e0cda89d 100644 --- a/node-launchpad/src/style.rs +++ b/node-launchpad/src/style.rs @@ -23,6 +23,7 @@ pub const SPACE_CADET: Color = Color::Indexed(17); pub const DARK_GUNMETAL: Color = Color::Indexed(235); // 266 is incorrect pub const INDIGO: Color = Color::Indexed(60); pub const VIVID_SKY_BLUE: Color = Color::Indexed(45); +pub const RED: Color = Color::Indexed(196); // Clears the area and sets the background color pub fn clear_area(f: &mut Frame<'_>, area: Rect) { From 2f7bddf122da4d6787dac05e93e62ea18e3b3327 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 28 Aug 2024 15:07:13 +0200 Subject: [PATCH 12/24] fix(launchpad): flickering input field --- node-launchpad/src/components/popup/reset_nodes.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/node-launchpad/src/components/popup/reset_nodes.rs b/node-launchpad/src/components/popup/reset_nodes.rs index c021598278..7389b8e472 100644 --- a/node-launchpad/src/components/popup/reset_nodes.rs +++ b/node-launchpad/src/components/popup/reset_nodes.rs @@ -155,11 +155,7 @@ impl Component for ResetNodesPopup { let input = Paragraph::new(Span::styled( format!("{}{} ", spaces, self.confirmation_input_field.value()), - Style::default() - .fg(VIVID_SKY_BLUE) - .bg(INDIGO) - .underlined() - .underline_color(VIVID_SKY_BLUE), + Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(), )) .alignment(Alignment::Center); From 828c56682f126454c5e012b8542bf9705b972a9e Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 28 Aug 2024 15:09:07 +0200 Subject: [PATCH 13/24] feat(launchpad): nodes scaling up and down with custom ports --- .../src/components/popup/port_range.rs | 443 +++++++++++------- node-launchpad/src/components/status.rs | 76 +-- node-launchpad/src/node_mgmt.rs | 351 ++++++++++---- sn_node_manager/src/cmd/node.rs | 177 ++++--- 4 files changed, 673 insertions(+), 374 deletions(-) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index 9b44116a08..d167724d0e 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -6,6 +6,9 @@ // KIND, either express or implied. Please review the Licences for the specific language governing // permissions and limitations relating to use of the SAFE Network Software. +use std::rc::Rc; + +use super::super::super::node_mgmt::{PORT_MAX, PORT_MIN}; use super::super::utils::centered_rect_fixed; use super::super::Component; use crate::{ @@ -19,14 +22,20 @@ use crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::{prelude::*, widgets::*}; use tui_input::{backend::crossterm::EventHandler, Input}; -pub const PORT_MAX: u32 = 65535; -pub const PORT_MIN: u32 = 1024; pub const PORT_ALLOCATION: u32 = 49; // We count the port_from as well const INPUT_SIZE: u32 = 5; const INPUT_AREA: u32 = INPUT_SIZE + 2; // +2 for the left and right padding +#[derive(Default)] +enum PortRangeState { + #[default] + Selection, + ConfirmChange, +} + pub struct PortRangePopUp { active: bool, + state: PortRangeState, connection_mode: ConnectionMode, port_from: Input, port_to: Input, @@ -39,6 +48,7 @@ impl PortRangePopUp { pub fn new(connection_mode: ConnectionMode, port_from: u32, port_to: u32) -> Self { Self { active: false, + state: PortRangeState::Selection, connection_mode, port_from: Input::default().with_value(port_from.to_string()), port_to: Input::default().with_value(port_to.to_string()), @@ -59,162 +69,16 @@ impl PortRangePopUp { && port_from <= port_to; } } -} - -impl Component for PortRangePopUp { - fn handle_key_events(&mut self, key: KeyEvent) -> Result> { - if !self.active { - return Ok(vec![]); - } - // while in entry mode, keybinds are not captured, so gotta exit entry mode from here - let send_back = match key.code { - KeyCode::Enter => { - let port_from = self.port_from.value(); - let port_to = self.port_to.value(); - - if port_from.is_empty() || port_to.is_empty() || !self.can_save { - debug!("Got Enter, but port_from or port_to is empty, ignoring."); - return Ok(vec![]); - } - debug!("Got Enter, saving the ports and switching to Options Screen",); - vec![ - Action::StorePortRange( - self.port_from.value().parse().unwrap_or_default(), - self.port_to.value().parse().unwrap_or_default(), - ), - Action::OptionsActions(OptionsActions::UpdatePortRange( - self.port_from.value().parse().unwrap_or_default(), - self.port_to.value().parse().unwrap_or_default(), - )), - Action::SwitchScene(Scene::Options), - ] - } - KeyCode::Esc => { - debug!("Got Esc, restoring the old values and switching to actual screen"); - // reset to old value - self.port_from = self - .port_from - .clone() - .with_value(self.port_from_old_value.to_string()); - self.port_to = self - .port_to - .clone() - .with_value(self.port_to_old_value.to_string()); - vec![Action::SwitchScene(Scene::Options)] - } - KeyCode::Char(c) if !c.is_numeric() => vec![], - KeyCode::Up => { - if self.port_from.value().parse::().unwrap_or_default() < PORT_MAX { - self.port_from = self.port_from.clone().with_value( - (self.port_from.value().parse::().unwrap_or_default() + 1).to_string(), - ); - let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); - if port_from_value + PORT_ALLOCATION <= PORT_MAX { - self.port_to = Input::default() - .with_value((port_from_value + PORT_ALLOCATION).to_string()); - } else { - self.port_to = Input::default().with_value("-".to_string()); - } - }; - self.validate(); - vec![] - } - KeyCode::Down => { - if self.port_from.value().parse::().unwrap_or_default() > 0 { - self.port_from = self.port_from.clone().with_value( - (self.port_from.value().parse::().unwrap_or_default() - 1).to_string(), - ); - let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); - if port_from_value + PORT_ALLOCATION <= PORT_MAX { - self.port_to = Input::default() - .with_value((port_from_value + PORT_ALLOCATION).to_string()); - } else { - self.port_to = Input::default().with_value("-".to_string()); - } - }; - self.validate(); - vec![] - } - KeyCode::Backspace => { - self.port_from.handle_event(&Event::Key(key)); - let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); - self.port_to = - Input::default().with_value((port_from_value + PORT_ALLOCATION).to_string()); - self.validate(); - vec![] - } - _ => { - // if max limit reached, we should not allow any more inputs. - if self.port_from.value().len() < INPUT_SIZE as usize { - self.port_from.handle_event(&Event::Key(key)); - let port_from_value: u32 = self.port_from.value().parse().unwrap_or_default(); - if port_from_value + PORT_ALLOCATION <= PORT_MAX { - self.port_to = Input::default() - .with_value((port_from_value + PORT_ALLOCATION).to_string()); - } else { - self.port_to = Input::default().with_value("-".to_string()); - } - }; - self.validate(); - vec![] - } - }; - Ok(send_back) - } - - fn update(&mut self, action: Action) -> Result> { - let send_back = match action { - Action::SwitchScene(scene) => match scene { - Scene::ChangePortsPopUp => { - if self.connection_mode == ConnectionMode::CustomPorts { - self.active = true; - self.validate(); - self.port_from_old_value = - self.port_from.value().parse().unwrap_or_default(); - self.port_to_old_value = self.port_to.value().parse().unwrap_or_default(); - // Set to InputMode::Entry as we want to handle everything within our handle_key_events - // so by default if this scene is active, we capture inputs. - Some(Action::SwitchInputMode(InputMode::Entry)) - } else { - self.active = false; - Some(Action::SwitchScene(Scene::Options)) - } - } - _ => { - self.active = false; - None - } - }, - // Useful when the user has selected a connection mode but didn't confirm it - Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { - self.connection_mode = connection_mode; - None - } - _ => None, - }; - Ok(send_back) - } - fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { - if !self.active { - return Ok(()); - } - - let layer_zero = centered_rect_fixed(52, 15, area); - - let layer_one = Layout::new( - Direction::Vertical, - [ - // for the pop_up_border - Constraint::Length(2), - // for the input field - Constraint::Min(1), - // for the pop_up_border - Constraint::Length(1), - ], - ) - .split(layer_zero); + // -- Draw functions -- + // Draws the Port Selection screen + fn draw_selection_state( + &mut self, + f: &mut crate::tui::Frame<'_>, + layer_zero: Rect, + layer_one: Rc<[Rect]>, + ) -> Paragraph { // layer zero let pop_up_border = Paragraph::new("").block( Block::default() @@ -256,11 +120,7 @@ impl Component for PortRangePopUp { let input_line = Line::from(vec![ Span::styled( format!("{}{} ", spaces_from, self.port_from.value()), - Style::default() - .fg(VIVID_SKY_BLUE) - .bg(INDIGO) - .underlined() - .underline_color(VIVID_SKY_BLUE), + Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(), ), Span::styled(" to ", Style::default().fg(GHOST_WHITE)), Span::styled( @@ -297,12 +157,265 @@ impl Component for PortRangePopUp { Style::default().fg(LIGHT_PERIWINKLE) }; f.render_widget(button_no, buttons_layer[0]); - let button_yes = Line::from(vec![Span::styled( - "Save Port Range [Enter]", - button_yes_style, - )]); + + let button_yes = Line::from(vec![ + Span::styled("Save Port Range ", button_yes_style), + Span::styled("[Enter]", Style::default().fg(GHOST_WHITE)), + ]); f.render_widget(button_yes, buttons_layer[1]); + pop_up_border + } + + // Draws the Confirmation screen + fn draw_confirm_change_state( + &mut self, + f: &mut crate::tui::Frame<'_>, + layer_zero: Rect, + layer_one: Rc<[Rect]>, + ) -> Paragraph { + // layer zero + let pop_up_border = Paragraph::new("").block( + Block::default() + .borders(Borders::ALL) + .title(" Port Forwarding For Private IPs ") + .title_style(Style::new().fg(VIVID_SKY_BLUE)) + .padding(Padding::uniform(2)) + .border_style(Style::new().fg(VIVID_SKY_BLUE)), + ); + clear_area(f, layer_zero); + + // split into 4 parts, for the prompt, input, text, dash , and buttons + let layer_two = Layout::new( + Direction::Vertical, + [ + // for the text + Constraint::Length(8), + // gap + Constraint::Length(3), + // for the buttons + Constraint::Length(1), + ], + ) + .split(layer_one[1]); + + let paragraph_text = Paragraph::new(vec![ + Line::from(Span::styled("\n\n",Style::default())), + Line::from(Span::styled("If you have a Private IP (which you probably do) you’ll now need to set your router to…\n\n", Style::default().fg(LIGHT_PERIWINKLE))), + Line::from(Span::styled("\n\n",Style::default())), + Line::from(Span::styled( + format!("Port Forward ports {}-{} ", self.port_from.value(), self.port_to.value()), + Style::default().fg(GHOST_WHITE), + )), + Line::from(Span::styled("\n\n",Style::default())), + Line::from(Span::styled("You can do this in your router’s admin panel.\n\n", Style::default().fg(LIGHT_PERIWINKLE))), + ]) + .alignment(Alignment::Left) + .wrap(Wrap { trim: true }) + .block(block::Block::default().padding(Padding::horizontal(2))); + + f.render_widget(paragraph_text, layer_two[0]); + + let dash = Block::new() + .borders(Borders::BOTTOM) + .border_style(Style::new().fg(GHOST_WHITE)); + f.render_widget(dash, layer_two[1]); + + let buttons_layer = + Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(layer_two[2]); + + let button_ok = Line::from(vec![ + Span::styled("OK ", Style::default().fg(EUCALYPTUS)), + Span::styled("[Enter] ", Style::default().fg(GHOST_WHITE)), + ]) + .alignment(Alignment::Right); + + f.render_widget(button_ok, buttons_layer[1]); + + pop_up_border + } +} + +impl Component for PortRangePopUp { + fn handle_key_events(&mut self, key: KeyEvent) -> Result> { + if !self.active { + return Ok(vec![]); + } + // while in entry mode, keybinds are not captured, so gotta exit entry mode from here + let send_back: Vec = match &self.state { + PortRangeState::Selection => { + match key.code { + KeyCode::Enter => { + let port_from = self.port_from.value(); + let port_to = self.port_to.value(); + + if port_from.is_empty() || port_to.is_empty() || !self.can_save { + debug!("Got Enter, but port_from or port_to is empty, ignoring."); + return Ok(vec![]); + } + debug!("Got Enter, saving the ports and switching to Options Screen",); + self.state = PortRangeState::ConfirmChange; + vec![ + Action::StorePortRange( + self.port_from.value().parse().unwrap_or_default(), + self.port_to.value().parse().unwrap_or_default(), + ), + Action::OptionsActions(OptionsActions::UpdatePortRange( + self.port_from.value().parse().unwrap_or_default(), + self.port_to.value().parse().unwrap_or_default(), + )), + ] + } + KeyCode::Esc => { + debug!("Got Esc, restoring the old values and switching to actual screen"); + // reset to old value + self.port_from = self + .port_from + .clone() + .with_value(self.port_from_old_value.to_string()); + self.port_to = self + .port_to + .clone() + .with_value(self.port_to_old_value.to_string()); + vec![Action::SwitchScene(Scene::Options)] + } + KeyCode::Char(c) if !c.is_numeric() => vec![], + KeyCode::Up => { + if self.port_from.value().parse::().unwrap_or_default() < PORT_MAX { + self.port_from = self.port_from.clone().with_value( + (self.port_from.value().parse::().unwrap_or_default() + 1) + .to_string(), + ); + let port_from_value: u32 = + self.port_from.value().parse().unwrap_or_default(); + if port_from_value + PORT_ALLOCATION <= PORT_MAX { + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + } else { + self.port_to = Input::default().with_value("-".to_string()); + } + }; + self.validate(); + vec![] + } + KeyCode::Down => { + if self.port_from.value().parse::().unwrap_or_default() > 0 { + self.port_from = self.port_from.clone().with_value( + (self.port_from.value().parse::().unwrap_or_default() - 1) + .to_string(), + ); + let port_from_value: u32 = + self.port_from.value().parse().unwrap_or_default(); + if port_from_value + PORT_ALLOCATION <= PORT_MAX { + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + } else { + self.port_to = Input::default().with_value("-".to_string()); + } + }; + self.validate(); + vec![] + } + KeyCode::Backspace => { + self.port_from.handle_event(&Event::Key(key)); + let port_from_value: u32 = + self.port_from.value().parse().unwrap_or_default(); + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + self.validate(); + vec![] + } + _ => { + // if max limit reached, we should not allow any more inputs. + if self.port_from.value().len() < INPUT_SIZE as usize { + self.port_from.handle_event(&Event::Key(key)); + let port_from_value: u32 = + self.port_from.value().parse().unwrap_or_default(); + if port_from_value + PORT_ALLOCATION <= PORT_MAX { + self.port_to = Input::default() + .with_value((port_from_value + PORT_ALLOCATION).to_string()); + } else { + self.port_to = Input::default().with_value("-".to_string()); + } + }; + self.validate(); + vec![] + } + } + } + PortRangeState::ConfirmChange => match key.code { + KeyCode::Enter => { + debug!("Got Enter, saving the ports and switching to Options Screen",); + self.state = PortRangeState::Selection; + vec![Action::SwitchScene(Scene::Options)] + } + _ => vec![], + }, + }; + Ok(send_back) + } + + fn update(&mut self, action: Action) -> Result> { + let send_back = match action { + Action::SwitchScene(scene) => match scene { + Scene::ChangePortsPopUp => { + if self.connection_mode == ConnectionMode::CustomPorts { + self.active = true; + self.validate(); + self.port_from_old_value = + self.port_from.value().parse().unwrap_or_default(); + self.port_to_old_value = self.port_to.value().parse().unwrap_or_default(); + // Set to InputMode::Entry as we want to handle everything within our handle_key_events + // so by default if this scene is active, we capture inputs. + Some(Action::SwitchInputMode(InputMode::Entry)) + } else { + self.active = false; + Some(Action::SwitchScene(Scene::Options)) + } + } + _ => { + self.active = false; + None + } + }, + // Useful when the user has selected a connection mode but didn't confirm it + Action::OptionsActions(OptionsActions::UpdateConnectionMode(connection_mode)) => { + self.connection_mode = connection_mode; + None + } + _ => None, + }; + Ok(send_back) + } + + fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { + if !self.active { + return Ok(()); + } + + let layer_zero = centered_rect_fixed(52, 15, area); + + let layer_one = Layout::new( + Direction::Vertical, + [ + // for the pop_up_border + Constraint::Length(2), + // for the input field + Constraint::Min(1), + // for the pop_up_border + Constraint::Length(1), + ], + ) + .split(layer_zero); + + let pop_up_border: Paragraph = match self.state { + PortRangeState::Selection => self.draw_selection_state(f, layer_zero, layer_one), + PortRangeState::ConfirmChange => { + self.draw_confirm_change_state(f, layer_zero, layer_one) + } + }; + // We render now so the borders are on top of the other widgets f.render_widget(pop_up_border, layer_zero); Ok(()) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index e307afd05f..7e6ffb3590 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -13,10 +13,11 @@ use super::{ Component, Frame, }; use crate::action::OptionsActions; -use crate::components::popup::port_range::{PORT_ALLOCATION, PORT_MAX, PORT_MIN}; +use crate::components::popup::port_range::PORT_ALLOCATION; use crate::config::get_launchpad_nodes_data_dir_path; use crate::connection_mode::ConnectionMode; use crate::node_mgmt::MaintainNodesArgs; +use crate::node_mgmt::{PORT_MAX, PORT_MIN}; use crate::{ action::{Action, StatusActions}, config::Config, @@ -165,7 +166,7 @@ impl Status { .filter(|node| node.status != ServiceStatus::Removed) .collect(); info!( - "Loaded node registry. Running nodes: {:?}", + "Loaded node registry. Maintaining {:?} nodes.", self.node_services.len() ); @@ -238,6 +239,16 @@ impl Status { .get(service_idx) .map(|data| data.service_name.clone()) } + + fn show_starting_nodes_popup(&mut self) { + debug!("Showing starting nodes popup"); + self.lock_registry = Some(LockRegistryState::StartingNodes); + } + + fn hide_starting_nodes_popup(&mut self) { + debug!("Hiding starting nodes popup"); + self.lock_registry = None; + } } impl Component for Status { @@ -323,6 +334,7 @@ impl Component for Status { } StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => { self.lock_registry = None; + self.hide_starting_nodes_popup(); self.load_node_registry_and_update_states()?; } StatusActions::ResetNodesCompleted { trigger_start_node } => { @@ -331,6 +343,7 @@ impl Component for Status { if trigger_start_node { debug!("Reset nodes completed. Triggering start nodes."); + self.lock_registry = Some(LockRegistryState::StartingNodes); return Ok(Some(Action::StatusActions(StatusActions::StartNodes))); } debug!("Reset nodes completed"); @@ -359,10 +372,6 @@ impl Component for Status { } StatusActions::StartNodes => { debug!("Got action to start nodes"); - if self.lock_registry.is_some() { - error!("Registry is locked. Cannot start node now."); - return Ok(None); - } if self.nodes_to_start == 0 { info!("Nodes to start not set. Ask for input."); @@ -371,37 +380,26 @@ impl Component for Status { ))); } - // Port calculation - // Check if the port range is valid and we shorten the range based on the nodes to start - if self.port_from.unwrap_or(PORT_MIN) - 1 + self.nodes_to_start as u32 - > PORT_MAX - { - error!("Port range exceeds maximum port number. Cannot start nodes."); - //TODO: Give feedback to the user - return Ok(None); - } - - let port_range_str = if self.nodes_to_start > 1 { - format!( - "{}-{}", - self.port_from.unwrap_or(PORT_MIN), - self.port_from.unwrap_or(PORT_MIN) - 1 + self.nodes_to_start as u32 - ) - } else { - format!("{}", self.port_from.unwrap_or(PORT_MIN)) - }; - - let port_range = match PortRange::parse(&port_range_str) { - Ok(port_range) => port_range, - Err(err) => { - error!("When starting nodes, we got an error while parsing port range: {err:?}"); + if self.lock_registry.is_some() { + error!("Registry is locked. Attempting to unlock..."); + // Attempt to unlock the registry + self.lock_registry = None; + // Reload the node registry to ensure consistency + if let Err(e) = self.load_node_registry_and_update_states() { + //TODO: Show error & Popup + error!("Failed to reload node registry after unlocking: {:?}", e); return Ok(None); } - }; + } + self.show_starting_nodes_popup(); + + let port_range = PortRange::Range( + self.port_from.unwrap_or(PORT_MIN) as u16, + self.port_to.unwrap_or(PORT_MAX) as u16, + ); self.lock_registry = Some(LockRegistryState::StartingNodes); let action_sender = self.get_actions_sender()?; - info!("Running maintain node count: {:?}", self.nodes_to_start); let maintain_nodes_args = MaintainNodesArgs { count: self.nodes_to_start as u16, @@ -416,9 +414,12 @@ impl Component for Status { port_range: Some(port_range), }; - //TODO: Handle errors and give feedback to the user - stop_nodes(self.get_running_nodes(), action_sender.clone()); + debug!("Calling maintain_n_running_nodes"); + maintain_n_running_nodes(maintain_nodes_args); + + self.lock_registry = None; + self.load_node_registry_and_update_states()?; } StatusActions::StopNodes => { debug!("Got action to stop nodes"); @@ -542,8 +543,9 @@ impl Component for Status { ]); // Combine "Nanos Earned" and "Discord Username" into a single row + let discord_username_placeholder = "Discord Username: "; // Used to calculate the width of the username column let discord_username_title = Span::styled( - "Discord Username: ".to_string(), + discord_username_placeholder, Style::default().fg(VIVID_SKY_BLUE), ); @@ -580,8 +582,10 @@ impl Component for Status { let stats_width = [Constraint::Length(5)]; let column_constraints = [ Constraint::Length(23), - Constraint::Percentage(25), Constraint::Fill(1), + Constraint::Length( + (discord_username_placeholder.len() + self.discord_username.len()) as u16, + ), ]; let stats_table = Table::new(stats_rows, stats_width) .block( diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index f3c81b4de4..ff804b81fe 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -1,14 +1,20 @@ use std::path::PathBuf; -use sn_node_manager::{add_services::config::PortRange, VerbosityLevel}; +use sn_node_manager::{ + add_services::config::PortRange, config::get_node_registry_path, VerbosityLevel, +}; use sn_peers_acquisition::PeersArgs; +use sn_service_management::NodeRegistry; use tokio::sync::mpsc::UnboundedSender; use crate::action::{Action, StatusActions}; -use color_eyre::eyre::Result; use crate::connection_mode::ConnectionMode; +pub const PORT_MAX: u32 = 65535; +pub const PORT_MIN: u32 = 1024; + +/// Stop the specified services pub fn stop_nodes(services: Vec, action_sender: UnboundedSender) { tokio::task::spawn_local(async move { if let Err(err) = @@ -38,108 +44,42 @@ pub struct MaintainNodesArgs { pub port_range: Option, } +/// Maintain the specified number of nodes pub fn maintain_n_running_nodes(args: MaintainNodesArgs) { + debug!("Maintaining {} nodes", args.count); tokio::task::spawn_local(async move { if args.run_nat_detection { - info!("Running nat detection...."); - if let Err(err) = run_nat_detection_process().await { - error!("Error while running nat detection {err:?}. Registering the error."); - if let Err(err) = args.action_sender.send(Action::StatusActions( - StatusActions::ErrorWhileRunningNatDetection, - )) { - error!("Error while sending action: {err:?}"); - } - } else { - info!("Successfully ran nat detection."); - } + run_nat_detection(&args.action_sender).await; } - let auto_set_nat_flags: bool = args.connection_mode == ConnectionMode::Automatic; - let upnp: bool = args.connection_mode == ConnectionMode::UPnP; - let home_network: bool = args.connection_mode == ConnectionMode::HomeNetwork; - let custom_ports: Option = if args.connection_mode == ConnectionMode::CustomPorts - { - match args.port_range { - Some(port_range) => { - debug!("Port range to run nodes: {port_range:?}"); - Some(port_range) - } - None => { - debug!("Port range not provided. Using default port range."); - None - } - } - } else { - None - }; - let owner = if args.owner.is_empty() { - None - } else { - Some(args.owner) - }; - - debug!("************"); - debug!( - "Maintaining {} running nodes with the following args:", - args.count - ); - debug!( - " owner: {:?}, peers_args: {:?}, safenode_path: {:?}", - owner, args.peers_args, args.safenode_path - ); - debug!( - " data_dir_path: {:?}, connection_mode: {:?}", - args.data_dir_path, args.connection_mode - ); - debug!( - " auto_set_nat_flags: {:?}, custom_ports: {:?}, upnp: {}, home_network: {}", - auto_set_nat_flags, custom_ports, upnp, home_network - ); - - if let Err(err) = sn_node_manager::cmd::node::maintain_n_running_nodes( - false, - auto_set_nat_flags, - 120, - args.count, - args.data_dir_path, - true, - None, - home_network, - false, - None, - None, - None, - custom_ports, - owner, - args.peers_args, - None, - None, - args.safenode_path, - None, - upnp, - None, - None, - VerbosityLevel::Minimal, - None, - ) - .await - { - error!( - "Error while maintaining {:?} running nodes {err:?}", - args.count - ); + let config = prepare_node_config(&args); + debug_log_config(&config, &args); + + let node_registry = NodeRegistry::load(&get_node_registry_path().unwrap()).unwrap(); //FIXME: unwrap + let mut used_ports = get_used_ports(&node_registry); + let (mut current_port, max_port) = get_port_range(&config.custom_ports); + + let nodes_to_add = args.count as i32 - node_registry.nodes.len() as i32; + + if nodes_to_add <= 0 { + scale_down_nodes(&config, args.count).await; } else { - info!("Maintained {} running nodes successfully.", args.count); - } - if let Err(err) = args - .action_sender - .send(Action::StatusActions(StatusActions::StartNodesCompleted)) - { - error!("Error while sending action: {err:?}"); + add_nodes( + &config, + nodes_to_add, + &mut used_ports, + &mut current_port, + max_port, + ) + .await; } + + debug!("Finished maintaining {} nodes", args.count); + send_completion_action(&args.action_sender); }); } +/// Reset all the nodes pub fn reset_nodes(action_sender: UnboundedSender, start_nodes_after_reset: bool) { tokio::task::spawn_local(async move { if let Err(err) = sn_node_manager::cmd::node::reset(true, VerbosityLevel::Minimal).await { @@ -157,15 +97,228 @@ pub fn reset_nodes(action_sender: UnboundedSender, start_nodes_after_res }); } -async fn run_nat_detection_process() -> Result<()> { - sn_node_manager::cmd::nat_detection::run_nat_detection( +// --- Helper functions --- + +struct NodeConfig { + auto_set_nat_flags: bool, + upnp: bool, + home_network: bool, + custom_ports: Option, + owner: Option, + count: u16, + data_dir_path: Option, + peers_args: PeersArgs, + safenode_path: Option, +} + +/// Run the NAT detection process +async fn run_nat_detection(action_sender: &UnboundedSender) { + info!("Running nat detection...."); + if let Err(err) = sn_node_manager::cmd::nat_detection::run_nat_detection( None, true, None, None, - Some("0.1.0".to_string()), + Some("0.1.0".to_string()), //FIXME: hardcoded version!! VerbosityLevel::Minimal, ) - .await?; - Ok(()) + .await + { + error!("Error while running nat detection {err:?}. Registering the error."); + if let Err(err) = action_sender.send(Action::StatusActions( + StatusActions::ErrorWhileRunningNatDetection, + )) { + error!("Error while sending action: {err:?}"); + } + } else { + info!("Successfully ran nat detection."); + } +} + +fn prepare_node_config(args: &MaintainNodesArgs) -> NodeConfig { + NodeConfig { + auto_set_nat_flags: args.connection_mode == ConnectionMode::Automatic, + upnp: args.connection_mode == ConnectionMode::UPnP, + home_network: args.connection_mode == ConnectionMode::HomeNetwork, + custom_ports: if args.connection_mode == ConnectionMode::CustomPorts { + args.port_range.clone() + } else { + None + }, + owner: if args.owner.is_empty() { + None + } else { + Some(args.owner.clone()) + }, + count: args.count, + data_dir_path: args.data_dir_path.clone(), + peers_args: args.peers_args.clone(), + safenode_path: args.safenode_path.clone(), + } +} + +/// Debug log the node config +fn debug_log_config(config: &NodeConfig, args: &MaintainNodesArgs) { + debug!("************ STARTING NODE MAINTENANCE ************"); + debug!( + "Maintaining {} running nodes with the following args:", + config.count + ); + debug!( + " owner: {:?}, peers_args: {:?}, safenode_path: {:?}", + config.owner, config.peers_args, config.safenode_path + ); + debug!( + " data_dir_path: {:?}, connection_mode: {:?}", + config.data_dir_path, args.connection_mode + ); + debug!( + " auto_set_nat_flags: {:?}, custom_ports: {:?}, upnp: {}, home_network: {}", + config.auto_set_nat_flags, config.custom_ports, config.upnp, config.home_network + ); +} + +/// Get the currently used ports from the node registry +fn get_used_ports(node_registry: &NodeRegistry) -> Vec { + let used_ports: Vec = node_registry + .nodes + .iter() + .filter_map(|node| node.node_port) + .collect(); + debug!("Currently used ports: {:?}", used_ports); + used_ports +} + +/// Get the port range (u16, u16) from the custom ports PortRange +fn get_port_range(custom_ports: &Option) -> (u16, u16) { + match custom_ports { + Some(PortRange::Single(port)) => (*port, *port), + Some(PortRange::Range(start, end)) => (*start, *end), + None => (PORT_MIN as u16, PORT_MAX as u16), + } +} + +/// Scale down the nodes +async fn scale_down_nodes(config: &NodeConfig, count: u16) { + info!("No nodes to add"); + match sn_node_manager::cmd::node::maintain_n_running_nodes( + false, + config.auto_set_nat_flags, + 120, + count, + config.data_dir_path.clone(), + true, + None, + config.home_network, + false, + None, + None, + None, + None, // We don't care about the port, as we are scaling down + config.owner.clone(), + config.peers_args.clone(), + None, + None, + config.safenode_path.clone(), + None, + config.upnp, + None, + None, + VerbosityLevel::Minimal, + None, + ) + .await + { + Ok(_) => { + info!("Scaling down to {} nodes", count); + } + Err(err) => { + error!("Error while scaling down to {} nodes: {err:?}", count); + } + } +} + +/// Add the specified number of nodes +async fn add_nodes( + config: &NodeConfig, + mut nodes_to_add: i32, + used_ports: &mut Vec, + current_port: &mut u16, + max_port: u16, +) { + let mut retry_count = 0; + let max_retries = 5; + + while nodes_to_add > 0 && retry_count < max_retries { + // Find the next available port + while used_ports.contains(current_port) && *current_port <= max_port { + *current_port += 1; + } + + if *current_port > max_port { + error!("Reached maximum port number. Unable to find an available port."); + break; + } + + let port_range = Some(PortRange::Single(*current_port)); + match sn_node_manager::cmd::node::maintain_n_running_nodes( + false, + config.auto_set_nat_flags, + 120, + config.count, + config.data_dir_path.clone(), + true, + None, + config.home_network, + false, + None, + None, + None, + port_range, + config.owner.clone(), + config.peers_args.clone(), + None, + None, + config.safenode_path.clone(), + None, + config.upnp, + None, + None, + VerbosityLevel::Minimal, + None, + ) + .await + { + Ok(_) => { + info!("Successfully added a node on port {}", current_port); + used_ports.push(*current_port); + nodes_to_add -= 1; + *current_port += 1; + retry_count = 0; // Reset retry count on success + } + Err(err) => { + if err.to_string().contains("is being used by another service") { + warn!( + "Port {} is being used, retrying with a different port. Attempt {}/{}", + current_port, + retry_count + 1, + max_retries + ); + *current_port += 1; + retry_count += 1; + } else { + error!("Error while adding node on port {}: {err:?}", current_port); + retry_count += 1; + } + } + } + } +} + +/// Send the completion action +fn send_completion_action(action_sender: &UnboundedSender) { + if let Err(err) = action_sender.send(Action::StatusActions(StatusActions::StartNodesCompleted)) + { + error!("Error while sending action: {err:?}"); + } } diff --git a/sn_node_manager/src/cmd/node.rs b/sn_node_manager/src/cmd/node.rs index e6561472dd..3578b62f97 100644 --- a/sn_node_manager/src/cmd/node.rs +++ b/sn_node_manager/src/cmd/node.rs @@ -621,58 +621,51 @@ pub async fn maintain_n_running_nodes( let running_nodes = node_registry .nodes .iter() - .filter_map(|node| { - if node.status == ServiceStatus::Running { - Some(node.service_name.clone()) - } else { - None - } - }) + .filter(|node| node.status == ServiceStatus::Running) + .map(|node| node.service_name.clone()) .collect::>(); - match running_nodes.len().cmp(&(max_nodes_to_run as usize)) { + let running_count = running_nodes.len(); + let target_count = max_nodes_to_run as usize; + + info!( + "Current running nodes: {}, Target: {}", + running_count, target_count + ); + + match running_count.cmp(&target_count) { Ordering::Greater => { - // stop some nodes if we are running more nodes than needed. - let to_stop_count = running_nodes.len() - max_nodes_to_run as usize; + let to_stop_count = running_count - target_count; let services_to_stop = running_nodes .into_iter() + .rev() // Stop the oldest nodes first .take(to_stop_count) .collect::>(); info!( - ?max_nodes_to_run, - ?to_stop_count, - "We are stopping these services: {services_to_stop:?}" + "Stopping {} excess nodes: {:?}", + to_stop_count, services_to_stop ); - stop(vec![], services_to_stop, verbosity).await?; } Ordering::Less => { - // Run some nodes - let to_start_count = max_nodes_to_run as usize - running_nodes.len(); - - let mut inactive_nodes = node_registry + let to_start_count = target_count - running_count; + let inactive_nodes = node_registry .nodes .iter() - .filter_map(|node| { - if node.status == ServiceStatus::Stopped || node.status == ServiceStatus::Added - { - Some(node.service_name.clone()) - } else { - None - } + .filter(|node| { + node.status == ServiceStatus::Stopped || node.status == ServiceStatus::Added }) + .map(|node| node.service_name.clone()) .collect::>(); - // If we have enough inactive nodes, then we can just start them. Else we might have to add new ones and - // then start them. + info!("Inactive nodes available: {}", inactive_nodes.len()); + if to_start_count <= inactive_nodes.len() { - // start these nodes let nodes_to_start = inactive_nodes.into_iter().take(to_start_count).collect(); info!( - ?max_nodes_to_run, - ?to_start_count, - "We are starting these pre-existing services: {nodes_to_start:?}" + "Starting {} existing inactive nodes: {:?}", + to_start_count, nodes_to_start ); start( connection_timeout_s, @@ -683,60 +676,96 @@ pub async fn maintain_n_running_nodes( ) .await?; } else { - // add + start nodes let to_add_count = to_start_count - inactive_nodes.len(); - info!( - ?max_nodes_to_run, - ?to_add_count, - "We are adding+starting {to_add_count:?} nodes + starting these services: {inactive_nodes:?}" - ); - - let added_service_list = add( - auto_restart, - auto_set_nat_flags, - Some(to_add_count as u16), - data_dir_path, - enable_metrics_server, - env_variables, - home_network, - local, - log_dir_path, - log_format, - metrics_port, - node_port, - owner, - peers, - rpc_address, - rpc_port, - src_path, - upnp, - url, - user, - version, - verbosity, - ) - .await?; - inactive_nodes.extend(added_service_list); + "Adding {} new nodes and starting all {} inactive nodes", + to_add_count, + inactive_nodes.len() + ); - start( - connection_timeout_s, - start_node_interval, - vec![], - inactive_nodes, - verbosity, - ) - .await?; + let ports_to_use = match node_port { + Some(PortRange::Single(port)) => vec![port], + Some(PortRange::Range(start, end)) => { + (start..=end).take(to_add_count).collect() + } + None => vec![], + }; + + for (i, port) in ports_to_use.into_iter().enumerate() { + let added_service = add( + auto_restart, + auto_set_nat_flags, + Some(1), + data_dir_path.clone(), + enable_metrics_server, + env_variables.clone(), + home_network, + local, + log_dir_path.clone(), + log_format, + metrics_port.clone(), + Some(PortRange::Single(port)), + owner.clone(), + peers.clone(), + rpc_address, + rpc_port.clone(), + src_path.clone(), + upnp, + url.clone(), + user.clone(), + version.clone(), + verbosity, + ) + .await?; + + if i == 0 { + start( + connection_timeout_s, + start_node_interval, + vec![], + added_service, + verbosity, + ) + .await?; + } + } + + if !inactive_nodes.is_empty() { + start( + connection_timeout_s, + start_node_interval, + vec![], + inactive_nodes, + verbosity, + ) + .await?; + } } } Ordering::Equal => { info!( - ?max_nodes_to_run, - "We already have the correct number of nodes. Do nothing." + "Current node count ({}) matches target ({}). No action needed.", + running_count, target_count ); } } + // Verify final state + let final_node_registry = NodeRegistry::load(&config::get_node_registry_path()?)?; + let final_running_count = final_node_registry + .nodes + .iter() + .filter(|node| node.status == ServiceStatus::Running) + .count(); + + info!("Final running node count: {}", final_running_count); + if final_running_count != target_count { + warn!( + "Failed to reach target node count. Expected {}, but got {}", + target_count, final_running_count + ); + } + Ok(()) } From 84679ed27f706ffae855bfc7fa71c5ed0c97b9c6 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 28 Aug 2024 15:09:43 +0200 Subject: [PATCH 14/24] fix(launchpad): app not runing without app_data --- node-launchpad/src/app.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 668f308308..7b44daa9d0 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -24,6 +24,7 @@ use crate::{ config::{get_launchpad_nodes_data_dir_path, AppData, Config}, connection_mode::ConnectionMode, mode::{InputMode, Scene}, + node_mgmt::{PORT_MAX, PORT_MIN}, style::SPACE_CADET, system::{get_default_mount_point, get_primary_mount_point, get_primary_mount_point_name}, tui, @@ -94,7 +95,7 @@ impl App { peers_args, safenode_path, data_dir_path, - connection_mode, + connection_mode: connection_mode.clone(), port_from: Some(port_from), port_to: Some(port_to), }; @@ -259,10 +260,10 @@ impl App { self.app_data.connection_mode = Some(mode.clone()); self.app_data.save(None)?; } - Action::StorePortRange(from, to) => { + Action::StorePortRange(ref from, ref to) => { debug!("Storing port range: {from:?}, {to:?}"); - self.app_data.port_from = Some(from); - self.app_data.port_to = Some(to); + self.app_data.port_from = Some(*from); + self.app_data.port_to = Some(*to); self.app_data.save(None)?; } Action::StoreDiscordUserName(ref username) => { @@ -270,15 +271,9 @@ impl App { self.app_data.discord_username.clone_from(username); self.app_data.save(None)?; } - Action::StoreNodesToStart(count) => { + Action::StoreNodesToStart(ref count) => { debug!("Storing nodes to start: {count:?}"); - self.app_data.nodes_to_start = count; - self.app_data.save(None)?; - } - Action::StoreStorageDrive(ref drive_mountpoint, ref drive_name) => { - debug!("Storing storage drive: {drive_mountpoint:?}, {drive_name:?}"); - self.app_data.storage_mountpoint = Some(drive_mountpoint.clone()); - self.app_data.storage_drive = Some(drive_name.as_str().to_string()); + self.app_data.nodes_to_start = *count; self.app_data.save(None)?; } _ => {} From bbc3ddbf71818c0636f0a71fffc87b0b0fcb4383 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Wed, 28 Aug 2024 19:27:44 +0200 Subject: [PATCH 15/24] feat(launchpad): status feedback and latest nat crate feedback --- node-launchpad/src/components/status.rs | 52 ++++++++++++------------- node-launchpad/src/node_mgmt.rs | 23 ++++++++++- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 7e6ffb3590..51a9bdd535 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -239,16 +239,6 @@ impl Status { .get(service_idx) .map(|data| data.service_name.clone()) } - - fn show_starting_nodes_popup(&mut self) { - debug!("Showing starting nodes popup"); - self.lock_registry = Some(LockRegistryState::StartingNodes); - } - - fn hide_starting_nodes_popup(&mut self) { - debug!("Hiding starting nodes popup"); - self.lock_registry = None; - } } impl Component for Status { @@ -299,6 +289,7 @@ impl Component for Status { self.discord_username = username; if we_have_nodes && has_changed { + debug!("Setting lock_registry to ResettingNodes"); self.lock_registry = Some(LockRegistryState::ResettingNodes); info!("Resetting safenode services because the Discord Username was reset."); let action_sender = self.get_actions_sender()?; @@ -306,6 +297,7 @@ impl Component for Status { } } Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => { + debug!("Setting lock_registry to ResettingNodes"); self.lock_registry = Some(LockRegistryState::ResettingNodes); info!("Resetting safenode services because the Storage Drive was changed."); let action_sender = self.get_actions_sender()?; @@ -314,16 +306,18 @@ impl Component for Status { get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?; } Action::StoreConnectionMode(connection_mode) => { - self.connection_mode = connection_mode; + debug!("Setting lock_registry to ResettingNodes"); self.lock_registry = Some(LockRegistryState::ResettingNodes); + self.connection_mode = connection_mode; info!("Resetting safenode services because the Connection Mode range was changed."); let action_sender = self.get_actions_sender()?; reset_nodes(action_sender, false); } Action::StorePortRange(port_from, port_range) => { + debug!("Setting lock_registry to ResettingNodes"); + self.lock_registry = Some(LockRegistryState::ResettingNodes); self.port_from = Some(port_from); self.port_to = Some(port_range); - self.lock_registry = Some(LockRegistryState::ResettingNodes); info!("Resetting safenode services because the Port Range was changed."); let action_sender = self.get_actions_sender()?; reset_nodes(action_sender, false); @@ -333,16 +327,18 @@ impl Component for Status { self.node_stats = stats; } StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => { + debug!("Setting lock_registry to None"); self.lock_registry = None; - self.hide_starting_nodes_popup(); self.load_node_registry_and_update_states()?; } StatusActions::ResetNodesCompleted { trigger_start_node } => { + debug!("Setting lock_registry to None"); self.lock_registry = None; self.load_node_registry_and_update_states()?; if trigger_start_node { debug!("Reset nodes completed. Triggering start nodes."); + debug!("Setting lock_registry to StartingNodes"); self.lock_registry = Some(LockRegistryState::StartingNodes); return Ok(Some(Action::StatusActions(StatusActions::StartNodes))); } @@ -383,6 +379,7 @@ impl Component for Status { if self.lock_registry.is_some() { error!("Registry is locked. Attempting to unlock..."); // Attempt to unlock the registry + debug!("Setting lock_registry to None"); self.lock_registry = None; // Reload the node registry to ensure consistency if let Err(e) = self.load_node_registry_and_update_states() { @@ -391,22 +388,21 @@ impl Component for Status { return Ok(None); } } - self.show_starting_nodes_popup(); + debug!("Setting lock_registry to StartingNodes"); + self.lock_registry = Some(LockRegistryState::StartingNodes); let port_range = PortRange::Range( self.port_from.unwrap_or(PORT_MIN) as u16, self.port_to.unwrap_or(PORT_MAX) as u16, ); - self.lock_registry = Some(LockRegistryState::StartingNodes); let action_sender = self.get_actions_sender()?; let maintain_nodes_args = MaintainNodesArgs { count: self.nodes_to_start as u16, owner: self.discord_username.clone(), peers_args: self.peers_args.clone(), - run_nat_detection: self.should_we_run_nat_detection() - && self.connection_mode == ConnectionMode::Automatic, + run_nat_detection: self.should_we_run_nat_detection(), safenode_path: self.safenode_path.clone(), data_dir_path: Some(self.data_dir_path.clone()), action_sender: action_sender.clone(), @@ -417,9 +413,6 @@ impl Component for Status { debug!("Calling maintain_n_running_nodes"); maintain_n_running_nodes(maintain_nodes_args); - - self.lock_registry = None; - self.load_node_registry_and_update_states()?; } StatusActions::StopNodes => { debug!("Got action to stop nodes"); @@ -429,6 +422,7 @@ impl Component for Status { } let running_nodes = self.get_running_nodes(); + debug!("Setting lock_registry to StoppingNodes"); self.lock_registry = Some(LockRegistryState::StoppingNodes); let action_sender = self.get_actions_sender()?; info!("Stopping node service: {running_nodes:?}"); @@ -443,6 +437,7 @@ impl Component for Status { return Ok(None); } + debug!("Setting lock_registry to ResettingNodes"); self.lock_registry = Some(LockRegistryState::ResettingNodes); let action_sender = self.get_actions_sender()?; info!("Got action to reset nodes"); @@ -712,7 +707,12 @@ impl Component for Status { Line::raw("This may take a couple minutes."), ] } else { - vec![Line::raw("Starting nodes...")] + vec![ + Line::raw(""), + Line::raw(""), + Line::raw(""), + Line::raw("Starting nodes..."), + ] } } LockRegistryState::StoppingNodes => vec![Line::raw("Stopping nodes...")], @@ -722,22 +722,20 @@ impl Component for Status { Direction::Vertical, vec![ // border - Constraint::Length(1), - Constraint::Min(1), + Constraint::Length(2), // our text goes here - Constraint::Length(3), - Constraint::Min(1), + Constraint::Min(5), // border Constraint::Length(1), ], ) - .split(popup_area)[2]; + .split(popup_area); let text = Paragraph::new(popup_text) .block(Block::default().padding(Padding::horizontal(2))) .wrap(Wrap { trim: false }) .alignment(Alignment::Center) .fg(EUCALYPTUS); - f.render_widget(text, centred_area); + f.render_widget(text, centred_area[1]); f.render_widget(popup_border, popup_area); } diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index ff804b81fe..e914847fd5 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -11,6 +11,8 @@ use crate::action::{Action, StatusActions}; use crate::connection_mode::ConnectionMode; +use sn_releases::{self, ReleaseType, SafeReleaseRepoActions}; + pub const PORT_MAX: u32 = 65535; pub const PORT_MIN: u32 = 1024; @@ -62,8 +64,10 @@ pub fn maintain_n_running_nodes(args: MaintainNodesArgs) { let nodes_to_add = args.count as i32 - node_registry.nodes.len() as i32; if nodes_to_add <= 0 { + debug!("Scaling down nodes to {}", nodes_to_add); scale_down_nodes(&config, args.count).await; } else { + debug!("Scaling up nodes to {}", nodes_to_add); add_nodes( &config, nodes_to_add, @@ -114,12 +118,29 @@ struct NodeConfig { /// Run the NAT detection process async fn run_nat_detection(action_sender: &UnboundedSender) { info!("Running nat detection...."); + + let release_repo = ::default_config(); + let version = match release_repo + .get_latest_version(&ReleaseType::NatDetection) + .await + { + Ok(v) => { + info!("Using NAT detection version {}", v.to_string()); + v.to_string() + } + Err(err) => { + info!("No NAT detection release found, using fallback version 0.1.0"); + info!("Error: {err}"); + "0.1.0".to_string() + } + }; + if let Err(err) = sn_node_manager::cmd::nat_detection::run_nat_detection( None, true, None, None, - Some("0.1.0".to_string()), //FIXME: hardcoded version!! + Some(version), VerbosityLevel::Minimal, ) .await From f4eebe7615a1badabb67d6defd8663e24c051508 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 29 Aug 2024 09:32:24 +0200 Subject: [PATCH 16/24] chore(launchpad): update sn releases dependency --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ee1a8dbc4b..2bfa88e954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3426,7 +3426,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.51.1", ] [[package]] From d29579457fc3d06841d6232c07830ddf0e762c81 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 29 Aug 2024 10:21:15 +0200 Subject: [PATCH 17/24] fix(launchpad): we cannot set to none the registry --- node-launchpad/src/components/status.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 51a9bdd535..929792761a 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -377,17 +377,10 @@ impl Component for Status { } if self.lock_registry.is_some() { - error!("Registry is locked. Attempting to unlock..."); - // Attempt to unlock the registry - debug!("Setting lock_registry to None"); - self.lock_registry = None; - // Reload the node registry to ensure consistency - if let Err(e) = self.load_node_registry_and_update_states() { - //TODO: Show error & Popup - error!("Failed to reload node registry after unlocking: {:?}", e); - return Ok(None); - } + error!("Registry is locked. Cannot start node now."); + return Ok(None); } + debug!("Setting lock_registry to StartingNodes"); self.lock_registry = Some(LockRegistryState::StartingNodes); From 4f50801b49509bbb8d66cf303ef60d4324ec3a21 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 29 Aug 2024 10:21:58 +0200 Subject: [PATCH 18/24] chore(launchpad): todo message --- node-launchpad/src/node_mgmt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index e914847fd5..333c33de00 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -318,6 +318,7 @@ async fn add_nodes( retry_count = 0; // Reset retry count on success } Err(err) => { + //TODO: We should use concrete error types here instead of string matching (sn_node_manager) if err.to_string().contains("is being used by another service") { warn!( "Port {} is being used, retrying with a different port. Attempt {}/{}", From 9074e613852439c2348af10409577b329a946f1a Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 29 Aug 2024 15:41:30 +0200 Subject: [PATCH 19/24] feat(launchpad): error messages on status screen --- node-launchpad/src/action.rs | 6 +- .../src/components/popup/port_range.rs | 3 +- node-launchpad/src/components/status.rs | 99 +++++++++++++++++-- node-launchpad/src/error.rs | 16 ++- node-launchpad/src/node_mgmt.rs | 92 ++++++++++++++--- 5 files changed, 190 insertions(+), 26 deletions(-) diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index 8ab87cf0eb..19b8bc7125 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -50,7 +50,11 @@ pub enum StatusActions { ResetNodesCompleted { trigger_start_node: bool }, SuccessfullyDetectedNatStatus, ErrorWhileRunningNatDetection, - + ErrorLoadingNodeRegistry { raw_error: String }, + ErrorGettingNodeRegistryPath { raw_error: String }, + ErrorScalingUpNodes { raw_error: String }, + ErrorStoppingNodes { raw_error: String }, + ErrorResettingNodes { raw_error: String }, NodesStatsObtained(NodeStats), TriggerManageNodes, diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index d167724d0e..c3a41c0fe5 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -11,6 +11,7 @@ use std::rc::Rc; use super::super::super::node_mgmt::{PORT_MAX, PORT_MIN}; use super::super::utils::centered_rect_fixed; use super::super::Component; +use super::manage_nodes::MAX_NODE_COUNT; use crate::{ action::{Action, OptionsActions}, connection_mode::ConnectionMode, @@ -22,7 +23,7 @@ use crossterm::event::{Event, KeyCode, KeyEvent}; use ratatui::{prelude::*, widgets::*}; use tui_input::{backend::crossterm::EventHandler, Input}; -pub const PORT_ALLOCATION: u32 = 49; // We count the port_from as well +pub const PORT_ALLOCATION: u32 = MAX_NODE_COUNT as u32 - 1; // We count the port_from as well const INPUT_SIZE: u32 = 5; const INPUT_AREA: u32 = INPUT_SIZE + 2; // +2 for the left and right padding diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 929792761a..5bcaac42cd 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -16,8 +16,10 @@ use crate::action::OptionsActions; use crate::components::popup::port_range::PORT_ALLOCATION; use crate::config::get_launchpad_nodes_data_dir_path; use crate::connection_mode::ConnectionMode; +use crate::error::ErrorPopup; use crate::node_mgmt::MaintainNodesArgs; use crate::node_mgmt::{PORT_MAX, PORT_MIN}; +use crate::tui::Event; use crate::{ action::{Action, StatusActions}, config::Config, @@ -80,6 +82,7 @@ pub struct Status { port_from: Option, // Port to port_to: Option, + error_popup: Option, } #[derive(Clone)] @@ -121,6 +124,7 @@ impl Status { connection_mode: config.connection_mode, port_from: config.port_from, port_to: config.port_to, + error_popup: None, }; let now = Instant::now(); @@ -256,6 +260,14 @@ impl Component for Status { Ok(()) } + fn handle_events(&mut self, event: Option) -> Result> { + let r = match event { + Some(Event::Key(key_event)) => self.handle_key_events(key_event)?, + _ => vec![], + }; + Ok(r) + } + #[allow(clippy::comparison_chain)] fn update(&mut self, action: Action) -> Result> { match action { @@ -327,19 +339,15 @@ impl Component for Status { self.node_stats = stats; } StatusActions::StartNodesCompleted | StatusActions::StopNodesCompleted => { - debug!("Setting lock_registry to None"); self.lock_registry = None; self.load_node_registry_and_update_states()?; } StatusActions::ResetNodesCompleted { trigger_start_node } => { - debug!("Setting lock_registry to None"); self.lock_registry = None; self.load_node_registry_and_update_states()?; if trigger_start_node { debug!("Reset nodes completed. Triggering start nodes."); - debug!("Setting lock_registry to StartingNodes"); - self.lock_registry = Some(LockRegistryState::StartingNodes); return Ok(Some(Action::StatusActions(StatusActions::StartNodes))); } debug!("Reset nodes completed"); @@ -357,6 +365,55 @@ impl Component for Status { self.error_while_running_nat_detection ); } + StatusActions::ErrorLoadingNodeRegistry { raw_error } + | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => { + self.error_popup = Some(ErrorPopup::new( + " Error ".to_string(), + "Error getting node registry path".to_string(), + raw_error, + )); + if let Some(error_popup) = &mut self.error_popup { + error_popup.show(); + } + // Switch back to entry mode so we can handle key events + return Ok(Some(Action::SwitchInputMode(InputMode::Entry))); + } + StatusActions::ErrorScalingUpNodes { raw_error } => { + self.error_popup = Some(ErrorPopup::new( + " Error ".to_string(), + "Error adding new nodes".to_string(), + raw_error, + )); + if let Some(error_popup) = &mut self.error_popup { + error_popup.show(); + } + // Switch back to entry mode so we can handle key events + return Ok(Some(Action::SwitchInputMode(InputMode::Entry))); + } + StatusActions::ErrorStoppingNodes { raw_error } => { + self.error_popup = Some(ErrorPopup::new( + " Error ".to_string(), + "Error stopping nodes".to_string(), + raw_error, + )); + if let Some(error_popup) = &mut self.error_popup { + error_popup.show(); + } + // Switch back to entry mode so we can handle key events + return Ok(Some(Action::SwitchInputMode(InputMode::Entry))); + } + StatusActions::ErrorResettingNodes { raw_error } => { + self.error_popup = Some(ErrorPopup::new( + " Error ".to_string(), + "Error resetting nodes".to_string(), + raw_error, + )); + if let Some(error_popup) = &mut self.error_popup { + error_popup.show(); + } + // Switch back to entry mode so we can handle key events + return Ok(Some(Action::SwitchInputMode(InputMode::Entry))); + } StatusActions::TriggerManageNodes => { return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp))); } @@ -676,6 +733,16 @@ impl Component for Status { // ===== Popup ===== + // Error Popup + if let Some(error_popup) = &self.error_popup { + if error_popup.is_visible() { + error_popup.draw_error(f, area); + + return Ok(()); + } + } + + // Status Popup if let Some(registry_state) = &self.lock_registry { let popup_area = centered_rect_fixed(50, 12, area); clear_area(f, popup_area); @@ -708,8 +775,22 @@ impl Component for Status { ] } } - LockRegistryState::StoppingNodes => vec![Line::raw("Stopping nodes...")], - LockRegistryState::ResettingNodes => vec![Line::raw("Resetting nodes...")], + LockRegistryState::StoppingNodes => { + vec![ + Line::raw(""), + Line::raw(""), + Line::raw(""), + Line::raw("Stopping nodes..."), + ] + } + LockRegistryState::ResettingNodes => { + vec![ + Line::raw(""), + Line::raw(""), + Line::raw(""), + Line::raw("Resetting nodes..."), + ] + } }; let centred_area = Layout::new( Direction::Vertical, @@ -738,6 +819,12 @@ impl Component for Status { fn handle_key_events(&mut self, key: KeyEvent) -> Result> { debug!("Key received in Status: {:?}", key); + if let Some(error_popup) = &mut self.error_popup { + if error_popup.is_visible() { + error_popup.handle_input(key); + return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]); + } + } Ok(vec![]) } } diff --git a/node-launchpad/src/error.rs b/node-launchpad/src/error.rs index 352bbfa8fd..9f05fe292c 100644 --- a/node-launchpad/src/error.rs +++ b/node-launchpad/src/error.rs @@ -45,18 +45,21 @@ use ratatui::{ /// if let Some(error_popup) = &mut self.error_popup { /// if error_popup.is_visible() { /// error_popup.handle_input(key); -/// return Ok(vec![]); +/// return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]); /// } /// } /// // ... Your keys being handled here ... /// } /// fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> { +/// // ... Your drawing code here ... +/// // Be sure to include the background elements here /// if let Some(error_popup) = &self.error_popup { /// if error_popup.is_visible() { /// error_popup.draw_error(f, area); /// return Ok(()); /// } /// } +/// // Be sure to include your popups here /// // ... Your drawing code here ... /// } /// } @@ -75,6 +78,7 @@ use ratatui::{ /// } /// ``` +#[derive(Clone)] pub struct ErrorPopup { visible: bool, title: String, @@ -126,9 +130,9 @@ impl ErrorPopup { Direction::Vertical, [ // for the message - Constraint::Length(5), + Constraint::Length(4), // for the error_message - Constraint::Length(5), + Constraint::Length(7), // gap Constraint::Length(1), // for the buttons @@ -138,7 +142,11 @@ impl ErrorPopup { .split(layer_one[1]); let prompt = Paragraph::new(self.message.clone()) - .block(Block::default().padding(Padding::horizontal(2))) + .block( + Block::default() + .padding(Padding::horizontal(2)) + .padding(Padding::vertical(1)), + ) .alignment(Alignment::Center) .wrap(Wrap { trim: true }); diff --git a/node-launchpad/src/node_mgmt.rs b/node-launchpad/src/node_mgmt.rs index 333c33de00..88c48dd566 100644 --- a/node-launchpad/src/node_mgmt.rs +++ b/node-launchpad/src/node_mgmt.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use color_eyre::eyre::{eyre, Error}; use sn_node_manager::{ add_services::config::PortRange, config::get_node_registry_path, VerbosityLevel, }; @@ -16,6 +17,8 @@ use sn_releases::{self, ReleaseType, SafeReleaseRepoActions}; pub const PORT_MAX: u32 = 65535; pub const PORT_MIN: u32 = 1024; +const PORT_ASSIGNMENT_MAX_RETRIES: u32 = 5; + /// Stop the specified services pub fn stop_nodes(services: Vec, action_sender: UnboundedSender) { tokio::task::spawn_local(async move { @@ -23,6 +26,13 @@ pub fn stop_nodes(services: Vec, action_sender: UnboundedSender) sn_node_manager::cmd::node::stop(vec![], services, VerbosityLevel::Minimal).await { error!("Error while stopping services {err:?}"); + if let Err(err) = + action_sender.send(Action::StatusActions(StatusActions::ErrorStoppingNodes { + raw_error: err.to_string(), + })) + { + error!("Error while sending action: {err:?}"); + } } else { info!("Successfully stopped services"); } @@ -57,7 +67,13 @@ pub fn maintain_n_running_nodes(args: MaintainNodesArgs) { let config = prepare_node_config(&args); debug_log_config(&config, &args); - let node_registry = NodeRegistry::load(&get_node_registry_path().unwrap()).unwrap(); //FIXME: unwrap + let node_registry = match load_node_registry(&args.action_sender).await { + Ok(registry) => registry, + Err(err) => { + error!("Failed to load node registry: {:?}", err); + return; + } + }; let mut used_ports = get_used_ports(&node_registry); let (mut current_port, max_port) = get_port_range(&config.custom_ports); @@ -69,6 +85,7 @@ pub fn maintain_n_running_nodes(args: MaintainNodesArgs) { } else { debug!("Scaling up nodes to {}", nodes_to_add); add_nodes( + &args.action_sender, &config, nodes_to_add, &mut used_ports, @@ -79,7 +96,12 @@ pub fn maintain_n_running_nodes(args: MaintainNodesArgs) { } debug!("Finished maintaining {} nodes", args.count); - send_completion_action(&args.action_sender); + if let Err(err) = args + .action_sender + .send(Action::StatusActions(StatusActions::StartNodesCompleted)) + { + error!("Error while sending action: {err:?}"); + } }); } @@ -88,6 +110,13 @@ pub fn reset_nodes(action_sender: UnboundedSender, start_nodes_after_res tokio::task::spawn_local(async move { if let Err(err) = sn_node_manager::cmd::node::reset(true, VerbosityLevel::Minimal).await { error!("Error while resetting services {err:?}"); + if let Err(err) = + action_sender.send(Action::StatusActions(StatusActions::ErrorResettingNodes { + raw_error: err.to_string(), + })) + { + error!("Error while sending action: {err:?}"); + } } else { info!("Successfully reset services"); } @@ -103,6 +132,39 @@ pub fn reset_nodes(action_sender: UnboundedSender, start_nodes_after_res // --- Helper functions --- +/// Load the node registry and handle errors +async fn load_node_registry( + action_sender: &UnboundedSender, +) -> Result { + match get_node_registry_path() { + Ok(path) => match NodeRegistry::load(&path) { + Ok(registry) => Ok(registry), + Err(err) => { + error!("Failed to load NodeRegistry: {}", err); + if let Err(send_err) = action_sender.send(Action::StatusActions( + StatusActions::ErrorLoadingNodeRegistry { + raw_error: err.to_string(), + }, + )) { + error!("Error while sending action: {}", send_err); + } + Err(eyre!("Failed to load NodeRegistry")) + } + }, + Err(err) => { + error!("Failed to get node registry path: {}", err); + if let Err(send_err) = action_sender.send(Action::StatusActions( + StatusActions::ErrorGettingNodeRegistryPath { + raw_error: err.to_string(), + }, + )) { + error!("Error while sending action: {}", send_err); + } + Err(eyre!("Failed to get node registry path")) + } + } +} + struct NodeConfig { auto_set_nat_flags: bool, upnp: bool, @@ -221,7 +283,6 @@ fn get_port_range(custom_ports: &Option) -> (u16, u16) { /// Scale down the nodes async fn scale_down_nodes(config: &NodeConfig, count: u16) { - info!("No nodes to add"); match sn_node_manager::cmd::node::maintain_n_running_nodes( false, config.auto_set_nat_flags, @@ -261,6 +322,7 @@ async fn scale_down_nodes(config: &NodeConfig, count: u16) { /// Add the specified number of nodes async fn add_nodes( + action_sender: &UnboundedSender, config: &NodeConfig, mut nodes_to_add: i32, used_ports: &mut Vec, @@ -268,9 +330,8 @@ async fn add_nodes( max_port: u16, ) { let mut retry_count = 0; - let max_retries = 5; - while nodes_to_add > 0 && retry_count < max_retries { + while nodes_to_add > 0 && retry_count < PORT_ASSIGNMENT_MAX_RETRIES { // Find the next available port while used_ports.contains(current_port) && *current_port <= max_port { *current_port += 1; @@ -278,6 +339,16 @@ async fn add_nodes( if *current_port > max_port { error!("Reached maximum port number. Unable to find an available port."); + if let Err(err) = + action_sender.send(Action::StatusActions(StatusActions::ErrorScalingUpNodes { + raw_error: format!( + "Reached maximum port number ({}).\nUnable to find an available port.", + max_port + ), + })) + { + error!("Error while sending action: {err:?}"); + } break; } @@ -324,11 +395,12 @@ async fn add_nodes( "Port {} is being used, retrying with a different port. Attempt {}/{}", current_port, retry_count + 1, - max_retries + PORT_ASSIGNMENT_MAX_RETRIES ); *current_port += 1; retry_count += 1; } else { + error!("Range of ports to be used {:?}", *current_port..max_port); error!("Error while adding node on port {}: {err:?}", current_port); retry_count += 1; } @@ -336,11 +408,3 @@ async fn add_nodes( } } } - -/// Send the completion action -fn send_completion_action(action_sender: &UnboundedSender) { - if let Err(err) = action_sender.send(Action::StatusActions(StatusActions::StartNodesCompleted)) - { - error!("Error while sending action: {err:?}"); - } -} From 7c984df1d1e26ff17c049906d3b58052135a3e53 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Thu, 29 Aug 2024 15:49:21 +0200 Subject: [PATCH 20/24] fix(launchpad): not resetting when ports do not change --- node-launchpad/src/components/popup/port_range.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index c3a41c0fe5..f97c18cd61 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -248,6 +248,14 @@ impl Component for PortRangePopUp { PortRangeState::Selection => { match key.code { KeyCode::Enter => { + if self.port_from_old_value + == self.port_from.value().parse::().unwrap_or_default() + && self.port_to_old_value + == self.port_to.value().parse::().unwrap_or_default() + { + debug!("Got Enter, but nothing changed, ignoring."); + return Ok(vec![Action::SwitchScene(Scene::Options)]); + } let port_from = self.port_from.value(); let port_to = self.port_to.value(); From 2c172cc6f163d7bec7191f3ab82d2c33d9e5effc Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 2 Sep 2024 13:28:15 +0200 Subject: [PATCH 21/24] fix(launchpad): default ports and cancel during setting the ports --- node-launchpad/src/app.rs | 11 +++++--- node-launchpad/src/components/options.rs | 6 +++-- .../src/components/popup/connection_mode.rs | 12 +++++---- .../src/components/popup/port_range.rs | 25 +++++++++++++++++-- node-launchpad/src/components/status.rs | 2 +- node-launchpad/src/connection_mode.rs | 2 +- node-launchpad/src/mode.rs | 6 ++++- 7 files changed, 48 insertions(+), 16 deletions(-) diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 7b44daa9d0..d5e8083148 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -73,7 +73,7 @@ impl App { }; debug!("Data dir path for nodes: {data_dir_path:?}"); - // App data validations + // App data default values let connection_mode = app_data .connection_mode .unwrap_or(ConnectionMode::Automatic); @@ -95,7 +95,7 @@ impl App { peers_args, safenode_path, data_dir_path, - connection_mode: connection_mode.clone(), + connection_mode, port_from: Some(port_from), port_to: Some(port_to), }; @@ -114,7 +114,10 @@ impl App { // Popups let reset_nodes = ResetNodesPopup::default(); - let discord_username_input = BetaProgramme::new(app_data.discord_username.clone()); + let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; + let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; + let port_range = PortRangePopUp::new(connection_mode, port_from, port_to); + let beta_programme = BetaProgramme::new(app_data.discord_username.clone()); let manage_nodes = ManageNodes::new(app_data.nodes_to_start, storage_mountpoint.clone())?; let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; @@ -257,7 +260,7 @@ impl App { } Action::StoreConnectionMode(ref mode) => { debug!("Storing connection mode: {mode:?}"); - self.app_data.connection_mode = Some(mode.clone()); + self.app_data.connection_mode = Some(*mode); self.app_data.save(None)?; } Action::StorePortRange(ref from, ref to) => { diff --git a/node-launchpad/src/components/options.rs b/node-launchpad/src/components/options.rs index d2605f1759..3a2c8658fc 100644 --- a/node-launchpad/src/components/options.rs +++ b/node-launchpad/src/components/options.rs @@ -348,7 +348,7 @@ impl Component for Options { Scene::Options | Scene::ChangeDrivePopUp | Scene::ChangeConnectionModePopUp - | Scene::ChangePortsPopUp + | Scene::ChangePortsPopUp { .. } | Scene::BetaProgrammePopUp | Scene::ResetNodesPopUp => { self.active = true; @@ -372,7 +372,9 @@ impl Component for Options { self.connection_mode = mode; } OptionsActions::TriggerChangePortRange => { - return Ok(Some(Action::SwitchScene(Scene::ChangePortsPopUp))); + return Ok(Some(Action::SwitchScene(Scene::ChangePortsPopUp { + connection_mode_old_value: None, + }))); } OptionsActions::UpdatePortRange(from, to) => { self.port_from = Some(from); diff --git a/node-launchpad/src/components/popup/connection_mode.rs b/node-launchpad/src/components/popup/connection_mode.rs index 3e0b4b8b8f..134a068e3b 100644 --- a/node-launchpad/src/components/popup/connection_mode.rs +++ b/node-launchpad/src/components/popup/connection_mode.rs @@ -45,7 +45,7 @@ impl ChangeConnectionModePopUp { let mut selected_connection_mode: ConnectionModeItem = ConnectionModeItem::default(); let connection_modes_items: Vec = ConnectionMode::iter() .map(|connection_mode_item| ConnectionModeItem { - connection_mode: connection_mode_item.clone(), + connection_mode: connection_mode_item, status: if connection_mode == connection_mode_item { selected_connection_mode = ConnectionModeItem { connection_mode: connection_mode_item, @@ -128,14 +128,16 @@ impl Component for ChangeConnectionModePopUp { self.connection_mode_initial_state = self.connection_mode_selection.clone(); self.assign_connection_mode_selection(); vec![ - Action::StoreConnectionMode( - self.connection_mode_selection.connection_mode.clone(), - ), + Action::StoreConnectionMode(self.connection_mode_selection.connection_mode), Action::OptionsActions(OptionsActions::UpdateConnectionMode( connection_mode.clone().connection_mode, )), if connection_mode.connection_mode == ConnectionMode::CustomPorts { - Action::SwitchScene(Scene::ChangePortsPopUp) + Action::SwitchScene(Scene::ChangePortsPopUp { + connection_mode_old_value: Some( + self.connection_mode_initial_state.connection_mode, + ), + }) } else { Action::SwitchScene(Scene::Options) }, diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index f97c18cd61..6565c1097b 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -38,6 +38,7 @@ pub struct PortRangePopUp { active: bool, state: PortRangeState, connection_mode: ConnectionMode, + connection_mode_old_value: Option, port_from: Input, port_to: Input, port_from_old_value: u32, @@ -51,6 +52,7 @@ impl PortRangePopUp { active: false, state: PortRangeState::Selection, connection_mode, + connection_mode_old_value: None, port_from: Input::default().with_value(port_from.to_string()), port_to: Input::default().with_value(port_to.to_string()), port_from_old_value: Default::default(), @@ -252,6 +254,7 @@ impl Component for PortRangePopUp { == self.port_from.value().parse::().unwrap_or_default() && self.port_to_old_value == self.port_to.value().parse::().unwrap_or_default() + && self.can_save { debug!("Got Enter, but nothing changed, ignoring."); return Ok(vec![Action::SwitchScene(Scene::Options)]); @@ -278,7 +281,22 @@ impl Component for PortRangePopUp { } KeyCode::Esc => { debug!("Got Esc, restoring the old values and switching to actual screen"); - // reset to old value + // if the old values are 0 means that is the first time the user opens the app, + // so we should set the connection mode to automatic. + if self.port_from_old_value.to_string() == "0" + && self.port_to_old_value.to_string() == "0" + { + self.connection_mode = self + .connection_mode_old_value + .unwrap_or(ConnectionMode::Automatic); + return Ok(vec![ + Action::StoreConnectionMode(self.connection_mode), + Action::OptionsActions(OptionsActions::UpdateConnectionMode( + self.connection_mode, + )), + Action::SwitchScene(Scene::Options), + ]); + } self.port_from = self .port_from .clone() @@ -368,9 +386,12 @@ impl Component for PortRangePopUp { fn update(&mut self, action: Action) -> Result> { let send_back = match action { Action::SwitchScene(scene) => match scene { - Scene::ChangePortsPopUp => { + Scene::ChangePortsPopUp { + connection_mode_old_value, + } => { if self.connection_mode == ConnectionMode::CustomPorts { self.active = true; + self.connection_mode_old_value = connection_mode_old_value; self.validate(); self.port_from_old_value = self.port_from.value().parse().unwrap_or_default(); diff --git a/node-launchpad/src/components/status.rs b/node-launchpad/src/components/status.rs index 5bcaac42cd..9f054f15a0 100644 --- a/node-launchpad/src/components/status.rs +++ b/node-launchpad/src/components/status.rs @@ -456,7 +456,7 @@ impl Component for Status { safenode_path: self.safenode_path.clone(), data_dir_path: Some(self.data_dir_path.clone()), action_sender: action_sender.clone(), - connection_mode: self.connection_mode.clone(), + connection_mode: self.connection_mode, port_range: Some(port_range), }; diff --git a/node-launchpad/src/connection_mode.rs b/node-launchpad/src/connection_mode.rs index 51286d7b0a..c3f5290327 100644 --- a/node-launchpad/src/connection_mode.rs +++ b/node-launchpad/src/connection_mode.rs @@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter, Result}; use serde::{Deserialize, Serialize}; use strum::EnumIter; -#[derive(Clone, Debug, Default, EnumIter, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, EnumIter, Eq, Hash, PartialEq)] pub enum ConnectionMode { #[default] Automatic, diff --git a/node-launchpad/src/mode.rs b/node-launchpad/src/mode.rs index 9a515383e5..2f0d356599 100644 --- a/node-launchpad/src/mode.rs +++ b/node-launchpad/src/mode.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; +use crate::connection_mode::ConnectionMode; + #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Scene { #[default] @@ -16,7 +18,9 @@ pub enum Scene { Help, ChangeDrivePopUp, ChangeConnectionModePopUp, - ChangePortsPopUp, + ChangePortsPopUp { + connection_mode_old_value: Option, + }, BetaProgrammePopUp, ManageNodesPopUp, ResetNodesPopUp, From 356d6a424e5a86cfd9b4b3b07757cab90146d801 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 2 Sep 2024 13:40:52 +0200 Subject: [PATCH 22/24] feat(launchpad): port range validation user feedback --- .../src/components/popup/port_range.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/node-launchpad/src/components/popup/port_range.rs b/node-launchpad/src/components/popup/port_range.rs index 6565c1097b..491294a96a 100644 --- a/node-launchpad/src/components/popup/port_range.rs +++ b/node-launchpad/src/components/popup/port_range.rs @@ -12,6 +12,7 @@ use super::super::super::node_mgmt::{PORT_MAX, PORT_MIN}; use super::super::utils::centered_rect_fixed; use super::super::Component; use super::manage_nodes::MAX_NODE_COUNT; +use crate::style::RED; use crate::{ action::{Action, OptionsActions}, connection_mode::ConnectionMode, @@ -123,7 +124,10 @@ impl PortRangePopUp { let input_line = Line::from(vec![ Span::styled( format!("{}{} ", spaces_from, self.port_from.value()), - Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(), + Style::default() + .fg(if self.can_save { VIVID_SKY_BLUE } else { RED }) + .bg(INDIGO) + .underlined(), ), Span::styled(" to ", Style::default().fg(GHOST_WHITE)), Span::styled( @@ -135,10 +139,22 @@ impl PortRangePopUp { f.render_widget(input_line, layer_two[1]); - let text = Paragraph::new("Choose the start of the port range. The range will then match the number of nodes on this device.") - .block(block::Block::default().padding(Padding::horizontal(2))) - .alignment(Alignment::Center) - .wrap(Wrap { trim: true }); + let text = Paragraph::new(vec![ + Line::from(Span::styled( + format!( + "Choose the start of the range of {} ports.", + PORT_ALLOCATION + 1 + ), + Style::default().fg(GHOST_WHITE), + )), + Line::from(Span::styled( + format!("This must be between {} and {}.", PORT_MIN, PORT_MAX), + Style::default().fg(if self.can_save { GHOST_WHITE } else { RED }), + )), + ]) + .block(block::Block::default().padding(Padding::horizontal(2))) + .alignment(Alignment::Center) + .wrap(Wrap { trim: true }); f.render_widget(text.fg(GHOST_WHITE), layer_two[2]); let dash = Block::new() From cc11832e41c5f4eb63a6979693f4fcfad37b57e9 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 2 Sep 2024 18:22:13 +0200 Subject: [PATCH 23/24] test(launchpad): avoid executing code in comments --- node-launchpad/src/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-launchpad/src/error.rs b/node-launchpad/src/error.rs index 9f05fe292c..1f487ee558 100644 --- a/node-launchpad/src/error.rs +++ b/node-launchpad/src/error.rs @@ -25,7 +25,7 @@ use ratatui::{ /// 6. Handle the input for the error popup by calling the `handle_input` method. /// /// Example: -/// ```rust +/// ```ignore /// use crate::error::ErrorPopup; /// /// pub struct MyComponent { @@ -67,7 +67,7 @@ use ratatui::{ /// /// How to trigger the error /// -/// ```rust +/// ```ignore /// self.error_popup = Some(ErrorPopup::new( /// "Error".to_string(), /// "This is a test error message".to_string(), From c3b3d56718ab7d663ebf44331679849298a9e4e0 Mon Sep 17 00:00:00 2001 From: Lautaro Mazzitelli Date: Mon, 2 Sep 2024 18:22:40 +0200 Subject: [PATCH 24/24] chore(launchpad): fixing merge conflicts --- node-launchpad/src/app.rs | 117 +++++++++++++++++++++++++++++------ node-launchpad/src/config.rs | 61 ++++++------------ 2 files changed, 116 insertions(+), 62 deletions(-) diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index d5e8083148..f5163a5837 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -114,10 +114,6 @@ impl App { // Popups let reset_nodes = ResetNodesPopup::default(); - let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; - let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; - let port_range = PortRangePopUp::new(connection_mode, port_from, port_to); - let beta_programme = BetaProgramme::new(app_data.discord_username.clone()); let manage_nodes = ManageNodes::new(app_data.nodes_to_start, storage_mountpoint.clone())?; let change_drive = ChangeDrivePopup::new(storage_mountpoint.clone())?; let change_connection_mode = ChangeConnectionModePopUp::new(connection_mode)?; @@ -126,7 +122,15 @@ impl App { Ok(Self { config, - app_data, + app_data: AppData { + discord_username: app_data.discord_username.clone(), + nodes_to_start: app_data.nodes_to_start, + storage_mountpoint: Some(storage_mountpoint), + storage_drive: Some(storage_drive), + connection_mode: Some(connection_mode), + port_from: Some(port_from), + port_to: Some(port_to), + }, tick_rate, frame_rate, components: vec![ @@ -322,14 +326,17 @@ mod tests { let mountpoint = get_primary_mount_point(); - // Create a valid configuration file + // Create a valid configuration file with all fields let valid_config = format!( r#" {{ "discord_username": "happy_user", "nodes_to_start": 5, "storage_mountpoint": "{}", - "storage_drive": "C:" + "storage_drive": "C:", + "connection_mode": "Automatic", + "port_from": 12000, + "port_to": 13000 }} "#, mountpoint.display() @@ -348,13 +355,17 @@ mod tests { match app_result { Ok(app) => { - // Check if the discord_username and nodes_to_start were correctly loaded + // Check if all fields were correctly loaded assert_eq!(app.app_data.discord_username, "happy_user"); assert_eq!(app.app_data.nodes_to_start, 5); - // Check if the storage_mountpoint is set correctly assert_eq!(app.app_data.storage_mountpoint, Some(mountpoint)); - // Check if the storage_drive is set correctly assert_eq!(app.app_data.storage_drive, Some("C:".to_string())); + assert_eq!( + app.app_data.connection_mode, + Some(ConnectionMode::Automatic) + ); + assert_eq!(app.app_data.port_from, Some(12000)); + assert_eq!(app.app_data.port_to, Some(13000)); write!(output, "App created successfully with valid configuration")?; } @@ -382,11 +393,14 @@ mod tests { let temp_dir = tempdir()?; let test_app_data_path = temp_dir.path().join("test_app_data.json"); - // Create a custom configuration file with only the first two settings + // Create a custom configuration file with only some settings let custom_config = r#" { "discord_username": "test_user", - "nodes_to_start": 3 + "nodes_to_start": 3, + "connection_mode": "Custom Ports", + "port_from": 12000, + "port_to": 13000 } "#; std::fs::write(&test_app_data_path, custom_config)?; @@ -402,13 +416,20 @@ mod tests { match app_result { Ok(app) => { - // Check if the discord_username and nodes_to_start were correctly loaded + // Check if the fields were correctly loaded assert_eq!(app.app_data.discord_username, "test_user"); assert_eq!(app.app_data.nodes_to_start, 3); - // Check if the storage_mountpoint is None (not set) - assert_eq!(app.app_data.storage_mountpoint, None); - // Check if the storage_drive is None (not set) - assert_eq!(app.app_data.storage_drive, None); + // Check if the storage_mountpoint is Some (automatically set) + assert!(app.app_data.storage_mountpoint.is_some()); + // Check if the storage_drive is Some (automatically set) + assert!(app.app_data.storage_drive.is_some()); + // Check the new fields + assert_eq!( + app.app_data.connection_mode, + Some(ConnectionMode::CustomPorts) + ); + assert_eq!(app.app_data.port_from, Some(12000)); + assert_eq!(app.app_data.port_to, Some(13000)); write!( output, @@ -453,8 +474,14 @@ mod tests { Ok(app) => { assert_eq!(app.app_data.discord_username, ""); assert_eq!(app.app_data.nodes_to_start, 1); - assert_eq!(app.app_data.storage_mountpoint, None); - assert_eq!(app.app_data.storage_drive, None); + assert!(app.app_data.storage_mountpoint.is_some()); + assert!(app.app_data.storage_drive.is_some()); + assert_eq!( + app.app_data.connection_mode, + Some(ConnectionMode::Automatic) + ); + assert_eq!(app.app_data.port_from, Some(PORT_MIN)); + assert_eq!(app.app_data.port_to, Some(PORT_MAX)); write!( output, @@ -491,7 +518,10 @@ mod tests { "discord_username": "test_user", "nodes_to_start": 5, "storage_mountpoint": "/non/existent/path", - "storage_drive": "Z:" + "storage_drive": "Z:", + "connection_mode": "Custom Ports", + "port_from": 12000, + "port_to": 13000 } "#; std::fs::write(&config_path, invalid_config)?; @@ -521,4 +551,51 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_app_default_connection_mode_and_ports() -> Result<()> { + // Create a temporary directory for our test + let temp_dir = tempdir()?; + let test_app_data_path = temp_dir.path().join("test_app_data.json"); + + // Create a custom configuration file without connection mode and ports + let custom_config = r#" + { + "discord_username": "test_user", + "nodes_to_start": 3 + } + "#; + std::fs::write(&test_app_data_path, custom_config)?; + + // Create default PeersArgs + let peers_args = PeersArgs::default(); + + // Create and run the App + let app_result = App::new(60.0, 60.0, peers_args, None, Some(test_app_data_path)).await; + + match app_result { + Ok(app) => { + // Check if the discord_username and nodes_to_start were correctly loaded + assert_eq!(app.app_data.discord_username, "test_user"); + assert_eq!(app.app_data.nodes_to_start, 3); + + // Check if the connection_mode is set to the default (Automatic) + assert_eq!( + app.app_data.connection_mode, + Some(ConnectionMode::Automatic) + ); + + // Check if the port range is set to the default values + assert_eq!(app.app_data.port_from, Some(PORT_MIN)); + assert_eq!(app.app_data.port_to, Some(PORT_MAX)); + + println!("App created successfully with default connection mode and ports"); + } + Err(e) => { + panic!("App creation failed: {}", e); + } + } + + Ok(()) + } } diff --git a/node-launchpad/src/config.rs b/node-launchpad/src/config.rs index 59dfe3a975..93b0bd60be 100644 --- a/node-launchpad/src/config.rs +++ b/node-launchpad/src/config.rs @@ -7,7 +7,6 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::connection_mode::ConnectionMode; -use crate::system; use crate::system::get_primary_mount_point; use crate::{action::Action, mode::Scene}; use color_eyre::eyre::{eyre, Result}; @@ -141,20 +140,9 @@ impl AppData { let data = std::fs::read_to_string(&config_path) .map_err(|_| color_eyre::eyre::eyre!("Failed to read app data file"))?; - let mut app_data: AppData = serde_json::from_str(&data) + let app_data: AppData = serde_json::from_str(&data) .map_err(|_| color_eyre::eyre::eyre!("Failed to parse app data"))?; - if app_data.storage_mountpoint.is_none() || app_data.storage_drive.is_none() { - // If the storage drive is not set, set it to the default mount point - let drive_info = system::get_default_mount_point()?; - app_data.storage_drive = Some(drive_info.0); - app_data.storage_mountpoint = Some(drive_info.1); - debug!("Setting storage drive to {:?}", app_data.storage_mountpoint); - } - - if app_data.connection_mode.is_none() { - app_data.connection_mode = Some(ConnectionMode::default()); - } Ok(app_data) } @@ -724,6 +712,9 @@ mod tests { assert_eq!(app_data.nodes_to_start, 1); assert_eq!(app_data.storage_mountpoint, None); assert_eq!(app_data.storage_drive, None); + assert_eq!(app_data.connection_mode, None); + assert_eq!(app_data.port_from, None); + assert_eq!(app_data.port_to, None); Ok(()) } @@ -748,6 +739,9 @@ mod tests { assert_eq!(app_data.nodes_to_start, 3); assert_eq!(app_data.storage_mountpoint, None); assert_eq!(app_data.storage_drive, None); + assert_eq!(app_data.connection_mode, None); + assert_eq!(app_data.port_from, None); + assert_eq!(app_data.port_to, None); Ok(()) } @@ -773,35 +767,9 @@ mod tests { assert_eq!(app_data.nodes_to_start, 3); assert_eq!(app_data.storage_mountpoint, None); assert_eq!(app_data.storage_drive, Some("C:".to_string())); - - Ok(()) - } - - #[test] - fn test_app_data_complete_info() -> Result<()> { - let temp_dir = tempdir()?; - let complete_data_path = temp_dir.path().join("complete_app_data.json"); - - let complete_data = r#" - { - "discord_username": "complete_user", - "nodes_to_start": 5, - "storage_mountpoint": "/mnt/data", - "storage_drive": "D:" - } - "#; - - std::fs::write(&complete_data_path, complete_data)?; - - let app_data = AppData::load(Some(complete_data_path))?; - - assert_eq!(app_data.discord_username, "complete_user"); - assert_eq!(app_data.nodes_to_start, 5); - assert_eq!( - app_data.storage_mountpoint, - Some(PathBuf::from("/mnt/data")) - ); - assert_eq!(app_data.storage_drive, Some("D:".to_string())); + assert_eq!(app_data.connection_mode, None); + assert_eq!(app_data.port_from, None); + assert_eq!(app_data.port_to, None); Ok(()) } @@ -817,6 +785,9 @@ mod tests { app_data.nodes_to_start = 4; app_data.storage_mountpoint = Some(PathBuf::from("/mnt/test")); app_data.storage_drive = Some("E:".to_string()); + app_data.connection_mode = Some(ConnectionMode::CustomPorts); + app_data.port_from = Some(12000); + app_data.port_to = Some(13000); // Save to custom path app_data.save(Some(test_path.clone()))?; @@ -831,6 +802,12 @@ mod tests { Some(PathBuf::from("/mnt/test")) ); assert_eq!(loaded_data.storage_drive, Some("E:".to_string())); + assert_eq!( + loaded_data.connection_mode, + Some(ConnectionMode::CustomPorts) + ); + assert_eq!(loaded_data.port_from, Some(12000)); + assert_eq!(loaded_data.port_to, Some(13000)); Ok(()) }