@amritk/nish-x86_64-linux 0.27.2 → 0.29.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.
Files changed (54) hide show
  1. package/INSTALL.md +16 -8
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +118 -26
  4. package/package.json +1 -1
  5. package/runtime/core/host.d.ts +2 -1
  6. package/runtime/core/host.mjs +4 -0
  7. package/runtime/core.d.ts +47 -0
  8. package/runtime/core.mjs +67 -0
  9. package/runtime/nish.d.ts +176 -0
  10. package/runtime/nish.h +120 -6
  11. package/runtime/nish.mjs +24 -4
  12. package/runtime/runtime-checksum.c +270 -0
  13. package/runtime/runtime-net.c +386 -5
  14. package/runtime/runtime-os.c +59 -8
  15. package/runtime/runtime-parallel.c +81 -5
  16. package/runtime/runtime-sandbox.c +479 -0
  17. package/runtime/runtime-sha256.c +239 -0
  18. package/runtime/runtime-shm.c +541 -0
  19. package/runtime/runtime.c +827 -791
  20. package/scripts/build.sh +79 -28
  21. package/std/README.md +25 -13
  22. package/std/collections.ts +5 -5
  23. package/std/compress/checksum.ts +88 -75
  24. package/std/compress/deflate.ts +224 -0
  25. package/std/compress/gzip.ts +52 -0
  26. package/std/crypto/bcrypt.ts +816 -280
  27. package/std/crypto/hmac.ts +305 -2
  28. package/std/crypto/jwt.ts +1661 -0
  29. package/std/json-equal.ts +335 -0
  30. package/std/json-format.ts +1070 -0
  31. package/std/json-number.ts +428 -0
  32. package/std/json-pointer.ts +245 -0
  33. package/std/json-typed.ts +34 -0
  34. package/std/json-value.ts +510 -17
  35. package/std/json.ts +35 -6
  36. package/std/net/handler.ts +1348 -0
  37. package/std/net/http-client.ts +97 -16
  38. package/std/net/http-compress.ts +301 -16
  39. package/std/net/http1-client.ts +104 -19
  40. package/std/net/http1-server.ts +127 -24
  41. package/std/net/http1.ts +24 -4
  42. package/std/net/outbound.ts +409 -0
  43. package/std/net/postgres.ts +405 -0
  44. package/std/net/prefork.ts +344 -20
  45. package/std/net/websocket.ts +295 -14
  46. package/std/regex.ts +613 -52
  47. package/std/secret.ts +1 -1
  48. package/std/sync/ring.ts +587 -0
  49. package/std/sync/shm.ts +284 -0
  50. package/std/term-io.ts +35 -8
  51. package/std/term-rate.ts +13 -1
  52. package/std/threads.ts +35 -3
  53. package/std/yaml-json.ts +841 -0
  54. /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
@@ -31,7 +31,25 @@
31
31
  * takes `process.argv` from its caller because only a program with a `main`
32
32
  * has one. The environment is inherited whole, and every descriptor the
33
33
  * supervisor holds is close-on-exec, so a worker starts with none of them
34
- * (`docs/security/runtime.md`, RT-17, RT-19).
34
+ * but the channel ends below (`docs/security/runtime.md`, RT-17, RT-19).
35
+ *
36
+ * **Channels.** With `supervisorChannels` on, each worker is started with one
37
+ * end of a fresh `unixSocketPair` (`channelKind`: stream by default, since a
38
+ * seqpacket pair needs Linux and the supervisor panics on one elsewhere) and
39
+ * the supervisor keeps the other; `preforkSupervise` hands each readable end to
40
+ * a callback. With `serviceWorker` set, every other worker gets a pair to that
41
+ * worker. The ends go through `spawnWithFds`, from descriptor 3 in a fixed
42
+ * order: the supervisor channel, then a worker's service channel, or in the
43
+ * service worker its end to each other worker by index, skipping its own. Where
44
+ * they landed rides in `--nish-prefork-channels=` just before the worker flag,
45
+ * and `preforkSupervisorChannel`, `preforkServiceChannel` and
46
+ * `preforkServiceChannelOf` read it back. A pair cannot be handed to a worker
47
+ * already running, so service channels make the workers one group: when any of
48
+ * them exits, the supervisor sends the rest SIGTERM (SIGKILL after `graceMs`),
49
+ * and once none is left, starts them all again on fresh pairs, after the
50
+ * backoff of the one that exited first. No worker keeps an end whose peer is
51
+ * gone longer than that. With `restart` off nothing is signalled; a worker sees
52
+ * its peer's exit as a hang-up.
35
53
  *
