Skip to content
Open
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** Automatic file-access tracking now works inside coding-agent sandboxes, including the default Codex CLI and Claude Code sandboxes ([#563](https://github.com/voidzero-dev/vite-task/issues/563), [#576](https://github.com/voidzero-dev/vite-task/pull/576)).
- **Added** Tasks now run with `VP_RUN=1` set, so tools can tell they are running under `vp run` instead of being invoked directly ([#570](https://github.com/voidzero-dev/vite-task/pull/570)).
- **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)).
Expand Down
16 changes: 0 additions & 16 deletions Cargo.lock

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

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ jsonc-parser = { version = "0.32.0", features = ["serde"] }
libc = "0.2.185"
libtest-mimic = "0.8.2"
memmap2 = "0.9.11"
memfd = "0.6.5"
monostate = "1.0.2"
napi = "3"
napi-build = "2"
Expand Down
89 changes: 24 additions & 65 deletions crates/fspy_shared/src/ipc/channel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,22 @@

mod shm_io;

use std::{env::temp_dir, fs::File, io, mem::MaybeUninit, ops::Deref, path::PathBuf, sync::Arc};
use std::{env::temp_dir, fs::File, io, ops::Deref, path::PathBuf};

use fspy_shm::Shm;
use fspy_shm::{Mapping, ShmKeeper};
pub use shm_io::FrameMut;
use shm_io::{ShmReader, ShmWriter};
use tracing::debug;
use uuid::Uuid;
use wincode::{
SchemaRead, SchemaWrite,
config::Config,
error::{ReadResult, WriteResult},
io::{Reader, Writer},
};
use wincode::{SchemaRead, SchemaWrite};

use super::NativeStr;

/// wincode schema adapter for `Arc<str>`, which is a foreign type with unsized inner.
pub(crate) struct ArcStrSchema;

// SAFETY: Delegates to `str`'s SchemaWrite impl, preserving its size/write invariants.
unsafe impl<C: Config> SchemaWrite<C> for ArcStrSchema {
type Src = Arc<str>;

fn size_of(src: &Self::Src) -> WriteResult<usize> {
<str as SchemaWrite<C>>::size_of(src)
}

fn write(writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
<str as SchemaWrite<C>>::write(writer, src)
}
}

// SAFETY: Delegates to `&str`'s SchemaRead impl; dst is initialized on Ok.
unsafe impl<'de, C: Config> SchemaRead<'de, C> for ArcStrSchema {
type Dst = Arc<str>;

fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
let s: &str = <&str as SchemaRead<'de, C>>::get(&mut reader)?;
dst.write(Arc::from(s));
Ok(())
}
}

/// Serializable configuration to create channel senders.
#[derive(SchemaWrite, SchemaRead, Clone, Debug)]
pub struct ChannelConf {
lock_file_path: Box<NativeStr>,
#[wincode(with = "ArcStrSchema")]
shm_id: Arc<str>,
shm_id: Box<NativeStr>,
}

/// Creates a mpsc IPC channel with one receiver and a `ChannelConf` that can be passed around processes and used to create multiple senders
Expand All @@ -62,12 +29,15 @@ pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> {
// Initialize the lock file with a unique name.
let lock_file_path = temp_dir().join(format!("fspy_ipc_{}.lock", Uuid::new_v4()));

let shm = fspy_shm::create(capacity)?;
let (keeper, handle) = fspy_shm::create(capacity)?;
let mapping = handle.map()?;

let conf =
ChannelConf { lock_file_path: lock_file_path.as_os_str().into(), shm_id: shm.id().into() };
let conf = ChannelConf {
lock_file_path: lock_file_path.as_os_str().into(),
shm_id: keeper.id().into(),
};

let receiver = Receiver::new(lock_file_path, shm)?;
let receiver = Receiver::new(lock_file_path, keeper, mapping)?;
Ok((conf, receiver))
}

Expand All @@ -83,16 +53,17 @@ impl ChannelConf {
let lock_file = File::open(self.lock_file_path.to_cow_os_str())?;
lock_file.try_lock_shared()?;

let shm = fspy_shm::open(&self.shm_id)?;
// SAFETY: `shm` is a freshly opened shared memory region with valid pointer and size.
// Exclusive write access is ensured by the shared file lock held by this sender.
let writer = unsafe { ShmWriter::new(shm) };
let mapping = fspy_shm::open(&self.shm_id.to_cow_os_str())?.map()?;
// SAFETY: `mapping` is a freshly mapped shared memory region with valid
// pointer and size. Exclusive write access is ensured by the shared
// file lock held by this sender.
let writer = unsafe { ShmWriter::new(mapping) };
Ok(Sender { writer, lock_file, lock_file_path: self.lock_file_path.clone() })
}
}

