mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
uds: add async Unix socket crate (#18254)
## Summary - add a codex-uds crate with async UnixListener and UnixStream wrappers - expose helpers for private socket directory setup and stale socket path checks - migrate codex-stdio-to-uds onto codex-uds and Tokio-based stdio/socket relaying - update the CLI stdio-to-uds command path for the async runner ## Tests - cargo test -p codex-uds -p codex-stdio-to-uds - cargo test -p codex-cli - just fmt - just fix -p codex-uds - just fix -p codex-stdio-to-uds - just fix -p codex-cli - just bazel-lock-check - git diff --check
This commit is contained in:
1 parent
1029742cf7
commit
97d4b42583
12 files changed
+641
-133
No files matched your search
Generated
+15
-1
@@ -2891,10 +2891,11 @@ name = "codex-stdio-to-uds"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"codex-uds",
|
||||||
"codex-utils-cargo-bin",
|
"codex-utils-cargo-bin",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"uds_windows",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3058,6 +3059,18 @@ dependencies = [
|
|||||||
"winsplit",
|
"winsplit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codex-uds"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"pretty_assertions",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"uds_windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codex-utils-absolute-path"
|
name = "codex-utils-absolute-path"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@@ -10970,6 +10983,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ members = [
|
|||||||
"terminal-detection",
|
"terminal-detection",
|
||||||
"test-binary-support",
|
"test-binary-support",
|
||||||
"thread-store",
|
"thread-store",
|
||||||
|
"uds",
|
||||||
"codex-experimental-api-macros",
|
"codex-experimental-api-macros",
|
||||||
"plugin",
|
"plugin",
|
||||||
"model-provider",
|
"model-provider",
|
||||||
@@ -175,6 +176,7 @@ codex-test-binary-support = { path = "test-binary-support" }
|
|||||||
codex-thread-store = { path = "thread-store" }
|
codex-thread-store = { path = "thread-store" }
|
||||||
codex-tools = { path = "tools" }
|
codex-tools = { path = "tools" }
|
||||||
codex-tui = { path = "tui" }
|
codex-tui = { path = "tui" }
|
||||||
|
codex-uds = { path = "uds" }
|
||||||
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||||
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
||||||
codex-utils-cache = { path = "utils/cache" }
|
codex-utils-cache = { path = "utils/cache" }
|
||||||
@@ -212,6 +214,7 @@ arc-swap = "1.9.0"
|
|||||||
assert_cmd = "2"
|
assert_cmd = "2"
|
||||||
assert_matches = "1.5.0"
|
assert_matches = "1.5.0"
|
||||||
async-channel = "2.3.1"
|
async-channel = "2.3.1"
|
||||||
|
async-io = "2.6.0"
|
||||||
async-stream = "0.3.6"
|
async-stream = "0.3.6"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
axum = { version = "0.8", default-features = false }
|
axum = { version = "0.8", default-features = false }
|
||||||
|
|||||||
@@ -1090,8 +1090,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
"stdio-to-uds",
|
"stdio-to-uds",
|
||||||
)?;
|
)?;
|
||||||
let socket_path = cmd.socket_path;
|
let socket_path = cmd.socket_path;
|
||||||
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
|
codex_stdio_to_uds::run(socket_path.as_path()).await?;
|
||||||
.await??;
|
|
||||||
}
|
}
|
||||||
Some(Subcommand::ExecServer(cmd)) => {
|
Some(Subcommand::ExecServer(cmd)) => {
|
||||||
reject_remote_mode_for_subcommand(
|
reject_remote_mode_for_subcommand(
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
codex-uds = { workspace = true }
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
tokio = { workspace = true, features = [
|
||||||
uds_windows = { workspace = true }
|
"io-std",
|
||||||
|
"io-util",
|
||||||
|
"macros",
|
||||||
|
"rt-multi-thread",
|
||||||
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
codex-utils-cargo-bin = { workspace = true }
|
codex-utils-cargo-bin = { workspace = true }
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ Unfortunately, the Rust standard library does not provide support for UNIX domai
|
|||||||
|
|
||||||
https://github.com/rust-lang/rust/issues/56533
|
https://github.com/rust-lang/rust/issues/56533
|
||||||
|
|
||||||
As a workaround, this crate leverages https://crates.io/crates/uds_windows as a dependency on Windows.
|
As a workaround, this crate uses `codex-uds`, which provides a cross-platform async UDS API backed by https://crates.io/crates/uds_windows on Windows.
|
||||||
@@ -1,56 +1,46 @@
|
|||||||
#![deny(clippy::print_stdout)]
|
#![deny(clippy::print_stdout)]
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
|
||||||
use std::net::Shutdown;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::thread;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::anyhow;
|
use codex_uds::UnixStream;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::net::UnixStream;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use uds_windows::UnixStream;
|
|
||||||
|
|
||||||
/// Connects to the Unix Domain Socket at `socket_path` and relays data between
|
/// Connects to the Unix Domain Socket at `socket_path` and relays data between
|
||||||
/// standard input/output and the socket.
|
/// standard input/output and the socket.
|
||||||
pub fn run(socket_path: &Path) -> anyhow::Result<()> {
|
pub async fn run(socket_path: &Path) -> anyhow::Result<()> {
|
||||||
let mut stream = UnixStream::connect(socket_path)
|
let stream = UnixStream::connect(socket_path)
|
||||||
|
.await
|
||||||
.with_context(|| format!("failed to connect to socket at {}", socket_path.display()))?;
|
.with_context(|| format!("failed to connect to socket at {}", socket_path.display()))?;
|
||||||
|
let (mut socket_reader, mut socket_writer) = tokio::io::split(stream);
|
||||||
|
|
||||||
let mut reader = stream
|
let copy_socket_to_stdout = async {
|
||||||
.try_clone()
|
let mut stdout = tokio::io::stdout();
|
||||||
.context("failed to clone socket for reading")?;
|
tokio::io::copy(&mut socket_reader, &mut stdout).await?;
|
||||||
|
stdout.flush().await?;
|
||||||
let stdout_thread = thread::spawn(move || -> io::Result<()> {
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let mut handle = stdout.lock();
|
|
||||||
io::copy(&mut reader, &mut handle)?;
|
|
||||||
handle.flush()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
};
|
||||||
|
let copy_stdin_to_socket = async {
|
||||||
|
let mut stdin = tokio::io::stdin();
|
||||||
|
tokio::io::copy(&mut stdin, &mut socket_writer)
|
||||||
|
.await
|
||||||
|
.context("failed to copy data from stdin to socket")?;
|
||||||
|
|
||||||
let stdin = io::stdin();
|
// The peer can close immediately after sending its response; in that
|
||||||
{
|
// race, half-closing our write side can report NotConnected on some
|
||||||
let mut handle = stdin.lock();
|
// platforms.
|
||||||
io::copy(&mut handle, &mut stream).context("failed to copy data from stdin to socket")?;
|
if let Err(err) = socket_writer.shutdown().await
|
||||||
}
|
&& err.kind() != io::ErrorKind::NotConnected
|
||||||
|
{
|
||||||
|
return Err(err).context("failed to shutdown socket writer");
|
||||||
|
}
|
||||||
|
|
||||||
// The peer can close immediately after sending its response; in that race,
|
anyhow::Ok(())
|
||||||
// half-closing our write side can report NotConnected on some platforms.
|
};
|
||||||
if let Err(err) = stream.shutdown(Shutdown::Write)
|
|
||||||
&& err.kind() != io::ErrorKind::NotConnected
|
|
||||||
{
|
|
||||||
return Err(err).context("failed to shutdown socket writer");
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout_result = stdout_thread
|
tokio::try_join!(copy_stdin_to_socket, copy_socket_to_stdout)
|
||||||
.join()
|
.context("failed to relay data between stdio and socket")?;
|
||||||
.map_err(|_| anyhow!("thread panicked while copying socket data to stdout"))?;
|
|
||||||
stdout_result.context("failed to copy data from socket to stdout")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@ use std::env;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
let mut args = env::args_os().skip(1);
|
let mut args = env::args_os().skip(1);
|
||||||
let Some(socket_path) = args.next() else {
|
let Some(socket_path) = args.next() else {
|
||||||
eprintln!("Usage: codex-stdio-to-uds <socket-path>");
|
eprintln!("Usage: codex-stdio-to-uds <socket-path>");
|
||||||
@@ -15,5 +16,5 @@ fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let socket_path = PathBuf::from(socket_path);
|
let socket_path = PathBuf::from(socket_path);
|
||||||
codex_stdio_to_uds::run(&socket_path)
|
codex_stdio_to_uds::run(&socket_path).await
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::io::Write;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::process::ExitStatus;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
@@ -9,17 +9,13 @@ use std::time::Duration;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::anyhow;
|
use codex_uds::UnixListener;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[tokio::test]
|
||||||
use std::os::unix::net::UnixListener;
|
async fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use uds_windows::UnixListener;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
|
|
||||||
// This test intentionally avoids `read_to_end()` on the server side because
|
// This test intentionally avoids `read_to_end()` on the server side because
|
||||||
// waiting for EOF can race with socket half-close behavior on slower runners.
|
// waiting for EOF can race with socket half-close behavior on slower runners.
|
||||||
// Reading the exact request length keeps the test deterministic.
|
// Reading the exact request length keeps the test deterministic.
|
||||||
@@ -32,7 +28,7 @@ fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
|
|||||||
let request = b"request";
|
let request = b"request";
|
||||||
let request_path = dir.path().join("request.txt");
|
let request_path = dir.path().join("request.txt");
|
||||||
std::fs::write(&request_path, request).context("failed to write child stdin fixture")?;
|
std::fs::write(&request_path, request).context("failed to write child stdin fixture")?;
|
||||||
let listener = match UnixListener::bind(&socket_path) {
|
let listener = match UnixListener::bind(&socket_path).await {
|
||||||
Ok(listener) => listener,
|
Ok(listener) => listener,
|
||||||
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
|
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
|
||||||
eprintln!("skipping test: failed to bind unix socket: {err}");
|
eprintln!("skipping test: failed to bind unix socket: {err}");
|
||||||
@@ -43,105 +39,119 @@ fn pipes_stdin_and_stdout_through_socket() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
let (event_tx, event_rx) = mpsc::channel();
|
let (event_tx, event_rx) = mpsc::channel();
|
||||||
let server_thread = thread::spawn(move || -> anyhow::Result<()> {
|
let server_task = tokio::spawn(async move {
|
||||||
|
let mut listener = listener;
|
||||||
let _ = event_tx.send("waiting for accept".to_string());
|
let _ = event_tx.send("waiting for accept".to_string());
|
||||||
let (mut connection, _) = listener
|
let mut connection = listener
|
||||||
.accept()
|
.accept()
|
||||||
|
.await
|
||||||
.context("failed to accept test connection")?;
|
.context("failed to accept test connection")?;
|
||||||
let _ = event_tx.send("accepted connection".to_string());
|
let _ = event_tx.send("accepted connection".to_string());
|
||||||
let mut received = vec![0; request.len()];
|
let mut received = vec![0; request.len()];
|
||||||
connection
|
connection
|
||||||
.read_exact(&mut received)
|
.read_exact(&mut received)
|
||||||
|
.await
|
||||||
.context("failed to read data from client")?;
|
.context("failed to read data from client")?;
|
||||||
let _ = event_tx.send(format!("read {} bytes", received.len()));
|
let _ = event_tx.send(format!("read {} bytes", received.len()));
|
||||||
tx.send(received)
|
|
||||||
.map_err(|_| anyhow!("failed to send received bytes to test thread"))?;
|
|
||||||
connection
|
connection
|
||||||
.write_all(b"response")
|
.write_all(b"response")
|
||||||
|
.await
|
||||||
.context("failed to write response to client")?;
|
.context("failed to write response to client")?;
|
||||||
let _ = event_tx.send("wrote response".to_string());
|
let _ = event_tx.send("wrote response".to_string());
|
||||||
Ok(())
|
anyhow::Ok(received)
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdin = std::fs::File::open(&request_path).context("failed to open child stdin fixture")?;
|
struct ChildOutput {
|
||||||
let mut child = Command::new(codex_utils_cargo_bin::cargo_bin("codex-stdio-to-uds")?)
|
status: ExitStatus,
|
||||||
.arg(&socket_path)
|
stdout: Vec<u8>,
|
||||||
.stdin(Stdio::from(stdin))
|
stderr: Vec<u8>,
|
||||||
.stdout(Stdio::piped())
|
server_events: Vec<String>,
|
||||||
.stderr(Stdio::piped())
|
}
|
||||||
.spawn()
|
|
||||||
.context("failed to spawn codex-stdio-to-uds")?;
|
|
||||||
|
|
||||||
let mut child_stdout = child.stdout.take().context("missing child stdout")?;
|
let child_task = tokio::task::spawn_blocking(move || -> anyhow::Result<ChildOutput> {
|
||||||
let mut child_stderr = child.stderr.take().context("missing child stderr")?;
|
let stdin =
|
||||||
let (stdout_tx, stdout_rx) = mpsc::channel();
|
std::fs::File::open(&request_path).context("failed to open child stdin fixture")?;
|
||||||
let (stderr_tx, stderr_rx) = mpsc::channel();
|
let mut child = Command::new(codex_utils_cargo_bin::cargo_bin("codex-stdio-to-uds")?)
|
||||||
thread::spawn(move || {
|
.arg(&socket_path)
|
||||||
let mut stdout = Vec::new();
|
.stdin(Stdio::from(stdin))
|
||||||
let result = child_stdout.read_to_end(&mut stdout).map(|_| stdout);
|
.stdout(Stdio::piped())
|
||||||
let _ = stdout_tx.send(result);
|
.stderr(Stdio::piped())
|
||||||
});
|
.spawn()
|
||||||
thread::spawn(move || {
|
.context("failed to spawn codex-stdio-to-uds")?;
|
||||||
let mut stderr = Vec::new();
|
|
||||||
let result = child_stderr.read_to_end(&mut stderr).map(|_| stderr);
|
let mut child_stdout = child.stdout.take().context("missing child stdout")?;
|
||||||
let _ = stderr_tx.send(result);
|
let mut child_stderr = child.stderr.take().context("missing child stderr")?;
|
||||||
|
let (stdout_tx, stdout_rx) = mpsc::channel();
|
||||||
|
let (stderr_tx, stderr_rx) = mpsc::channel();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut stdout = Vec::new();
|
||||||
|
let result = child_stdout.read_to_end(&mut stdout).map(|_| stdout);
|
||||||
|
let _ = stdout_tx.send(result);
|
||||||
|
});
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut stderr = Vec::new();
|
||||||
|
let result = child_stderr.read_to_end(&mut stderr).map(|_| stderr);
|
||||||
|
let _ = stderr_tx.send(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut server_events = Vec::new();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
let status = loop {
|
||||||
|
while let Ok(event) = event_rx.try_recv() {
|
||||||
|
server_events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = child.try_wait().context("failed to poll child status")? {
|
||||||
|
break status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
let stderr = stderr_rx
|
||||||
|
.recv_timeout(Duration::from_secs(1))
|
||||||
|
.context("timed out waiting for child stderr after kill")?
|
||||||
|
.context("failed to read child stderr")?;
|
||||||
|
anyhow::bail!(
|
||||||
|
"codex-stdio-to-uds did not exit in time; server events: {:?}; stderr: {}",
|
||||||
|
server_events,
|
||||||
|
String::from_utf8_lossy(&stderr).trim_end()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(25));
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdout = stdout_rx
|
||||||
|
.recv_timeout(Duration::from_secs(1))
|
||||||
|
.context("timed out waiting for child stdout")?
|
||||||
|
.context("failed to read child stdout")?;
|
||||||
|
let stderr = stderr_rx
|
||||||
|
.recv_timeout(Duration::from_secs(1))
|
||||||
|
.context("timed out waiting for child stderr")?
|
||||||
|
.context("failed to read child stderr")?;
|
||||||
|
|
||||||
|
Ok(ChildOutput {
|
||||||
|
status,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
server_events,
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut server_events = Vec::new();
|
let child_output = child_task.await.context("child task panicked")??;
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
|
||||||
let status = loop {
|
|
||||||
while let Ok(event) = event_rx.try_recv() {
|
|
||||||
server_events.push(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(status) = child.try_wait().context("failed to poll child status")? {
|
|
||||||
break status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if Instant::now() >= deadline {
|
|
||||||
let _ = child.kill();
|
|
||||||
let _ = child.wait();
|
|
||||||
let stderr = stderr_rx
|
|
||||||
.recv_timeout(Duration::from_secs(1))
|
|
||||||
.context("timed out waiting for child stderr after kill")?
|
|
||||||
.context("failed to read child stderr")?;
|
|
||||||
anyhow::bail!(
|
|
||||||
"codex-stdio-to-uds did not exit in time; server events: {:?}; stderr: {}",
|
|
||||||
server_events,
|
|
||||||
String::from_utf8_lossy(&stderr).trim_end()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(25));
|
|
||||||
};
|
|
||||||
|
|
||||||
let stdout = stdout_rx
|
|
||||||
.recv_timeout(Duration::from_secs(1))
|
|
||||||
.context("timed out waiting for child stdout")?
|
|
||||||
.context("failed to read child stdout")?;
|
|
||||||
let stderr = stderr_rx
|
|
||||||
.recv_timeout(Duration::from_secs(1))
|
|
||||||
.context("timed out waiting for child stderr")?
|
|
||||||
.context("failed to read child stderr")?;
|
|
||||||
assert!(
|
assert!(
|
||||||
status.success(),
|
child_output.status.success(),
|
||||||
"codex-stdio-to-uds exited with {status}; server events: {:?}; stderr: {}",
|
"codex-stdio-to-uds exited with {status}; server events: {:?}; stderr: {}",
|
||||||
server_events,
|
child_output.server_events,
|
||||||
String::from_utf8_lossy(&stderr).trim_end()
|
String::from_utf8_lossy(&child_output.stderr).trim_end(),
|
||||||
|
status = child_output.status
|
||||||
);
|
);
|
||||||
assert_eq!(stdout, b"response");
|
assert_eq!(child_output.stdout, b"response");
|
||||||
|
|
||||||
let received = rx
|
let received = server_task.await.context("server task panicked")??;
|
||||||
.recv_timeout(Duration::from_secs(1))
|
|
||||||
.context("server did not receive data in time")?;
|
|
||||||
assert_eq!(received, request);
|
assert_eq!(received, request);
|
||||||
|
|
||||||
let server_result = server_thread
|
|
||||||
.join()
|
|
||||||
.map_err(|_| anyhow!("server thread panicked"))?;
|
|
||||||
server_result.context("server failed")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
load("//:defs.bzl", "codex_rust_crate")
|
||||||
|
|
||||||
|
codex_rust_crate(
|
||||||
|
name = "uds",
|
||||||
|
crate_name = "codex_uds",
|
||||||
|
)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "codex-uds"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "codex_uds"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { workspace = true, features = ["fs", "net", "rt"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
async-io = { workspace = true }
|
||||||
|
tokio-util = { workspace = true, features = ["compat"] }
|
||||||
|
uds_windows = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = [
|
||||||
|
"io-util",
|
||||||
|
"macros",
|
||||||
|
"rt-multi-thread",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
//! Cross-platform async Unix domain socket helpers.
|
||||||
|
|
||||||
|
use std::io::Result as IoResult;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::Context;
|
||||||
|
use std::task::Poll;
|
||||||
|
|
||||||
|
use tokio::io::AsyncRead;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
use tokio::io::ReadBuf;
|
||||||
|
|
||||||
|
/// Creates `socket_dir` if needed and restricts it to the current user where
|
||||||
|
/// the platform exposes Unix permissions.
|
||||||
|
pub async fn prepare_private_socket_directory(socket_dir: impl AsRef<Path>) -> IoResult<()> {
|
||||||
|
platform::prepare_private_socket_directory(socket_dir.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether `socket_path` points at a stale Unix socket rendezvous path.
|
||||||
|
///
|
||||||
|
/// On Unix this checks the file type. On Windows, `uds_windows` represents the
|
||||||
|
/// rendezvous as a regular path, so existence is the only useful stale-path
|
||||||
|
/// signal available.
|
||||||
|
pub async fn is_stale_socket_path(socket_path: impl AsRef<Path>) -> IoResult<bool> {
|
||||||
|
platform::is_stale_socket_path(socket_path.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async Unix domain socket listener.
|
||||||
|
pub struct UnixListener {
|
||||||
|
inner: platform::Listener,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnixListener {
|
||||||
|
/// Binds a new listener at `socket_path`.
|
||||||
|
pub async fn bind(socket_path: impl AsRef<Path>) -> IoResult<Self> {
|
||||||
|
platform::bind_listener(socket_path.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|inner| Self { inner })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepts the next incoming stream.
|
||||||
|
pub async fn accept(&mut self) -> IoResult<UnixStream> {
|
||||||
|
self.inner.accept().await.map(|inner| UnixStream { inner })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Async Unix domain socket stream.
|
||||||
|
pub struct UnixStream {
|
||||||
|
inner: platform::Stream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnixStream {
|
||||||
|
/// Connects to `socket_path`.
|
||||||
|
pub async fn connect(socket_path: impl AsRef<Path>) -> IoResult<Self> {
|
||||||
|
platform::connect_stream(socket_path.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|inner| Self { inner })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for UnixStream {
|
||||||
|
fn poll_read(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<IoResult<()>> {
|
||||||
|
Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for UnixStream {
|
||||||
|
fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<IoResult<usize>> {
|
||||||
|
Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||||
|
Pin::new(&mut self.get_mut().inner).poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||||
|
Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod platform {
|
||||||
|
use std::io;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::io::Result as IoResult;
|
||||||
|
use std::os::unix::fs::FileTypeExt;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::net::UnixListener;
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
|
/// Owner-only access keeps the control socket directory private while
|
||||||
|
/// preserving owner traversal and socket path creation.
|
||||||
|
const SOCKET_DIR_MODE: u32 = 0o700;
|
||||||
|
const SOCKET_DIR_PERMISSION_BITS: u32 = 0o777;
|
||||||
|
|
||||||
|
pub(super) type Stream = UnixStream;
|
||||||
|
|
||||||
|
pub(super) struct Listener(UnixListener);
|
||||||
|
|
||||||
|
pub(super) async fn prepare_private_socket_directory(socket_dir: &Path) -> IoResult<()> {
|
||||||
|
let mut dir_builder = fs::DirBuilder::new();
|
||||||
|
dir_builder.mode(SOCKET_DIR_MODE);
|
||||||
|
match dir_builder.create(socket_dir).await {
|
||||||
|
Ok(()) => return Ok(()),
|
||||||
|
Err(err) if err.kind() == ErrorKind::AlreadyExists => {}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = fs::symlink_metadata(socket_dir).await?;
|
||||||
|
if !metadata.is_dir() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
ErrorKind::AlreadyExists,
|
||||||
|
format!(
|
||||||
|
"socket directory path exists and is not a directory: {}",
|
||||||
|
socket_dir.display()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let permissions = metadata.permissions();
|
||||||
|
// The SSH-over-UDS control socket is reachable by path, so the
|
||||||
|
// rendezvous directory must be owner-traversable while denying
|
||||||
|
// group/other access; exact 0700 fixes insecure modes and unusable
|
||||||
|
// owner-only modes like 0600.
|
||||||
|
if permissions.mode() & SOCKET_DIR_PERMISSION_BITS != SOCKET_DIR_MODE {
|
||||||
|
fs::set_permissions(socket_dir, std::fs::Permissions::from_mode(SOCKET_DIR_MODE))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn bind_listener(socket_path: &Path) -> IoResult<Listener> {
|
||||||
|
UnixListener::bind(socket_path).map(Listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Listener {
|
||||||
|
pub(super) async fn accept(&mut self) -> IoResult<Stream> {
|
||||||
|
self.0.accept().await.map(|(stream, _addr)| stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn connect_stream(socket_path: &Path) -> IoResult<Stream> {
|
||||||
|
UnixStream::connect(socket_path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn is_stale_socket_path(socket_path: &Path) -> IoResult<bool> {
|
||||||
|
Ok(fs::symlink_metadata(socket_path)
|
||||||
|
.await?
|
||||||
|
.file_type()
|
||||||
|
.is_socket())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod platform {
|
||||||
|
use std::io;
|
||||||
|
use std::io::Result as IoResult;
|
||||||
|
use std::net::Shutdown;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::os::windows::io::AsRawSocket;
|
||||||
|
use std::os::windows::io::AsSocket;
|
||||||
|
use std::os::windows::io::BorrowedSocket;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::Context;
|
||||||
|
use std::task::Poll;
|
||||||
|
use std::task::ready;
|
||||||
|
|
||||||
|
use async_io::Async;
|
||||||
|
use tokio::io::AsyncRead;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
use tokio::io::ReadBuf;
|
||||||
|
use tokio::task;
|
||||||
|
use tokio_util::compat::Compat;
|
||||||
|
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||||
|
|
||||||
|
pub(super) struct Stream(Compat<Async<WindowsUnixStream>>);
|
||||||
|
|
||||||
|
pub(super) async fn prepare_private_socket_directory(socket_dir: &Path) -> IoResult<()> {
|
||||||
|
tokio::fs::create_dir_all(socket_dir).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct Listener(Async<WindowsUnixListener>);
|
||||||
|
|
||||||
|
pub(super) async fn bind_listener(socket_path: &Path) -> IoResult<Listener> {
|
||||||
|
let socket_path = socket_path.to_path_buf();
|
||||||
|
let listener =
|
||||||
|
spawn_blocking_io(move || uds_windows::UnixListener::bind(socket_path)).await?;
|
||||||
|
Async::new(WindowsUnixListener::from(listener)).map(Listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Listener {
|
||||||
|
pub(super) async fn accept(&mut self) -> IoResult<Stream> {
|
||||||
|
let (stream, _addr) = self.0.read_with(|listener| listener.accept()).await?;
|
||||||
|
Async::new(WindowsUnixStream::from(stream))
|
||||||
|
.map(FuturesAsyncReadCompatExt::compat)
|
||||||
|
.map(Stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn connect_stream(socket_path: &Path) -> IoResult<Stream> {
|
||||||
|
let socket_path = socket_path.to_path_buf();
|
||||||
|
let stream =
|
||||||
|
spawn_blocking_io(move || uds_windows::UnixStream::connect(socket_path)).await?;
|
||||||
|
Async::new(WindowsUnixStream::from(stream))
|
||||||
|
.map(FuturesAsyncReadCompatExt::compat)
|
||||||
|
.map(Stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn is_stale_socket_path(socket_path: &Path) -> IoResult<bool> {
|
||||||
|
tokio::fs::try_exists(socket_path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_blocking_io<T>(
|
||||||
|
operation: impl FnOnce() -> IoResult<T> + Send + 'static,
|
||||||
|
) -> IoResult<T>
|
||||||
|
where
|
||||||
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
task::spawn_blocking(operation)
|
||||||
|
.await
|
||||||
|
.map_err(|err| io::Error::other(format!("blocking socket task failed: {err}")))?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct WindowsUnixListener(uds_windows::UnixListener);
|
||||||
|
|
||||||
|
impl From<uds_windows::UnixListener> for WindowsUnixListener {
|
||||||
|
fn from(listener: uds_windows::UnixListener) -> Self {
|
||||||
|
Self(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for WindowsUnixListener {
|
||||||
|
type Target = uds_windows::UnixListener;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsSocket for WindowsUnixListener {
|
||||||
|
fn as_socket(&self) -> BorrowedSocket<'_> {
|
||||||
|
unsafe { BorrowedSocket::borrow_raw(self.as_raw_socket()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct WindowsUnixStream(uds_windows::UnixStream);
|
||||||
|
|
||||||
|
impl From<uds_windows::UnixStream> for WindowsUnixStream {
|
||||||
|
fn from(stream: uds_windows::UnixStream) -> Self {
|
||||||
|
Self(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for WindowsUnixStream {
|
||||||
|
type Target = uds_windows::UnixStream;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsSocket for WindowsUnixStream {
|
||||||
|
fn as_socket(&self) -> BorrowedSocket<'_> {
|
||||||
|
unsafe { BorrowedSocket::borrow_raw(self.as_raw_socket()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for WindowsUnixStream {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
|
||||||
|
io::Read::read(&mut self.0, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for WindowsUnixStream {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
|
||||||
|
io::Write::write(&mut self.0, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> IoResult<()> {
|
||||||
|
io::Write::flush(&mut self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for Stream {
|
||||||
|
fn poll_read(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<IoResult<()>> {
|
||||||
|
Pin::new(&mut self.get_mut().0).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for Stream {
|
||||||
|
fn poll_write(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<IoResult<usize>> {
|
||||||
|
Pin::new(&mut self.get_mut().0).poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||||
|
Pin::new(&mut self.get_mut().0).poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||||
|
let stream = &mut self.get_mut().0;
|
||||||
|
ready!(Pin::new(&mut *stream).poll_flush(cx))?;
|
||||||
|
// `Compat<Async<_>>` maps shutdown to `poll_close()`, which only
|
||||||
|
// flushes for `async_io::Async`; call the socket shutdown directly.
|
||||||
|
stream.get_ref().get_ref().shutdown(Shutdown::Write)?;
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl async_io::IoSafe for WindowsUnixListener {}
|
||||||
|
unsafe impl async_io::IoSafe for WindowsUnixStream {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod lib_tests;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prepare_private_socket_directory_creates_directory() {
|
||||||
|
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||||
|
let socket_dir = temp_dir.path().join("app-server-control");
|
||||||
|
|
||||||
|
prepare_private_socket_directory(&socket_dir)
|
||||||
|
.await
|
||||||
|
.expect("socket dir should be created");
|
||||||
|
|
||||||
|
assert!(socket_dir.is_dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prepare_private_socket_directory_sets_existing_permissions_to_owner_only() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||||
|
for mode in [0o755, 0o600] {
|
||||||
|
let socket_dir = temp_dir.path().join(format!("app-server-control-{mode:o}"));
|
||||||
|
std::fs::create_dir(&socket_dir).expect("socket dir should be created");
|
||||||
|
std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(mode))
|
||||||
|
.expect("socket dir permissions should be changed");
|
||||||
|
|
||||||
|
prepare_private_socket_directory(&socket_dir)
|
||||||
|
.await
|
||||||
|
.expect("socket dir permissions should be set exactly");
|
||||||
|
|
||||||
|
let mode = std::fs::metadata(&socket_dir)
|
||||||
|
.expect("socket dir metadata")
|
||||||
|
.permissions()
|
||||||
|
.mode();
|
||||||
|
assert_eq!(mode & 0o777, 0o700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn regular_file_path_is_not_stale_socket_path() {
|
||||||
|
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||||
|
let regular_file = temp_dir.path().join("not-a-socket");
|
||||||
|
std::fs::write(®ular_file, b"not a socket").expect("regular file should be created");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!is_stale_socket_path(®ular_file)
|
||||||
|
.await
|
||||||
|
.expect("stale socket check should succeed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn bound_listener_path_is_stale_socket_path() {
|
||||||
|
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||||
|
let socket_path = temp_dir.path().join("socket");
|
||||||
|
let _listener = match UnixListener::bind(&socket_path).await {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
|
||||||
|
eprintln!("skipping test: failed to bind unix socket: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => panic!("failed to bind test socket: {err}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
is_stale_socket_path(&socket_path)
|
||||||
|
.await
|
||||||
|
.expect("stale socket check should succeed")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn stream_round_trips_data_between_listener_and_client() {
|
||||||
|
let temp_dir = tempfile::TempDir::new().expect("temp dir");
|
||||||
|
let socket_path = temp_dir.path().join("socket");
|
||||||
|
let mut listener = match UnixListener::bind(&socket_path).await {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
|
||||||
|
eprintln!("skipping test: failed to bind unix socket: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(err) => panic!("failed to bind test socket: {err}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let mut server_stream = listener.accept().await.expect("connection should accept");
|
||||||
|
let mut request = [0; 7];
|
||||||
|
server_stream
|
||||||
|
.read_exact(&mut request)
|
||||||
|
.await
|
||||||
|
.expect("server should read request");
|
||||||
|
assert_eq!(&request, b"request");
|
||||||
|
server_stream
|
||||||
|
.write_all(b"response")
|
||||||
|
.await
|
||||||
|
.expect("server should write response");
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut client_stream = UnixStream::connect(&socket_path)
|
||||||
|
.await
|
||||||
|
.expect("client should connect");
|
||||||
|
client_stream
|
||||||
|
.write_all(b"request")
|
||||||
|
.await
|
||||||
|
.expect("client should write request");
|
||||||
|
let mut response = [0; 8];
|
||||||
|
client_stream
|
||||||
|
.read_exact(&mut response)
|
||||||
|
.await
|
||||||
|
.expect("client should read response");
|
||||||
|
assert_eq!(&response, b"response");
|
||||||
|
|
||||||
|
server_task.await.expect("server task should join");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user