Skip to content

Commit 1980691

Browse files
wan9chiclaude
andcommitted
refactor(execute): reshape for IPC server lifecycle
Reshape `vite_task::session::execute` (and the supporting modules: `spawn`, `fingerprint`, `cache`, `tracked_accesses`, `event`, and the summary reporter) to accommodate a per-task IPC server. The server itself is a placeholder on this branch: instead of actually calling `vite_task_server::serve(...)`, we construct an empty `Recorder` whose driver future resolves immediately with no traffic, and bind a `StopAccepting::noop()`. The downstream plumbing — async-join of the child with the server, `Reports` flowing into post-run fingerprinting and into the cache update — is fully in place but sees only the empty `Reports`, so behaviour is byte-for-byte identical to today's runner. The follow-up wires up the real `serve(...)`, embeds the napi addon, and injects `VP_RUN_NODE_CLIENT_PATH` into the child. That PR's `serve(Recorder::new(env_map))` is the only call that changes from "future::ready(Ok(recorder))" to the real bind. Adds `StopAccepting::noop()` to `vite_task_server` for the same placeholder use case; it's a tiny helper that's also useful for tests that need a value of the type without running a server. Also folds in the unrelated-but-blocking output-config refactor (`output: Option<UserInputsConfig>` instead of `Option<Vec<UserOutputEntry>>`) — both sides now route through `ResolvedGlobConfig::from_user_config`. The 80-odd plan-snapshot updates are mechanical consequences of that type change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 00420ad commit 1980691

99 files changed

Lines changed: 1100 additions & 536 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,6 @@ ignored = [
177177
"fspy_preload_unix",
178178
"fspy_preload_windows",
179179
"vite_task_client_napi",
180-
# Registered in the workspace dependency table so downstream PRs in the
181-
# runner-aware-tools stack can pick it up via `workspace = true` without
182-
# touching this file. No in-tree consumer in this PR.
183-
"vite_task_server",
184180
]
185181

186182
[profile.dev]

crates/vite_task/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ tokio-util = { workspace = true }
4444
tracing = { workspace = true }
4545
twox-hash = { workspace = true }
4646
uuid = { workspace = true, features = ["v4"] }
47+
vite_glob = { workspace = true }
4748
vite_path = { workspace = true }
4849
vite_select = { workspace = true }
4950
vite_str = { workspace = true }
5051
vite_task_graph = { workspace = true }
5152
vite_task_plan = { workspace = true }
53+
vite_task_server = { workspace = true }
5254
vite_workspace = { workspace = true }
5355
wax = { workspace = true }
5456
zstd = { workspace = true }

crates/vite_task/docs/task-cache.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ The cache entry key uniquely identifies a command execution context:
9292
```rust
9393
pub struct CacheEntryKey {
9494
pub spawn_fingerprint: SpawnFingerprint,
95-
pub input_config: ResolvedInputConfig,
95+
pub input_config: ResolvedGlobConfig,
9696
}
9797
```
9898

@@ -303,7 +303,7 @@ Cache entries are serialized using `bincode` for efficient storage.
303303
│ ────────────────────── │
304304
│ CacheEntryKey { │
305305
│ spawn_fingerprint: SpawnFingerprint { ... }, │
306-
│ input_config: ResolvedInputConfig { ... }, │
306+
│ input_config: ResolvedGlobConfig { ... }, │
307307
│ } │
308308
│ ExecutionCacheKey::UserTask { │
309309
│ task_name: "build", │

crates/vite_task/src/session/cache/display.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,16 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
179179
let desc = format_input_change_str(*kind, path.as_str());
180180
return Some(vite_str::format!("○ cache miss: {desc}, executing"));
181181
}
182+
FingerprintMismatch::TrackedEnvChanged { name, .. } => {
183+
return Some(vite_str::format!(
184+
"○ cache miss: tracked env '{name}' changed, executing"
185+
));
186+
}
187+
FingerprintMismatch::TrackedEnvGlobChanged { pattern, .. } => {
188+
return Some(vite_str::format!(
189+
"○ cache miss: tracked env glob '{pattern}' changed, executing"
190+
));
191+
}
182192
};
183193
Some(vite_str::format!("○ cache miss: {reason}, executing"))
184194
}

crates/vite_task/src/session/cache/mod.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ pub enum FingerprintMismatch {
158158
kind: InputChangeKind,
159159
path: RelativePathBuf,
160160
},
161+
/// A tool-tracked env var changed between runs.
162+
TrackedEnvChanged {
163+
name: Str,
164+
old: Option<Str>,
165+
new: Option<Str>,
166+
},
167+
/// A tool-tracked env glob's match-set changed between runs.
168+
TrackedEnvGlobChanged {
169+
pattern: Str,
170+
diff: crate::session::execute::fingerprint::EnvGlobDiff,
171+
},
161172
}
162173

