Many fixes for the clipboard and others
- Reverse connections must be maintained to ensure messages are processed in order. (Whoops!) - The clipboard context must remain live in order for the data to remain available for applications, at least on X11. (And it couldn't hurt elsewhere, either, I guess.) - Print out the server version at startup time, so we can be sure what we're talking to. - Print out the full details of the error when something goes wrong with `browse` or `clip`.
This commit is contained in:
parent
a3fa032500
commit
2a582e25a8
7 changed files with 110 additions and 57 deletions
|
|
@ -1,7 +1,6 @@
|
|||
use crate::message::{Message, MessageReader, MessageWriter};
|
||||
use anyhow::{bail, Result};
|
||||
use bytes::BytesMut;
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use log::LevelFilter;
|
||||
use log::{debug, error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -213,16 +212,11 @@ async fn client_handle_messages<T: AsyncRead + Unpin>(
|
|||
}
|
||||
|
||||
ClipStart(id) => {
|
||||
info!("Starting clip op {id}");
|
||||
clipboard_messages.insert(id, Vec::new());
|
||||
}
|
||||
|
||||
ClipData(id, mut data) => match clipboard_messages.get_mut(&id) {
|
||||
Some(bytes) => {
|
||||
info!(
|
||||
"Received data for clip op {id} ({len} bytes)",
|
||||
len = data.len()
|
||||
);
|
||||
if bytes.len() < MAX_CLIPBOARD_SIZE {
|
||||
bytes.append(&mut data);
|
||||
}
|
||||
|
|
@ -243,9 +237,10 @@ async fn client_handle_messages<T: AsyncRead + Unpin>(
|
|||
continue;
|
||||
};
|
||||
|
||||
let mut ctx = ClipboardContext::new().unwrap();
|
||||
if let Err(e) = ctx.set_contents(data) {
|
||||
error!("Unable to set clipboard data for op {id}: {e:?}");
|
||||
if let Err(e) =
|
||||
events.send(ui::UIEvent::SetClipboard(data)).await
|
||||
{
|
||||
error!("Error sending clipboard request: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use super::{client_listen, config::ServerConfig};
|
||||
use crate::message::PortDesc;
|
||||
use anyhow::Result;
|
||||
use copypasta::{ClipboardContext, ClipboardProvider};
|
||||
use crossterm::{
|
||||
event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
||||
execute,
|
||||
|
|
@ -33,6 +34,7 @@ pub enum UIEvent {
|
|||
ServerLine(String),
|
||||
LogLine(log::Level, String),
|
||||
Ports(Vec<PortDesc>),
|
||||
SetClipboard(String),
|
||||
}
|
||||
|
||||
pub enum UIReturn {
|
||||
|
|
@ -166,7 +168,6 @@ impl Listener {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UI {
|
||||
events: mpsc::Receiver<UIEvent>,
|
||||
ports: HashMap<u16, Listener>,
|
||||
|
|
@ -179,6 +180,7 @@ pub struct UI {
|
|||
show_help: bool,
|
||||
alternate_screen: bool,
|
||||
raw_mode: bool,
|
||||
clipboard: ClipboardContext,
|
||||
}
|
||||
|
||||
impl UI {
|
||||
|
|
@ -195,6 +197,8 @@ impl UI {
|
|||
config,
|
||||
alternate_screen: false,
|
||||
raw_mode: false,
|
||||
clipboard: ClipboardContext::new()
|
||||
.expect("Unable to initialize clipboard context"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -621,6 +625,14 @@ impl UI {
|
|||
}
|
||||
self.lines.push_back(format!("[CLIENT] {line}"));
|
||||
}
|
||||
Some(UIEvent::SetClipboard(contents)) => {
|
||||
let length = contents.len();
|
||||
if let Err(e) = self.clipboard.set_contents(contents) {
|
||||
error!("Error setting clipboard contents: {e:#}");
|
||||
} else {
|
||||
info!("Received clipboard contents ({length} bytes)");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.running = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ mod message;
|
|||
mod reverse;
|
||||
mod server;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const REV: &str = env!("REPO_REV");
|
||||
pub const DIRTY: &str = env!("REPO_DIRTY");
|
||||
|
||||
pub use client::run_client;
|
||||
pub use reverse::browse_url;
|
||||
pub use reverse::clip_file;
|
||||
|
|
|
|||
10
src/main.rs
10
src/main.rs
|
|
@ -1,10 +1,6 @@
|
|||
// TODO: An actual proper command line parsing
|
||||
use indoc::indoc;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const REV: &str = env!("REPO_REV");
|
||||
const DIRTY: &str = env!("REPO_DIRTY");
|
||||
|
||||
fn usage() {
|
||||
println!(indoc! {"
|
||||
usage: fwd [options] (<server> | browse <url> | clip [<file>])
|
||||
|
|
@ -121,7 +117,7 @@ fn parse_args(args: Vec<String>) -> Args {
|
|||
async fn browse_url(url: &str) {
|
||||
if let Err(e) = fwd::browse_url(url).await {
|
||||
eprintln!("Unable to open {url}");
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +125,7 @@ async fn browse_url(url: &str) {
|
|||
async fn clip_file(file: String) {
|
||||
if let Err(e) = fwd::clip_file(&file).await {
|
||||
eprintln!("Unable to copy to the clipboard");
|
||||
eprintln!("{}", e);
|
||||
eprintln!("{:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +137,7 @@ async fn main() {
|
|||
usage();
|
||||
}
|
||||
Args::Version => {
|
||||
println!("fwd {VERSION} (rev {REV}{DIRTY})");
|
||||
println!("fwd {} (rev {}{})", fwd::VERSION, fwd::REV, fwd::DIRTY);
|
||||
}
|
||||
Args::Server => {
|
||||
fwd::run_server().await;
|
||||
|
|
|
|||
|
|
@ -6,16 +6,28 @@ use tokio::io::{AsyncRead, AsyncReadExt};
|
|||
mod unix;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub use unix::{handle_reverse_connections, send_reverse_message};
|
||||
pub use unix::{handle_reverse_connections, ReverseConnection};
|
||||
|
||||
use crate::message::Message;
|
||||
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
pub async fn send_reverse_message(_message: Message) -> Result<()> {
|
||||
use anyhow::anyhow;
|
||||
Err(anyhow!(
|
||||
"Server-side operations are not supported on this platform"
|
||||
))
|
||||
pub struct ReverseConnection {}
|
||||
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
impl ReverseConnection {
|
||||
pub async fn new() -> Result<Self> {
|
||||
use anyhow::anyhow;
|
||||
Err(anyhow!(
|
||||
"Server-side operations are not supported on this platform"
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, message: Message) -> Result<()> {
|
||||
use anyhow::anyhow;
|
||||
Err(anyhow!(
|
||||
"Server-side operations are not supported on this platform"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "unix"))]
|
||||
|
|
@ -27,12 +39,16 @@ pub async fn handle_reverse_connections(
|
|||
|
||||
#[inline]
|
||||
pub async fn browse_url(url: &str) -> Result<()> {
|
||||
send_reverse_message(Message::Browse(url.to_string())).await
|
||||
ReverseConnection::new()
|
||||
.await?
|
||||
.send(Message::Browse(url.to_string()))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn clip_reader<T: AsyncRead + Unpin>(reader: &mut T) -> Result<()> {
|
||||
let mut connection = ReverseConnection::new().await?;
|
||||
let clip_id: u64 = random();
|
||||
send_reverse_message(Message::ClipStart(clip_id)).await?;
|
||||
connection.send(Message::ClipStart(clip_id)).await?;
|
||||
|
||||
let mut count = 0;
|
||||
let mut buf = vec![0; 1024];
|
||||
|
|
@ -43,7 +59,7 @@ async fn clip_reader<T: AsyncRead + Unpin>(reader: &mut T) -> Result<()> {
|
|||
}
|
||||
count += read;
|
||||
if count == buf.len() {
|
||||
send_reverse_message(Message::ClipData(clip_id, buf)).await?;
|
||||
connection.send(Message::ClipData(clip_id, buf)).await?;
|
||||
buf = vec![0; 1024];
|
||||
count = 0;
|
||||
}
|
||||
|
|
@ -51,10 +67,10 @@ async fn clip_reader<T: AsyncRead + Unpin>(reader: &mut T) -> Result<()> {
|
|||
|
||||
if count > 0 {
|
||||
buf.resize(count, 0);
|
||||
send_reverse_message(Message::ClipData(clip_id, buf)).await?;
|
||||
connection.send(Message::ClipData(clip_id, buf)).await?;
|
||||
}
|
||||
|
||||
send_reverse_message(Message::ClipEnd(clip_id)).await?;
|
||||
connection.send(Message::ClipEnd(clip_id)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,32 +9,27 @@ use tokio::sync::mpsc;
|
|||
|
||||
use crate::message::{Message, MessageReader, MessageWriter};
|
||||
|
||||
pub async fn send_reverse_message(message: Message) -> Result<()> {
|
||||
let path = socket_path().context("Error getting socket path")?;
|
||||
let stream = match UnixStream::connect(&path).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => bail!(
|
||||
"Error connecting to socket: {e} (is fwd actually connected here?)"
|
||||
),
|
||||
};
|
||||
let mut writer = MessageWriter::new(stream);
|
||||
writer
|
||||
.write(message)
|
||||
.await
|
||||
.context("Error sending browse message")?;
|
||||
Ok(())
|
||||
pub struct ReverseConnection {
|
||||
writer: MessageWriter<UnixStream>,
|
||||
}
|
||||
|
||||
fn socket_directory() -> Result<std::path::PathBuf> {
|
||||
let base_directories = xdg::BaseDirectories::new()
|
||||
.context("Error creating BaseDirectories")?;
|
||||
match base_directories.place_runtime_file("fwd") {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => {
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("fwd{}", users::get_current_uid()));
|
||||
Ok(path)
|
||||
}
|
||||
impl ReverseConnection {
|
||||
pub async fn new() -> Result<Self> {
|
||||
let path = socket_path().context("Error getting socket path")?;
|
||||
let stream = match UnixStream::connect(&path).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => bail!("Error connecting to socket: {e} (is fwd actually connected here?)"),
|
||||
};
|
||||
|
||||
Ok(ReverseConnection { writer: MessageWriter::new(stream) })
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, message: Message) -> Result<()> {
|
||||
self.writer
|
||||
.write(message)
|
||||
.await
|
||||
.context("Error sending reverse message")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,6 +48,19 @@ pub fn socket_path() -> Result<PathBuf> {
|
|||
Ok(socket_path)
|
||||
}
|
||||
|
||||
fn socket_directory() -> Result<std::path::PathBuf> {
|
||||
let base_directories = xdg::BaseDirectories::new()
|
||||
.context("Error creating BaseDirectories")?;
|
||||
match base_directories.place_runtime_file("fwd") {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_) => {
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("fwd{}", users::get_current_uid()));
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_reverse_connections(
|
||||
messages: mpsc::Sender<Message>,
|
||||
) -> Result<()> {
|
||||
|
|
@ -87,10 +95,18 @@ async fn handle_connection(
|
|||
sender: mpsc::Sender<Message>,
|
||||
) -> Result<()> {
|
||||
let mut reader = MessageReader::new(socket);
|
||||
let message = reader.read().await.context("Error reading message")?;
|
||||
match message {
|
||||
Message::Browse(url) => sender.send(Message::Browse(url)).await?,
|
||||
_ => bail!("Unsupported message: {:?}", message),
|
||||
while let Ok(message) = reader.read().await {
|
||||
match message {
|
||||
Message::Browse(url) => sender.send(Message::Browse(url)).await?,
|
||||
Message::ClipStart(id) => {
|
||||
sender.send(Message::ClipStart(id)).await?
|
||||
}
|
||||
Message::ClipData(id, data) => {
|
||||
sender.send(Message::ClipData(id, data)).await?
|
||||
}
|
||||
Message::ClipEnd(id) => sender.send(Message::ClipEnd(id)).await?,
|
||||
_ => bail!("Unsupported message: {:?}", message),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -26,12 +26,26 @@ async fn server_loop<Reader: AsyncRead + Unpin>(
|
|||
) -> Result<()> {
|
||||
// The first message we send must be an announcement.
|
||||
writer.send(Message::Hello(0, 2, vec![])).await?;
|
||||
|
||||
let mut version_reported = false;
|
||||
loop {
|
||||
use Message::*;
|
||||
match reader.read().await? {
|
||||
Ping => (),
|
||||
Refresh => {
|
||||
// Just log the version, if we haven't yet. We do this extra
|
||||
// work to avoid spamming the log, but we wait until we
|
||||
// receive the first message to be sure that the client is in
|
||||
// a place to display our logging properly.
|
||||
if !version_reported {
|
||||
eprintln!(
|
||||
"fwd server {} (rev {}{})",
|
||||
crate::VERSION,
|
||||
crate::REV,
|
||||
crate::DIRTY
|
||||
);
|
||||
version_reported = true;
|
||||
}
|
||||
|
||||
let ports = match refresh::get_entries().await {
|
||||
Ok(ports) => ports,
|
||||
Err(e) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue