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

  1. 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.
  2. If binding a privileged port, use a port above 1024 or run with sufficient privileges.
  3. On macOS, note SO_REUSEADDR is not applied: ensure no previous listener instance is still running on the port.
  4. 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

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)