@amritk/nish-x86_64-linux 0.24.0 → 0.26.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 (43) hide show
  1. package/bin/nish +0 -0
  2. package/docs/AI.md +45 -4
  3. package/package.json +1 -1
  4. package/runtime/browser.mjs +4 -3
  5. package/runtime/core/host.d.ts +12 -0
  6. package/runtime/core/host.mjs +74 -2
  7. package/runtime/nish.d.ts +148 -0
  8. package/runtime/nish.h +59 -0
  9. package/runtime/nish.mjs +25 -0
  10. package/runtime/node.mjs +136 -6
  11. package/runtime/runtime-net.c +44 -3
  12. package/runtime/runtime-os.c +351 -18
  13. package/scripts/build.sh +13 -3
  14. package/std/README.md +27 -10
  15. package/std/cli-args.ts +1216 -0
  16. package/std/cli-flags.ts +585 -0
  17. package/std/cli.ts +308 -19
  18. package/std/compress/checksum.ts +132 -20
  19. package/std/compress/deflate.ts +638 -248
  20. package/std/compress/gzip.ts +20 -6
  21. package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
  22. package/std/crypto/p256.ts +63 -1
  23. package/std/crypto/p384.ts +6044 -0
  24. package/std/crypto/pki-roots.ts +322 -0
  25. package/std/crypto/pki.ts +1207 -0
  26. package/std/crypto/rsa.ts +663 -0
  27. package/std/crypto/x509.ts +1054 -58
  28. package/std/net/dns-config.ts +734 -0
  29. package/std/net/dns-message.ts +517 -0
  30. package/std/net/dns-resolver.ts +1015 -0
  31. package/std/net/dns.ts +478 -0
  32. package/std/net/http1-client.ts +1227 -0
  33. package/std/net/http1-server.ts +7 -2
  34. package/std/net/http1.ts +9 -9
  35. package/std/net/postgres.ts +259 -26
  36. package/std/net/tls/client.ts +1094 -0
  37. package/std/net/tls/codec.ts +585 -3
  38. package/std/net/tls/record-client.ts +1192 -0
  39. package/std/net/tls/verify.ts +221 -0
  40. package/std/net/tls-tcp.ts +642 -11
  41. package/std/net/tls.ts +2 -2
  42. package/std/net/url.ts +559 -4
  43. package/std/yaml-write.ts +415 -0
@@ -49,8 +49,10 @@
49
49
  #include <unistd.h>
50
50
  #ifndef __wasi__
51
51
  /* WASI has no processes, so `spawn.h` and `wait.h` do not exist there. */
52
+ #include <poll.h>
52
53
  #include <signal.h>
53
54
  #include <spawn.h>
55
+ #include <sys/resource.h>
54
56
  #include <sys/wait.h>
55
57
  extern char **environ;
56
58
  #endif
@@ -169,6 +171,19 @@ nish_str *nish_read_file(const nish_str *path) {
169
171
  return s;
170
172
  }
171
173
 
174
+ /* All `n` bytes of `p` to `fd`, answering 0 or the negative errno of the
175
+ write that failed (-EIO for one that wrote nothing). A write cut short is
176
+ finished and `EINTR` retried (RT-13). Every writer below shares it. */
177
+ static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
178
+ for (uint64_t done = 0; done < n;) {
179
+ ssize_t k = write(fd, p + done, n - done);
180
+ if (k > 0) done += k;
181
+ else if (k == 0) return -EIO;
182
+ else if (errno != EINTR) return -errno;
183
+ }
184
+ return 0;
185
+ }
186
+
172
187
  /* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
173
188
  component of the path is refused, as an unwritable file is, rather than
174
189
  followed to whatever file it names. `nish file.ts` writes `file.ll` beside
@@ -176,25 +191,145 @@ nish_str *nish_read_file(const nish_str *path) {
176
191
  truncate any file the user can write. A link among the directories on the
177
192
  way is still followed.
178
193
 
179
- `O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
180
- as the read retries it (RT-13): a write cut short is finished, and one that
181
- fails is `cannot write`, never a silently short file. */
194
+ `O_CLOEXEC` for the reason the read has it (RT-12), and the bytes go out
195
+ through `nish_put_all`, so a write that fails is `cannot write`, never a
196
+ silently short file. */
182
197
  static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
183
198
  int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
