@secure-exec/core 0.1.1-rc.3 → 0.2.0-rc.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/esm-compiler.d.ts +5 -1
- package/dist/esm-compiler.js +5 -1
- package/dist/fs-helpers.d.ts +1 -1
- package/dist/generated/isolate-runtime.d.ts +15 -15
- package/dist/generated/isolate-runtime.js +15 -15
- package/dist/index.d.ts +24 -5
- package/dist/index.js +23 -3
- package/dist/isolate-runtime/apply-custom-global-policy.js +3 -3
- package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +2 -2
- package/dist/isolate-runtime/apply-timing-mitigation-off.js +2 -2
- package/dist/isolate-runtime/bridge-attach.js +2 -2
- package/dist/isolate-runtime/bridge-initial-globals.js +145 -6
- package/dist/isolate-runtime/eval-script-result.js +1 -1
- package/dist/isolate-runtime/global-exposure-helpers.js +2 -2
- package/dist/isolate-runtime/init-commonjs-module-globals.js +2 -2
- package/dist/isolate-runtime/override-process-cwd.js +1 -1
- package/dist/isolate-runtime/override-process-env.js +1 -1
- package/dist/isolate-runtime/require-setup.js +1600 -338
- package/dist/isolate-runtime/set-commonjs-file-globals.js +2 -2
- package/dist/isolate-runtime/set-stdin-data.js +1 -1
- package/dist/isolate-runtime/setup-dynamic-import.js +47 -19
- package/dist/isolate-runtime/setup-fs-facade.js +62 -23
- package/dist/kernel/command-registry.d.ts +44 -0
- package/dist/kernel/command-registry.js +114 -0
- package/dist/kernel/device-layer.d.ts +12 -0
- package/dist/kernel/device-layer.js +262 -0
- package/dist/kernel/dns-cache.d.ts +29 -0
- package/dist/kernel/dns-cache.js +52 -0
- package/dist/kernel/fd-table.d.ts +84 -0
- package/dist/kernel/fd-table.js +278 -0
- package/dist/kernel/file-lock.d.ts +34 -0
- package/dist/kernel/file-lock.js +123 -0
- package/dist/kernel/host-adapter.d.ts +50 -0
- package/dist/kernel/host-adapter.js +8 -0
- package/dist/kernel/index.d.ts +36 -0
- package/dist/kernel/index.js +34 -0
- package/dist/kernel/inode-table.d.ts +43 -0
- package/dist/kernel/inode-table.js +85 -0
- package/dist/kernel/kernel.d.ts +9 -0
- package/dist/kernel/kernel.js +1396 -0
- package/dist/kernel/permissions.d.ts +27 -0
- package/dist/kernel/permissions.js +118 -0
- package/dist/kernel/pipe-manager.d.ts +64 -0
- package/dist/kernel/pipe-manager.js +267 -0
- package/dist/kernel/proc-layer.d.ts +11 -0
- package/dist/kernel/proc-layer.js +501 -0
- package/dist/kernel/process-table.d.ts +124 -0
- package/dist/kernel/process-table.js +631 -0
- package/dist/kernel/pty.d.ts +108 -0
- package/dist/kernel/pty.js +541 -0
- package/dist/kernel/socket-table.d.ts +305 -0
- package/dist/kernel/socket-table.js +1124 -0
- package/dist/kernel/timer-table.d.ts +54 -0
- package/dist/kernel/timer-table.js +108 -0
- package/dist/kernel/types.d.ts +500 -0
- package/dist/kernel/types.js +89 -0
- package/dist/kernel/user.d.ts +29 -0
- package/dist/kernel/user.js +35 -0
- package/dist/kernel/vfs.d.ts +54 -0
- package/dist/kernel/vfs.js +8 -0
- package/dist/kernel/wait.d.ts +45 -0
- package/dist/kernel/wait.js +112 -0
- package/dist/kernel/wstatus.d.ts +21 -0
- package/dist/kernel/wstatus.js +33 -0
- package/dist/module-resolver.d.ts +4 -0
- package/dist/module-resolver.js +4 -0
- package/dist/package-bundler.d.ts +6 -1
- package/dist/runtime-driver.d.ts +3 -1
- package/dist/shared/bridge-contract.d.ts +329 -20
- package/dist/shared/bridge-contract.js +60 -5
- package/dist/shared/console-formatter.js +8 -4
- package/dist/shared/global-exposure.js +269 -19
- package/dist/shared/in-memory-fs.d.ts +30 -11
- package/dist/shared/in-memory-fs.js +383 -109
- package/dist/shared/permissions.d.ts +4 -6
- package/dist/shared/permissions.js +19 -39
- package/dist/types.d.ts +8 -159
- package/dist/types.js +5 -0
- package/package.json +12 -22
- package/dist/bridge/active-handles.d.ts +0 -22
- package/dist/bridge/active-handles.js +0 -55
- package/dist/bridge/child-process.d.ts +0 -99
- package/dist/bridge/child-process.js +0 -670
- package/dist/bridge/fs.d.ts +0 -281
- package/dist/bridge/fs.js +0 -2235
- package/dist/bridge/index.d.ts +0 -10
- package/dist/bridge/index.js +0 -41
- package/dist/bridge/module.d.ts +0 -75
- package/dist/bridge/module.js +0 -308
- package/dist/bridge/network.d.ts +0 -350
- package/dist/bridge/network.js +0 -2050
- package/dist/bridge/os.d.ts +0 -13
- package/dist/bridge/os.js +0 -256
- package/dist/bridge/polyfills.d.ts +0 -2
- package/dist/bridge/polyfills.js +0 -11
- package/dist/bridge/process.d.ts +0 -89
- package/dist/bridge/process.js +0 -1015
- package/dist/bridge.js +0 -12496
- package/dist/python-runtime.d.ts +0 -16
- package/dist/python-runtime.js +0 -45
- package/dist/runtime.d.ts +0 -31
- package/dist/runtime.js +0 -69
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission enforcement layer.
|
|
3
|
+
*
|
|
4
|
+
* Deny-by-default access control. Wraps VFS and other kernel operations
|
|
5
|
+
* with permission checks that throw on denial.
|
|
6
|
+
*/
|
|
7
|
+
import type { Permissions } from "./types.js";
|
|
8
|
+
import type { VirtualFileSystem } from "./vfs.js";
|
|
9
|
+
/**
|
|
10
|
+
* Wrap a VFS with permission checks on every operation.
|
|
11
|
+
*/
|
|
12
|
+
export declare function wrapFileSystem(fs: VirtualFileSystem, permissions?: Permissions): VirtualFileSystem;
|
|
13
|
+
/**
|
|
14
|
+
* Filter an env record through the env permission check.
|
|
15
|
+
* Returns only allowed key-value pairs.
|
|
16
|
+
*/
|
|
17
|
+
export declare function filterEnv(env: Record<string, string> | undefined, permissions?: Permissions): Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Check childProcess permission before spawning.
|
|
20
|
+
* No-op when no permissions or no childProcess check is configured.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkChildProcess(permissions: Permissions | undefined, command: string, args: string[], cwd?: string): void;
|
|
23
|
+
export declare const allowAllFs: Pick<Permissions, "fs">;
|
|
24
|
+
export declare const allowAllNetwork: Pick<Permissions, "network">;
|
|
25
|
+
export declare const allowAllChildProcess: Pick<Permissions, "childProcess">;
|
|
26
|
+
export declare const allowAllEnv: Pick<Permissions, "env">;
|
|
27
|
+
export declare const allowAll: Permissions;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission enforcement layer.
|
|
3
|
+
*
|
|
4
|
+
* Deny-by-default access control. Wraps VFS and other kernel operations
|
|
5
|
+
* with permission checks that throw on denial.
|
|
6
|
+
*/
|
|
7
|
+
import { KernelError } from "./types.js";
|
|
8
|
+
function checkPermission(check, request, errorFactory) {
|
|
9
|
+
if (!check)
|
|
10
|
+
throw errorFactory(request);
|
|
11
|
+
const decision = check(request);
|
|
12
|
+
if (!decision?.allow)
|
|
13
|
+
throw errorFactory(request, decision?.reason);
|
|
14
|
+
}
|
|
15
|
+
function fsError(op, path, reason) {
|
|
16
|
+
const msg = reason
|
|
17
|
+
? `permission denied, ${op} '${path ?? ""}': ${reason}`
|
|
18
|
+
: `permission denied, ${op} '${path ?? ""}'`;
|
|
19
|
+
return new KernelError("EACCES", msg);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a VFS with permission checks on every operation.
|
|
23
|
+
*/
|
|
24
|
+
export function wrapFileSystem(fs, permissions) {
|
|
25
|
+
const check = (op, path) => {
|
|
26
|
+
checkPermission(permissions?.fs, { op, path }, (req, reason) => fsError(op, req.path, reason));
|
|
27
|
+
};
|
|
28
|
+
const wrapped = {
|
|
29
|
+
prepareOpenSync: (path, flags) => {
|
|
30
|
+
if ((flags & 0o100) !== 0 || (flags & 0o1000) !== 0) {
|
|
31
|
+
check("write", path);
|
|
32
|
+
}
|
|
33
|
+
const syncFs = fs;
|
|
34
|
+
return syncFs.prepareOpenSync?.(path, flags) ?? false;
|
|
35
|
+
},
|
|
36
|
+
readFile: async (path) => { check("read", path); return fs.readFile(path); },
|
|
37
|
+
readTextFile: async (path) => { check("read", path); return fs.readTextFile(path); },
|
|
38
|
+
readDir: async (path) => { check("readdir", path); return fs.readDir(path); },
|
|
39
|
+
readDirWithTypes: async (path) => { check("readdir", path); return fs.readDirWithTypes(path); },
|
|
40
|
+
writeFile: async (path, content) => { check("write", path); return fs.writeFile(path, content); },
|
|
41
|
+
createDir: async (path) => { check("createDir", path); return fs.createDir(path); },
|
|
42
|
+
mkdir: async (path, options) => { check("mkdir", path); return fs.mkdir(path, options); },
|
|
43
|
+
exists: async (path) => { check("exists", path); return fs.exists(path); },
|
|
44
|
+
stat: async (path) => { check("stat", path); return fs.stat(path); },
|
|
45
|
+
removeFile: async (path) => { check("rm", path); return fs.removeFile(path); },
|
|
46
|
+
removeDir: async (path) => { check("rm", path); return fs.removeDir(path); },
|
|
47
|
+
rename: async (oldPath, newPath) => {
|
|
48
|
+
check("rename", oldPath);
|
|
49
|
+
check("rename", newPath);
|
|
50
|
+
return fs.rename(oldPath, newPath);
|
|
51
|
+
},
|
|
52
|
+
realpath: async (path) => { check("read", path); return fs.realpath(path); },
|
|
53
|
+
symlink: async (target, linkPath) => { check("symlink", linkPath); return fs.symlink(target, linkPath); },
|
|
54
|
+
readlink: async (path) => { check("readlink", path); return fs.readlink(path); },
|
|
55
|
+
lstat: async (path) => { check("stat", path); return fs.lstat(path); },
|
|
56
|
+
link: async (oldPath, newPath) => { check("link", newPath); return fs.link(oldPath, newPath); },
|
|
57
|
+
chmod: async (path, mode) => { check("chmod", path); return fs.chmod(path, mode); },
|
|
58
|
+
chown: async (path, uid, gid) => { check("chown", path); return fs.chown(path, uid, gid); },
|
|
59
|
+
utimes: async (path, atime, mtime) => { check("utimes", path); return fs.utimes(path, atime, mtime); },
|
|
60
|
+
truncate: async (path, length) => { check("truncate", path); return fs.truncate(path, length); },
|
|
61
|
+
pread: async (path, offset, length) => { check("read", path); return fs.pread(path, offset, length); },
|
|
62
|
+
};
|
|
63
|
+
return wrapped;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Filter an env record through the env permission check.
|
|
67
|
+
* Returns only allowed key-value pairs.
|
|
68
|
+
*/
|
|
69
|
+
export function filterEnv(env, permissions) {
|
|
70
|
+
if (!env)
|
|
71
|
+
return {};
|
|
72
|
+
if (!permissions?.env)
|
|
73
|
+
return {};
|
|
74
|
+
const result = {};
|
|
75
|
+
for (const [key, value] of Object.entries(env)) {
|
|
76
|
+
const request = { op: "read", key, value };
|
|
77
|
+
const decision = permissions.env(request);
|
|
78
|
+
if (decision?.allow) {
|
|
79
|
+
result[key] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check childProcess permission before spawning.
|
|
86
|
+
* No-op when no permissions or no childProcess check is configured.
|
|
87
|
+
*/
|
|
88
|
+
export function checkChildProcess(permissions, command, args, cwd) {
|
|
89
|
+
if (!permissions?.childProcess)
|
|
90
|
+
return;
|
|
91
|
+
const request = { command, args, cwd };
|
|
92
|
+
const decision = permissions.childProcess(request);
|
|
93
|
+
if (!decision?.allow) {
|
|
94
|
+
const msg = decision?.reason
|
|
95
|
+
? `permission denied, spawn '${command}': ${decision.reason}`
|
|
96
|
+
: `permission denied, spawn '${command}'`;
|
|
97
|
+
throw new KernelError("EACCES", msg);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Permission presets
|
|
101
|
+
export const allowAllFs = {
|
|
102
|
+
fs: () => ({ allow: true }),
|
|
103
|
+
};
|
|
104
|
+
export const allowAllNetwork = {
|
|
105
|
+
network: () => ({ allow: true }),
|
|
106
|
+
};
|
|
107
|
+
export const allowAllChildProcess = {
|
|
108
|
+
childProcess: () => ({ allow: true }),
|
|
109
|
+
};
|
|
110
|
+
export const allowAllEnv = {
|
|
111
|
+
env: () => ({ allow: true }),
|
|
112
|
+
};
|
|
113
|
+
export const allowAll = {
|
|
114
|
+
...allowAllFs,
|
|
115
|
+
...allowAllNetwork,
|
|
116
|
+
...allowAllChildProcess,
|
|
117
|
+
...allowAllEnv,
|
|
118
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipe manager.
|
|
3
|
+
*
|
|
4
|
+
* Creates and manages pipes for inter-process communication.
|
|
5
|
+
* Supports cross-runtime pipes: data flows through kernel-managed buffers.
|
|
6
|
+
* SharedArrayBuffer ring buffers are deferred — this uses buffered pipes.
|
|
7
|
+
*/
|
|
8
|
+
import type { FileDescription } from "./types.js";
|
|
9
|
+
import { FILETYPE_PIPE } from "./types.js";
|
|
10
|
+
import type { ProcessFDTable } from "./fd-table.js";
|
|
11
|
+
export interface PipeEnd {
|
|
12
|
+
description: FileDescription;
|
|
13
|
+
filetype: typeof FILETYPE_PIPE;
|
|
14
|
+
}
|
|
15
|
+
/** Maximum buffered bytes per pipe before writers block or O_NONBLOCK returns EAGAIN. */
|
|
16
|
+
export declare const MAX_PIPE_BUFFER_BYTES = 65536;
|
|
17
|
+
export declare class PipeManager {
|
|
18
|
+
private pipes;
|
|
19
|
+
/** Map description ID → pipe ID for routing reads/writes */
|
|
20
|
+
private descToPipe;
|
|
21
|
+
private nextPipeId;
|
|
22
|
+
private nextDescId;
|
|
23
|
+
/** Called before EPIPE when a write hits a closed read end. Receives writer PID. */
|
|
24
|
+
onBrokenPipe: ((pid: number) => void) | null;
|
|
25
|
+
/**
|
|
26
|
+
* Create a pipe. Returns two FileDescriptions:
|
|
27
|
+
* one for reading and one for writing.
|
|
28
|
+
*/
|
|
29
|
+
createPipe(): {
|
|
30
|
+
read: PipeEnd;
|
|
31
|
+
write: PipeEnd;
|
|
32
|
+
};
|
|
33
|
+
/** Write data to a pipe's write end. Delivers SIGPIPE via onBrokenPipe when read end is closed. */
|
|
34
|
+
write(descriptionId: number, data: Uint8Array, writerPid?: number): number | Promise<number>;
|
|
35
|
+
/** Read data from a pipe's read end. Returns null on EOF. */
|
|
36
|
+
read(descriptionId: number, length: number): Promise<Uint8Array | null>;
|
|
37
|
+
/** Close one end of a pipe. */
|
|
38
|
+
close(descriptionId: number): void;
|
|
39
|
+
/** Check if a description ID belongs to a pipe */
|
|
40
|
+
isPipe(descriptionId: number): boolean;
|
|
41
|
+
/** Query poll state for a pipe end (used by poll/select syscalls). */
|
|
42
|
+
pollState(descriptionId: number): {
|
|
43
|
+
readable: boolean;
|
|
44
|
+
writable: boolean;
|
|
45
|
+
hangup: boolean;
|
|
46
|
+
} | null;
|
|
47
|
+
/** Get the pipe ID for a description, or undefined if not a pipe */
|
|
48
|
+
pipeIdFor(descriptionId: number): number | undefined;
|
|
49
|
+
/** Wait for a pipe poll state change (data, capacity, or hangup). */
|
|
50
|
+
waitForPoll(descriptionId: number, timeoutMs?: number): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Create pipe FDs in the given FD table.
|
|
53
|
+
* Returns the FD numbers for {read, write}.
|
|
54
|
+
*/
|
|
55
|
+
createPipeFDs(fdTable: ProcessFDTable): {
|
|
56
|
+
readFd: number;
|
|
57
|
+
writeFd: number;
|
|
58
|
+
};
|
|
59
|
+
private bufferSize;
|
|
60
|
+
private drainBuffer;
|
|
61
|
+
private writeBlocking;
|
|
62
|
+
private writeAvailable;
|
|
63
|
+
private assertWriteOpen;
|
|
64
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipe manager.
|
|
3
|
+
*
|
|
4
|
+
* Creates and manages pipes for inter-process communication.
|
|
5
|
+
* Supports cross-runtime pipes: data flows through kernel-managed buffers.
|
|
6
|
+
* SharedArrayBuffer ring buffers are deferred — this uses buffered pipes.
|
|
7
|
+
*/
|
|
8
|
+
import { FILETYPE_PIPE, O_NONBLOCK, O_RDONLY, O_WRONLY, KernelError } from "./types.js";
|
|
9
|
+
import { WaitQueue } from "./wait.js";
|
|
10
|
+
/** Maximum buffered bytes per pipe before writers block or O_NONBLOCK returns EAGAIN. */
|
|
11
|
+
export const MAX_PIPE_BUFFER_BYTES = 65_536; // 64 KB — matches Linux default
|
|
12
|
+
export class PipeManager {
|
|
13
|
+
pipes = new Map();
|
|
14
|
+
/** Map description ID → pipe ID for routing reads/writes */
|
|
15
|
+
descToPipe = new Map();
|
|
16
|
+
nextPipeId = 1;
|
|
17
|
+
nextDescId = 100_000; // High range to avoid FD table collisions
|
|
18
|
+
/** Called before EPIPE when a write hits a closed read end. Receives writer PID. */
|
|
19
|
+
onBrokenPipe = null;
|
|
20
|
+
/**
|
|
21
|
+
* Create a pipe. Returns two FileDescriptions:
|
|
22
|
+
* one for reading and one for writing.
|
|
23
|
+
*/
|
|
24
|
+
createPipe() {
|
|
25
|
+
const id = this.nextPipeId++;
|
|
26
|
+
const readDesc = {
|
|
27
|
+
id: this.nextDescId++,
|
|
28
|
+
path: `pipe:${id}:read`,
|
|
29
|
+
cursor: 0n,
|
|
30
|
+
flags: O_RDONLY,
|
|
31
|
+
refCount: 0, // Not in any FD table yet — openWith() will bump
|
|
32
|
+
};
|
|
33
|
+
const writeDesc = {
|
|
34
|
+
id: this.nextDescId++,
|
|
35
|
+
path: `pipe:${id}:write`,
|
|
36
|
+
cursor: 0n,
|
|
37
|
+
flags: O_WRONLY,
|
|
38
|
+
refCount: 0, // Not in any FD table yet — openWith() will bump
|
|
39
|
+
};
|
|
40
|
+
const state = {
|
|
41
|
+
id,
|
|
42
|
+
buffer: [],
|
|
43
|
+
closed: { read: false, write: false },
|
|
44
|
+
readDescription: readDesc,
|
|
45
|
+
writeDescription: writeDesc,
|
|
46
|
+
readWaiters: [],
|
|
47
|
+
writeWaiters: new WaitQueue(),
|
|
48
|
+
pollWaiters: new WaitQueue(),
|
|
49
|
+
};
|
|
50
|
+
this.pipes.set(id, state);
|
|
51
|
+
this.descToPipe.set(readDesc.id, { pipeId: id, end: "read" });
|
|
52
|
+
this.descToPipe.set(writeDesc.id, { pipeId: id, end: "write" });
|
|
53
|
+
return {
|
|
54
|
+
read: { description: readDesc, filetype: FILETYPE_PIPE },
|
|
55
|
+
write: { description: writeDesc, filetype: FILETYPE_PIPE },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Write data to a pipe's write end. Delivers SIGPIPE via onBrokenPipe when read end is closed. */
|
|
59
|
+
write(descriptionId, data, writerPid) {
|
|
60
|
+
const ref = this.descToPipe.get(descriptionId);
|
|
61
|
+
if (!ref || ref.end !== "write")
|
|
62
|
+
throw new KernelError("EBADF", "not a pipe write end");
|
|
63
|
+
const state = this.pipes.get(ref.pipeId);
|
|
64
|
+
if (!state)
|
|
65
|
+
throw new KernelError("EBADF", "pipe not found");
|
|
66
|
+
const nonBlocking = (state.writeDescription.flags & O_NONBLOCK) !== 0;
|
|
67
|
+
const written = this.writeAvailable(state, data, writerPid);
|
|
68
|
+
if (written === data.length) {
|
|
69
|
+
return data.length;
|
|
70
|
+
}
|
|
71
|
+
if (nonBlocking) {
|
|
72
|
+
if (written === 0) {
|
|
73
|
+
throw new KernelError("EAGAIN", "pipe buffer full");
|
|
74
|
+
}
|
|
75
|
+
return written;
|
|
76
|
+
}
|
|
77
|
+
return this.writeBlocking(state, data, written, writerPid);
|
|
78
|
+
}
|
|
79
|
+
/** Read data from a pipe's read end. Returns null on EOF. */
|
|
80
|
+
read(descriptionId, length) {
|
|
81
|
+
const ref = this.descToPipe.get(descriptionId);
|
|
82
|
+
if (!ref || ref.end !== "read")
|
|
83
|
+
throw new KernelError("EBADF", "not a pipe read end");
|
|
84
|
+
const state = this.pipes.get(ref.pipeId);
|
|
85
|
+
if (!state)
|
|
86
|
+
throw new KernelError("EBADF", "pipe not found");
|
|
87
|
+
// Data available in buffer
|
|
88
|
+
if (state.buffer.length > 0) {
|
|
89
|
+
const data = this.drainBuffer(state, length);
|
|
90
|
+
state.writeWaiters.wakeOne();
|
|
91
|
+
state.pollWaiters.wakeAll();
|
|
92
|
+
return Promise.resolve(data);
|
|
93
|
+
}
|
|
94
|
+
// Write end closed — EOF
|
|
95
|
+
if (state.closed.write) {
|
|
96
|
+
return Promise.resolve(null);
|
|
97
|
+
}
|
|
98
|
+
// Block until data or EOF
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
state.readWaiters.push(resolve);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Close one end of a pipe. */
|
|
104
|
+
close(descriptionId) {
|
|
105
|
+
const ref = this.descToPipe.get(descriptionId);
|
|
106
|
+
if (!ref)
|
|
107
|
+
return;
|
|
108
|
+
const state = this.pipes.get(ref.pipeId);
|
|
109
|
+
if (!state)
|
|
110
|
+
return;
|
|
111
|
+
if (ref.end === "read") {
|
|
112
|
+
state.closed.read = true;
|
|
113
|
+
state.writeWaiters.wakeAll();
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
state.closed.write = true;
|
|
117
|
+
// Notify any blocked readers with EOF
|
|
118
|
+
for (const waiter of state.readWaiters) {
|
|
119
|
+
waiter(null);
|
|
120
|
+
}
|
|
121
|
+
state.readWaiters.length = 0;
|
|
122
|
+
state.writeWaiters.wakeAll();
|
|
123
|
+
}
|
|
124
|
+
state.pollWaiters.wakeAll();
|
|
125
|
+
this.descToPipe.delete(descriptionId);
|
|
126
|
+
// Clean up when both ends are closed
|
|
127
|
+
if (state.closed.read && state.closed.write) {
|
|
128
|
+
this.pipes.delete(ref.pipeId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Check if a description ID belongs to a pipe */
|
|
132
|
+
isPipe(descriptionId) {
|
|
133
|
+
return this.descToPipe.has(descriptionId);
|
|
134
|
+
}
|
|
135
|
+
/** Query poll state for a pipe end (used by poll/select syscalls). */
|
|
136
|
+
pollState(descriptionId) {
|
|
137
|
+
const ref = this.descToPipe.get(descriptionId);
|
|
138
|
+
if (!ref)
|
|
139
|
+
return null;
|
|
140
|
+
const state = this.pipes.get(ref.pipeId);
|
|
141
|
+
if (!state)
|
|
142
|
+
return null;
|
|
143
|
+
if (ref.end === "read") {
|
|
144
|
+
const hasData = this.bufferSize(state) > 0;
|
|
145
|
+
return {
|
|
146
|
+
readable: hasData || state.closed.write,
|
|
147
|
+
writable: false,
|
|
148
|
+
hangup: state.closed.write,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
return {
|
|
153
|
+
readable: false,
|
|
154
|
+
writable: !state.closed.read && this.bufferSize(state) < MAX_PIPE_BUFFER_BYTES,
|
|
155
|
+
hangup: state.closed.read,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Get the pipe ID for a description, or undefined if not a pipe */
|
|
160
|
+
pipeIdFor(descriptionId) {
|
|
161
|
+
return this.descToPipe.get(descriptionId)?.pipeId;
|
|
162
|
+
}
|
|
163
|
+
/** Wait for a pipe poll state change (data, capacity, or hangup). */
|
|
164
|
+
async waitForPoll(descriptionId, timeoutMs) {
|
|
165
|
+
const ref = this.descToPipe.get(descriptionId);
|
|
166
|
+
if (!ref)
|
|
167
|
+
throw new KernelError("EBADF", "not a pipe description");
|
|
168
|
+
const state = this.pipes.get(ref.pipeId);
|
|
169
|
+
if (!state)
|
|
170
|
+
throw new KernelError("EBADF", "pipe not found");
|
|
171
|
+
const handle = state.pollWaiters.enqueue(timeoutMs);
|
|
172
|
+
try {
|
|
173
|
+
await handle.wait();
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
state.pollWaiters.remove(handle);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create pipe FDs in the given FD table.
|
|
181
|
+
* Returns the FD numbers for {read, write}.
|
|
182
|
+
*/
|
|
183
|
+
createPipeFDs(fdTable) {
|
|
184
|
+
const { read, write } = this.createPipe();
|
|
185
|
+
const readFd = fdTable.openWith(read.description, read.filetype);
|
|
186
|
+
const writeFd = fdTable.openWith(write.description, write.filetype);
|
|
187
|
+
return { readFd, writeFd };
|
|
188
|
+
}
|
|
189
|
+
bufferSize(state) {
|
|
190
|
+
let size = 0;
|
|
191
|
+
for (const chunk of state.buffer)
|
|
192
|
+
size += chunk.length;
|
|
193
|
+
return size;
|
|
194
|
+
}
|
|
195
|
+
drainBuffer(state, length) {
|
|
196
|
+
// Concatenate buffered chunks up to `length` bytes
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let remaining = length;
|
|
199
|
+
while (remaining > 0 && state.buffer.length > 0) {
|
|
200
|
+
const chunk = state.buffer[0];
|
|
201
|
+
if (chunk.length <= remaining) {
|
|
202
|
+
chunks.push(chunk);
|
|
203
|
+
remaining -= chunk.length;
|
|
204
|
+
state.buffer.shift();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
chunks.push(chunk.subarray(0, remaining));
|
|
208
|
+
state.buffer[0] = chunk.subarray(remaining);
|
|
209
|
+
remaining = 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (chunks.length === 1)
|
|
213
|
+
return chunks[0];
|
|
214
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
215
|
+
const result = new Uint8Array(total);
|
|
216
|
+
let offset = 0;
|
|
217
|
+
for (const chunk of chunks) {
|
|
218
|
+
result.set(chunk, offset);
|
|
219
|
+
offset += chunk.length;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
async writeBlocking(state, data, offset, writerPid) {
|
|
224
|
+
while (offset < data.length) {
|
|
225
|
+
const handle = state.writeWaiters.enqueue();
|
|
226
|
+
try {
|
|
227
|
+
await handle.wait();
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
state.writeWaiters.remove(handle);
|
|
231
|
+
}
|
|
232
|
+
offset += this.writeAvailable(state, data.subarray(offset), writerPid);
|
|
233
|
+
}
|
|
234
|
+
return data.length;
|
|
235
|
+
}
|
|
236
|
+
writeAvailable(state, data, writerPid) {
|
|
237
|
+
this.assertWriteOpen(state, writerPid);
|
|
238
|
+
if (data.length === 0)
|
|
239
|
+
return 0;
|
|
240
|
+
// If readers are waiting, deliver directly without growing the buffer.
|
|
241
|
+
if (state.readWaiters.length > 0 && state.buffer.length === 0) {
|
|
242
|
+
const waiter = state.readWaiters.shift();
|
|
243
|
+
waiter(new Uint8Array(data));
|
|
244
|
+
state.pollWaiters.wakeAll();
|
|
245
|
+
return data.length;
|
|
246
|
+
}
|
|
247
|
+
const capacity = MAX_PIPE_BUFFER_BYTES - this.bufferSize(state);
|
|
248
|
+
if (capacity <= 0) {
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
const bytesToWrite = Math.min(capacity, data.length);
|
|
252
|
+
state.buffer.push(new Uint8Array(data.subarray(0, bytesToWrite)));
|
|
253
|
+
state.pollWaiters.wakeAll();
|
|
254
|
+
return bytesToWrite;
|
|
255
|
+
}
|
|
256
|
+
assertWriteOpen(state, writerPid) {
|
|
257
|
+
if (state.closed.write)
|
|
258
|
+
throw new KernelError("EPIPE", "write end closed");
|
|
259
|
+
if (state.closed.read) {
|
|
260
|
+
// Deliver SIGPIPE before EPIPE (POSIX: signal first, then errno)
|
|
261
|
+
if (writerPid !== undefined && this.onBrokenPipe) {
|
|
262
|
+
this.onBrokenPipe(writerPid);
|
|
263
|
+
}
|
|
264
|
+
throw new KernelError("EPIPE", "read end closed");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FDTableManager } from "./fd-table.js";
|
|
2
|
+
import type { ProcessTable } from "./process-table.js";
|
|
3
|
+
import type { VirtualFileSystem } from "./vfs.js";
|
|
4
|
+
export interface ProcLayerOptions {
|
|
5
|
+
processTable: ProcessTable;
|
|
6
|
+
fdTableManager: FDTableManager;
|
|
7
|
+
hostname?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveProcSelfPath(path: string, pid: number): string;
|
|
10
|
+
export declare function createProcessScopedFileSystem(vfs: VirtualFileSystem, pid: number): VirtualFileSystem;
|
|
11
|
+
export declare function createProcLayer(vfs: VirtualFileSystem, options: ProcLayerOptions): VirtualFileSystem;
|