@volter/tabnode 0.3.0 → 0.4.0
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-BcDCBbhG.js → runtime-worker-DoV8lqBn.js} +202 -61
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +229 -78
- package/dist/node-lib/binding/fs.d.ts +8 -8
- 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 +66 -48
- package/src/node-lib/binding/handles.ts +27 -1
- package/src/server-bridge.ts +12 -2
- package/src/shims/child_process.ts +58 -4
- package/src/virtual-fs.ts +28 -3
|
@@ -45,7 +45,7 @@ export declare class FSEvent {
|
|
|
45
45
|
#private;
|
|
46
46
|
onchange: ((status: number, event: string, filename: string) => void) | null;
|
|
47
47
|
initialized: boolean;
|
|
48
|
-
start(path: unknown,
|
|
48
|
+
start(path: unknown, persistent?: boolean, recursive?: boolean, _encoding?: string): number;
|
|
49
49
|
close(): void;
|
|
50
50
|
ref(): void;
|
|
51
51
|
unref(): void;
|
|
@@ -95,11 +95,11 @@ export declare const fsBinding: {
|
|
|
95
95
|
read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number, req?: FSReq): number | undefined;
|
|
96
96
|
readBuffers(fd: number, buffers: Uint8Array[], position: number, req?: FSReq): number | undefined;
|
|
97
97
|
/** Node's fast path for `readFile` with an encoding it can decode itself. */
|
|
98
|
-
readFileUtf8(path: unknown,
|
|
98
|
+
readFileUtf8(path: unknown, flags: number): string;
|
|
99
99
|
writeBuffer(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, req?: FSReq): number | undefined;
|
|
100
100
|
writeBuffers(fd: number, buffers: Uint8Array[], position: number | null, req?: FSReq): number | undefined;
|
|
101
101
|
writeString(fd: number, value: string, position: number | null, encoding: string | undefined, req?: FSReq): number | undefined;
|
|
102
|
-
writeFileUtf8(path: unknown, data: string, flags: number,
|
|
102
|
+
writeFileUtf8(path: unknown, data: string, flags: number, mode: number): undefined;
|
|
103
103
|
stat(path: unknown, bigint: boolean, req?: FSReq, throwIfNoEntry?: boolean): Float64Array | BigInt64Array | undefined;
|
|
104
104
|
lstat(path: unknown, bigint: boolean, req?: FSReq, throwIfNoEntry?: boolean): Float64Array | BigInt64Array | undefined;
|
|
105
105
|
fstat(fd: number, bigint: boolean, req?: FSReq): Float64Array | BigInt64Array | undefined;
|
|
@@ -109,7 +109,7 @@ export declare const fsBinding: {
|
|
|
109
109
|
* a disk: a block size, and a count no program can exhaust.
|
|
110
110
|
*/
|
|
111
111
|
statfs(_path: unknown, bigint: boolean, req?: FSReq): Float64Array | BigInt64Array | undefined;
|
|
112
|
-
access(path: unknown,
|
|
112
|
+
access(path: unknown, mode: number, req?: FSReq): undefined;
|
|
113
113
|
existsSync(path: unknown): boolean;
|
|
114
114
|
/**
|
|
115
115
|
* Node's `cpSync` asks this before it copies: a directory is not a file,
|
|
@@ -143,12 +143,12 @@ export declare const fsBinding: {
|
|
|
143
143
|
mkdtemp(prefix: unknown, encoding: string, req?: FSReq): string | Uint8Array | undefined;
|
|
144
144
|
chmod(path: unknown, mode: number, req?: FSReq): undefined;
|
|
145
145
|
fchmod(fd: number, mode: number, req?: FSReq): undefined;
|
|
146
|
-
chown(
|
|
146
|
+
chown(_path: unknown, _uid: number, _gid: number, req?: FSReq): undefined;
|
|
147
147
|
fchown(_fd: number, _uid: number, _gid: number, req?: FSReq): undefined;
|
|
148
148
|
lchown(_path: unknown, _uid: number, _gid: number, req?: FSReq): undefined;
|
|
149
|
-
utimes(
|
|
150
|
-
futimes(
|
|
151
|
-
lutimes(
|
|
149
|
+
utimes(path: unknown, atime: number, mtime: number, req?: FSReq): undefined;
|
|
150
|
+
futimes(fd: number, atime: number, mtime: number, req?: FSReq): undefined;
|
|
151
|
+
lutimes(path: unknown, atime: number, mtime: number, req?: FSReq): undefined;
|
|
152
152
|
fsync(_fd: number, req?: FSReq): undefined;
|
|
153
153
|
fdatasync(_fd: number, req?: FSReq): undefined;
|
|
154
154
|
ftruncate(fd: number, length: number, req?: FSReq): undefined;
|
package/dist/server-bridge.d.ts
CHANGED
|
@@ -65,6 +65,7 @@ export declare class ServerBridge extends EventEmitter {
|
|
|
65
65
|
* news comes from the net binding, because that is where a port is taken;
|
|
66
66
|
* the engine's `http` is Node's own file and knows nothing of a page.
|
|
67
67
|
*/
|
|
68
|
+
private readonly guestServers;
|
|
68
69
|
private watchGuestPorts;
|
|
69
70
|
close(): void;
|
|
70
71
|
/**
|
|
@@ -32,6 +32,12 @@ type RunStdin = {
|
|
|
32
32
|
* output went down the child's channel.
|
|
33
33
|
*/
|
|
34
34
|
export interface RunStreams {
|
|
35
|
+
stdinStream?: AsyncIterable<Uint8Array>;
|
|
36
|
+
terminal?: {
|
|
37
|
+
columns: number;
|
|
38
|
+
rows: number;
|
|
39
|
+
onResize?: (listener: (columns: number, rows: number) => void) => () => void;
|
|
40
|
+
};
|
|
35
41
|
onStdout?: (data: string) => void;
|
|
36
42
|
onStderr?: (data: string) => void;
|
|
37
43
|
signal?: AbortSignal;
|
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,9 +477,15 @@ 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);
|
|
486
|
+
// VirtualFS writes create parents for image loading; open(2) does not.
|
|
487
|
+
const parent = name.slice(0, name.lastIndexOf('/')) || '/';
|
|
488
|
+
if (!tree.statSync(parent).isDirectory()) throw createNodeError('ENOTDIR', 'open', name);
|
|
472
489
|
tree.writeFileSync(name, '');
|
|
473
490
|
} else if ((flags & bits.truncate) !== 0) {
|
|
474
491
|
tree.writeFileSync(name, '');
|
|
@@ -481,13 +498,14 @@ export const fsBinding = {
|
|
|
481
498
|
},
|
|
482
499
|
|
|
483
500
|
close(fd: number, req?: FSReq): undefined {
|
|
484
|
-
return answer(req, () => { openFiles.delete(fd); return undefined; });
|
|
501
|
+
return answer(req, () => { fileFor(fd); openFiles.delete(fd); return undefined; });
|
|
485
502
|
},
|
|
486
503
|
|
|
487
504
|
// ---- reading ------------------------------------------------------------
|
|
488
505
|
read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number, req?: FSReq): number | undefined {
|
|
489
506
|
return answer(req, () => {
|
|
490
507
|
const file = fileFor(fd);
|
|
508
|
+
if ((file.flags & 3) === 1) throw createNodeError('EBADF', 'read', file.path);
|
|
491
509
|
if (file.cached === undefined) file.cached = file.tree.readFileSync(file.path) as Uint8Array;
|
|
492
510
|
const bytes = file.cached;
|
|
493
511
|
const from = position === null || position === undefined || position < 0 ? file.position : position;
|
|
@@ -512,18 +530,27 @@ export const fsBinding = {
|
|
|
512
530
|
},
|
|
513
531
|
|
|
514
532
|
/** Node's fast path for `readFile` with an encoding it can decode itself. */
|
|
515
|
-
readFileUtf8(path: unknown,
|
|
516
|
-
|
|
533
|
+
readFileUtf8(path: unknown, flags: number): string {
|
|
534
|
+
const owned = typeof path !== 'number';
|
|
535
|
+
const fd = owned ? fsBinding.open(path, flags, 0o666) as number : path as number;
|
|
536
|
+
try {
|
|
537
|
+
const file = fileFor(fd);
|
|
538
|
+
const bytes = new Uint8Array(file.tree.statSync(file.path).size);
|
|
539
|
+
const length = fsBinding.read(fd, bytes, 0, bytes.length, -1) as number;
|
|
540
|
+
return new TextDecoder().decode(bytes.subarray(0, length));
|
|
541
|
+
} finally { if (owned) fsBinding.close(fd); }
|
|
517
542
|
},
|
|
518
543
|
|
|
519
544
|
// ---- writing ------------------------------------------------------------
|
|
520
545
|
writeBuffer(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, req?: FSReq): number | undefined {
|
|
521
546
|
return answer(req, () => {
|
|
522
547
|
const file = fileFor(fd);
|
|
548
|
+
if ((file.flags & 3) === 0) throw createNodeError('EBADF', 'write', file.path);
|
|
523
549
|
const tree = file.tree;
|
|
524
550
|
const slice = buffer.subarray(offset, offset + length);
|
|
525
551
|
const existing = file.cached ?? (tree.existsSync(file.path) ? tree.readFileSync(file.path) as Uint8Array : new Uint8Array(0));
|
|
526
|
-
const at =
|
|
552
|
+
const at = (file.flags & flagBits().append) !== 0 ? existing.length
|
|
553
|
+
: position === null || position === undefined || position < 0 ? file.position : position;
|
|
527
554
|
const size = Math.max(existing.length, at + slice.length);
|
|
528
555
|
const next = new Uint8Array(size);
|
|
529
556
|
next.set(existing, 0);
|
|
@@ -553,15 +580,13 @@ export const fsBinding = {
|
|
|
553
580
|
return fsBinding.writeBuffer(fd, bytes, 0, bytes.length, position ?? null, req);
|
|
554
581
|
},
|
|
555
582
|
|
|
556
|
-
writeFileUtf8(path: unknown, data: string, flags: number,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
tree.writeFileSync(name, data);
|
|
583
|
+
writeFileUtf8(path: unknown, data: string, flags: number, mode: number): undefined {
|
|
584
|
+
// Node's UTF-8 fast path accepts a path OR an already-open descriptor.
|
|
585
|
+
// Reuse open/write so flags, offsets and errors match the buffer path.
|
|
586
|
+
const owned = typeof path !== 'number';
|
|
587
|
+
const fd = owned ? fsBinding.open(path, flags, mode) as number : path as number;
|
|
588
|
+
try { fsBinding.writeString(fd, data, null, 'utf8'); }
|
|
589
|
+
finally { if (owned) fsBinding.close(fd); }
|
|
565
590
|
return undefined;
|
|
566
591
|
},
|
|
567
592
|
|
|
@@ -612,10 +637,10 @@ export const fsBinding = {
|
|
|
612
637
|
});
|
|
613
638
|
},
|
|
614
639
|
|
|
615
|
-
access(path: unknown,
|
|
640
|
+
access(path: unknown, mode: number, req?: FSReq): undefined {
|
|
616
641
|
return answer(req, () => {
|
|
617
642
|
const name = asPath(path);
|
|
618
|
-
|
|
643
|
+
vfs().accessSync(name, mode);
|
|
619
644
|
return undefined;
|
|
620
645
|
});
|
|
621
646
|
},
|
|
@@ -726,13 +751,10 @@ export const fsBinding = {
|
|
|
726
751
|
if (tree.existsSync(name)) {
|
|
727
752
|
const stats = tree.statSync(name);
|
|
728
753
|
if (!recursive || !stats.isDirectory()) throw createNodeError('EEXIST', 'mkdir', name);
|
|
729
|
-
const stored = pathModes.get(name);
|
|
730
|
-
if (stored !== undefined && (stored & 0o777) !== wanted) throw createNodeError('EEXIST', 'mkdir', name);
|
|
731
|
-
pathModes.set(name, wanted);
|
|
732
754
|
return undefined;
|
|
733
755
|
}
|
|
734
756
|
tree.mkdirSync(name, { recursive });
|
|
735
|
-
|
|
757
|
+
tree.chmodSync(name, wanted);
|
|
736
758
|
return undefined;
|
|
737
759
|
});
|
|
738
760
|
},
|
|
@@ -806,43 +828,39 @@ export const fsBinding = {
|
|
|
806
828
|
});
|
|
807
829
|
},
|
|
808
830
|
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
// are kept here, because a program that sets a mode and stats it back must
|
|
812
|
-
// 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.
|
|
813
833
|
chmod(path: unknown, mode: number, req?: FSReq): undefined {
|
|
814
834
|
return answer(req, () => {
|
|
815
835
|
const name = asPath(path);
|
|
816
|
-
|
|
817
|
-
pathModes.set(name, mode & 0o7777);
|
|
836
|
+
vfs().chmodSync(name, mode);
|
|
818
837
|
return undefined;
|
|
819
838
|
});
|
|
820
839
|
},
|
|
821
840
|
fchmod(fd: number, mode: number, req?: FSReq): undefined {
|
|
822
841
|
return answer(req, () => {
|
|
823
842
|
const file = fileFor(fd);
|
|
824
|
-
|
|
825
|
-
return undefined;
|
|
826
|
-
});
|
|
827
|
-
},
|
|
828
|
-
chown(path: unknown, _uid: number, _gid: number, req?: FSReq): undefined {
|
|
829
|
-
return answer(req, () => {
|
|
830
|
-
const name = asPath(path);
|
|
831
|
-
if (!vfs().existsSync(name)) throw createNodeError('ENOENT', 'chown', name);
|
|
843
|
+
file.tree.chmodSync(file.path, mode);
|
|
832
844
|
return undefined;
|
|
833
845
|
});
|
|
834
846
|
},
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
+
}); },
|
|
840
857
|
fsync(_fd: number, req?: FSReq): undefined { return answer(req, () => undefined); },
|
|
841
858
|
fdatasync(_fd: number, req?: FSReq): undefined { return answer(req, () => undefined); },
|
|
842
859
|
|
|
843
860
|
ftruncate(fd: number, length: number, req?: FSReq): undefined {
|
|
844
861
|
return answer(req, () => {
|
|
845
862
|
const file = fileFor(fd);
|
|
863
|
+
if ((file.flags & 3) === 0) throw createNodeError('EBADF', 'ftruncate', file.path);
|
|
846
864
|
const tree = file.tree;
|
|
847
865
|
const bytes = file.cached ?? (tree.readFileSync(file.path) as Uint8Array);
|
|
848
866
|
const next = new Uint8Array(length);
|
|
@@ -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
|
}
|
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 } : {}),
|
|
@@ -541,12 +543,26 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
541
543
|
// For long-running commands (watch mode), report as TTY so tools like
|
|
542
544
|
// vitest set up interactive features (file watching, stdin commands). A
|
|
543
545
|
// spawned child's stdio is a pipe, as Node's is, and gets none of this.
|
|
544
|
-
if (streams?.held) {
|
|
546
|
+
if (streams?.held || streams?.terminal) {
|
|
545
547
|
proc.stdout.isTTY = true;
|
|
546
548
|
proc.stderr.isTTY = true;
|
|
547
549
|
proc.stdin.isTTY = true;
|
|
548
550
|
proc.stdin.setRawMode = () => proc.stdin;
|
|
549
551
|
}
|
|
552
|
+
const terminal = streams?.terminal;
|
|
553
|
+
const resize = (columns: number, rows: number): void => {
|
|
554
|
+
for (const stream of [proc.stdout, proc.stderr]) {
|
|
555
|
+
stream.columns = columns;
|
|
556
|
+
stream.rows = rows;
|
|
557
|
+
stream.emit('resize');
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
let stopResize: (() => void) | undefined;
|
|
561
|
+
let inputActive = true;
|
|
562
|
+
let inputIterator: AsyncIterator<Uint8Array> | undefined;
|
|
563
|
+
const readableInput = proc.stdin as unknown as { listenerCount(event: string): number; readableLength: number; readableHighWaterMark: number; readableFlowing: boolean | null; _readableState?: { reading?: boolean; needReadable?: boolean } };
|
|
564
|
+
const inputReading = () => readableInput.readableFlowing === true || readableInput._readableState?.reading === true
|
|
565
|
+
|| readableInput._readableState?.needReadable === true || readableInput.listenerCount('data') > 0 || readableInput.listenerCount('readable') > 0;
|
|
550
566
|
|
|
551
567
|
// A child started with a channel wires its own end of it before its
|
|
552
568
|
// module runs, which is what `lib/internal/process/pre_execution.js` does
|
|
@@ -606,6 +622,41 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
606
622
|
};
|
|
607
623
|
const detachUncaught = __onUncaughtException(proc, onUncaughtException);
|
|
608
624
|
try {
|
|
625
|
+
// The host's pipe delivers bytes after the entry has attached its reader.
|
|
626
|
+
// EOF closes that same run's stream, never the most recently started run.
|
|
627
|
+
if (streams?.stdinStream) {
|
|
628
|
+
inputIterator = streams.stdinStream[Symbol.asyncIterator]();
|
|
629
|
+
const input = inputIterator;
|
|
630
|
+
void (async () => {
|
|
631
|
+
const push = (chunk: Uint8Array | null) => {
|
|
632
|
+
if (runToken === null) proc.stdin.push(chunk);
|
|
633
|
+
else enterRun(runToken, () => proc.stdin.push(chunk));
|
|
634
|
+
};
|
|
635
|
+
try {
|
|
636
|
+
while (inputActive && !streams.signal?.aborted) {
|
|
637
|
+
// A pipe is read on demand. Bound queued bytes by the Readable's
|
|
638
|
+
// high-water mark and do not pull a producer nobody consumes.
|
|
639
|
+
while (inputActive && !streams.signal?.aborted && (!inputReading()
|
|
640
|
+
|| readableInput.readableLength >= readableInput.readableHighWaterMark)) {
|
|
641
|
+
await new Promise(resolve => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, resolve, 10));
|
|
642
|
+
}
|
|
643
|
+
if (!inputActive || streams.signal?.aborted) break;
|
|
644
|
+
const next = await input.next();
|
|
645
|
+
if (next.done) break;
|
|
646
|
+
if (!inputActive || streams.signal?.aborted) break;
|
|
647
|
+
push(next.value);
|
|
648
|
+
}
|
|
649
|
+
if (inputActive && !streams.signal?.aborted) push(null);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
if (inputActive) onUncaughtException(error);
|
|
652
|
+
} finally { streams.stdinOpen = false; }
|
|
653
|
+
})();
|
|
654
|
+
}
|
|
655
|
+
if (terminal) resize(terminal.columns, terminal.rows);
|
|
656
|
+
stopResize = terminal?.onResize?.((columns, rows) => {
|
|
657
|
+
if (runToken === null) resize(columns, rows);
|
|
658
|
+
else enterRun(runToken, () => resize(columns, rows));
|
|
659
|
+
});
|
|
609
660
|
|
|
610
661
|
let entrySettling: Promise<unknown> | undefined;
|
|
611
662
|
try {
|
|
@@ -676,7 +727,7 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
676
727
|
// handle is one socket back to the server that forked it and which sets no
|
|
677
728
|
// timer — as idle, with exit 0, three times over.
|
|
678
729
|
const __ownsHandles = () => runToken !== null && (__ownedServerPorts(runToken).length > 0 || __ownedHandleCount(runToken) > 0);
|
|
679
|
-
const __printedThenWorking = () => pendingGuestTimers(proc) > 0 || heldWork().count > 0 || __ownsHandles();
|
|
730
|
+
const __printedThenWorking = () => (streams?.stdinOpen === true && inputReading()) || pendingGuestTimers(proc) > 0 || heldWork().count > 0 || __ownsHandles();
|
|
680
731
|
if ((stdout.length > 0 || stderr.length > 0) && !__printedThenWorking()) {
|
|
681
732
|
// Settling the command is host work. Killing guest timers must not
|
|
682
733
|
// cancel this continuation and leave the command's promise unresolved.
|
|
@@ -725,7 +776,7 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
725
776
|
let idleMs = 0;
|
|
726
777
|
// A timer the guest still holds is work in Node's loop, whether or not
|
|
727
778
|
// the program has printed; so is a handle it has open.
|
|
728
|
-
const stillWorking = (): boolean => pendingGuestTimers(proc) > 0 || __ownsHandles();
|
|
779
|
+
const stillWorking = (): boolean => (streams?.stdinOpen === true && inputReading()) || pendingGuestTimers(proc) > 0 || __ownsHandles();
|
|
729
780
|
|
|
730
781
|
// When an abort signal is present (e.g. watch mode), don't apply idle timeout —
|
|
731
782
|
// only exit when aborted or process.exit is called.
|
|
@@ -787,6 +838,9 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
787
838
|
} finally {
|
|
788
839
|
detachRejections();
|
|
789
840
|
detachUncaught();
|
|
841
|
+
inputActive = false;
|
|
842
|
+
try { void Promise.resolve(inputIterator?.return?.()).catch(() => {}); } catch { /* producer cleanup cannot prevent process cleanup */ }
|
|
843
|
+
try { stopResize?.(); } catch { /* host cleanup cannot prevent handle release */ }
|
|
790
844
|
// A process that ends releases its listening sockets, whatever ended it:
|
|
791
845
|
// a return from the entry, a throw, an exit. Only `process.exit` released
|
|
792
846
|
// them here, so `vite --host` — which opened 5173 and then died at startup
|