Skip to content

Commit e95dfd6

Browse files
wan9chiclaude
andcommitted
refactor(ipc): hide env-name detail; serve returns envs iterator
- Add `IPC_ENV_NAME` constant in `vite_task_ipc_shared` as the single source of truth for the IPC env var name (shared between server and client, not exposed to callers). - `serve()` now returns `impl Iterator<Item = (&'static OsStr, OsString)>` instead of a bare `OsString` name. Callers chain the iterator directly into spawn's envs without knowing the env var name. - `Client::from_env()` → `from_envs(envs)`: takes an env-pair iterator, scans for the IPC env. Symmetric with server output. - Unify temp socket prefix to `vite_task_ipc_` on both platforms. - Drop lingering `VP_RUN_IPC` / `VP_RUN_CLIENT_NAPI` references from docs; the env var names are now internal to server/client crates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cb88ce2 commit e95dfd6

8 files changed

Lines changed: 45 additions & 29 deletions

File tree

crates/vite_task_client/src/lib.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
use std::{
2-
env,
32
ffi::{OsStr, OsString},
43
io::{self, Read, Write},
54
};
65

76
use interprocess::local_socket::{Stream, prelude::*};
87
use native_str::NativeStr;
98
use vite_path::AbsolutePath;
10-
use vite_task_ipc_shared::{Request, Response, ResponseBody};
11-
12-
const IPC_ENV: &str = "VP_RUN_IPC";
9+
use vite_task_ipc_shared::{IPC_ENV_NAME, Request, Response, ResponseBody};
1310