184
- if (fd < 0) nish_io_fail("write ", path);
185
- for (uint64_t done = 0; done < data->len;) {
186
- ssize_t n = write(fd, data->data + done, data->len - done);
187
- if (n > 0) {
188
- done += n;
189
- } else if (n == 0 || errno != EINTR) {
190
- nish_io_fail("write ", path);
191
- }
192
- }
199
+ if (fd < 0 || nish_put_all(fd, data->data, data->len) < 0) nish_io_fail("write ", path);
193
200
  close(fd);
194
201
  }
195
202
  void nish_write_file(const nish_str *path, const nish_str *data) { nish_put_file(path, data, O_TRUNC); }
196
203
  void nish_append_file(const nish_str *path, const nish_str *data) { nish_put_file(path, data, O_APPEND); }
197
204
 
205
+ /* ---- Replacing and removing files: `renameSync`, `unlinkSync` and
206
+ `writeFileBytesSync`. Unlike the writers above, none exits: each answers 0
207
+ (or the bytes written) on success and a negative errno otherwise, so a
208
+ program that writes a file beside its target and renames it over the
209
+ target owns its own diagnostic. A path holding a NUL answers -EINVAL before
210
+ any system call, rather than reaching the file its prefix names
211
+ (docs/security/runtime.md, RT-3). */
212
+
213
+ /* `renameSync(from, to)`: rename(2). On one file system it replaces `to`
214
+ atomically, so a reader sees the old file or the new one and never a part;
215
+ across two it answers -EXDEV and moves nothing, since a copy and a delete
216
+ would not be atomic. A link at `from` is moved as a link, not followed. */
217
+ int32_t nish_rename(const nish_str *from, const nish_str *to) {
218
+ const char *f = nish_cpath(from), *t = nish_cpath(to);
219
+ if (f != from->data || t != to->data) return -EINVAL;
220
+ return rename(f, t) == 0 ? 0 : -errno;
221
+ }
222
+
223
+ /* `unlinkSync(path)`: unlink(2). A directory is refused with whatever the
224
+ kernel answers (-EISDIR on Linux, -EPERM elsewhere) and is never handed to
225
+ rmdir. A link is removed, not the file it names. */
226
+ int32_t nish_unlink(const nish_str *path) {
227
+ const char *p = nish_cpath(path);
228
+ if (p != path->data) return -EINVAL;
229
+ return unlink(p) == 0 ? 0 : -errno;
230
+ }
231
+
232
+ /* `writeFileBytesSync(path, bytes)`: every element of a `u8[]` to the file,
233
+ opened as `writeFileSync` opens it (`O_NOFOLLOW`, `O_CLOEXEC`, `O_TRUNC`,
234
+ 0644 before the umask) and written through `nish_put_all`, a short write
235
+ finished and `EINTR` retried. It answers the length written or the negative
236
+ errno of the `open` or `write` that failed (-EIO for a write that wrote
237
+ nothing); a failed write leaves the file truncated to what got out. */
238
+ int32_t nish_write_file_bytes(const nish_str *path, const nish_array *bytes) {
239
+ const char *p = nish_cpath(path);
240
+ if (p != path->data) return -EINVAL;
241
+ if (bytes->len > NISH_LENGTH_MAX) return -EFBIG;
242
+ int fd = open(p, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0644);
243
+ if (fd < 0) return -errno;
244
+ int32_t r = nish_put_all(fd, bytes->data, bytes->len);
245
+ /* A write NFS or a quota refuses can surface only at `close`, and the file
246
+ is then short: an answer of the full length would let the caller rename
247
+ it over the one it replaces. */
248
+ if (close(fd) != 0 && r == 0 && errno != EINTR) r = -errno;
249
+ return r < 0 ? r : (int32_t)bytes->len;
250
+ }
251
+
252
+ /* ---- Files and directories with an explicit mode (docs/security/runtime.md,
253
+ RT-24 to RT-26): `writeFileSyncMode`, `createFileExclusiveSync` and
254
+ `mkdirSyncMode`. Each answers a value and never exits, as the writers just
255
+ above do. A mode outside 0..0777 is -EINVAL before any system call: the
256
+ setuid, setgid and sticky bits are refused, not applied, so the mode asked
257
+ for is the mode the file has. WASI
258
+ has no file modes to apply, so there each answers -ENOSYS and creates
259
+ nothing rather than a file whose mode was not set. */
260
+
261
+ /* The two file writers' shared body: `open(p, O_WRONLY | O_CREAT |
262
+ O_NOFOLLOW | O_CLOEXEC | flags, mode)`, then `fchmod(fd, mode)` before a
263
+ byte is written, so the file is exactly `mode` whatever the umask and
264
+ whatever mode a file already there had: no reader that opens it while it is
265
+ written finds it wider (RT-24). Then the data through `nish_put_all` and a
266
+ checked `close`, as `nish_write_file_bytes` does. The open truncates
267
+ before the `fchmod`, so a file there that this process may write but does
268
+ not own is emptied and then answers -EPERM. When `flags` holds `O_EXCL`,
269
+ any failure after the create unlinks the file this call made, so a lock
270
+ whose write failed does not stay taken. */
271
+ static int32_t nish_put_file_mode(const nish_str *path, const nish_str *data, int flags, int32_t mode) {
272
+ const char *p = nish_cpath(path);
273
+ if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
274
+ if (data->len > NISH_LENGTH_MAX) return -EFBIG;
275
+ #ifdef __wasi__
276
+ (void)flags;
277
+ return -ENOSYS;
278
+ #else
279
+ int fd = open(p, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, (mode_t)mode);
280
+ if (fd < 0) return -errno;
281
+ int32_t r = fchmod(fd, (mode_t)mode) == 0 ? 0 : -errno;
282
+ if (r == 0) r = nish_put_all(fd, data->data, data->len);
283
+ if (close(fd) != 0 && r == 0 && errno != EINTR) r = -errno;
284
+ if (r < 0 && (flags & O_EXCL)) unlink(p);
285
+ return r < 0 ? r : (int32_t)data->len;
286
+ #endif
287
+ }
288
+
289
+ /* `writeFileSyncMode(path, data, mode)`: `writeFileSync` with the mode given
290
+ and an answer instead of an exit. A link at the path is refused (-ELOOP on
291
+ Linux) and the file it names is untouched (RT-25). */
292
+ int32_t nish_write_file_mode(const nish_str *path, const nish_str *data, int32_t mode) {
293
+ return nish_put_file_mode(path, data, O_TRUNC, mode);
294
+ }
295
+
296
+ /* `createFileExclusiveSync(path, data, mode)`: `O_EXCL`, so the path is
297
+ created by this call or the call answers -EEXIST, a link there included,
298
+ dangling or not. Of N processes racing for one path exactly one wins, which
299
+ is what a lock file needs; a winner whose write then fails removes the file
300
+ and answers the error. */
301
+ int32_t nish_create_file_exclusive(const nish_str *path, const nish_str *data, int32_t mode) {
302
+ return nish_put_file_mode(path, data, O_EXCL, mode);
303
+ }
304
+
305
+ /* `mkdirSyncMode(path, mode)`: mkdir(2) with `mode`, one directory, not
306
+ recursive. A path already there is -EEXIST and its mode is left as it was.
307
+ The kernel applies the umask, so the directory is `mode & ~umask`: 0700 is
308
+ exact under any umask that leaves the owner's bits (RT-26). */
309
+ int32_t nish_mkdir_mode(const nish_str *path, int32_t mode) {
310
+ const char *p = nish_cpath(path);
311
+ if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
312
+ #ifdef __wasi__
313
+ return -ENOSYS;
314
+ #else
315
+ return mkdir(p, (mode_t)mode) == 0 ? 0 : -errno;
316
+ #endif
317
+ }
318
+
319
+ /* `writeBytes(bytes, off, len)` (nish:io): the bytes `[off, off + len)` of a
320
+ `u8[]` on stdout, through `nish_put_all`, so a short write is finished and `EINTR` retried. The
321
+ emitter has range-checked `off` and `len` against `bytes->len`
322
+ (`emitNetCall`). It answers `len`, every byte out, or the negative errno of
323
+ the write that failed (`-EIO` for one that wrote nothing), and never exits:
324
+ how much of a failed call reached the descriptor is not said. A pipe whose
325
+ reader has gone raises SIGPIPE, as it does for `write`. */
326
+ int32_t nish_write_bytes(const nish_array *bytes, int64_t off, int64_t len) {
327
+ /* An empty array's data may be NULL, and NULL + 0 is undefined. */
328
+ if (len == 0) return 0;
329
+ int32_t r = nish_put_all(1, bytes->data + off, (uint64_t)len);
330
+ return r < 0 ? r : (int32_t)len;
331
+ }
332
+
198
333
  /* ---- Directories and subprocesses (WP14 D4): what a driver needs to create
199
334
  its own output directory and shell out for `--link`. Both answer a value
200
335
  instead of exiting, as `nish_read_file_or_null` does — there are no
@@ -223,17 +358,19 @@ _Bool nish_mkdir(const nish_str *path) {
223
358
  value rather than through a child that has already run.
224
359
 
225
360
  `out` and `err` are paths for the child's stdout and stderr, or NULL to let
226
- it inherit this process's. Every spawn builtin starts its child here, so the
361
+ it inherit this process's; `dup`, when not NULL, is three descriptors the
362
+ child takes as its stdin, stdout and stderr instead (`spawnPiped`'s pipes). Every spawn builtin starts its child here, so the
227
363
  argv vector and the redirects are written once; it is `static`, so a program
228
364
  that spawns nothing loses it to `--gc-sections`. The answer is the child's
229
365
  pid, or a negative errno: -EINVAL for an empty argv or an argument holding a
230
366
  NUL, and `posix_spawnp`'s own error otherwise (-ENOENT for a command that is
231
367
  not on PATH, on glibc). */
