Skip to content

Commit 1d8eec4

Browse files
committed
feat(client): serve getEnv over runner IPC
Motivation: The JS client already exposes getEnv, but before this change it always fell back to no-op behavior. Runner-aware tools need a real round-trip way to ask the runner for an env value before those reads can be used for cache tracking. Scope: Add the GetEnv request/response frame, response reading in the Rust client, server-side resolution from the spawned task env map, and a real NAPI getEnv implementation. This PR intentionally does not add tracked envs to cache fingerprints. Verification: - cargo test -p vite_task_server --test integration - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_reads_declared_env -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_reads_declared_env -- --ignored
1 parent 1ae40ca commit 1d8eec4

14 files changed

Lines changed: 225 additions & 85 deletions

File tree

Cargo.lock

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

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::{
1818
};
1919

2020
use futures_util::future::LocalBoxFuture;
21+
use rustc_hash::FxHashMap;
2122
use tokio_util::sync::CancellationToken;
2223
use vite_path::{AbsolutePath, RelativePathBuf};
2324
use vite_task_ipc_shared::NODE_CLIENT_PATH_ENV_NAME;
@@ -156,6 +157,7 @@ impl<'a> ExecutionMode<'a> {
156157
cache_metadata: Option<&'a CacheMetadata>,
157158
stdio_config: StdioConfig,
158159
globbed_inputs: BTreeMap<RelativePathBuf, u64>,
160+
envs: &BTreeMap<Arc<OsStr>, Arc<OsStr>>,
159161
) -> Result<Self, ExecutionError> {
160162
let Some(metadata) = cache_metadata else {
161163
return Ok(Self::Uncached {
@@ -182,8 +184,10 @@ impl<'a> ExecutionMode<'a> {
182184
// Bind runner IPC for every cached task. The merged cache-control API
183185
// (`disableCache`) must work even when a task uses explicit inputs and
184186
// therefore does not need fspy auto-input inference.
187+
let ipc_env_map: FxHashMap<Arc<OsStr>, Arc<OsStr>> =
188+
envs.iter().map(|(name, value)| (Arc::clone(name), Arc::clone(value))).collect();
185189
let (ipc_envs, ServerHandle { driver, stop_accepting }) =
186-
serve(Recorder::new()).map_err(ExecutionError::IpcServerBind)?;
190+
serve(Recorder::new(Arc::new(ipc_env_map))).map_err(ExecutionError::IpcServerBind)?;
187191
let tracking =
188192
Tracking { fspy, ipc_envs: ipc_envs.collect(), ipc_server_fut: driver, stop_accepting };
189193

@@ -405,8 +409,13 @@ async fn run(
405409
};
406410

407411
// 4. Fold the cache/fspy/stdio decisions into the typed mode.
408-
let mut mode = ExecutionMode::build(cache_metadata, stdio_config, globbed_inputs)
409-
.map_err(Report::failed)?;
412+
let mut mode = ExecutionMode::build(
413+
cache_metadata,
414+
stdio_config,
415+
globbed_inputs,
416+
&spawn_execution.spawn_command.all_envs,
417+
)
418+
.map_err(Report::failed)?;
410419

411420
// Measure end-to-end duration here — spawn() doesn't track time.
412421
let start = Instant::now();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
3+
const value = getEnv('PROBE_ENV') ?? '(unset)';
4+
5+
console.log('PROBE_ENV=' + value);

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,22 @@ steps = [
4646
"--last-details",
4747
], comment = "summary names the opt-out as the not-cached reason" },
4848
]
49+
50+
[[e2e]]
51+
name = "fetch_env_reads_declared_env"
52+
comment = """
53+
Exercises `getEnv(name)`: the tool asks the runner for a declared env var and prints the served value. This verifies the round-trip IPC behavior before any runner-reported env is added to the cache fingerprint.
54+
"""
55+
ignore = true
56+
steps = [
57+
{ argv = [
58+
"vt",
59+
"run",
60+
"fetch-env-declared",
61+
], envs = [
62+
[
63+
"PROBE_ENV",
64+
"served",
65+
],
66+
], comment = "runner serves PROBE_ENV from the spawned task env map" },
67+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# fetch_env_reads_declared_env
2+
3+
Exercises `getEnv(name)`: the tool asks the runner for a declared env var and prints the served value. This verifies the round-trip IPC behavior before any runner-reported env is added to the cache fingerprint.
4+
5+
## `PROBE_ENV=served vt run fetch-env-declared`
6+
7+
runner serves PROBE_ENV from the spawned task env map
8+
9+
```
10+
$ node scripts/fetch_env.mjs
11+
PROBE_ENV=served
12+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"command": "node scripts/disable_cache.mjs",
99
"input": [],
1010
"cache": true
11+
},
12+
"fetch-env-declared": {
13+
"command": "node scripts/fetch_env.mjs",
14+
"env": ["PROBE_ENV"],
15+
"cache": true
1116
}
1217
}
1318
}

crates/vite_task_client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ publish = false
77
rust-version.workspace = true
88

99
[dependencies]
10+
native_str = { workspace = true }
1011
vite_task_ipc_shared = { workspace = true }
1112
wincode = { workspace = true, features = ["derive"] }
1213

crates/vite_task_client/src/lib.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::{
22
cell::RefCell,
33
ffi::OsStr,
4-
io::{self, Write},
4+
io::{self, Read, Write},
5+
sync::Arc,
56
};
67

7-
use vite_task_ipc_shared::{IPC_ENV_NAME, Request};
8+
use native_str::NativeStr;
9+
use vite_task_ipc_shared::{GetEnvResponse, IPC_ENV_NAME, Request};
810

911
#[cfg(unix)]
1012
type Stream = std::os::unix::net::UnixStream;
@@ -13,6 +15,7 @@ type Stream = std::fs::File;
1315

1416
pub struct Client {
1517
stream: RefCell<Stream>,
18+
scratch: RefCell<Vec<u8>>,
1619
}
1720

1821
impl Client {
@@ -38,7 +41,7 @@ impl Client {
3841
}
3942

4043
const fn from_stream(stream: Stream) -> Self {
41-
Self { stream: RefCell::new(stream) }
44+
Self { stream: RefCell::new(stream), scratch: RefCell::new(Vec::new()) }
4245
}
4346

4447
/// Fire-and-forget: the call returns once the request is flushed to the
@@ -53,7 +56,26 @@ impl Client {
5356
self.send(&Request::DisableCache)
5457
}
5558

56-
fn send(&self, request: &Request) -> io::Result<()> {
59+
/// Requests an env value from the runner. Returns `None` if the runner
60+
/// reports the env is not available.
61+
///
62+
/// # Errors
63+
///
64+
/// Returns an error if the request or response fails.
65+
pub fn get_env(&self, name: &OsStr) -> io::Result<Option<Arc<OsStr>>> {
66+
let name = Box::<NativeStr>::from(name);
67+
68+
self.send(&Request::GetEnv { name: &name })?;
69+
self.recv_with(|bytes| {
70+
let response: GetEnvResponse<'_> = wincode::deserialize_exact(bytes)
71+
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
72+
Ok(response
73+
.env_value
74+
.map(|env_value| Arc::<OsStr>::from(env_value.to_cow_os_str().as_ref())))
75+
})
76+
}
77+
78+
fn send(&self, request: &Request<'_>) -> io::Result<()> {
5779
let bytes = wincode::serialize(request)
5880
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
5981
let len = u32::try_from(bytes.len())
@@ -64,6 +86,18 @@ impl Client {
6486
stream.flush()?;
6587
Ok(())
6688
}
89+
90+
fn recv_with<T>(&self, extract: impl FnOnce(&[u8]) -> io::Result<T>) -> io::Result<T> {
91+
let mut stream = self.stream.borrow_mut();
92+
let mut scratch = self.scratch.borrow_mut();
93+
let mut len_bytes = [0u8; 4];
94+
stream.read_exact(&mut len_bytes)?;
95+
let len = u32::from_le_bytes(len_bytes) as usize;
96+
scratch.clear();
97+
scratch.resize(len, 0);
98+
stream.read_exact(&mut scratch)?;
99+
extract(&scratch)
100+
}
67101
}
68102

69103
#[cfg(unix)]

crates/vite_task_client_napi/src/lib.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
reason = "no-op stubs keep the signature of the real implementations that replace them"
3838
)]
3939

40-
use std::collections::HashMap;
40+
use std::{collections::HashMap, ffi::OsStr};
4141

4242
use napi::{Error, Result};
4343
use napi_derive::napi;
@@ -92,16 +92,17 @@ impl RunnerClient {
9292
self.client.disable_cache().map_err(|err| err_string(vite_str::format!("{err}")))
9393
}
9494

95-
/// No-op for now: always returns `None`, so callers fall back to their
96-
/// own process env. Becomes a real request once env tracking can
97-
/// fingerprint the served values.
9895
#[napi]
99-
pub fn get_env(
100-
&self,
101-
_name: String,
102-
_options: Option<GetEnvOptions>,
103-
) -> Result<Option<String>> {
104-
Ok(None)
96+
pub fn get_env(&self, name: String, _options: Option<GetEnvOptions>) -> Result<Option<String>> {
97+
let value = self
98+
.client
99+
.get_env(OsStr::new(&name))
100+
.map_err(|err| err_string(vite_str::format!("{err}")))?;
101+
value.map_or(Ok(None), |value| {
102+
value.to_str().map(|s| Some(s.to_owned())).ok_or_else(|| {
103+
err_string(vite_str::format!("env value for {name} is not valid UTF-8"))
104+
})
105+
})
105106
}
106107

107108
/// No-op for now: always returns an empty match-set — see

crates/vite_task_ipc_shared/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ publish = false
77
rust-version.workspace = true
88

99
[dependencies]
10+
native_str = { workspace = true }
1011
wincode = { workspace = true, features = ["derive"] }
1112

1213
[lints]

0 commit comments

Comments
 (0)