1411
pub struct Client {
1512
stream: Stream,
@@ -18,14 +15,25 @@ pub struct Client {
1815
}
1916

2017
impl Client {
21-
/// `Ok(None)` if `VP_RUN_IPC` is not set (running outside the runner).
18+
/// Scans `envs` for the runner's IPC connection info and connects if
19+
/// present. Typical callers pass `std::env::vars_os()`.
20+
///
21+
/// Returns `Ok(None)` if the IPC env is absent (running outside the runner).
2222
/// `Err(..)` if the env is set but connecting fails.
2323
///
2424
/// # Errors
2525
///
2626
/// Returns an error if the env var is set but the server cannot be reached.
27-
pub fn from_env() -> io::Result<Option<Self>> {
28-
env::var_os(IPC_ENV).map_or_else(|| Ok(None), |name| Self::from_name(&name).map(Some))
27+
pub fn from_envs(
28+
envs: impl Iterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
29+
) -> io::Result<Option<Self>> {
30+
for (name, value) in envs {
31+
if name.as_ref() == IPC_ENV_NAME {
32+
let stream = Stream::connect(resolve_name(value.as_ref())?)?;
33+
return Ok(Some(Self { stream, next_id: 0, scratch: Vec::new() }));
34+
}
35+
}
36+
Ok(None)
2937
}
3038

3139
/// Connects to a server at the given socket path (Unix) or pipe name (Windows).

crates/vite_task_ipc_shared/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use native_str::NativeStr;
22
use wincode::{SchemaRead, SchemaWrite};
33

4+
pub const IPC_ENV_NAME: &str = "VITE_TASK_IPC_NAME";
5+
46
#[derive(Debug, SchemaWrite, SchemaRead)]
57
pub enum Request<'a> {
68
IgnoreInput(&'a NativeStr),

crates/vite_task_server/src/lib.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use native_str::NativeStr;
1313
use tokio::io::{AsyncReadExt, AsyncWriteExt};
1414
use tokio_util::sync::CancellationToken;
1515
use vite_path::AbsolutePath;
16-
use vite_task_ipc_shared::{Request, Response, ResponseBody};
16+
use vite_task_ipc_shared::{IPC_ENV_NAME, Request, Response, ResponseBody};
1717

1818
pub trait Handler {
1919
fn ignore_input(&self, path: &Arc<AbsolutePath>);
@@ -50,15 +50,17 @@ impl StopAccepting {
5050

5151
/// Starts an IPC server.
5252
///
53-
/// Returns the socket path / pipe name (for `VP_RUN_IPC`) plus a handle
54-
/// bundling the driver future and the `StopAccepting` signal. See
55-
/// [`ServerHandle`] for driver semantics.
53+
/// Returns the env entries that a child process must inherit to find and
54+
/// connect to this server, plus a handle bundling the driver future and the
55+
/// `StopAccepting` signal. See [`ServerHandle`] for driver semantics.
5656
///
5757
/// # Errors
5858
///
5959
/// Returns an error if creating the listener fails (on Unix, this includes
6060
/// creating the temp socket path).
61-
pub fn serve<'h, H: Handler + 'h>(handler: H) -> io::Result<(OsString, ServerHandle<'h, H>)> {
61+
pub fn serve<'h, H: Handler + 'h>(
62+
handler: H,
63+
) -> io::Result<(impl Iterator<Item = (&'static OsStr, OsString)>, ServerHandle<'h, H>)> {
6264
let stop_token = CancellationToken::new();
6365
let (name, bound) = bind_listener()?;
6466

@@ -69,7 +71,10 @@ pub fn serve<'h, H: Handler + 'h>(handler: H) -> io::Result<(OsString, ServerHan
6971
}
7072
.boxed_local();
7173

72-
Ok((name, ServerHandle { driver, stop_accepting: StopAccepting { token: stop_token } }))
74+
Ok((
75+
std::iter::once((OsStr::new(IPC_ENV_NAME), name)),
76+
ServerHandle { driver, stop_accepting: StopAccepting { token: stop_token } },
77+
))
7378
}
7479

7580
#[cfg(unix)]
@@ -81,7 +86,7 @@ type Bound = Listener;
8186
fn bind_listener() -> io::Result<(OsString, Bound)> {
8287
use interprocess::local_socket::{GenericFilePath, ToFsName};
8388

84-
let bound = tempfile::Builder::new().prefix("vp_run_ipc_").make(|path| {
89+
let bound = tempfile::Builder::new().prefix("vite_task_ipc_").make(|path| {
8590
let name = path.to_fs_name::<GenericFilePath>()?;
8691
ListenerOptions::new().name(name).create_tokio()
8792
})?;
@@ -93,7 +98,7 @@ fn bind_listener() -> io::Result<(OsString, Bound)> {
9398
fn bind_listener() -> io::Result<(OsString, Bound)> {
9499
use interprocess::local_socket::{GenericNamespaced, ToNsName};
95100

96-
let name_str = vite_str::format!("vp_run_ipc_{}", uuid::Uuid::new_v4());
101+
let name_str = vite_str::format!("vite_task_ipc_{}", uuid::Uuid::new_v4());
97102
let name = name_str.as_str().to_ns_name::<GenericNamespaced>()?;
98103
let listener = ListenerOptions::new().name(name).create_tokio()?;
99104
Ok((OsString::from(name_str.as_str()), listener))

crates/vite_task_server/tests/integration.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ where
5050
{
5151
let rt = Builder::new_current_thread().enable_all().build().unwrap();
5252
rt.block_on(async move {
53-
let (name, ServerHandle { driver, stop_accepting }) = serve(handler).expect("bind server");
53+
let (envs, ServerHandle { driver, stop_accepting }) = serve(handler).expect("bind server");
54+
let name = envs.into_iter().next().expect("serve should yield an IPC env").1;
5455

5556
let client = async move {
5657
tokio::task::spawn_blocking(move || client_work(name))

docs/runner-task-ipc/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Rust crates:
2121

2222
1. crates/vite_task_ipc_shared: defines the IPC message types shared between client and server. Uses wincode's zero-copy `SchemaWrite`/`SchemaRead` to minimize allocation. `Request` variants: `IgnoreInput`, `IgnoreOutput`, `GetEnv { id, name, tracked }`, `DisableCache`. Only `GetEnv` expects a `Response` (correlated via `id`); the others are fire-and-forget.
2323
2. vite_task_server: exposes a `Handler` trait and a `serve(&handler, shutdown)` free function that binds a listener (via `interprocess`, auto-cleaned via `tempfile` on Unix) and returns the socket path / pipe name plus a single-threaded future. The future accepts new clients until the `shutdown` future resolves, then stops accepting and waits for every in-flight per-client task to drain (each drains naturally when its client closes the stream — e.g. the task process exits). Uses `FuturesUnordered` (not `spawn_local`) so the handler can be borrowed and hold `!Send` state (`Rc`, `RefCell`) without locking.
24-
3. vite_task_client: a sync, blocking client with `&mut self` methods. Reads `VP_RUN_IPC` and `VP_RUN_CLIENT_NAPI` from the process env to set up the IPC connection, so that env var names are implementation details. Falls back to no-op if the env vars are not defined, so that it won't break the tool when it's not run by the runner.
24+
3. vite_task_client: a sync, blocking client with `&mut self` methods. Reads IPC connection info and the client-napi dylib path from the process env (specific env var names are an implementation detail shared with `vite_task_server`). Falls back to no-op if the envs are not defined, so that it won't break the tool when it's not run by the runner.
2525
4. vite_task_client_napi: a NAPI wrapper around the client, so that tools can require it in JavaScript/TypeScript and use it to communicate with the vite task runner.
2626

2727
Js Packages:
@@ -38,7 +38,7 @@ Workflow:
3838
1. `vite_task_server` uses crate `interprocess` to create a server along with a unique name, and listens to messages from tools.
3939
2. `vite_task` calls vite_task_server to run server for every spawn execution. and collect what's reported from tools, and respone with requested envs
4040
3. `vite_task` embed `vite_task_client_napi` dylib and write to temp folder in the same way as `crates/fspy/src/unix/mod.rs:56`. (we should extract crates/fspy/src/artifact.rs into a separate crate)
41-
4. `vite_task` passes VP_RUN_CLIENT_NAPI env with the path of the dylib to the tool's process env, and VP_RUN_IPC env with the unique name of the server for the tool to connect.
41+
4. `vite_task` passes both the dylib path and the IPC connection info to the tool's process env, via env vars that `vite_task_client` knows to look up.
4242
5. the tool is expected to use `@voidzero-dev/vite-task-client`
4343
6. `@voidzero-dev/vite-task-client` initializes `vite_task_client_napi`, which internally reads the env vars and sets up the connection.
4444

docs/runner-task-ipc/plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
2. **Transport**`vite_task_server` + `vite_task_client`. Build both sides, test them against each other directly in Rust. ✅
55
3. **Extract artifact** — Pull `artifact.rs` out of fspy into a shared crate. Prerequisite for dylib embedding.
66
4. **JS bridge**`vite_task_client_napi` (real impl) + `@voidzero-dev/vite-task-client` (JS wrapper with `fetchEnvs` logic).
7-
5. **Runner integration** — Wire into `vite_task` spawn: start server per execution, embed/extract dylib, pass `VP_RUN_IPC` + `VP_RUN_CLIENT_NAPI`.
7+
5. **Runner integration** — Wire into `vite_task` spawn: start server per execution, embed/extract dylib, inject the IPC envs via `serve()`'s returned iterator.
88
6. **Cache integration** — Runner consumes the reported data (ignored inputs/outputs, requested envs, disable cache) and adjusts caching behavior.

docs/runner-task-ipc/server-design.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub trait Handler {
2323

2424
pub fn serve<'h, H: Handler + 'h>(
2525
handler: H,
26-
) -> io::Result<(OsString, ServerHandle<'h, H>)>;
26+
) -> io::Result<(impl Iterator<Item = (&'static OsStr, OsString)>, ServerHandle<'h, H>)>;
2727

2828
pub struct ServerHandle<'h, H> {
2929
pub driver: LocalBoxFuture<'h, H>,
@@ -55,17 +55,17 @@ Only when fspy is enabled (`cache_metadata.input_config.includes_auto`). No fspy
5555

5656
### Construction (at `ExecutionMode` build time)
5757

58+
`serve()` yields an env-pair iterator that the caller chains directly into the spawn's envs. The specific env var(s) used for IPC handoff are an implementation detail between the server and client crates — the runner never has to know their names.
59+
5860
```rust
59-
let (name, server) = serve(IpcRecorder::new(env_config))?;
60-
let envs = cmd.all_envs.iter()
61-
.map(|(k, v)| (&**k, &**v))
62-
.chain(std::iter::once((OsStr::new("VP_RUN_IPC"), name.as_os_str())));
61+
let (ipc_envs, server) = serve(IpcRecorder::new(env_config))?;
62+
let envs = cmd.all_envs.iter().map(|(k, v)| (&**k, &**v)).chain(ipc_envs);
6363
let child = spawn(&cmd, envs, true, SpawnStdio::Piped, token).await?;
64-
// `name` dropped here — it was only needed for envs.
64+
// After the child is spawned, nothing else needs the IPC envs.
6565

6666
let fspy = FspyState {
6767
negatives,
68-
server, // ServerHandle<IpcRecorder>
68+
server, // ServerHandle<'h, IpcRecorder>
6969
};
7070
```
7171

@@ -80,7 +80,7 @@ struct FspyState {
8080

8181
**Not stored:**
8282

83-
- `name` — consumed once to build envs, dropped immediately.
83+
- IPC env name/value — consumed once to build the spawn envs, dropped immediately.
8484
- `handler` — lives inside `server.driver`'s async state; recovered by value when the driver resolves.
8585

8686
### Driving the server during `pipe_stdio` / `child.wait`
@@ -144,4 +144,4 @@ Earlier direction: "server doesn't care about cancellation token; it simply stop
144144

145145
### On `spawn()` changes (deferred)
146146

147-
`spawn()` will need an `extra_envs`-style parameter (or an `envs: impl IntoIterator<(impl AsRef<OsStr>, impl AsRef<OsStr>)>`) so the caller can inject `VP_RUN_IPC` without cloning `Arc<BTreeMap>`. Not part of this step.
147+
`spawn()` will need to accept extra envs (e.g. `envs: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>`) so the caller can inject the IPC envs without cloning `Arc<BTreeMap>`. Not part of this step.

docs/runner-task-ipc/transport.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Cross-platform IPC via `interprocess` crate:
77
| Unix (macOS/Linux) | Unix domain socket |
88
| Windows | Named pipe |
99

10-
The socket path or pipe name is passed to the task process via the `VP_RUN_IPC` env var. Clients check for its presence and skip IPC gracefully if absent.
10+
The socket path or pipe name is passed to the task process via an env var shared between `vite_task_server` and `vite_task_client` (the specific name is an implementation detail). Clients check for its presence and skip IPC gracefully if absent.
1111

1212
## Server Model
1313

0 commit comments

Comments
 (0)