[ENG-1333] Display active P2P listeners (#1644)

* wip

* share the actual port

* fixes

* wip: remove it

* cargo fmt

* thanks @niikeec
This commit is contained in:
Oscar Beaumont 2023-11-13 15:02:03 +08:00 committed by GitHub
parent 561eacfb6e
commit 5705f40aae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 34 deletions

View file

@ -1,6 +1,7 @@
use crate::{invalidate_query, job::JobProgressEvent, node::config::NodeConfig, Node};
use itertools::Itertools;
use rspc::{alpha::Rspc, Config, ErrorCode};
use sd_p2p::P2PStatus;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::sync::{atomic::Ordering, Arc};
@ -91,11 +92,12 @@ impl From<NodeConfig> for SanitisedNodeConfig {
}
}
#[derive(Serialize, Deserialize, Debug, Type)]
#[derive(Serialize, Debug, Type)]
struct NodeState {
#[serde(flatten)]
config: SanitisedNodeConfig,
data_path: String,
p2p: P2PStatus,
}
pub(crate) fn mount() -> Arc<Router> {
@ -124,6 +126,7 @@ pub(crate) fn mount() -> Arc<Router> {
.to_str()
.expect("Found non-UTF-8 path")
.to_string(),
p2p: node.p2p.manager.status(),
})
})
})

View file

@ -31,8 +31,10 @@ use crate::{
#[derive(Debug)]
pub(crate) struct DynamicManagerState {
pub(crate) config: ManagerConfig,
pub(crate) ipv4_listener_id: Option<ListenerId>,
pub(crate) ipv6_listener_id: Option<ListenerId>,
pub(crate) ipv4_listener_id: Option<Result<ListenerId, String>>,
pub(crate) ipv4_port: Option<u16>,
pub(crate) ipv6_listener_id: Option<Result<ListenerId, String>>,
pub(crate) ipv6_port: Option<u16>,
// A map of connected clients.
// This includes both inbound and outbound connections!
pub(crate) connected: HashMap<libp2p::PeerId, RemoteIdentity>,
@ -84,7 +86,9 @@ impl Manager {
state: RwLock::new(DynamicManagerState {
config,
ipv4_listener_id: None,
ipv4_port: None,
ipv6_listener_id: None,
ipv6_port: None,
connected: Default::default(),
connections: Default::default(),
}),
@ -260,6 +264,28 @@ impl Manager {
)
}
pub fn status(&self) -> P2PStatus {
let state = self.state.read().unwrap_or_else(PoisonError::into_inner);
P2PStatus {
ipv4: match state.ipv4_listener_id.clone() {
Some(Ok(_)) => match state.ipv4_port {
Some(port) => ListenerStatus::Listening { port },
None => ListenerStatus::Enabling,
},
Some(Err(error)) => ListenerStatus::Error { error },
None => ListenerStatus::Disabled,
},
ipv6: match state.ipv6_listener_id.clone() {
Some(Ok(_)) => match state.ipv6_port {
Some(port) => ListenerStatus::Listening { port },
None => ListenerStatus::Enabling,
},
Some(Err(error)) => ListenerStatus::Error { error },
None => ListenerStatus::Disabled,
},
}
}
pub async fn shutdown(&self) {
let (tx, rx) = oneshot::channel();
if self
@ -310,6 +336,21 @@ impl Default for ManagerConfig {
}
}
#[derive(Serialize, Debug, Type)]
pub struct P2PStatus {
ipv4: ListenerStatus,
ipv6: ListenerStatus,
}
#[derive(Serialize, Debug, Type)]
#[serde(tag = "status")]
pub enum ListenerStatus {
Disabled,
Enabling,
Listening { port: u16 },
Error { error: String },
}
fn ok<T>(v: Result<T, Infallible>) -> T {
match v {
Ok(v) => v,

View file

@ -93,38 +93,48 @@ impl ManagerStream {
if state.config.enabled {
let port = state.config.port.unwrap_or(0);
if state.ipv4_listener_id.is_none() {
match swarm.listen_on(socketaddr_to_quic_multiaddr(&SocketAddr::from((
Ipv4Addr::UNSPECIFIED,
port,
)))) {
Ok(listener_id) => {
debug!("created ipv4 listener with id '{:?}'", listener_id);
state.ipv4_listener_id = Some(listener_id);
}
Err(err) => error!("failed to listener on '0.0.0.0:{port}': {err}"),
};
if state.ipv4_listener_id.is_none() || matches!(state.ipv6_listener_id, Some(Err(_))) {
state.ipv4_listener_id = Some(
swarm
.listen_on(socketaddr_to_quic_multiaddr(&SocketAddr::from((
Ipv4Addr::UNSPECIFIED,
port,
))))
.map(|id| {
debug!("registered ipv4 listener: {id:?}");
id
})
.map_err(|err| {
error!("failed to register ipv4 listener on port {port}: {err}");
err.to_string()
}),
);
}
if state.ipv6_listener_id.is_none() {
match swarm.listen_on(socketaddr_to_quic_multiaddr(&SocketAddr::from((
Ipv6Addr::UNSPECIFIED,
port,
)))) {
Ok(listener_id) => {
debug!("created ipv6 listener with id '{:?}'", listener_id);
state.ipv6_listener_id = Some(listener_id);
}
Err(err) => error!("failed to listener on '[::]:{port}': {err}"),
};
if state.ipv4_listener_id.is_none() || matches!(state.ipv6_listener_id, Some(Err(_))) {
state.ipv6_listener_id = Some(
swarm
.listen_on(socketaddr_to_quic_multiaddr(&SocketAddr::from((
Ipv6Addr::UNSPECIFIED,
port,
))))
.map(|id| {
debug!("registered ipv6 listener: {id:?}");
id
})
.map_err(|err| {
error!("failed to register ipv6 listener on port {port}: {err}");
err.to_string()
}),
);
}
} else {
if let Some(listener) = state.ipv4_listener_id.take() {
if let Some(Ok(listener)) = state.ipv4_listener_id.take() {
debug!("removing ipv4 listener with id '{:?}'", listener);
swarm.remove_listener(listener);
}
if let Some(listener) = state.ipv6_listener_id.take() {
if let Some(Ok(listener)) = state.ipv6_listener_id.take() {
debug!("removing ipv6 listener with id '{:?}'", listener);
swarm.remove_listener(listener);
}
@ -221,7 +231,30 @@ impl ManagerStream {
SwarmEvent::IncomingConnection { local_addr, .. } => debug!("incoming connection from '{}'", local_addr),
SwarmEvent::IncomingConnectionError { local_addr, error, .. } => warn!("handshake error with incoming connection from '{}': {}", local_addr, error),
SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => warn!("error establishing connection with '{:?}': {}", peer_id, error),
SwarmEvent::NewListenAddr { address, .. } => {
SwarmEvent::NewListenAddr { listener_id, address, .. } => {
let addr = match quic_multiaddr_to_socketaddr(address.clone()) {
Ok(addr) => addr,
Err(err) => {
warn!("error passing listen address '{address:?}': {err:?}");
continue;
}
};
{
let mut state = self.manager.state.write().unwrap_or_else(PoisonError::into_inner);
if let Some(Ok(lid)) = &state.ipv4_listener_id {
if *lid == listener_id {
state.ipv4_port = Some(addr.port());
}
}
if let Some(Ok(lid)) = &state.ipv6_listener_id {
if *lid == listener_id {
state.ipv6_port = Some(addr.port());
}
}
}
match quic_multiaddr_to_socketaddr(address) {
Ok(addr) => {
trace!("listen address added: {}", addr);

View file

@ -1,11 +1,11 @@
import { captureException } from '@sentry/browser';
import { useEffect, useState } from 'react';
import { PropsWithChildren, useEffect, useState } from 'react';
import {
ErrorBoundary,
ErrorBoundaryPropsWithComponent,
FallbackProps
} from 'react-error-boundary';
import { Navigate, useRouteError } from 'react-router';
import { useRouteError } from 'react-router';
import { useDebugState } from '@sd/client';
import { Button, Dialogs } from '@sd/ui';
@ -170,7 +170,7 @@ export const BetterErrorBoundary = ({
children,
FallbackComponent,
...props
}: ErrorBoundaryPropsWithComponent) => {
}: PropsWithChildren<ErrorBoundaryPropsWithComponent>) => {
useEffect(() => {
const id = setTimeout(
() => localStorage.removeItem(RENDERING_ERROR_LOCAL_STORAGE_KEY),

View file

@ -171,6 +171,16 @@ export const Component = () => {
<div className="flex flex-col gap-4">
<h1 className="mb-3 text-lg font-bold text-ink">Networking</h1>
{/* TODO: Add some UI for this stuff */}
{/* {node.data?.p2p.ipv4.status === 'Listening' ||
node.data?.p2p.ipv4.status === 'Enabling'
? `0.0.0.0:${node.data?.p2p.ipv4?.port || 0}`
: ''}
{node.data?.p2p.ipv6.status === 'Listening' ||
node.data?.p2p.ipv6.status === 'Enabling'
? `[::1]:${node.data?.p2p.ipv6?.port || 0}`
: ''} */}
<Setting
mini
title="Enable Networking"

View file

@ -1,4 +1,6 @@
import { useFeatureFlag, useP2PEvents, withFeatureFlag } from '@sd/client';
import { useEffect, useState } from 'react';
import { useBridgeQuery, useFeatureFlag, useP2PEvents, withFeatureFlag } from '@sd/client';
import { toast } from '@sd/ui';
import { startPairing } from './pairing';
import { SpacedropUI } from './Spacedrop';
@ -23,3 +25,49 @@ export function P2P() {
</>
);
}
export function useP2PErrorToast() {
const nodeState = useBridgeQuery(['nodeState']);
const [didShowError, setDidShowError] = useState({
ipv4: false,
ipv6: false
});
// TODO: This can probally be improved in the future. Theorically if you enable -> disable -> then enable and it fails both enables the error won't be shown.
useEffect(() => {
const ipv4Error =
(nodeState.data?.p2p_enabled && nodeState.data?.p2p.ipv4.status === 'Error') || false;
const ipv6Error =
(nodeState.data?.p2p_enabled && nodeState.data?.p2p.ipv6.status === 'Error') || false;
if (!didShowError.ipv4 && ipv4Error)
toast.error(
{
title: 'Error starting up P2P!',
body: 'Error creating the IPv4 listener. Please check your firewall settings!'
},
{
id: 'ipv4-listener-error'
}
);
if (!didShowError.ipv6 && ipv6Error)
toast.error(
{
title: 'Error starting up P2P!',
body: 'Error creating the IPv6 listener. Please check your firewall settings!'
},
{
id: 'ipv6-listener-error'
}
);
setDidShowError({
ipv4: ipv4Error,
ipv6: ipv6Error
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodeState.data]);
return null;
}

View file

@ -17,7 +17,7 @@ import {
} from '@sd/client';
import { TooltipProvider } from '@sd/ui';
import { P2P } from './app/p2p';
import { P2P, useP2PErrorToast } from './app/p2p';
import { WithPrismTheme } from './components/TextViewer/prism';
import ErrorFallback, { BetterErrorBoundary } from './ErrorFallback';
@ -56,6 +56,7 @@ const Devtools = () => {
export const SpacedriveInterface = (props: { router: RouterProviderProps['router'] }) => {
useLoadBackendFeatureFlags();
useP2PErrorToast();
return (
<BetterErrorBoundary FallbackComponent={ErrorFallback}>

View file

@ -276,6 +276,8 @@ export type LibraryPreferences = { location?: { [key: string]: LocationSettings
export type LightScanArgs = { location_id: number; sub_path: string }
export type ListenerStatus = { status: "Disabled" } | { status: "Enabling" } | { status: "Listening"; port: number } | { status: "Error"; error: string }
export type Location = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null }
/**
@ -320,7 +322,7 @@ export type MediaLocation = { latitude: number; longitude: number; pluscode: Plu
export type MediaMetadata = ({ type: "Image" } & ImageMetadata) | ({ type: "Video" } & VideoMetadata) | ({ type: "Audio" } & AudioMetadata)
export type NodeState = ({ id: string; name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[] }) & { data_path: string }
export type NodeState = ({ id: string; name: string; p2p_enabled: boolean; p2p_port: number | null; features: BackendFeature[] }) & { data_path: string; p2p: P2PStatus }
export type NonIndexedFileSystemEntries = { entries: ExplorerItem[]; errors: Error[] }
@ -374,6 +376,8 @@ export type P2PEvent = { type: "DiscoveredPeer"; identity: string; metadata: Pee
export type P2PState = { node: { [key: string]: PeerStatus }; libraries: ([string, { [key: string]: PeerStatus }])[]; self_peer_id: PeerId; self_identity: string; config: ManagerConfig; manager_connected: { [key: PeerId]: string }; manager_connections: PeerId[]; dicovery_services: { [key: string]: { [key: string]: string } | null }; discovery_discovered: { [key: string]: { [key: string]: [PeerId, { [key: string]: string }, string[]] } }; discovery_known: { [key: string]: string[] } }
export type P2PStatus = { ipv4: ListenerStatus; ipv6: ListenerStatus }
export type PairingDecision = { decision: "accept"; libraryId: string } | { decision: "reject" }
export type PairingStatus = { type: "EstablishingConnection" } | { type: "PairingRequested" } | { type: "LibraryAlreadyExists" } | { type: "PairingDecisionRequest" } | { type: "PairingInProgress"; data: { library_name: string; library_description: string | null } } | { type: "InitialSyncProgress"; data: number } | { type: "PairingComplete"; data: string } | { type: "PairingRejected" }