pub struct Sender {
writer: ShmWriter<Shm>,
writer: ShmWriter<Mapping>,
lock_file_path: Box<NativeStr>,
lock_file: File,
}
Expand All @@ -106,20 +77,13 @@ impl Drop for Sender {
}

impl Deref for Sender {
type Target = ShmWriter<Shm>;
type Target = ShmWriter<Mapping>;

fn deref(&self) -> &Self::Target {
&self.writer
}
}

#[cfg_attr(
target_os = "windows",
expect(
clippy::non_send_fields_in_send_ty,
reason = "`Sender` holds a shared file lock that ensures there's no reader, so `shm` can be safely written to"
)
)]
/// SAFETY: `Sender` holds a shared file lock that ensures there's no reader, so `shm` can be safely written to.
unsafe impl Send for Sender {}

Expand All @@ -131,16 +95,11 @@ unsafe impl Sync for Sender {}
pub struct Receiver {
lock_file_path: PathBuf,
lock_file: File,
shm: Shm,
/// Keeps the backing file's name alive for as long as senders may attach.
_keeper: ShmKeeper,
mapping: Mapping,
}

#[cfg_attr(
target_os = "windows",
expect(
clippy::non_send_fields_in_send_ty,
reason = "Receiver doesn't read or write `shm`. It only passes it to `ReceiverLockGuard` under the lock"
)
)]
/// SAFETY: `Receiver` doesn't read or write `shm`. It only passes it to `ReceiverLockGuard` under the lock.
unsafe impl Send for Receiver {}

Expand All @@ -156,9 +115,9 @@ impl Drop for Receiver {
}

