@ai-ecoverse/slicc-kernel 1.71.1 → 1.73.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 +21 -6
- package/dist/index.js +42 -3
- package/dist/index.js.map +2 -2
- package/dist/kernel-worker.js +470 -51
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +37 -6
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +432 -48
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +37 -6
- package/dist/process-worker.js.map +2 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -39,11 +39,11 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
|
|
|
39
39
|
| `processMounts` | `true` | whether programs may call `mount(2)` and `umount2(2)`: `false`, or `(req) => boolean \| Promise<boolean>` to decide each call (see [Mounting from a program](#mounting-from-a-program)) |
|
|
40
40
|
| `hostname` | `'slicc'` | the kernel's node name (see [Node name](#node-name)) |
|
|
41
41
|
| `users` | `'off'` | `'off'`, `'report'` or `'enforce'`: whether files have owners and their modes are checked (see [File owners](#file-owners) and [Permissions](#permissions)). `'off'` behaves exactly as kernels before owners did; any other value rejects. `createNodeKernel` takes it too |
|
|
42
|
-
| `onDenial` | none | `({ pid, uid, op, path, errno }) => void`: called for every permission check that fails, under `'report'` and `'enforce'` (see [Permissions](#permissions)). `createNodeKernel` takes it too |
|
|
42
|
+
| `onDenial` | none | `({ pid, uid, op, path, errno, tag? }) => void`: called for every permission check that fails, under `'report'` and `'enforce'` (see [Permissions](#permissions)). `createNodeKernel` takes it too |
|
|
43
43
|
| `sealed` | none | `(req, { signal }) => Promise<object>`: answers requests written to [`/dev/sealed`](#devsealed). `createNodeKernel` takes it too |
|
|
44
44
|
| `approve` | none | `(req, { signal }) => 'once' \| 'always' \| 'deny'` (or a promise of one): decides a `sudo` request that needs approval (see [Sudo](#sudo)); without it, such a request is denied. `createNodeKernel` takes it too |
|
|
45
45
|
| `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
|
|
46
|
-
| `protect` | none | `{ paths }`: directories no process may change, root included, in every `users` mode (see [Protected paths](#protected-paths)). `createNodeKernel` takes it too |
|
|
46
|
+
| `protect` | none | `{ paths, writers? }`: directories no process may change, root included, in every `users` mode, except processes with one of the `writers` tags (see [Protected paths](#protected-paths)). `createNodeKernel` takes it too |
|
|
47
47
|
|
|
48
48
|
### `kernel.run(argv, options?) → Promise<{ status, stdout, stderr }>`
|
|
49
49
|
|
|
@@ -131,6 +131,20 @@ Reach a server a program runs on the kernel's loopback (a dev server, `python -m
|
|
|
131
131
|
- **Errors** reject with an `Error` whose `code` is the errno: `ECONNREFUSED` when nothing listens on the port or the port is kernel-only (`9222`, the [CDP facade](#browser-automation-cdp)), `ENETUNREACH` for a host that is not the loopback, `EINVAL` for a port outside 1–65535.
|
|
132
132
|
- **Names**: a page reaches the kernel's loopback as `<port>.kernel.localhost`, which is the loopback and a secure context, so a request no service worker routes lands on port 80 of the real machine instead of a real server on that port; the service worker routes the name to `loopbackFetch` with that port. Programs find the name in `SLICC_PAGE_LOOPBACK` (`kernel.localhost`) and `SLICC_PAGE_LOOPBACK_URL` (`http://{port}.kernel.localhost`), to print or inject the right URL.
|
|
133
133
|
|
|
134
|
+
### `kernel.attest(path)` and `kernel.watch(paths, onChange)`
|
|
135
|
+
|
|
136
|
+
Page-only: neither is on an attached client (`attachKernel`, `connect()`), so no process or client can ask for them. They are for a page that has to trust a root-owned file, such as a policy file that only `sudo` may change.
|
|
137
|
+
|
|
138
|
+
- **`attest(path)`** resolves with exactly `{ path, bytes, uid, gid, mode, ino, mtimeMs, sha256, users, onRootFs, parents }`.
|
|
139
|
+
- `path` is the canonical absolute path. `bytes` is a `Uint8Array` of its own, holding the file's contents. `sha256` is the lowercase hex digest of exactly those bytes. `mode` includes the type bits. `users` is the kernel's `users` mode. `onRootFs` is always `true`. `parents` lists `{ path, uid, gid, mode }` for every directory from `/` down to the file's own.
|
|
140
|
+
- The kernel answers it itself, over the root file system without permission checks, and lstats every component. It reads the file between two lstats and accepts the read only when the inode, the modification time and the size are unchanged and the bytes match the size. Otherwise it reads once more, then fails with `EAGAIN`.
|
|
141
|
+
- It refuses with an `Error` whose `code` is: `EACCES` for a symlink at the path or in any parent, for a path at or under a mount (any mount, one at `/` included; a mount that failed and isn't installed doesn't count), and for a parent that isn't owned by uid 0 or that group or others may write; `EISDIR` for a directory; `EINVAL` for another kind of file or a path that isn't absolute; `ENOTDIR` for a parent that isn't a directory; `ENOENT` for a missing one; and `EFBIG` for a file over 1 MiB.
|
|
142
|
+
- Under `users: 'off'` there are no owners, so the parent check is skipped and the answer shows what the file system shows (uid and gid 1000, as processes see them). A consumer should accept only `users: 'enforce'`.
|
|
143
|
+
- **`watch(paths, onChange)`** resolves with an unsubscribe function. `onChange(path)` is called once per burst of changes, 50 ms after the last one, for each watched file that changed.
|
|
144
|
+
- A change is a write, a truncate, a rename, a removal, or a change of mode, owner or times of the file or of any directory above it. A change to a parent directory's own metadata counts too, and creating or removing a sibling changes it when the change comes from another kernel.
|
|
145
|
+
- It sees changes made through any slicc kernel on the origin, in this tab or another one: the kernels tell each other which files are watched and report writes to those. `watch` resolves once every other live kernel on the origin has the watch (a kernel that doesn't answer within 500 ms is not waited for), so a write made after it resolves is reported. A kernel that ends with watches still on is forgotten (by its Web Lock) at the next reported write or unanswered announcement. This needs the shared metadata (the default): with `metadata: false` a kernel sees only its own changes. It doesn't see writes made with the OPFS API outside a kernel, such as a page script or an installer writing the root directly. After such a write the page should attest again itself (and may also attest on tab focus, as a safety net).
|
|
146
|
+
- The cost when no kernel on the origin watches anything is one empty-set check per write.
|
|
147
|
+
|
|
134
148
|
### `attachKernel(port, options?) → Promise<KernelClient>`
|
|
135
149
|
|
|
136
150
|
Attaches to a kernel over a port from `kernel.connect()`, in any realm: a worker, another tab, or Node. `options.timeoutMs` (10 s by default) bounds the handshake: a port with no kernel behind it rejects with `KernelGoneError`.
|
|
@@ -257,10 +271,11 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
257
271
|
- `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
|
|
258
272
|
- `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
|
|
259
273
|
- `sync.run({ argv, env, cwd, input, stdin, stdout, stderr, maxBuffer, timeout, killSignal })` runs a command to its end and answers `{ pid, status, signal, stdout, stderr, error }`, as Node's `spawnSync` does. It suits `execSync`, `spawnSync` and `execFileSync`. The kernel does the whole run while the program's worker waits; other processes go on.
|
|
260
|
-
- Each stdio is `'pipe'` (the default), `'ignore'`, `'inherit'` (the program's own, a terminal included), or one of the program's descriptors by number, which the child gets as it is (two slots on one descriptor share it, so their writes keep their order). `stderr: 'stdout'` sends stderr to whatever stdout is, the stdout capture included, in the order the child writes. With `'pipe'`, stdin gets `input` (bytes, or a string as UTF-8) and then its end, so `cat` stops; `stdout` and `stderr` come back as bytes and are absent for the other choices. Both are drained while the child runs, so a child that writes more than a pipe holds never stalls. As in Node, the call returns once the child has exited and every captured stream has reached its end, so output a background grandchild writes later is still collected; `timeout` bounds that wait too. Once the outcome is decided (the end of output, an overflow or a timeout), the captured streams are closed, so a writer still left on them gets `EPIPE` (and `SIGPIPE`) instead of running on.
|
|
274
|
+
- Each stdio is `'pipe'` (the default), `'ignore'`, `'inherit'` (the program's own, a terminal included), or one of the program's descriptors by number, which the child gets as it is (two slots on one descriptor share it, so their writes keep their order). `stderr: 'stdout'` sends stderr to whatever stdout is, the stdout capture included, in the order the child writes. With `'pipe'`, stdin gets `input` (bytes, or a string as UTF-8) and then its end, so `cat` stops; `stdout` and `stderr` come back as bytes and are absent for the other choices. Both are drained while the child runs, so a child that writes more than a pipe holds never stalls. As in Node, the call returns once the child has exited and every captured stream has reached its end, so output a background grandchild writes later is still collected; `timeout` bounds that wait too, so, as with Node's `spawnSync`, a run blocks for as long as a background process keeps a captured stream open unless `timeout` is set; pass one for a command that may leave something in the background. Once the outcome is decided (the end of output, an overflow or a timeout), the captured streams are closed, so a writer still left on them gets `EPIPE` (and `SIGPIPE`) instead of running on.
|
|
261
275
|
- `status` is the exit status, or `null` with `signal` set when a signal ended the child. A non-zero status is not an error. A command that cannot start (it does not exist, or is not executable) answers `error: { code: 'ENOENT' }` (or the errno), `status: null` and `pid: 0` instead of throwing.
|
|
262
276
|
- More than `maxBuffer` bytes on `stdout` or `stderr` (1 MiB by default) sends the child `killSignal` (`SIGTERM` by default), keeps the first `maxBuffer` bytes, and answers `error: { code: 'ENOBUFS' }`. The kernel keeps at most 64 MiB of each stream whatever `maxBuffer` says, `Infinity` included, since the buffers live in the kernel worker that all processes share. `timeout` (ms) sends `killSignal` when it runs out and answers `error: { code: 'ETIMEDOUT' }`. A child that ignores `killSignal` is still waited for, as in Node.
|
|
263
277
|
- The child is in the program's process group, so `^C` reaches it. The kernel reaps it, so it leaves no zombie, and its CPU time counts as the program's children's. A signal the program catches does not cut the run short; its handler runs when the call returns. A signal that kills the program kills it as usual;
|
|
278
|
+
- `sync.kill(pid, signal)`, a synchronous `kill`. When the signal reaches the program itself (its own pid, `0` for its process group, or `-pgid` of its own group) and its action is to end the program (no handler, not ignored), the call never returns: the program's writes reach their descriptors and its terminal modes are put back, as for `sync.exit`, then the rest of the group is signalled and the program ends by the signal, so a parent's `wait` and `sync.run` report `signal`, as Node does for `process.kill(process.pid)`. A signal with a handler runs it when the call returns, an ignored one does nothing, and a stop signal stops the program. A signal whose default action dumps core (`SIGQUIT`, `SIGABRT`, `SIGSEGV`, …) ends it the same way; the kernel writes no core and sets no core flag in the wait status. `-1` signals every process the program may signal except itself and pid 1, as on Linux, and returns (`ESRCH` when there was none). Without users, every process runs as root, so `kill -9 -1` from any program ends every other process; with users it reaches only what the sender may signal (its own uid's processes, or all of them as root);
|
|
264
279
|
- `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. A JS program has no threads of its own (`Worker` is withheld), so nothing of it outlives the call. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
|
|
265
280
|
- `spawn({ argv, env, cwd, stdin, stdout, stderr })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. A command that does not exist rejects with `ENOENT`;
|
|
266
281
|
- `wait(pid)` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
|
|
@@ -565,7 +580,7 @@ With `createKernel({ users: 'report' })` or `'enforce'`, files have owners, and
|
|
|
565
580
|
|
|
566
581
|
### Permissions
|
|
567
582
|
|
|
568
|
-
With `users: 'enforce'` the kernel checks every file call that reaches it against the caller's effective uid, effective gid and supplementary groups, as Linux does: the owner bits for the file's owner, else the group bits for a member of its group, else the other bits. Root (effective uid 0) passes every check except executing a file with no `x` bit at all. A failed check is `EACCES`; changing what only the owner may change is `EPERM`. With `users: 'report'` the kernel makes the same checks and refuses nothing: it logs each denial once per user, operation, errno and path, with `console.warn`, up to 10,000 of them, then says that more were suppressed. With both, `onDenial` gets each denial as `{ pid, uid, op, path, errno }` (`pid` 0 for a client's `fs`); the shape is stable. With `users: 'off'` nothing is checked and no call is added.
|
|
583
|
+
With `users: 'enforce'` the kernel checks every file call that reaches it against the caller's effective uid, effective gid and supplementary groups, as Linux does: the owner bits for the file's owner, else the group bits for a member of its group, else the other bits. Root (effective uid 0) passes every check except executing a file with no `x` bit at all. A failed check is `EACCES`; changing what only the owner may change is `EPERM`. With `users: 'report'` the kernel makes the same checks and refuses nothing: it logs each denial once per user, operation, errno and path, with `console.warn`, up to 10,000 of them, then says that more were suppressed. With both, `onDenial` gets each denial as `{ pid, uid, op, path, errno, tag? }` (`pid` 0 for a client's `fs`); `tag` is the denied process's tag (see `kernel.run`), or the client's for its `fs`, and is left out when there is none; the shape is stable. The `'report'` log line ends with `, tag <tag>` then too. With `users: 'off'` nothing is checked and no call is added.
|
|
569
584
|
|
|
570
585
|
- **Paths:** a path needs `x` on every directory it goes through, the targets of symlinks on the way included, resolved as the file system resolves them (a `..` after a symlinked directory goes to the target's parent). `stat`, `lstat`, `readlink` and `chown` need only that; `exists` answers `false` where it's refused. Listing a directory needs `r` and `x` on it.
|
|
571
586
|
- **Creating and removing:** `mkdir`, creating a file and `symlink` need `w` and `x` on the directory; `mkdir -p` needs them on the closest directory that exists, and nothing for one that exists already. Unlinking, `rmdir` and `rename` need `w` and `x` on both directories, and a directory moved to another needs `w` on itself. A recursive remove (`ctx.fs.rm`, a client's `rm`) needs `r`, `w` and `x` on every directory in the tree.
|
|
@@ -617,10 +632,10 @@ A character device (`0666`, major 10, minor 250) through which a program talks t
|
|
|
617
632
|
- `/` itself, which holds all of these.
|
|
618
633
|
Paths in the layout the kernel stamps with root's owner at boot are skipped when protected.
|
|
619
634
|
- **Symlinks as paths:** a path that is itself a symlink protects the directory it names too, by its real path, once the kernel has prepared.
|
|
620
|
-
- **Mounts:** nothing can be mounted at, under or above a path, not even by the embedder (`kernel.mount`, `/etc/fstab`). So a path always lies on the root file system, where that check holds.
|
|
635
|
+
- **Mounts:** nothing can be mounted at, under or above a path, not even by the embedder (`kernel.mount`, `/etc/fstab`), and not by a writer. Above includes `/`: with any path protected, nothing can be mounted on `/`. So a path always lies on the root file system, where that check holds.
|
|
621
636
|
- **Clients:** a client's (`kernel.connect`) file calls, mounts and unmounts see the paths read-only, as its processes do.
|
|
622
637
|
- **Not affected:** the page's own writes to OPFS.
|
|
623
|
-
- **Writers:**
|
|
638
|
+
- **Writers:** `protect: { paths, writers: ['bios-installer'] }` names process tags whose processes may change the paths, such as the embedder's own installs (`kernel.run(['pnpm', 'install'], { tag: 'bios-installer' })`). A tag matches exactly, with no prefix or case folding. A process gets its tag only from the embedder, through `kernel.run`, `kernel.openTerminal` or `kernel.connect({ tag })`, and passes it to its children, forks and `exec`s; a program or a client can't choose one. Writers work on their own view of the root file system, so a file a writer changes is seen by other processes as between two tabs: close-to-open, at their next open after the writer closes it.
|
|
624
639
|
- **Where it is a boundary:** it's one in a browser, where programs' own code can't reach OPFS around the kernel (see [JS programs](#js-programs)). Under the Node entry it isn't, until [#407](https://github.com/ai-ecoverse/slicc-kernel/issues/407), and there `kernel.writeFile` under a path fails too, so write those files into `root` before creating the kernel.
|
|
625
640
|
|
|
626
641
|
### `/proc`
|
package/dist/index.js
CHANGED
|
@@ -288,7 +288,17 @@ async function loopbackFetch(dial, input, options) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
// src/fs/protect.ts
|
|
291
|
-
|
|
291
|
+
function protectInit(protection) {
|
|
292
|
+
const writers = [...writerTags(protection)];
|
|
293
|
+
return { paths: [...protection.paths], ...writers.length > 0 ? { writers } : {} };
|
|
294
|
+
}
|
|
295
|
+
function writerTags(protection) {
|
|
296
|
+
const writers = protection?.writers ?? [];
|
|
297
|
+
if (!Array.isArray(writers) || writers.some((tag) => typeof tag !== "string" || tag === "")) {
|
|
298
|
+
throw new TypeError("protect.writers must be a list of process tags");
|
|
299
|
+
}
|
|
300
|
+
return new Set(writers);
|
|
301
|
+
}
|
|
292
302
|
|
|
293
303
|
// src/kernel/net/loopback-names.ts
|
|
294
304
|
var HOST_LOOPBACK = "host.slicc.internal";
|
|
@@ -1845,7 +1855,7 @@ function initCall(options, transport) {
|
|
|
1845
1855
|
...options.onDenial ? { denials: true } : {},
|
|
1846
1856
|
...options.sealed ? { sealed: true } : {},
|
|
1847
1857
|
...options.approve ? { approve: true } : {},
|
|
1848
|
-
...options.protect ? { protect:
|
|
1858
|
+
...options.protect ? { protect: protectInit(options.protect) } : {},
|
|
1849
1859
|
...options.processMounts !== void 0 ? {
|
|
1850
1860
|
processMounts: typeof options.processMounts === "function" ? "ask" : options.processMounts
|
|
1851
1861
|
} : {}
|
|
@@ -1900,6 +1910,28 @@ async function mountPolicy(options, worker, { id, req }) {
|
|
|
1900
1910
|
}
|
|
1901
1911
|
worker.postMessage({ mountAllowed: { id, allowed } });
|
|
1902
1912
|
}
|
|
1913
|
+
function fileWatches(request, call) {
|
|
1914
|
+
const watching = /* @__PURE__ */ new Map();
|
|
1915
|
+
return {
|
|
1916
|
+
event(data) {
|
|
1917
|
+
if (!("fileChanged" in data)) return false;
|
|
1918
|
+
watching.get(data.fileChanged.watch)?.(data.fileChanged.path);
|
|
1919
|
+
return true;
|
|
1920
|
+
},
|
|
1921
|
+
async watch(paths, onChange) {
|
|
1922
|
+
const { id, done } = request({ op: "watch-files", paths });
|
|
1923
|
+
watching.set(id, onChange);
|
|
1924
|
+
await done.catch((err) => {
|
|
1925
|
+
watching.delete(id);
|
|
1926
|
+
throw err;
|
|
1927
|
+
});
|
|
1928
|
+
return () => {
|
|
1929
|
+
if (!watching.delete(id)) return;
|
|
1930
|
+
void call({ op: "unwatch-files", watch: id }).catch(() => void 0);
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1903
1935
|
function approver(approve, worker) {
|
|
1904
1936
|
const asked = /* @__PURE__ */ new Map();
|
|
1905
1937
|
const handlers = {
|
|
@@ -1940,13 +1972,17 @@ function checkOptions(options) {
|
|
|
1940
1972
|
}
|
|
1941
1973
|
async function createKernel(options = {}) {
|
|
1942
1974
|
checkOptions(options);
|
|
1943
|
-
|
|
1975
|
+
writerTags(options.protect);
|
|
1944
1976
|
const url = options.worker ?? new URL("./kernel-worker.js", import.meta.url);
|
|
1945
1977
|
const worker = new Worker(url, { type: "module", name: "slicc-kernel" });
|
|
1946
1978
|
const pending = /* @__PURE__ */ new Map();
|
|
1947
1979
|
let nextId = 0;
|
|
1948
1980
|
let failure3;
|
|
1949
1981
|
const dials = dialer((req) => call(req));
|
|
1982
|
+
const files = fileWatches(
|
|
1983
|
+
(req) => request(req),
|
|
1984
|
+
(req) => call(req)
|
|
1985
|
+
);
|
|
1950
1986
|
const cdp = cdpBridge(options.cdp);
|
|
1951
1987
|
const sealed = sealedAnswerer(worker, options.sealed);
|
|
1952
1988
|
const approvals = approver(options.approve, worker);
|
|
@@ -1990,6 +2026,7 @@ async function createKernel(options = {}) {
|
|
|
1990
2026
|
insert: async () => void await call({ op: "insert", target, source, handle: await chosen(handle) })
|
|
1991
2027
|
});
|
|
1992
2028
|
const aside = (data) => {
|
|
2029
|
+
if (files.event(data)) return true;
|
|
1993
2030
|
if (!("denial" in data)) return sealed.handle(data);
|
|
1994
2031
|
options.onDenial?.(data.denial);
|
|
1995
2032
|
return true;
|
|
@@ -2087,6 +2124,8 @@ async function createKernel(options = {}) {
|
|
|
2087
2124
|
umount: async (target) => void await call({ op: "umount", target }),
|
|
2088
2125
|
mounts: async () => await call({ op: "mounts" }),
|
|
2089
2126
|
insert: async (target, handle) => void await call({ op: "insert", target, handle }),
|
|
2127
|
+
attest: async (path) => await call({ op: "attest", path }),
|
|
2128
|
+
watch: files.watch,
|
|
2090
2129
|
terminate() {
|
|
2091
2130
|
fail(new Error("slicc-kernel terminated"));
|
|
2092
2131
|
worker.terminate();
|