@amritk/nish-x86_64-linux 0.17.0 → 0.20.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 +43 -25
- package/bin/nish +0 -0
- package/package.json +2 -2
- package/runtime/nish.d.ts +55 -5
- package/runtime/nish.h +231 -20
- package/runtime/nish.mjs +16 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +7 -6
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +213 -15
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +16 -3
- package/runtime/runtime.c +89 -23
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +225 -20
- package/std/README.md +86 -55
- package/std/bytes.ts +163 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- 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 +245 -53
- package/std/crypto/p256.ts +1376 -42
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +9 -9
- package/std/json-tokenizer.ts +1296 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1163 -0
- package/std/json.ts +1412 -70
- 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 +417 -55
- 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 +231 -45
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-conn-params.ts +86 -16
- package/std/net/quic-listener.ts +576 -219
- package/std/net/quic-packet.ts +443 -58
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +434 -28
- package/std/net/quic.ts +922 -206
- 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 +204 -57
- package/std/net/tls.ts +1132 -305
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/text.ts +77 -0
- package/std/threads.ts +438 -14
|
@@ -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())
|
|
@@ -126,6 +126,42 @@ export class QuicTransportParameters {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** Empties `to`, keeping its room. */
|
|
130
|
+
const quicParamsEmpty = (to: u8[]): void => {
|
|
131
|
+
while (to.length > 0) {
|
|
132
|
+
to.pop()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Sets `p` back to what a new `QuicTransportParameters` holds, keeping its arrays, emptied. */
|
|
137
|
+
const quicParamsDefaults = (p: QuicTransportParameters): void => {
|
|
138
|
+
p.error = 0
|
|
139
|
+
p.maxIdleTimeout = 0
|
|
140
|
+
p.maxUdpPayloadSize = 65527
|
|
141
|
+
p.initialMaxData = 0
|
|
142
|
+
p.initialMaxStreamDataBidiLocal = 0
|
|
143
|
+
p.initialMaxStreamDataBidiRemote = 0
|
|
144
|
+
p.initialMaxStreamDataUni = 0
|
|
145
|
+
p.initialMaxStreamsBidi = 0
|
|
146
|
+
p.initialMaxStreamsUni = 0
|
|
147
|
+
p.ackDelayExponent = 3
|
|
148
|
+
p.maxAckDelay = 25
|
|
149
|
+
p.activeConnectionIdLimit = 2
|
|
150
|
+
p.maxDatagramFrameSize = 0
|
|
151
|
+
p.errorParameter = 0
|
|
152
|
+
quicParamsEmpty(p.originalDcid)
|
|
153
|
+
quicParamsEmpty(p.statelessResetToken)
|
|
154
|
+
quicParamsEmpty(p.preferredAddress)
|
|
155
|
+
quicParamsEmpty(p.initialScid)
|
|
156
|
+
quicParamsEmpty(p.retryScid)
|
|
157
|
+
p.hasOriginalDcid = false
|
|
158
|
+
p.hasStatelessResetToken = false
|
|
159
|
+
p.hasPreferredAddress = false
|
|
160
|
+
p.hasInitialScid = false
|
|
161
|
+
p.hasRetryScid = false
|
|
162
|
+
p.disableActiveMigration = false
|
|
163
|
+
}
|
|
164
|
+
|
|
129
165
|
/** Appends one parameter whose value is a varint, when it differs from `fallback`, its default. */
|
|
130
166
|
const quicParamsPushVarint = (out: u8[], id: i32, value: i64, fallback: i64): void => {
|
|
131
167
|
if (value === fallback || quicVarintSize(value) === 0) {
|
|
@@ -158,6 +194,18 @@ const quicParamsPushBytes = (out: u8[], id: i32, bytes: u8[], present: boolean):
|
|
|
158
194
|
*/
|
|
159
195
|
export const quicEncodeTransportParameters = (p: QuicTransportParameters): u8[] => {
|
|
160
196
|
const out: u8[] = []
|
|
197
|
+
quicEncodeTransportParametersInto(p, out)
|
|
198
|
+
return out
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `quicEncodeTransportParameters` into `out`, which is emptied first: the
|
|
203
|
+
* encoding is pushed into the room the array already has, so a connection
|
|
204
|
+
* slot that keeps one array for its parameters grows it only when they
|
|
205
|
+
* outgrow every earlier connection's.
|
|
206
|
+
*/
|
|
207
|
+
export const quicEncodeTransportParametersInto = (p: QuicTransportParameters, out: u8[]): void => {
|
|
208
|
+
quicParamsEmpty(out)
|
|
161
209
|
quicParamsPushBytes(out, QUIC_TP_ORIGINAL_DCID, p.originalDcid, p.hasOriginalDcid)
|
|
162
210
|
quicParamsPushVarint(out, QUIC_TP_MAX_IDLE_TIMEOUT, p.maxIdleTimeout, 0)
|
|
163
211
|
quicParamsPushBytes(out, QUIC_TP_STATELESS_RESET_TOKEN, p.statelessResetToken, p.hasStatelessResetToken)
|
|
@@ -177,19 +225,20 @@ export const quicEncodeTransportParameters = (p: QuicTransportParameters): u8[]
|
|
|
177
225
|
quicParamsPushBytes(out, QUIC_TP_INITIAL_SCID, p.initialScid, p.hasInitialScid)
|
|
178
226
|
quicParamsPushBytes(out, QUIC_TP_RETRY_SCID, p.retryScid, p.hasRetryScid)
|
|
179
227
|
quicParamsPushVarint(out, QUIC_TP_MAX_DATAGRAM_FRAME_SIZE, p.maxDatagramFrameSize, 0)
|
|
180
|
-
return out
|
|
181
228
|
}
|
|
182
229
|
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
230
|
+
/**
|
|
231
|
+
* `bytes[from .. from + length)` into `to`, which is emptied first and
|
|
232
|
+
* refilled in the room it has: a parameters object reused for connection
|
|
233
|
+
* after connection keeps its arrays. The caller has checked the window.
|
|
234
|
+
*/
|
|
235
|
+
const quicParamsCopyInto = (to: u8[], data: u8[], from: i32, length: i32): void => {
|
|
236
|
+
quicParamsEmpty(to)
|
|
237
|
+
for (let k: i32 = 0; k < length; k += 1) {
|
|
188
238
|
if (from + k >= 0 && from + k < toI32(data.length)) {
|
|
189
|
-
|
|
239
|
+
to.push(data[from + k])
|
|
190
240
|
}
|
|
191
241
|
}
|
|
192
|
-
return out
|
|
193
242
|
}
|
|
194
243
|
|
|
195
244
|
/** Fails `p` over parameter `id` and answers it. */
|
|
@@ -293,30 +342,35 @@ const quicParamsStoreBytes = (
|
|
|
293
342
|
at: i32,
|
|
294
343
|
length: i32
|
|
295
344
|
): boolean => {
|
|
345
|
+
// A connection ID or reset token is copied only once its length is one the
|
|
346
|
+
// parameter allows, so a reused object's arrays never grow past it whatever
|
|
347
|
+
// a peer sends. `preferred_address` is server-only and so never read from a
|
|
348
|
+
// client into a server's slot; its length is the server's own.
|
|
349
|
+
const cidFits: boolean = length <= QUIC_MAX_CID_LENGTH
|
|
296
350
|
switch (id) {
|
|
297
351
|
case QUIC_TP_ORIGINAL_DCID:
|
|
298
|
-
p.originalDcid
|
|
352
|
+
quicParamsCopyInto(p.originalDcid, bytes, at, cidFits ? length : 0)
|
|
299
353
|
p.hasOriginalDcid = true
|
|
300
|
-
return
|
|
354
|
+
return cidFits
|
|
301
355
|
case QUIC_TP_STATELESS_RESET_TOKEN:
|
|
302
|
-
p.statelessResetToken
|
|
356
|
+
quicParamsCopyInto(p.statelessResetToken, bytes, at, length === QUIC_RESET_TOKEN_SIZE ? length : 0)
|
|
303
357
|
p.hasStatelessResetToken = true
|
|
304
358
|
return length === QUIC_RESET_TOKEN_SIZE
|
|
305
359
|
case QUIC_TP_DISABLE_ACTIVE_MIGRATION:
|
|
306
360
|
p.disableActiveMigration = true
|
|
307
361
|
return length === 0
|
|
308
362
|
case QUIC_TP_PREFERRED_ADDRESS:
|
|
309
|
-
p.preferredAddress
|
|
363
|
+
quicParamsCopyInto(p.preferredAddress, bytes, at, length)
|
|
310
364
|
p.hasPreferredAddress = true
|
|
311
365
|
return true
|
|
312
366
|
case QUIC_TP_INITIAL_SCID:
|
|
313
|
-
p.initialScid
|
|
367
|
+
quicParamsCopyInto(p.initialScid, bytes, at, cidFits ? length : 0)
|
|
314
368
|
p.hasInitialScid = true
|
|
315
|
-
return
|
|
369
|
+
return cidFits
|
|
316
370
|
default:
|
|
317
|
-
p.retryScid
|
|
371
|
+
quicParamsCopyInto(p.retryScid, bytes, at, cidFits ? length : 0)
|
|
318
372
|
p.hasRetryScid = true
|
|
319
|
-
return
|
|
373
|
+
return cidFits
|
|
320
374
|
}
|
|
321
375
|
}
|
|
322
376
|
|
|
@@ -336,6 +390,22 @@ const quicParamsIsVarint = (id: i32): boolean =>
|
|
|
336
390
|
*/
|
|
337
391
|
export const quicParseTransportParameters = (bytes: u8[], fromServer: boolean): QuicTransportParameters => {
|
|
338
392
|
const p: QuicTransportParameters = new QuicTransportParameters()
|
|
393
|
+
quicParseTransportParametersInto(p, bytes, fromServer)
|
|
394
|
+
return p
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* `quicParseTransportParameters` into `p`, which is set back to every
|
|
399
|
+
* default first, its byte-valued fields emptied and refilled in the room
|
|
400
|
+
* their arrays have: a connection slot parses each client's parameters into
|
|
401
|
+
* one object of its own. Answers `p`.
|
|
402
|
+
*/
|
|
403
|
+
export const quicParseTransportParametersInto = (
|
|
404
|
+
p: QuicTransportParameters,
|
|
405
|
+
bytes: u8[],
|
|
406
|
+
fromServer: boolean
|
|
407
|
+
): QuicTransportParameters => {
|
|
408
|
+
quicParamsDefaults(p)
|
|
339
409
|
const end: i32 = toI32(bytes.length)
|
|
340
410
|
// The known IDs are 0 to 16, so one bit each of an i32 records which were
|
|
341
411
|
// seen; RFC 9221's 0x20 has a flag of its own.
|