232
- static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err) {
368
+ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err, const int *dup) {
233
369
  #ifdef __wasi__
234
370
  (void)argv;
235
371
  (void)out;
236
372
  (void)err;
373
+ (void)dup;
237
374
  return -ENOSYS;
238
375
  #else
239
376
  if (!argv->len) return -EINVAL; /* argv[0] would read past an empty array */
@@ -256,6 +393,13 @@ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, con
256
393
  posix_spawn_file_actions_t fa;
257
394
  int failed = posix_spawn_file_actions_init(&fa);
258
395
  if (failed) return -failed;
396
+ /* A `dup2` action that could not be recorded would start the child on this
397
+ process's own stream in its place, so it fails the spawn. */
398
+ for (int k = 0; dup && k < 3 && !failed; k++) failed = posix_spawn_file_actions_adddup2(&fa, dup[k], k);
399
+ if (failed) {
400
+ posix_spawn_file_actions_destroy(&fa);
401
+ return -failed;
402
+ }
259
403
  const nish_str *to[2] = { out, err };
260
404
  for (int k = 0; k < 2; k++) {
261
405
  /* The child opens the file, not this process: a redirect that this process
@@ -281,7 +425,7 @@ static int32_t nish_exit_code(int status) {
281
425
  /* The two blocking builtins: start, then wait for that one child. A failed
282
426
  start is -1, the answer they have always given. */
283
427
  static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
284
- int32_t pid = nish_spawn_start(argv, out, err);
428
+ int32_t pid = nish_spawn_start(argv, out, err, 0);
285
429
  if (pid < 0) return -1;
286
430
  #ifdef __wasi__
287
431
  return -1;
@@ -312,7 +456,160 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
312
456
  /* `spawn(argv)`: the child's pid, or a negative errno, without waiting. The
313
457
  child is this process's until a `waitPid` reaps it; one never reaped stays
314
458
  a zombie until the program exits, as it would in C. */
315
- int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0); }
459
+ int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0, 0); }
460
+
461
+ /* `spawnPiped(argv, stdin, stdout, stderr)`: run the child as `nish_spawn`
462
+ does, with `stdin`'s bytes on its standard input and its standard output
463
+ and error appended to `stdout` and `stderr`, and answer its status. -1 is
464
+ `nish_spawn`'s failure (an empty argv, a NUL, a child that cannot start or
465
+ be waited for, a pipe or `poll` that fails), and -2 is a stream that passed
466
+ NISH_PIPED_CAP bytes: the child is killed with SIGKILL and reaped, and the
467
+ array holds the first NISH_PIPED_CAP bytes of that stream.
468
+
469
+ The three pipes are close-on-exec, so no other child this process starts
470
+ inherits an end, and each end is moved off descriptors 0 to 2, where a
471
+ `dup2` onto itself would leave close-on-exec set and the child without
472
+ that stream. Darwin has no `pipe2`, so there an end is close-on-exec only
473
+ once `F_DUPFD_CLOEXEC` has moved it: a child another thread starts in
474
+ that window inherits it (docs/security/runtime.md, RT-30).
475
+
476
+ One `poll` loop over non-blocking ends writes stdin and reads both outputs
477
+ as each is ready, so a child that fills its stdout before it reads all its
478
+ stdin, or the reverse, cannot deadlock against this process. A child that
479
+ exits, or closes its stdin, before reading everything ends the writing
480
+ with EPIPE rather than SIGPIPE: the signal is blocked on this thread once
481
+ the child has started, the one a write raised is taken off the pending
482
+ set, and the mask is put back before the answer (docs/security/runtime.md,
483
+ RT-29). Ignoring it instead would be process-wide, and every child
484
+ started meanwhile, this one included, would inherit SIGPIPE ignored and
485
+ never die of a reader that has gone.
486
+
487
+ The bytes are written from the caller's array as they stand, and nothing
488
+ copies them: the only copies are the kernel's pipe buffer and the child.
489
+ The length is read once, so `stdout` being the same array as `stdin`
490
+ appends to it without feeding the output back in, and the pointer at each
491
+ write, because that array moves when it grows and its stdin bytes stay
492
+ its first `n`. */
493
+ #define NISH_PIPED_CAP (16u << 20)
494
+ #ifndef __wasi__
495
+ static int nish_pipe(int *p) {
496
+ #ifdef __linux__
497
+ if (pipe2(p, O_CLOEXEC)) return -1;
498
+ #else
499
+ if (pipe(p)) return -1;
500
+ #endif
501
+ for (int k = 0; k < 2; k++) {
502
+ #ifdef __linux__
503
+ if (p[k] > 2) continue;
504
+ #endif
505
+ int moved = fcntl(p[k], F_DUPFD_CLOEXEC, 3);
506
+ close(p[k]);
507
+ p[k] = moved;
508
+ if (moved < 0) return -1;
509
+ }
510
+ return 0;
511
+ }
512
+
513
+ #endif
514
+
515
+ int32_t nish_spawn_piped(const nish_array *argv, const nish_array *in, nish_array *out, nish_array *err) {
516
+ #ifdef __wasi__
517
+ (void)argv;
518
+ (void)in;
519
+ (void)out;
520
+ (void)err;
521
+ return -1;
522
+ #else
523
+ uint64_t n = in->len, sent = 0, got[3] = { 0, 0, 0 };
524
+ int p[6] = { -1, -1, -1, -1, -1, -1 };
525
+ int32_t rc = -1;
526
+ if (nish_pipe(p) || nish_pipe(p + 2) || nish_pipe(p + 4)) goto done;
527
+ int child[3] = { p[0], p[3], p[5] };
528
+ int32_t pid = nish_spawn_start(argv, 0, 0, child);
529
+ /* The child holds its ends now; this process keeps only its own. */
530
+ for (int k = 0; k < 3; k++) {
531
+ close(child[k]);
532
+ p[k ? 2 * k + 1 : 0] = -1;
533
+ }
534
+ if (pid < 0) goto done;
535
+ sigset_t pipe_sig, mask, pending;
536
+ sigemptyset(&pipe_sig);
537
+ sigaddset(&pipe_sig, SIGPIPE);
538
+ pthread_sigmask(SIG_BLOCK, &pipe_sig, &mask);
539
+ sigpending(&pending);
540
+ int queued = sigismember(&pending, SIGPIPE);
541
+ struct pollfd f[3] = { { p[1], POLLOUT, 0 }, { p[2], POLLIN, 0 }, { p[4], POLLIN, 0 } };
542
+ p[1] = p[2] = p[4] = -1;
543
+ /* A blocking end could stall the loop on one stream while the child fills
544
+ another, so an end that stays blocking fails the run: the loop is skipped
545
+ and the child killed. */
546
+ rc = 0;
547
+ for (int k = 0; k < 3; k++)
548
+ if (fcntl(f[k].fd, F_SETFL, O_NONBLOCK) < 0) rc = -1;
549
+ if (!n) {
550
+ close(f[0].fd);
551
+ f[0].fd = -1;
552
+ }
553
+ nish_array *to[3] = { 0, out, err };
554
+ while (!rc && (f[0].fd >= 0 || f[1].fd >= 0 || f[2].fd >= 0)) {
555
+ if (poll(f, 3, -1) < 0) {
556
+ if (errno != EINTR) rc = -1;
557
+ continue;
558
+ }
559
+ if (f[0].revents) {
560
+ ssize_t k = write(f[0].fd, in->data + sent, n - sent);
561
+ if (k > 0) sent += k;
562
+ if (sent == n || (k < 0 && errno != EINTR && errno != EAGAIN)) {
563
+ close(f[0].fd);
564
+ f[0].fd = -1;
565
+ }
566
+ }
567
+ for (int s = 1; s < 3 && !rc; s++) {
568
+ if (!f[s].revents) continue;
569
+ nish_array *a = to[s];
570
+ /* Grown as `push` grows it, so the array keeps the shape every other
571
+ array has, and an ASan build tracks the block the same way. */
572
+ if (a->len == a->cap) nish_array_grow(a, 1);
573
+ /* One byte past the cap at most, which is how a stream that ends at
574
+ exactly the cap is told from one that goes on. */
575
+ uint64_t room = a->cap - a->len, left = NISH_PIPED_CAP + 1 - got[s];
576
+ ssize_t k = read(f[s].fd, a->data + a->len, room < left ? room : left);
577
+ if (k > 0) {
578
+ a->len += k;
579
+ got[s] += k;
580
+ if (got[s] > NISH_PIPED_CAP) {
581
+ a->len--;
582
+ rc = -2;
583
+ }
584
+ } else if (!k || (errno != EINTR && errno != EAGAIN)) {
585
+ close(f[s].fd);
586
+ f[s].fd = -1;
587
+ }
588
+ }
589
+ }
590
+ sigpending(&pending);
591
+ if (!queued && sigismember(&pending, SIGPIPE)) {
592
+ int sig;
593
+ sigwait(&pipe_sig, &sig);
594
+ }
595
+ pthread_sigmask(SIG_SETMASK, &mask, 0);
596
+ if (rc) kill(pid, SIGKILL);
597
+ for (int k = 0; k < 3; k++)
598
+ if (f[k].fd >= 0) close(f[k].fd);
599
+ int status;
600
+ while (waitpid(pid, &status, 0) < 0) {
601
+ if (errno != EINTR) {
602
+ rc = -1;
603
+ break;
604
+ }
605
+ }
606
+ if (!rc) rc = nish_exit_code(status);
607
+ done:
608
+ for (int k = 0; k < 6; k++)
609
+ if (p[k] >= 0) close(p[k]);
610
+ return rc;
611
+ #endif
612
+ }
316
613
 
