Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Fixed** `vp run` no longer fails while setting up task communication in default Codex CLI and Claude Code sandboxes that block Unix-domain sockets; Unix now uses named FIFOs ([#562](https://github.com/voidzero-dev/vite-task/issues/562)).
- **Fixed** The task cache now supports much larger automatically tracked input sets without hitting wincode's default 4 MiB sequence preallocation limit ([#554](https://github.com/voidzero-dev/vite-task/pull/554)).
- **Fixed** npm workspace patterns beginning with `./` now discover matching packages correctly ([vite-plus#2201](https://github.com/voidzero-dev/vite-plus/issues/2201), [#547](https://github.com/voidzero-dev/vite-task/pull/547)).
- **Fixed** Failures while forwarding output from a started task process no longer incorrectly say the process failed to spawn ([#506](https://github.com/voidzero-dev/vite-task/issues/506)).
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ create the session temp that Claude Code supplies before sandboxed Bash commands

## `srt --settings claude-code-default-sandbox.json vt run inner`

**Exit code:** 1

```
$ vtt print-file input.txt
✗ Failed to set up task communication: Operation not permitted (os error 1)
tracked input
```

## `vtt replace-file-content input.txt tracked modified`
Expand All @@ -29,9 +27,7 @@ $ vtt print-file input.txt

## `srt --settings claude-code-default-sandbox.json vt run inner`

**Exit code:** 1

```
$ vtt print-file input.txt
✗ Failed to set up task communication: Operation not permitted (os error 1)
$ vtt print-file input.txt ○ cache miss: 'input.txt' modified, executing
modified input
```
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The nested `vt` enables fspy for automatic input inference; changing the file re

```
$ vtt print-file input.txt
✗ Failed to set up task communication: Operation not permitted (os error 1)
✗ Failed to spawn process: failed to create IPC channel: Operation not permitted (os error 1)
```

## `vtt replace-file-content input.txt tracked modified`
Expand All @@ -24,5 +24,5 @@ $ vtt print-file input.txt

```
$ vtt print-file input.txt
✗ Failed to set up task communication: Operation not permitted (os error 1)
✗ Failed to spawn process: failed to create IPC channel: Operation not permitted (os error 1)
```
3 changes: 3 additions & 0 deletions crates/vite_task_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ vite_path = { workspace = true }
vite_task_ipc_shared = { workspace = true }
wincode = { workspace = true, features = ["derive"] }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true, features = ["fs"] }

[target.'cfg(windows)'.dependencies]
winapi = { workspace = true, features = ["namedpipeapi"] }

Expand Down
62 changes: 5 additions & 57 deletions crates/vite_task_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use vite_task_ipc_shared::{
};
use wincode::{SchemaRead, config::DefaultConfig};

#[cfg(unix)]
type Stream = std::os::unix::net::UnixStream;
#[cfg(windows)]
type Stream = std::fs::File;
#[doc(hidden)]
pub mod transport;

use transport::Stream;

pub struct Client {
stream: RefCell<Stream>,
Expand Down Expand Up @@ -44,7 +44,7 @@ impl Client {
) -> io::Result<Option<Self>> {
for (name, value) in envs {
if name.as_ref() == IPC_ENV_NAME {
let stream = connect(value.as_ref())?;
let stream = transport::connect(value.as_ref())?;
return Ok(Some(Self::from_stream(stream)));
}
}
Expand Down Expand Up @@ -188,55 +188,3 @@ fn resolve_path(path: &OsStr) -> io::Result<Box<NativeStr>> {
absolute.push(path);
Ok(Box::<NativeStr>::from(absolute.as_absolute_path().as_path().as_os_str()))
}

#[cfg(unix)]
fn connect(name: &OsStr) -> io::Result<Stream> {
std::os::unix::net::UnixStream::connect(name)
}

/// Open a Windows named pipe as a client.
///
/// `OpenOptions::open` on a named-pipe path fails with `ERROR_PIPE_BUSY` when
/// the server's only pending instance has just been claimed by another client
/// — the brief window between the server accepting one connection and creating
/// the next instance. On `ERROR_PIPE_BUSY` we hand off to the kernel via
/// `WaitNamedPipeW`, which blocks until an instance becomes available (or
/// fails if the named pipe is gone). No polling and no arbitrary timeouts.
///
/// This matches what the `interprocess` crate does internally.
#[cfg(windows)]
fn connect(name: &OsStr) -> io::Result<Stream> {
use std::{fs::OpenOptions, os::windows::ffi::OsStrExt};

use winapi::um::namedpipeapi::WaitNamedPipeW;

// ERROR_PIPE_BUSY — see WinError.h. `std::io::Error` does not expose a
// typed constant for this, so the raw OS code is the cleanest test.
const ERROR_PIPE_BUSY: i32 = 231;
// NMPWAIT_WAIT_FOREVER — see WinBase.h. winapi 0.3 doesn't define the
// NMPWAIT_* constants yet (only the comment placeholder).
const NMPWAIT_WAIT_FOREVER: u32 = 0xFFFF_FFFF;

// `WaitNamedPipeW` needs a NUL-terminated UTF-16 path.
let mut wide: Vec<u16> = name.encode_wide().collect();
wide.push(0);

loop {
match OpenOptions::new().read(true).write(true).open(name) {
Ok(file) => return Ok(file),
Err(err) if err.raw_os_error() == Some(ERROR_PIPE_BUSY) => {
// SAFETY: `wide` is NUL-terminated; pointer stays valid for
// the call's duration. `NMPWAIT_WAIT_FOREVER` makes this a
// bounded kernel wait (server's pipe wait-timeout is the
// upper bound on each retry; default ~50ms, then we loop).
let ok = unsafe { WaitNamedPipeW(wide.as_ptr(), NMPWAIT_WAIT_FOREVER) };
if ok == 0 {
return Err(io::Error::last_os_error());
}
// Loop and re-open — another client may have raced us to the
// newly-available instance.
}
Err(err) => return Err(err),
}
}
}
162 changes: 162 additions & 0 deletions crates/vite_task_client/src/transport.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use std::{ffi::OsStr, io};
#[cfg(windows)]
use std::{fs::File, os::windows::ffi::OsStrExt};
#[cfg(unix)]
use std::{
fs::{File, OpenOptions},
io::{Read, Write},
os::unix::fs::OpenOptionsExt,
};

#[cfg(unix)]
use nix::{fcntl::OFlag, sys::stat::Mode, unistd::mkfifo};
#[cfg(unix)]
use vite_task_ipc_shared::fifo::{
ConnectionId, READY_BYTE, connect_fifo, connection_paths, root_from_name,
};

#[cfg(unix)]
pub struct Stream {
reader: File,
writer: File,
}

#[cfg(windows)]
pub type Stream = File;

#[cfg(unix)]
impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
}

#[cfg(unix)]
impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}

fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}

#[cfg(unix)]
/// # Errors
///
/// Returns an error when the FIFO root is invalid, an endpoint cannot be
/// created or opened, or the server does not complete the rendezvous.
pub fn connect(name: &OsStr) -> io::Result<Stream> {
let root = root_from_name(name)?;
let id = ConnectionId::random();
let paths = connection_paths(root, id);
let mode = Mode::S_IRUSR | Mode::S_IWUSR;
mkfifo(paths.request.as_path(), mode).map_err(io::Error::from)?;
mkfifo(paths.response.as_path(), mode).map_err(io::Error::from)?;

// Opening the request FIFO for reading first lets this process open its
// writer without waiting for the server. Keep that bootstrap reader alive
// until the server's one-byte response confirms that both server ends are
// open.
let bootstrap_reader = OpenOptions::new()
.read(true)
.custom_flags(OFlag::O_NONBLOCK.bits())
.open(&paths.request)?;
let writer = OpenOptions::new().write(true).open(&paths.request)?;
let response_bootstrap = OpenOptions::new()
.read(true)
.custom_flags(OFlag::O_NONBLOCK.bits())
.open(&paths.response)?;

let mut rendezvous = OpenOptions::new().write(true).open(connect_fifo(root))?;
write_connection_id(&mut rendezvous, id)?;
drop(rendezvous);

// The bootstrap reader lets the server open its response writer without
// polling. This blocking open then waits until that writer is ready.
let mut reader = OpenOptions::new().read(true).open(&paths.response)?;
drop(response_bootstrap);

let mut ready = [0];
reader.read_exact(&mut ready)?;
if ready[0] != READY_BYTE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid runner IPC rendezvous response",
));
}
drop(bootstrap_reader);

Ok(Stream { reader, writer })
}

#[cfg(unix)]
fn write_connection_id(rendezvous: &mut File, id: ConnectionId) -> io::Result<()> {
// A single fixed-size write is smaller than PIPE_BUF, so concurrent
// clients cannot interleave their connection IDs.
loop {
match rendezvous.write(id.as_bytes()) {
Ok(written) if written == id.as_bytes().len() => return Ok(()),
Ok(_written) => {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"short runner IPC rendezvous write",
));
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err) => return Err(err),
}
}
}

