Revamp inner client loop and connect folder list to UI
This commit is contained in:
parent
94ead04391
commit
4c989e0991
9 changed files with 261 additions and 403 deletions
6
Justfile
6
Justfile
|
@ -6,3 +6,9 @@ doc-open:
|
|||
|
||||
watch:
|
||||
cargo watch -x 'clippy --all --all-features'
|
||||
|
||||
run:
|
||||
cargo run -- --log-file output.log
|
||||
|
||||
tail:
|
||||
tail -f output.log
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Result;
|
||||
use futures::stream::StreamExt;
|
||||
|
||||
use crate::command::Command;
|
||||
use crate::response::{Response, ResponseDone, Status};
|
||||
|
@ -11,6 +12,8 @@ pub trait Auth {
|
|||
// TODO: return the unauthed client if failed?
|
||||
async fn perform_auth(self, client: ClientUnauthenticated) -> Result<ClientAuthenticated>;
|
||||
|
||||
/// Converts the wrappers around the client once the authentication has happened. Should only
|
||||
/// be called by the `perform_auth` function.
|
||||
fn convert_client(client: ClientUnauthenticated) -> ClientAuthenticated {
|
||||
match client {
|
||||
ClientUnauthenticated::Encrypted(e) => ClientAuthenticated::Encrypted(e),
|
||||
|
@ -32,19 +35,26 @@ impl Auth for Plain {
|
|||
password: self.password,
|
||||
};
|
||||
|
||||
let (result, _) = client.execute(command).await?;
|
||||
let result = result.await?;
|
||||
let result = client.execute(command).await?;
|
||||
let done = result.done().await?;
|
||||
|
||||
if !matches!(
|
||||
result,
|
||||
Response::Done(ResponseDone {
|
||||
status: Status::Ok,
|
||||
..
|
||||
})
|
||||
) {
|
||||
bail!("unable to login: {:?}", result);
|
||||
assert!(done.is_some());
|
||||
let done = done.unwrap();
|
||||
|
||||
if done.status != Status::Ok {
|
||||
bail!("unable to login: {:?}", done);
|
||||
}
|
||||
|
||||
// if !matches!(
|
||||
// result,
|
||||
// Response::Done(ResponseDone {
|
||||
// status: Status::Ok,
|
||||
// ..
|
||||
// })
|
||||
// ) {
|
||||
// bail!("unable to login: {:?}", result);
|
||||
// }
|
||||
|
||||
Ok(<Self as Auth>::convert_client(client))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
use std::collections::{HashSet, VecDeque};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use anyhow::{Context as AnyhowContext, Error, Result};
|
||||
use anyhow::Result;
|
||||
use futures::{
|
||||
future::{self, Either, Future, FutureExt, TryFutureExt},
|
||||
stream::StreamExt,
|
||||
future::{self, Either, FutureExt, TryFutureExt},
|
||||
stream::{Peekable, Stream, StreamExt},
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use parking_lot::Mutex;
|
||||
use tokio::{
|
||||
io::{
|
||||
self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf,
|
||||
},
|
||||
sync::{mpsc, oneshot},
|
||||
sync::{
|
||||
mpsc,
|
||||
oneshot::{self, error::TryRecvError},
|
||||
},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use tokio_rustls::{
|
||||
|
@ -23,169 +25,89 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
|||
|
||||
use crate::command::Command;
|
||||
use crate::parser::{parse_capability, parse_response};
|
||||
use crate::response::{Capability, Response, ResponseCode, ResponseData, ResponseDone, Status};
|
||||
use crate::response::{Response, ResponseDone};
|
||||
|
||||
use super::ClientConfig;
|
||||
|
||||
pub type CapsLock = Arc<RwLock<Option<HashSet<Capability>>>>;
|
||||
pub type ResponseFuture = Box<dyn Future<Output = Result<Response>> + Send + Unpin>;
|
||||
pub type ResponseSender = mpsc::UnboundedSender<Response>;
|
||||
pub type ResponseStream = mpsc::UnboundedReceiver<Response>;
|
||||
type ResultQueue = Arc<RwLock<VecDeque<HandlerResult>>>;
|
||||
pub type GreetingState = Arc<RwLock<(Option<Response>, Option<Waker>)>>;
|
||||
pub const TAG_PREFIX: &str = "ptag";
|
||||
type Command2 = (Command, mpsc::UnboundedSender<Response>);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct HandlerResult {
|
||||
id: usize,
|
||||
end: Option<oneshot::Sender<Response>>,
|
||||
sender: ResponseSender,
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
/// The lower-level Client struct, that is shared by all of the exported structs in the state machine.
|
||||
pub struct Client<C> {
|
||||
ctr: usize,
|
||||
config: ClientConfig,
|
||||
|
||||
/// write half of the connection
|
||||
conn: WriteHalf<C>,
|
||||
|
||||
/// counter for monotonically incrementing unique ids
|
||||
id: usize,
|
||||
|
||||
results: ResultQueue,
|
||||
|
||||
/// cached set of capabilities
|
||||
caps: CapsLock,
|
||||
|
||||
/// join handle for the listener thread
|
||||
cmd_tx: mpsc::UnboundedSender<Command2>,
|
||||
greeting_rx: Option<oneshot::Receiver<()>>,
|
||||
exit_tx: oneshot::Sender<()>,
|
||||
listener_handle: JoinHandle<Result<ReadHalf<C>>>,
|
||||
|
||||
/// used for telling the listener thread to stop and return the read half
|
||||
exit_tx: mpsc::Sender<()>,
|
||||
|
||||
/// used for receiving the greeting
|
||||
greeting: GreetingState,
|
||||
}
|
||||
|
||||
impl<C> Client<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
/// Creates a new client that wraps a connection
|
||||
pub fn new(conn: C, config: ClientConfig) -> Self {
|
||||
let (read_half, write_half) = io::split(conn);
|
||||
let results = Arc::new(RwLock::new(VecDeque::new()));
|
||||
let (exit_tx, exit_rx) = mpsc::channel(1);
|
||||
let greeting = Arc::new(RwLock::new((None, None)));
|
||||
let caps: CapsLock = Arc::new(RwLock::new(None));
|
||||
|
||||
let listener_handle = tokio::spawn(
|
||||
listen(
|
||||
read_half,
|
||||
caps.clone(),
|
||||
results.clone(),
|
||||
exit_rx,
|
||||
greeting.clone(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||
let (greeting_tx, greeting_rx) = oneshot::channel();
|
||||
let (exit_tx, exit_rx) = oneshot::channel();
|
||||
let handle = tokio::spawn(
|
||||
listen(read_half, cmd_rx, greeting_tx, exit_rx).map_err(|err| {
|
||||
error!("Help, the listener loop died: {}", err);
|
||||
err
|
||||
}),
|
||||
);
|
||||
|
||||
Client {
|
||||
config,
|
||||
ctr: 0,
|
||||
conn: write_half,
|
||||
id: 0,
|
||||
results,
|
||||
listener_handle,
|
||||
caps,
|
||||
config,
|
||||
cmd_tx,
|
||||
greeting_rx: Some(greeting_rx),
|
||||
exit_tx,
|
||||
greeting,
|
||||
listener_handle: handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a future that doesn't resolve until we receive a greeting from the server.
|
||||
pub fn wait_for_greeting(&self) -> GreetingWaiter {
|
||||
debug!("waiting for greeting");
|
||||
GreetingWaiter(self.greeting.clone())
|
||||
pub async fn wait_for_greeting(&mut self) -> Result<()> {
|
||||
if let Some(greeting_rx) = self.greeting_rx.take() {
|
||||
greeting_rx.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends a command to the server and returns a handle to retrieve the result
|
||||
pub async fn execute(&mut self, cmd: Command) -> Result<(ResponseFuture, ResponseStream)> {
|
||||
// debug!("executing command {:?}", cmd);
|
||||
let id = self.id;
|
||||
self.id += 1;
|
||||
pub async fn execute(&mut self, cmd: Command) -> Result<ResponseStream> {
|
||||
let id = self.ctr;
|
||||
self.ctr += 1;
|
||||
|
||||
// create a channel for sending the final response
|
||||
let (end_tx, end_rx) = oneshot::channel();
|
||||
|
||||
// create a channel for sending responses for this particular client call
|
||||
// this should queue up responses
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
debug!("EX[{}]: adding handler result to the handlers queue", id);
|
||||
{
|
||||
let mut handlers = self.results.write();
|
||||
handlers.push_back(HandlerResult {
|
||||
id,
|
||||
end: Some(end_tx),
|
||||
sender: tx,
|
||||
waker: None,
|
||||
});
|
||||
}
|
||||
|
||||
debug!("EX[{}]: send the command to the server", id);
|
||||
let cmd_str = format!("{}{} {}\r\n", TAG_PREFIX, id, cmd);
|
||||
self.conn.write_all(cmd_str.as_bytes()).await?;
|
||||
self.conn.flush().await?;
|
||||
|
||||
debug!("EX[{}]: hellosu", id);
|
||||
let q = self.results.clone();
|
||||
// let end = Box::new(end_rx.map_err(|err| Error::from).map(move |resp| resp));
|
||||
let end = Box::new(end_rx.map_err(Error::from).map(move |resp| {
|
||||
debug!("EX[{}]: -end result- {:?}", id, resp);
|
||||
// pop the first entry from the list
|
||||
let mut results = q.write();
|
||||
results.pop_front();
|
||||
resp
|
||||
}));
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
self.cmd_tx.send((cmd, tx))?;
|
||||
|
||||
Ok((end, rx))
|
||||
Ok(ResponseStream { inner: rx })
|
||||
}
|
||||
|
||||
/// Executes the CAPABILITY command
|
||||
pub async fn capabilities(&mut self, force: bool) -> Result<()> {
|
||||
{
|
||||
let caps = self.caps.read();
|
||||
if caps.is_some() && !force {
|
||||
return Ok(());
|
||||
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
||||
// TODO: cache capabilities if needed?
|
||||
let cap = cap.as_ref();
|
||||
let cap = parse_capability(cap)?;
|
||||
|
||||
let resp = self.execute(Command::Capability).await?;
|
||||
let (_, data) = resp.wait().await?;
|
||||
|
||||
for resp in data {
|
||||
if let Response::Capabilities(caps) = resp {
|
||||
return Ok(caps.contains(&cap));
|
||||
}
|
||||
// debug!("cap: {:?}", resp);
|
||||
}
|
||||
|
||||
let cmd = Command::Capability;
|
||||
// debug!("sending: {:?} {:?}", cmd, cmd.to_string());
|
||||
let (result, intermediate) = self
|
||||
.execute(cmd)
|
||||
.await
|
||||
.context("error executing CAPABILITY command")?;
|
||||
let _ = result.await?;
|
||||
|
||||
if let Some(Response::Capabilities(new_caps)) = UnboundedReceiverStream::new(intermediate)
|
||||
.filter(|resp| future::ready(matches!(resp, Response::Capabilities(_))))
|
||||
.next()
|
||||
.await
|
||||
{
|
||||
debug!("FOUND NEW CAPABILITIES: {:?}", new_caps);
|
||||
let mut caps = self.caps.write();
|
||||
*caps = Some(new_caps.iter().cloned().collect());
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts to upgrade this connection using STARTTLS
|
||||
pub async fn upgrade(mut self) -> Result<Client<TlsStream<C>>> {
|
||||
// TODO: make sure STARTTLS is in the capability list
|
||||
if !self.has_capability("STARTTLS").await? {
|
||||
|
@ -193,17 +115,17 @@ where
|
|||
}
|
||||
|
||||
// first, send the STARTTLS command
|
||||
let (resp, _) = self.execute(Command::Starttls).await?;
|
||||
let resp = resp.await?;
|
||||
let mut resp = self.execute(Command::Starttls).await?;
|
||||
let resp = resp.next().await.unwrap();
|
||||
debug!("server response to starttls: {:?}", resp);
|
||||
|
||||
debug!("sending exit for upgrade");
|
||||
self.exit_tx.send(()).await?;
|
||||
// TODO: check that the channel is still open?
|
||||
self.exit_tx.send(()).unwrap();
|
||||
let reader = self.listener_handle.await??;
|
||||
let writer = self.conn;
|
||||
|
||||
let conn = reader.unsplit(writer);
|
||||
|
||||
let server_name = &self.config.hostname;
|
||||
|
||||
let mut tls_config = RustlsConfig::new();
|
||||
|
@ -217,148 +139,107 @@ where
|
|||
|
||||
Ok(Client::new(stream, self.config))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this client has a particular capability
|
||||
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
||||
let cap = cap.as_ref().to_owned();
|
||||
debug!("checking for the capability: {:?}", cap);
|
||||
let cap = parse_capability(cap)?;
|
||||
pub struct ResponseStream {
|
||||
inner: mpsc::UnboundedReceiver<Response>,
|
||||
}
|
||||
|
||||
self.capabilities(false).await?;
|
||||
let caps = self.caps.read();
|
||||
// TODO: refresh caps
|
||||
impl ResponseStream {
|
||||
/// Retrieves just the DONE item in the stream, discarding the rest
|
||||
pub async fn done(mut self) -> Result<Option<ResponseDone>> {
|
||||
while let Some(resp) = self.inner.recv().await {
|
||||
if let Response::Done(done) = resp {
|
||||
return Ok(Some(done));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
let caps = caps.as_ref().unwrap();
|
||||
let result = caps.contains(&cap);
|
||||
debug!("cap result: {:?}", result);
|
||||
Ok(result)
|
||||
/// Waits for the entire stream to finish, returning the DONE status and the stream
|
||||
pub async fn wait(mut self) -> Result<(Option<ResponseDone>, Vec<Response>)> {
|
||||
let mut done = None;
|
||||
let mut vec = Vec::new();
|
||||
while let Some(resp) = self.inner.recv().await {
|
||||
if let Response::Done(d) = resp {
|
||||
done = Some(d);
|
||||
break;
|
||||
} else {
|
||||
vec.push(resp);
|
||||
}
|
||||
}
|
||||
Ok((done, vec))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GreetingWaiter(GreetingState);
|
||||
|
||||
impl Future for GreetingWaiter {
|
||||
type Output = Response;
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let (state, waker) = &mut *self.0.write();
|
||||
debug!("g {:?}", state);
|
||||
if waker.is_none() {
|
||||
*waker = Some(cx.waker().clone());
|
||||
}
|
||||
|
||||
match state.take() {
|
||||
Some(v) => Poll::Ready(v),
|
||||
None => Poll::Pending,
|
||||
}
|
||||
impl Stream for ResponseStream {
|
||||
type Item = Response;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
self.inner.poll_recv(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Main listen loop for the application
|
||||
#[allow(unreachable_code)]
|
||||
async fn listen<C>(
|
||||
conn: C,
|
||||
caps: CapsLock,
|
||||
results: ResultQueue,
|
||||
mut exit: mpsc::Receiver<()>,
|
||||
greeting: GreetingState,
|
||||
) -> Result<C>
|
||||
conn: ReadHalf<C>,
|
||||
mut cmd_rx: mpsc::UnboundedReceiver<Command2>,
|
||||
greeting_tx: oneshot::Sender<()>,
|
||||
mut exit_rx: oneshot::Receiver<()>,
|
||||
) -> Result<ReadHalf<C>>
|
||||
where
|
||||
C: AsyncRead + Unpin,
|
||||
{
|
||||
// debug!("amogus");
|
||||
let mut reader = BufReader::new(conn);
|
||||
let mut greeting = Some(greeting);
|
||||
let mut greeting_tx = Some(greeting_tx);
|
||||
let mut curr_cmd: Option<Command2> = None;
|
||||
let mut exit_rx = exit_rx.map_err(|_| ()).shared();
|
||||
// let mut exit_fut = Some(exit_rx.fuse());
|
||||
// let mut fut1 = None;
|
||||
|
||||
loop {
|
||||
let mut next_line = String::new();
|
||||
let fut = reader.read_line(&mut next_line).fuse();
|
||||
pin_mut!(fut);
|
||||
let fut2 = exit.recv().fuse();
|
||||
pin_mut!(fut2);
|
||||
let read_fut = reader.read_line(&mut next_line).fuse();
|
||||
pin_mut!(read_fut);
|
||||
|
||||
match future::select(fut, fut2).await {
|
||||
Either::Left((res, _)) => {
|
||||
let bytes = res.context("read failed")?;
|
||||
if bytes == 0 {
|
||||
bail!("connection probably died");
|
||||
}
|
||||
// only listen for a new command if there isn't one already
|
||||
let mut cmd_fut = if let Some(_) = curr_cmd {
|
||||
// if there is one, just make a future that never resolves so it'll always pick the
|
||||
// other options in the select.
|
||||
future::pending().boxed().fuse()
|
||||
} else {
|
||||
cmd_rx.recv().boxed().fuse()
|
||||
};
|
||||
|
||||
debug!("[LISTEN] got a new line {:?}", next_line);
|
||||
let resp = parse_response(next_line)?;
|
||||
debug!("[LISTEN] parsed as {:?}", resp);
|
||||
|
||||
// if this is the very first message, treat it as a greeting
|
||||
if let Some(greeting) = greeting.take() {
|
||||
let (greeting, waker) = &mut *greeting.write();
|
||||
debug!("[LISTEN] received greeting!");
|
||||
*greeting = Some(resp.clone());
|
||||
if let Some(waker) = waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
|
||||
// update capabilities list
|
||||
// TODO: probably not really necessary here (done somewhere else)?
|
||||
if let Response::Capabilities(new_caps)
|
||||
| Response::Data(ResponseData {
|
||||
status: Status::Ok,
|
||||
code: Some(ResponseCode::Capabilities(new_caps)),
|
||||
..
|
||||
}) = &resp
|
||||
{
|
||||
let caps = &mut *caps.write();
|
||||
*caps = Some(new_caps.iter().cloned().collect());
|
||||
debug!("new caps: {:?}", caps);
|
||||
}
|
||||
|
||||
match &resp {
|
||||
// bye
|
||||
Response::Data(ResponseData {
|
||||
status: Status::Bye,
|
||||
..
|
||||
}) => {
|
||||
bail!("disconnected");
|
||||
}
|
||||
|
||||
Response::Done(ResponseDone { tag, .. }) => {
|
||||
if tag.starts_with(TAG_PREFIX) {
|
||||
// let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
|
||||
debug!("[LISTEN] Done: {:?}", tag);
|
||||
let mut results = results.write();
|
||||
if let Some(HandlerResult { end, waker, .. }) =
|
||||
results.iter_mut().next()
|
||||
{
|
||||
if let Some(end) = end.take() {
|
||||
end.send(resp).unwrap();
|
||||
}
|
||||
// *opt = Some(resp);
|
||||
if let Some(waker) = waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
debug!("[LISTEN] RESPONSE: {:?}", resp);
|
||||
let mut results = results.write();
|
||||
if let Some(HandlerResult { id, sender, .. }) = results.iter_mut().next() {
|
||||
// we don't really care if it fails to send
|
||||
// this just means that the other side has dropped the channel
|
||||
//
|
||||
// which is fine since that just means they don't care about
|
||||
// intermediate messages
|
||||
let _ = sender.send(resp);
|
||||
debug!("[LISTEN] pushed to intermediate for id {}", id);
|
||||
debug!("[LISTEN] res: {:?}", results);
|
||||
}
|
||||
} // _ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Either::Right((_, _)) => {
|
||||
debug!("exiting read loop");
|
||||
select! {
|
||||
_ = exit_rx => {
|
||||
debug!("exiting the loop");
|
||||
break;
|
||||
}
|
||||
|
||||
cmd = cmd_fut => {
|
||||
if curr_cmd.is_none() {
|
||||
curr_cmd = cmd;
|
||||
}
|
||||
}
|
||||
|
||||
len = read_fut => {
|
||||
// res should not be None here
|
||||
let resp = parse_response(next_line)?;
|
||||
|
||||
// if this is the very first response, then it's a greeting
|
||||
if let Some(greeting_tx) = greeting_tx.take() {
|
||||
greeting_tx.send(());
|
||||
}
|
||||
|
||||
if let Response::Done(_) = resp {
|
||||
// since this is the DONE message, clear curr_cmd so another one can be sent
|
||||
if let Some((_, cmd_tx)) = curr_cmd.take() {
|
||||
cmd_tx.send(resp)?;
|
||||
}
|
||||
} else if let Some((ref cmd, ref mut cmd_tx)) = curr_cmd {
|
||||
cmd_tx.send(resp)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ use tokio_rustls::{
|
|||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use crate::command::Command;
|
||||
use crate::response::{Response, ResponseData, ResponseDone};
|
||||
use crate::response::{MailboxData, Response, ResponseData, ResponseDone};
|
||||
|
||||
pub use self::inner::{Client, ResponseFuture, ResponseStream};
|
||||
pub use self::inner::{Client, ResponseStream};
|
||||
|
||||
/// Struct used to start building the config for a client.
|
||||
///
|
||||
|
@ -93,12 +93,12 @@ impl ClientConfig {
|
|||
let dnsname = DNSNameRef::try_from_ascii_str(hostname).unwrap();
|
||||
let conn = tls_config.connect(dnsname, conn).await?;
|
||||
|
||||
let inner = Client::new(conn, self);
|
||||
inner.wait_for_greeting().await;
|
||||
let mut inner = Client::new(conn, self);
|
||||
inner.wait_for_greeting().await?;
|
||||
return Ok(ClientUnauthenticated::Encrypted(inner));
|
||||
} else {
|
||||
let inner = Client::new(conn, self);
|
||||
inner.wait_for_greeting().await;
|
||||
let mut inner = Client::new(conn, self);
|
||||
inner.wait_for_greeting().await?;
|
||||
return Ok(ClientUnauthenticated::Unencrypted(inner));
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ impl ClientUnauthenticated {
|
|||
}
|
||||
|
||||
/// Exposing low-level execute
|
||||
async fn execute(&mut self, cmd: Command) -> Result<(ResponseFuture, ResponseStream)> {
|
||||
async fn execute(&mut self, cmd: Command) -> Result<ResponseStream> {
|
||||
match self {
|
||||
ClientUnauthenticated::Encrypted(e) => e.execute(cmd).await,
|
||||
ClientUnauthenticated::Unencrypted(e) => e.execute(cmd).await,
|
||||
|
@ -137,12 +137,6 @@ impl ClientUnauthenticated {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ResponseCombined {
|
||||
pub data: Vec<Response>,
|
||||
pub done: ResponseDone,
|
||||
}
|
||||
|
||||
pub enum ClientAuthenticated {
|
||||
Encrypted(Client<TlsStream<TcpStream>>),
|
||||
Unencrypted(Client<TcpStream>),
|
||||
|
@ -150,45 +144,13 @@ pub enum ClientAuthenticated {
|
|||
|
||||
impl ClientAuthenticated {
|
||||
/// Exposing low-level execute
|
||||
async fn execute(&mut self, cmd: Command) -> Result<(ResponseFuture, ResponseStream)> {
|
||||
async fn execute(&mut self, cmd: Command) -> Result<ResponseStream> {
|
||||
match self {
|
||||
ClientAuthenticated::Encrypted(e) => e.execute(cmd).await,
|
||||
ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around `execute` that waits for the response and returns a combined data
|
||||
/// structure containing the intermediate results as well as the final status
|
||||
async fn execute_combined(&mut self, cmd: Command) -> Result<ResponseCombined> {
|
||||
let (resp, mut stream) = self.execute(cmd).await?;
|
||||
let mut resp = resp.into_stream(); // turn into stream to avoid mess with returning futures from select
|
||||
|
||||
let mut data = Vec::new();
|
||||
debug!("[COMBI] loop");
|
||||
let done = loop {
|
||||
let fut1 = resp.next().fuse();
|
||||
let fut2 = stream.recv().fuse();
|
||||
pin_mut!(fut1);
|
||||
pin_mut!(fut2);
|
||||
|
||||
match future::select(fut1, fut2).await {
|
||||
Either::Left((Some(Ok(Response::Done(done))), _)) => {
|
||||
debug!("[COMBI] left: {:?}", done);
|
||||
break done;
|
||||
}
|
||||
Either::Left(_) => unreachable!("got non-Response::Done from listen!"),
|
||||
|
||||
Either::Right((Some(resp), _)) => {
|
||||
debug!("[COMBI] right: {:?}", resp);
|
||||
data.push(resp);
|
||||
}
|
||||
Either::Right(_) => unreachable!(),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ResponseCombined { data, done })
|
||||
}
|
||||
|
||||
/// Runs the LIST command
|
||||
pub async fn list(&mut self) -> Result<Vec<String>> {
|
||||
let cmd = Command::List {
|
||||
|
@ -196,29 +158,17 @@ impl ClientAuthenticated {
|
|||
mailbox: "*".to_owned(),
|
||||
};
|
||||
|
||||
let res = self.execute_combined(cmd).await?;
|
||||
debug!("res: {:?}", res);
|
||||
todo!()
|
||||
let res = self.execute(cmd).await?;
|
||||
let (_, data) = res.wait().await?;
|
||||
|
||||
// let mut folders = Vec::new();
|
||||
// loop {
|
||||
// let st_next = st.recv();
|
||||
// pin_mut!(st_next);
|
||||
let mut folders = Vec::new();
|
||||
for resp in data {
|
||||
if let Response::MailboxData(MailboxData::List { name, .. }) = resp {
|
||||
folders.push(name.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// match future::select(resp, st_next).await {
|
||||
// Either::Left((v, _)) => {
|
||||
// break;
|
||||
// }
|
||||
|
||||
// Either::Right((v, _) ) => {
|
||||
// debug!("RESP: {:?}", v);
|
||||
// // folders.push(v);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// let resp = resp.await?;
|
||||
// debug!("list response: {:?}", resp);
|
||||
Ok(folders)
|
||||
}
|
||||
|
||||
/// Runs the SELECT command
|
||||
|
@ -226,11 +176,12 @@ impl ClientAuthenticated {
|
|||
let cmd = Command::Select {
|
||||
mailbox: mailbox.as_ref().to_owned(),
|
||||
};
|
||||
let (resp, mut st) = self.execute(cmd).await?;
|
||||
let mut stream = self.execute(cmd).await?;
|
||||
// let (resp, mut st) = self.execute(cmd).await?;
|
||||
debug!("execute called returned...");
|
||||
debug!("ST: {:?}", st.recv().await);
|
||||
let resp = resp.await?;
|
||||
debug!("select response: {:?}", resp);
|
||||
debug!("ST: {:?}", stream.next().await);
|
||||
// let resp = resp.await?;
|
||||
// debug!("select response: {:?}", resp);
|
||||
|
||||
// nuke the capabilities cache
|
||||
self.nuke_capabilities();
|
||||
|
@ -240,10 +191,10 @@ impl ClientAuthenticated {
|
|||
|
||||
/// Runs the IDLE command
|
||||
#[cfg(feature = "rfc2177-idle")]
|
||||
pub async fn idle(&mut self) -> Result<UnboundedReceiverStream<Response>> {
|
||||
pub async fn idle(&mut self) -> Result<ResponseStream> {
|
||||
let cmd = Command::Idle;
|
||||
let (_, stream) = self.execute(cmd).await?;
|
||||
Ok(UnboundedReceiverStream::new(stream))
|
||||
let stream = self.execute(cmd).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn nuke_capabilities(&mut self) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt;
|
||||
|
||||
/// Commands, without the tag part.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub enum Command {
|
||||
Capability,
|
||||
Starttls,
|
||||
|
@ -21,6 +21,22 @@ pub enum Command {
|
|||
Idle,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Command::*;
|
||||
match self {
|
||||
Capability => write!(f, "CAPABILITY"),
|
||||
Starttls => write!(f, "STARTTLS"),
|
||||
Login { .. } => write!(f, "LOGIN"),
|
||||
Select { mailbox } => write!(f, "SELECT {}", mailbox),
|
||||
List { reference, mailbox } => write!(f, "LIST {:?} {:?}", reference, mailbox),
|
||||
|
||||
#[cfg(feature = "rfc2177-idle")]
|
||||
Idle => write!(f, "IDLE"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Command {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Command::*;
|
||||
|
|
|
@ -21,6 +21,7 @@ use tokio_stream::wrappers::WatchStream;
|
|||
use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod};
|
||||
|
||||
/// Command sent to the mail thread by something else (i.e. UI)
|
||||
#[derive(Debug)]
|
||||
pub enum MailCommand {
|
||||
/// Refresh the list
|
||||
Refresh,
|
||||
|
@ -30,6 +31,7 @@ pub enum MailCommand {
|
|||
}
|
||||
|
||||
/// Possible events returned from the server that should be sent to the UI
|
||||
#[derive(Debug)]
|
||||
pub enum MailEvent {
|
||||
/// Got the list of folders
|
||||
FolderList(Vec<String>),
|
||||
|
|
14
src/main.rs
14
src/main.rs
|
@ -4,7 +4,11 @@ use std::thread;
|
|||
use anyhow::Result;
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use futures::future::TryFutureExt;
|
||||
use panorama::{config::spawn_config_watcher_system, mail, report_err, ui};
|
||||
use panorama::{
|
||||
config::spawn_config_watcher_system,
|
||||
mail::{self, MailEvent},
|
||||
report_err, ui,
|
||||
};
|
||||
use structopt::StructOpt;
|
||||
use tokio::{
|
||||
runtime::{Builder as RuntimeBuilder, Runtime},
|
||||
|
@ -63,7 +67,7 @@ async fn run(opt: Opt) -> Result<()> {
|
|||
});
|
||||
|
||||
if !opt.headless {
|
||||
run_ui(exit_tx);
|
||||
run_ui(exit_tx, mail2ui_rx);
|
||||
}
|
||||
|
||||
exit_rx.recv().await;
|
||||
|
@ -76,7 +80,7 @@ async fn run(opt: Opt) -> Result<()> {
|
|||
}
|
||||
|
||||
// Spawns the entire UI in a different thread, since it must be thread-local
|
||||
fn run_ui(exit_tx: mpsc::Sender<()>) {
|
||||
fn run_ui(exit_tx: mpsc::Sender<()>, mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>) {
|
||||
let stdout = std::io::stdout();
|
||||
|
||||
let rt = RuntimeBuilder::new_current_thread()
|
||||
|
@ -88,7 +92,9 @@ fn run_ui(exit_tx: mpsc::Sender<()>) {
|
|||
let localset = LocalSet::new();
|
||||
|
||||
localset.spawn_local(async {
|
||||
ui::run_ui(stdout, exit_tx).unwrap_or_else(report_err).await;
|
||||
ui::run_ui(stdout, exit_tx, mail2ui_rx)
|
||||
.unwrap_or_else(report_err)
|
||||
.await;
|
||||
});
|
||||
|
||||
rt.block_on(localset);
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
use tui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::*,
|
||||
};
|
||||
|
||||
pub struct MailTabState {}
|
||||
use super::FrameType;
|
||||
|
||||
impl MailTabState {
|
||||
pub fn new() -> Self {
|
||||
MailTabState {}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MailTab;
|
||||
|
||||
impl StatefulWidget for MailTab {
|
||||
type State = MailTabState;
|
||||
|
||||
fn render(self, rect: Rect, buffer: &mut Buffer, state: &mut Self::State) {}
|
||||
pub fn render_mail_tab(f: &mut FrameType, area: Rect, folders: &[String]) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints([Constraint::Length(20), Constraint::Max(5000)])
|
||||
.split(area);
|
||||
|
||||
let items = folders
|
||||
.iter()
|
||||
.map(|s| ListItem::new(s.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let dirlist = List::new(items)
|
||||
.block(Block::default().borders(Borders::NONE))
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
.highlight_symbol(">>");
|
||||
|
||||
f.render_widget(dirlist, chunks[0]);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use crossterm::{
|
|||
event::{self, Event, KeyCode, KeyEvent},
|
||||
style, terminal,
|
||||
};
|
||||
use futures::{future::FutureExt, select, stream::StreamExt};
|
||||
use tokio::{sync::mpsc, time};
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
|
@ -22,70 +23,42 @@ use tui::{
|
|||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use self::mail_tab::{MailTab, MailTabState};
|
||||
use crate::mail::MailEvent;
|
||||
|
||||
// pub(crate) type FrameType<'a> = Frame<'a, CrosstermBackend<Stdout>>;
|
||||
use self::mail_tab::render_mail_tab;
|
||||
|
||||
pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>;
|
||||
|
||||
const FRAME_DURATION: Duration = Duration::from_millis(17);
|
||||
|
||||
/// Main entrypoint for the UI
|
||||
pub async fn run_ui(mut stdout: Stdout, exit_tx: mpsc::Sender<()>) -> Result<()> {
|
||||
pub async fn run_ui(
|
||||
mut stdout: Stdout,
|
||||
exit_tx: mpsc::Sender<()>,
|
||||
mut mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>,
|
||||
) -> Result<()> {
|
||||
execute!(stdout, cursor::Hide, terminal::EnterAlternateScreen)?;
|
||||
terminal::enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(&mut stdout);
|
||||
let mut term = Terminal::new(backend)?;
|
||||
|
||||
let mut mail_state = MailTabState::new();
|
||||
let mut folders = Vec::<String>::new();
|
||||
|
||||
loop {
|
||||
term.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(5000),
|
||||
// Constraint::Percentage(10),
|
||||
// Constraint::Percentage(80),
|
||||
// Constraint::Percentage(10),
|
||||
])
|
||||
.constraints([Constraint::Length(1), Constraint::Max(5000)])
|
||||
.split(f.size());
|
||||
|
||||
// let chunks2 = Layout::default()
|
||||
// .direction(Direction::Horizontal)
|
||||
// .margin(0)
|
||||
// .constraints([
|
||||
// Constraint::Length(20),
|
||||
// Constraint::Max(5000),
|
||||
// //
|
||||
// ])
|
||||
// .split(chunks[1]);
|
||||
|
||||
// this is the title bar
|
||||
let titles = vec!["hellosu"].into_iter().map(Spans::from).collect();
|
||||
let tabs = Tabs::new(titles);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
|
||||
let mail_tab = MailTab;
|
||||
f.render_stateful_widget(mail_tab, chunks[1], &mut mail_state);
|
||||
// TODO: check active tab
|
||||
// let items = [
|
||||
// ListItem::new("Osu"),
|
||||
// ListItem::new("Game").style(Style::default().add_modifier(Modifier::BOLD)),
|
||||
// ];
|
||||
// let dirlist = List::new(items)
|
||||
// .block(Block::default().title("List").borders(Borders::ALL))
|
||||
// .style(Style::default().fg(Color::White))
|
||||
// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
// .highlight_symbol(">>");
|
||||
// f.render_widget(dirlist, chunks2[0]);
|
||||
|
||||
// let block = Block::default().title("Block").borders(Borders::ALL);
|
||||
// f.render_widget(block, chunks2[1]);
|
||||
|
||||
// let block = Block::default().title("Block 2").borders(Borders::ALL);
|
||||
// f.render_widget(block, chunks[1]);
|
||||
render_mail_tab(f, chunks[1], &folders);
|
||||
})?;
|
||||
|
||||
let event = if event::poll(FRAME_DURATION)? {
|
||||
|
@ -105,17 +78,22 @@ pub async fn run_ui(mut stdout: Stdout, exit_tx: mpsc::Sender<()>) -> Result<()>
|
|||
None
|
||||
};
|
||||
|
||||
// approx 60fps
|
||||
time::sleep(FRAME_DURATION).await;
|
||||
select! {
|
||||
mail_evt = mail2ui_rx.recv().fuse() => {
|
||||
debug!("received mail event: {:?}", mail_evt);
|
||||
// TODO: handle case that channel is closed later
|
||||
let mail_evt = mail_evt.unwrap();
|
||||
|
||||
// if let Event::Input(input) = events.next()? {
|
||||
// match input {
|
||||
// Key::Char('q') => {
|
||||
// break;
|
||||
// }
|
||||
// _ => {}
|
||||
// }
|
||||
// }
|
||||
match mail_evt {
|
||||
MailEvent::FolderList(new_folders) => {
|
||||
folders = new_folders;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// approx 60fps
|
||||
_ = time::sleep(FRAME_DURATION).fuse() => {}
|
||||
}
|
||||
}
|
||||
|
||||
mem::drop(term);
|
||||
|
|
Loading…
Reference in a new issue