Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wait for account to be ready to connect #1486

Merged
merged 7 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nym-vpn-app/src-tauri/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ pub enum ErrorKey {
NoActiveSubscription,
DeviceNotRegistered,
DeviceNotActive,
ReadyToConnectPending,
// Forwarded from proto `connection_status_update::StatusType`
EntryGatewayNotRouting,
ExitRouterPingIpv4,
Expand Down Expand Up @@ -309,6 +310,7 @@ impl From<ConnectRequestErrorType> for ErrorKey {
ConnectRequestErrorType::NoActiveSubscription => ErrorKey::NoActiveSubscription,
ConnectRequestErrorType::DeviceNotRegistered => ErrorKey::DeviceNotRegistered,
ConnectRequestErrorType::DeviceNotActive => ErrorKey::DeviceNotActive,
ConnectRequestErrorType::Pending => ErrorKey::ReadyToConnectPending,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions nym-vpn-app/src/hooks/useI18nError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ function useI18nError() {
return t('account.device.not-registered');
case 'DeviceNotActive':
return t('account.device.not-active');
case 'ReadyToConnectPending':
return t('connection.ready-to-connect');
case 'EntryGatewayNotRouting':
return t('entry-node-routing');
case 'ExitRouterPingIpv4':
Expand Down
3 changes: 2 additions & 1 deletion nym-vpn-app/src/i18n/en/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
},
"add-ipv6-route": "Failed to add ipv6 default route to capture ipv6 traffic",
"tun-device": "Tun device failed",
"routing": "Routing failed"
"routing": "Routing failed",
"ready-to-connect": "It was not yet possible to determine if we are ready to connect during the check"
},
"account": {
"invalid-recovery-phrase": "Invalid recovery phrase",
Expand Down
1 change: 1 addition & 0 deletions nym-vpn-app/src/types/tauri-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export type ErrorKey =
| 'NoActiveSubscription'
| 'DeviceNotRegistered'
| 'DeviceNotActive'
| 'ReadyToConnectPending'
| 'EntryGatewayNotRouting'
| 'ExitRouterPingIpv4'
| 'ExitRouterPingIpv6'
Expand Down
4 changes: 1 addition & 3 deletions nym-vpn-app/src/ui/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ type NavLocation = {
noBackground?: boolean;
};

type NavBarData = {
[key in Routes]: NavLocation;
};
type NavBarData = Record<Routes, NavLocation>;

export default function TopBar() {
const location = useLocation();
Expand Down
99 changes: 82 additions & 17 deletions nym-vpn-core/crates/nym-vpn-account-controller/src/shared_state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

use std::{fmt, sync::Arc};
use std::{fmt, sync::Arc, time::Duration};

use nym_vpn_api_client::response::{
NymVpnAccountStatusResponse, NymVpnAccountSummarySubscription, NymVpnDeviceStatus,
Expand Down Expand Up @@ -77,23 +77,28 @@ impl SharedAccountState {
}

pub async fn is_ready_to_connect(&self) -> ReadyToConnect {
let state = self.lock().await.clone();
if state.mnemonic != Some(MnemonicState::Stored) {
return ReadyToConnect::NoMnemonicStored;
}
if state.account != Some(AccountState::Active) {
return ReadyToConnect::AccountNotActive;
}
if state.subscription != Some(SubscriptionState::Active) {
return ReadyToConnect::NoActiveSubscription;
}
match state.device {
None => return ReadyToConnect::DeviceNotRegistered,
Some(DeviceState::NotRegistered) => return ReadyToConnect::DeviceNotRegistered,
Some(DeviceState::Inactive) => return ReadyToConnect::DeviceNotActive,
_ => {}
self.lock().await.is_ready_now()
}

// Wait until the account status has been fetched from the API.
// Returns:
// - Some: is the readyness status,
// - None: timeout waiting for the status from the API.
pub async fn wait_for_ready_to_connect(&self, timeout: Duration) -> Option<ReadyToConnect> {
tracing::info!("Waiting for account state to be ready to connect");
let start = tokio::time::Instant::now();
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
if start.elapsed() > timeout {
tracing::error!("Timed out waiting for account state to be ready to connect");
return None;
}
if let Some(ready_to_connect) = self.lock().await.is_ready() {
tracing::info!("Account readyness status: {}", ready_to_connect);
return Some(ready_to_connect);
}
}
ReadyToConnect::Ready
}

pub(crate) async fn is_ready_to_register_device(&self) -> ReadyToRegisterDevice {
Expand Down Expand Up @@ -233,6 +238,66 @@ pub enum DeviceState {
DeleteMe,
}

impl AccountStateSummary {
// If we are ready right right now.
fn is_ready_now(&self) -> ReadyToConnect {
if self.mnemonic != Some(MnemonicState::Stored) {
return ReadyToConnect::NoMnemonicStored;
}
if self.account != Some(AccountState::Active) {
return ReadyToConnect::AccountNotActive;
}
if self.subscription != Some(SubscriptionState::Active) {
return ReadyToConnect::NoActiveSubscription;
}
match self.device {
None => return ReadyToConnect::DeviceNotRegistered,
Some(DeviceState::NotRegistered) => return ReadyToConnect::DeviceNotRegistered,
Some(DeviceState::Inactive) => return ReadyToConnect::DeviceNotActive,
_ => {}
}
ReadyToConnect::Ready
}

// If we know if we are ready
// - Some: we know if we are ready
// - None: we don't yet know if we are ready
fn is_ready(&self) -> Option<ReadyToConnect> {
match self.mnemonic {
Some(MnemonicState::NotStored) => return Some(ReadyToConnect::NoMnemonicStored),
Some(MnemonicState::Stored) => {}
None => return None,
}
match self.account {
Some(AccountState::NotRegistered) => return Some(ReadyToConnect::AccountNotActive),
Some(AccountState::Inactive) => return Some(ReadyToConnect::AccountNotActive),
Some(AccountState::DeleteMe) => return Some(ReadyToConnect::AccountNotActive),
Some(AccountState::Active) => {}
None => return None,
}
match self.subscription {
Some(SubscriptionState::NotActive) => {
return Some(ReadyToConnect::NoActiveSubscription)
}
Some(SubscriptionState::Pending) => return Some(ReadyToConnect::NoActiveSubscription),
Some(SubscriptionState::Complete) => return Some(ReadyToConnect::NoActiveSubscription),
Some(SubscriptionState::Active) => {}
None => return None,
}
match self.device {
Some(DeviceState::NotRegistered) => return Some(ReadyToConnect::DeviceNotRegistered),
Some(DeviceState::Inactive) => return Some(ReadyToConnect::DeviceNotActive),
Some(DeviceState::DeleteMe) => return Some(ReadyToConnect::DeviceNotActive),
Some(DeviceState::Active) => {}
None => return None,
}
if self.pending_zk_nym {
return None;
}
Some(ReadyToConnect::Ready)
}
}

impl fmt::Display for AccountStateSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
Expand Down
36 changes: 20 additions & 16 deletions nym-vpn-core/crates/nym-vpn-lib/src/platform/account.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

use std::{path::PathBuf, str::FromStr, sync::Arc};
use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration};

use nym_vpn_account_controller::{AccountCommand, ReadyToConnect, SharedAccountState};
use nym_vpn_store::{keys::KeyStore, mnemonic::MnemonicStorage};
Expand Down Expand Up @@ -84,8 +84,8 @@ impl AccountControllerHandle {
}
}

async fn is_ready_to_connect(&self) -> ReadyToConnect {
self.shared_state.is_ready_to_connect().await
async fn wait_for_ready_to_connect(&self, timeout: Duration) -> Option<ReadyToConnect> {
self.shared_state.wait_for_ready_to_connect(timeout).await
}

async fn shutdown_and_wait(self) {
Expand Down Expand Up @@ -118,23 +118,27 @@ async fn get_shared_account_state() -> Result<SharedAccountState, VpnError> {
}
}

async fn is_account_ready_to_connect() -> Result<ReadyToConnect, VpnError> {
async fn wait_for_account_ready_to_connect(timeout: Duration) -> Result<ReadyToConnect, VpnError> {
if let Some(guard) = &*ACCOUNT_CONTROLLER_HANDLE.lock().await {
Ok(guard.is_ready_to_connect().await)
guard
.wait_for_ready_to_connect(timeout)
.await
.ok_or(VpnError::AccountStatusUnknown)
} else {
Err(VpnError::InvalidStateError {
details: "Account controller is not running.".to_owned(),
})
}
}

pub(super) async fn assert_account_ready_to_connect() -> Result<(), VpnError> {
match is_account_ready_to_connect().await? {
pub(super) async fn assert_account_ready_to_connect(timeout: Duration) -> Result<(), VpnError> {
match wait_for_account_ready_to_connect(timeout).await? {
ReadyToConnect::Ready => Ok(()),
not_ready_to_connect => {
tracing::warn!("Not ready to connect: {:?}", not_ready_to_connect);
Err(not_ready_to_connect.into())
}
ReadyToConnect::NoMnemonicStored => Err(VpnError::NoAccountStored),
ReadyToConnect::AccountNotActive => Err(VpnError::AccountNotActive),
ReadyToConnect::NoActiveSubscription => Err(VpnError::NoActiveSubscription),
ReadyToConnect::DeviceNotRegistered => Err(VpnError::AccountDeviceNotRegistered),
ReadyToConnect::DeviceNotActive => Err(VpnError::AccountDeviceNotActive),
}
}

Expand Down Expand Up @@ -164,8 +168,6 @@ pub(super) async fn store_account_mnemonic(mnemonic: &str, path: &str) -> Result
details: err.to_string(),
})?;

send_account_command(AccountCommand::UpdateSharedAccountState).await?;

Ok(())
}

Expand Down Expand Up @@ -196,8 +198,6 @@ pub(super) async fn remove_account_mnemonic(path: &str) -> Result<bool, VpnError
details: err.to_string(),
})?;

send_account_command(AccountCommand::UpdateSharedAccountState).await?;

Ok(is_account_removed_success)
}

Expand All @@ -211,7 +211,11 @@ pub(super) async fn reset_device_identity(path: &str) -> Result<(), VpnError> {
})
}