impl Receiver {
fn new(lock_file_path: PathBuf, shm: Shm) -> io::Result<Self> {
fn new(lock_file_path: PathBuf, keeper: ShmKeeper, mapping: Mapping) -> io::Result<Self> {
let lock_file = File::create(&lock_file_path)?;
Ok(Self { lock_file_path, lock_file, shm })
Ok(Self { lock_file_path, lock_file, _keeper: keeper, mapping })
}

/// Lock the shared memory for unique read access.
Expand All @@ -172,7 +131,7 @@ impl Receiver {
self.lock_file.lock()?;
// SAFETY: The exclusive file lock is held, so no writers can access the shared memory.
// The lock ensures all prior writes are visible to this thread.
let reader = ShmReader::new(unsafe { self.shm.as_slice() });
let reader = ShmReader::new(unsafe { self.mapping.as_slice() });
Ok(ReceiverLockGuard { reader, lock_file: &self.lock_file })
}
}
Expand Down
37 changes: 15 additions & 22 deletions crates/fspy_shared/src/ipc/channel/shm_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{
};

use bytemuck::must_cast;
use fspy_shm::Shm;
use fspy_shm::Mapping;
use wincode::{SchemaWrite, Serialize as _, config::DefaultConfig};

// `ShmWriter` writes headers using atomic operations to prevent partial writes due to crashes,
Expand All @@ -28,7 +28,7 @@ pub trait AsRawSlice {
fn as_raw_slice(&self) -> *mut [u8];
}

impl AsRawSlice for Shm {
impl AsRawSlice for Mapping {
fn as_raw_slice(&self) -> *mut [u8] {
slice_from_raw_parts_mut(self.as_ptr(), self.len())
}
Expand Down Expand Up @@ -672,31 +672,24 @@ mod tests {

const SHM_SIZE: usize = 1024 * 1024;

// On Linux, `fspy_shm::create` spawns the mapping's broker onto the
// ambient tokio runtime, which serves the child processes' opens.
#[cfg(target_os = "linux")]
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_io()
.enable_time()
.build()
.unwrap();
#[cfg(target_os = "linux")]
let _guard = runtime.enter();

let shm = fspy_shm::create(SHM_SIZE).unwrap();
let shm_name = shm.id().to_owned();
let (keeper, handle) = fspy_shm::create(SHM_SIZE).unwrap();
let shm_name = keeper.id().to_str().expect("test temp dir is UTF-8").to_owned();
// Map before the children run. Windows keeps views coherent while they
// exist at the same time; a view created after every writer exited can
// observe the file before the writers' dirty pages reach it.
let mapping = handle.map().unwrap();

let children: Vec<Child> = (0..CHILD_COUNT)
.map(|child_index| {
let cmd = command_for_fn!(
(shm_name.clone(), child_index),
|(shm_name, child_index): (String, usize)| {
let shm = fspy_shm::open(&shm_name).unwrap();
// SAFETY: `shm` is a freshly opened shared memory region with a valid
// pointer and size. Concurrent write access is safe because `ShmWriter`
// uses atomic operations.
let writer = unsafe { ShmWriter::new(shm) };
let mapping =
fspy_shm::open(std::ffi::OsStr::new(&shm_name)).unwrap().map().unwrap();
// SAFETY: `mapping` is a freshly mapped shared memory region with a
// valid pointer and size. Concurrent write access is safe because
// `ShmWriter` uses atomic operations.
let writer = unsafe { ShmWriter::new(mapping) };
for i in 0..FRAME_COUNT_EACH_CHILD {
let frame_data = std::format!("{child_index} {i}");
assert!(writer.try_write_frame(frame_data.as_bytes()));
Expand All @@ -714,7 +707,7 @@ mod tests {

// SAFETY: All child processes have exited (waited above), so no concurrent writers exist.
// The shared memory is valid and fully written.
let shm = unsafe { shm.as_slice() };
let shm = unsafe { mapping.as_slice() };
let reader = ShmReader::new(shm);
let frames = reader.iter_frames().map(BStr::new).collect::<FxHashSet<&BStr>>();
assert_eq!(frames.len(), CHILD_COUNT * FRAME_COUNT_EACH_CHILD);
Expand Down
18 changes: 1 addition & 17 deletions crates/fspy_shm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,21 @@ license.workspace = true
publish = false
rust-version.workspace = true

[target.'cfg(target_os = "linux")'.dependencies]
[dependencies]
memmap2 = { workspace = true }
memfd = { workspace = true }
passfd = { workspace = true, features = ["async"] }
nix = { workspace = true, features = ["fs", "socket", "user"] }
tokio = { workspace = true, features = ["macros", "net", "rt", "time"] }
tokio-util = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }

[target.'cfg(target_os = "macos")'.dependencies]
base64 = { workspace = true }
memmap2 = { workspace = true }
nix = { workspace = true, features = ["fs", "mman"] }
uuid = { workspace = true, features = ["v4"] }

[target.'cfg(target_os = "windows")'.dependencies]
uuid = { workspace = true, features = ["v4"] }
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_IO",
"Win32_System_Ioctl",
"Win32_System_Memory",
] }

[dev-dependencies]
ctor = { workspace = true }
subprocess_test = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

[lints]
workspace = true
Expand Down
Loading
Loading