317
614
  /* `kill(pid, sig)`: 0, or a negative errno. Only the four signals a program
318
615
  asks a child to stop or report with are sent -- 0 (is it there), SIGINT,
@@ -477,6 +774,36 @@ int64_t nish_total_memory(void) {
477
774
  #endif
478
775
  }
479
776
 
777
+ /* `setNice(n)`: this process's nice value set to `n`, 0 or a negative errno.
778
+ The kernel clamps `n` to its range (-20 to 19), so only lowering the value
779
+ below what the process has can fail: -EACCES (Linux, Darwin) or -EPERM
780
+ without the privilege to raise a priority. Linux keeps the value per thread,
781
+ so it is the calling thread's and every thread started after it. */
782
+ int32_t nish_set_nice(int32_t n) {
783
+ #ifdef __wasi__
784
+ (void)n;
785
+ return -ENOSYS;
786
+ #else
787
+ return setpriority(PRIO_PROCESS, 0, n) == 0 ? 0 : -errno;
788
+ #endif
789
+ }
790
+
791
+ /* `setIdlePriority()`: this process moved to `SCHED_IDLE`, which runs it only
792
+ when nothing else wants the CPU. Linux alone has the policy; elsewhere the
793
+ answer is -EOPNOTSUPP's Linux number, -95, as runtime-net.c answers it. An
794
+ unprivileged process cannot leave the policy again. Like the nice value, the
795
+ policy is the calling thread's. */
796
+ int32_t nish_set_idle_priority(void) {
797
+ #if defined(__linux__)
798
+ struct sched_param param = {0};
799
+ return sched_setscheduler(0, SCHED_IDLE, &param) == 0 ? 0 : -errno;
800
+ #elif defined(__wasi__)
801
+ return -ENOSYS;
802
+ #else
803
+ return -95;
804
+ #endif
805
+ }
806
+
480
807
  /* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
481
808
 
482
809
  **Sorted by bytes**, which `readdir(3)` and Node are not, for two reasons
@@ -545,11 +872,17 @@ nish_array *nish_readdir(const nish_str *path) {
545
872
  backwards and make an elapsed time negative. Only the difference between two
546
873
  reads means anything — the origin is arbitrary and is not comparable across
547
874
  processes or machines — and an i64 of nanoseconds is 292 years of it. */
