How can I elicit a full response from rust-analyzer to my initialization request?

I want to call rust-analyzer from within my rust program, but I am having trouble getting a full response to my initialization request. Part of me suspects it has to do with line ending issues in my calls to read_line(buf).

Running the code below seems to elicit a partial response from rust-analyzer. I get the response header ("Content-Length" and a "\r\n\r\n"), but I do not get the body of the response which contains json-rpc data.

I do notice that when I send another request to the rust-analyzer server, I will see the body of the initialization response, but I will also see various errors. You can try that out by commenting out the code near the bottom of my script.

How can I elicit a full response from rust-analyzer to my initialization request?

use std::{
    io::{BufRead, BufReader, BufWriter, Write},
    process::{Command, Stdio},
    thread,
    time::Duration,
};

fn main() {
    let mut ra = Command::new("rust-analyzer")
        //.env("RA_LOG", "trace")
        .stderr(Stdio::piped())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let stdin = ra.stdin.take().expect("Failed to take stdin.");
    let stdout = ra.stdout.take().expect("Failed to take stdout.");
    let stderr = ra.stderr.take().expect("Failed to take stederr");

    let mut stdin = BufWriter::new(stdin);
    let mut stdout = BufReader::new(stdout);
    let mut stderr = BufReader::new(stderr);

    // A thread for Stdout
    let out_h = thread::spawn(move || {
        for i in 0..10 {
            println!("out_iteration: {}", i);
            let mut buf = String::new();
            let _ = stdout.read_line(&mut buf).unwrap();
            println!("out_buf: {}", buf);
        }
        println!("No more out_rx iterations");
    });

    // A thread for Stderr
    let err_h = thread::spawn(move || {
        for i in 0..10 {
            println!("err_iteration: {}", i);
            let mut buf = String::new();
            let _ = stderr.read_line(&mut buf).unwrap();
            println!("err_buf: {}", buf);
        }
        println!("No more err_rx iterations");
    });

    // Create and send an initialize message to rust-analyzer
    let header = "Content-Length: 164\r\n\r\n";
    let body = r#"{"id":111280,"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"My Clinet","version":"1.0"},"processId":111280,"rootUri":null}}"#;
    let _ = stdin.write(header.as_bytes());
    let _ = stdin.write(body.as_bytes());
    let _ = stdin.flush();

    // Can we send an initiailized message?
    // thread::sleep(Duration::from_secs(1));
    // let header = "Content-Length;: 52\r\n\r\n";
    // let body = r#"{"jsonrpc":"2.0","method":"initialized","params":{}}"#;
    // let _ = stdin.write(header.as_bytes());
    // let _ = stdin.write(body.as_bytes());
    // let _ = stdin.flush();

    // Cleanup
    ra.wait().unwrap();
    out_h.join().unwrap();
    err_h.join().unwrap();
}

You should be using write_all instead of write. write returns how many bytes were written, which may only cover some of the data write was called with. cargo clippy will warn you about this if you don't throw away the return value with let _ = :

    // ...
    let header = "Content-Length: 164\r\n\r\n";
    let body = r#"{"id":111280,"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"My Clinet","version":"1.0"},"processId":111280,"rootUri":null}}"#;
    stdin.write(header.as_bytes()).unwrap();
    // ...
error: written amount is not handled
  --> src\main.rs:50:5
   |
50 |     stdin.write(header.as_bytes()).unwrap();
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: use `Write::write_all` instead, or handle partial writes
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unused_io_amount
   = note: `#[deny(clippy::unused_io_amount)]` on by default

I also added a newline after the body with stdin.write_all(b"\n"); and was able to get output.

3 Likes

Hi @Heliozoa! I went ahead and changed write to write_all like you suggested. Thank you! However, I noticed that adding the "\n" to the request caused an error to pop up which seems to result in a broken pipe on future calls.

I have updated the code below to incorporate your changes and to include some sleep functions to illustrate what I am seeing.

There must be a way to talk to rust-analyzer, but I can't seem to get over this hurdle.

use std::{
    io::{BufRead, BufReader, BufWriter, Write},
    process::{Command, Stdio},
    thread,
    time::Duration,
};

fn main() {
    let mut ra = Command::new("rust-analyzer")
        //.env("RA_LOG", "trace")
        .stderr(Stdio::piped())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();

    let stdin = ra.stdin.take().expect("Failed to take stdin.");
    let stdout = ra.stdout.take().expect("Failed to take stdout.");
    let stderr = ra.stderr.take().expect("Failed to take stederr");

    let mut stdin = BufWriter::new(stdin);
    let mut stdout = BufReader::new(stdout);
    let mut stderr = BufReader::new(stderr);

    // A thread for Stdout
    let out_h = thread::spawn(move || {
        for i in 0..10 {
            println!("out_iteration: {}", i);
            let mut buf = String::new();
            let _ = stdout.read_line(&mut buf).unwrap();
            println!("out_buf: {}", buf);
            thread::sleep(Duration::from_secs(1));
        }
        println!("No more out_rx iterations");
    });

    // A thread for Stderr
    let err_h = thread::spawn(move || {
        for i in 0..10 {
            println!("err_iteration: {}", i);
            let mut buf = String::new();
            let _ = stderr.read_line(&mut buf).unwrap();
            println!("err_buf: {}", buf);
            thread::sleep(Duration::from_secs(1));
        }
        println!("No more err_rx iterations");
    });

    // Create and send an initialize message to rust-analyzer
    let header = "Content-Length: 164\r\n\r\n";
    let body = r#"{"id":111280,"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"My Clinet","version":"1.0"},"processId":111280,"rootUri":null}}"#;
    stdin.write_all(header.as_bytes()).unwrap();
    stdin.write_all(body.as_bytes()).unwrap();
    stdin.write_all(b"\n").unwrap(); // Does this lead to a broken pipe?
    stdin.flush().unwrap();

    // This code reveals a broken pipe
    thread::sleep(Duration::from_secs(4));
    let header = "Content-Length;: 52\r\n\r\n";
    let body = r#"{"jsonrpc":"2.0","method":"initialized","params":{}}"#;
    stdin.write_all(header.as_bytes()).unwrap();
    stdin.write_all(body.as_bytes()).unwrap();
    stdin.flush().unwrap();

    // Cleanup
    ra.wait().unwrap();
    out_h.join().unwrap();
    err_h.join().unwrap();
}

I read a little bit about the LSP here: Specification

I think you're supposed to read the Content-Length of the response and read that amount of bytes instead of just reading lines. There's no requirement in the LSP to send newlines after each message, or to not include newlines inside a message, so reading the message contents with read_line won't work. Here's a simple example of what I mean

    let out_h = thread::spawn(move || {
        loop {
            // read headers
            let mut content_length = None;
            loop {
                let mut buf = String::new();
                stdout.read_line(&mut buf).unwrap();
                if let Some(content_header) = buf.trim().strip_prefix("Content-Length: ") {
                    content_length = Some(content_header.parse::<usize>().unwrap());
                }
                if buf.trim().is_empty() {
                    // headers over
                    break;
                }
            }

            // read message contents
            let content_length = content_length.expect("Should have read Content-Length");
            let mut buf = vec![0; content_length];
            stdout.read_exact(&mut buf).unwrap();
            println!("got {}", String::from_utf8(buf).unwrap());
        }
    });

4 Likes

Thanks @Heliozoa! Your solution really did the trick :smiley:
Thank you for taking the time to help me figure out that issue.

1 Like

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.