36
54
  * **How many.** `NISH_WORKERS` when it is a positive whole number, else
37
55
  * `PreforkConfig.workers` when that is positive, else
@@ -61,7 +79,16 @@
61
79
  * Private helpers share the importing program's flat symbol namespace
62
80
  * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
63
81
  */
64
- import { netClose, pollAdd, pollCreate, pollWait } from "nish:net"
82
+ import {
83
+ NET_UNIX_SEQPACKET,
84
+ NET_UNIX_STREAM,
85
+ netClose,
86
+ netRead,
87
+ pollAdd,
88
+ pollCreate,
89
+ pollWait,
90
+ unixSocketPair,
91
+ } from "nish:net"
65
92
  import {
66
93
  availableParallelism,
67
94
  getenv,
@@ -71,11 +98,19 @@ import {
71
98
  readSignal,
72
99
  signalFd,
73
100
  spawn,
101
+ spawnWithFds,
74
102
  waitPid,
75
103
  } from "nish:process"
76
104
 
77
105
  /** The argument a worker is started with, its index after the `=`. */
78
106
  export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
107
+ /**
108
+ * The argument just before the worker flag when the worker has channels: the
109
+ * descriptor of its supervisor channel, of its service channel, then in the
110
+ * service worker of its end to each worker by index, comma-separated, `-1`
111
+ * for none.
112
+ */
113
+ export const PREFORK_CHANNELS_FLAG: string = "--nish-prefork-channels="
79
114
 
80
115
  /** What `preforkWith` answers in the supervisor once its workers are gone. */
81
116
  export const PREFORK_STOPPED: i32 = -1
@@ -109,6 +144,16 @@ export class PreforkConfig {
109
144
  * and the supervisor returns once none is left.
110
145
  */
111
146
  restart: boolean = true
147
+ /** Give each worker a socketpair to the supervisor (`preforkSupervise`). */
148
+ supervisorChannels: boolean = false
149
+ /** The worker every other worker gets a socketpair to; -1 for none. */
150
+ serviceWorker: i32 = -1
151
+ /**
152
+ * What every channel is: `NET_UNIX_STREAM` (1), or `NET_UNIX_SEQPACKET` (2),
153
+ * which keeps each write one message but only Linux has. Stream is the
154
+ * default because it works on Darwin too.
155
+ */
156
+ channelKind: i32 = 1
112
157
  }
113
158
 
114
159
  /** True when `s` is one or more ASCII digits and nothing else. */
@@ -160,9 +205,18 @@ export const preforkIndex = (args: string[]): i32 => {
160
205
  return preforkDigits(digits) ? parseInt(digits) : -1
161
206
  }
162
207
 
163
- /** `args` (`process.argv`) without the worker flag, the same in every process. */
208
+ /**
209
+ * `args` (`process.argv`) without the worker flag and the channels flag before
210
+ * it, the same in every process.
211
+ */
164
212
  export const preforkArgs = (args: string[]): string[] => {
165
- const keep: i32 = preforkIndex(args) >= 0 ? toI32(args.length) - 1 : toI32(args.length)
213
+ let keep: i32 = toI32(args.length)
214
+ if (preforkIndex(args) >= 0) {
215
+ keep = keep - 1
216
+ if (keep > 1 && args[keep - 1].startsWith(PREFORK_CHANNELS_FLAG)) {
217
+ keep = keep - 1
218
+ }
219
+ }
166
220
  const out: string[] = []
167
221
  for (let i: i32 = 0; i < toI32(args.length); i++) {
168
222
  if (i < keep) {
@@ -172,6 +226,38 @@ export const preforkArgs = (args: string[]): string[] => {
172
226
  return out
173
227
  }
174
228
 
229
+ /** Field `at` of the channels flag in a worker's `args`, or -1. */
230
+ const preforkChannel = (args: string[], at: i32): i32 => {
231
+ if (preforkIndex(args) < 0 || args.length < 3) {
232
+ return -1
233
+ }
234
+ const flag: string = args[args.length - 2]
235
+ if (!flag.startsWith(PREFORK_CHANNELS_FLAG)) {
236
+ return -1
237
+ }
238
+ let rest: string = flag.substring(PREFORK_CHANNELS_FLAG.length)
239
+ for (let k: i32 = 0; k < at; k++) {
240
+ const comma: i32 = toI32(rest.indexOf(","))
241
+ if (comma < 0) {
242
+ return -1
243
+ }
244
+ rest = rest.substring(comma + 1)
245
+ }
246
+ const comma: i32 = toI32(rest.indexOf(","))
247
+ const field: string = comma < 0 ? rest : rest.substring(0, comma)
248
+ return preforkDigits(field) ? parseInt(field) : -1
249
+ }
250
+
251
+ /** In a worker, its end of its supervisor channel; -1 when it has none. */
252
+ export const preforkSupervisorChannel = (args: string[]): i32 => preforkChannel(args, 0)
253
+
254
+ /** In a worker, its end of its channel to the service worker; -1 in the service worker or when none. */
255
+ export const preforkServiceChannel = (args: string[]): i32 => preforkChannel(args, 1)
256
+
257
+ /** In the service worker, its end of the channel to worker `index`; -1 for itself or elsewhere. */
258
+ export const preforkServiceChannelOf = (args: string[], index: i32): i32 =>
259
+ index < 0 || index > 2147483645 ? -1 : preforkChannel(args, 2 + index)
260
+
175
261
  /** Milliseconds on the monotonic clock. */
176
262
  const preforkNowMs = (): i64 => monotonicNanos() / 1000000
177
263
 
@@ -189,6 +275,8 @@ class PreforkSlot {
189
275
  delay: i32 = 0
190
276
  /** The worker exited with `restart` off, and is never started again. */
191
277
  gone: boolean = false
278
+ /** The supervisor's end of the worker's channel, -1 when it has none. */
279
+ channel: i32 = -1
192
280
  }
193
281
 
194
282
  /** The supervisor's state while it runs. */
@@ -197,31 +285,98 @@ class PreforkSupervisor {
197
285
  program: string
198
286
  args: string[]
199
287
  slots: PreforkSlot[]
288
+ /** While the group starts: each worker's end to the service worker, -1 for none. */
289
+ toService: i32[]
290
+ /** While the group starts: the service worker's end to each worker, -1 for none. */
291
+ fromWorkers: i32[]
292
+ /** When a group still running gets SIGKILL. */
293
+ cascadeAt: i64 = 0
200
294
  running: i32 = 0
295
+ /** The readiness loop each supervisor channel is added to. */
296
+ loop: i32
297
+ /** `config.serviceWorker`: with it at 0 or above, the workers are one group. */
298
+ service: i32
299
+ /** The group is being stopped, to be started again once none of it is left. */
300
+ cascading: boolean = false
201
301
 
202
- constructor(config: PreforkConfig, args: string[], count: i32) {
302
+ constructor(config: PreforkConfig, args: string[], count: i32, loop: i32) {
203
303
  this.config = config
304
+ this.loop = loop
305
+ this.service = config.serviceWorker
306
+ this.toService = []
307
+ this.fromWorkers = []
204
308
  this.args = preforkArgs(args)
205
309
  const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
206
310
  this.program = self !== null ? self : this.args[0]
207
311
  this.slots = []
312
+ // No end yet; typed so an f64 build still pushes an i32.
313
+ const none: i32 = -1
208
314
  for (let i: i32 = 0; i < count; i++) {
209
315
  this.slots.push(new PreforkSlot())
316
+ this.toService.push(none)
317
+ this.fromWorkers.push(none)
210
318
  }
211
319
  }
212
320
 
213
- /** Start worker `index`; false when `spawn` refused. */
321
+ /**
322
+ * Start worker `index` with its channel ends, the service ones from
323
+ * `pairs`; false when a socketpair, the loop or the spawn refused it.
324
+ */
214
325
  start(index: i32): boolean {
326
+ const fds: i32[] = []
327
+ let own: i32 = -1
328
+ if (this.config.supervisorChannels) {
329
+ const pair: i32[] = [-1, -1]
330
+ if (unixSocketPair(this.config.channelKind, pair) !== 0) {
331
+ return false
332
+ }
333
+ // On the loop before the worker starts, so a refusal is a failed start.
334
+ if (pollAdd(this.loop, pair[0], 1, index + 1) < 0) {
335
+ netClose(pair[0])
336
+ netClose(pair[1])
337
+ return false
338
+ }
339
+ own = pair[0]
340
+ fds.push(pair[1])
341
+ }
342
+ // Entry i of `fds` is the child's descriptor 3 + i.
343
+ const fields: string[] = [own >= 0 ? "3" : "-1"]
344
+ if (this.service >= 0 && index !== this.service) {
345
+ fds.push(this.toService[index])
346
+ fields.push(`${2 + fds.length}`)
347
+ } else if (this.service >= 0) {
348
+ fields.push("-1")
349
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
350
+ if (i === index) {
351
+ fields.push("-1")
352
+ } else {
353
+ fds.push(this.fromWorkers[i])
354
+ fields.push(`${2 + fds.length}`)
355
+ }
356
+ }
357
+ }
215
358
  const argv: string[] = [this.program]
216
359
  for (let i: i32 = 1; i < toI32(this.args.length); i++) {
217
360
  argv.push(this.args[i])
218
361
  }
362
+ if (fds.length > 0) {
363
+ argv.push(`${PREFORK_CHANNELS_FLAG}${fields.join(",")}`)
364
+ }
219
365
  argv.push(`${PREFORK_WORKER_FLAG}${index}`)
220
- const pid: i32 = spawn(argv)
366
+ const pid: i32 = fds.length > 0 ? spawnWithFds(argv, fds) : spawn(argv)
367
+ if (own >= 0) {
368
+ netClose(fds[0])
369
+ }
221
370
  if (pid <= 0) {
371
+ if (own >= 0) {
372
+ netClose(own)
373
+ }
222
374
  return false
223
375
  }
224
376
  const slot: PreforkSlot = this.slots[index]
377
+ if (own >= 0) {
378
+ slot.channel = own
379
+ }
225
380
  slot.pid = pid
226
381
  slot.at = preforkNowMs()
227
382
  this.running = this.running + 1
@@ -281,21 +436,132 @@ class PreforkSupervisor {
281
436
  const slot: PreforkSlot = this.slots[index]
282
437
  this.running = this.running - 1
283
438
  slot.pid = 0
439
+ this.closeChannel(slot)
284
440
  if (!this.config.restart) {
285
441
  slot.gone = true
286
442
  } else if (!stopping) {
287
- this.backoff(slot, preforkNowMs(), false)
443
+ const now: i64 = preforkNowMs()
444
+ if (this.cascading) {
445
+ // Stopped with its group, so it waits only on the group.
446
+ slot.at = now
447
+ } else {
448
+ this.backoff(slot, now, false)
449
+ if (this.service >= 0) {
450
+ this.cascade(now)
451
+ }
452
+ }
453
+ }
454
+ }
455
+ return true
456
+ }
457
+
458
+ /** Close the supervisor's end of `slot`'s channel, which takes it off the loop. */
459
+ closeChannel(slot: PreforkSlot): void {
460
+ if (slot.channel >= 0) {
461
+ netClose(slot.channel)
462
+ slot.channel = -1
463
+ }
464
+ }
465
+
466
+ /** Stop the group so it starts again whole: SIGTERM now, SIGKILL after `graceMs`. */
467
+ cascade(now: i64): void {
468
+ this.cascading = true
469
+ this.cascadeAt = now + toI64(this.config.graceMs)
470
+ this.signal(PREFORK_SIGTERM)
471
+ }
472
+
473
+ /** SIGKILL a stopping group that outlived `graceMs`, again each `graceMs`. */
474
+ escalate(): void {
475
+ if (!this.cascading || this.running <= 0) {
476
+ return
477
+ }
478
+ const now: i64 = preforkNowMs()
479
+ if (now >= this.cascadeAt) {
480
+ this.cascadeAt = now + toI64(this.config.graceMs)
481
+ this.signal(PREFORK_SIGKILL)
482
+ }
483
+ }
484
+
485
+ /** A pair from each worker to the service worker; false when one failed. */
486
+ pairs(): boolean {
487
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
488
+ if (i !== this.service) {
489
+ const pair: i32[] = [-1, -1]
490
+ if (unixSocketPair(this.config.channelKind, pair) !== 0) {
491
+ return false
492
+ }
493
+ this.toService[i] = pair[0]
494
+ this.fromWorkers[i] = pair[1]
288
495
  }
289
496
  }
290
497
  return true
291
498
  }
292
499
 
500
+ /** Close every end `pairs` made: the workers hold their copies now. */
501
+ closePairs(): void {
502
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
503
+ if (this.toService[i] >= 0) {
504
+ netClose(this.toService[i])
505
+ this.toService[i] = -1
506
+ }
507
+ if (this.fromWorkers[i] >= 0) {
508
+ netClose(this.fromWorkers[i])
509
+ this.fromWorkers[i] = -1
510
+ }
511
+ }
512
+ }
513
+
514
+ /**
515
+ * With a service worker: once no worker of the group runs and the latest
516
+ * slot's time has come, start them all on fresh pairs. A failure answers
517
+ * false when `first`; otherwise it backs off the slot that failed and stops
518
+ * the workers it started, to be tried again whole.
519
+ */
520
+ restartGroup(first: boolean): boolean {
521
+ if (this.running > 0) {
522
+ return true
523
+ }
524
+ const now: i64 = preforkNowMs()
525
+ let due: i64 = 0
526
+ let waiting = false
527
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
528
+ if (!this.slots[i].gone) {
529
+ waiting = true
530
+ due = this.slots[i].at > due ? this.slots[i].at : due
531
+ }
532
+ }
533
+ if (!waiting || due > now) {
534
+ return true
535
+ }
536
+ this.cascading = false
537
+ let failed: i32 = this.pairs() ? -1 : this.service
538
+ for (let i: i32 = 0; i < toI32(this.slots.length) && failed < 0; i++) {
539
+ if (!this.start(i)) {
540
+ failed = i
541
+ }
542
+ }
543
+ this.closePairs()
544
+ if (failed < 0) {
545
+ return true
546
+ }
547
+ if (first) {
548
+ return false
549
+ }
550
+ console.error(`nish/net/prefork: cannot restart the workers at worker ${failed}`)
551
+ this.backoff(this.slots[failed], now, true)
552
+ this.cascade(now)
553
+ return true
554
+ }
555
+
293
556
  /**
294
557
  * Start every waiting worker whose time has come. When `first`, answer false
295
558
  * at the first `spawn` that fails; otherwise a failed start waits on the
296
559
  * same backoff as a worker that exited at once, and the answer is true.
297
560
  */
298
561
  restart(first: boolean): boolean {
562
+ if (this.service >= 0) {
563
+ return this.restartGroup(first)
564
+ }
299
565
  const now: i64 = preforkNowMs()
300
566
  for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
301
567
  const slot: PreforkSlot = this.slots[i]
@@ -324,9 +590,13 @@ class PreforkSupervisor {
324
590
  * worker is left. Two signals that arrive before the supervisor reads the
325
591
  * first count as the second here too. Every reap goes through `reap`, so a
326
592
  * child that is not a worker is skipped in both phases, and the SIGKILL
327
- * phase ends only when the last worker is reaped or no child is left.
593
+ * phase ends only when the last worker is reaped or no child is left. The
594
+ * supervisor's channel ends are closed first, so only `signals` wakes it.
328
595
  */
329
596
  stop(loop: i32, signals: i32, ready: i32[]): void {
597
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
598
+ this.closeChannel(this.slots[i])
599
+ }
330
600
  this.signal(PREFORK_SIGTERM)
331
601
  const deadline: i64 = preforkNowMs() + toI64(this.config.graceMs)
332
602
  this.reap(true)
@@ -364,9 +634,27 @@ const preforkInvalid = (config: PreforkConfig): string => {
364
634
  if (config.quickExitMs < 0 || config.graceMs < 0) {
365
635
  return "quickExitMs or graceMs below 0"
366
636
  }
637
+ if (config.channelKind !== NET_UNIX_STREAM && config.channelKind !== NET_UNIX_SEQPACKET) {
638
+ return "channelKind not NET_UNIX_STREAM or NET_UNIX_SEQPACKET"
639
+ }
640
+ const channels = config.supervisorChannels || config.serviceWorker >= 0
641
+ if (channels && config.channelKind === NET_UNIX_SEQPACKET && platform !== "linux") {
642
+ return "channelKind NET_UNIX_SEQPACKET: seqpacket channels need Linux"
643
+ }
644
+ if (config.serviceWorker < -1) {
645
+ return "serviceWorker below -1"
646
+ }
367
647
  return ""
368
648
  }
369
649
 
650
+ /** Read and drop what a channel holds: `preforkWith`'s callback. */
651
+ const preforkDiscard = (buf: u8[], _index: i32, fd: i32, _events: i32): void => {
652
+ const size: i32 = toI32(buf.length)
653
+ while (netRead(fd, buf, 0, size) > 0) {
654
+ // Dropped.
655
+ }
656
+ }
657
+
370
658
  /**
371
659
  * In a worker, its index. In the supervisor, run the workers until SIGTERM or
372
660
  * SIGINT and answer `PREFORK_STOPPED`; with `restart` off, also until every
@@ -374,9 +662,32 @@ const preforkInvalid = (config: PreforkConfig): string => {
374
662
  * `PREFORK_SPAWN_FAILED` when a worker's
375
663
  * first start failed (those already started are stopped first) or no signal
376
664
  * listener could be made (no worker is started). Panics on a config
377
- * `preforkInvalid` names a field of.
665
+ * `preforkInvalid` names a field of. Supervisor channels are read and
666
+ * dropped; `preforkSupervise` hands them to the program.
378
667
  */
379
668
  export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
669
+ const buf: u8[] = new Array<u8>(512)
670
+ return preforkSupervise(args, config, buf, preforkDiscard)
671
+ }
672
+
673
+ /**
674
+ * `preforkWith`, calling `onChannel(state, index, fd, events)` in the
675
+ * supervisor whenever worker `index`'s supervisor channel `fd` is readable or
676
+ * hung up: `events` is `pollWait`'s, 1 readable and 4 added on a hang-up or
677
+ * error. The callback reads what is waiting, since an end left readable wakes
678
+ * the loop again at once, and may write back, but never closes `fd`: the
679
+ * supervisor closes it after a hang-up, when the worker is reaped and when it
680
+ * stops, and a restarted worker comes with a new one. Panics, besides, when
681
+ * `serviceWorker` is not below the worker count or the count is above 64 with
682
+ * a service worker, the most `spawnWithFds` hands one child; and, through
683
+ * `preforkInvalid`, on seqpacket channels off Linux.
684
+ */
685
+ export const preforkSupervise = <S>(
686
+ args: string[],
687
+ config: PreforkConfig,
688
+ state: S,
689
+ onChannel: (state: S, index: i32, fd: i32, events: i32) => void
690
+ ): i32 => {
380
691
  const invalid: string = preforkInvalid(config)
381
692
  if (invalid !== "") {
382
693
  panic(`nish/net/prefork: ${invalid}`)
@@ -385,6 +696,10 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
385
696
  if (index >= 0) {
386
697
  return index
387
698
  }
699
+ const count: i32 = preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
700
+ if (config.serviceWorker >= count || (config.serviceWorker >= 0 && count > 64)) {
701
+ panic(`nish/net/prefork: serviceWorker not below ${count} workers, or above 64 workers`)
702
+ }
388
703
  const signals: i32 = signalFd()
389
704
  const loop: i32 = pollCreate()
390
705
  if (signals < 0 || loop < 0 || pollAdd(loop, signals, 1, 0) < 0) {
@@ -396,12 +711,9 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
396
711
  }
397
712
  return PREFORK_SPAWN_FAILED
398
713
  }
399
- const supervisor = new PreforkSupervisor(
400
- config,
401
- args,
402
- preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
403
- )
404
- const ready: i32[] = new Array<i32>(2)
714
+ const supervisor = new PreforkSupervisor(config, args, count, loop)
715
+ // Token 0 is the signal pipe, token i + 1 worker i's channel.
716
+ const ready: i32[] = new Array<i32>(2 * count + 2)
405
717
  let result: i32 = PREFORK_STOPPED
406
718
  if (!supervisor.restart(true)) {
407
719
  console.error(`nish/net/prefork: cannot start ${supervisor.program}`)
@@ -410,15 +722,27 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
410
722
  let running = true
411
723
  while (running) {
412
724
  const n: i32 = pollWait(loop, ready, config.tickMs)
413
- if (n > 0) {
414
- readSignal(signals)
415
- running = false
416
- } else {
725
+ for (let k: i32 = 0; k < n; k++) {
726
+ const token: i32 = ready[2 * k]
727
+ const events: i32 = ready[2 * k + 1]
728
+ if (token === 0) {
729
+ readSignal(signals)
730
+ running = false
731
+ } else if (supervisor.slots[token - 1].channel >= 0) {
732
+ const slot: PreforkSlot = supervisor.slots[token - 1]
733
+ onChannel(state, token - 1, slot.channel, events)
734
+ if ((events & 4) !== 0) {
735
+ supervisor.closeChannel(slot)
736
+ }
737
+ }
738
+ }
739
+ if (running) {
417
740
  supervisor.reap(false)
418
741
  if (!config.restart && supervisor.running === 0) {
419
742
  result = PREFORK_WORKERS_EXITED
420
743
  running = false
421
744
  }
745
+ supervisor.escalate()
422
746
  supervisor.restart(false)
423
747
  }
424
748
  }