70 %
Chris Biscardi

Rust JavaScript communication over unix domain sockets with Tide

First, a Rust webserver using Tide and async_std listening on a unix domain socket.

main.rs
rust
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
let mut app = tide::new();
let sock = format!("http+unix://{}", "/var/tmp/toaster.sock");
app.at("/").get(|_| async { Ok("Hello, world!") });
app.listen(sock).await?;
Ok(())
}

We can test it by running the server with cargo run main.rs.

curl --unix-socket ./toaster.sock http://localhost/
Hello, world!%

Then in our node app we'll pull in undici.

note: I experienced segfaults on node v11, make sure you use something like v14 with undici

JS
const undici = require("undici");
main();
async function main() {
const client = new undici.Client(`http://localhost`, {
socketPath: "/var/tmp/toaster.sock",
});
const res = await client.request({ path: "/", method: "GET" });
res.body.on("data", (buf) => console.log(buf.toString()));
client.close();
}

With the cargo run main.rs server running, we can hit the socket with node.

node index.js
Hello, world!

Spawning node from Rust

This is great and we want to take it further. We want to be able to spawn node.js processes from Rust and have them communicate back. To do this we'll take further advantage of Rust's async support in our server. We take advantage of try_join!

rust
use async_std::task;
use futures::try_join;
use std::io::{self, Write};
use std::process::Command;
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
// server creation
let mut app = tide::new();
let sock = format!("http+unix://{}", "/var/tmp/toaster.sock");
app.at("/").get(|_| async { Ok("Hello, world!") });
let server = app.listen(sock);
// node script creation
let child = task::spawn(async {
let cmd = Command::new("node").arg("index.js").output();
io::stdout().write_all(&cmd.unwrap().stdout).unwrap();
Ok(())
});
// results of both futures
// result is Result<((),()), Error>
let result = try_join!(server, child);
match result {
Err(e) => Err(e),
Ok(_) => Ok(()),
}
}

We don't need to modify our node client at all. The output comes back through the io stdout write that we're putting the node subcommand output into.

➜ cargo run src/main.rs
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/server src/main.rs`
Hello, world!

The server then hangs, waiting for any other requests which we can continue to send it. Any other requests don't result in console output on the server though, since we're only logging the stdout from our node process.

Misc

We aren't handling the removal of the sock file after running the server, so if we run it twice we get an error returned Address already in use.

rust
Err(Os { code: 48, kind: AddrInUse, message: "Address already in use" })

You will have to rm /var/tmp/toaster.sock in this case before starting the server again. We could handle this in the future (pun intended).