163174
impl Display for FingerprintMismatch {
@@ -175,6 +186,42 @@ impl Display for FingerprintMismatch {
175186
Self::InputChanged { kind, path } => {
176187
write!(f, "{}", display::format_input_change_str(*kind, path.as_str()))
177188
}
189+
Self::TrackedEnvChanged { name, old, new } => {
190+
write!(f, "tracked env {name}: ")?;
191+
match old {
192+
Some(value) => write!(f, "{:?}", value.as_str())?,
193+
None => write!(f, "(unset)")?,
194+
}
195+
write!(f, " → ")?;
196+
match new {
197+
Some(value) => write!(f, "{:?}", value.as_str()),
198+
None => write!(f, "(unset)"),
199+
}
200+
}
201+
Self::TrackedEnvGlobChanged { pattern, diff } => {
202+
write!(f, "tracked env glob {:?}:", pattern.as_str())?;
203+
let mut first = true;
204+
for (name, value) in &diff.added {
205+
write!(f, "{} +{}={:?}", if first { "" } else { "," }, name, value.as_str())?;
206+
first = false;
207+
}
208+
for (name, value) in &diff.removed {
209+
write!(f, "{} -{}={:?}", if first { "" } else { "," }, name, value.as_str())?;
210+
first = false;
211+
}
212+
for (name, (old, new)) in &diff.changed {
213+
write!(
214+
f,
215+
"{} {}: {:?} → {:?}",
216+
if first { "" } else { "," },
217+
name,
218+
old.as_str(),
219+
new.as_str()
220+
)?;
221+
first = false;
222+
}
223+
Ok(())
224+
}
178225
}
179226
}
180227
}
@@ -266,11 +313,24 @@ impl ExecutionCache {
266313
return Ok(Err(CacheMiss::FingerprintMismatch(mismatch)));
267314
}
268315

