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** A task no longer fails when file-access tracking cannot start, for example because the tracking library cannot be written to the temporary directory. The task runs untracked and is reported as not cached ([#752](https://github.com/voidzero-dev/vite-task/pull/752)).
- **Fixed** A mistyped or misplaced setting in a task now gets an error that names it. For example, `cache: { outputs: [...] }` now fails with an `unknown field` error that names `outputs` and lists the accepted settings, instead of `data did not match any variant of untagged enum UserTaskDefinition` ([vite-plus#2635](https://github.com/voidzero-dev/vite-plus/issues/2635), [#815](https://github.com/voidzero-dev/vite-task/pull/815)).
- **Fixed** In cached tasks on Linux and macOS, programs that pass `-1` as a directory descriptor to calls such as `openat` no longer abort. For example, `ps -p <pid>` from procps-ng 4.0.6 or later aborted when the process had already exited ([#862](https://github.com/voidzero-dev/vite-task/pull/862)).
- **Fixed** `vp run` no longer panics before running a script that contains non-ASCII characters, such as `echo こんにちは`, and no longer cuts off such commands where it displays them ([#861](https://github.com/voidzero-dev/vite-task/pull/861)).
Expand Down
10 changes: 8 additions & 2 deletions crates/fspy/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
ffi::{OsStr, OsString},
io,
path::{Path, PathBuf},
process::Stdio,
};
Expand Down Expand Up @@ -169,13 +170,18 @@ impl Command {
///
/// # Errors
///
/// Returns [`SpawnError`] if program resolution fails or the process cannot be spawned.
/// Returns [`SpawnError`] if program resolution fails, the tracking
/// machinery cannot be initialized (e.g. the preload library cannot be
/// materialized), or the process cannot be spawned.
pub async fn spawn(
mut self,
cancellation_token: CancellationToken,
) -> Result<TrackedChild, SpawnError> {
self.resolve_program()?;
SPY_IMPL.spawn(self, cancellation_token).await
match &*SPY_IMPL {
Ok(spy) => spy.spawn(self, cancellation_token).await,
Err(e) => Err(SpawnError::Injection(io::Error::new(e.kind(), e.to_string()))),
}
}

/// Resolve program name to full path using `PATH` and cwd.
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ pub struct TrackedChild {
pub process_handle: std::os::windows::io::OwnedHandle,
}

pub(crate) static SPY_IMPL: LazyLock<SpyImpl> = LazyLock::new(|| {
pub(crate) static SPY_IMPL: LazyLock<io::Result<SpyImpl>> = LazyLock::new(|| {
let tmp_dir = temp_dir().join("fspy");
let _ = create_dir(&tmp_dir);
SpyImpl::init_in(&tmp_dir).expect("Failed to initialize global spy")
SpyImpl::init_in(&tmp_dir)
});
5 changes: 2 additions & 3 deletions crates/vt/src/napi_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ use vt_path::{AbsolutePath, AbsolutePathBuf};
///
/// # Panics
///
/// Panics if the materialization fails on first call — this mirrors fspy's
/// `SPY_IMPL` and the same reasoning applies: if we can't write into the
/// system temp dir, the runner can't run tasks anyway.
/// Panics if the materialization fails on first call: if we can't write
/// into the system temp dir, the runner can't run tasks anyway.
#[must_use]
pub fn napi_client_path() -> &'static AbsolutePath {
static PATH: LazyLock<AbsolutePathBuf> = LazyLock::new(|| {
Expand Down
4 changes: 4 additions & 0 deletions crates/vt/src/session/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub enum CacheNotUpdatedReason {
/// (its `input` config includes auto-inference). Task ran but cannot
/// be cached without tracked path accesses.
FspyUnsupported,
/// fspy is compiled in, but the tracked spawn failed (e.g. the preload
/// library could not be materialized), so the task ran untracked and
/// cannot be cached without tracked path accesses.
FspyUnavailable,
/// The runner's IPC server failed during execution, so the collected
/// reports may be incomplete. Caching such a run would risk stale
/// inputs/outputs on the next hit. Carries the underlying error for
Expand Down
17 changes: 14 additions & 3 deletions crates/vt/src/session/execute/cache_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ pub(super) async fn update_cache(
}

if fspy_outcome.is_none() && fspy.is_some() {
// Task requested fspy auto-inference but this binary was built without
// `cfg(fspy)`. Task ran, but we can't compute a valid cache entry
// Task requested fspy auto-inference but produced no tracked
// accesses. Task ran, but we can't compute a valid cache entry
// without tracked path accesses.
return (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::FspyUnsupported), None);
return (CacheUpdateStatus::NotUpdated(fspy_missing_reason(outcome)), None);
}

// Collect tool-reported tracked envs for the post-run fingerprint. Env
Expand Down Expand Up @@ -196,6 +196,17 @@ pub(super) async fn update_cache(
}
}

/// Why a run that requested fspy auto-inference produced no tracked
/// accesses: either this binary was built without `cfg(fspy)`, or the
/// tracked spawn failed and the task ran untracked (see [`super::spawn`]).
const fn fspy_missing_reason(outcome: &ChildOutcome) -> CacheNotUpdatedReason {
if outcome.fspy_unavailable {
CacheNotUpdatedReason::FspyUnavailable
} else {
CacheNotUpdatedReason::FspyUnsupported
}
}

/// Summarize the run's fspy observations. `Some` iff tracking was both
/// requested (`tracking.fspy.is_some()`) and compiled in (`cfg(fspy)`). On a
/// `cfg(not(fspy))` build this is always `None`, and [`update_cache`]
Expand Down
43 changes: 40 additions & 3 deletions crates/vt/src/session/execute/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub struct ChildOutcome {
/// `Err` when a tracked process could not record everything it did.
#[cfg(fspy)]
pub path_accesses: Option<Result<PathAccessIterable, fspy::TrackingIncomplete>>,
/// `true` when fspy tracking was requested but the tracked spawn failed
/// and the child ran untracked (see [`spawn`]). Always `false` on builds
/// without `cfg(fspy)`.
pub fspy_unavailable: bool,
}

/// Spawn a command with the requested fspy and stdio configuration.
Expand All @@ -58,6 +62,11 @@ pub struct ChildOutcome {
///
/// On builds without `cfg(fspy)`, the `fspy` argument is ignored and the tokio
/// path is always taken.
///
/// When fspy is requested but the tracked spawn itself fails (e.g. the preload
/// library cannot be materialized), the command falls back to an untracked
/// spawn with [`ChildOutcome::fspy_unavailable`] set, so the run is not
/// cached — a task must never fail to run because its tracing did.
#[tracing::instrument(level = "debug", skip_all)]
pub async fn spawn<E, K, V>(
cmd: &SpawnCommand,
Expand All @@ -71,9 +80,29 @@ where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let extra_envs: Vec<(K, V)> = extra_envs.into_iter().collect();

#[cfg(fspy)]
let mut fspy_unavailable = false;
#[cfg(not(fspy))]
let fspy_unavailable = false;

#[cfg(fspy)]
if fspy {
return spawn_fspy(cmd, stdio, cancellation_token, extra_envs).await;
match spawn_fspy(
cmd,
stdio,
cancellation_token.clone(),
extra_envs.iter().map(|(k, v)| (k, v)),
)
.await
{
Ok(handle) => return Ok(handle),
Err(err) => {
tracing::warn!("fspy spawn failed, falling back to untracked spawn: {err:#}");
fspy_unavailable = true;
}
}
}
#[cfg(not(fspy))]
let _ = fspy;
Expand All @@ -82,12 +111,17 @@ where
tokio_cmd.args(cmd.args.iter().map(vt_str::Str::as_str));
tokio_cmd.env_clear();
tokio_cmd.envs(cmd.spawn_envs.iter());
tokio_cmd.envs(extra_envs);
tokio_cmd.envs(extra_envs.iter().map(|(k, v)| (k, v)));
tokio_cmd.current_dir(&*cmd.cwd);
apply_stdio(&mut tokio_cmd, stdio);
spawn_tokio(tokio_cmd, cancellation_token)
spawn_tokio(tokio_cmd, cancellation_token, fspy_unavailable)
}

/// Spawn through fspy's tracked [`fspy::Command`].
///
/// A failure here must not fail the task: [`spawn`] falls back to an
/// untracked spawn and marks the outcome [`ChildOutcome::fspy_unavailable`]
/// so the run is not cached.
#[cfg(fspy)]
async fn spawn_fspy<E, K, V>(
cmd: &SpawnCommand,
Expand Down Expand Up @@ -148,6 +182,7 @@ where
Ok(ChildOutcome {
exit_status: termination.status,
path_accesses: Some(termination.path_accesses),
fspy_unavailable: false,
})
}
.boxed_local();
Expand All @@ -158,6 +193,7 @@ where
fn spawn_tokio(
mut cmd: tokio::process::Command,
cancellation_token: CancellationToken,
fspy_unavailable: bool,
) -> anyhow::Result<ChildHandle> {
let mut child = cmd.spawn()?;

Expand Down Expand Up @@ -192,6 +228,7 @@ fn spawn_tokio(
exit_status,
#[cfg(fspy)]
path_accesses: None,
fspy_unavailable,
})
}
.boxed_local();
Expand Down
Loading