Skip to content

Commit

Permalink
fix: bind/publish to different addresses in non linux (#425)
Browse files Browse the repository at this point in the history
This makes:

* Linux bind to 127.0.0.1 and publish on 127.255.255.255 which allows
multiple listeners on the same port as expected.
* Windows bind and publish on 127.0.0.1, which also allows multiple
listeners.
* Mac's lo interface doesn't have `BROADCAST` enabled, so binding
multiple listeners is not supported since while we could allow binding
multiple listeners, only one of them would get the packets so instead we
err. Maybe there's a better way of doing this but in practice nobody's
going to be running multiple presentations at once so making this more
complicated provides no gain.
  • Loading branch information
mfontanini authored Jan 25, 2025
2 parents 5f81ef3 + 5506a7b commit efc8335
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ version = "0.9.0"
edition = "2021"

[dependencies]
anyhow = "1"
ansi-parser = "0.9"
base64 = "0.22"
bincode = "1.3"
Expand Down
46 changes: 43 additions & 3 deletions src/commands/speaker_notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ impl SpeakerNotesEventPublisher {
// ignore unrelated events.
let envelope = SpeakerNotesEventEnvelope { event, presentation_path: self.presentation_path.clone() };
let data = serde_json::to_string(&envelope).expect("serialization failed");
self.socket.send(data.as_bytes())?;
Ok(())
match self.socket.send(data.as_bytes()) {
Ok(_) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::ConnectionRefused => Ok(()),
Err(e) => Err(e),
}
}
}

Expand All @@ -38,6 +41,7 @@ impl SpeakerNotesEventListener {
pub fn new(address: SocketAddr, presentation_path: PathBuf) -> io::Result<Self> {
let s = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
// Use SO_REUSEADDR so we can have multiple listeners on the same port.
#[cfg(not(target_os = "macos"))]
s.set_reuse_address(true)?;
// Don't block so we can listen to the keyboard and this socket at the same time.
s.set_nonblocking(true)?;
Expand All @@ -61,7 +65,7 @@ impl SpeakerNotesEventListener {
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "command")]
pub(crate) enum SpeakerNotesEvent {
GoToSlide { slide: u32 },
Expand All @@ -73,3 +77,39 @@ struct SpeakerNotesEventEnvelope {
presentation_path: PathBuf,
event: SpeakerNotesEvent,
}

#[cfg(not(target_os = "macos"))]
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{default_speaker_notes_listen_address, default_speaker_notes_publish_address};
use std::{thread::sleep, time::Duration};

fn make_listener(path: PathBuf) -> SpeakerNotesEventListener {
SpeakerNotesEventListener::new(default_speaker_notes_listen_address(), path).expect("building listener")
}

fn make_publisher(path: PathBuf) -> SpeakerNotesEventPublisher {
SpeakerNotesEventPublisher::new(default_speaker_notes_publish_address(), path).expect("building publisher")
}

#[test]
fn bind_multiple() {
let _l1 = make_listener("".into());
let _l2 = make_listener("".into());
}

#[test]
fn multicast() {
let path = PathBuf::from("/tmp/test.md");
let l1 = make_listener(path.clone());
let l2 = make_listener(path.clone());
let publisher = make_publisher(path);
let event = SpeakerNotesEvent::Exit;
publisher.send(event.clone()).expect("send failed");
sleep(Duration::from_millis(100));

assert_eq!(l1.try_recv().expect("recv first failed"), Some(event.clone()));
assert_eq!(l2.try_recv().expect("recv second failed"), Some(event));
}
}
22 changes: 16 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,11 @@ impl Default for KeyBindingsConfig {
#[serde(deny_unknown_fields)]
pub struct SpeakerNotesConfig {
/// The address in which to listen for speaker note events.
#[serde(default = "default_speaker_notes_address")]
#[serde(default = "default_speaker_notes_listen_address")]
pub listen_address: SocketAddr,

/// The address in which to publish speaker notes events.
#[serde(default = "default_speaker_notes_address")]
#[serde(default = "default_speaker_notes_publish_address")]
pub publish_address: SocketAddr,

/// Whether to always publish speaker notes.
Expand All @@ -408,8 +408,8 @@ pub struct SpeakerNotesConfig {
impl Default for SpeakerNotesConfig {
fn default() -> Self {
Self {
listen_address: default_speaker_notes_address(),
publish_address: default_speaker_notes_address(),
listen_address: default_speaker_notes_listen_address(),
publish_address: default_speaker_notes_publish_address(),
always_publish: false,
}
}
Expand Down Expand Up @@ -480,12 +480,22 @@ fn default_suspend_bindings() -> Vec<KeyBinding> {
}

#[cfg(target_os = "linux")]
fn default_speaker_notes_address() -> SocketAddr {
pub(crate) fn default_speaker_notes_listen_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 255, 255, 255)), 59418)
}

#[cfg(not(target_os = "linux"))]
fn default_speaker_notes_address() -> SocketAddr {
pub(crate) fn default_speaker_notes_listen_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 59418)
}

#[cfg(not(target_os = "macos"))]
pub(crate) fn default_speaker_notes_publish_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 255, 255, 255)), 59418)
}

#[cfg(target_os = "macos")]
pub(crate) fn default_speaker_notes_publish_address() -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 59418)
}

Expand Down
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
theme::{PresentationTheme, PresentationThemeSet},
third_party::{ThirdPartyConfigs, ThirdPartyRender},
};
use anyhow::anyhow;
use clap::{CommandFactory, Parser, error::ErrorKind};
use commands::speaker_notes::{SpeakerNotesEventListener, SpeakerNotesEventPublisher};
use comrak::Arena;
Expand Down Expand Up @@ -320,11 +321,13 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
.then(|| {
SpeakerNotesEventPublisher::new(config.speaker_notes.publish_address, full_presentation_path.clone())
})
.transpose()?;
.transpose()
.map_err(|e| anyhow!("failed to create speaker notes publisher: {e}"))?;
let events_listener = cli
.listen_speaker_notes
.then(|| SpeakerNotesEventListener::new(config.speaker_notes.listen_address, full_presentation_path))
.transpose()?;
.transpose()
.map_err(|e| anyhow!("failed to create speaker notes listener: {e}"))?;
let command_listener = CommandListener::new(config.bindings.clone(), events_listener)?;

options.print_modal_background = matches!(graphics_mode, GraphicsMode::Kitty { .. });
Expand Down

0 comments on commit efc8335

Please sign in to comment.