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 backs its shared memory with a sparse temporary file on every platform, so tasks are still tracked inside coding-agent sandboxes that deny POSIX shared memory and Unix domain sockets ([#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
83 changes: 21 additions & 62 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,16 @@ 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 = fspy_shm::create(capacity)?;
// The creating process has no privileged view; it opens one like any other.
let mapping = fspy_shm::open(keeper.id())?;

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,7 +54,7 @@ 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)?;
let shm = fspy_shm::open(&self.shm_id.to_cow_os_str())?;
// 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) };
Expand All @@ -92,7 +63,7 @@ impl ChannelConf {
}

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
25 changes: 7 additions & 18 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,27 +672,15 @@ 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 = fspy_shm::create(SHM_SIZE).unwrap();
let shm_name = keeper.id().to_str().expect("test temp dir is UTF-8").to_owned();

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();
let shm = fspy_shm::open(std::ffi::OsStr::new(&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.
Expand All @@ -712,9 +700,10 @@ mod tests {
assert!(status.success());
}

let mapping = fspy_shm::open(keeper.id()).unwrap();
// 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
16 changes: 2 additions & 14 deletions crates/fspy_shm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,13 @@ license.workspace = true
publish = false
rust-version.workspace = true

[target.'cfg(target_os = "linux")'.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 }
[dependencies]
uuid = { workspace = true, features = ["v4"] }

[target.'cfg(target_os = "macos")'.dependencies]
base64 = { workspace = true }
[target.'cfg(unix)'.dependencies]
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",
Expand All @@ -37,7 +26,6 @@ windows-sys = { workspace = true, features = [
[dev-dependencies]
ctor = { workspace = true }
subprocess_test = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

[lints]
workspace = true
Expand Down
Loading
Loading