Compare commits

...

4 commits

Author SHA1 Message Date
6736cdd431 Additional tests for configuration 2024-08-08 10:26:36 -07:00
e1768a0433 Honor the configured description for a port, if any
Get rid of that dumb warning, can't believe I forgot to wire this for
so long.
2024-08-08 10:15:42 -07:00
2a582e25a8 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`.
2024-08-08 10:07:57 -07:00
a3fa032500 Fix git status parsing 2024-08-08 07:18:13 -07:00
9 changed files with 188 additions and 66 deletions

View file

@ -52,8 +52,9 @@ fn emit_git_commit() {
eprintln!("`git rev-parse --short HEAD` failed, stderr: {stderr}");
panic!("`git rev-parse --short HEAD` failed");
}
let rev =
std::str::from_utf8(&output.stdout).expect("git did not output utf8");
let rev = std::str::from_utf8(&output.stdout)
.expect("git did not output utf8")
.trim();
println!("cargo::rustc-env=REPO_REV={rev}");
}
@ -86,13 +87,20 @@ fn emit_git_dirty() {
// If there *was* any output, parse it and tell cargo to re-run if any of
// these files changed. (Maybe they get reverted! Then the repo status
// will change.)
for line in output.lines() {
let fields: Vec<_> = line.split('\x00').collect();
let parts: Vec<_> = fields[0].split(' ').collect();
let path = parts[1];
println!("cargo::rerun-if-changed={path}");
let mut split = output.split('\x00');
while let Some(field) = split.next() {
if field.is_empty() {
continue;
}
let prefix = &field[0..3];
println!("cargo::rerun-if-changed={}", &field[3..]);
let b = prefix.as_bytes();
if b[0] == b'R' || b[1] == b'R' || b[0] == b'C' || b[1] == b'C' {
if let Some(additional) = split.next() {
println!("cargo::rerun-if-changed={additional}");
}
}
}
// Emit the repository status.
let dirty = if output.trim().is_empty() {
""

View file

@ -20,6 +20,11 @@ impl ServerConfig {
ServerConfig { auto: true, ports: HashMap::new() }
}
#[cfg(test)]
pub fn insert(&mut self, port: u16, config: PortConfig) {
self.ports.insert(port, config);
}
pub fn contains_key(&self, port: u16) -> bool {
self.ports.contains_key(&port)
}

View file

@ -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);
}
}

View file

@ -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"),
}
}
@ -567,7 +571,7 @@ impl UI {
// Grab the selected port
let selected_port = self.get_selected_port();
for port_desc in p.into_iter() {
for mut port_desc in p.into_iter() {
leftover_ports.remove(&port_desc.port);
if let Some(listener) = self.ports.get_mut(&port_desc.port)
{
@ -575,6 +579,8 @@ impl UI {
} else {
let config = self.config.get(port_desc.port);
info!("Port config {port_desc:?} -> {config:?}");
port_desc.desc =
config.description.unwrap_or(port_desc.desc);
self.ports.insert(
port_desc.port,
@ -621,6 +627,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;
}
@ -660,6 +674,8 @@ fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect {
#[cfg(test)]
mod tests {
use crate::client::config::PortConfig;
use super::*;
use assert_matches::assert_matches;
@ -924,4 +940,56 @@ mod tests {
assert_eq!(centered.width, 10);
assert_eq!(centered.height, 10);
}
#[test]
fn port_config_description_respected() {
let (sender, receiver) = mpsc::channel(64);
let mut config = ServerConfig::default();
config.insert(
8080,
PortConfig {
enabled: true,
description: Some("override".to_string()),
},
);
let mut ui = UI::new(receiver, config);
// There are ports...
ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc {
port: 8080,
desc: "my-service".to_string(),
}])));
let description = ui.ports.get(&8080).unwrap().desc.as_ref().unwrap();
assert_eq!(description.desc, "override");
drop(sender);
}
#[test]
fn port_config_enabled_respected() {
let (sender, receiver) = mpsc::channel(64);
let mut config = ServerConfig::default();
config.insert(
8080,
PortConfig {
enabled: false,
description: Some("override".to_string()),
},
);
let mut ui = UI::new(receiver, config);
// There are ports...
ui.handle_internal_event(Some(UIEvent::Ports(vec![PortDesc {
port: 8080,
desc: "my-service".to_string(),
}])));
let state = ui.ports.get(&8080).unwrap().state();
assert_eq!(state, State::Disabled);
drop(sender);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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(())
}

View file

@ -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(())

View file

@ -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) => {