mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 12:13:27 +00:00
[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:
parent
561eacfb6e
commit
5705f40aae
|
@ -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(),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in a new issue