io-event 1.18.0 → 1.19.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ad9ab9326ef0bb4d637833e5d9705940a7d150c0e42154ee4642fbf6f4b4b49
4
- data.tar.gz: bb1274afaa893df427b3eb422aa5c198da08493126e8b5f3ee2daac375f1ebda
3
+ metadata.gz: ba7c5ec2b2a68b1d615e75f33a081973b1f6c28122128e21e014dd5a348118b1
4
+ data.tar.gz: 78435a88a40c5b6458a13887cc709fcfc386c6ed7268b382fc7f976e422aa9c5
5
5
  SHA512:
6
- metadata.gz: 543bdb171056e37849b7042c0d822e7421908bbd006227be92b1b2c5c12d7988c8201ce02316c9f0aa32df9746f4a6d88345c8eb9f38f9c5018f4adbaeca27b9
7
- data.tar.gz: 65b3f2b61cf38b3c66cb2d0e9299ece198e546d0e30676f8bbd616fdccb043b47f032f0e8fd00bb5bf14bdef471f5ebb1c4769e8de86654900916fc8952c2be7
6
+ metadata.gz: b72b4d8e4f03515215af878ffe81c7bacf34be736bf8bc642d3e12e21d78c39eed7e1ae78ebb68786cddeae07042588a202b29f7a382352ed72fa08b1673cf88
7
+ data.tar.gz: 780583a7ef578f08f3e9c6fdec3547ea1140de80b514c934d3f3bd56d95eeb776a07f5cd5fc6fea2c65708a7730401c172bf981dc2d4e086325123e4cb6e0ae4
checksums.yaml.gz.sig CHANGED
Binary file
data/ext/extconf.rb CHANGED
@@ -6,6 +6,7 @@
6
6
  # Copyright, 2023, by Math Ieu.
7
7
  # Copyright, 2025, by Stanislav (Stas) Katkov.
8
8
  # Copyright, 2026, by Stan Hu.
9
+ # Copyright, 2026, by Sharon Rosner.
9
10
 
10
11
  return if RUBY_DESCRIPTION =~ /jruby/
11
12
 
@@ -32,9 +33,7 @@ have_func("rb_ext_ractor_safe")
32
33
  have_func("&rb_fiber_transfer")
33
34
 
34
35
  if have_library("uring") and have_header("liburing.h")
35
- # We might want to consider using this in the future:
36
- # have_func("io_uring_submit_and_wait_timeout", "liburing.h")
37
-
36
+ have_func("io_uring_prep_waitid", "liburing.h")
38
37
  $srcs << "io/event/selector/uring.c"
39
38
  end
40
39
 
@@ -462,7 +462,7 @@ VALUE process_wait_transfer(VALUE _arguments) {
462
462
  IO_Event_Selector_loop_yield(&arguments->selector->backend);
463
463
 
464
464
  if (arguments->waiting->ready) {
465
- return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags);
465
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
466
466
  } else {
467
467
  return Qfalse;
468
468
  }
