node: Implement `sessions` command
Returns session information through the control socket. We also update `rad node status` to display sessions.
This commit is contained in:
parent
f73389fc62
commit
1a1e63d998
|
|
@ -1442,9 +1442,12 @@ checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "localtime"
|
name = "localtime"
|
||||||
version = "1.2.0"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2f2e37f115cdc432fcf760063d45a928f0ea80bcf10be3a6e516cbba883b15e"
|
checksum = "71c67b83b03434bb31132aef0b314b8a49a0db55ce195c7e3c29d27bbf003819"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
|
|
@ -1935,6 +1938,7 @@ dependencies = [
|
||||||
"git-ref-format",
|
"git-ref-format",
|
||||||
"json-color",
|
"json-color",
|
||||||
"lexopt",
|
"lexopt",
|
||||||
|
"localtime",
|
||||||
"log",
|
"log",
|
||||||
"nonempty 0.8.1",
|
"nonempty 0.8.1",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std"
|
||||||
git-ref-format = { version = "0.2.3", features = ["macro"] }
|
git-ref-format = { version = "0.2.3", features = ["macro"] }
|
||||||
json-color = { version = "0.7" }
|
json-color = { version = "0.7" }
|
||||||
lexopt = { version = "0.2" }
|
lexopt = { version = "0.2" }
|
||||||
|
localtime = { version = "1.2.0" }
|
||||||
log = { version = "0.4", features = ["std"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
nonempty = { version = "0.8" }
|
nonempty = { version = "0.8" }
|
||||||
# N.b. this is required to use macros, even though it's re-exported
|
# N.b. this is required to use macros, even though it's re-exported
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ node status` command (or just `rad node` for short):
|
||||||
|
|
||||||
```
|
```
|
||||||
$ rad node status
|
$ rad node status
|
||||||
✓ The node is running
|
✓ Node is running
|
||||||
```
|
```
|
||||||
|
|
||||||
The node also allows us to connect with other nodes in the
|
The node also allows us to connect with other nodes in the
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,15 @@ use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
|
||||||
use std::{process, thread, time};
|
use std::{process, thread, time};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
use localtime::LocalTime;
|
||||||
|
|
||||||
|
use radicle::node;
|
||||||
use radicle::node::{Address, Handle as _, NodeId};
|
use radicle::node::{Address, Handle as _, NodeId};
|
||||||
use radicle::Node;
|
use radicle::Node;
|
||||||
use radicle::{profile, Profile};
|
use radicle::{profile, Profile};
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::Element as _;
|
||||||
|
|
||||||
pub fn start(daemon: bool, options: Vec<OsString>, profile: &Profile) -> anyhow::Result<()> {
|
pub fn start(daemon: bool, options: Vec<OsString>, profile: &Profile) -> anyhow::Result<()> {
|
||||||
// Ask passphrase here, otherwise it'll be a fatal error when running the daemon
|
// Ask passphrase here, otherwise it'll be a fatal error when running the daemon
|
||||||
|
|
@ -119,10 +122,17 @@ pub fn connect(node: &mut Node, nid: NodeId, addr: Address) -> anyhow::Result<()
|
||||||
|
|
||||||
pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
|
pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
|
||||||
if node.is_running() {
|
if node.is_running() {
|
||||||
term::success!("The node is {}", term::format::positive("running"));
|
term::success!("Node is {}", term::format::positive("running"));
|
||||||
} else {
|
} else {
|
||||||
term::info!("The node is {}", term::format::negative("stopped"));
|
term::info!("Node is {}", term::format::negative("stopped"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sessions = sessions(node)?;
|
||||||
|
if let Some(table) = sessions {
|
||||||
|
term::blank();
|
||||||
|
table.print();
|
||||||
|
}
|
||||||
|
|
||||||
if profile.home.node().join("node.log").exists() {
|
if profile.home.node().join("node.log").exists() {
|
||||||
term::blank();
|
term::blank();
|
||||||
// If we're running the node via `systemd` for example, there won't be a log file
|
// If we're running the node via `systemd` for example, there won't be a log file
|
||||||
|
|
@ -131,3 +141,48 @@ pub fn status(node: &Node, profile: &Profile) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sessions(node: &Node) -> Result<Option<term::Table<4, term::Label>>, node::Error> {
|
||||||
|
let sessions = node.sessions()?;
|
||||||
|
if sessions.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let mut table = term::Table::new(term::table::TableOptions::bordered());
|
||||||
|
let now = LocalTime::now();
|
||||||
|
|
||||||
|
table.push([
|
||||||
|
term::format::bold("Peer").into(),
|
||||||
|
term::format::bold("Address").into(),
|
||||||
|
term::format::bold("State").into(),
|
||||||
|
term::format::bold("Since").into(),
|
||||||
|
]);
|
||||||
|
table.divider();
|
||||||
|
|
||||||
|
for sess in sessions {
|
||||||
|
let nid = term::format::tertiary(sess.nid).into();
|
||||||
|
let (addr, state, time) = match sess.state {
|
||||||
|
node::State::Initial => (
|
||||||
|
term::Label::blank(),
|
||||||
|
term::Label::from(term::format::dim("initial")),
|
||||||
|
term::Label::blank(),
|
||||||
|
),
|
||||||
|
node::State::Attempted { addr } => (
|
||||||
|
addr.to_string().into(),
|
||||||
|
term::Label::from(term::format::tertiary("attempted")),
|
||||||
|
term::Label::blank(),
|
||||||
|
),
|
||||||
|
node::State::Connected { addr, since, .. } => (
|
||||||
|
addr.to_string().into(),
|
||||||
|
term::Label::from(term::format::positive("connected")),
|
||||||
|
term::format::dim(now - since).into(),
|
||||||
|
),
|
||||||
|
node::State::Disconnected { retry_at, .. } => (
|
||||||
|
term::Label::blank(),
|
||||||
|
term::Label::from(term::format::negative("disconnected")),
|
||||||
|
term::format::dim(retry_at - now).into(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
table.push([nid, addr, state, time]);
|
||||||
|
}
|
||||||
|
Ok(Some(table))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,10 @@ pub enum Error {
|
||||||
pub fn listen<H: Handle<Error = runtime::HandleError> + 'static>(
|
pub fn listen<H: Handle<Error = runtime::HandleError> + 'static>(
|
||||||
listener: UnixListener,
|
listener: UnixListener,
|
||||||
handle: H,
|
handle: H,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
H::Sessions: serde::Serialize,
|
||||||
|
{
|
||||||
log::debug!(target: "control", "Control thread listening on socket..");
|
log::debug!(target: "control", "Control thread listening on socket..");
|
||||||
let nid = handle.nid()?;
|
let nid = handle.nid()?;
|
||||||
|
|
||||||
|
|
@ -79,7 +82,10 @@ enum CommandError {
|
||||||
fn command<H: Handle<Error = runtime::HandleError> + 'static>(
|
fn command<H: Handle<Error = runtime::HandleError> + 'static>(
|
||||||
stream: &UnixStream,
|
stream: &UnixStream,
|
||||||
mut handle: H,
|
mut handle: H,
|
||||||
) -> Result<(), CommandError> {
|
) -> Result<(), CommandError>
|
||||||
|
where
|
||||||
|
H::Sessions: serde::Serialize,
|
||||||
|
{
|
||||||
let mut reader = BufReader::new(stream);
|
let mut reader = BufReader::new(stream);
|
||||||
let mut writer = LineWriter::new(stream);
|
let mut writer = LineWriter::new(stream);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
@ -109,6 +115,11 @@ fn command<H: Handle<Error = runtime::HandleError> + 'static>(
|
||||||
|
|
||||||
json::to_writer(writer, &seeds)?;
|
json::to_writer(writer, &seeds)?;
|
||||||
}
|
}
|
||||||
|
CommandName::Sessions => {
|
||||||
|
let sessions = handle.sessions()?;
|
||||||
|
|
||||||
|
json::to_writer(writer, &sessions)?;
|
||||||
|
}
|
||||||
CommandName::TrackRepo => {
|
CommandName::TrackRepo => {
|
||||||
let (rid, scope) = parse::args(cmd)?;
|
let (rid, scope) = parse::args(cmd)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ use crate::profile::Home;
|
||||||
use crate::runtime::Emitter;
|
use crate::runtime::Emitter;
|
||||||
use crate::service;
|
use crate::service;
|
||||||
use crate::service::tracking;
|
use crate::service::tracking;
|
||||||
|
use crate::service::NodeId;
|
||||||
use crate::service::{CommandError, QueryState};
|
use crate::service::{CommandError, QueryState};
|
||||||
use crate::service::{Event, Events};
|
use crate::service::{Event, Events};
|
||||||
use crate::service::{NodeId, Sessions};
|
|
||||||
use crate::wire;
|
use crate::wire;
|
||||||
use crate::wire::StreamId;
|
use crate::wire::StreamId;
|
||||||
use crate::worker::TaskResult;
|
use crate::worker::TaskResult;
|
||||||
|
|
@ -121,7 +121,7 @@ impl Handle {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl radicle::node::Handle for Handle {
|
impl radicle::node::Handle for Handle {
|
||||||
type Sessions = Sessions;
|
type Sessions = Vec<radicle::node::Session>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn nid(&self) -> Result<NodeId, Self::Error> {
|
fn nid(&self) -> Result<NodeId, Self::Error> {
|
||||||
|
|
@ -211,7 +211,16 @@ impl radicle::node::Handle for Handle {
|
||||||
fn sessions(&self) -> Result<Self::Sessions, Error> {
|
fn sessions(&self) -> Result<Self::Sessions, Error> {
|
||||||
let (sender, receiver) = chan::unbounded();
|
let (sender, receiver) = chan::unbounded();
|
||||||
let query: Arc<QueryState> = Arc::new(move |state| {
|
let query: Arc<QueryState> = Arc::new(move |state| {
|
||||||
sender.send(state.sessions().clone()).ok();
|
let sessions = state
|
||||||
|
.sessions()
|
||||||
|
.iter()
|
||||||
|
.map(|(nid, s)| radicle::node::Session {
|
||||||
|
nid: *nid,
|
||||||
|
state: s.state.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
sender.send(sessions).ok();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
let (err_sender, err_receiver) = chan::bounded(1);
|
let (err_sender, err_receiver) = chan::bounded(1);
|
||||||
|
|
|
||||||
|
|
@ -710,7 +710,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connected(&mut self, remote: NodeId, link: Link) {
|
pub fn connected(&mut self, remote: NodeId, addr: Address, link: Link) {
|
||||||
info!(target: "service", "Connected to {} ({:?})", remote, link);
|
info!(target: "service", "Connected to {} ({:?})", remote, link);
|
||||||
self.emitter.emit(Event::PeerConnected { nid: remote });
|
self.emitter.emit(Event::PeerConnected { nid: remote });
|
||||||
|
|
||||||
|
|
@ -736,6 +736,7 @@ where
|
||||||
Entry::Vacant(e) => {
|
Entry::Vacant(e) => {
|
||||||
let peer = e.insert(Session::inbound(
|
let peer = e.insert(Session::inbound(
|
||||||
remote,
|
remote,
|
||||||
|
addr,
|
||||||
self.config.is_persistent(&remote),
|
self.config.is_persistent(&remote),
|
||||||
self.rng.clone(),
|
self.rng.clone(),
|
||||||
self.clock,
|
self.clock,
|
||||||
|
|
@ -1538,7 +1539,7 @@ where
|
||||||
pub trait ServiceState {
|
pub trait ServiceState {
|
||||||
/// Get the Node ID.
|
/// Get the Node ID.
|
||||||
fn nid(&self) -> &NodeId;
|
fn nid(&self) -> &NodeId;
|
||||||
/// Get the connected peers.
|
/// Get the existing sessions.
|
||||||
fn sessions(&self) -> &Sessions;
|
fn sessions(&self) -> &Sessions;
|
||||||
/// Get a repository from storage, using the local node's key.
|
/// Get a repository from storage, using the local node's key.
|
||||||
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError>;
|
fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, IdentityError>;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
use std::{fmt, mem};
|
use std::fmt;
|
||||||
|
|
||||||
use crate::service::config::Limits;
|
use crate::service::config::Limits;
|
||||||
use crate::service::message;
|
use crate::service::message;
|
||||||
|
|
@ -7,60 +7,7 @@ use crate::service::message::Message;
|
||||||
use crate::service::{Address, Id, LocalTime, NodeId, Outbox, Rng};
|
use crate::service::{Address, Id, LocalTime, NodeId, Outbox, Rng};
|
||||||
use crate::Link;
|
use crate::Link;
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
|
pub use crate::node::{PingState, State};
|
||||||
pub enum PingState {
|
|
||||||
#[default]
|
|
||||||
/// The peer has not been sent a ping.
|
|
||||||
None,
|
|
||||||
/// A ping has been sent and is waiting on the peer's response.
|
|
||||||
AwaitingResponse(u16),
|
|
||||||
/// The peer was successfully pinged.
|
|
||||||
Ok,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
pub enum State {
|
|
||||||
/// Initial state for outgoing connections.
|
|
||||||
Initial,
|
|
||||||
/// Connection attempted successfully.
|
|
||||||
Attempted { addr: Address },
|
|
||||||
/// Initial state after handshake protocol hand-off.
|
|
||||||
Connected {
|
|
||||||
/// Connected since this time.
|
|
||||||
since: LocalTime,
|
|
||||||
/// Ping state.
|
|
||||||
ping: PingState,
|
|
||||||
/// Ongoing fetches.
|
|
||||||
fetching: HashSet<Id>,
|
|
||||||
},
|
|
||||||
/// When a peer is disconnected.
|
|
||||||
Disconnected {
|
|
||||||
/// Since when has this peer been disconnected.
|
|
||||||
since: LocalTime,
|
|
||||||
/// When to retry the connection.
|
|
||||||
retry_at: LocalTime,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for State {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Initial => {
|
|
||||||
write!(f, "initial")
|
|
||||||
}
|
|
||||||
Self::Attempted { .. } => {
|
|
||||||
write!(f, "attempted")
|
|
||||||
}
|
|
||||||
Self::Connected { .. } => {
|
|
||||||
write!(f, "connected")
|
|
||||||
}
|
|
||||||
Self::Disconnected { .. } => {
|
|
||||||
write!(f, "disconnected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return value of [`Session::fetch`].
|
/// Return value of [`Session::fetch`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -171,6 +118,7 @@ impl Session {
|
||||||
|
|
||||||
pub fn inbound(
|
pub fn inbound(
|
||||||
id: NodeId,
|
id: NodeId,
|
||||||
|
addr: Address,
|
||||||
persistent: bool,
|
persistent: bool,
|
||||||
rng: Rng,
|
rng: Rng,
|
||||||
time: LocalTime,
|
time: LocalTime,
|
||||||
|
|
@ -179,6 +127,7 @@ impl Session {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
state: State::Connected {
|
state: State::Connected {
|
||||||
|
addr,
|
||||||
since: time,
|
since: time,
|
||||||
ping: PingState::default(),
|
ping: PingState::default(),
|
||||||
fetching: HashSet::default(),
|
fetching: HashSet::default(),
|
||||||
|
|
@ -256,19 +205,18 @@ impl Session {
|
||||||
pub fn to_connected(&mut self, since: LocalTime) -> Address {
|
pub fn to_connected(&mut self, since: LocalTime) -> Address {
|
||||||
self.attempts = 0;
|
self.attempts = 0;
|
||||||
|
|
||||||
let previous = mem::replace(
|
let addr = if let State::Attempted { addr } = &self.state {
|
||||||
&mut self.state,
|
addr.clone()
|
||||||
State::Connected {
|
|
||||||
since,
|
|
||||||
ping: PingState::default(),
|
|
||||||
fetching: HashSet::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if let State::Attempted { addr } = previous {
|
|
||||||
addr
|
|
||||||
} else {
|
} else {
|
||||||
panic!("Session::to_connected: can only transition to 'connected' state from 'connecting' state");
|
panic!("Session::to_connected: can only transition to 'connected' state from 'attempted' state");
|
||||||
}
|
};
|
||||||
|
self.state = State::Connected {
|
||||||
|
addr: addr.clone(),
|
||||||
|
since,
|
||||||
|
ping: PingState::default(),
|
||||||
|
fetching: HashSet::default(),
|
||||||
|
};
|
||||||
|
addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move the session state to "disconnected". Returns any pending RID
|
/// Move the session state to "disconnected". Returns any pending RID
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use std::{io, time};
|
||||||
use crate::identity::Id;
|
use crate::identity::Id;
|
||||||
use crate::node::{Event, FetchResult, Seeds};
|
use crate::node::{Event, FetchResult, Seeds};
|
||||||
use crate::runtime::HandleError;
|
use crate::runtime::HandleError;
|
||||||
|
use crate::service::tracking;
|
||||||
use crate::service::NodeId;
|
use crate::service::NodeId;
|
||||||
use crate::service::{self, tracking};
|
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
|
|
@ -18,7 +18,7 @@ pub struct Handle {
|
||||||
|
|
||||||
impl radicle::node::Handle for Handle {
|
impl radicle::node::Handle for Handle {
|
||||||
type Error = HandleError;
|
type Error = HandleError;
|
||||||
type Sessions = service::Sessions;
|
type Sessions = Vec<radicle::node::Session>;
|
||||||
|
|
||||||
fn nid(&self) -> Result<NodeId, Self::Error> {
|
fn nid(&self) -> Result<NodeId, Self::Error> {
|
||||||
Ok(NodeId::from_str("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap())
|
Ok(NodeId::from_str("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").unwrap())
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,8 @@ where
|
||||||
let remote_id = simulator::Peer::<S, G>::id(peer);
|
let remote_id = simulator::Peer::<S, G>::id(peer);
|
||||||
|
|
||||||
self.initialize();
|
self.initialize();
|
||||||
self.service.connected(remote_id, Link::Inbound);
|
self.service
|
||||||
|
.connected(remote_id, peer.address(), Link::Inbound);
|
||||||
|
|
||||||
let mut msgs = self.messages(remote_id);
|
let mut msgs = self.messages(remote_id);
|
||||||
msgs.find(|m| {
|
msgs.find(|m| {
|
||||||
|
|
@ -334,8 +335,9 @@ where
|
||||||
.find(|o| matches!(o, Io::Connect { .. }))
|
.find(|o| matches!(o, Io::Connect { .. }))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
self.service.attempted(remote_id, remote_addr);
|
self.service.attempted(remote_id, remote_addr.clone());
|
||||||
self.service.connected(remote_id, Link::Outbound);
|
self.service
|
||||||
|
.connected(remote_id, remote_addr, Link::Outbound);
|
||||||
|
|
||||||
let mut msgs = self.messages(remote_id);
|
let mut msgs = self.messages(remote_id);
|
||||||
msgs.find(|m| {
|
msgs.find(|m| {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use std::marker::PhantomData;
|
||||||
use std::ops::{Deref, DerefMut, Range};
|
use std::ops::{Deref, DerefMut, Range};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{fmt, io};
|
use std::{fmt, io, net};
|
||||||
|
|
||||||
use localtime::{LocalDuration, LocalTime};
|
use localtime::{LocalDuration, LocalTime};
|
||||||
use log::*;
|
use log::*;
|
||||||
|
|
@ -56,6 +56,8 @@ pub enum Input {
|
||||||
Connected {
|
Connected {
|
||||||
/// Remote peer id.
|
/// Remote peer id.
|
||||||
id: NodeId,
|
id: NodeId,
|
||||||
|
/// Remote peer address.
|
||||||
|
addr: Address,
|
||||||
/// Link direction.
|
/// Link direction.
|
||||||
link: Link,
|
link: Link,
|
||||||
},
|
},
|
||||||
|
|
@ -381,13 +383,13 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
p.attempted(id, addr);
|
p.attempted(id, addr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Input::Connected { id, link } => {
|
Input::Connected { id, addr, link } => {
|
||||||
let conn = (node, id);
|
let conn = (node, id);
|
||||||
|
|
||||||
let attempted = link.is_outbound() && self.attempts.remove(&conn);
|
let attempted = link.is_outbound() && self.attempts.remove(&conn);
|
||||||
if attempted || link.is_inbound() {
|
if attempted || link.is_inbound() {
|
||||||
if self.connections.insert(conn) {
|
if self.connections.insert(conn) {
|
||||||
p.connected(id, link);
|
p.connected(id, addr, link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -498,7 +500,10 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
Scheduled {
|
Scheduled {
|
||||||
node,
|
node,
|
||||||
remote,
|
remote,
|
||||||
input: Input::Connecting { id: remote, addr },
|
input: Input::Connecting {
|
||||||
|
id: remote,
|
||||||
|
addr: addr.clone(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -535,6 +540,7 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
remote: node,
|
remote: node,
|
||||||
input: Input::Connected {
|
input: Input::Connected {
|
||||||
id: node,
|
id: node,
|
||||||
|
addr: Address::from(net::SocketAddr::from(([0, 0, 0, 0], 0))),
|
||||||
link: Link::Inbound,
|
link: Link::Inbound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -547,6 +553,7 @@ impl<S: WriteStorage + 'static, G: Signer> Simulation<S, G> {
|
||||||
node,
|
node,
|
||||||
input: Input::Connected {
|
input: Input::Connected {
|
||||||
id: remote,
|
id: remote,
|
||||||
|
addr,
|
||||||
link: Link::Outbound,
|
link: Link::Outbound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -967,7 +967,7 @@ fn test_persistent_peer_reconnect_success() {
|
||||||
.expect("Alice attempts a re-connection");
|
.expect("Alice attempts a re-connection");
|
||||||
|
|
||||||
alice.attempted(bob.id(), bob.addr());
|
alice.attempted(bob.id(), bob.addr());
|
||||||
alice.connected(bob.id(), Link::Outbound);
|
alice.connected(bob.id(), bob.addr(), Link::Outbound);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -729,8 +729,18 @@ fn test_connection_crossing() {
|
||||||
|
|
||||||
thread::sleep(time::Duration::from_secs(1));
|
thread::sleep(time::Duration::from_secs(1));
|
||||||
|
|
||||||
let s1 = alice.handle.sessions().unwrap().contains_key(&bob.id);
|
let s1 = alice
|
||||||
let s2 = bob.handle.sessions().unwrap().contains_key(&alice.id);
|
.handle
|
||||||
|
.sessions()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.nid == bob.id);
|
||||||
|
let s2 = bob
|
||||||
|
.handle
|
||||||
|
.sessions()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.nid == alice.id);
|
||||||
|
|
||||||
assert!(s1 ^ s2, "Exactly one session should be established");
|
assert!(s1 ^ s2, "Exactly one session should be established");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,12 +141,17 @@ impl Streams {
|
||||||
/// Peer connection state machine.
|
/// Peer connection state machine.
|
||||||
enum Peer {
|
enum Peer {
|
||||||
/// The initial state of an inbound peer before handshake is completed.
|
/// The initial state of an inbound peer before handshake is completed.
|
||||||
Inbound {},
|
Inbound { addr: NetAddr<HostName> },
|
||||||
/// The initial state of an outbound peer before handshake is completed.
|
/// The initial state of an outbound peer before handshake is completed.
|
||||||
Outbound { id: NodeId },
|
Outbound {
|
||||||
|
addr: NetAddr<HostName>,
|
||||||
|
nid: NodeId,
|
||||||
|
},
|
||||||
/// The state after handshake is completed.
|
/// The state after handshake is completed.
|
||||||
/// Peers in this state are handled by the underlying service.
|
/// Peers in this state are handled by the underlying service.
|
||||||
Connected {
|
Connected {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
addr: NetAddr<HostName>,
|
||||||
link: Link,
|
link: Link,
|
||||||
nid: NodeId,
|
nid: NodeId,
|
||||||
inbox: Deserializer<Frame>,
|
inbox: Deserializer<Frame>,
|
||||||
|
|
@ -155,7 +160,7 @@ enum Peer {
|
||||||
/// The peer was scheduled for disconnection. Once the transport is handed over
|
/// The peer was scheduled for disconnection. Once the transport is handed over
|
||||||
/// by the reactor, we can consider it disconnected.
|
/// by the reactor, we can consider it disconnected.
|
||||||
Disconnecting {
|
Disconnecting {
|
||||||
id: Option<NodeId>,
|
nid: Option<NodeId>,
|
||||||
reason: DisconnectReason,
|
reason: DisconnectReason,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -163,8 +168,8 @@ enum Peer {
|
||||||
impl std::fmt::Debug for Peer {
|
impl std::fmt::Debug for Peer {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Inbound {} => write!(f, "Inbound"),
|
Self::Inbound { addr } => write!(f, "Inbound({addr})"),
|
||||||
Self::Outbound { id } => write!(f, "Outbound({id})"),
|
Self::Outbound { nid, .. } => write!(f, "Outbound({nid})"),
|
||||||
Self::Connected { link, nid, .. } => write!(f, "Connected({link:?}, {nid})"),
|
Self::Connected { link, nid, .. } => write!(f, "Connected({link:?}, {nid})"),
|
||||||
Self::Disconnecting { .. } => write!(f, "Disconnecting"),
|
Self::Disconnecting { .. } => write!(f, "Disconnecting"),
|
||||||
}
|
}
|
||||||
|
|
@ -175,49 +180,58 @@ impl Peer {
|
||||||
/// Return the peer's id, if any.
|
/// Return the peer's id, if any.
|
||||||
fn id(&self) -> Option<&NodeId> {
|
fn id(&self) -> Option<&NodeId> {
|
||||||
match self {
|
match self {
|
||||||
Peer::Outbound { id }
|
Peer::Outbound { nid, .. }
|
||||||
| Peer::Connected { nid: id, .. }
|
| Peer::Connected { nid, .. }
|
||||||
| Peer::Disconnecting { id: Some(id), .. } => Some(id),
|
| Peer::Disconnecting { nid: Some(nid), .. } => Some(nid),
|
||||||
Peer::Inbound {} => None,
|
Peer::Inbound { .. } => None,
|
||||||
Peer::Disconnecting { id: None, .. } => None,
|
Peer::Disconnecting { nid: None, .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a new inbound connecting peer.
|
/// Return a new inbound connecting peer.
|
||||||
fn inbound() -> Self {
|
fn inbound(addr: NetAddr<HostName>) -> Self {
|
||||||
Self::Inbound {}
|
Self::Inbound { addr }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a new inbound connecting peer.
|
/// Return a new outbound connecting peer.
|
||||||
fn outbound(id: NodeId) -> Self {
|
fn outbound(addr: NetAddr<HostName>, nid: NodeId) -> Self {
|
||||||
Self::Outbound { id }
|
Self::Outbound { addr, nid }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to connected state.
|
/// Switch to connected state.
|
||||||
fn connected(&mut self, id: NodeId) -> Link {
|
fn connected(&mut self, nid: NodeId) -> (NetAddr<HostName>, Link) {
|
||||||
if let Self::Inbound {} = self {
|
if let Self::Inbound { addr } = self {
|
||||||
let link = Link::Inbound;
|
let link = Link::Inbound;
|
||||||
|
let addr = addr.clone();
|
||||||
|
|
||||||
*self = Self::Connected {
|
*self = Self::Connected {
|
||||||
link,
|
link,
|
||||||
nid: id,
|
addr: addr.clone(),
|
||||||
|
nid,
|
||||||
inbox: Deserializer::default(),
|
inbox: Deserializer::default(),
|
||||||
streams: Streams::new(link),
|
streams: Streams::new(link),
|
||||||
};
|
};
|
||||||
link
|
(addr, link)
|
||||||
} else if let Self::Outbound { id: expected } = self {
|
} else if let Self::Outbound {
|
||||||
assert_eq!(id, *expected);
|
addr,
|
||||||
|
nid: expected,
|
||||||
|
} = self
|
||||||
|
{
|
||||||
|
assert_eq!(nid, *expected);
|
||||||
|
|
||||||
let link = Link::Outbound;
|
let link = Link::Outbound;
|
||||||
|
let addr = addr.clone();
|
||||||
|
|
||||||
*self = Self::Connected {
|
*self = Self::Connected {
|
||||||
link,
|
link,
|
||||||
nid: id,
|
addr: addr.clone(),
|
||||||
|
nid,
|
||||||
inbox: Deserializer::default(),
|
inbox: Deserializer::default(),
|
||||||
streams: Streams::new(link),
|
streams: Streams::new(link),
|
||||||
};
|
};
|
||||||
link
|
(addr, link)
|
||||||
} else {
|
} else {
|
||||||
panic!("Peer::connected: session for {id} is already established");
|
panic!("Peer::connected: session for {nid} is already established");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,14 +241,14 @@ impl Peer {
|
||||||
streams.shutdown();
|
streams.shutdown();
|
||||||
|
|
||||||
*self = Self::Disconnecting {
|
*self = Self::Disconnecting {
|
||||||
id: Some(*nid),
|
nid: Some(*nid),
|
||||||
reason,
|
reason,
|
||||||
};
|
};
|
||||||
} else if let Self::Inbound {} = self {
|
} else if let Self::Inbound { .. } = self {
|
||||||
*self = Self::Disconnecting { id: None, reason };
|
*self = Self::Disconnecting { nid: None, reason };
|
||||||
} else if let Self::Outbound { id } = self {
|
} else if let Self::Outbound { nid, .. } = self {
|
||||||
*self = Self::Disconnecting {
|
*self = Self::Disconnecting {
|
||||||
id: Some(*id),
|
nid: Some(*nid),
|
||||||
reason,
|
reason,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -280,9 +294,9 @@ impl Peers {
|
||||||
|
|
||||||
fn active(&self) -> impl Iterator<Item = (RawFd, &NodeId)> {
|
fn active(&self) -> impl Iterator<Item = (RawFd, &NodeId)> {
|
||||||
self.0.iter().filter_map(|(fd, peer)| match peer {
|
self.0.iter().filter_map(|(fd, peer)| match peer {
|
||||||
Peer::Inbound {} => None,
|
Peer::Inbound { .. } => None,
|
||||||
Peer::Outbound { id } => Some((*fd, id)),
|
Peer::Outbound { nid, .. } => Some((*fd, nid)),
|
||||||
Peer::Connected { nid: id, .. } => Some((*fd, id)),
|
Peer::Connected { nid, .. } => Some((*fd, nid)),
|
||||||
Peer::Disconnecting { .. } => None,
|
Peer::Disconnecting { .. } => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -463,7 +477,10 @@ where
|
||||||
"Accepting inbound peer connection from {}..",
|
"Accepting inbound peer connection from {}..",
|
||||||
connection.remote_addr()
|
connection.remote_addr()
|
||||||
);
|
);
|
||||||
self.peers.insert(connection.as_raw_fd(), Peer::inbound());
|
self.peers.insert(
|
||||||
|
connection.as_raw_fd(),
|
||||||
|
Peer::inbound(connection.remote_addr().into()),
|
||||||
|
);
|
||||||
|
|
||||||
let session = accept::<G>(connection, self.signer.clone());
|
let session = accept::<G>(connection, self.signer.clone());
|
||||||
let transport = match NetTransport::with_session(session, Link::Inbound) {
|
let transport = match NetTransport::with_session(session, Link::Inbound) {
|
||||||
|
|
@ -519,9 +536,9 @@ where
|
||||||
log::error!(target: "wire", "Session not found for fd {fd}");
|
log::error!(target: "wire", "Session not found for fd {fd}");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let link = peer.connected(id);
|
let (addr, link) = peer.connected(id);
|
||||||
|
|
||||||
self.service.connected(id, link);
|
self.service.connected(id, addr.into(), link);
|
||||||
}
|
}
|
||||||
SessionEvent::Data(data) => {
|
SessionEvent::Data(data) => {
|
||||||
if let Some(Peer::Connected {
|
if let Some(Peer::Connected {
|
||||||
|
|
@ -691,7 +708,9 @@ where
|
||||||
match self.peers.entry(fd) {
|
match self.peers.entry(fd) {
|
||||||
Entry::Occupied(e) => {
|
Entry::Occupied(e) => {
|
||||||
match e.get() {
|
match e.get() {
|
||||||
Peer::Disconnecting { id, reason, .. } => {
|
Peer::Disconnecting {
|
||||||
|
nid: id, reason, ..
|
||||||
|
} => {
|
||||||
// Disconnect TCP stream.
|
// Disconnect TCP stream.
|
||||||
drop(transport);
|
drop(transport);
|
||||||
|
|
||||||
|
|
@ -771,11 +790,13 @@ where
|
||||||
NetTransport::<WireSession<G>>::with_session(session, Link::Outbound)
|
NetTransport::<WireSession<G>>::with_session(session, Link::Outbound)
|
||||||
}) {
|
}) {
|
||||||
Ok(transport) => {
|
Ok(transport) => {
|
||||||
self.service.attempted(node_id, addr);
|
self.service.attempted(node_id, addr.clone());
|
||||||
// TODO: Keep track of peer address for when peer disconnects before
|
// TODO: Keep track of peer address for when peer disconnects before
|
||||||
// handshake is complete.
|
// handshake is complete.
|
||||||
self.peers
|
self.peers.insert(
|
||||||
.insert(transport.as_raw_fd(), Peer::outbound(node_id));
|
transport.as_raw_fd(),
|
||||||
|
Peer::outbound(addr.to_inner(), node_id),
|
||||||
|
);
|
||||||
|
|
||||||
self.actions
|
self.actions
|
||||||
.push_back(reactor::Action::RegisterTransport(transport));
|
.push_back(reactor::Action::RegisterTransport(transport));
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ crossbeam-channel = { version = "0.5.6" }
|
||||||
cyphernet = { version = "0.2.0", features = ["tor", "dns", "ed25519"] }
|
cyphernet = { version = "0.2.0", features = ["tor", "dns", "ed25519"] }
|
||||||
fastrand = { version = "1.9.0" }
|
fastrand = { version = "1.9.0" }
|
||||||
multibase = { version = "0.9.1" }
|
multibase = { version = "0.9.1" }
|
||||||
localtime = { version = "1.2.0" }
|
localtime = { version = "1.2.0", features = ["serde"] }
|
||||||
log = { version = "0.4.17", features = ["std"] }
|
log = { version = "0.4.17", features = ["std"] }
|
||||||
nonempty = { version = "0.8.1", features = ["serialize"] }
|
nonempty = { version = "0.8.1", features = ["serialize"] }
|
||||||
once_cell = { version = "1.13" }
|
once_cell = { version = "1.13" }
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use std::{fmt, io, net, thread, time};
|
||||||
|
|
||||||
use amplify::WrapperMut;
|
use amplify::WrapperMut;
|
||||||
use cyphernet::addr::{HostName, NetAddr};
|
use cyphernet::addr::{HostName, NetAddr};
|
||||||
|
use localtime::LocalTime;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json as json;
|
use serde_json as json;
|
||||||
|
|
@ -48,6 +49,64 @@ pub const NODE_ANNOUNCEMENT_FILE: &str = "announcement.wire";
|
||||||
/// Milliseconds since epoch.
|
/// Milliseconds since epoch.
|
||||||
pub type Timestamp = u64;
|
pub type Timestamp = u64;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub enum PingState {
|
||||||
|
#[default]
|
||||||
|
/// The peer has not been sent a ping.
|
||||||
|
None,
|
||||||
|
/// A ping has been sent and is waiting on the peer's response.
|
||||||
|
AwaitingResponse(u16),
|
||||||
|
/// The peer was successfully pinged.
|
||||||
|
Ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum State {
|
||||||
|
/// Initial state for outgoing connections.
|
||||||
|
Initial,
|
||||||
|
/// Connection attempted successfully.
|
||||||
|
Attempted { addr: Address },
|
||||||
|
/// Initial state after handshake protocol hand-off.
|
||||||
|
Connected {
|
||||||
|
/// Remote address.
|
||||||
|
addr: Address,
|
||||||
|
/// Connected since this time.
|
||||||
|
since: LocalTime,
|
||||||
|
/// Ping state.
|
||||||
|
#[serde(skip)]
|
||||||
|
ping: PingState,
|
||||||
|
/// Ongoing fetches.
|
||||||
|
fetching: HashSet<Id>,
|
||||||
|
},
|
||||||
|
/// When a peer is disconnected.
|
||||||
|
Disconnected {
|
||||||
|
/// Since when has this peer been disconnected.
|
||||||
|
since: LocalTime,
|
||||||
|
/// When to retry the connection.
|
||||||
|
retry_at: LocalTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for State {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Initial => {
|
||||||
|
write!(f, "initial")
|
||||||
|
}
|
||||||
|
Self::Attempted { .. } => {
|
||||||
|
write!(f, "attempted")
|
||||||
|
}
|
||||||
|
Self::Connected { .. } => {
|
||||||
|
write!(f, "connected")
|
||||||
|
}
|
||||||
|
Self::Disconnected { .. } => {
|
||||||
|
write!(f, "disconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of a command, on the node control socket.
|
/// Result of a command, on the node control socket.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "status")]
|
#[serde(tag = "status")]
|
||||||
|
|
@ -101,10 +160,10 @@ impl From<CommandResult> for Result<bool, Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peer public protocol address.
|
/// Peer public protocol address.
|
||||||
#[derive(Wrapper, WrapperMut, Clone, Eq, PartialEq, Debug, From)]
|
#[derive(Wrapper, WrapperMut, Clone, Eq, PartialEq, Debug, From, Serialize, Deserialize)]
|
||||||
#[wrapper(Deref, Display, FromStr)]
|
#[wrapper(Deref, Display, FromStr)]
|
||||||
#[wrapper_mut(DerefMut)]
|
#[wrapper_mut(DerefMut)]
|
||||||
pub struct Address(NetAddr<HostName>);
|
pub struct Address(#[serde(with = "crate::serde_ext::string")] NetAddr<HostName>);
|
||||||
|
|
||||||
impl cyphernet::addr::Host for Address {
|
impl cyphernet::addr::Host for Address {
|
||||||
fn requires_proxy(&self) -> bool {
|
fn requires_proxy(&self) -> bool {
|
||||||
|
|
@ -141,6 +200,8 @@ pub enum CommandName {
|
||||||
Connect,
|
Connect,
|
||||||
/// Lookup seeds for the given repository in the routing table.
|
/// Lookup seeds for the given repository in the routing table.
|
||||||
Seeds,
|
Seeds,
|
||||||
|
/// Get the current peer sessions.
|
||||||
|
Sessions,
|
||||||
/// Fetch the given repository from the network.
|
/// Fetch the given repository from the network.
|
||||||
Fetch,
|
Fetch,
|
||||||
/// Track the given repository.
|
/// Track the given repository.
|
||||||
|
|
@ -207,6 +268,13 @@ impl Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An established network connection with a peer.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Session {
|
||||||
|
pub nid: NodeId,
|
||||||
|
pub state: State,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[serde(tag = "state", content = "id")]
|
#[serde(tag = "state", content = "id")]
|
||||||
|
|
@ -541,7 +609,7 @@ impl Node {
|
||||||
// TODO(finto): repo_policies, node_policies, and routing should all
|
// TODO(finto): repo_policies, node_policies, and routing should all
|
||||||
// attempt to return iterators instead of allocating vecs.
|
// attempt to return iterators instead of allocating vecs.
|
||||||
impl Handle for Node {
|
impl Handle for Node {
|
||||||
type Sessions = ();
|
type Sessions = Vec<Session>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn nid(&self) -> Result<NodeId, Error> {
|
fn nid(&self) -> Result<NodeId, Error> {
|
||||||
|
|
@ -694,7 +762,14 @@ impl Handle for Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sessions(&self) -> Result<Self::Sessions, Error> {
|
fn sessions(&self) -> Result<Self::Sessions, Error> {
|
||||||
todo!();
|
let sessions = self
|
||||||
|
.call::<&str, Vec<Session>>(CommandName::Sessions, [], DEFAULT_TIMEOUT)?
|
||||||
|
.next()
|
||||||
|
.ok_or(Error::EmptyResponse {
|
||||||
|
cmd: CommandName::Sessions,
|
||||||
|
})??;
|
||||||
|
|
||||||
|
Ok(sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shutdown(self) -> Result<(), Error> {
|
fn shutdown(self) -> Result<(), Error> {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,31 @@ pub mod string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unlike the default `serde` instance for `LocalTime`, this encodes and decodes using seconds
|
||||||
|
/// instead of milliseconds.
|
||||||
|
pub mod localtime {
|
||||||
|
use localtime::LocalTime;
|
||||||
|
use serde::{de, Deserialize, Deserializer, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize<S>(value: &LocalTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.collect_str(&value.as_secs())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<LocalTime, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let seconds: u64 = String::deserialize(deserializer)?
|
||||||
|
.parse()
|
||||||
|
.map_err(de::Error::custom)?;
|
||||||
|
|
||||||
|
Ok(LocalTime::from_secs(seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return true if the given value is the default for that type.
|
/// Return true if the given value is the default for that type.
|
||||||
pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
|
pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
|
||||||
t == &T::default()
|
t == &T::default()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue