@volter/tabnode 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{runtime-worker-CUO9SNFs.js → runtime-worker-LWfW6VWE.js} +180 -52
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +207 -69
- package/dist/node-lib/binding/fs.d.ts +6 -6
- package/dist/node-lib/binding/process_wrap.d.ts +1 -1
- package/dist/process-tokens.d.ts +5 -0
- package/dist/server-bridge.d.ts +1 -0
- package/dist/shims/child_process.d.ts +6 -0
- package/dist/virtual-fs.d.ts +3 -0
- package/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/node-lib/binding/fs.ts +41 -35
- package/src/node-lib/binding/handles.ts +27 -1
- package/src/node-lib/binding/process_wrap.ts +23 -10
- package/src/process-tokens.ts +5 -0
- package/src/server-bridge.ts +12 -2
- package/src/shims/child_process.ts +61 -4
- package/src/virtual-fs.ts +15 -1
package/dist/virtual-fs.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface FSNode {
|
|
|
28
28
|
target?: string;
|
|
29
29
|
children?: Map<string, FSNode>;
|
|
30
30
|
mtime: number;
|
|
31
|
+
mode?: number;
|
|
31
32
|
/**
|
|
32
33
|
* When the file was last read. Node reports it apart from the write time
|
|
33
34
|
* and `utimes` sets the two separately; this filesystem reported the write
|
|
@@ -154,6 +155,8 @@ export declare class VirtualFS {
|
|
|
154
155
|
/**
|
|
155
156
|
* Get stats for path
|
|
156
157
|
*/
|
|
158
|
+
chmodSync(path: string, mode: number): void;
|
|
159
|
+
utimesSync(path: string, atime: number | Date, mtime: number | Date): void;
|
|
157
160
|
statSync(path: string): Stats;
|
|
158
161
|
/**
|
|
159
162
|
* lstatSync - same as statSync for our virtual FS (no symlinks)
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface RunOptions {
|
|
|
68
68
|
env?: Record<string, string>;
|
|
69
69
|
/** What the shell reads on stdin, so a builtin reads what was piped to it. */
|
|
70
70
|
stdin?: string;
|
|
71
|
+
stdinStream?: AsyncIterable<Uint8Array>;
|
|
72
|
+
terminal?: { columns: number; rows: number; onResize?: (listener: (columns: number, rows: number) => void) => () => void };
|
|
71
73
|
/** Callback for streaming stdout chunks as they arrive (for long-running commands like vitest watch) */
|
|
72
74
|
onStdout?: (data: string) => void;
|
|
73
75
|
/** Callback for streaming stderr chunks as they arrive */
|
|
@@ -168,6 +170,9 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
168
170
|
onStderr: runOptions?.onStderr,
|
|
169
171
|
signal: runOptions?.signal,
|
|
170
172
|
held: runOptions?.held === true,
|
|
173
|
+
stdinStream: runOptions?.stdinStream,
|
|
174
|
+
stdinOpen: runOptions?.stdinStream !== undefined,
|
|
175
|
+
terminal: runOptions?.terminal,
|
|
171
176
|
});
|
|
172
177
|
|
|
173
178
|
return new Promise((resolve) => {
|
|
@@ -180,7 +185,7 @@ export function createContainer(options?: ContainerOptions): {
|
|
|
180
185
|
resolve({
|
|
181
186
|
stdout: String(stdout),
|
|
182
187
|
stderr: String(stderr),
|
|
183
|
-
exitCode: error ? (error.code ?? 1) : 0,
|
|
188
|
+
exitCode: runOptions?.signal?.aborted ? 143 : error ? (error.code ?? 1) : 0,
|
|
184
189
|
});
|
|
185
190
|
});
|
|
186
191
|
});
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* own tree.
|
|
29
29
|
*/
|
|
30
30
|
import { createNodeError as vfsError } from '../../virtual-fs';
|
|
31
|
+
import { registerHandle, releaseHandle, refHandle, unrefHandle, handleHasRef, currentOwner } from './handles';
|
|
32
|
+
import { enterRun } from '../../process-tokens';
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* An errno error, as the filesystem makes one. The tree's own maker knows the
|
|
@@ -51,8 +53,10 @@ function refuseLinks(syscall: string, path: string): never {
|
|
|
51
53
|
);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
function unsupportedMetadata(syscall: string): never {
|
|
57
|
+
throw Object.assign(new Error(`ENOTSUP: filesystem does not support ${syscall}`), { code: 'ENOTSUP', syscall });
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
|
|
57
61
|
function treeHoldsLinks(): boolean {
|
|
58
62
|
return (vfs() as VirtualFS & { holdsLinks?: boolean }).holdsLinks === true;
|
|
@@ -173,11 +177,10 @@ function statArray(stats: VfsStats, bigint: boolean, path?: string): Float64Arra
|
|
|
173
177
|
// The tree keeps permissions but not the type bits; `Stats.isFile()` is
|
|
174
178
|
// `mode & S_IFMT`, so the type the tree does know is put where Node looks.
|
|
175
179
|
// `chmod` stores what it set; a path it has not touched keeps the tree's.
|
|
176
|
-
const
|
|
177
|
-
const permissions = (stored !== undefined ? stored : Number(stats.mode ?? 0)) & 0o7777;
|
|
180
|
+
const permissions = Number(stats.mode ?? (stats.isDirectory?.() ? 0o755 : 0o644)) & 0o7777;
|
|
178
181
|
const type = stats.isDirectory?.() ? S_IFDIR : stats.isSymbolicLink?.() ? S_IFLNK : S_IFREG;
|
|
179
182
|
const values = [
|
|
180
|
-
Number(stats.dev ?? 0), type |
|
|
183
|
+
Number(stats.dev ?? 0), type | permissions, Number(stats.nlink ?? 1),
|
|
181
184
|
Number(stats.uid ?? 0), Number(stats.gid ?? 0), Number(stats.rdev ?? 0),
|
|
182
185
|
Number(stats.blksize ?? 4096), Number(stats.ino ?? 0), Number(stats.size ?? 0),
|
|
183
186
|
Number(stats.blocks ?? Math.ceil(Number(stats.size ?? 0) / 512)),
|
|
@@ -366,16 +369,23 @@ export class FSEvent {
|
|
|
366
369
|
#watcher: { close(): void } | null = null;
|
|
367
370
|
initialized = false;
|
|
368
371
|
|
|
369
|
-
start(path: unknown,
|
|
372
|
+
start(path: unknown, persistent?: boolean, recursive?: boolean, _encoding?: string): number {
|
|
370
373
|
const name = asPath(path);
|
|
371
374
|
const tree = vfs();
|
|
375
|
+
const cwd = callingCwd();
|
|
376
|
+
const owner = currentOwner();
|
|
372
377
|
try {
|
|
373
378
|
this.#watcher = tree.watch(name, { recursive: Boolean(recursive) }, ((event: string, filename: string | null) => {
|
|
374
379
|
// libuv names a change `change` and a create or a remove `rename`,
|
|
375
380
|
// which is what Node's `FSWatcher` turns into its own two events.
|
|
376
|
-
|
|
381
|
+
const notify = () => withFilesystem(tree, () => withCwd(cwd, () =>
|
|
382
|
+
this.onchange?.(0, event === 'rename' ? 'rename' : 'change', filename ?? '')));
|
|
383
|
+
if (owner === null) notify();
|
|
384
|
+
else enterRun(owner, notify);
|
|
377
385
|
}) as never) as unknown as { close(): void };
|
|
378
386
|
this.initialized = true;
|
|
387
|
+
registerHandle(this);
|
|
388
|
+
if (persistent === false) unrefHandle(this);
|
|
379
389
|
return 0;
|
|
380
390
|
} catch (error) {
|
|
381
391
|
const code = (error as { code?: string }).code;
|
|
@@ -384,15 +394,16 @@ export class FSEvent {
|
|
|
384
394
|
}
|
|
385
395
|
|
|
386
396
|
close(): void {
|
|
397
|
+
releaseHandle(this);
|
|
387
398
|
this.initialized = false;
|
|
388
399
|
try { this.#watcher?.close(); } catch { /* a watcher already closed is closed */ }
|
|
389
400
|
this.#watcher = null;
|
|
390
401
|
}
|
|
391
402
|
|
|
392
|
-
ref(): void {}
|
|
393
|
-
unref(): void {}
|
|
403
|
+
ref(): void { if (this.initialized) refHandle(this); }
|
|
404
|
+
unref(): void { unrefHandle(this); }
|
|
394
405
|
getAsyncId(): number { return 0; }
|
|
395
|
-
hasRef(): boolean { return this.initialized; }
|
|
406
|
+
hasRef(): boolean { return this.initialized && handleHasRef(this); }
|
|
396
407
|
}
|
|
397
408
|
|
|
398
409
|
/**
|
|
@@ -466,6 +477,9 @@ export const fsBinding = {
|
|
|
466
477
|
const tree = vfs();
|
|
467
478
|
const bits = flagBits();
|
|
468
479
|
const exists = tree.existsSync(name);
|
|
480
|
+
if (exists && ((flags & 3) !== 0 || (flags & (bits.create | bits.truncate | bits.append)) !== 0)) {
|
|
481
|
+
tree.accessSync(name, 2);
|
|
482
|
+
}
|
|
469
483
|
if ((flags & bits.excl) !== 0 && (flags & bits.create) !== 0 && exists) throw createNodeError('EEXIST', 'open', name);
|
|
470
484
|
if (!exists) {
|
|
471
485
|
if ((flags & bits.create) === 0) throw createNodeError('ENOENT', 'open', name);
|
|
@@ -623,10 +637,10 @@ export const fsBinding = {
|
|
|
623
637
|
});
|
|
624
638
|
},
|
|
625
639
|
|
|
626
|
-
access(path: unknown,
|
|
640
|
+
access(path: unknown, mode: number, req?: FSReq): undefined {
|
|
627
641
|
return answer(req, () => {
|
|
628
642
|
const name = asPath(path);
|
|
629
|
-
|
|
643
|
+
vfs().accessSync(name, mode);
|
|
630
644
|
return undefined;
|
|
631
645
|
});
|
|
632
646
|
},
|
|
@@ -737,13 +751,10 @@ export const fsBinding = {
|
|
|
737
751
|
if (tree.existsSync(name)) {
|
|
738
752
|
const stats = tree.statSync(name);
|
|
739
753
|
if (!recursive || !stats.isDirectory()) throw createNodeError('EEXIST', 'mkdir', name);
|
|
740
|
-
const stored = pathModes.get(name);
|
|
741
|
-
if (stored !== undefined && (stored & 0o777) !== wanted) throw createNodeError('EEXIST', 'mkdir', name);
|
|
742
|
-
pathModes.set(name, wanted);
|
|
743
754
|
return undefined;
|
|
744
755
|
}
|
|
745
756
|
tree.mkdirSync(name, { recursive });
|
|
746
|
-
|
|
757
|
+
tree.chmodSync(name, wanted);
|
|
747
758
|
return undefined;
|
|
748
759
|
});
|
|
749
760
|
},
|
|
@@ -817,37 +828,32 @@ export const fsBinding = {
|
|
|
817
828
|
});
|
|
818
829
|
},
|
|
819
830
|
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
// are kept here, because a program that sets a mode and stats it back must
|
|
823
|
-
// see what it set, and a missing path is ENOENT as Node's is.
|
|
831
|
+
// Metadata belongs to the filesystem, including a host's permission view.
|
|
832
|
+
// Never keep a realm-global mode table or bypass that view for descriptors.
|
|
824
833
|
chmod(path: unknown, mode: number, req?: FSReq): undefined {
|
|
825
834
|
return answer(req, () => {
|
|
826
835
|
const name = asPath(path);
|
|
827
|
-
|
|
828
|
-
pathModes.set(name, mode & 0o7777);
|
|
836
|
+
vfs().chmodSync(name, mode);
|
|
829
837
|
return undefined;
|
|
830
838
|
});
|
|
831
839
|
},
|
|
832
840
|
fchmod(fd: number, mode: number, req?: FSReq): undefined {
|
|
833
841
|
return answer(req, () => {
|
|
834
842
|
const file = fileFor(fd);
|
|
835
|
-
|
|
843
|
+
file.tree.chmodSync(file.path, mode);
|
|
836
844
|
return undefined;
|
|
837
845
|
});
|
|
838
846
|
},
|
|
839
|
-
chown(
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
futimes(_fd: number, _atime: number, _mtime: number, req?: FSReq): undefined { return answer(req, () => undefined); },
|
|
850
|
-
lutimes(_path: unknown, _atime: number, _mtime: number, req?: FSReq): undefined { return answer(req, () => undefined); },
|
|
847
|
+
chown(_path: unknown, _uid: number, _gid: number, req?: FSReq): undefined { return answer(req, () => unsupportedMetadata('chown')); },
|
|
848
|
+
fchown(_fd: number, _uid: number, _gid: number, req?: FSReq): undefined { return answer(req, () => unsupportedMetadata('fchown')); },
|
|
849
|
+
lchown(_path: unknown, _uid: number, _gid: number, req?: FSReq): undefined { return answer(req, () => unsupportedMetadata('lchown')); },
|
|
850
|
+
utimes(path: unknown, atime: number, mtime: number, req?: FSReq): undefined { return answer(req, () => { vfs().utimesSync(asPath(path), new Date(atime * 1000), new Date(mtime * 1000)); return undefined; }); },
|
|
851
|
+
futimes(fd: number, atime: number, mtime: number, req?: FSReq): undefined { return answer(req, () => { const file = fileFor(fd); file.tree.utimesSync(file.path, new Date(atime * 1000), new Date(mtime * 1000)); return undefined; }); },
|
|
852
|
+
lutimes(path: unknown, atime: number, mtime: number, req?: FSReq): undefined { return answer(req, () => {
|
|
853
|
+
if (treeHoldsLinks()) return unsupportedMetadata('lutimes');
|
|
854
|
+
vfs().utimesSync(asPath(path), new Date(atime * 1000), new Date(mtime * 1000));
|
|
855
|
+
return undefined;
|
|
856
|
+
}); },
|
|
851
857
|
fsync(_fd: number, req?: FSReq): undefined { return answer(req, () => undefined); },
|
|
852
858
|
fdatasync(_fd: number, req?: FSReq): undefined { return answer(req, () => undefined); },
|
|
853
859
|
|
|
@@ -23,6 +23,8 @@ export interface OwnedHandle {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const owners = new Map<ProcessToken, Set<OwnedHandle>>();
|
|
26
|
+
// Unref affects liveness, not ownership: exit must close unreferenced handles too.
|
|
27
|
+
const allOwned = new Map<ProcessToken, Set<OwnedHandle>>();
|
|
26
28
|
const heldBy = new WeakMap<OwnedHandle, ProcessToken | null>();
|
|
27
29
|
const refHeld = new WeakSet<OwnedHandle>();
|
|
28
30
|
/**
|
|
@@ -68,6 +70,12 @@ export function ownerOf(handle: OwnedHandle): ProcessToken | null {
|
|
|
68
70
|
/** A new handle is the current run's, ref'd and active, as libuv's start. */
|
|
69
71
|
export function registerHandle(handle: OwnedHandle): void {
|
|
70
72
|
heldBy.set(handle, currentOwner());
|
|
73
|
+
const token = heldBy.get(handle);
|
|
74
|
+
if (token != null) {
|
|
75
|
+
let owned = allOwned.get(token);
|
|
76
|
+
if (!owned) { owned = new Set(); allOwned.set(token, owned); }
|
|
77
|
+
owned.add(handle);
|
|
78
|
+
}
|
|
71
79
|
refHeld.add(handle);
|
|
72
80
|
activeHandles.add(handle);
|
|
73
81
|
hold(handle);
|
|
@@ -103,6 +111,12 @@ export function handleHasRef(handle: OwnedHandle): boolean {
|
|
|
103
111
|
/** A closed handle holds nothing. */
|
|
104
112
|
export function releaseHandle(handle: OwnedHandle): void {
|
|
105
113
|
drop(handle);
|
|
114
|
+
const token = heldBy.get(handle);
|
|
115
|
+
if (token != null) {
|
|
116
|
+
const owned = allOwned.get(token);
|
|
117
|
+
owned?.delete(handle);
|
|
118
|
+
if (owned?.size === 0) allOwned.delete(token);
|
|
119
|
+
}
|
|
106
120
|
heldBy.delete(handle);
|
|
107
121
|
refHeld.delete(handle);
|
|
108
122
|
activeHandles.delete(handle);
|
|
@@ -161,8 +175,9 @@ export function __releaseOwnedServers(token: ProcessToken, stopTimers = true, st
|
|
|
161
175
|
* Called where the run's servers and timers are released.
|
|
162
176
|
*/
|
|
163
177
|
export function __releaseOwnedHandles(token: ProcessToken): void {
|
|
164
|
-
const held =
|
|
178
|
+
const held = allOwned.get(token);
|
|
165
179
|
if (!held) return;
|
|
180
|
+
allOwned.delete(token);
|
|
166
181
|
owners.delete(token);
|
|
167
182
|
for (const handle of [...held]) {
|
|
168
183
|
try { handle.close(); } catch { /* a handle already closed is already released */ }
|
|
@@ -182,6 +197,17 @@ export function __releaseOwnedHandles(token: ProcessToken): void {
|
|
|
182
197
|
export function __adoptHandle(handle: OwnedHandle | null | undefined, token: ProcessToken | null): void {
|
|
183
198
|
if (!handle) return;
|
|
184
199
|
drop(handle);
|
|
200
|
+
const previous = heldBy.get(handle);
|
|
201
|
+
if (previous != null) {
|
|
202
|
+
const owned = allOwned.get(previous);
|
|
203
|
+
owned?.delete(handle);
|
|
204
|
+
if (owned?.size === 0) allOwned.delete(previous);
|
|
205
|
+
}
|
|
185
206
|
heldBy.set(handle, token);
|
|
207
|
+
if (token !== null) {
|
|
208
|
+
let owned = allOwned.get(token);
|
|
209
|
+
if (!owned) { owned = new Set(); allOwned.set(token, owned); }
|
|
210
|
+
owned.add(handle);
|
|
211
|
+
}
|
|
186
212
|
hold(handle);
|
|
187
213
|
}
|
|
@@ -25,9 +25,9 @@ import { Pipe, constants as pipeConstants } from './pipe_wrap';
|
|
|
25
25
|
import { UV_ENOENT, UV_ESRCH } from './uv';
|
|
26
26
|
import {
|
|
27
27
|
registerHandle, refHandle, unrefHandle, handleHasRef, releaseHandle, __adoptHandle,
|
|
28
|
-
type OwnedHandle,
|
|
28
|
+
ownerOf, type OwnedHandle,
|
|
29
29
|
} from './handles';
|
|
30
|
-
import type
|
|
30
|
+
import { __runFor, type ProcessToken } from '../../process-tokens';
|
|
31
31
|
|
|
32
32
|
/** One entry of Node's `options.stdio`, as `getValidStdio` builds it. */
|
|
33
33
|
export interface StdioEntry {
|
|
@@ -154,18 +154,31 @@ function signalNameOf(signal: number | string): string {
|
|
|
154
154
|
* `/work` means `/work/child.js`, and resolved against the engine's shell's
|
|
155
155
|
* own root it meant `/child.js` and a child that exited 1.
|
|
156
156
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
// A shared realm's process global is whichever guest ran last. The spawning
|
|
158
|
+
// handle belongs to the parent: inherited stdio and cwd must come from that
|
|
159
|
+
// parent's process, even while a sibling starts or exits. A failed fork was
|
|
160
|
+
// replaying stderr into its own closed IPC-backed stream (write EBADF).
|
|
161
|
+
function parentProcess(token: ProcessToken | null) {
|
|
162
|
+
return token !== null ? __runFor(token)?.process
|
|
163
|
+
: (globalThis as unknown as { process?: { cwd?: () => string; stdout?: { write(text: string): unknown }; stderr?: { write(text: string): unknown } } }).process;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function spawningDirectory(token: ProcessToken | null): string | undefined {
|
|
167
|
+
const realm = parentProcess(token);
|
|
159
168
|
if (typeof realm?.cwd !== 'function') return undefined;
|
|
160
169
|
try { return realm.cwd(); } catch { return undefined; }
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
/** Where an `inherit` entry's bytes go: the spawning program's own stream. */
|
|
164
|
-
function inheritedWriter(fd: number): ((text: string) => void) | null {
|
|
173
|
+
function inheritedWriter(fd: number, token: ProcessToken | null): ((text: string) => void) | null {
|
|
165
174
|
if (fd !== 1 && fd !== 2) return null;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
175
|
+
// Inherit the descriptor's original sink, not a guest replacement of
|
|
176
|
+
// process.stderr.write (a child commonly redirects its console over IPC).
|
|
177
|
+
if (token !== null) {
|
|
178
|
+
const parent = __runFor(token);
|
|
179
|
+
return (fd === 1 ? parent?.stdout : parent?.stderr) ?? null;
|
|
180
|
+
}
|
|
181
|
+
const realm = parentProcess(token);
|
|
169
182
|
const stream = fd === 1 ? realm?.stdout : realm?.stderr;
|
|
170
183
|
if (!stream || typeof stream.write !== 'function') return null;
|
|
171
184
|
return (text: string) => {
|
|
@@ -230,7 +243,7 @@ export class Process implements OwnedHandle {
|
|
|
230
243
|
const request: RunRequest = {
|
|
231
244
|
file: options.file,
|
|
232
245
|
args: options.args ?? [options.file],
|
|
233
|
-
cwd: options.cwd ?? spawningDirectory(),
|
|
246
|
+
cwd: options.cwd ?? spawningDirectory(ownerOf(this)),
|
|
234
247
|
env,
|
|
235
248
|
detached: options.detached === true,
|
|
236
249
|
stdout: null,
|
|
@@ -270,7 +283,7 @@ export class Process implements OwnedHandle {
|
|
|
270
283
|
request.stderrIsPipe = true;
|
|
271
284
|
}
|
|
272
285
|
} else if (entry.type === 'inherit' || entry.type === 'fd') {
|
|
273
|
-
const inherited = inheritedWriter(entry.fd ?? index);
|
|
286
|
+
const inherited = inheritedWriter(entry.fd ?? index, ownerOf(this));
|
|
274
287
|
if (index === 1) request.stdout = inherited;
|
|
275
288
|
else request.stderr = inherited;
|
|
276
289
|
}
|
package/src/process-tokens.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* command gives its process has it removed.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { Process } from './shims/process';
|
|
18
19
|
import { AsyncLocalStorage } from './shims/async_hooks';
|
|
19
20
|
|
|
20
21
|
/** Whatever the embedding runtime uses to name one guest process. */
|
|
@@ -25,6 +26,10 @@ export const PROCESS_TOKEN_ENV = '__TABNODE_PROCESS_TOKEN';
|
|
|
25
26
|
|
|
26
27
|
/** What the engine can answer about one named run. */
|
|
27
28
|
export interface OwnedRun {
|
|
29
|
+
/** The guest whose cwd and inherited streams belong to this run. */
|
|
30
|
+
process: Process;
|
|
31
|
+
stdout(text: string): void;
|
|
32
|
+
stderr(text: string): void;
|
|
28
33
|
/** The timers the run's guest still holds, as Node's loop counts them. */
|
|
29
34
|
pendingTimers(): number;
|
|
30
35
|
/** Clear those timers, as ending the process clears them. */
|
package/src/server-bridge.ts
CHANGED
|
@@ -417,16 +417,26 @@ export class ServerBridge extends EventEmitter {
|
|
|
417
417
|
* news comes from the net binding, because that is where a port is taken;
|
|
418
418
|
* the engine's `http` is Node's own file and knows nothing of a page.
|
|
419
419
|
*/
|
|
420
|
+
private readonly guestServers = new WeakSet<IVirtualServer>();
|
|
421
|
+
|
|
420
422
|
private watchGuestPorts(): void {
|
|
421
423
|
setPortWatchers(
|
|
422
424
|
(port, address) => {
|
|
423
425
|
if (this.servers.has(port)) return;
|
|
424
426
|
const hostname = address || '0.0.0.0';
|
|
425
|
-
|
|
427
|
+
const server = guestListeningServer(port, hostname);
|
|
428
|
+
this.guestServers.add(server);
|
|
429
|
+
this.registerServer(server, port, hostname);
|
|
426
430
|
},
|
|
427
431
|
(port) => {
|
|
428
432
|
const entry = this.servers.get(port);
|
|
429
|
-
|
|
433
|
+
// Closing a guest's listener releases its bridge port too. Guest
|
|
434
|
+
// registrations carry a request adapter now, not the old null sentinel;
|
|
435
|
+
// a host replacement on that port must still be left alone.
|
|
436
|
+
if (entry && (entry.server === null || this.guestServers.has(entry.server))) {
|
|
437
|
+
if (entry.server) entry.server.listening = false;
|
|
438
|
+
this.unregisterServer(port);
|
|
439
|
+
}
|
|
430
440
|
},
|
|
431
441
|
);
|
|
432
442
|
}
|
|
@@ -127,6 +127,8 @@ type RunStdin = { emit: (event: string, ...args: unknown[]) => void; push: (chun
|
|
|
127
127
|
* output went down the child's channel.
|
|
128
128
|
*/
|
|
129
129
|
export interface RunStreams {
|
|
130
|
+
stdinStream?: AsyncIterable<Uint8Array>;
|
|
131
|
+
terminal?: { columns: number; rows: number; onResize?: (listener: (columns: number, rows: number) => void) => () => void };
|
|
130
132
|
onStdout?: (data: string) => void;
|
|
131
133
|
onStderr?: (data: string) => void;
|
|
132
134
|
signal?: AbortSignal;
|
|
@@ -470,7 +472,7 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
470
472
|
// open, as a pipe whose writer has not closed.
|
|
471
473
|
stdin: typeof ctx.stdin === 'string' ? ctx.stdin : '',
|
|
472
474
|
...(streams?.held || streams?.stdinOpen ? { stdinHeld: true } : {}),
|
|
473
|
-
...(streams?.held ? { tty: true } : {}),
|
|
475
|
+
...(streams?.held || streams?.terminal ? { tty: true } : {}),
|
|
474
476
|
// The numbers this run was started with, so the guest's `process.pid`
|
|
475
477
|
// is the one its parent's handle carries.
|
|
476
478
|
...(runPid(runToken) ? { pid: runPid(runToken)!.pid, ppid: runPid(runToken)!.ppid } : {}),
|
|
@@ -483,6 +485,9 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
483
485
|
// whether a pid is a live process.
|
|
484
486
|
if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid);
|
|
485
487
|
const releaseRun = runToken === null ? null : __recordRun(runToken, {
|
|
488
|
+
process: proc,
|
|
489
|
+
stdout: appendStdout,
|
|
490
|
+
stderr: appendStderr,
|
|
486
491
|
pendingTimers: () => pendingGuestTimers(proc),
|
|
487
492
|
stopTimers: () => stopGuestTimers(proc),
|
|
488
493
|
reportUncaught: (error: unknown) => __reportUncaughtException(proc, error),
|
|
@@ -541,12 +546,26 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
541
546
|
// For long-running commands (watch mode), report as TTY so tools like
|
|
542
547
|
// vitest set up interactive features (file watching, stdin commands). A
|
|
543
548
|
// spawned child's stdio is a pipe, as Node's is, and gets none of this.
|
|
544
|
-
if (streams?.held) {
|
|
549
|
+
if (streams?.held || streams?.terminal) {
|
|
545
550
|
proc.stdout.isTTY = true;
|
|
546
551
|
proc.stderr.isTTY = true;
|
|
547
552
|
proc.stdin.isTTY = true;
|
|
548
553
|
proc.stdin.setRawMode = () => proc.stdin;
|
|
549
554
|
}
|
|
555
|
+
const terminal = streams?.terminal;
|
|
556
|
+
const resize = (columns: number, rows: number): void => {
|
|
557
|
+
for (const stream of [proc.stdout, proc.stderr]) {
|
|
558
|
+
stream.columns = columns;
|
|
559
|
+
stream.rows = rows;
|
|
560
|
+
stream.emit('resize');
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
let stopResize: (() => void) | undefined;
|
|
564
|
+
let inputActive = true;
|
|
565
|
+
let inputIterator: AsyncIterator<Uint8Array> | undefined;
|
|
566
|
+
const readableInput = proc.stdin as unknown as { listenerCount(event: string): number; readableLength: number; readableHighWaterMark: number; readableFlowing: boolean | null; _readableState?: { reading?: boolean; needReadable?: boolean } };
|
|
567
|
+
const inputReading = () => readableInput.readableFlowing === true || readableInput._readableState?.reading === true
|
|
568
|
+
|| readableInput._readableState?.needReadable === true || readableInput.listenerCount('data') > 0 || readableInput.listenerCount('readable') > 0;
|
|
550
569
|
|
|
551
570
|
// A child started with a channel wires its own end of it before its
|
|
552
571
|
// module runs, which is what `lib/internal/process/pre_execution.js` does
|
|
@@ -606,6 +625,41 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
606
625
|
};
|
|
607
626
|
const detachUncaught = __onUncaughtException(proc, onUncaughtException);
|
|
608
627
|
try {
|
|
628
|
+
// The host's pipe delivers bytes after the entry has attached its reader.
|
|
629
|
+
// EOF closes that same run's stream, never the most recently started run.
|
|
630
|
+
if (streams?.stdinStream) {
|
|
631
|
+
inputIterator = streams.stdinStream[Symbol.asyncIterator]();
|
|
632
|
+
const input = inputIterator;
|
|
633
|
+
void (async () => {
|
|
634
|
+
const push = (chunk: Uint8Array | null) => {
|
|
635
|
+
if (runToken === null) proc.stdin.push(chunk);
|
|
636
|
+
else enterRun(runToken, () => proc.stdin.push(chunk));
|
|
637
|
+
};
|
|
638
|
+
try {
|
|
639
|
+
while (inputActive && !streams.signal?.aborted) {
|
|
640
|
+
// A pipe is read on demand. Bound queued bytes by the Readable's
|
|
641
|
+
// high-water mark and do not pull a producer nobody consumes.
|
|
642
|
+
while (inputActive && !streams.signal?.aborted && (!inputReading()
|
|
643
|
+
|| readableInput.readableLength >= readableInput.readableHighWaterMark)) {
|
|
644
|
+
await new Promise(resolve => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, resolve, 10));
|
|
645
|
+
}
|
|
646
|
+
if (!inputActive || streams.signal?.aborted) break;
|
|
647
|
+
const next = await input.next();
|
|
648
|
+
if (next.done) break;
|
|
649
|
+
if (!inputActive || streams.signal?.aborted) break;
|
|
650
|
+
push(next.value);
|
|
651
|
+
}
|
|
652
|
+
if (inputActive && !streams.signal?.aborted) push(null);
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (inputActive) onUncaughtException(error);
|
|
655
|
+
} finally { streams.stdinOpen = false; }
|
|
656
|
+
})();
|
|
657
|
+
}
|
|
658
|
+
if (terminal) resize(terminal.columns, terminal.rows);
|
|
659
|
+
stopResize = terminal?.onResize?.((columns, rows) => {
|
|
660
|
+
if (runToken === null) resize(columns, rows);
|
|
661
|
+
else enterRun(runToken, () => resize(columns, rows));
|
|
662
|
+
});
|
|
609
663
|
|
|
610
664
|
let entrySettling: Promise<unknown> | undefined;
|
|
611
665
|
try {
|
|
@@ -676,7 +730,7 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
676
730
|
// handle is one socket back to the server that forked it and which sets no
|
|
677
731
|
// timer — as idle, with exit 0, three times over.
|
|
678
732
|
const __ownsHandles = () => runToken !== null && (__ownedServerPorts(runToken).length > 0 || __ownedHandleCount(runToken) > 0);
|
|
679
|
-
const __printedThenWorking = () => pendingGuestTimers(proc) > 0 || heldWork().count > 0 || __ownsHandles();
|
|
733
|
+
const __printedThenWorking = () => (streams?.stdinOpen === true && inputReading()) || pendingGuestTimers(proc) > 0 || heldWork().count > 0 || __ownsHandles();
|
|
680
734
|
if ((stdout.length > 0 || stderr.length > 0) && !__printedThenWorking()) {
|
|
681
735
|
// Settling the command is host work. Killing guest timers must not
|
|
682
736
|
// cancel this continuation and leave the command's promise unresolved.
|
|
@@ -725,7 +779,7 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
725
779
|
let idleMs = 0;
|
|
726
780
|
// A timer the guest still holds is work in Node's loop, whether or not
|
|
727
781
|
// the program has printed; so is a handle it has open.
|
|
728
|
-
const stillWorking = (): boolean => pendingGuestTimers(proc) > 0 || __ownsHandles();
|
|
782
|
+
const stillWorking = (): boolean => (streams?.stdinOpen === true && inputReading()) || pendingGuestTimers(proc) > 0 || __ownsHandles();
|
|
729
783
|
|
|
730
784
|
// When an abort signal is present (e.g. watch mode), don't apply idle timeout —
|
|
731
785
|
// only exit when aborted or process.exit is called.
|
|
@@ -787,6 +841,9 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
787
841
|
} finally {
|
|
788
842
|
detachRejections();
|
|
789
843
|
detachUncaught();
|
|
844
|
+
inputActive = false;
|
|
845
|
+
try { void Promise.resolve(inputIterator?.return?.()).catch(() => {}); } catch { /* producer cleanup cannot prevent process cleanup */ }
|
|
846
|
+
try { stopResize?.(); } catch { /* host cleanup cannot prevent handle release */ }
|
|
790
847
|
// A process that ends releases its listening sockets, whatever ended it:
|
|
791
848
|
// a return from the entry, a throw, an exit. Only `process.exit` released
|
|
792
849
|
// them here, so `vite --host` — which opened 5173 and then died at startup
|
package/src/virtual-fs.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface FSNode {
|
|
|
35
35
|
target?: string;
|
|
36
36
|
children?: Map<string, FSNode>;
|
|
37
37
|
mtime: number;
|
|
38
|
+
mode?: number;
|
|
38
39
|
/**
|
|
39
40
|
* When the file was last read. Node reports it apart from the write time
|
|
40
41
|
* and `utimes` sets the two separately; this filesystem reported the write
|
|
@@ -470,6 +471,19 @@ export class VirtualFS {
|
|
|
470
471
|
/**
|
|
471
472
|
* Get stats for path
|
|
472
473
|
*/
|
|
474
|
+
chmodSync(path: string, mode: number): void {
|
|
475
|
+
const node = this.getNode(path);
|
|
476
|
+
if (!node) throw createNodeError('ENOENT', 'chmod', path);
|
|
477
|
+
node.mode = mode & 0o7777;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
utimesSync(path: string, atime: number | Date, mtime: number | Date): void {
|
|
481
|
+
const node = this.getNode(path);
|
|
482
|
+
if (!node) throw createNodeError('ENOENT', 'utimes', path);
|
|
483
|
+
node.atime = atime instanceof Date ? atime.getTime() : atime * 1000;
|
|
484
|
+
node.mtime = mtime instanceof Date ? mtime.getTime() : mtime * 1000;
|
|
485
|
+
}
|
|
486
|
+
|
|
473
487
|
statSync(path: string): Stats {
|
|
474
488
|
const node = this.getNode(path);
|
|
475
489
|
if (!node) {
|
|
@@ -489,7 +503,7 @@ export class VirtualFS {
|
|
|
489
503
|
isFIFO: () => false,
|
|
490
504
|
isSocket: () => false,
|
|
491
505
|
size,
|
|
492
|
-
mode: node.type === 'directory' ? 0o755 : 0o644,
|
|
506
|
+
mode: node.mode ?? (node.type === 'directory' ? 0o755 : 0o644),
|
|
493
507
|
mtime: new Date(mtime),
|
|
494
508
|
atime: new Date(atime),
|
|
495
509
|
ctime: new Date(mtime),
|