@amritk/nish-x86_64-linux 0.18.0 → 0.21.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/INSTALL.md +58 -28
- package/bin/nish +0 -0
- package/docs/AI.md +2759 -0
- package/package.json +3 -2
- package/runtime/browser.d.ts +14 -0
- package/runtime/browser.mjs +88 -0
- package/runtime/core/arrays.d.ts +18 -0
- package/runtime/core/arrays.mjs +125 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +70 -0
- package/runtime/core/host.mjs +311 -0
- package/runtime/core/numbers.d.ts +53 -0
- package/runtime/core/numbers.mjs +316 -0
- package/runtime/core/result.d.ts +41 -0
- package/runtime/core/result.mjs +127 -0
- package/runtime/core/strings.d.ts +14 -0
- package/runtime/core/strings.mjs +169 -0
- package/runtime/core.d.ts +9 -0
- package/runtime/core.mjs +55 -0
- package/runtime/host.d.ts +6 -0
- package/runtime/nish.d.ts +99 -5
- package/runtime/nish.h +289 -19
- package/runtime/nish.mjs +39 -3
- package/runtime/node.mjs +383 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-bytes.c +401 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +8 -7
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +214 -16
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +235 -11
- package/runtime/runtime-term.c +504 -0
- package/runtime/runtime-wasm.c +5 -14
- package/runtime/runtime.c +257 -17
- package/runtime/shim.mjs +6 -915
- package/runtime/sqlite/LICENSE.md +85 -0
- package/runtime/sqlite/README.md +45 -0
- package/runtime/sqlite/nish-sqlite.c +359 -0
- package/runtime/sqlite/sqlite3.c +271131 -0
- package/runtime/sqlite/sqlite3.h +14447 -0
- package/scripts/build.sh +437 -28
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +108 -58
- package/std/base64.ts +116 -0
- package/std/bytes.ts +419 -0
- package/std/cli.ts +328 -0
- package/std/collections.ts +6 -5
- package/std/crypto/LICENSE-crypt_blowfish +26 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/base64url.ts +125 -0
- package/std/crypto/bcrypt.ts +760 -0
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +382 -54
- package/std/crypto/p256.ts +932 -61
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +20 -131
- package/std/json-tokenizer.ts +4237 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1182 -0
- package/std/json.ts +1202 -34
- package/std/math.ts +608 -0
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +501 -62
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +261 -43
- package/std/net/http3.ts +187 -28
- package/std/net/postgres.ts +1875 -0
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-listener.ts +108 -11
- package/std/net/quic-packet.ts +243 -82
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +424 -24
- package/std/net/quic.ts +519 -167
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +196 -53
- package/std/net/tls.ts +1018 -260
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/regex.ts +4274 -0
- package/std/sqlite.ts +391 -0
- package/std/term-io.ts +978 -0
- package/std/term-pixels.ts +874 -0
- package/std/term-probe.ts +459 -0
- package/std/term-rate.ts +993 -0
- package/std/term.ts +2453 -0
- package/std/text.ts +90 -7
- package/std/threads.ts +438 -14
- package/std/yaml-lines.ts +141 -0
- package/std/yaml.ts +4675 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/prefork` — run a server on every core: a supervisor process that
|
|
3
|
+
* starts one worker process per core, restarts one that dies, and stops them
|
|
4
|
+
* all on SIGTERM or SIGINT (`docs/wp42-multicore-server.md` §2.3).
|
|
5
|
+
*
|
|
6
|
+
* import { preforkWorkers } from "nish/net/prefork";
|
|
7
|
+
*
|
|
8
|
+
* export const main = (): number => {
|
|
9
|
+
* const worker: i32 = preforkWorkers(process.argv);
|
|
10
|
+
* if (worker < 0) { return worker === -1 ? 0 : 1; } // the supervisor, done
|
|
11
|
+
* const listener = tcpListenFlags("0.0.0.0", 8080, 1024, 1); // SO_REUSEPORT
|
|
12
|
+
* // ... the single-threaded readiness loop, unchanged ...
|
|
13
|
+
* };
|
|
14
|
+
*
|
|
15
|
+
* **One program, two roles.** `preforkWorkers(process.argv)` answers in two kinds of
|
|
16
|
+
* process. In a worker it answers the worker's index, `0` to `N - 1`, at once,
|
|
17
|
+
* and the program goes on to bind its listener and serve. In the process the
|
|
18
|
+
* user started it supervises, and answers only once every worker has exited:
|
|
19
|
+
* `-1` after a shutdown, `-2` when the workers could not be started or the
|
|
20
|
+
* supervisor could not listen for signals. Each worker binds its own listener
|
|
21
|
+
* with `tcpListenFlags`' flag 1 (`SO_REUSEPORT`), so on Linux the kernel
|
|
22
|
+
* spreads new connections across them and no state is shared:
|
|
23
|
+
* a worker is the single-threaded server it was before, `Arena.used()` flat
|
|
24
|
+
* per worker.
|
|
25
|
+
*
|
|
26
|
+
* **How a worker is told.** The supervisor runs the program again with
|
|
27
|
+
* `--nish-prefork-worker=<index>` appended to its arguments, through `spawn`;
|
|
28
|
+
* on Linux it runs `/proc/self/exe`, so a program started through `PATH` or a
|
|
29
|
+
* relative path finds itself. `preforkArgs(process.argv)` answers the
|
|
30
|
+
* arguments without that flag, for a worker that reads its own; the module
|
|
31
|
+
* takes `process.argv` from its caller because only a program with a `main`
|
|
32
|
+
* has one. The environment is inherited whole, and every descriptor the
|
|
33
|
+
* supervisor holds is close-on-exec, so a worker starts with none of them
|
|
34
|
+
* (`docs/security/runtime.md`, RT-17, RT-19).
|
|
35
|
+
*
|
|
36
|
+
* **How many.** `NISH_WORKERS` when it is a positive whole number, else
|
|
37
|
+
* `PreforkConfig.workers` when that is positive, else
|
|
38
|
+
* `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
|
|
39
|
+
* `SO_REUSEPORT` port without spreading connections across it. The count has
|
|
40
|
+
* no upper bound, so `NISH_WORKERS` is the operator's to set, not a client's.
|
|
41
|
+
*
|
|
42
|
+
* **Restarts.** A worker that exits, by a signal or with any code, is started
|
|
43
|
+
* again at the same index while the supervisor is not stopping. One that dies
|
|
44
|
+
* within `quickExitMs` of starting waits before its restart: `backoffMs` the
|
|
45
|
+
* first time, twice the last wait after that, up to `backoffCapMs`; one that
|
|
46
|
+
* ran longer starts again at once and clears its wait, so a crash loop costs a
|
|
47
|
+
* spawn every `backoffCapMs`, not a core. A restart `spawn` refuses waits on
|
|
48
|
+
* the same backoff and is tried again; only the first start of a worker that
|
|
49
|
+
* fails ends the supervisor, with `PREFORK_SPAWN_FAILED`.
|
|
50
|
+
*
|
|
51
|
+
* **Shutdown.** The supervisor listens through `signalFd()`, made before the
|
|
52
|
+
* first spawn so no signal falls between. On SIGTERM or SIGINT it stops
|
|
53
|
+
* restarting, sends every worker SIGTERM, reaps them for `graceMs`, sends what
|
|
54
|
+
* is left SIGKILL and reaps that. A second signal during the grace period sends
|
|
55
|
+
* SIGKILL at once. The workers get the default dispositions back from `exec`,
|
|
56
|
+
* so a worker that never asked for `signalFd()` ends on the SIGTERM. A
|
|
57
|
+
* supervisor that dies any other way (SIGKILL, the OOM killer, or the default
|
|
58
|
+
* action of SIGHUP or SIGQUIT, which `signalFd()` does not catch) leaves its
|
|
59
|
+
* workers running, reparented to init.
|
|
60
|
+
*
|
|
61
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
62
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
63
|
+
*/
|
|
64
|
+
import { netClose, pollAdd, pollCreate, pollWait } from "nish:net"
|
|
65
|
+
import {
|
|
66
|
+
availableParallelism,
|
|
67
|
+
getenv,
|
|
68
|
+
kill,
|
|
69
|
+
monotonicNanos,
|
|
70
|
+
platform,
|
|
71
|
+
readSignal,
|
|
72
|
+
signalFd,
|
|
73
|
+
spawn,
|
|
74
|
+
waitPid,
|
|
75
|
+
} from "nish:process"
|
|
76
|
+
|
|
77
|
+
/** The argument a worker is started with, its index after the `=`. */
|
|
78
|
+
export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
|
|
79
|
+
|
|
80
|
+
/** What `preforkWith` answers in the supervisor once its workers are gone. */
|
|
81
|
+
export const PREFORK_STOPPED: i32 = -1
|
|
82
|
+
/**
|
|
83
|
+
* What `preforkWith` answers when a worker could not be started, or when the
|
|
84
|
+
* supervisor could not listen for SIGTERM and so starts none.
|
|
85
|
+
*/
|
|
86
|
+
export const PREFORK_SPAWN_FAILED: i32 = -2
|
|
87
|
+
|
|
88
|
+
/** The supervisor's numbers; every one is checked by `preforkWith`. */
|
|
89
|
+
export class PreforkConfig {
|
|
90
|
+
/** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
|
|
91
|
+
workers: i32 = 0
|
|
92
|
+
/** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
|
|
93
|
+
tickMs: i32 = 100
|
|
94
|
+
/** The first wait before restarting a worker that died young. */
|
|
95
|
+
backoffMs: i32 = 100
|
|
96
|
+
/** The longest wait before a restart. */
|
|
97
|
+
backoffCapMs: i32 = 5000
|
|
98
|
+
/** A worker that exits within this many milliseconds of starting died young. */
|
|
99
|
+
quickExitMs: i32 = 1000
|
|
100
|
+
/** How long the workers get to exit after SIGTERM before SIGKILL. */
|
|
101
|
+
graceMs: i32 = 5000
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True when `s` is one or more ASCII digits and nothing else. */
|
|
105
|
+
const preforkDigits = (s: string): boolean => {
|
|
106
|
+
if (s.length === 0 || s.length > 9) {
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
for (let i: i32 = 0; i < toI32(s.length); i++) {
|
|
110
|
+
const c: i32 = toI32(s.charCodeAt(i))
|
|
111
|
+
if (c < 48 || c > 57) {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* How many workers to start: `env` (`NISH_WORKERS`, `null` when unset) when it
|
|
120
|
+
* is a positive whole number, else `requested` when positive, else the
|
|
121
|
+
* platform's default.
|
|
122
|
+
*/
|
|
123
|
+
export const preforkWorkerCount = (requested: i32, env: string | null): i32 => {
|
|
124
|
+
if (env !== null && preforkDigits(env)) {
|
|
125
|
+
const n: i32 = parseInt(env)
|
|
126
|
+
if (n > 0) {
|
|
127
|
+
return n
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (requested > 0) {
|
|
131
|
+
return requested
|
|
132
|
+
}
|
|
133
|
+
if (platform !== "linux") {
|
|
134
|
+
return 1
|
|
135
|
+
}
|
|
136
|
+
const cores: i32 = availableParallelism()
|
|
137
|
+
return cores > 0 ? cores : 1
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The worker index `args` carries in its last element, or -1 when none. */
|
|
141
|
+
export const preforkIndex = (args: string[]): i32 => {
|
|
142
|
+
if (args.length < 2) {
|
|
143
|
+
return -1
|
|
144
|
+
}
|
|
145
|
+
const last: string = args[args.length - 1]
|
|
146
|
+
if (!last.startsWith(PREFORK_WORKER_FLAG)) {
|
|
147
|
+
return -1
|
|
148
|
+
}
|
|
149
|
+
const digits: string = last.substring(PREFORK_WORKER_FLAG.length)
|
|
150
|
+
return preforkDigits(digits) ? parseInt(digits) : -1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `args` (`process.argv`) without the worker flag, the same in every process. */
|
|
154
|
+
export const preforkArgs = (args: string[]): string[] => {
|
|
155
|
+
const keep: i32 = preforkIndex(args) >= 0 ? toI32(args.length) - 1 : toI32(args.length)
|
|
156
|
+
const out: string[] = []
|
|
157
|
+
for (let i: i32 = 0; i < toI32(args.length); i++) {
|
|
158
|
+
if (i < keep) {
|
|
159
|
+
out.push(args[i])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Milliseconds on the monotonic clock. */
|
|
166
|
+
const preforkNowMs = (): i64 => monotonicNanos() / 1000000
|
|
167
|
+
|
|
168
|
+
/** The signals the supervisor stops its workers with. */
|
|
169
|
+
const PREFORK_SIGTERM: i32 = 15
|
|
170
|
+
const PREFORK_SIGKILL: i32 = 9
|
|
171
|
+
|
|
172
|
+
/** One worker's slot in the supervisor. */
|
|
173
|
+
class PreforkSlot {
|
|
174
|
+
/** When the running worker started, or when the waiting one may start. */
|
|
175
|
+
at: i64 = 0
|
|
176
|
+
/** The running worker's pid, 0 while it waits to be started again. */
|
|
177
|
+
pid: i32 = 0
|
|
178
|
+
/** The wait before the last restart, 0 after a worker that ran long. */
|
|
179
|
+
delay: i32 = 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The supervisor's state while it runs. */
|
|
183
|
+
class PreforkSupervisor {
|
|
184
|
+
config: PreforkConfig
|
|
185
|
+
program: string
|
|
186
|
+
args: string[]
|
|
187
|
+
slots: PreforkSlot[]
|
|
188
|
+
running: i32 = 0
|
|
189
|
+
|
|
190
|
+
constructor(config: PreforkConfig, args: string[], count: i32) {
|
|
191
|
+
this.config = config
|
|
192
|
+
this.args = preforkArgs(args)
|
|
193
|
+
const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
|
|
194
|
+
this.program = self !== null ? self : this.args[0]
|
|
195
|
+
this.slots = []
|
|
196
|
+
for (let i: i32 = 0; i < count; i++) {
|
|
197
|
+
this.slots.push(new PreforkSlot())
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Start worker `index`; false when `spawn` refused. */
|
|
202
|
+
start(index: i32): boolean {
|
|
203
|
+
const argv: string[] = [this.program]
|
|
204
|
+
for (let i: i32 = 1; i < toI32(this.args.length); i++) {
|
|
205
|
+
argv.push(this.args[i])
|
|
206
|
+
}
|
|
207
|
+
argv.push(`${PREFORK_WORKER_FLAG}${index}`)
|
|
208
|
+
const pid: i32 = spawn(argv)
|
|
209
|
+
if (pid <= 0) {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
const slot: PreforkSlot = this.slots[index]
|
|
213
|
+
slot.pid = pid
|
|
214
|
+
slot.at = preforkNowMs()
|
|
215
|
+
this.running = this.running + 1
|
|
216
|
+
return true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The slot `pid` ran in, or -1. */
|
|
220
|
+
find(pid: i32): i32 {
|
|
221
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
222
|
+
if (this.slots[i].pid === pid) {
|
|
223
|
+
return i
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return -1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Give `slot` the time it may start again: at once after a worker that ran
|
|
231
|
+
* at least `quickExitMs`, else, or when `failed` (`spawn` refused it), after
|
|
232
|
+
* a wait that starts at `backoffMs` and doubles up to `backoffCapMs`.
|
|
233
|
+
*/
|
|
234
|
+
backoff(slot: PreforkSlot, now: i64, failed: boolean): void {
|
|
235
|
+
if (failed || now - slot.at < toI64(this.config.quickExitMs)) {
|
|
236
|
+
const cap: i32 = this.config.backoffCapMs
|
|
237
|
+
if (slot.delay === 0) {
|
|
238
|
+
slot.delay = this.config.backoffMs
|
|
239
|
+
} else {
|
|
240
|
+
// Compared before doubling, so a cap near the i32 limit cannot overflow.
|
|
241
|
+
slot.delay = slot.delay > cap / 2 ? cap : slot.delay * 2
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
slot.delay = 0
|
|
245
|
+
}
|
|
246
|
+
slot.at = now + toI64(slot.delay)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reap every worker that has exited. Unless `stopping`, each one's slot is
|
|
251
|
+
* given the time it may start again. A child that is not a worker (one the
|
|
252
|
+
* program spawned itself before calling `preforkWith`) is reaped too, and
|
|
253
|
+
* its status is discarded. Answers false when `waitPid` fails, which means
|
|
254
|
+
* no child is left to wait for.
|
|
255
|
+
*/
|
|
256
|
+
reap(stopping: boolean): boolean {
|
|
257
|
+
while (this.running > 0) {
|
|
258
|
+
const done: i32[] = waitPid(-1, 1)
|
|
259
|
+
if (done[0] === -4) {
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
if (done[0] <= 0) {
|
|
263
|
+
return done[0] === 0
|
|
264
|
+
}
|
|
265
|
+
const index: i32 = this.find(done[0])
|
|
266
|
+
if (index < 0) {
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
const slot: PreforkSlot = this.slots[index]
|
|
270
|
+
this.running = this.running - 1
|
|
271
|
+
slot.pid = 0
|
|
272
|
+
if (!stopping) {
|
|
273
|
+
this.backoff(slot, preforkNowMs(), false)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Start every waiting worker whose time has come. When `first`, answer false
|
|
281
|
+
* at the first `spawn` that fails; otherwise a failed start waits on the
|
|
282
|
+
* same backoff as a worker that exited at once, and the answer is true.
|
|
283
|
+
*/
|
|
284
|
+
restart(first: boolean): boolean {
|
|
285
|
+
const now: i64 = preforkNowMs()
|
|
286
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
287
|
+
const slot: PreforkSlot = this.slots[i]
|
|
288
|
+
if (slot.pid === 0 && slot.at <= now && !this.start(i)) {
|
|
289
|
+
if (first) {
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
console.error(`nish/net/prefork: cannot restart worker ${i}`)
|
|
293
|
+
this.backoff(slot, now, true)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return true
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Send `sig` to every running worker. */
|
|
300
|
+
signal(sig: i32): void {
|
|
301
|
+
for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
|
|
302
|
+
if (this.slots[i].pid > 0) {
|
|
303
|
+
kill(this.slots[i].pid, sig)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* SIGTERM, reap until `graceMs` or a second signal, SIGKILL, reap until no
|
|
310
|
+
* worker is left. Two signals that arrive before the supervisor reads the
|
|
311
|
+
* first count as the second here too. Every reap goes through `reap`, so a
|
|
312
|
+
* child that is not a worker is skipped in both phases, and the SIGKILL
|
|
313
|
+
* phase ends only when the last worker is reaped or no child is left.
|
|
314
|
+
*/
|
|
315
|
+
stop(loop: i32, signals: i32, ready: i32[]): void {
|
|
316
|
+
this.signal(PREFORK_SIGTERM)
|
|
317
|
+
const deadline: i64 = preforkNowMs() + toI64(this.config.graceMs)
|
|
318
|
+
this.reap(true)
|
|
319
|
+
let forced = false
|
|
320
|
+
while (this.running > 0 && !forced && preforkNowMs() < deadline) {
|
|
321
|
+
const n: i32 = pollWait(loop, ready, this.config.tickMs)
|
|
322
|
+
if (n > 0) {
|
|
323
|
+
readSignal(signals)
|
|
324
|
+
forced = true
|
|
325
|
+
}
|
|
326
|
+
this.reap(true)
|
|
327
|
+
}
|
|
328
|
+
if (this.running > 0) {
|
|
329
|
+
this.signal(PREFORK_SIGKILL)
|
|
330
|
+
while (this.reap(true) && this.running > 0) {
|
|
331
|
+
if (pollWait(loop, ready, this.config.tickMs) > 0) {
|
|
332
|
+
readSignal(signals)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The first field of `config` the supervisor cannot run with, or "" when none. */
|
|
340
|
+
const preforkInvalid = (config: PreforkConfig): string => {
|
|
341
|
+
if (config.workers < 0) {
|
|
342
|
+
return "workers below 0"
|
|
343
|
+
}
|
|
344
|
+
if (config.tickMs <= 0) {
|
|
345
|
+
return "tickMs not above 0"
|
|
346
|
+
}
|
|
347
|
+
if (config.backoffMs <= 0 || config.backoffCapMs < config.backoffMs) {
|
|
348
|
+
return "backoffMs not above 0 or above backoffCapMs"
|
|
349
|
+
}
|
|
350
|
+
if (config.quickExitMs < 0 || config.graceMs < 0) {
|
|
351
|
+
return "quickExitMs or graceMs below 0"
|
|
352
|
+
}
|
|
353
|
+
return ""
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* In a worker, its index. In the supervisor, run the workers until SIGTERM or
|
|
358
|
+
* SIGINT and answer `PREFORK_STOPPED`, or `PREFORK_SPAWN_FAILED` when a worker's
|
|
359
|
+
* first start failed (those already started are stopped first) or no signal
|
|
360
|
+
* listener could be made (no worker is started). Panics on a config
|
|
361
|
+
* `preforkInvalid` names a field of.
|
|
362
|
+
*/
|
|
363
|
+
export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
|
|
364
|
+
const invalid: string = preforkInvalid(config)
|
|
365
|
+
if (invalid !== "") {
|
|
366
|
+
panic(`nish/net/prefork: ${invalid}`)
|
|
367
|
+
}
|
|
368
|
+
const index: i32 = preforkIndex(args)
|
|
369
|
+
if (index >= 0) {
|
|
370
|
+
return index
|
|
371
|
+
}
|
|
372
|
+
const signals: i32 = signalFd()
|
|
373
|
+
const loop: i32 = pollCreate()
|
|
374
|
+
if (signals < 0 || loop < 0 || pollAdd(loop, signals, 1, 0) < 0) {
|
|
375
|
+
// `signals` is the process's one signal pipe, which every later
|
|
376
|
+
// `signalFd()` answers, so only the loop is closed.
|
|
377
|
+
console.error("nish/net/prefork: cannot listen for SIGTERM")
|
|
378
|
+
if (loop >= 0) {
|
|
379
|
+
netClose(loop)
|
|
380
|
+
}
|
|
381
|
+
return PREFORK_SPAWN_FAILED
|
|
382
|
+
}
|
|
383
|
+
const supervisor = new PreforkSupervisor(
|
|
384
|
+
config,
|
|
385
|
+
args,
|
|
386
|
+
preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
|
|
387
|
+
)
|
|
388
|
+
const ready: i32[] = new Array<i32>(2)
|
|
389
|
+
let result: i32 = PREFORK_STOPPED
|
|
390
|
+
if (!supervisor.restart(true)) {
|
|
391
|
+
console.error(`nish/net/prefork: cannot start ${supervisor.program}`)
|
|
392
|
+
result = PREFORK_SPAWN_FAILED
|
|
393
|
+
} else {
|
|
394
|
+
let running = true
|
|
395
|
+
while (running) {
|
|
396
|
+
const n: i32 = pollWait(loop, ready, config.tickMs)
|
|
397
|
+
if (n > 0) {
|
|
398
|
+
readSignal(signals)
|
|
399
|
+
running = false
|
|
400
|
+
} else {
|
|
401
|
+
supervisor.reap(false)
|
|
402
|
+
supervisor.restart(false)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
supervisor.stop(loop, signals, ready)
|
|
407
|
+
netClose(loop)
|
|
408
|
+
return result
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** `preforkWith` with the default `PreforkConfig`. */
|
|
412
|
+
export const preforkWorkers = (args: string[]): i32 => preforkWith(args, new PreforkConfig())
|
package/std/net/quic-listener.ts
CHANGED
|
@@ -68,9 +68,13 @@
|
|
|
68
68
|
* of in one burst that overruns a queue on the path.
|
|
69
69
|
*
|
|
70
70
|
* let out: u8[] | null = quicListenerTakePaced(conn, now);
|
|
71
|
-
* while (out !== null) { … send it … ; out = quicListenerTakePaced(conn, now); }
|
|
71
|
+
* while (out !== null) { … send it with conn.datagramEcn … ; out = quicListenerTakePaced(conn, now); }
|
|
72
72
|
* … and wake at the earlier of conn.deadline() and quicListenerPaceTime(conn, now)
|
|
73
73
|
*
|
|
74
|
+
* A carrier that sends whole flights with `quicListenerTakeFlight`, below,
|
|
75
|
+
* wakes at `quicListenerFlightTime` instead, as `Http3Server` does: a flight
|
|
76
|
+
* waits for more credit than one datagram, so the pace time can come first.
|
|
77
|
+
*
|
|
74
78
|
* **GSO and GRO** (`nish:net`'s `UDP_SEGMENT` and `UDP_GRO`). A carrier that
|
|
75
79
|
* sends with segmentation offload takes a whole flight at once:
|
|
76
80
|
* `quicListenerTakeFlight` writes as many datagrams as the pacer has credit
|
|
@@ -79,13 +83,23 @@
|
|
|
79
83
|
* cuts back into datagrams; each is written in place, so a flight allocates
|
|
80
84
|
* nothing. The flight is paced as a whole: it is as long as the pacer's
|
|
81
85
|
* credit, and charged to it datagram by datagram, so a burst is never more
|
|
82
|
-
* than the initial window however the carrier sends it.
|
|
86
|
+
* than the initial window however the carrier sends it. It starts only on a
|
|
87
|
+
* millisecond's earning, up to four datagrams, so a fast connection's pacer,
|
|
88
|
+
* which earns by the microsecond, does not let it out a datagram per send;
|
|
89
|
+
* `quicListenerFlightTime` says when. On the receiving
|
|
83
90
|
* side, `quicListenerReceiveSegments` hands a connection the datagrams of
|
|
84
91
|
* one GRO receive, segment by segment, in place.
|
|
85
92
|
*
|
|
93
|
+
* **ECN** (RFC 9000 §13.4). A carrier whose platform sends the ECN bits it
|
|
94
|
+
* is given sets the connection's `ecnMarking` and sends each datagram with
|
|
95
|
+
* the codepoint the connection marked it with: `conn.datagramEcn` after a
|
|
96
|
+
* `takeDatagramInto` or `quicListenerTakePaced`, `flight.ecn` for a flight.
|
|
97
|
+
* One send carries one codepoint, so a flight ends before a datagram the
|
|
98
|
+
* connection would mark otherwise. The listener's own answers go unmarked.
|
|
99
|
+
*
|
|
86
100
|
* const flight = new QuicFlight();
|
|
87
101
|
* const n: i32 = quicListenerTakeFlight(conn, now, buf, 0, toI32(buf.length), flight);
|
|
88
|
-
* if (n > 0) { udpSendTo(fd, buf, 0, n, peer, flight.count > 1 ? flight.segment : 0,
|
|
102
|
+
* if (n > 0) { udpSendTo(fd, buf, 0, n, peer, flight.count > 1 ? flight.segment : 0, flight.ecn); }
|
|
89
103
|
*
|
|
90
104
|
* **Sans-IO and deterministic.** The caller gives the time, the client's
|
|
91
105
|
* address and, once, `QUIC_LISTENER_ENTROPY_SIZE` random bytes: every
|
|
@@ -95,9 +109,10 @@
|
|
|
95
109
|
* byte for byte.
|
|
96
110
|
*
|
|
97
111
|
* **Secrets.** The seed lives in the listener and the static keys in the
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* `
|
|
112
|
+
* `QuicServerConfig`, for the server's lifetime, as plain bytes, since a
|
|
113
|
+
* `Secret` may not be a field (NL2430). `release()` wipes the listener's own,
|
|
114
|
+
* and `Http3Server.stop` the configuration's with them; QUIC-5 in
|
|
115
|
+
* `docs/security/quic.md` records what is left.
|
|
101
116
|
*
|
|
102
117
|
* Written from RFC 9000, in this module's own structure; nothing here is
|
|
103
118
|
* ported from another implementation. Private names carry the
|
|
@@ -212,12 +227,38 @@ export class QuicListenerAnswer {
|
|
|
212
227
|
retryScid: u8[]
|
|
213
228
|
kind: i32 = 0
|
|
214
229
|
retried: boolean = false
|
|
230
|
+
room: i32 = 0
|
|
215
231
|
|
|
216
232
|
constructor(room: i32) {
|
|
217
|
-
this.
|
|
233
|
+
this.room = room > 0 ? room : 0
|
|
234
|
+
this.reply = quicListenerRoom(this.room)
|
|
218
235
|
this.originalDcid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
|
|
219
236
|
this.retryScid = quicListenerRoom(QUIC_MAX_CID_LENGTH)
|
|
220
237
|
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Zeroes the last answer: `reply` across its whole room and each ID across
|
|
241
|
+
* its 20 bytes, so a Retry's token, a stateless reset's token and the IDs
|
|
242
|
+
* leave no copy beyond the arrays' lengths either. Each is left that long
|
|
243
|
+
* and all zero, `kind` a drop. An answer whose reply grew past its room
|
|
244
|
+
* keeps what lies beyond the longest length it is wiped at; one made with
|
|
245
|
+
* `QUIC_LISTENER_ANSWER_SIZE` never grows.
|
|
246
|
+
*/
|
|
247
|
+
wipe(): void {
|
|
248
|
+
quicListenerWipeRoom(this.reply, this.room)
|
|
249
|
+
quicListenerWipeRoom(this.originalDcid, QUIC_MAX_CID_LENGTH)
|
|
250
|
+
quicListenerWipeRoom(this.retryScid, QUIC_MAX_CID_LENGTH)
|
|
251
|
+
this.kind = QUIC_LISTEN_DROP
|
|
252
|
+
this.retried = false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Grows `bytes` to at least `room` in the room it already has and zeroes all of it. */
|
|
257
|
+
const quicListenerWipeRoom = (bytes: u8[], room: i32): void => {
|
|
258
|
+
if (toI32(bytes.length) < room) {
|
|
259
|
+
quicListenerResize(bytes, room)
|
|
260
|
+
}
|
|
261
|
+
secureZero(bytes)
|
|
221
262
|
}
|
|
222
263
|
|
|
223
264
|
/** An empty array with room for `size` bytes, so pushing up to that many allocates nothing. */
|
|
@@ -365,6 +406,30 @@ export class QuicListener {
|
|
|
365
406
|
secureZero(entropy)
|
|
366
407
|
}
|
|
367
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Ends the listener: wipes the seed and the generator's unused bytes, the
|
|
411
|
+
* tag and the HMAC and HKDF scratch, the INVALID_TOKEN close's Initial
|
|
412
|
+
* secrets and key slots, and `handle`'s spare answer, which holds the last
|
|
413
|
+
* one; `handle` and `handleWindow` then drop every datagram they are
|
|
414
|
+
* handed. The other methods still run, under the zeroed seed, and are the
|
|
415
|
+
* caller's not to call. What `handle` returned is the caller's to wipe. The
|
|
416
|
+
* configuration's keys are not the listener's to wipe: `Http3Server.stop`
|
|
417
|
+
* wipes them with the rest of the server.
|
|
418
|
+
*/
|
|
419
|
+
release(): void {
|
|
420
|
+
this.usable = false
|
|
421
|
+
secureZero(this.seed)
|
|
422
|
+
secureZero(this.pool)
|
|
423
|
+
this.poolHead = QUIC_LISTENER_POOL
|
|
424
|
+
secureZero(this.tag)
|
|
425
|
+
this.kdf.wipe()
|
|
426
|
+
secureZero(this.initialClient)
|
|
427
|
+
secureZero(this.initialServer)
|
|
428
|
+
this.readSlot.wipe()
|
|
429
|
+
this.writeSlot.wipe()
|
|
430
|
+
this.spare.wipe()
|
|
431
|
+
}
|
|
432
|
+
|
|
368
433
|
/**
|
|
369
434
|
* One unpredictable value in 0 to 255: the next byte of HMAC-SHA256 under
|
|
370
435
|
* the seed of a counter, a block at a time, each byte wiped once taken.
|
|
@@ -843,6 +908,24 @@ export const quicListenerTakePaced = (conn: QuicConnection, now: i64): u8[] | nu
|
|
|
843
908
|
export const quicListenerPaceTime = (conn: QuicConnection, now: i64): i64 =>
|
|
844
909
|
now + conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE)
|
|
845
910
|
|
|
911
|
+
/** The most credit a paced flight waits for before it starts: four datagrams. */
|
|
912
|
+
export const QUIC_LISTENER_FLIGHT_QUANTUM: i32 = 4800
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* The credit `quicListenerTakeFlight` waits for before `conn` starts a
|
|
916
|
+
* flight: what its pacer earns in a millisecond, from one datagram up to
|
|
917
|
+
* `QUIC_LISTENER_FLIGHT_QUANTUM`.
|
|
918
|
+
*/
|
|
919
|
+
const quicListenerQuantum = (conn: QuicConnection): i32 =>
|
|
920
|
+
conn.recovery.pacerQuantum(QUIC_CONN_DATAGRAM_SIZE, QUIC_LISTENER_FLIGHT_QUANTUM)
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* When `quicListenerTakeFlight` next lets `conn` start a flight, in the
|
|
924
|
+
* caller's milliseconds: `now` when it may already.
|
|
925
|
+
*/
|
|
926
|
+
export const quicListenerFlightTime = (conn: QuicConnection, now: i64): i64 =>
|
|
927
|
+
now + conn.recovery.pacerDelay(now, quicListenerQuantum(conn))
|
|
928
|
+
|
|
846
929
|
/** The most datagrams one GSO send carries: Linux's `UDP_MAX_SEGMENTS`. */
|
|
847
930
|
export const QUIC_LISTENER_FLIGHT_MAX: i32 = 64
|
|
848
931
|
|
|
@@ -852,19 +935,23 @@ export class QuicFlight {
|
|
|
852
935
|
count: i32 = 0
|
|
853
936
|
/** Every datagram's size but the last's, which may be shorter: the `UDP_SEGMENT` to send with. */
|
|
854
937
|
segment: i32 = 0
|
|
938
|
+
/** The ECN codepoint every datagram of the flight was marked with: the one to send with. */
|
|
939
|
+
ecn: i32 = 0
|
|
855
940
|
}
|
|
856
941
|
|
|
857
942
|
/**
|
|
858
943
|
* Writes the next flight `conn` sends at `now` into `buf[at .. at + cap)`,
|
|
859
944
|
* datagram after datagram, for one GSO send, and answers its length in
|
|
860
945
|
* bytes (0 for none); `flight` says how many datagrams it holds and the
|
|
861
|
-
* segment size.
|
|
862
|
-
*
|
|
946
|
+
* segment size. A flight starts only once the pacer has the credit
|
|
947
|
+
* `quicListenerFlightTime` waits for, a millisecond's earning of one to four
|
|
948
|
+
* datagrams; then each datagram goes only when the pacer has credit for a
|
|
949
|
+
* full one (RFC 9002 §7.7) and is charged at its real size, as
|
|
863
950
|
* `quicListenerTakePaced` does, so the flight is the pacer's whole credit
|
|
864
951
|
* and no more. It stops at the first datagram shorter than
|
|
865
952
|
* `QUIC_CONN_DATAGRAM_SIZE`, which can only be the last of a segmented send,
|
|
866
|
-
* at `QUIC_LISTENER_FLIGHT_MAX` datagrams,
|
|
867
|
-
* another.
|
|
953
|
+
* at `QUIC_LISTENER_FLIGHT_MAX` datagrams, when `cap` has no room for
|
|
954
|
+
* another, and before a datagram whose ECN codepoint would not be the first's.
|
|
868
955
|
*/
|
|
869
956
|
export const quicListenerTakeFlight = (
|
|
870
957
|
conn: QuicConnection,
|
|
@@ -876,12 +963,21 @@ export const quicListenerTakeFlight = (
|
|
|
876
963
|
): i32 => {
|
|
877
964
|
flight.count = 0
|
|
878
965
|
flight.segment = 0
|
|
966
|
+
flight.ecn = 0
|
|
879
967
|
let length: i32 = 0
|
|
968
|
+
if (conn.recovery.pacerDelay(now, quicListenerQuantum(conn)) > 0) {
|
|
969
|
+
return length
|
|
970
|
+
}
|
|
880
971
|
while (
|
|
881
972
|
flight.count < QUIC_LISTENER_FLIGHT_MAX &&
|
|
882
973
|
length + QUIC_CONN_DATAGRAM_SIZE <= cap &&
|
|
883
974
|
conn.recovery.pacerDelay(now, QUIC_CONN_DATAGRAM_SIZE) <= 0
|
|
884
975
|
) {
|
|
976
|
+
// The codepoint `takeDatagramInto` would mark the next datagram with: one send carries one.
|
|
977
|
+
const next: i32 = conn.ecnMarking ? conn.recovery.ecnCodepoint() : 0
|
|
978
|
+
if (flight.count > 0 && next !== flight.ecn) {
|
|
979
|
+
return length
|
|
980
|
+
}
|
|
885
981
|
const n: i32 = conn.takeDatagramInto(buf, at + length, now)
|
|
886
982
|
if (n <= 0) {
|
|
887
983
|
return length
|
|
@@ -889,6 +985,7 @@ export const quicListenerTakeFlight = (
|
|
|
889
985
|
conn.recovery.onPaced(now, n)
|
|
890
986
|
if (flight.count === 0) {
|
|
891
987
|
flight.segment = n
|
|
988
|
+
flight.ecn = conn.datagramEcn
|
|
892
989
|
}
|
|
893
990
|
flight.count = flight.count + 1
|
|
894
991
|
length = length + n
|