269-
// Validate post-run fingerprint (inferred inputs from fspy)
270-
if let Some((kind, path)) = cache_value.post_run_fingerprint.validate(workspace_root)? {
271-
return Ok(Err(CacheMiss::FingerprintMismatch(
272-
FingerprintMismatch::InputChanged { kind, path },
273-
)));
316+
// Validate post-run fingerprint (inferred inputs + tracked envs)
317+
if let Some(mismatch) = cache_value.post_run_fingerprint.validate(workspace_root)? {
318+
let fingerprint_mismatch = match mismatch {
319+
crate::session::execute::fingerprint::PostRunMismatch::InputChanged {
320+
kind,
321+
path,
322+
} => FingerprintMismatch::InputChanged { kind, path },
323+
crate::session::execute::fingerprint::PostRunMismatch::TrackedEnvChanged {
324+
name,
325+
old,
326+
new,
327+
} => FingerprintMismatch::TrackedEnvChanged { name, old, new },
328+
crate::session::execute::fingerprint::PostRunMismatch::TrackedEnvGlobChanged {
329+
pattern,
330+
diff,
331+
} => FingerprintMismatch::TrackedEnvGlobChanged { pattern, diff },
332+
};
333+
return Ok(Err(CacheMiss::FingerprintMismatch(fingerprint_mismatch)));
274334
}
275335
// Associate the execution key to the cache entry key if not already,
276336
// so that next time we can find it and report what changed

crates/vite_task/src/session/event.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{process::ExitStatus, time::Duration};
22

33
use vite_path::RelativePathBuf;
4+
use vite_task_server::Error as IpcServerError;
45

56
use super::cache::CacheMiss;
67

@@ -43,6 +44,13 @@ pub enum ExecutionError {
4344
/// Creating the post-run fingerprint failed after successful execution.
4445
#[error("Failed to create post-run fingerprint")]
4546
PostRunFingerprint(#[source] anyhow::Error),
47+
48+
/// The runner-aware IPC server failed to bind for this task. Reported
49+
/// instead of silently degrading so that `{ auto: true }` inputs stay
50+
/// observable end-to-end.
51+
#[expect(dead_code, reason = "placeholder; constructed once the real `serve()` lands")]
52+
#[error("Failed to start runner IPC server")]
53+
IpcServerBind(#[source] std::io::Error),
4654
}
4755

4856
#[derive(Debug, Clone)]
@@ -76,6 +84,14 @@ pub enum CacheNotUpdatedReason {
7684
/// (its `input` config includes auto-inference). Task ran but cannot
7785
/// be cached without tracked path accesses.
7886
FspyUnsupported,
87+
/// The runner's IPC server failed during execution, so the collected
88+
/// reports may be incomplete. Caching such a run would risk stale
89+
/// inputs/outputs on the next hit. Carries the underlying error for
90+
/// user-facing reporting.
91+
IpcServerError(IpcServerError),
92+
/// A runner-aware tool explicitly requested that this run not be cached
93+
/// (e.g. vite dev-server, a watch task).
94+
ToolRequested,
7995
}
8096

8197
#[derive(Debug)]

crates/vite_task/src/session/execute/fingerprint.rs

Lines changed: 128 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,53 @@ pub struct PostRunFingerprint {
3131
/// Paths inferred from fspy during execution with their content fingerprints.
3232
/// Only populated when `input_config.includes_auto` is true.
3333
pub inferred_inputs: HashMap<RelativePathBuf, PathFingerprint>,
34+
35+
/// Env vars observed via runner-aware IPC `getEnv` with `tracked: true`.
36+
/// Key is the env name; value is the env value at execution time (or
37+
/// `None` if unset). Validated at cache lookup by comparing against the
38+
/// current parent env.
39+
pub tracked_envs: BTreeMap<Str, Option<Str>>,
40+
41+
/// Glob-pattern env queries (`getEnvs`) made with `tracked: true`.
42+
/// Outer key is the glob pattern, inner map is the match-set at
43+
/// execution time (name → value). Validated at cache lookup by
44+
/// re-matching against the current parent env and comparing the
45+
/// resulting set.
46+
pub tracked_env_globs: BTreeMap<Str, BTreeMap<Str, Str>>,
47+
}
48+
49+
/// A mismatch between the stored post-run fingerprint and the current state.
50+
#[expect(
51+
clippy::enum_variant_names,
52+
reason = "all three variants describe different kinds of post-run changes; \
53+
dropping the `Changed` suffix on any one of them would be misleading"
54+
)]
55+
#[derive(Debug, Clone)]
56+
pub enum PostRunMismatch {
57+
/// An inferred input file or directory changed.
58+
InputChanged { kind: InputChangeKind, path: RelativePathBuf },
59+
/// A tool-tracked env var changed value (or was added/removed).
60+
TrackedEnvChanged { name: Str, old: Option<Str>, new: Option<Str> },
61+
/// A tool-tracked env glob's match-set changed between runs. The glob
62+
/// still matches the same pattern, but added/removed/mutated entries.
63+
TrackedEnvGlobChanged { pattern: Str, diff: EnvGlobDiff },
64+
}
65+
66+
/// Per-pattern diff between stored and current match-set. Each map is a
67+
/// subset of the symmetric difference keyed by env name. `changed` holds
68+
/// names present in both with differing values; `added` / `removed` are
69+
/// exclusive to one side.
70+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
71+
pub struct EnvGlobDiff {
72+
pub added: BTreeMap<Str, Str>,
73+
pub removed: BTreeMap<Str, Str>,
74+
pub changed: BTreeMap<Str, (Str, Str)>,
75+
}
76+
77+
impl EnvGlobDiff {
78+
fn is_empty(&self) -> bool {
79+
self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
80+
}
3481
}
3582

3683
/// Fingerprint for a single path (file or directory)
@@ -69,11 +116,15 @@ impl PostRunFingerprint {
69116
/// * `inferred_path_reads` - Map of paths that were read during execution (from fspy)
70117
/// * `base_dir` - Workspace root for resolving relative paths
71118
/// * `globbed_inputs` - Prerun glob fingerprint; paths here are skipped
119+
/// * `tracked_envs` - Tool-requested env vars (name → value), validated on lookup
120+
/// * `tracked_env_globs` - Tool-requested env globs (pattern → matches), validated on lookup
72121
#[tracing::instrument(level = "debug", skip_all, name = "create_post_run_fingerprint")]
73122
pub fn create(
74123
inferred_path_reads: &HashMap<RelativePathBuf, PathRead>,
75124
base_dir: &AbsolutePath,
76125
globbed_inputs: &BTreeMap<RelativePathBuf, u64>,
126+
tracked_envs: BTreeMap<Str, Option<Str>>,
127+
tracked_env_globs: BTreeMap<Str, BTreeMap<Str, Str>>,
77128
) -> anyhow::Result<Self> {
78129
let inferred_inputs = inferred_path_reads
79130
.par_iter()
@@ -85,16 +136,13 @@ impl PostRunFingerprint {
85136
})
86137
.collect::<anyhow::Result<HashMap<_, _>>>()?;
87138

88-
Ok(Self { inferred_inputs })
139+
Ok(Self { inferred_inputs, tracked_envs, tracked_env_globs })
89140
}
90141

91-
/// Validates the fingerprint against current filesystem state.
92-
/// Returns `Some((kind, path))` if an input changed, `None` if all valid.
142+
/// Validates the fingerprint against current filesystem and env state.
143+
/// Returns `Some(mismatch)` on the first divergence, `None` if all valid.
93144
#[tracing::instrument(level = "debug", skip_all, name = "validate_post_run_fingerprint")]
94-
pub fn validate(
95-
&self,
96-
base_dir: &AbsolutePath,
97-
) -> anyhow::Result<Option<(InputChangeKind, RelativePathBuf)>> {
145+
pub fn validate(&self, base_dir: &AbsolutePath) -> anyhow::Result<Option<PostRunMismatch>> {
98146
let input_mismatch = self.inferred_inputs.par_iter().find_map_any(
99147
|(input_relative_path, path_fingerprint)| {
100148
let input_full_path = Arc::<AbsolutePath>::from(base_dir.join(input_relative_path));
@@ -120,12 +168,83 @@ impl PostRunFingerprint {
120168
} else {
121169
input_relative_path.clone()
122170
};
123-
Some(Ok((kind, path)))
171+
Some(Ok(PostRunMismatch::InputChanged { kind, path }))
124172
}
125173
},
126174
);
127-
input_mismatch.transpose()
175+
if let Some(result) = input_mismatch {
176+
return result.map(Some);
177+
}
178+
179+
// Validate tracked envs against the current parent env.
180+
for (name, stored_value) in &self.tracked_envs {
181+
let current_value =
182+
std::env::var_os(name.as_str()).and_then(|v| v.to_str().map(Str::from));
183+
if current_value.as_ref() != stored_value.as_ref() {
184+
return Ok(Some(PostRunMismatch::TrackedEnvChanged {
185+
name: name.clone(),
186+
old: stored_value.clone(),
187+
new: current_value,
188+
}));
189+
}
190+
}
191+
192+
// Validate tracked env globs: re-enumerate parent env for each
193+
// pattern and diff against the stored match-set.
194+
for (pattern, stored_matches) in &self.tracked_env_globs {
195+
let current_matches = match_env_glob(pattern.as_str())?;
196+
let diff = diff_env_glob(stored_matches, &current_matches);
197+
if !diff.is_empty() {
198+
return Ok(Some(PostRunMismatch::TrackedEnvGlobChanged {
199+
pattern: pattern.clone(),
200+
diff,
201+
}));
202+
}
203+
}
204+
205+
Ok(None)
206+
}
207+
}
208+
209+
/// Build the current match-set for `pattern` by enumerating
210+
/// `std::env::vars_os()` and keeping UTF-8 names whose representation matches
211+
/// the glob. Mirrors the server-side match (see
212+
/// `vite_task_server::Recorder::get_envs`).
213+
fn match_env_glob(pattern: &str) -> anyhow::Result<BTreeMap<Str, Str>> {
214+
let set = vite_glob::GlobPatternSet::new(std::iter::once(pattern))?;
215+
Ok(std::env::vars_os()
216+
.filter_map(|(name, value)| {
217+
let name_str = name.to_str()?.to_owned();
218+
let value_str = value.to_str()?.to_owned();
219+
if set.is_match(&name_str) {
220+
Some((Str::from(name_str.as_str()), Str::from(value_str.as_str())))
221+
} else {
222+
None
223+
}
224+
})
225+
.collect())
226+
}
227+
228+
/// Compute the diff of two match-sets for the same glob pattern.
229+
fn diff_env_glob(stored: &BTreeMap<Str, Str>, current: &BTreeMap<Str, Str>) -> EnvGlobDiff {
230+
let mut diff = EnvGlobDiff::default();
231+
for (name, stored_value) in stored {
232+
match current.get(name) {
233+
None => {
234+
diff.removed.insert(name.clone(), stored_value.clone());
235+
}
236+
Some(current_value) if current_value != stored_value => {
237+
diff.changed.insert(name.clone(), (stored_value.clone(), current_value.clone()));
238+
}
239+
Some(_) => {}
240+
}
241+
}
242+
for (name, current_value) in current {
243+
if !stored.contains_key(name) {
244+
diff.added.insert(name.clone(), current_value.clone());
245+
}
128246
}
247+
diff
129248
}
130249

131250
/// Determine the kind of change between two differing path fingerprints.

0 commit comments

Comments
 (0)