548
- int64_t nish_monotonic_nanos(void) {
875
+ static int64_t nish_clock_nanos(clockid_t clock) {
549
876
  struct timespec ts;
550
- clock_gettime(CLOCK_MONOTONIC, &ts);
877
+ clock_gettime(clock, &ts);
551
878
  return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
552
879
  }
880
+ int64_t nish_monotonic_nanos(void) { return nish_clock_nanos(CLOCK_MONOTONIC); }
881
+
882
+ /* `wallClockNanos()`: `CLOCK_REALTIME`, nanoseconds since the Unix epoch, for
883
+ dating an event. It steps when the system time is set, so a duration is
884
+ `monotonicNanos`'s to measure. */
885
+ int64_t nish_wall_clock_nanos(void) { return nish_clock_nanos(CLOCK_REALTIME); }
553
886
 
554
887
  /* A C string copied into the arena, or NULL for NULL: the shape of every
555
888
  answer libc hands back by pointer, `getenv`'s here and `realpath`'s below. */
package/scripts/build.sh CHANGED
@@ -62,6 +62,10 @@
62
62
  # put in the .ll (line table, variables) reaches the binary. `nish
63
63
  # --link -g` passes it through automatically.
64
64
  #
65
+ # Build id: every native ELF binary carries an NT_GNU_BUILD_ID note
66
+ # (--build-id=sha1). A -g link hashes other bytes than a plain one, so strip
67
+ # the -g build to pair a binary with its debug file (docs/guide/using-nish/cli.md).
68
+ #
65
69
  # CPU level (WP38 S2): `--cpu x86-64-v3` compiles and links every input --
66
70
  # the module, the runtime and any other C -- with -march=x86-64-v3
67
71
  # -ffp-contract=off, so the code generator may use AVX2, BMI2 and the other
@@ -281,8 +285,8 @@ case "$(uname -s)" in
281
285
  # followed by SIGABRT, so the stage1 that linked went on to abort the
282
286
  # moment the bootstrap ran it. Measured too, on the run after the one that
283
287
  # named the UUID. An unloadable compiler is worse than any reproducibility,
284
- # so the UUID stays. The ELF branch's --build-id=none below is the flag
285
- # this would have been; ELF has no loader that insists on one.
288
+ # so the UUID stays. ELF's twin is the build-id note below, which ELF's
289
+ # loader never asks for and which is a hash of the output's bytes anyway.
286
290
  #
287
291
  # It costs nothing, because the UUID was measured stable across two links
288
292
  # of one input to one output path, and differing on a link to another. That
@@ -294,8 +298,14 @@ case "$(uname -s)" in
294
298
  strip_flag=(-Wl,-x) ;;
295
299
  *)
296
300
  # shellcheck disable=SC2054 # -Wl,<flag> is one argument: the comma is the linker's
297
- gc=(-Wl,--gc-sections -Wl,--as-needed -Wl,-O2 -Wl,--build-id=none); strip_flag=(-s)
301
+ gc=(-Wl,--gc-sections -Wl,--as-needed -Wl,-O2); strip_flag=(-s)
298
302
  elf=(-fno-plt)
303
+ # A GNU build-id note on every native link, debug included, so perf, gdb
304
+ # and debuginfod can match a stripped binary to its debug file. sha1 is a
305
+ # hash of the output's own bytes in lld, GNU ld and gold, so one input
306
+ # links to one id and `stage3 == stage2` holds; uuid would be random.
307
+ # shellcheck disable=SC2054
308
+ common+=(-Wl,--build-id=sha1)
299
309
  # GNU ld needs the gold plugin for LTO; prefer lld when clang can find it.
300
310
  if command -v ld.lld >/dev/null 2>&1; then common+=(-fuse-ld=lld); lld=1; fi ;;
301
311
  esac