/// Open a Windows named pipe as a client.
///
/// `OpenOptions::open` on a named-pipe path fails with `ERROR_PIPE_BUSY` when
/// the server's only pending instance has just been claimed by another client
/// — the brief window between the server accepting one connection and creating
/// the next instance. On `ERROR_PIPE_BUSY` we hand off to the kernel via
/// `WaitNamedPipeW`, which blocks until an instance becomes available (or
/// fails if the named pipe is gone). No polling and no arbitrary timeouts.
///
/// This matches what the `interprocess` crate does internally.
///
/// # Errors
///
/// Returns an error when the named pipe cannot be opened or waited on.
#[cfg(windows)]
pub fn connect(name: &OsStr) -> io::Result<Stream> {
use std::fs::OpenOptions;

use winapi::um::namedpipeapi::WaitNamedPipeW;

// ERROR_PIPE_BUSY — see WinError.h. `std::io::Error` does not expose a
// typed constant for this, so the raw OS code is the cleanest test.
const ERROR_PIPE_BUSY: i32 = 231;
// NMPWAIT_WAIT_FOREVER — see WinBase.h. winapi 0.3 doesn't define the
// NMPWAIT_* constants yet (only the comment placeholder).
const NMPWAIT_WAIT_FOREVER: u32 = 0xFFFF_FFFF;

// `WaitNamedPipeW` needs a NUL-terminated UTF-16 path.
let mut wide: Vec<u16> = name.encode_wide().collect();
wide.push(0);

loop {
match OpenOptions::new().read(true).write(true).open(name) {
Ok(file) => return Ok(file),
Err(err) if err.raw_os_error() == Some(ERROR_PIPE_BUSY) => {
// SAFETY: `wide` is NUL-terminated; pointer stays valid for
// the call's duration. `NMPWAIT_WAIT_FOREVER` makes this a
// bounded kernel wait (server's pipe wait-timeout is the
// upper bound on each retry; default ~50ms, then we loop).
let ok = unsafe { WaitNamedPipeW(wide.as_ptr(), NMPWAIT_WAIT_FOREVER) };
if ok == 0 {
return Err(io::Error::last_os_error());
}
// Loop and re-open — another client may have raced us to the
// newly-available instance.
}
Err(err) => return Err(err),
}
}
}
4 changes: 4 additions & 0 deletions crates/vite_task_ipc_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ native_str = { workspace = true }
rustc-hash = { workspace = true }
wincode = { workspace = true, features = ["derive"] }

[target.'cfg(unix)'.dependencies]
uuid = { workspace = true, features = ["v4"] }
vite_path = { workspace = true }

[lints]
workspace = true

Expand Down
Loading
Loading