@ai-ecoverse/slicc-kernel 1.116.1 → 1.117.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 +19 -1
- package/dist/kernel-worker.js +147 -25
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +24 -8
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +147 -25
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +24 -8
- package/dist/process-worker.js.map +3 -3
- package/dist/types/fs/opfs.d.ts +1 -0
- package/dist/types/kernel/fd-table.d.ts +1 -0
- package/dist/types/kernel/host-ops.d.ts +10 -1
- package/dist/types/kernel/locks.d.ts +11 -0
- package/dist/types/kernel/process.d.ts +2 -0
- package/dist/types/process/wasi/slicc-lock.d.ts +3 -0
- package/dist/types/process/wasi/wasi-fds.d.ts +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -589,6 +589,7 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
|
|
|
589
589
|
- **The node's identity, not its name, picks the FIFO:** an open FIFO keeps working after an `unlink` or a `rename`, and a new `mkfifo` at the same name is another FIFO. `/proc/<pid>/fd/N` names the FIFO's path, also in children that inherit the end.
|
|
590
590
|
- **One kernel per storage root:** the table of open FIFOs is per kernel, so two tabs (two kernels on one origin) opening the same FIFO don't connect, as with record locks ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)).
|
|
591
591
|
- GNU make 4.4's `-jN` uses a FIFO jobserver this way (`--jobserver-auth=fifo:PATH`), so sub-makes join it by path.
|
|
592
|
+
- **Below an open** ([#908](https://github.com/ai-ecoverse/slicc-kernel/issues/908)): a FIFO or socket node is never read or written as a plain file. A JS program's `ctx.fs.readFile`/`writeFile` and the kernel API's `readFile`/`writeFile` (and the positioned reads, writes and truncates under them) fail with `ENXIO` on either, because they read or write a whole file rather than open one. Emscripten and WASI programs' opens of a FIFO go through the kernel's FIFO open as above, and a WASI program's open of a socket node is `ENXIO`, as on Linux.
|
|
592
593
|
|
|
593
594
|
`mmap` of a regular file works on every file system, for Emscripten programs built with `mmap` (whose glue exports `emscripten_builtin_memalign`) as well as for WASIX ones: the kernel reads the mapped range into fresh memory of the process, as Emscripten's MEMFS does. A `MAP_PRIVATE` mapping may be written, and its changes stay in the process. A writable `MAP_SHARED` mapping of such a file is `ENODEV`; a file in the process's own in-memory file system (MEMFS, as under `/dev`) keeps Emscripten's own mapping, which writes back. A read-only `MAP_SHARED` mapping is a copy taken at `mmap`, so later changes to the file do not show in it.
|
|
594
595
|
|
|
@@ -617,6 +618,13 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
|
|
|
617
618
|
- **F_GETLK:** with a conflicting lock, it writes that lock back (`l_whence` 0, its absolute start, its length, the holder's pid). Without one, it sets only `l_type` to `2`.
|
|
618
619
|
- **Errors (WASI numbers):** `EAGAIN` for a conflict on `F_SETLK` (never `EACCES`), `EINTR` when a signal with a handler ends an `F_SETLKW` wait, `EDEADLK` for a wait that would close a cycle, `EBADF` for a bad descriptor or a lock type the descriptor's access mode doesn't allow, and `EINVAL` for a bad `l_type` or `l_whence` or a start before 0.
|
|
619
620
|
- **Semantics:** the same as Emscripten's, above. A libc that finds no import on an older kernel gets `ENOSYS` and keeps its local answer.
|
|
621
|
+
- **Any kind of descriptor** ([#893](https://github.com/ai-ecoverse/slicc-kernel/issues/893)): a pipe, a socket or a terminal takes record locks too, as on Linux, keyed by the object (both ends of a pipe are one object, each socket is its own, a terminal is one object however often it's opened). `l_whence` `CUR` and `END` count from 0 there.
|
|
622
|
+
- **`slicc.flock(fd, op) -> errno`** ([#893](https://github.com/ai-ecoverse/slicc-kernel/issues/893)): BSD `flock(2)` locks, with Linux's semantics, for a WASIX program on any descriptor: a regular file, a directory, a pipe, a socket, a terminal or a `/dev` device.
|
|
623
|
+
- **op:** Linux's bits, `LOCK_SH` `1`, `LOCK_EX` `2` or `LOCK_UN` `8`, optionally with `LOCK_NB` `4`. Anything else is `EINVAL`.
|
|
624
|
+
- **Owner:** the open file description, not the process. A `dup`'d descriptor and one a forked child or a spawned program inherits share it, so the child's `LOCK_EX|LOCK_NB` is granted and its `LOCK_UN` releases the lock for the parent too. So children that `flock(STDOUT)` an inherited STDOUT don't exclude each other, on Linux either: for that, each child opens a lock file itself. A second open of the same object is another description and conflicts: the file opened again (by any name of it), `/dev/tty` opened again, the other end of a pipe.
|
|
625
|
+
- **Rules:** shared locks coexist, an exclusive one excludes every other description, and any access mode takes either kind, so `LOCK_EX` on a read-only open excludes (homescoop#758). A conversion drops the old lock before it takes the new one, as on Linux, so a refused `LOCK_NB` conversion leaves no lock. The lock goes with `LOCK_UN` or when the description's last descriptor closes, at exit or kill too. flock locks and `fcntl` record locks are separate and don't block each other.
|
|
626
|
+
- **Errors (WASI numbers):** `EAGAIN` for a conflict with `LOCK_NB`, `EINTR` when a signal with a handler ends a wait (as for `F_SETLKW`: a libc that wants `SA_RESTART` asks again), `EBADF` for a bad descriptor, `EINVAL` for a bad op. There's no deadlock detection, as on Linux.
|
|
627
|
+
- **Limits:** locks are per kernel ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)). A file the program buffers itself is handed to the kernel at its first `flock`, with every other descriptor the program has open on it. A libc that finds no import on an older kernel gets `ENOSYS`; wasix-libc's `flock()` then keeps its old answer. Emscripten programs and the imports module's `tryLock` are unchanged.
|
|
620
628
|
- **Rules on a mount:**
|
|
621
629
|
- a rename across mounts is `EXDEV`, so `mv` copies;
|
|
622
630
|
- a symlink on the root file system that leads onto a mount, such as `~/proj` → `/mnt/drive/proj`, or a chain from a mount to the root and back, is followed onto the mount by every operation, in every runtime: reads, writes, `stat`, listings, `mkdir`, removals, renames, `chmod`, `chown` and times. Removing the link itself removes the link. Before [#547](https://github.com/ai-ecoverse/slicc-kernel/issues/547) was fixed, the kernel resolved such a link against the directory under the mount point for JS and WASI programs, the embedder's `fs` and attached clients (Emscripten programs weren't affected): reads failed with `ENOENT`, and writes went into that hidden directory, which shows again once the mount is gone;
|
|
@@ -969,7 +977,7 @@ Releases are cut by semantic-release on every push to `main`, in two jobs, and o
|
|
|
969
977
|
Every change is certified in its pull request, before it merges (`cert-review-pr.yml`, next section), and `cert-review` is a required check of the merge queue, so every commit on `main`, and every release built from it, was certified first. A release is then **verified on npm** (`release / verify on npm` in `release.yml`, `tools/cert-review/verify.mjs`), in code, for every tag the run made:
|
|
970
978
|
|
|
971
979
|
- It waits up to an hour for npm to serve the version (an accepted publish can take a while to appear), then downloads the tarball and checks `dist.integrity` (sha512), the SLSA v1 provenance (the tarball's sha512 is its subject; `release.yml` on `refs/heads/main`, a GitHub-hosted builder; `gitCommit` = `gitHead` = the tag, on `main`), and `npm audit signatures`. A version that `release-recover.mjs` skipped because npm's `latest` is newer (#837) has nothing to verify.
|
|
972
|
-
- It then writes `certified @ai-ecoverse/slicc-kernel@<v> sha256=<64 hex> (covers <first>–<v>; ci: <run>; pr-certified: <each merged pull request's cert-review>; known: <open known-issue issues>)` into the GitHub release's notes, between `<!-- cert-verify -->` markers. homescoop's ci reads exactly that prefix and re-hashes the npm tarball against it. `covers` starts after the last version npm serves, so a skipped version is covered by the next. Notes that already carry the identical line stay unchanged.
|
|
980
|
+
- It then writes `certified @ai-ecoverse/slicc-kernel@<v> sha256=<64 hex> (covers <first>–<v>; ci: <run>; pr-certified: <each merged pull request's cert-review at its merge>; known: <open known-issue issues>)` into the GitHub release's notes, between `<!-- cert-verify -->` markers. homescoop's ci reads exactly that prefix and re-hashes the npm tarball against it. `covers` starts after the last version npm serves, so a skipped version is covered by the next. Notes that already carry the identical line stay unchanged. A pull request's `cert-review` is the newest finished one (success, failure or error) on its head at or before its merge, or the newest pending one if none had finished. A re-review that a later `merge-ok` re-label starts on the same head, still pending at the merge or ending after it, doesn't change what the merge queue saw, and a status posted after the merge can't change a release's line.
|
|
973
981
|
- A mismatch writes no line. The job is red with a `supply-chain alert`, and so is a line already in the notes with another sha256.
|
|
974
982
|
|
|
975
983
|
The tools in `tools/cert-review/`:
|
|
@@ -1240,6 +1248,16 @@ A WASIX C program for `test/integration/wasix-locks.test.mjs` (#722). It calls `
|
|
|
1240
1248
|
|
|
1241
1249
|
**Build:** `INSTALL_WASIXCC=<homescoop>/scripts/install-wasixcc.sh WASIX_SYSROOT_28=<unpacked @ai-ecoverse/wasix-sysroot 2025.9.30-28> ./build.sh`, with homescoop's pinned wasixcc 0.4.7 (LLVM 21.1.206, binaryen 133) and Homebrew's binaryen `wasm-opt` 132 on `PATH` for the Asyncify pass (`proc_fork`). Two builds give the same bytes: `bin/wlocktest.wasm` is 56,958 bytes, sha256 `31308a54e13e7dcac497ad9d8a05799008c8beaa4ae3a36c76e218af7235ec23`.
|
|
1242
1250
|
|
|
1251
|
+
### `flocktest`
|
|
1252
|
+
|
|
1253
|
+
A WASIX C program for `test/integration/flock.test.mjs` (#893). It calls `slicc.flock` and `slicc.fcntl_lock` directly (`import_module("slicc")`). Its arguments are a sequence of steps, run in order:
|
|
1254
|
+
- `open SLOT r|w|rw PATH`, `pipe SLOT SLOT`, `pair SLOT SLOT` (a `socketpair`), `dup FROM TO`, `fd SLOT N` (names an inherited descriptor), `close SLOT`;
|
|
1255
|
+
- `flock SLOT OP`, where OP is made of `sh`, `ex`, `un` and `nb` (`exnb` is `LOCK_EX|LOCK_NB`) or is a number; `badfd`; `fcntl SLOT r|w|u` (`F_SETLK` on the whole file);
|
|
1256
|
+
- `fork … end` (the child runs the steps up to the matching `end`, its lines start with `child `, and the parent waits for it before it goes on after `end`);
|
|
1257
|
+
- `touch PATH`, `await PATH`, `sleep MS`, `say TEXT`, `trap` (a `SIGUSR1` handler).
|
|
1258
|
+
|
|
1259
|
+
**Build:** as `wlocktest`. Two builds give the same bytes: `bin/flocktest.wasm` is 58,442 bytes, sha256 `687ecf21c836adda4ef2e328b6032eed28ca7d37efc8e88d3aaefb8fc117115a`.
|
|
1260
|
+
|
|
1243
1261
|
## License
|
|
1244
1262
|
|
|
1245
1263
|
Apache-2.0
|
package/dist/kernel-worker.js
CHANGED
|
@@ -1699,13 +1699,20 @@ var OpfsFs = class {
|
|
|
1699
1699
|
if (handle.kind === "directory") throw fsError("EISDIR", path);
|
|
1700
1700
|
return handle;
|
|
1701
1701
|
}
|
|
1702
|
+
async refuseNode(path, empty) {
|
|
1703
|
+
if (!empty) return;
|
|
1704
|
+
const [entry] = await this.meta.get([(await this.locate(path, true)).path]);
|
|
1705
|
+
if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
|
|
1706
|
+
}
|
|
1702
1707
|
async readFileBuffer(path) {
|
|
1703
1708
|
const handle = await this.existing(path);
|
|
1704
|
-
|
|
1709
|
+
const bytes = await this.retrying(
|
|
1705
1710
|
path,
|
|
1706
1711
|
async () => new Uint8Array(await (await handle.getFile()).arrayBuffer()),
|
|
1707
1712
|
false
|
|
1708
1713
|
);
|
|
1714
|
+
await this.refuseNode(path, bytes.length === 0);
|
|
1715
|
+
return bytes;
|
|
1709
1716
|
}
|
|
1710
1717
|
async readVersioned(path, cached = false) {
|
|
1711
1718
|
const handle = await this.existing(path);
|
|
@@ -1715,6 +1722,7 @@ var OpfsFs = class {
|
|
|
1715
1722
|
const stamp = await this.stamp(path, cached);
|
|
1716
1723
|
const file = await handle.getFile();
|
|
1717
1724
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1725
|
+
await this.refuseNode(path, bytes.length === 0);
|
|
1718
1726
|
return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
|
|
1719
1727
|
},
|
|
1720
1728
|
false
|
|
@@ -1736,7 +1744,11 @@ var OpfsFs = class {
|
|
|
1736
1744
|
const handle = await this.existing(path);
|
|
1737
1745
|
return this.retrying(
|
|
1738
1746
|
path,
|
|
1739
|
-
async () =>
|
|
1747
|
+
async () => {
|
|
1748
|
+
const file = await handle.getFile();
|
|
1749
|
+
await this.refuseNode(path, file.size === 0);
|
|
1750
|
+
return new Uint8Array(await file.slice(offset, offset + length2).arrayBuffer());
|
|
1751
|
+
},
|
|
1740
1752
|
false
|
|
1741
1753
|
);
|
|
1742
1754
|
}
|
|
@@ -1752,6 +1764,7 @@ var OpfsFs = class {
|
|
|
1752
1764
|
}
|
|
1753
1765
|
async truncate(path, size) {
|
|
1754
1766
|
const handle = await this.existing(path, true);
|
|
1767
|
+
await this.refuseNode(path, (await handle.getFile()).size === 0);
|
|
1755
1768
|
const resolve2 = this.ranged || this.remote?.watched();
|
|
1756
1769
|
const target = resolve2 || this.counted.length > 0 ? (await this.locate(path, true)).path : path;
|
|
1757
1770
|
await this.counting(target);
|
|
@@ -1804,6 +1817,10 @@ var OpfsFs = class {
|
|
|
1804
1817
|
}
|
|
1805
1818
|
async fileFor(path) {
|
|
1806
1819
|
const [target, existed] = await this.writeTarget(path);
|
|
1820
|
+
if (existed) {
|
|
1821
|
+
const [entry] = await this.meta.get([target]);
|
|
1822
|
+
if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
|
|
1823
|
+
}
|
|
1807
1824
|
const [dir, name2] = await this.parent(target);
|
|
1808
1825
|
try {
|
|
1809
1826
|
const handle = await this.inDir(
|
|
@@ -3780,7 +3797,8 @@ function openPipe(capacity) {
|
|
|
3780
3797
|
},
|
|
3781
3798
|
close: () => pipe.closeRead(),
|
|
3782
3799
|
poll: () => ({ readable: pipe.readReady, writable: false, hangup: pipe.writersGone }),
|
|
3783
|
-
changed: (signal) => pipe.changed(signal)
|
|
3800
|
+
changed: (signal) => pipe.changed(signal),
|
|
3801
|
+
identity: pipe
|
|
3784
3802
|
}),
|
|
3785
3803
|
write: new OpenFile({
|
|
3786
3804
|
write: async (bytes, signal) => {
|
|
@@ -3792,7 +3810,8 @@ function openPipe(capacity) {
|
|
|
3792
3810
|
},
|
|
3793
3811
|
close: () => pipe.closeWrite(),
|
|
3794
3812
|
poll: () => ({ readable: false, writable: pipe.writeReady, hangup: pipe.readersGone }),
|
|
3795
|
-
changed: (signal) => pipe.changed(signal)
|
|
3813
|
+
changed: (signal) => pipe.changed(signal),
|
|
3814
|
+
identity: pipe
|
|
3796
3815
|
})
|
|
3797
3816
|
};
|
|
3798
3817
|
}
|
|
@@ -6241,7 +6260,7 @@ function satisfies(version, range2) {
|
|
|
6241
6260
|
}
|
|
6242
6261
|
|
|
6243
6262
|
// src/version.ts
|
|
6244
|
-
var KERNEL_VERSION = true ? "1.
|
|
6263
|
+
var KERNEL_VERSION = true ? "1.117.0" : "0.0.0-dev";
|
|
6245
6264
|
var OS_RELEASE = "6.0.0-slicc";
|
|
6246
6265
|
function procVersion() {
|
|
6247
6266
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -9607,6 +9626,7 @@ var FifoTable = class {
|
|
|
9607
9626
|
let open = true;
|
|
9608
9627
|
return new OpenFile({
|
|
9609
9628
|
fifo: path,
|
|
9629
|
+
identity: fifo,
|
|
9610
9630
|
...reads ? {
|
|
9611
9631
|
read: async (max, signal) => {
|
|
9612
9632
|
try {
|
|
@@ -13040,7 +13060,71 @@ function lockRange(start2, len) {
|
|
|
13040
13060
|
if (from < 0) throw new KernelError("EINVAL");
|
|
13041
13061
|
return { start: from, end };
|
|
13042
13062
|
}
|
|
13063
|
+
function changedAt(wakers, key, signal) {
|
|
13064
|
+
return new Promise((resolve2, reject) => {
|
|
13065
|
+
const set3 = wakers.get(key) ?? /* @__PURE__ */ new Set();
|
|
13066
|
+
wakers.set(key, set3);
|
|
13067
|
+
const done4 = () => {
|
|
13068
|
+
set3.delete(wake);
|
|
13069
|
+
if (set3.size === 0 && wakers.get(key) === set3) wakers.delete(key);
|
|
13070
|
+
signal.removeEventListener("abort", abort);
|
|
13071
|
+
};
|
|
13072
|
+
const wake = () => {
|
|
13073
|
+
done4();
|
|
13074
|
+
resolve2();
|
|
13075
|
+
};
|
|
13076
|
+
const abort = () => {
|
|
13077
|
+
done4();
|
|
13078
|
+
reject(new KernelError("EINTR"));
|
|
13079
|
+
};
|
|
13080
|
+
set3.add(wake);
|
|
13081
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
13082
|
+
});
|
|
13083
|
+
}
|
|
13084
|
+
function wakeAt(wakers, key) {
|
|
13085
|
+
for (const wake of [...wakers.get(key) ?? []]) wake();
|
|
13086
|
+
}
|
|
13087
|
+
var FlockTable = class {
|
|
13088
|
+
held = /* @__PURE__ */ new Map();
|
|
13089
|
+
keys = /* @__PURE__ */ new WeakMap();
|
|
13090
|
+
wakers = /* @__PURE__ */ new Map();
|
|
13091
|
+
free(key, owner, kind) {
|
|
13092
|
+
for (const [other, has] of this.held.get(key) ?? [])
|
|
13093
|
+
if (other !== owner && (kind === "exclusive" || has === "exclusive")) return false;
|
|
13094
|
+
return true;
|
|
13095
|
+
}
|
|
13096
|
+
set(key, owner, kind) {
|
|
13097
|
+
const has = this.held.get(key)?.get(owner);
|
|
13098
|
+
if (has === kind) return true;
|
|
13099
|
+
if (has !== void 0) this.drop(owner);
|
|
13100
|
+
if (kind === "unlock") return true;
|
|
13101
|
+
if (!this.free(key, owner, kind)) return false;
|
|
13102
|
+
const holders = this.held.get(key) ?? /* @__PURE__ */ new Map();
|
|
13103
|
+
holders.set(owner, kind);
|
|
13104
|
+
this.held.set(key, holders);
|
|
13105
|
+
this.keys.set(owner, key);
|
|
13106
|
+
return true;
|
|
13107
|
+
}
|
|
13108
|
+
async wait(key, owner, kind, blocking) {
|
|
13109
|
+
if (this.set(key, owner, kind)) return;
|
|
13110
|
+
const signal = blocking();
|
|
13111
|
+
while (!this.set(key, owner, kind)) {
|
|
13112
|
+
if (signal.aborted) throw new KernelError("EINTR");
|
|
13113
|
+
await changedAt(this.wakers, key, signal);
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
drop(owner) {
|
|
13117
|
+
const key = this.keys.get(owner);
|
|
13118
|
+
if (key === void 0) return;
|
|
13119
|
+
this.keys.delete(owner);
|
|
13120
|
+
const holders = this.held.get(key);
|
|
13121
|
+
holders?.delete(owner);
|
|
13122
|
+
if (holders?.size === 0) this.held.delete(key);
|
|
13123
|
+
wakeAt(this.wakers, key);
|
|
13124
|
+
}
|
|
13125
|
+
};
|
|
13043
13126
|
var LockTable = class {
|
|
13127
|
+
flocks = new FlockTable();
|
|
13044
13128
|
records = /* @__PURE__ */ new Map();
|
|
13045
13129
|
blocked = /* @__PURE__ */ new Map();
|
|
13046
13130
|
wakers = /* @__PURE__ */ new Map();
|
|
@@ -13098,28 +13182,10 @@ var LockTable = class {
|
|
|
13098
13182
|
return false;
|
|
13099
13183
|
}
|
|
13100
13184
|
changed(key, signal) {
|
|
13101
|
-
return
|
|
13102
|
-
const set3 = this.wakers.get(key) ?? /* @__PURE__ */ new Set();
|
|
13103
|
-
this.wakers.set(key, set3);
|
|
13104
|
-
const done4 = () => {
|
|
13105
|
-
set3.delete(wake);
|
|
13106
|
-
if (set3.size === 0 && this.wakers.get(key) === set3) this.wakers.delete(key);
|
|
13107
|
-
signal.removeEventListener("abort", abort);
|
|
13108
|
-
};
|
|
13109
|
-
const wake = () => {
|
|
13110
|
-
done4();
|
|
13111
|
-
resolve2();
|
|
13112
|
-
};
|
|
13113
|
-
const abort = () => {
|
|
13114
|
-
done4();
|
|
13115
|
-
reject(new KernelError("EINTR"));
|
|
13116
|
-
};
|
|
13117
|
-
set3.add(wake);
|
|
13118
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
13119
|
-
});
|
|
13185
|
+
return changedAt(this.wakers, key, signal);
|
|
13120
13186
|
}
|
|
13121
13187
|
wake(key) {
|
|
13122
|
-
|
|
13188
|
+
wakeAt(this.wakers, key);
|
|
13123
13189
|
}
|
|
13124
13190
|
store(key, locks) {
|
|
13125
13191
|
if (locks.length === 0) this.records.delete(key);
|
|
@@ -13184,6 +13250,7 @@ var HOST_OPS = [
|
|
|
13184
13250
|
"lk-set",
|
|
13185
13251
|
"lk-get",
|
|
13186
13252
|
"lk-close",
|
|
13253
|
+
"flock",
|
|
13187
13254
|
"async-submit",
|
|
13188
13255
|
"async-wait",
|
|
13189
13256
|
"async-take",
|
|
@@ -13336,6 +13403,8 @@ async function hostSyscall(req, ctx) {
|
|
|
13336
13403
|
case "lk-get":
|
|
13337
13404
|
case "lk-close":
|
|
13338
13405
|
return recordLock2(req, ctx);
|
|
13406
|
+
case "flock":
|
|
13407
|
+
return flock(req, ctx);
|
|
13339
13408
|
case "async-submit":
|
|
13340
13409
|
if (!ctx.valid(req.req)) throw new KernelError("EINVAL");
|
|
13341
13410
|
if (req.ack) ctx.ops.ack(req.ack);
|
|
@@ -13358,6 +13427,17 @@ async function hostSyscall(req, ctx) {
|
|
|
13358
13427
|
return done2;
|
|
13359
13428
|
}
|
|
13360
13429
|
}
|
|
13430
|
+
var FLOCK_KINDS = /* @__PURE__ */ new Set(["shared", "exclusive", "unlock"]);
|
|
13431
|
+
async function flock(req, ctx) {
|
|
13432
|
+
const { locks, flockOf } = ctx;
|
|
13433
|
+
if (!locks || !flockOf) throw new KernelError("ENOLCK");
|
|
13434
|
+
if (!FLOCK_KINDS.has(req.kind)) throw new KernelError("EINVAL");
|
|
13435
|
+
const { key, owner } = await flockOf(req.fd);
|
|
13436
|
+
if (req.kind !== "unlock" && req.wait)
|
|
13437
|
+
await locks.flocks.wait(key, owner, req.kind, ctx.blocking);
|
|
13438
|
+
else if (!locks.flocks.set(key, owner, req.kind)) throw new KernelError("EAGAIN");
|
|
13439
|
+
return done2;
|
|
13440
|
+
}
|
|
13361
13441
|
async function recordLock2(req, ctx) {
|
|
13362
13442
|
const { locks, fileKey } = ctx;
|
|
13363
13443
|
if (!locks || !fileKey) throw new KernelError("ENOLCK");
|
|
@@ -17486,6 +17566,18 @@ var fileKeys = /* @__PURE__ */ new WeakMap();
|
|
|
17486
17566
|
function identityKey(st, path) {
|
|
17487
17567
|
return st.ino === void 0 ? path : `${st.dev ?? 0}:${st.ino}`;
|
|
17488
17568
|
}
|
|
17569
|
+
var objectIds = /* @__PURE__ */ new WeakMap();
|
|
17570
|
+
var nextObjectId = 1;
|
|
17571
|
+
function objectKey(file) {
|
|
17572
|
+
const at = file.tty ?? file.identity ?? file.pty ?? file;
|
|
17573
|
+
let id2 = objectIds.get(at);
|
|
17574
|
+
if (id2 === void 0) {
|
|
17575
|
+
id2 = nextObjectId++;
|
|
17576
|
+
objectIds.set(at, id2);
|
|
17577
|
+
}
|
|
17578
|
+
return `object:${id2}`;
|
|
17579
|
+
}
|
|
17580
|
+
var flockHooked = /* @__PURE__ */ new WeakSet();
|
|
17489
17581
|
var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
|
|
17490
17582
|
var HOSTS_ONLY = new Resolver({ routes: new Routes() });
|
|
17491
17583
|
function timeLeft(req, started) {
|
|
@@ -17786,6 +17878,7 @@ var WasmProcess = class {
|
|
|
17786
17878
|
pid: this.pid,
|
|
17787
17879
|
owner: () => this.lockOwner(),
|
|
17788
17880
|
fileKey: (target, kind, setting) => this.lockKey(target, kind, setting),
|
|
17881
|
+
flockOf: (fd) => this.flockOf(fd),
|
|
17789
17882
|
locks: this.options.locks,
|
|
17790
17883
|
ops: this.asyncOps,
|
|
17791
17884
|
blocking: () => this.blockingSignal(cancelled),
|
|
@@ -17854,6 +17947,7 @@ var WasmProcess = class {
|
|
|
17854
17947
|
throw new KernelError("EBADF");
|
|
17855
17948
|
const known = file ? fileKeys.get(file) : this.pathKeys.get(target.path ?? "");
|
|
17856
17949
|
if (kind === "close" && !file && target.path !== void 0) this.pathKeys.delete(target.path);
|
|
17950
|
+
if (access2 && !access2.stat) return objectKey(access2);
|
|
17857
17951
|
const want = kind === "read" ? R_OK : kind === "write" ? W_OK : 0;
|
|
17858
17952
|
const path = await this.lockPath(target, want);
|
|
17859
17953
|
if (known !== void 0) return known;
|
|
@@ -17868,6 +17962,33 @@ var WasmProcess = class {
|
|
|
17868
17962
|
else if (this.pathKeys.size < 4096) this.pathKeys.set(path, key);
|
|
17869
17963
|
return key;
|
|
17870
17964
|
}
|
|
17965
|
+
async flockOf(fd) {
|
|
17966
|
+
const open = this.fds.get(fd);
|
|
17967
|
+
const { locks } = this.options;
|
|
17968
|
+
if (locks && !flockHooked.has(open)) {
|
|
17969
|
+
flockHooked.add(open);
|
|
17970
|
+
open.onClose(() => locks.flocks.drop(open));
|
|
17971
|
+
}
|
|
17972
|
+
return { key: await this.objectKeyOf(open), owner: open };
|
|
17973
|
+
}
|
|
17974
|
+
async objectKeyOf(open) {
|
|
17975
|
+
const { file } = open;
|
|
17976
|
+
const meta = file.heldMeta;
|
|
17977
|
+
if (meta && "device" in meta) return `device:${meta.device}`;
|
|
17978
|
+
if (meta && "dir" in meta) {
|
|
17979
|
+
const st2 = await this.options.fs.stat?.(meta.dir).catch(() => void 0);
|
|
17980
|
+
return st2 ? identityKey(st2, meta.dir) : objectKey(file);
|
|
17981
|
+
}
|
|
17982
|
+
const known = fileKeys.get(open);
|
|
17983
|
+
if (known !== void 0) return known;
|
|
17984
|
+
const vfs = await file.stat?.().catch(() => void 0);
|
|
17985
|
+
if (!vfs || vfs.orphan) return objectKey(file);
|
|
17986
|
+
const st = await this.options.fs.stat?.(vfs.path).catch(() => void 0);
|
|
17987
|
+
if (!st) return objectKey(file);
|
|
17988
|
+
const key = identityKey(st, vfs.path);
|
|
17989
|
+
fileKeys.set(open, key);
|
|
17990
|
+
return key;
|
|
17991
|
+
}
|
|
17871
17992
|
async dropRecordLocks(fd) {
|
|
17872
17993
|
const { locks } = this.options;
|
|
17873
17994
|
const owner = this.options.jobs?.shown(this.pid) ?? this.pid;
|
|
@@ -20148,6 +20269,7 @@ function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
|
|
|
20148
20269
|
},
|
|
20149
20270
|
changed,
|
|
20150
20271
|
...file.tty ? { tty: file.tty } : {},
|
|
20272
|
+
identity: file.identity ?? file,
|
|
20151
20273
|
...file.clipboard ? { clipboard: file.clipboard } : {},
|
|
20152
20274
|
...file.write ? {
|
|
20153
20275
|
write: async (bytes, signal) => {
|