@@ -488,6 +488,11 @@ VALUE IO_Event_Selector_EPoll_process_wait(VALUE self, VALUE fiber, VALUE _pid,
488
488
  pid_t pid = NUM2PIDT(_pid);
489
489
  int flags = NUM2INT(_flags);
490
490
 
491
+ // `pidfd_open` can only refer to a specific process, so waiting for any child or a process group (pid <= 0) is delegated to the threaded fallback:
492
+ if (pid <= 0) {
493
+ return IO_Event_Selector_process_wait(pid, flags);
494
+ }
495
+
491
496
  int descriptor = pidfd_open(pid, 0);
492
497
 
493
498
  if (descriptor == -1) {
@@ -497,7 +502,7 @@ VALUE IO_Event_Selector_EPoll_process_wait(VALUE self, VALUE fiber, VALUE _pid,
497
502
  rb_update_max_fd(descriptor);
498
503
 
499
504
  // `pidfd_open` (above) may be edge triggered, so we need to check if the process is already exited, and if so, return immediately, otherwise we will block indefinitely.
500
- VALUE status = IO_Event_Selector_process_status_wait(pid, flags);
505
+ VALUE status = IO_Event_Selector_process_status_reap(pid, flags);
501
506
  if (status != Qnil) {
502
507
  close(descriptor);
503
508
  return status;
@@ -469,7 +469,7 @@ VALUE process_wait_transfer(VALUE _arguments) {
469
469
 
470
470
  if (arguments->waiting->ready) {
471
471
  process_prewait(arguments->pid);
472
- return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags);
472
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
473
473
  } else {
474
474
  return Qfalse;
475
475
  }
@@ -493,6 +493,11 @@ VALUE IO_Event_Selector_KQueue_process_wait(VALUE self, VALUE fiber, VALUE _pid,
493
493
  pid_t pid = NUM2PIDT(_pid);
494
494
  int flags = NUM2INT(_flags);
495
495
 
496
+ // `EVFILT_PROC` can only refer to a specific process, so waiting for any child or a process group (pid <= 0) is delegated to the threaded fallback:
497
+ if (pid <= 0) {
498
+ return IO_Event_Selector_process_wait(pid, flags);
499
+ }
500
+
496
501
  struct IO_Event_Selector_KQueue_Waiting waiting = {
497
502
  .list = {.type = &IO_Event_Selector_KQueue_process_wait_list_type},
498
503
  .fiber = fiber,
@@ -514,7 +519,7 @@ VALUE IO_Event_Selector_KQueue_process_wait(VALUE self, VALUE fiber, VALUE _pid,
514
519
  if (errno == ESRCH) {
515
520
  process_prewait(pid);
516
521
 
517
- return IO_Event_Selector_process_status_wait(pid, flags);
522
+ return IO_Event_Selector_process_status_reap(pid, flags);
518
523
  }
519
524
 
520
525
  rb_sys_fail("IO_Event_Selector_KQueue_process_wait:IO_Event_Selector_KQueue_Waiting_register");
@@ -24,7 +24,7 @@ static VALUE rb_Process_Status = Qnil;
24
24
 
25
25
  VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags)
26
26
  {
27
- return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags | WNOHANG));
27
+ return rb_funcall(rb_Process_Status, id_wait, 2, PIDT2NUM(pid), INT2NUM(flags));
28
28
  }
29
29
  #endif
30
30
 
@@ -80,9 +80,21 @@ static VALUE IO_Event_Selector_nonblock(VALUE class, VALUE io)
80
80
  return rb_ensure(rb_yield, io, IO_Event_Selector_nonblock_ensure, (VALUE)&arguments);
81
81
  }
82
82
 
83
+ static VALUE rb_IO_Event_Selector = Qnil;
84
+ static ID id_process_wait;
85
+
86
+ // Wait for a process when the selector cannot do so natively (e.g. `pid <= 0`: any child, or a process group). Delegates to the pure-Ruby `IO::Event::Selector.process_wait`, which performs a blocking wait on a separate thread; joining it is fiber-scheduler aware, so the reactor keeps running.
87
+ VALUE IO_Event_Selector_process_wait(rb_pid_t pid, int flags) {
88
+ return rb_funcall(rb_IO_Event_Selector, id_process_wait, 2, PIDT2NUM(pid), INT2NUM(flags));
89
+ }
90
+
83
91
  void Init_IO_Event_Selector(VALUE IO_Event_Selector) {
84
92
  IO_Event_Selector_pending_interrupt_p_id = rb_intern("pending_interrupt?");
85
93
 
94
+ rb_IO_Event_Selector = IO_Event_Selector;
95
+ rb_gc_register_mark_object(rb_IO_Event_Selector);
96
+ id_process_wait = rb_intern("process_wait");
97
+
86
98
  #ifndef HAVE_RB_IO_DESCRIPTOR
87
99
  id_fileno = rb_intern("fileno");
88
100
  #endif
@@ -52,13 +52,21 @@ static inline int IO_Event_Selector_pending_interrupt(void) {
52
52
  int IO_Event_Selector_io_descriptor(VALUE io);
53
53
  #endif
54
54
 
55
- // Reap a process without hanging.
55
+ // Wait for a process to change state. This blocks until the process changes state, unless `WNOHANG` is given in `flags`.
56
56
  #ifdef HAVE_RB_PROCESS_STATUS_WAIT
57
- #define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags | WNOHANG)
57
+ #define IO_Event_Selector_process_status_wait(pid, flags) rb_process_status_wait(pid, flags)
58
58
  #else
59
59
  VALUE IO_Event_Selector_process_status_wait(rb_pid_t pid, int flags);
60
60
  #endif
61
61
 
62
+ // Reap a process that is known to have changed state (e.g. after a readiness event), without blocking.
63
+ static inline VALUE IO_Event_Selector_process_status_reap(rb_pid_t pid, int flags) {
64
+ return IO_Event_Selector_process_status_wait(pid, flags | WNOHANG);
65
+ }
66
+
67
+ // Wait for a process the selector cannot represent natively (e.g. `pid <= 0`: any child, or a process group), using a fiber-scheduler aware blocking wait on a separate thread.
68
+ VALUE IO_Event_Selector_process_wait(rb_pid_t pid, int flags);
69
+
62
70
  int IO_Event_Selector_nonblock_set(int file_descriptor);
63
71
  void IO_Event_Selector_nonblock_restore(int file_descriptor, int flags);
64
72
 
@@ -15,10 +15,17 @@
15
15
 
16
16
  #include "../interrupt.h"
17
17
 
18
- #include "pidfd.c"
19
-
20
18
  #include <linux/version.h>
21
19
 
20
+ // `io_uring` support for `IORING_OP_WAITID` was introduced in Linux 6.7. When available, we use it to wait for process exit directly in the ring, instead of polling on a pidfd.
21
+ #if defined(HAVE_IO_URING_PREP_WAITID) && (LINUX_VERSION_CODE >= KERNEL_VERSION(6,7,0))
22
+ #define IO_EVENT_SELECTOR_URING_USE_WAITID
23
+ #endif
24
+
25
+ #ifndef IO_EVENT_SELECTOR_URING_USE_WAITID
26
+ #include "pidfd.c"
27
+ #endif
28
+
22
29
  enum {
23
30
  DEBUG = 0,
24
31
  DEBUG_COMPLETION = 0,
@@ -500,13 +507,43 @@ struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) {
500
507
 
501
508
  #pragma mark - Process.wait
502
509
 
510
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
511
+ // Translate a Ruby/`waitpid`-style pid into the `waitid(2)` idtype and id, mirroring the semantics of `waitpid(2)`:
512
+ //
513
+ // pid == -1 -> any child (P_ALL)
514
+ // pid == 0 -> any child in the caller's process group (P_PGID, id 0; Linux >= 5.4)
515
+ // pid < -1 -> any child in process group |pid| (P_PGID)
516
+ // pid > 0 -> the specific child (P_PID)
517
+ //
518
+ static inline idtype_t process_waitid_type(pid_t pid, id_t *id) {
519
+ if (pid == -1) {
520
+ *id = 0;
521
+ return P_ALL;
522
+ } else if (pid == 0) {
523
+ *id = 0;
524
+ return P_PGID;
525
+ } else if (pid < -1) {
526
+ *id = (id_t)(-pid);
527
+ return P_PGID;
528
+ } else {
529
+ *id = (id_t)pid;
530
+ return P_PID;
531
+ }
532
+ }
533
+ #endif
534
+
503
535
  struct process_wait_arguments {
504
536
  struct IO_Event_Selector_URing *selector;
505
537
  struct IO_Event_Selector_URing_Waiting *waiting;
506
538
 
507
539
  pid_t pid;
508
540
  int flags;
541
+
542
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
543
+ siginfo_t siginfo;
544
+ #else
509
545
  int descriptor;
546
+ #endif
510
547
  };
511
548
 
512
549
  static
@@ -515,18 +552,34 @@ VALUE process_wait_transfer(VALUE _arguments) {
515
552
 
516
553
  IO_Event_Selector_loop_yield(&arguments->selector->backend);
517
554
 
555
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
556
+ int32_t result = arguments->waiting->result;
557
+
558
+ if (DEBUG) fprintf(stderr, "waitid result=%d pid=%d code=%d status=%d\n", result, arguments->siginfo.si_pid, arguments->siginfo.si_code, arguments->siginfo.si_status);
559
+
560
+ if (result < 0) {
561
+ // The `waitid` failed (e.g. `ECHILD` when there are no children). Reproduce the failure as a `Process::Status` carrying the error, rather than raising, so callers like `Process.waitall` / `Process.detach` (which expect `waitpid` to report the error, not raise) behave correctly:
562
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
563
+ }
564
+
565
+ // We waited with `WNOWAIT`, so the child has not been reaped yet. `si_pid` tells us exactly which child changed state (important when waiting for any child, e.g. pid -1). Reap it to obtain a correct `Process::Status`:
566
+ return IO_Event_Selector_process_status_reap(arguments->siginfo.si_pid, arguments->flags);
567
+ #else
518
568
  if (arguments->waiting->result) {
519
- return IO_Event_Selector_process_status_wait(arguments->pid, arguments->flags);
569
+ return IO_Event_Selector_process_status_reap(arguments->pid, arguments->flags);
520
570
  } else {
521
571
  return Qfalse;
522
572
  }
573
+ #endif
523
574
  }
524
575
 
525
576
  static
526
577
  VALUE process_wait_ensure(VALUE _arguments) {
527
578
  struct process_wait_arguments *arguments = (struct process_wait_arguments *)_arguments;
528
579
 
580
+ #ifndef IO_EVENT_SELECTOR_URING_USE_WAITID
529
581
  close(arguments->descriptor);
582
+ #endif
530
583
 
531
584
  IO_Event_Selector_URing_Waiting_cancel(arguments->waiting);
532
585
 
@@ -540,11 +593,18 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid,
540
593
  pid_t pid = NUM2PIDT(_pid);
541
594
  int flags = NUM2INT(_flags);
542
595
 
596
+ #ifndef IO_EVENT_SELECTOR_URING_USE_WAITID
597
+ // `pidfd_open` can only refer to a specific process, so waiting for any child or a process group (pid <= 0) is delegated to the threaded fallback:
598
+ if (pid <= 0) {
599
+ return IO_Event_Selector_process_wait(pid, flags);
600
+ }
601
+
543
602
  int descriptor = pidfd_open(pid, 0);
544
603
  if (descriptor < 0) {
545
604
  rb_syserr_fail(errno, "IO_Event_Selector_URing_process_wait:pidfd_open");
546
605
  }
547
606
  rb_update_max_fd(descriptor);
607
+ #endif
548
608
 
549
609
  struct IO_Event_Selector_URing_Waiting waiting = {
550
610
  .fiber = fiber,
@@ -559,12 +619,25 @@ VALUE IO_Event_Selector_URing_process_wait(VALUE self, VALUE fiber, VALUE _pid,
559
619
  .waiting = &waiting,
560
620
  .pid = pid,
561
621
  .flags = flags,
622
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
623
+ .siginfo = {0},
624
+ #else
562
625
  .descriptor = descriptor,
626
+ #endif
563
627
  };
564
628
 
565
- if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_poll_add(%p)\n", (void*)fiber);
566
629
  struct io_uring_sqe *sqe = io_get_sqe(selector);
630
+
631
+ #ifdef IO_EVENT_SELECTOR_URING_USE_WAITID
632
+ id_t id;
633
+ idtype_t idtype = process_waitid_type(pid, &id);
634
+ if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_waitid(fiber=%p, idtype=%d, id=%d, flags=%d)\n", (void*)fiber, idtype, (int)id, flags);
635
+ // `WNOWAIT` leaves the child in a waitable state so we can reap it with `rb_process_status_wait` afterwards and build a correct `Process::Status`:
636
+ io_uring_prep_waitid(sqe, idtype, id, &process_wait_arguments.siginfo, WEXITED | WNOWAIT, 0);
637
+ #else
638
+ if (DEBUG) fprintf(stderr, "IO_Event_Selector_URing_process_wait:io_uring_prep_poll_add(%p)\n", (void*)fiber);
567
639
  io_uring_prep_poll_add(sqe, descriptor, POLLIN|POLLHUP|POLLERR);
640
+ #endif
568
641
  io_uring_sqe_set_data(sqe, completion);
569
642
  io_uring_submit_pending(selector);
570
643
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2026, by Samuel Williams.
5
5
 
6
6
  module IO::Event
7
7
  # A thread safe synchronisation primative.
@@ -278,9 +278,7 @@ module IO::Event
278
278
  # @parameter flags [Integer] Flags to pass to Process::Status.wait.
279
279
  # @returns [Process::Status] The status of the waited process.
280
280
  def process_wait(fiber, pid, flags)
281
- Thread.new do
282
- Process::Status.wait(pid, flags)
283
- end.value
281
+ Selector.process_wait(pid, flags)
284
282
  end
285
283
 
286
284
  private def pop_ready
@@ -40,5 +40,23 @@ module IO::Event
40
40
 
41
41
  return selector
42
42
  end
43
+
44
+ # Wait for a process to change state, for the cases a selector cannot represent natively (e.g. `pid <= 0`: any child, or a process group). The native selectors integrate process waiting with the event loop using per-process primitives (`pidfd_open`, `EVFILT_PROC`) which can only refer to a single, specific process, and delegate here otherwise.
45
+ #
46
+ # The wait is performed on a separate thread, which has no fiber scheduler and therefore blocks. Joining it via `Thread#value` is fiber-scheduler aware, so the calling fiber yields to the event loop and the reactor keeps running other fibers.
47
+ #
48
+ # @parameter pid [Integer] The process ID (or process group) to wait for.
49
+ # @parameter flags [Integer] Flags to pass to `Process::Status.wait`.
50
+ # @returns [Process::Status] The status of the waited process.
51
+ def self.process_wait(pid, flags)
52
+ thread = ::Thread.new do
53
+ ::Process::Status.wait(pid, flags)
54
+ end
55
+
56
+ thread.value
57
+ ensure
58
+ # If the calling fiber was interrupted before the wait completed, don't leave the thread running:
59
+ thread&.kill
60
+ end
43
61
  end
44
62
  end
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.18.0"
10
+ VERSION = "1.19.1"
11
11
  end
12
12
  end
data/license.md CHANGED
@@ -19,6 +19,7 @@ Copyright, 2026, by John Hawthorn.
19
19
  Copyright, 2026, by Italo Brandão.
20
20
  Copyright, 2026, by Fletcher Dares.
21
21
  Copyright, 2026, by Tavian Barnes.
22
+ Copyright, 2026, by Sharon Rosner.
22
23
 
23
24
  Permission is hereby granted, free of charge, to any person obtaining a copy
24
25
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -18,6 +18,15 @@ Please see the [project documentation](https://socketry.github.io/io-event/) for
18
18
 
19
19
  Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases.
20
20
 
21
+ ### v1.19.1
22
+
23
+ - Fix `Process.waitall` / `Process.detach` under the `URing` selector: when `io_uring`'s `waitid` reported an error (e.g. `ECHILD` when there are no more children), the `process_wait` hook raised instead of returning the error as a `Process::Status`, so callers that expect `waitpid` to *report* "no more children" rather than raise would fail.
24
+
25
+ ### v1.19.0
26
+
27
+ - Use `io_uring_prep_waitid` for `process_wait` in the `URing` selector (Linux 6.7+), waiting for child exit directly in the ring instead of polling on a `pidfd`. The child is reaped via `rb_process_status_wait` (using `WEXITED | WNOWAIT`) to construct a correct `Process::Status`, and `process_wait(-1, ...)` / `process_wait(0, ...)` are now supported.
28
+ - Support waiting for any child or a process group (`pid <= 0`) on all selectors. The `EPoll` (`pidfd_open`) and `KQueue` (`EVFILT_PROC`) selectors can only watch a specific process, so these cases now fall back to a blocking wait on a dedicated thread; joining it is fiber-scheduler aware, so the reactor keeps running.
29
+
21
30
  ### v1.18.0
22
31
 
23
32
  - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
@@ -56,14 +65,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
56
65
 
57
66
  - Simplify closed-IO handling in the `Select` selector: rely on Ruby 4's `rb_thread_io_close_interrupt` to wake fibers waiting on a descriptor that's been closed, removing a custom error-recovery path that could mis-attribute `IOError` / `Errno::EBADF` to the wrong waiter.
58
67
 
59
- ### v1.15.0
60
-
61
- - Add bounds checks, in the unlikely event of a user providing an invalid offset that exceeds the buffer size. This prevents potential memory corruption and ensures safe operation when using buffered IO methods.
62
-
63
- ### v1.14.4
64
-
65
- - Allow `epoll_pwait2` to be disabled via `--disable-epoll_pwait2`.
66
-
67
68
  ## Contributing
68
69
 
69
70
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v1.19.1
4
+
5
+ - Fix `Process.waitall` / `Process.detach` under the `URing` selector: when `io_uring`'s `waitid` reported an error (e.g. `ECHILD` when there are no more children), the `process_wait` hook raised instead of returning the error as a `Process::Status`, so callers that expect `waitpid` to *report* "no more children" rather than raise would fail.
6
+
7
+ ## v1.19.0
8
+
9
+ - Use `io_uring_prep_waitid` for `process_wait` in the `URing` selector (Linux 6.7+), waiting for child exit directly in the ring instead of polling on a `pidfd`. The child is reaped via `rb_process_status_wait` (using `WEXITED | WNOWAIT`) to construct a correct `Process::Status`, and `process_wait(-1, ...)` / `process_wait(0, ...)` are now supported.
10
+ - Support waiting for any child or a process group (`pid <= 0`) on all selectors. The `EPoll` (`pidfd_open`) and `KQueue` (`EVFILT_PROC`) selectors can only watch a specific process, so these cases now fall back to a blocking wait on a dedicated thread; joining it is fiber-scheduler aware, so the reactor keeps running.
11
+
3
12
  ## v1.18.0
4
13
 
5
14
  - **Fixed**: Avoid entering a blocking native selector wait when an interrupt is already pending for the current thread.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-event
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.0
4
+ version: 1.19.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -20,6 +20,7 @@ authors:
20
20
  - John Hawthorn
21
21
  - Luke Gruber
22
22
  - Pavel Rosický
23
+ - Sharon Rosner
23
24
  - Stan Hu
24
25
  - Stanislav (Stas) Katkov
25
26
  - William T. Nelson
metadata.gz.sig CHANGED
Binary file