Writing to a socket in a loop

Hi, I've been trying to learn about sockets a bit for fun and came across some behavior I don't understand. I wanted to test out using the sockets exposed by the window manager hyprland. As a toy example, I wanted to write a request to the first socket whenever the second socket reported a particular kind of event. In this case, I wanted to write a command to open a terminal whenever a layer was closed (a particular event for the WM).

Here's the code I wrote:

use std::os::unix::net::UnixStream;
use std::io::{Read, BufRead, BufReader};
use std::io::{Write, BufWriter};


fn main() -> std::io::Result<()> {
    let his = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").expect("Not running hyprland!");

    let req_path = format!("/tmp/hypr/{}/.socket.sock", &his);
    let mut request_stream = UnixStream::connect(&req_path)?;

    let ev_path = format!("/tmp/hypr/{}/.socket2.sock", &his);
    let event_stream = UnixStream::connect(ev_path)?;
    let event_reader = BufReader::new(event_stream);

    /// This commented request works as expected; just once at start of program
    // request_stream.write_all(b"dispatch exec kitty\n")?;
    for line in event_reader.lines() {
        let content = line.unwrap();
        println!("{}", &content);  // Check events reported by event socket
        if content.contains("closelayer") {
            /// This request causes broken pipe unless the next line is uncommented
            /// Otherwise everything works as expected if uncommenting next line
            // let mut request_stream = UnixStream::connect(&req_path)?;
            println!("{:?}", request_stream.write_all(b"dispatch exec kitty\n"));
        }
    }
    Ok(())
}

The code compiles and runs fine, printing out the correct events when window related things occur. The problem is when I close a layer (e.g. close rofi) which triggers the inner if condition. In this case it reports a broken pipe from the write_all command, but the program continues to run.

What causes the broken pipe error in this case unless I declare the request_stream variable again in the local scope? If it was getting dropped between iterations of the loop, wouldn't the compiler give an error? But I can't see what else might be happening, since searching around suggests broken pipes are caused by trying to write to a closed connection. I thought in Rust the connection would last until the stream is dropped (at the end of the program)?

You should be reading hyprland's code to see why it closes the connection (if it isn't in documentation.)

Hi, thanks for the reply. Unfortunately the only docs about the socket are that short IPC page I linked above (as far as I can tell). I tried peeking through the codebase but haven't found anything promising yet. I was hoping this might be a simple example just to learn the basic parts about sockets in general, but perhaps this might get complicated fast.

Given this might be a hyprland specific issue, can I ask the following questions about the example/concepts instead?

  1. From a conceptual point, does this implementation "make sense" to write in Rust the idea of taking events from one socket, processing them in a loop over the lines it emits, and then pushing those to a request socket for another program to handle? I imagine the "best" way to do this involves async, which I haven't started learning yet, but wanted to start first with an implementation not using async to start learning.
  2. If this is an issue specific to the hyprland sockets, can you imagine a good reason why the request socket write call seems to work fine outside the event reader loop, but then gets a broken pipe inside? And why it might be the case that re-declaring the request stream inside the loop fixes it? This really felt like an implementation/Rust-specific problem, so I'd like to understand better why this might be specific to the socket rather than the code I've written. I thought that the write_all method would behave in some consistent way across all sockets no matter the context in the code. I suppose one naive way the error might arise is if the OS for some reason deleted the socket between the first declaration and then recreated it by the time the write method is called. But that seems a bit strange to me, so I wonder if there are other more plausible things that could cause this type of behavior when working with sockets?

wayland compositors will disconnect the socket if the client isn't consuming events fast enough and the buffers fill up, which can happen if the read loop stalls because it's blocked on something else.

Hi, thanks for the reply. Do you have suggestions on modifying the above code for educational purposes to make sure the events are "consumed" fast enough? Do you think adding a new connection every time an event is triggered is a "good idea" in this case? I'm still learning the basics of sockets, so don't have a good intuition for the overhead of opening a new connection to a Unix socket and if that would be considered wasteful.

It was only meant as a hint what might be happening. Maybe you can enable debug logging the wayland server to figure out why it's actually disconnecting you.

If that is indeed the cause then either using non-blocking IO or multiple threads could help. Just consume the events as fast as you can and make sure that that does not have to wait on anything else.

Thanks, I'll try experimenting with some async functions. That was next on my to-do list of trying to learn more about sockets, so this seems like a good motivation to start that sooner than later.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.