@wuyuchentr/webassembly-runtime-embed 1.0.0 → 3.0.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/README.md +52 -31
- package/package.json +1 -1
- package/src/host.js +34 -44
- package/src/index.js +7 -0
- package/src/pool.js +204 -0
- package/src/runtime.js +23 -1
- package/src/wasi.js +744 -0
- package/src/watcher.js +128 -0
package/README.md
CHANGED
|
@@ -16,7 +16,6 @@ const { createRuntime } = require('@wuyuchentr/webassembly-runtime-embed');
|
|
|
16
16
|
async function main() {
|
|
17
17
|
const runtime = createRuntime();
|
|
18
18
|
|
|
19
|
-
// Load and run a WASM module
|
|
20
19
|
const inst = await runtime.load('./add.wasm');
|
|
21
20
|
const result = inst.call('add', 1, 2);
|
|
22
21
|
console.log(result); // 3
|
|
@@ -26,16 +25,49 @@ async function main() {
|
|
|
26
25
|
## CLI
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
# Run a WASM function
|
|
30
28
|
webassembly-runtime-embed run add.wasm --export add --args 1 2
|
|
31
|
-
|
|
32
|
-
# Inspect module imports/exports
|
|
33
29
|
webassembly-runtime-embed info module.wasm
|
|
34
|
-
|
|
35
|
-
# Clear module cache
|
|
36
30
|
webassembly-runtime-embed cache clear
|
|
37
31
|
```
|
|
38
32
|
|
|
33
|
+
## WASI support (v2)
|
|
34
|
+
|
|
35
|
+
Full `wasi_snapshot_preview1` implementation with filesystem sandboxing.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const runtime = createRuntime();
|
|
39
|
+
|
|
40
|
+
runtime.registerWASI({
|
|
41
|
+
args: ['myapp.wasm', '--verbose'],
|
|
42
|
+
env: { HOME: '/data', DEBUG: '1' },
|
|
43
|
+
preopens: [
|
|
44
|
+
{ virtual: '/sandbox', real: '/path/to/host/dir' },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const inst = await runtime.load('./myapp.wasm');
|
|
49
|
+
// WASM can now use stdin/stdout/stderr, file I/O, clock, random, etc.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Supported WASI functions
|
|
53
|
+
|
|
54
|
+
| Category | Functions |
|
|
55
|
+
|----------|-----------|
|
|
56
|
+
| **Args** | `args_sizes_get`, `args_get` |
|
|
57
|
+
| **Environ** | `environ_sizes_get`, `environ_get` |
|
|
58
|
+
| **Clock** | `clock_time_get`, `clock_res_get` |
|
|
59
|
+
| **Random** | `random_get` |
|
|
60
|
+
| **Stdio** | `fd_read` (stdin), `fd_write` (stdout/stderr) |
|
|
61
|
+
| **File I/O** | `fd_read`, `fd_write`, `fd_close`, `fd_seek`, `fd_tell`, `fd_fdstat_get`, `fd_fdstat_set_flags`, `fd_filestat_get`, `fd_readdir`, `fd_datasync`, `fd_sync`, `fd_advise`, `fd_allocate`, `fd_renumber` |
|
|
62
|
+
| **Filesystem** | `path_open`, `path_create_directory`, `path_remove_directory`, `path_unlink_file`, `path_rename`, `path_filestat_get`, `path_readlink`, `path_symlink` |
|
|
63
|
+
| **Preopens** | `fd_prestat_get`, `fd_prestat_dir_name` |
|
|
64
|
+
| **Process** | `proc_exit` |
|
|
65
|
+
| **Poll** | `poll_oneoff`, `sched_yield` |
|
|
66
|
+
|
|
67
|
+
### Filesystem sandboxing
|
|
68
|
+
|
|
69
|
+
All WASM file operations are sandboxed within preopened directories. Paths like `/sandbox/file.txt` are transparently mapped to real host paths. Path traversal outside the preopened root is rejected.
|
|
70
|
+
|
|
39
71
|
## Features
|
|
40
72
|
|
|
41
73
|
### Function calling with type conversion
|
|
@@ -50,9 +82,8 @@ inst.call('multiply', 2.5, 3.0); // f64 → f64
|
|
|
50
82
|
```js
|
|
51
83
|
const mem = inst.getMemory();
|
|
52
84
|
mem.writeCString(0, 'hello world');
|
|
53
|
-
const str = mem.readCString(0);
|
|
85
|
+
const str = mem.readCString(0);
|
|
54
86
|
mem.writeF64(1024, 3.14159);
|
|
55
|
-
const pi = mem.readF64(1024); // 3.14159
|
|
56
87
|
```
|
|
57
88
|
|
|
58
89
|
### Host function registration
|
|
@@ -68,8 +99,8 @@ runtime.registerFunction('env', 'js_log', (ptr, len) => {
|
|
|
68
99
|
|
|
69
100
|
```js
|
|
70
101
|
const inst = await runtime.load('./module.wasm', {
|
|
71
|
-
maxMemory: 65536 * 4,
|
|
72
|
-
timeout: 1000,
|
|
102
|
+
maxMemory: 65536 * 4,
|
|
103
|
+
timeout: 1000,
|
|
73
104
|
});
|
|
74
105
|
```
|
|
75
106
|
|
|
@@ -77,7 +108,7 @@ const inst = await runtime.load('./module.wasm', {
|
|
|
77
108
|
|
|
78
109
|
```js
|
|
79
110
|
const inst = await runtime.loadRemote('https://example.com/module.wasm', {
|
|
80
|
-
ttl: 3600,
|
|
111
|
+
ttl: 3600,
|
|
81
112
|
});
|
|
82
113
|
```
|
|
83
114
|
|
|
@@ -85,31 +116,29 @@ const inst = await runtime.loadRemote('https://example.com/module.wasm', {
|
|
|
85
116
|
|
|
86
117
|
### `createRuntime(options)`
|
|
87
118
|
|
|
88
|
-
Create a new WASM runtime.
|
|
89
|
-
|
|
90
119
|
### `runtime.load(source, options)`
|
|
91
120
|
|
|
92
|
-
Load a WASM module from file path, buffer, or URL. Returns a `WasmInstance`.
|
|
93
|
-
|
|
94
121
|
| Option | Default | Description |
|
|
95
122
|
|--------|---------|-------------|
|
|
96
|
-
| `maxMemory` | `0` | Maximum memory in bytes
|
|
97
|
-
| `timeout` | `0` | Execution timeout in ms
|
|
123
|
+
| `maxMemory` | `0` | Maximum memory in bytes |
|
|
124
|
+
| `timeout` | `0` | Execution timeout in ms |
|
|
98
125
|
| `argTypes` | `[]` | Argument types for auto-conversion |
|
|
99
126
|
| `returnType` | `null` | Return type for auto-conversion |
|
|
100
127
|
|
|
101
|
-
### `runtime.
|
|
128
|
+
### `runtime.registerWASI(options)`
|
|
129
|
+
|
|
130
|
+
| Option | Default | Description |
|
|
131
|
+
|--------|---------|-------------|
|
|
132
|
+
| `args` | `[]` | Command-line arguments |
|
|
133
|
+
| `env` | `{}` | Environment variables |
|
|
134
|
+
| `preopens` | `[]` | Array of `{ virtual, real }` path mappings |
|
|
102
135
|
|
|
103
|
-
|
|
136
|
+
### `runtime.registerFunction(module, name, fn)`
|
|
104
137
|
|
|
105
138
|
### `inst.call(name, ...args)`
|
|
106
139
|
|
|
107
|
-
Call an exported WASM function.
|
|
108
|
-
|
|
109
140
|
### `inst.getMemory()`
|
|
110
141
|
|
|
111
|
-
Get a `MemoryView` for reading/writing WASM linear memory.
|
|
112
|
-
|
|
113
142
|
### `MemoryView` methods
|
|
114
143
|
|
|
115
144
|
- `readU8/16/32(offset)`, `readI32(offset)`, `readF32/64(offset)`
|
|
@@ -118,14 +147,6 @@ Get a `MemoryView` for reading/writing WASM linear memory.
|
|
|
118
147
|
- `writeF32/64(offset, value)`, `writeString(offset, str)`, `writeCString(offset, str)`
|
|
119
148
|
- `alloc(size)`, `allocString(str)`, `free()`
|
|
120
149
|
|
|
121
|
-
## Roadmap
|
|
122
|
-
|
|
123
|
-
- **v1** (current) — Basic WASM calls (i32/f64), memory management, host functions, resource limits, remote loading
|
|
124
|
-
- **v2** — WASI support (filesystem, networking)
|
|
125
|
-
- **v3** — Multi-instance pool + hot reload
|
|
126
|
-
- **v4** — Distributed WASM function scheduling
|
|
127
|
-
- **v5** — Component Model integration
|
|
128
|
-
|
|
129
150
|
## License
|
|
130
151
|
|
|
131
152
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wuyuchentr/webassembly-runtime-embed",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Embed WebAssembly runtime in Node.js with WASI support, multi-instance isolation, resource limits, and remote loading with cache.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"webassembly-runtime-embed": "bin/cli.js"
|
package/src/host.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
const { WasiContext } = require('./wasi.js');
|
|
2
|
+
|
|
1
3
|
class HostRegistry {
|
|
2
4
|
constructor() {
|
|
3
5
|
this._modules = new Map();
|
|
4
6
|
this._builtins = new Map();
|
|
7
|
+
this._wasiContext = null;
|
|
5
8
|
this._registerBuiltins();
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
_registerBuiltins() {
|
|
9
|
-
this._builtins.set('wasi_unstable', new Map());
|
|
10
|
-
this._builtins.set('wasi_snapshot_preview1', new Map());
|
|
11
12
|
this._builtins.set('env', new Map());
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -36,7 +37,13 @@ class HostRegistry {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
setWasiMemory(memory) {
|
|
41
|
+
if (this._wasiContext) {
|
|
42
|
+
this._wasiContext.setMemory(memory);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
buildImportObject() {
|
|
40
47
|
const importObject = {};
|
|
41
48
|
|
|
42
49
|
const allModules = new Set([
|
|
@@ -44,11 +51,27 @@ class HostRegistry {
|
|
|
44
51
|
...this._builtins.keys(),
|
|
45
52
|
]);
|
|
46
53
|
|
|
54
|
+
if (this._wasiContext) {
|
|
55
|
+
const wasiImports = this._wasiContext.toImportObject();
|
|
56
|
+
for (const [modName, funcs] of Object.entries(wasiImports)) {
|
|
57
|
+
allModules.add(modName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
for (const modName of allModules) {
|
|
48
62
|
const mod = this._modules.get(modName) || new Map();
|
|
49
63
|
const builtin = this._builtins.get(modName) || new Map();
|
|
50
64
|
const merged = new Map([...builtin, ...mod]);
|
|
51
65
|
|
|
66
|
+
if (this._wasiContext) {
|
|
67
|
+
const wasiImports = this._wasiContext.toImportObject();
|
|
68
|
+
if (wasiImports[modName]) {
|
|
69
|
+
for (const [name, fn] of Object.entries(wasiImports[modName])) {
|
|
70
|
+
merged.set(name, fn);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
if (merged.size === 0) continue;
|
|
53
76
|
|
|
54
77
|
importObject[modName] = {};
|
|
@@ -68,49 +91,16 @@ class HostRegistry {
|
|
|
68
91
|
}
|
|
69
92
|
|
|
70
93
|
registerWASI(options = {}) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
fdMap.set(0, { type: 'stdin' });
|
|
76
|
-
fdMap.set(1, { type: 'stdout' });
|
|
77
|
-
fdMap.set(2, { type: 'stderr' });
|
|
78
|
-
|
|
79
|
-
wasi.set('fd_write', (fd, iovs, iovsLen, nwritten) => {
|
|
80
|
-
if (fd === 1 || fd === 2) {
|
|
81
|
-
process.stdout.write(`[wasm:${fd > 1 ? 'err' : 'out'}] `);
|
|
82
|
-
}
|
|
83
|
-
return 0;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
wasi.set('fd_close', (fd) => {
|
|
87
|
-
fdMap.delete(fd);
|
|
88
|
-
return 0;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
wasi.set('environ_sizes_get', (countPtr, sizePtr) => {
|
|
92
|
-
return 0;
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
wasi.set('environ_get', (environPtr, environBufPtr) => {
|
|
96
|
-
return 0;
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
wasi.set('proc_exit', (code) => {
|
|
100
|
-
throw new Error(`WASI process exited with code ${code}`);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
wasi.set('args_sizes_get', (argcPtr, argvBufSizePtr) => {
|
|
104
|
-
return 0;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
wasi.set('args_get', (argvPtr, argvBufPtr) => {
|
|
108
|
-
return 0;
|
|
94
|
+
const ctx = new WasiContext({
|
|
95
|
+
args: options.args || [],
|
|
96
|
+
env: options.env || {},
|
|
97
|
+
preopens: options.preopens || [],
|
|
109
98
|
});
|
|
99
|
+
this._wasiContext = ctx;
|
|
100
|
+
}
|
|
110
101
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
102
|
+
getWasiContext() {
|
|
103
|
+
return this._wasiContext;
|
|
114
104
|
}
|
|
115
105
|
}
|
|
116
106
|
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,9 @@ const { WasmRuntime, WasmInstance } = require('./runtime.js');
|
|
|
2
2
|
const { MemoryView } = require('./memory.js');
|
|
3
3
|
const { HostRegistry } = require('./host.js');
|
|
4
4
|
const { ResourceLimits } = require('./limits.js');
|
|
5
|
+
const { WasiContext } = require('./wasi.js');
|
|
6
|
+
const { WasmPool, InstanceLease } = require('./pool.js');
|
|
7
|
+
const { HotReload } = require('./watcher.js');
|
|
5
8
|
const loader = require('./loader.js');
|
|
6
9
|
|
|
7
10
|
function createRuntime(options = {}) {
|
|
@@ -22,5 +25,9 @@ module.exports = {
|
|
|
22
25
|
MemoryView,
|
|
23
26
|
HostRegistry,
|
|
24
27
|
ResourceLimits,
|
|
28
|
+
WasiContext,
|
|
29
|
+
WasmPool,
|
|
30
|
+
InstanceLease,
|
|
31
|
+
HotReload,
|
|
25
32
|
loader,
|
|
26
33
|
};
|
package/src/pool.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class InstanceLease {
|
|
4
|
+
constructor(pool, instance) {
|
|
5
|
+
this.pool = pool;
|
|
6
|
+
this.instance = instance;
|
|
7
|
+
this._released = false;
|
|
8
|
+
this.id = crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
release() {
|
|
12
|
+
if (this._released) return false;
|
|
13
|
+
this._released = true;
|
|
14
|
+
this.pool._release(this);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async call(name, ...args) {
|
|
19
|
+
return this.instance.call(name, ...args);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class WasmPool {
|
|
24
|
+
constructor(runtime, options = {}) {
|
|
25
|
+
this.runtime = runtime;
|
|
26
|
+
this.source = options.source || null;
|
|
27
|
+
this.sourceOptions = options.sourceOptions || {};
|
|
28
|
+
this.maxSize = options.maxSize || 10;
|
|
29
|
+
this.minSize = options.minSize || 0;
|
|
30
|
+
this.acquireTimeout = options.acquireTimeout || 0;
|
|
31
|
+
this.idleTimeout = options.idleTimeout || 60000;
|
|
32
|
+
this._idle = [];
|
|
33
|
+
this._busy = new Map();
|
|
34
|
+
this._pending = [];
|
|
35
|
+
this._closed = false;
|
|
36
|
+
this._warmPromise = null;
|
|
37
|
+
this._reaperTimer = null;
|
|
38
|
+
this._instanceIdCounter = 0;
|
|
39
|
+
this._creating = 0;
|
|
40
|
+
|
|
41
|
+
if (this.idleTimeout > 0) {
|
|
42
|
+
this._startReaper();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_startReaper() {
|
|
47
|
+
this._reaperTimer = setInterval(() => {
|
|
48
|
+
this._reapIdle();
|
|
49
|
+
}, Math.min(this.idleTimeout, 30000));
|
|
50
|
+
if (this._reaperTimer.unref) this._reaperTimer.unref();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async warm() {
|
|
54
|
+
if (this._warmPromise) return this._warmPromise;
|
|
55
|
+
this._warmPromise = this._warmInternal();
|
|
56
|
+
return this._warmPromise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async _warmInternal() {
|
|
60
|
+
const target = this.minSize;
|
|
61
|
+
const toCreate = Math.max(0, target - this._countTotal());
|
|
62
|
+
const tasks = [];
|
|
63
|
+
for (let i = 0; i < toCreate; i++) {
|
|
64
|
+
tasks.push(this._createInstance());
|
|
65
|
+
}
|
|
66
|
+
const instances = await Promise.all(tasks);
|
|
67
|
+
for (const inst of instances) {
|
|
68
|
+
this._idle.push(inst);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _createInstance() {
|
|
73
|
+
const id = ++this._instanceIdCounter;
|
|
74
|
+
const inst = await this.runtime.load(this.source, this.sourceOptions);
|
|
75
|
+
inst._poolId = id;
|
|
76
|
+
inst._createdAt = Date.now();
|
|
77
|
+
return inst;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_countTotal() {
|
|
81
|
+
return this._idle.length + this._busy.size;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async acquire() {
|
|
85
|
+
if (this._closed) throw new Error('Pool is closed');
|
|
86
|
+
|
|
87
|
+
if (this._idle.length > 0) {
|
|
88
|
+
const inst = this._idle.pop();
|
|
89
|
+
this._busy.set(inst._poolId, inst);
|
|
90
|
+
return new InstanceLease(this, inst);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this._countTotal() + this._creating >= this.maxSize) {
|
|
94
|
+
return this._enqueue();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this._creating++;
|
|
98
|
+
try {
|
|
99
|
+
const inst = await this._createInstance();
|
|
100
|
+
this._creating--;
|
|
101
|
+
this._busy.set(inst._poolId, inst);
|
|
102
|
+
return new InstanceLease(this, inst);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this._creating--;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_enqueue() {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
let timer = null;
|
|
112
|
+
const entry = { resolve, reject, timer: null };
|
|
113
|
+
|
|
114
|
+
if (this.acquireTimeout > 0) {
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
const idx = this._pending.indexOf(entry);
|
|
117
|
+
if (idx !== -1) this._pending.splice(idx, 1);
|
|
118
|
+
reject(new Error(`Acquire timed out after ${this.acquireTimeout}ms`));
|
|
119
|
+
}, this.acquireTimeout);
|
|
120
|
+
entry.timer = timer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._pending.push(entry);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_release(lease) {
|
|
128
|
+
const inst = lease.instance;
|
|
129
|
+
this._busy.delete(inst._poolId);
|
|
130
|
+
|
|
131
|
+
if (this._closed) return;
|
|
132
|
+
|
|
133
|
+
if (this._pending.length > 0) {
|
|
134
|
+
this._busy.set(inst._poolId, inst);
|
|
135
|
+
const entry = this._pending.shift();
|
|
136
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
137
|
+
entry.resolve(new InstanceLease(this, inst));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
inst._lastUsedAt = Date.now();
|
|
142
|
+
this._idle.push(inst);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async exec(fn) {
|
|
146
|
+
const lease = await this.acquire();
|
|
147
|
+
try {
|
|
148
|
+
return await fn(lease.instance, lease);
|
|
149
|
+
} finally {
|
|
150
|
+
lease.release();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async drain() {
|
|
155
|
+
this._closed = true;
|
|
156
|
+
|
|
157
|
+
for (const entry of this._pending) {
|
|
158
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
159
|
+
entry.reject(new Error('Pool is draining'));
|
|
160
|
+
}
|
|
161
|
+
this._pending = [];
|
|
162
|
+
|
|
163
|
+
const all = [...this._idle, ...Array.from(this._busy.values())];
|
|
164
|
+
this._idle = [];
|
|
165
|
+
this._busy.clear();
|
|
166
|
+
|
|
167
|
+
if (this._reaperTimer) {
|
|
168
|
+
clearInterval(this._reaperTimer);
|
|
169
|
+
this._reaperTimer = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_reapIdle() {
|
|
174
|
+
if (this._idle.length === 0) return;
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
const keep = Math.max(0, this.minSize);
|
|
177
|
+
const reapable = this._idle
|
|
178
|
+
.filter(i => now - (i._lastUsedAt || i._createdAt) > this.idleTimeout)
|
|
179
|
+
.sort((a, b) => (a._lastUsedAt || a._createdAt) - (b._lastUsedAt || b._createdAt));
|
|
180
|
+
|
|
181
|
+
const toRemove = Math.max(0, reapable.length - keep);
|
|
182
|
+
const removeSet = new Set(reapable.slice(0, toRemove).map(i => i._poolId));
|
|
183
|
+
this._idle = this._idle.filter(i => {
|
|
184
|
+
if (removeSet.has(i._poolId)) {
|
|
185
|
+
this.runtime.removeInstance(i);
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
stats() {
|
|
193
|
+
return {
|
|
194
|
+
idle: this._idle.length,
|
|
195
|
+
busy: this._busy.size,
|
|
196
|
+
pending: this._pending.length,
|
|
197
|
+
total: this._countTotal(),
|
|
198
|
+
maxSize: this.maxSize,
|
|
199
|
+
minSize: this.minSize,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { WasmPool, InstanceLease };
|
package/src/runtime.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const { MemoryView } = require('./memory.js');
|
|
2
2
|
const { HostRegistry } = require('./host.js');
|
|
3
3
|
const { ResourceLimits } = require('./limits.js');
|
|
4
|
+
const { WasmPool } = require('./pool.js');
|
|
5
|
+
const { HotReload } = require('./watcher.js');
|
|
4
6
|
const loader = require('./loader.js');
|
|
5
7
|
|
|
6
8
|
class WasmInstance {
|
|
@@ -101,6 +103,10 @@ class WasmRuntime {
|
|
|
101
103
|
const inst = new WasmInstance(module, importObject, wasmOptions);
|
|
102
104
|
await inst.instantiate(importObject);
|
|
103
105
|
|
|
106
|
+
if (inst.memory) {
|
|
107
|
+
this.hosts.setWasiMemory(inst.memory);
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
inst._meta = { source: result.source, cached: result.cached };
|
|
105
111
|
this.instances.push(inst);
|
|
106
112
|
return inst;
|
|
@@ -115,7 +121,11 @@ class WasmRuntime {
|
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
registerWASI(options = {}) {
|
|
118
|
-
this.hosts.registerWASI(
|
|
124
|
+
this.hosts.registerWASI({
|
|
125
|
+
args: options.args || this.options.wasiArgs || [],
|
|
126
|
+
env: options.env || this.options.wasiEnv || {},
|
|
127
|
+
preopens: options.preopens || this.options.wasiPreopens || [],
|
|
128
|
+
});
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
removeInstance(instance) {
|
|
@@ -127,6 +137,18 @@ class WasmRuntime {
|
|
|
127
137
|
this.instances = [];
|
|
128
138
|
}
|
|
129
139
|
|
|
140
|
+
createPool(options = {}) {
|
|
141
|
+
if (!options.source && !options.sourceOptions) {
|
|
142
|
+
options.sourceOptions = { ...this.options };
|
|
143
|
+
}
|
|
144
|
+
return new WasmPool(this, options);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
watch(source, options = {}) {
|
|
148
|
+
const hr = new HotReload(this, source, options);
|
|
149
|
+
return hr;
|
|
150
|
+
}
|
|
151
|
+
|
|
130
152
|
getStats() {
|
|
131
153
|
return {
|
|
132
154
|
instances: this.instances.length,
|
package/src/wasi.js
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ERRNO_SUCCESS = 0;
|
|
5
|
+
const ERRNO_BADF = 8;
|
|
6
|
+
const ERRNO_NOSYS = 52;
|
|
7
|
+
const ERRNO_INVAL = 28;
|
|
8
|
+
const ERRNO_EXIST = 20;
|
|
9
|
+
const ERRNO_NOTDIR = 54;
|
|
10
|
+
const ERRNO_ISDIR = 31;
|
|
11
|
+
const ERRNO_ACCES = 2;
|
|
12
|
+
const ERRNO_NOSPC = 58;
|
|
13
|
+
const ERRNO_SPIPE = 70;
|
|
14
|
+
const ERRNO_NOMEM = 33;
|
|
15
|
+
const ERRNO_IO = 29;
|
|
16
|
+
const ERRNO_NFILES = 48;
|
|
17
|
+
const ERRNO_PIPE = 71;
|
|
18
|
+
const ERRNO_MFILE = 42;
|
|
19
|
+
|
|
20
|
+
const FILETYPE_DIRECTORY = 3;
|
|
21
|
+
const FILETYPE_REGULAR_FILE = 4;
|
|
22
|
+
const FILETYPE_CHARACTER_DEVICE = 2;
|
|
23
|
+
const FILETYPE_BLOCK_DEVICE = 1;
|
|
24
|
+
const FILETYPE_SOCKET_STREAM = 10;
|
|
25
|
+
const FILETYPE_SYMBOLIC_LINK = 7;
|
|
26
|
+
|
|
27
|
+
const RIGHTS_FD_READ = 1n << 1n;
|
|
28
|
+
const RIGHTS_FD_WRITE = 1n << 5n;
|
|
29
|
+
const RIGHTS_FD_SEEK = 1n << 3n;
|
|
30
|
+
const RIGHTS_PATH_OPEN = 1n << 12n;
|
|
31
|
+
const RIGHTS_FD_TELL = 1n << 4n;
|
|
32
|
+
const RIGHTS_FD_READDIR = 1n << 13n;
|
|
33
|
+
const RIGHTS_FD_FILESTAT_GET = 1n << 14n;
|
|
34
|
+
const RIGHTS_PATH_CREATE_DIRECTORY = 1n << 16n;
|
|
35
|
+
const RIGHTS_PATH_REMOVE_DIRECTORY = 1n << 18n;
|
|
36
|
+
const RIGHTS_PATH_UNLINK_FILE = 1n << 20n;
|
|
37
|
+
const RIGHTS_PATH_RENAME = 1n << 21n;
|
|
38
|
+
const RIGHTS_PATH_FILESTAT_GET = 1n << 23n;
|
|
39
|
+
const RIGHTS_POLL_FD_READWRITE = 1n << 26n;
|
|
40
|
+
|
|
41
|
+
const WHENCE_SET = 0;
|
|
42
|
+
const WHENCE_CUR = 1;
|
|
43
|
+
const WHENCE_END = 2;
|
|
44
|
+
|
|
45
|
+
class FdEntry {
|
|
46
|
+
constructor(type, options = {}) {
|
|
47
|
+
this.type = type;
|
|
48
|
+
this.realFd = options.realFd || null;
|
|
49
|
+
this.virtualPath = options.virtualPath || null;
|
|
50
|
+
this.realPath = options.realPath || null;
|
|
51
|
+
this.flags = options.flags || 0;
|
|
52
|
+
this.position = options.position || 0;
|
|
53
|
+
this.filetype = options.filetype || FILETYPE_REGULAR_FILE;
|
|
54
|
+
this.stream = options.stream || null;
|
|
55
|
+
this.dirEntries = options.dirEntries || null;
|
|
56
|
+
this.readable = !!options.readable;
|
|
57
|
+
this.writable = !!options.writable;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class WasiContext {
|
|
62
|
+
constructor(options = {}) {
|
|
63
|
+
this.args = options.args || [];
|
|
64
|
+
this.env = options.env || {};
|
|
65
|
+
this.preopens = options.preopens || [];
|
|
66
|
+
this._memory = null;
|
|
67
|
+
this._fdMap = new Map();
|
|
68
|
+
this._nextFd = 3;
|
|
69
|
+
this._initStdio();
|
|
70
|
+
this._initPreopens();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setMemory(memory) {
|
|
74
|
+
this._memory = memory;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get memory() {
|
|
78
|
+
return this._memory;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get _memView() {
|
|
82
|
+
return this._memory ? new Uint8Array(this._memory.buffer) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_initStdio() {
|
|
86
|
+
this._fdMap.set(0, new FdEntry('stdio', {
|
|
87
|
+
stream: process.stdin,
|
|
88
|
+
filetype: FILETYPE_CHARACTER_DEVICE,
|
|
89
|
+
readable: true,
|
|
90
|
+
writable: false,
|
|
91
|
+
}));
|
|
92
|
+
this._fdMap.set(1, new FdEntry('stdio', {
|
|
93
|
+
stream: process.stdout,
|
|
94
|
+
filetype: FILETYPE_CHARACTER_DEVICE,
|
|
95
|
+
readable: false,
|
|
96
|
+
writable: true,
|
|
97
|
+
}));
|
|
98
|
+
this._fdMap.set(2, new FdEntry('stdio', {
|
|
99
|
+
stream: process.stderr,
|
|
100
|
+
filetype: FILETYPE_CHARACTER_DEVICE,
|
|
101
|
+
readable: false,
|
|
102
|
+
writable: true,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_initPreopens() {
|
|
107
|
+
for (const preopen of this.preopens) {
|
|
108
|
+
const fd = this._nextFd++;
|
|
109
|
+
this._fdMap.set(fd, new FdEntry('preopen', {
|
|
110
|
+
virtualPath: preopen.virtual || '/',
|
|
111
|
+
realPath: preopen.real || preopen.virtual || '/',
|
|
112
|
+
filetype: FILETYPE_DIRECTORY,
|
|
113
|
+
readable: true,
|
|
114
|
+
writable: true,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_getFd(fd) {
|
|
120
|
+
const entry = this._fdMap.get(fd);
|
|
121
|
+
if (!entry) return null;
|
|
122
|
+
return entry;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_resolvePath(virtualPath) {
|
|
126
|
+
const abs = virtualPath.startsWith('/') ? virtualPath : '/' + virtualPath;
|
|
127
|
+
for (const [fd, entry] of this._fdMap) {
|
|
128
|
+
if (entry.type === 'preopen' && abs === entry.virtualPath) {
|
|
129
|
+
return { realPath: entry.realPath, preopen: entry };
|
|
130
|
+
}
|
|
131
|
+
if (entry.type === 'preopen' && (abs + '/').startsWith(entry.virtualPath + '/')) {
|
|
132
|
+
const relative = abs.slice(entry.virtualPath.length);
|
|
133
|
+
const realPath = path.join(entry.realPath, relative);
|
|
134
|
+
const resolved = path.resolve(realPath);
|
|
135
|
+
if (!resolved.startsWith(path.resolve(entry.realPath))) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return { realPath: resolved, preopen: entry };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_readWasmI32(ptr) {
|
|
145
|
+
if (!this._memory) return 0;
|
|
146
|
+
const dv = new DataView(this._memory.buffer);
|
|
147
|
+
return dv.getInt32(ptr, true);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_writeWasmI32(ptr, value) {
|
|
151
|
+
if (!this._memory) return;
|
|
152
|
+
const dv = new DataView(this._memory.buffer);
|
|
153
|
+
dv.setInt32(ptr, value, true);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_writeWasmI64(ptr, value) {
|
|
157
|
+
if (!this._memory) return;
|
|
158
|
+
const dv = new DataView(this._memory.buffer);
|
|
159
|
+
dv.setBigInt64(ptr, BigInt(value), true);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_writeWasmU64(ptr, value) {
|
|
163
|
+
if (!this._memory) return;
|
|
164
|
+
const dv = new DataView(this._memory.buffer);
|
|
165
|
+
dv.setBigUint64(ptr, BigInt(value), true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_writeWasmBytes(ptr, data) {
|
|
169
|
+
if (!this._memory) return;
|
|
170
|
+
new Uint8Array(this._memory.buffer, ptr, data.length).set(data);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_readIovs(iovsPtr, iovsLen) {
|
|
174
|
+
const buffers = [];
|
|
175
|
+
for (let i = 0; i < iovsLen; i++) {
|
|
176
|
+
const bufPtr = this._readWasmI32(iovsPtr + i * 8);
|
|
177
|
+
const bufLen = this._readWasmI32(iovsPtr + i * 8 + 4);
|
|
178
|
+
if (bufLen > 0 && this._memory) {
|
|
179
|
+
const slice = new Uint8Array(this._memory.buffer, bufPtr, bufLen);
|
|
180
|
+
buffers.push(slice);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return buffers;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_writeIovs(iovsPtr, iovsLen, data) {
|
|
187
|
+
let offset = 0;
|
|
188
|
+
for (let i = 0; i < iovsLen; i++) {
|
|
189
|
+
const bufPtr = this._readWasmI32(iovsPtr + i * 8);
|
|
190
|
+
const bufLen = this._readWasmI32(iovsPtr + i * 8 + 4);
|
|
191
|
+
const remaining = data.length - offset;
|
|
192
|
+
const toWrite = Math.min(bufLen, remaining);
|
|
193
|
+
if (toWrite > 0 && this._memory) {
|
|
194
|
+
new Uint8Array(this._memory.buffer, bufPtr, toWrite).set(data.slice(offset, offset + toWrite));
|
|
195
|
+
offset += toWrite;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return offset;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
|
|
202
|
+
const entry = this._getFd(fd);
|
|
203
|
+
if (!entry) return ERRNO_BADF;
|
|
204
|
+
if (!entry.writable) return ERRNO_BADF;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const buffers = this._readIovs(iovsPtr, iovsLen);
|
|
208
|
+
let totalWritten = 0;
|
|
209
|
+
|
|
210
|
+
if (fd <= 2) {
|
|
211
|
+
for (const buf of buffers) {
|
|
212
|
+
const str = new TextDecoder().decode(buf);
|
|
213
|
+
if (fd === 1) process.stdout.write(str);
|
|
214
|
+
else process.stderr.write(str);
|
|
215
|
+
totalWritten += buf.length;
|
|
216
|
+
}
|
|
217
|
+
} else if (entry.realFd !== null) {
|
|
218
|
+
const allData = Buffer.concat(buffers.map(b => Buffer.from(b)));
|
|
219
|
+
const written = fs.writeSync(entry.realFd, allData, 0, allData.length, entry.position);
|
|
220
|
+
if (written > 0) entry.position += written;
|
|
221
|
+
totalWritten = written;
|
|
222
|
+
} else {
|
|
223
|
+
const allData = Buffer.concat(buffers.map(b => Buffer.from(b)));
|
|
224
|
+
totalWritten = allData.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (nwrittenPtr != null) this._writeWasmI32(nwrittenPtr, totalWritten);
|
|
228
|
+
return ERRNO_SUCCESS;
|
|
229
|
+
} catch {
|
|
230
|
+
return ERRNO_IO;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fd_read(fd, iovsPtr, iovsLen, nreadPtr) {
|
|
235
|
+
const entry = this._getFd(fd);
|
|
236
|
+
if (!entry) return ERRNO_BADF;
|
|
237
|
+
if (!entry.readable) return ERRNO_BADF;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
let totalRead = 0;
|
|
241
|
+
if (fd === 0) {
|
|
242
|
+
} else if (entry.realFd !== null) {
|
|
243
|
+
const buffers = this._readIovs(iovsPtr, iovsLen);
|
|
244
|
+
for (const buf of buffers) {
|
|
245
|
+
const bytesRead = fs.readSync(entry.realFd, buf, 0, buf.length, entry.position);
|
|
246
|
+
entry.position += bytesRead;
|
|
247
|
+
totalRead += bytesRead;
|
|
248
|
+
if (bytesRead < buf.length) break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (nreadPtr != null) this._writeWasmI32(nreadPtr, totalRead);
|
|
253
|
+
return ERRNO_SUCCESS;
|
|
254
|
+
} catch {
|
|
255
|
+
return ERRNO_IO;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fd_close(fd) {
|
|
260
|
+
const entry = this._getFd(fd);
|
|
261
|
+
if (!entry) return ERRNO_BADF;
|
|
262
|
+
if (fd <= 2) return ERRNO_SUCCESS;
|
|
263
|
+
try {
|
|
264
|
+
if (entry.realFd !== null) fs.closeSync(entry.realFd);
|
|
265
|
+
this._fdMap.delete(fd);
|
|
266
|
+
return ERRNO_SUCCESS;
|
|
267
|
+
} catch {
|
|
268
|
+
return ERRNO_IO;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fd_seek(fd, offset, whence, newoffsetPtr) {
|
|
273
|
+
const entry = this._getFd(fd);
|
|
274
|
+
if (!entry) return ERRNO_BADF;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
let newPos;
|
|
278
|
+
switch (whence) {
|
|
279
|
+
case WHENCE_SET: newPos = offset; break;
|
|
280
|
+
case WHENCE_CUR: newPos = entry.position + offset; break;
|
|
281
|
+
case WHENCE_END:
|
|
282
|
+
if (entry.realFd !== null) {
|
|
283
|
+
const stat = fs.fstatSync(entry.realFd);
|
|
284
|
+
newPos = stat.size + offset;
|
|
285
|
+
} else {
|
|
286
|
+
newPos = entry.position;
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
default: return ERRNO_INVAL;
|
|
290
|
+
}
|
|
291
|
+
if (newPos < 0) return ERRNO_INVAL;
|
|
292
|
+
entry.position = newPos;
|
|
293
|
+
if (entry.realFd !== null) {
|
|
294
|
+
fs.fstatSync(entry.realFd);
|
|
295
|
+
}
|
|
296
|
+
if (newoffsetPtr != null) this._writeWasmI64(newoffsetPtr, newPos);
|
|
297
|
+
return ERRNO_SUCCESS;
|
|
298
|
+
} catch {
|
|
299
|
+
return ERRNO_IO;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fd_fdstat_get(fd, statPtr) {
|
|
304
|
+
const entry = this._getFd(fd);
|
|
305
|
+
if (!entry) return ERRNO_BADF;
|
|
306
|
+
if (statPtr == null || !this._memory) return ERRNO_INVAL;
|
|
307
|
+
|
|
308
|
+
const view = new DataView(this._memory.buffer);
|
|
309
|
+
view.setUint8(statPtr, entry.filetype);
|
|
310
|
+
view.setUint16(statPtr + 2, entry.flags, true);
|
|
311
|
+
const baseRights = entry.readable ? RIGHTS_FD_READ : 0n;
|
|
312
|
+
const writeRights = entry.writable ? RIGHTS_FD_WRITE : 0n;
|
|
313
|
+
view.setBigUint64(statPtr + 8, baseRights | writeRights, true);
|
|
314
|
+
view.setBigUint64(statPtr + 16, baseRights | writeRights, true);
|
|
315
|
+
|
|
316
|
+
return ERRNO_SUCCESS;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fd_prestat_get(fd, prestatPtr) {
|
|
320
|
+
const entry = this._getFd(fd);
|
|
321
|
+
if (!entry) return ERRNO_BADF;
|
|
322
|
+
if (entry.type !== 'preopen') return ERRNO_NOSYS;
|
|
323
|
+
|
|
324
|
+
const view = new DataView(this._memory.buffer);
|
|
325
|
+
view.setUint8(prestatPtr, 0);
|
|
326
|
+
const dirName = entry.virtualPath || '/';
|
|
327
|
+
view.setUint32(prestatPtr + 4, Buffer.byteLength(dirName), true);
|
|
328
|
+
return ERRNO_SUCCESS;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
fd_prestat_dir_name(fd, bufPtr, bufLen) {
|
|
332
|
+
const entry = this._getFd(fd);
|
|
333
|
+
if (!entry) return ERRNO_BADF;
|
|
334
|
+
if (entry.type !== 'preopen') return ERRNO_NOSYS;
|
|
335
|
+
|
|
336
|
+
const dirName = entry.virtualPath || '/';
|
|
337
|
+
const nameBytes = Buffer.from(dirName, 'utf8');
|
|
338
|
+
if (nameBytes.length > bufLen) return ERRNO_NOMEM;
|
|
339
|
+
|
|
340
|
+
this._writeWasmBytes(bufPtr, nameBytes);
|
|
341
|
+
return ERRNO_SUCCESS;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fd_filestat_get(fd, statPtr) {
|
|
345
|
+
const entry = this._getFd(fd);
|
|
346
|
+
if (!entry) return ERRNO_BADF;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
let stat;
|
|
350
|
+
if (entry.realFd !== null) {
|
|
351
|
+
stat = fs.fstatSync(entry.realFd);
|
|
352
|
+
} else {
|
|
353
|
+
stat = { size: 0, mode: 0, atimeMs: 0, mtimeMs: 0, ctimeMs: 0, dev: 0, ino: 0, nlink: 0 };
|
|
354
|
+
}
|
|
355
|
+
this._writeFilestat(statPtr, stat, entry.filetype);
|
|
356
|
+
return ERRNO_SUCCESS;
|
|
357
|
+
} catch {
|
|
358
|
+
return ERRNO_IO;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_writeFilestat(ptr, stat, filetype) {
|
|
363
|
+
const view = new DataView(this._memory.buffer);
|
|
364
|
+
view.setBigUint64(ptr, BigInt(stat.dev || 0), true);
|
|
365
|
+
view.setBigUint64(ptr + 8, BigInt(stat.ino || 0), true);
|
|
366
|
+
view.setUint8(ptr + 16, filetype || FILETYPE_REGULAR_FILE);
|
|
367
|
+
view.setBigUint64(ptr + 24, BigInt(stat.nlink || 1), true);
|
|
368
|
+
view.setBigUint64(ptr + 32, BigInt(stat.size || 0), true);
|
|
369
|
+
view.setBigUint64(ptr + 40, BigInt(Math.floor((stat.atimeMs || 0) * 1e6)), true);
|
|
370
|
+
view.setBigUint64(ptr + 48, BigInt(Math.floor((stat.mtimeMs || 0) * 1e6)), true);
|
|
371
|
+
view.setBigUint64(ptr + 56, BigInt(Math.floor((stat.ctimeMs || 0) * 1e6)), true);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
path_open(fd, dirFlags, pathPtr, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, openedFdPtr) {
|
|
375
|
+
const entry = this._getFd(fd);
|
|
376
|
+
if (!entry) return ERRNO_BADF;
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const name = this._readString(pathPtr, pathLen);
|
|
380
|
+
const resolved = this._resolvePath(name);
|
|
381
|
+
if (!resolved) return ERRNO_ACCES;
|
|
382
|
+
|
|
383
|
+
const realPath = resolved.realPath;
|
|
384
|
+
let hostFlags = fs.constants.O_RDONLY;
|
|
385
|
+
if (fsRightsBase & BigInt(RIGHTS_FD_WRITE)) {
|
|
386
|
+
hostFlags = fs.constants.O_RDWR;
|
|
387
|
+
}
|
|
388
|
+
if (oflags & 1) hostFlags |= fs.constants.O_CREAT;
|
|
389
|
+
if (oflags & 2) hostFlags |= fs.constants.O_EXCL;
|
|
390
|
+
if (oflags & 4) hostFlags |= fs.constants.O_TRUNC;
|
|
391
|
+
if (oflags & 8) hostFlags |= fs.constants.O_APPEND;
|
|
392
|
+
|
|
393
|
+
const realFd = fs.openSync(realPath, hostFlags);
|
|
394
|
+
const newFd = this._nextFd++;
|
|
395
|
+
const stat = fs.fstatSync(realFd);
|
|
396
|
+
const filetype = stat.isDirectory() ? FILETYPE_DIRECTORY : FILETYPE_REGULAR_FILE;
|
|
397
|
+
|
|
398
|
+
this._fdMap.set(newFd, new FdEntry('file', {
|
|
399
|
+
realFd,
|
|
400
|
+
virtualPath: name,
|
|
401
|
+
realPath,
|
|
402
|
+
filetype,
|
|
403
|
+
readable: !!(fsRightsBase & RIGHTS_FD_READ),
|
|
404
|
+
writable: !!(fsRightsBase & RIGHTS_FD_WRITE),
|
|
405
|
+
}));
|
|
406
|
+
|
|
407
|
+
if (openedFdPtr != null) this._writeWasmI32(openedFdPtr, newFd);
|
|
408
|
+
return ERRNO_SUCCESS;
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err.code === 'EEXIST') return ERRNO_EXIST;
|
|
411
|
+
if (err.code === 'ENOENT') return ERRNO_NOSYS;
|
|
412
|
+
if (err.code === 'EACCES') return ERRNO_ACCES;
|
|
413
|
+
if (err.code === 'ENOTDIR') return ERRNO_NOTDIR;
|
|
414
|
+
if (err.code === 'EISDIR') return ERRNO_ISDIR;
|
|
415
|
+
return ERRNO_IO;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
path_create_directory(fd, pathPtr, pathLen) {
|
|
420
|
+
try {
|
|
421
|
+
const name = this._readString(pathPtr, pathLen);
|
|
422
|
+
const resolved = this._resolvePath(name);
|
|
423
|
+
if (!resolved) return ERRNO_ACCES;
|
|
424
|
+
fs.mkdirSync(resolved.realPath, { recursive: true });
|
|
425
|
+
return ERRNO_SUCCESS;
|
|
426
|
+
} catch (err) {
|
|
427
|
+
if (err.code === 'EEXIST') return ERRNO_EXIST;
|
|
428
|
+
return ERRNO_IO;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
path_remove_directory(fd, pathPtr, pathLen) {
|
|
433
|
+
try {
|
|
434
|
+
const name = this._readString(pathPtr, pathLen);
|
|
435
|
+
const resolved = this._resolvePath(name);
|
|
436
|
+
if (!resolved) return ERRNO_ACCES;
|
|
437
|
+
fs.rmdirSync(resolved.realPath);
|
|
438
|
+
return ERRNO_SUCCESS;
|
|
439
|
+
} catch {
|
|
440
|
+
return ERRNO_IO;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
path_unlink_file(fd, pathPtr, pathLen) {
|
|
445
|
+
try {
|
|
446
|
+
const name = this._readString(pathPtr, pathLen);
|
|
447
|
+
const resolved = this._resolvePath(name);
|
|
448
|
+
if (!resolved) return ERRNO_ACCES;
|
|
449
|
+
fs.unlinkSync(resolved.realPath);
|
|
450
|
+
return ERRNO_SUCCESS;
|
|
451
|
+
} catch {
|
|
452
|
+
return ERRNO_IO;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
path_rename(fd, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) {
|
|
457
|
+
try {
|
|
458
|
+
const oldName = this._readString(oldPathPtr, oldPathLen);
|
|
459
|
+
const newName = this._readString(newPathPtr, newPathLen);
|
|
460
|
+
const oldResolved = this._resolvePath(oldName);
|
|
461
|
+
const newResolved = this._resolvePath(newName);
|
|
462
|
+
if (!oldResolved || !newResolved) return ERRNO_ACCES;
|
|
463
|
+
fs.renameSync(oldResolved.realPath, newResolved.realPath);
|
|
464
|
+
return ERRNO_SUCCESS;
|
|
465
|
+
} catch {
|
|
466
|
+
return ERRNO_IO;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
path_filestat_get(fd, flags, pathPtr, pathLen, statPtr) {
|
|
471
|
+
try {
|
|
472
|
+
const name = this._readString(pathPtr, pathLen);
|
|
473
|
+
const resolved = this._resolvePath(name);
|
|
474
|
+
if (!resolved) return ERRNO_ACCES;
|
|
475
|
+
|
|
476
|
+
const stat = fs.statSync(resolved.realPath);
|
|
477
|
+
const filetype = stat.isDirectory() ? FILETYPE_DIRECTORY : FILETYPE_REGULAR_FILE;
|
|
478
|
+
this._writeFilestat(statPtr, stat, filetype);
|
|
479
|
+
return ERRNO_SUCCESS;
|
|
480
|
+
} catch {
|
|
481
|
+
return ERRNO_IO;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fd_readdir(fd, bufPtr, bufLen, cookie, bufusedPtr) {
|
|
486
|
+
const entry = this._getFd(fd);
|
|
487
|
+
if (!entry) return ERRNO_BADF;
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
if (!entry.dirEntries) {
|
|
491
|
+
const realPath = entry.realPath || '.';
|
|
492
|
+
const names = fs.readdirSync(realPath);
|
|
493
|
+
entry.dirEntries = names;
|
|
494
|
+
entry._dirCookie = 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let offset = 0;
|
|
498
|
+
const startIdx = Number(cookie);
|
|
499
|
+
const view = new Uint8Array(this._memory.buffer);
|
|
500
|
+
|
|
501
|
+
for (let i = startIdx; i < entry.dirEntries.length && offset < bufLen; i++) {
|
|
502
|
+
const name = entry.dirEntries[i];
|
|
503
|
+
const nameBytes = Buffer.from(name, 'utf8');
|
|
504
|
+
const entrySize = 24 + nameBytes.length;
|
|
505
|
+
if (offset + entrySize > bufLen) break;
|
|
506
|
+
|
|
507
|
+
const dataView = new DataView(this._memory.buffer);
|
|
508
|
+
dataView.setBigUint64(bufPtr + offset, BigInt(i + 1), true);
|
|
509
|
+
dataView.setBigUint64(bufPtr + offset + 8, BigInt(nameBytes.length), true);
|
|
510
|
+
dataView.setUint8(bufPtr + offset + 16, FILETYPE_REGULAR_FILE);
|
|
511
|
+
view.set(nameBytes, bufPtr + offset + 24);
|
|
512
|
+
offset += entrySize;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (bufusedPtr != null) this._writeWasmI32(bufusedPtr, offset);
|
|
516
|
+
return ERRNO_SUCCESS;
|
|
517
|
+
} catch {
|
|
518
|
+
return ERRNO_IO;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
path_readlink(fd, pathPtr, pathLen, bufPtr, bufLen, bufusedPtr) {
|
|
523
|
+
try {
|
|
524
|
+
const name = this._readString(pathPtr, pathLen);
|
|
525
|
+
const resolved = this._resolvePath(name);
|
|
526
|
+
if (!resolved) return ERRNO_ACCES;
|
|
527
|
+
|
|
528
|
+
const target = fs.readlinkSync(resolved.realPath);
|
|
529
|
+
const targetBytes = Buffer.from(target, 'utf8');
|
|
530
|
+
const toWrite = Math.min(targetBytes.length, bufLen);
|
|
531
|
+
this._writeWasmBytes(bufPtr, targetBytes.slice(0, toWrite));
|
|
532
|
+
if (bufusedPtr != null) this._writeWasmI32(bufusedPtr, toWrite);
|
|
533
|
+
return ERRNO_SUCCESS;
|
|
534
|
+
} catch {
|
|
535
|
+
return ERRNO_IO;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
path_symlink(fd, oldPathPtr, oldPathLen, newPathPtr, newPathLen) {
|
|
540
|
+
try {
|
|
541
|
+
const oldName = this._readString(oldPathPtr, oldPathLen);
|
|
542
|
+
const newName = this._readString(newPathPtr, newPathLen);
|
|
543
|
+
const newResolved = this._resolvePath(newName);
|
|
544
|
+
if (!newResolved) return ERRNO_ACCES;
|
|
545
|
+
fs.symlinkSync(oldName, newResolved.realPath);
|
|
546
|
+
return ERRNO_SUCCESS;
|
|
547
|
+
} catch {
|
|
548
|
+
return ERRNO_IO;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
clock_time_get(clockId, precision, timePtr) {
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
const ns = clockId === 0 ? BigInt(now) * 1000000n : BigInt(process.hrtime.bigint());
|
|
555
|
+
if (timePtr != null && this._memory) {
|
|
556
|
+
const view = new DataView(this._memory.buffer);
|
|
557
|
+
view.setBigUint64(timePtr, ns, true);
|
|
558
|
+
}
|
|
559
|
+
return ERRNO_SUCCESS;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
clock_res_get(clockId, resolutionPtr) {
|
|
563
|
+
if (resolutionPtr != null && this._memory) {
|
|
564
|
+
const view = new DataView(this._memory.buffer);
|
|
565
|
+
view.setBigUint64(resolutionPtr, 1000n, true);
|
|
566
|
+
}
|
|
567
|
+
return ERRNO_SUCCESS;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
random_get(bufPtr, bufLen) {
|
|
571
|
+
const crypto = require('crypto');
|
|
572
|
+
const bytes = crypto.randomBytes(bufLen);
|
|
573
|
+
if (this._memory) {
|
|
574
|
+
this._writeWasmBytes(bufPtr, bytes);
|
|
575
|
+
}
|
|
576
|
+
return ERRNO_SUCCESS;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
proc_exit(code) {
|
|
580
|
+
throw new Error(`WASI process exited with code ${code}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
args_sizes_get(argcPtr, argvBufSizePtr) {
|
|
584
|
+
const totalSize = this.args.reduce((sum, a) => sum + Buffer.byteLength(a) + 1, 0);
|
|
585
|
+
if (argcPtr != null) this._writeWasmI32(argcPtr, this.args.length);
|
|
586
|
+
if (argvBufSizePtr != null) this._writeWasmI32(argvBufSizePtr, totalSize);
|
|
587
|
+
return ERRNO_SUCCESS;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
args_get(argvPtr, argvBufPtr) {
|
|
591
|
+
let bufOffset = 0;
|
|
592
|
+
for (let i = 0; i < this.args.length; i++) {
|
|
593
|
+
const arg = this.args[i];
|
|
594
|
+
const bytes = Buffer.from(arg, 'utf8');
|
|
595
|
+
this._writeWasmI32(argvPtr + i * 4, argvBufPtr + bufOffset);
|
|
596
|
+
this._writeWasmBytes(argvBufPtr + bufOffset, bytes);
|
|
597
|
+
this._writeWasmBytes(argvBufPtr + bufOffset + bytes.length, Buffer.from([0]));
|
|
598
|
+
bufOffset += bytes.length + 1;
|
|
599
|
+
}
|
|
600
|
+
return ERRNO_SUCCESS;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
environ_sizes_get(countPtr, sizePtr) {
|
|
604
|
+
const envKeys = Object.keys(this.env);
|
|
605
|
+
const totalSize = envKeys.reduce((sum, k) => {
|
|
606
|
+
return sum + Buffer.byteLength(k) + 1 + Buffer.byteLength(String(this.env[k])) + 1;
|
|
607
|
+
}, 0);
|
|
608
|
+
if (countPtr != null) this._writeWasmI32(countPtr, envKeys.length);
|
|
609
|
+
if (sizePtr != null) this._writeWasmI32(sizePtr, totalSize);
|
|
610
|
+
return ERRNO_SUCCESS;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
environ_get(environPtr, environBufPtr) {
|
|
614
|
+
let bufOffset = 0;
|
|
615
|
+
let i = 0;
|
|
616
|
+
for (const [key, val] of Object.entries(this.env)) {
|
|
617
|
+
const entry = `${key}=${val}`;
|
|
618
|
+
const bytes = Buffer.from(entry, 'utf8');
|
|
619
|
+
this._writeWasmI32(environPtr + i * 4, environBufPtr + bufOffset);
|
|
620
|
+
this._writeWasmBytes(environBufPtr + bufOffset, bytes);
|
|
621
|
+
this._writeWasmBytes(environBufPtr + bufOffset + bytes.length, Buffer.from([0]));
|
|
622
|
+
bufOffset += bytes.length + 1;
|
|
623
|
+
i++;
|
|
624
|
+
}
|
|
625
|
+
return ERRNO_SUCCESS;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
poll_oneoff(inPtr, outPtr, nsubscriptions, neventsPtr) {
|
|
629
|
+
if (neventsPtr != null) this._writeWasmI32(neventsPtr, 0);
|
|
630
|
+
return ERRNO_SUCCESS;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
sched_yield() {
|
|
634
|
+
return ERRNO_SUCCESS;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fd_advise(fd, offset, len, advice) {
|
|
638
|
+
return ERRNO_SUCCESS;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fd_allocate(fd, offset, len) {
|
|
642
|
+
return ERRNO_SUCCESS;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
fd_datasync(fd) {
|
|
646
|
+
const entry = this._getFd(fd);
|
|
647
|
+
if (!entry) return ERRNO_BADF;
|
|
648
|
+
try {
|
|
649
|
+
if (entry.realFd !== null) fs.fdatasyncSync(entry.realFd);
|
|
650
|
+
return ERRNO_SUCCESS;
|
|
651
|
+
} catch {
|
|
652
|
+
return ERRNO_IO;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
fd_sync(fd) {
|
|
657
|
+
const entry = this._getFd(fd);
|
|
658
|
+
if (!entry) return ERRNO_BADF;
|
|
659
|
+
try {
|
|
660
|
+
if (entry.realFd !== null) fs.fsyncSync(entry.realFd);
|
|
661
|
+
return ERRNO_SUCCESS;
|
|
662
|
+
} catch {
|
|
663
|
+
return ERRNO_IO;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
fd_fdstat_set_flags(fd, flags) {
|
|
668
|
+
const entry = this._getFd(fd);
|
|
669
|
+
if (!entry) return ERRNO_BADF;
|
|
670
|
+
entry.flags = flags;
|
|
671
|
+
return ERRNO_SUCCESS;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
fd_tell(fd, newoffsetPtr) {
|
|
675
|
+
const entry = this._getFd(fd);
|
|
676
|
+
if (!entry) return ERRNO_BADF;
|
|
677
|
+
if (newoffsetPtr != null) this._writeWasmI64(newoffsetPtr, entry.position);
|
|
678
|
+
return ERRNO_SUCCESS;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
fd_renumber(from, to) {
|
|
682
|
+
const fromEntry = this._getFd(from);
|
|
683
|
+
if (!fromEntry) return ERRNO_BADF;
|
|
684
|
+
const toEntry = this._getFd(to);
|
|
685
|
+
if (!toEntry) return ERRNO_BADF;
|
|
686
|
+
this._fdMap.set(to, fromEntry);
|
|
687
|
+
this._fdMap.delete(from);
|
|
688
|
+
return ERRNO_SUCCESS;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
_readString(ptr, len) {
|
|
692
|
+
if (!this._memory || ptr == null || len == null || !len) return '';
|
|
693
|
+
const bytes = new Uint8Array(this._memory.buffer, ptr, len);
|
|
694
|
+
return new TextDecoder().decode(bytes);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
toImportObject() {
|
|
698
|
+
const ctx = this;
|
|
699
|
+
const funcs = {
|
|
700
|
+
fd_write: (fd, iovs, iovsLen, nwritten) => ctx.fd_write(fd, iovs, iovsLen, nwritten),
|
|
701
|
+
fd_read: (fd, iovs, iovsLen, nread) => ctx.fd_read(fd, iovs, iovsLen, nread),
|
|
702
|
+
fd_close: (fd) => ctx.fd_close(fd),
|
|
703
|
+
fd_seek: (fd, offset, whence, newoffset) => ctx.fd_seek(fd, Number(offset), whence, newoffset),
|
|
704
|
+
fd_fdstat_get: (fd, stat) => ctx.fd_fdstat_get(fd, stat),
|
|
705
|
+
fd_fdstat_set_flags: (fd, flags) => ctx.fd_fdstat_set_flags(fd, flags),
|
|
706
|
+
fd_prestat_get: (fd, buf) => ctx.fd_prestat_get(fd, buf),
|
|
707
|
+
fd_prestat_dir_name: (fd, buf, len) => ctx.fd_prestat_dir_name(fd, buf, len),
|
|
708
|
+
fd_filestat_get: (fd, buf) => ctx.fd_filestat_get(fd, buf),
|
|
709
|
+
fd_readdir: (fd, buf, len, cookie, bufused) => ctx.fd_readdir(fd, buf, len, cookie, bufused),
|
|
710
|
+
fd_tell: (fd, newoffset) => ctx.fd_tell(fd, newoffset),
|
|
711
|
+
fd_renumber: (from, to) => ctx.fd_renumber(from, to),
|
|
712
|
+
fd_datasync: (fd) => ctx.fd_datasync(fd),
|
|
713
|
+
fd_sync: (fd) => ctx.fd_sync(fd),
|
|
714
|
+
fd_advise: (fd, offset, len, advice) => ctx.fd_advise(fd, Number(offset), Number(len), advice),
|
|
715
|
+
fd_allocate: (fd, offset, len) => ctx.fd_allocate(fd, Number(offset), Number(len)),
|
|
716
|
+
path_open: (fd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, openedFd) =>
|
|
717
|
+
ctx.path_open(fd, dirflags, path, pathLen, oflags, fsRightsBase, fsRightsInheriting, fdFlags, openedFd),
|
|
718
|
+
path_create_directory: (fd, path, pathLen) => ctx.path_create_directory(fd, path, pathLen),
|
|
719
|
+
path_remove_directory: (fd, path, pathLen) => ctx.path_remove_directory(fd, path, pathLen),
|
|
720
|
+
path_unlink_file: (fd, path, pathLen) => ctx.path_unlink_file(fd, path, pathLen),
|
|
721
|
+
path_rename: (fd, oldPath, oldPathLen, newFd, newPath, newPathLen) =>
|
|
722
|
+
ctx.path_rename(fd, oldPath, oldPathLen, newFd, newPath, newPathLen),
|
|
723
|
+
path_filestat_get: (fd, flags, path, pathLen, buf) => ctx.path_filestat_get(fd, flags, path, pathLen, buf),
|
|
724
|
+
path_readlink: (fd, path, pathLen, buf, bufLen, bufused) =>
|
|
725
|
+
ctx.path_readlink(fd, path, pathLen, buf, bufLen, bufused),
|
|
726
|
+
path_symlink: (fd, oldPath, oldPathLen, newPath, newPathLen) =>
|
|
727
|
+
ctx.path_symlink(fd, oldPath, oldPathLen, newPath, newPathLen),
|
|
728
|
+
clock_time_get: (id, precision, time) => ctx.clock_time_get(id, Number(precision), time),
|
|
729
|
+
clock_res_get: (id, resolution) => ctx.clock_res_get(id, resolution),
|
|
730
|
+
random_get: (buf, len) => ctx.random_get(buf, len),
|
|
731
|
+
proc_exit: (code) => ctx.proc_exit(code),
|
|
732
|
+
args_sizes_get: (argc, bufSize) => ctx.args_sizes_get(argc, bufSize),
|
|
733
|
+
args_get: (argv, buf) => ctx.args_get(argv, buf),
|
|
734
|
+
environ_sizes_get: (count, size) => ctx.environ_sizes_get(count, size),
|
|
735
|
+
environ_get: (environ, buf) => ctx.environ_get(environ, buf),
|
|
736
|
+
poll_oneoff: (inPtr, outPtr, nsubscriptions, nevents) =>
|
|
737
|
+
ctx.poll_oneoff(inPtr, outPtr, nsubscriptions, nevents),
|
|
738
|
+
sched_yield: () => ctx.sched_yield(),
|
|
739
|
+
};
|
|
740
|
+
return { 'wasi_snapshot_preview1': funcs, 'wasi_unstable': funcs };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
module.exports = { WasiContext, ERRNO_SUCCESS, ERRNO_BADF, ERRNO_INVAL, ERRNO_ACCES, ERRNO_EXIST };
|
package/src/watcher.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
|
|
5
|
+
class HotReload extends EventEmitter {
|
|
6
|
+
constructor(runtime, source, options = {}) {
|
|
7
|
+
super();
|
|
8
|
+
this.runtime = runtime;
|
|
9
|
+
this.source = path.resolve(source);
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.pollInterval = options.pollInterval || 1000;
|
|
12
|
+
this._currentModule = null;
|
|
13
|
+
this._currentInstance = null;
|
|
14
|
+
this._watcher = null;
|
|
15
|
+
this._pollTimer = null;
|
|
16
|
+
this._active = false;
|
|
17
|
+
this._reloading = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async start() {
|
|
21
|
+
if (this._active) return;
|
|
22
|
+
this._active = true;
|
|
23
|
+
|
|
24
|
+
const mode = this.options.mode || 'poll';
|
|
25
|
+
|
|
26
|
+
if (mode === 'watch') {
|
|
27
|
+
this._startWatcher();
|
|
28
|
+
} else {
|
|
29
|
+
this._startPolling();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await this._loadModule();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_startWatcher() {
|
|
36
|
+
try {
|
|
37
|
+
this._watcher = fs.watch(this.source, (eventType) => {
|
|
38
|
+
if (eventType === 'change') {
|
|
39
|
+
this._scheduleReload();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
this._startPolling();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_startPolling() {
|
|
48
|
+
this._lastMtime = this._getMtime();
|
|
49
|
+
this._pollTimer = setInterval(() => {
|
|
50
|
+
const mtime = this._getMtime();
|
|
51
|
+
if (mtime && mtime !== this._lastMtime) {
|
|
52
|
+
this._lastMtime = mtime;
|
|
53
|
+
this._scheduleReload();
|
|
54
|
+
}
|
|
55
|
+
}, this.pollInterval);
|
|
56
|
+
if (this._pollTimer.unref) this._pollTimer.unref();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_getMtime() {
|
|
60
|
+
try {
|
|
61
|
+
return fs.statSync(this.source).mtimeMs;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_scheduleReload() {
|
|
68
|
+
if (this._reloading) return;
|
|
69
|
+
this._reloading = true;
|
|
70
|
+
|
|
71
|
+
const debounceMs = this.options.debounce || 200;
|
|
72
|
+
if (this._reloadTimer) clearTimeout(this._reloadTimer);
|
|
73
|
+
|
|
74
|
+
this._reloadTimer = setTimeout(async () => {
|
|
75
|
+
try {
|
|
76
|
+
await this._loadModule();
|
|
77
|
+
this.emit('reload', this._currentInstance);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.emit('error', err);
|
|
80
|
+
} finally {
|
|
81
|
+
this._reloading = false;
|
|
82
|
+
}
|
|
83
|
+
}, debounceMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async _loadModule() {
|
|
87
|
+
const oldInstance = this._currentInstance;
|
|
88
|
+
const newInstance = await this.runtime.load(this.source, this.options.sourceOptions);
|
|
89
|
+
|
|
90
|
+
this._currentInstance = newInstance;
|
|
91
|
+
this._currentModule = true;
|
|
92
|
+
this.emit('load', newInstance);
|
|
93
|
+
|
|
94
|
+
if (oldInstance) {
|
|
95
|
+
this.runtime.removeInstance(oldInstance);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getInstance() {
|
|
100
|
+
return this._currentInstance;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async call(name, ...args) {
|
|
104
|
+
if (!this._currentInstance) {
|
|
105
|
+
throw new Error('Module not loaded yet');
|
|
106
|
+
}
|
|
107
|
+
return this._currentInstance.call(name, ...args);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
close() {
|
|
111
|
+
this._active = false;
|
|
112
|
+
if (this._watcher) {
|
|
113
|
+
this._watcher.close();
|
|
114
|
+
this._watcher = null;
|
|
115
|
+
}
|
|
116
|
+
if (this._pollTimer) {
|
|
117
|
+
clearInterval(this._pollTimer);
|
|
118
|
+
this._pollTimer = null;
|
|
119
|
+
}
|
|
120
|
+
if (this._reloadTimer) {
|
|
121
|
+
clearTimeout(this._reloadTimer);
|
|
122
|
+
this._reloadTimer = null;
|
|
123
|
+
}
|
|
124
|
+
this.removeAllListeners();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { HotReload };
|