mfontanini/presenterm · error · anyhow::Error
failed to create speaker notes listener: {e}
Error message
failed to create speaker notes listener: {e} What it means
This error is raised in `SpeakerNotesComponents::new` when `SpeakerNotesEventListener::new` returns an `io::Error`. The listener constructor creates a UDP (DGRAM) socket, sets SO_REUSEADDR and non-blocking mode, then binds to the configured listen address; any failing step is wrapped with this context. It means the speaker-notes listener could not claim its UDP port, so `--listen-speaker-notes` mode cannot start.
Solutions
- Check what holds the listen port (e.g. `ss -ulpen` or `lsof -i :<port>`) and stop that process, or pick a different `speaker_notes.listen_address` port in your config.
- If binding a privileged port, use a port above 1024 or run with sufficient privileges.
- On macOS, note SO_REUSEADDR is not applied: ensure no previous listener instance is still running on the port.
- Raise the file-descriptor limit if `Socket::new` fails with 'Too many open files', and verify the environment permits UDP sockets.
Example fix
// before (config.toml) - port already used by another listener [speaker_notes] listen_address = "127.0.0.1:48700" // after - choose a free port [speaker_notes] listen_address = "127.0.0.1:48701"
Defensive patterns
Strategy: validation
Validate before calling
// Before launching with --listen-speaker-notes, check the port is free:
use std::net::UdpSocket;
fn validate_listen_address(addr: std::net::SocketAddr) -> Result<(), String> {
match UdpSocket::bind(addr) {
Ok(_) => Ok(()),
Err(e) => Err(format!("listen address {addr} unusable: {e} (in use or privileged?)")),
}
} Try / catch
// Distinguish bind conflicts and fail with a clear message:
match SpeakerNotesComponents::new(...) {
Ok(components) => components,
Err(e) if e.to_string().contains("speaker notes listener") => {
eprintln!("listener setup failed: {e:#}; is another speaker-notes listener on this port?");
std::process::exit(1);
}
Err(e) => return Err(e),
} Prevention
- Pick an unprivileged, high-numbered UDP port for listen_address.
- Ensure only one speaker-notes listener runs per host on non-macOS, and none concurrently on macOS (no SO_REUSEADDR there).
- Check port usage with `ss -ulpen` / `lsof -i :<port>` before starting.
- Use distinct port pairs for publish/listen in shared config files across machines.
When it happens
Trigger: The CLI was started with `--listen-speaker-notes` and `SpeakerNotesEventListener::new(config.speaker_notes.listen_address, ...)` fails. Most common: `s.bind(&address)` fails with EADDRINUSE because another process (or the OS denying reuse on macOS where SO_REUSEADDR is not set) already holds the listen port; also `Socket::new` failing (no UDP socket support / fd exhaustion) or `set_reuse_address`/`set_nonblocking` returning an error.
Common situations: Another presenterm instance (or any UDP service) is already bound to the listen address, so the port is taken; the configured `speaker_notes.listen_address` is a privileged port (<1024) while running unprivileged (EACCES); running on macOS where the code skips SO_REUSEADDR and a stale socket still holds the port; sandboxed environments blocking socket creation.
Related errors
AI-assisted analysis of mfontanini/presenterm@5f8add11a2 (2026-09-12).
Data as JSON: /api/errors/6e4f20bf6b88d25a.
Report an issue: GitHub.
Appendix: source
Thrown at src/main.rs:377
events_publisher: Option<SpeakerNotesEventPublisher>,
}
impl SpeakerNotesComponents {
fn new(cli: &Cli, config: &Config, path: &Path) -> anyhow::Result<Self> {
let full_presentation_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let publish_speaker_notes =
cli.publish_speaker_notes || (config.speaker_notes.always_publish && !cli.listen_speaker_notes);
let events_publisher = publish_speaker_notes
.then(|| {
SpeakerNotesEventPublisher::new(config.speaker_notes.publish_address, full_presentation_path.clone())
})
.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()
.map_err(|e| anyhow!("failed to create speaker notes listener: {e}"))?;
Ok(Self { events_listener, events_publisher })
}
}
fn overflow_validation_enabled(mode: &PresentMode, config: &ValidateOverflows) -> bool {
match (config, mode) {
(ValidateOverflows::Always, _) => true,
(ValidateOverflows::Never, _) => false,
(ValidateOverflows::WhenPresenting, PresentMode::Presentation) => true,
(ValidateOverflows::WhenDeveloping, PresentMode::Development) => true,
_ => false,
}
}
fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "json-schema")]
if cli.generate_config_file_schema {
let schema = schemars::schema_for!(Config);View on GitHub (pinned to 5f8add11a2)