pub(super) async fn get_account_summary() -> Result<AccountStateSummary, VpnError> {
pub(super) async fn update_account_state() -> Result<(), VpnError> {
send_account_command(AccountCommand::UpdateSharedAccountState).await
}

pub(super) async fn get_account_state() -> Result<AccountStateSummary, VpnError> {
let shared_account_state = get_shared_account_state().await?;
let account_state_summary = shared_account_state.lock().await.clone();
Ok(AccountStateSummary::from(account_state_summary))
Expand Down
3 changes: 3 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib/src/platform/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub enum VpnError {

#[error("device not active")]
AccountDeviceNotActive,

#[error("account status unknown")]
AccountStatusUnknown,
}

impl From<nym_vpn_account_controller::ReadyToConnect> for VpnError {
Expand Down
15 changes: 11 additions & 4 deletions nym-vpn-core/crates/nym-vpn-lib/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub mod swift;

mod account;

use std::{env, path::PathBuf, sync::Arc};
use std::{env, path::PathBuf, sync::Arc, time::Duration};

use account::AccountControllerHandle;
use lazy_static::lazy_static;
Expand Down Expand Up @@ -60,7 +60,8 @@ async fn start_vpn_inner(config: VPNConfig) -> Result<(), VpnError> {
// We want to move this check into the state machine so that it happens during the connecting
// state instead. This would allow us more flexibility in waiting for the account to be ready
// and handle errors in a unified manner.
account::assert_account_ready_to_connect().await?;
let timeout = Duration::from_secs(10);
account::assert_account_ready_to_connect(timeout).await?;

let mut guard = STATE_MACHINE_HANDLE.lock().await;

Expand Down Expand Up @@ -164,8 +165,14 @@ pub fn resetDeviceIdentity(path: String) -> Result<(), VpnError> {

#[allow(non_snake_case)]
#[uniffi::export]
pub fn getAccountSummary() -> Result<AccountStateSummary, VpnError> {
RUNTIME.block_on(account::get_account_summary())
pub fn updateAccountState() -> Result<(), VpnError> {
RUNTIME.block_on(account::update_account_state())
}

#[allow(non_snake_case)]
#[uniffi::export]
pub fn getAccountState() -> Result<AccountStateSummary, VpnError> {
RUNTIME.block_on(account::get_account_state())
}

#[allow(non_snake_case)]
Expand Down
Loading
Loading