curb 1.1.0 → 1.2.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.
data/ext/curb_multi.c CHANGED
@@ -5,6 +5,9 @@
5
5
  */
6
6
  #include "curb_config.h"
7
7
  #include <ruby.h>
8
+ #ifdef HAVE_RUBY_IO_H
9
+ #include <ruby/io.h>
10
+ #endif
8
11
  #ifdef HAVE_RUBY_ST_H
9
12
  #include <ruby/st.h>
10
13
  #else
@@ -14,6 +17,9 @@
14
17
  #ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
15
18
  #include <ruby/thread.h>
16
19
  #endif
20
+ #ifdef HAVE_RUBY_FIBER_SCHEDULER_H
21
+ #include <ruby/fiber/scheduler.h>
22
+ #endif
17
23
 
18
24
  #include "curb_easy.h"
19
25
  #include "curb_errors.h"
@@ -21,13 +27,25 @@
21
27
  #include "curb_multi.h"
22
28
 
23
29
  #include <errno.h>
30
+ #include <stdarg.h>
31
+
32
+ /*
33
+ * Optional socket-action debug logging. Enabled by defining CURB_SOCKET_DEBUG=1
34
+ * at compile time (e.g. via environment variable passed to extconf.rb).
35
+ */
36
+ #ifndef CURB_SOCKET_DEBUG
37
+ #define CURB_SOCKET_DEBUG 0
38
+ #endif
39
+ #if !CURB_SOCKET_DEBUG
40
+ #define curb_debugf(...) ((void)0)
41
+ #endif
24
42
 
25
43
  #ifdef _WIN32
26
44
  // for O_RDWR and O_BINARY
27
45
  #include <fcntl.h>
28
46
  #endif
29
47
 
30
- #ifdef HAVE_CURL_MULTI_WAIT
48
+ #if 0 /* disabled curl_multi_wait in favor of scheduler-aware fdsets */
31
49
  #include <stdint.h> /* for intptr_t */
32
50
 
33
51
  struct wait_args {
@@ -58,6 +76,10 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
58
76
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
59
77
  static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running);
60
78
 
79
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg);
80
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm);
81
+ static void curl_multi_mark(void *ptr);
82
+
61
83
  static VALUE callback_exception(VALUE did_raise, VALUE exception) {
62
84
  // TODO: we could have an option to enable exception reporting
63
85
  /* VALUE ret = rb_funcall(exception, rb_intern("message"), 0);
@@ -79,8 +101,67 @@ static VALUE callback_exception(VALUE did_raise, VALUE exception) {
79
101
  return exception;
80
102
  }
81
103
 
104
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg) {
105
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)arg;
106
+ VALUE easy = (VALUE)val;
107
+ ruby_curl_easy *rbce = NULL;
108
+
109
+ if (RB_TYPE_P(easy, T_DATA)) {
110
+ Data_Get_Struct(easy, ruby_curl_easy, rbce);
111
+ }
112
+
113
+ if (!rbce) {
114
+ return ST_CONTINUE;
115
+ }
116
+
117
+ if (rbcm && rbcm->handle && rbce->curl) {
118
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
119
+ }
120
+
121
+ rbce->multi = Qnil;
122
+
123
+ return ST_CONTINUE;
124
+ }
125
+
126
+ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
127
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
128
+
129
+ if (!rbcm || !rbce || !rbcm->attached) {
130
+ return;
131
+ }
132
+
133
+ st_data_t key = (st_data_t)rbce;
134
+ st_delete(rbcm->attached, &key, NULL);
135
+ }
136
+
137
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
138
+ if (!rbcm || !rbcm->attached) {
139
+ return;
140
+ }
141
+
142
+ st_table *attached = rbcm->attached;
143
+ rbcm->attached = NULL;
144
+
145
+ st_foreach(attached, detach_easy_entry, (st_data_t)rbcm);
146
+
147
+ st_free_table(attached);
148
+
149
+ rbcm->active = 0;
150
+ rbcm->running = 0;
151
+ }
152
+
82
153
  void curl_multi_free(ruby_curl_multi *rbcm) {
83
- curl_multi_cleanup(rbcm->handle);
154
+ if (!rbcm) {
155
+ return;
156
+ }
157
+
158
+ rb_curl_multi_detach_all(rbcm);
159
+
160
+ if (rbcm->handle) {
161
+ curl_multi_cleanup(rbcm->handle);
162
+ rbcm->handle = NULL;
163
+ }
164
+
84
165
  free(rbcm);
85
166
  }
86
167
 
@@ -92,6 +173,18 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
92
173
 
93
174
  rbcm->active = 0;
94
175
  rbcm->running = 0;
176
+
177
+ if (rbcm->attached) {
178
+ st_free_table(rbcm->attached);
179
+ rbcm->attached = NULL;
180
+ }
181
+
182
+ rbcm->attached = st_init_numtable();
183
+ if (!rbcm->attached) {
184
+ curl_multi_cleanup(rbcm->handle);
185
+ rbcm->handle = NULL;
186
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
187
+ }
95
188
  }
96
189
 
97
190
  /*
@@ -102,6 +195,11 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
102
195
  */
103
196
  VALUE ruby_curl_multi_new(VALUE klass) {
104
197
  ruby_curl_multi *rbcm = ALLOC(ruby_curl_multi);
198
+ if (!rbcm) {
199
+ rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Multi");
200
+ }
201
+
202
+ MEMZERO(rbcm, ruby_curl_multi, 1);
105
203
 
106
204
  ruby_curl_multi_init(rbcm);
107
205
 
@@ -110,8 +208,8 @@ VALUE ruby_curl_multi_new(VALUE klass) {
110
208
  * If your structure references other Ruby objects, then your mark function needs to
111
209
  * identify these objects using rb_gc_mark(value). If the structure doesn't reference
112
210
  * other Ruby objects, you can simply pass 0 as a function pointer.
113
- */
114
- return Data_Wrap_Struct(klass, 0, curl_multi_free, rbcm);
211
+ */
212
+ return Data_Wrap_Struct(klass, curl_multi_mark, curl_multi_free, rbcm);
115
213
  }
116
214
 
117
215
  /*
@@ -271,6 +369,17 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
271
369
  * If this number is not correct, the next call to curl_multi_perform will correct it. */
272
370
  rbcm->running++;
273
371
 
372
+ if (!rbcm->attached) {
373
+ rbcm->attached = st_init_numtable();
374
+ if (!rbcm->attached) {
375
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
376
+ ruby_curl_easy_cleanup(easy, rbce);
377
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
378
+ }
379
+ }
380
+
381
+ st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
382
+
274
383
  /* track a reference to associated multi handle */
275
384
  rbce->multi = self;
276
385
 
@@ -311,9 +420,13 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
311
420
  raise_curl_multi_error_exception(result);
312
421
  }
313
422
 
314
- rbcm->active--;
423
+ if (rbcm->active > 0) {
424
+ rbcm->active--;
425
+ }
315
426
 
316
427
  ruby_curl_easy_cleanup( easy, rbce );
428
+
429
+ rb_curl_multi_forget_easy(rbcm, rbce);
317
430
  }
318
431
 
319
432
  // on_success, on_failure, on_complete
@@ -324,6 +437,23 @@ static VALUE call_status_handler2(VALUE ary) {
324
437
  return rb_funcall(rb_ary_entry(ary, 0), idCall, 2, rb_ary_entry(ary, 1), rb_ary_entry(ary, 2));
325
438
  }
326
439
 
440
+ static void flush_stderr_if_any(ruby_curl_easy *rbce) {
441
+ VALUE stderr_io = rb_easy_get("stderr_io");
442
+ if (stderr_io != Qnil) {
443
+ /* Flush via Ruby IO API */
444
+ rb_funcall(stderr_io, rb_intern("flush"), 0);
445
+ #ifdef HAVE_RUBY_IO_H
446
+ /* Additionally flush underlying FILE* to be extra safe. */
447
+ rb_io_t *open_f_ptr;
448
+ if (RB_TYPE_P(stderr_io, T_FILE)) {
449
+ GetOpenFile(stderr_io, open_f_ptr);
450
+ FILE *fp = rb_io_stdio_file(open_f_ptr);
451
+ if (fp) fflush(fp);
452
+ }
453
+ #endif
454
+ }
455
+ }
456
+
327
457
  static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
328
458
  long response_code = -1;
329
459
  VALUE easy;
@@ -336,6 +466,10 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
336
466
 
337
467
  rbce->last_result = result; /* save the last easy result code */
338
468
 
469
+ /* Ensure any verbose output redirected via CURLOPT_STDERR is flushed
470
+ * before we tear down handler state. */
471
+ flush_stderr_if_any(rbce);
472
+
339
473
  // remove the easy handle from multi on completion so it can be reused again
340
474
  rb_funcall(self, rb_intern("remove"), 1, easy);
341
475
 
@@ -345,6 +479,9 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
345
479
  rbce->curl_headers = NULL;
346
480
  }
347
481
 
482
+ /* Flush again after removal to cover any last buffered data. */
483
+ flush_stderr_if_any(rbce);
484
+
348
485
  if (ecode != 0) {
349
486
  raise_curl_easy_error_exception(ecode);
350
487
  }
@@ -361,7 +498,7 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
361
498
 
362
499
  #ifdef HAVE_CURLINFO_RESPONSE_CODE
363
500
  curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
364
- #else
501
+ #else /* use fdsets path for waiting */
365
502
  // old libcurl
366
503
  curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CODE, &response_code);
367
504
  #endif
@@ -386,11 +523,16 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
386
523
  CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
387
524
 
388
525
  } else if (!rb_easy_nil("redirect_proc") && ((response_code >= 300 && response_code < 400) || redirect_count > 0) ) {
389
- rbce->callback_active = 1;
390
- callargs = rb_ary_new3(3, rb_easy_get("redirect_proc"), easy, rb_curl_easy_error(result));
391
- rbce->callback_active = 0;
392
- rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
393
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
526
+ /* Skip on_redirect callback if follow_location is false AND max_redirects is 0 */
527
+ if (!rbce->follow_location && rbce->max_redirs == 0) {
528
+ // Do nothing - skip the callback
529
+ } else {
530
+ rbce->callback_active = 1;
531
+ callargs = rb_ary_new3(3, rb_easy_get("redirect_proc"), easy, rb_curl_easy_error(result));
532
+ rbce->callback_active = 0;
533
+ rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
534
+ CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
535
+ }
394
536
  } else if (!rb_easy_nil("missing_proc") &&
395
537
  (response_code >= 400 && response_code < 500)) {
396
538
  rbce->callback_active = 1;
@@ -466,6 +608,367 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
466
608
  */
467
609
  }
468
610
 
611
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
612
+ /* ---- socket-action implementation (scheduler-friendly) ---- */
613
+ typedef struct {
614
+ st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
615
+ long timeout_ms; /* last timeout set by libcurl timer callback */
616
+ } multi_socket_ctx;
617
+
618
+ #if CURB_SOCKET_DEBUG
619
+ static void curb_debugf(const char *fmt, ...) {
620
+ va_list ap;
621
+ va_start(ap, fmt);
622
+ vfprintf(stderr, fmt, ap);
623
+ fputc('\n', stderr);
624
+ fflush(stderr);
625
+ va_end(ap);
626
+ }
627
+
628
+ static const char *poll_what_str(int what, char *buf, size_t n) {
629
+ /* what is one of CURL_POLL_*, not a bitmask except INOUT */
630
+ if (what == CURL_POLL_REMOVE) snprintf(buf, n, "REMOVE");
631
+ else if (what == CURL_POLL_IN) snprintf(buf, n, "IN");
632
+ else if (what == CURL_POLL_OUT) snprintf(buf, n, "OUT");
633
+ else if (what == CURL_POLL_INOUT) snprintf(buf, n, "INOUT");
634
+ else snprintf(buf, n, "WHAT=%d", what);
635
+ return buf;
636
+ }
637
+
638
+ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
639
+ char tmp[32]; tmp[0] = 0;
640
+ int off = 0;
641
+ if (flags & CURL_CSELECT_IN) off += snprintf(tmp+off, (size_t)(sizeof(tmp)-off), "%sIN", off?"|":"");
642
+ if (flags & CURL_CSELECT_OUT) off += snprintf(tmp+off, (size_t)(sizeof(tmp)-off), "%sOUT", off?"|":"");
643
+ if (flags & CURL_CSELECT_ERR) off += snprintf(tmp+off, (size_t)(sizeof(tmp)-off), "%sERR", off?"|":"");
644
+ if (off == 0) snprintf(tmp, sizeof(tmp), "0");
645
+ snprintf(buf, n, "%s", tmp);
646
+ return buf;
647
+ }
648
+ #else
649
+ #define poll_what_str(...) ""
650
+ #define cselect_flags_str(...) ""
651
+ #endif
652
+
653
+ /* Protected call to rb_fiber_scheduler_io_wait to avoid unwinding into C on TypeError. */
654
+ struct fiber_io_wait_args { VALUE scheduler; VALUE io; int events; VALUE timeout; };
655
+ static VALUE fiber_io_wait_protected(VALUE argp) {
656
+ struct fiber_io_wait_args *a = (struct fiber_io_wait_args *)argp;
657
+ return rb_fiber_scheduler_io_wait(a->scheduler, a->io, a->events, a->timeout);
658
+ }
659
+
660
+ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp) {
661
+ multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
662
+ (void)easy; (void)socketp;
663
+ int fd = (int)s;
664
+
665
+ if (!ctx || !ctx->sock_map) return 0;
666
+
667
+ if (what == CURL_POLL_REMOVE) {
668
+ st_data_t k = (st_data_t)fd;
669
+ st_data_t rec;
670
+ st_delete(ctx->sock_map, &k, &rec);
671
+ {
672
+ char b[16];
673
+ curb_debugf("[curb.socket] sock_cb fd=%d what=%s (removed)", fd, poll_what_str(what, b, sizeof(b)));
674
+ }
675
+ } else {
676
+ /* store current interest mask for this fd */
677
+ st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
678
+ {
679
+ char b[16];
680
+ curb_debugf("[curb.socket] sock_cb fd=%d what=%s (tracked)", fd, poll_what_str(what, b, sizeof(b)));
681
+ }
682
+ }
683
+ return 0;
684
+ }
685
+
686
+ static int multi_timer_cb(CURLM *multi, long timeout_ms, void *userp) {
687
+ (void)multi;
688
+ multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
689
+ if (ctx) ctx->timeout_ms = timeout_ms;
690
+ curb_debugf("[curb.socket] timer_cb timeout_ms=%ld", timeout_ms);
691
+ return 0;
692
+ }
693
+
694
+ struct build_fdset_args { rb_fdset_t *r; rb_fdset_t *w; rb_fdset_t *e; int maxfd; };
695
+ static int rb_fdset_from_sockmap_i(st_data_t key, st_data_t val, st_data_t argp) {
696
+ struct build_fdset_args *a = (struct build_fdset_args *)argp;
697
+ int fd = (int)key;
698
+ int what = (int)val;
699
+ if (what & CURL_POLL_IN) rb_fd_set(fd, a->r);
700
+ if (what & CURL_POLL_OUT) rb_fd_set(fd, a->w);
701
+ rb_fd_set(fd, a->e);
702
+ if (fd > a->maxfd) a->maxfd = fd;
703
+ return ST_CONTINUE;
704
+ }
705
+ static void rb_fdset_from_sockmap(st_table *map, rb_fdset_t *rfds, rb_fdset_t *wfds, rb_fdset_t *efds, int *maxfd_out) {
706
+ if (!map) { *maxfd_out = -1; return; }
707
+ struct build_fdset_args a; a.r = rfds; a.w = wfds; a.e = efds; a.maxfd = -1;
708
+ st_foreach(map, rb_fdset_from_sockmap_i, (st_data_t)&a);
709
+ *maxfd_out = a.maxfd;
710
+ }
711
+
712
+ struct dispatch_args { CURLM *mh; int *running; CURLMcode mrc; rb_fdset_t *r; rb_fdset_t *w; rb_fdset_t *e; };
713
+ static int dispatch_ready_fd_i(st_data_t key, st_data_t val, st_data_t argp) {
714
+ (void)val;
715
+ struct dispatch_args *dp = (struct dispatch_args *)argp;
716
+ int fd = (int)key;
717
+ int flags = 0;
718
+ if (rb_fd_isset(fd, dp->r)) flags |= CURL_CSELECT_IN;
719
+ if (rb_fd_isset(fd, dp->w)) flags |= CURL_CSELECT_OUT;
720
+ if (rb_fd_isset(fd, dp->e)) flags |= CURL_CSELECT_ERR;
721
+ if (flags) {
722
+ dp->mrc = curl_multi_socket_action(dp->mh, (curl_socket_t)fd, flags, dp->running);
723
+ if (dp->mrc != CURLM_OK) return ST_STOP;
724
+ }
725
+ return ST_CONTINUE;
726
+ }
727
+
728
+ /* Helpers used with st_foreach to avoid compiler-specific nested functions. */
729
+ struct pick_one_state { int fd; int what; int found; };
730
+ static int st_pick_one_i(st_data_t key, st_data_t val, st_data_t argp) {
731
+ struct pick_one_state *s = (struct pick_one_state *)argp;
732
+ s->fd = (int)key;
733
+ s->what = (int)val;
734
+ s->found = 1;
735
+ return ST_STOP;
736
+ }
737
+ struct counter_state { int count; };
738
+ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
739
+ (void)k; (void)v;
740
+ struct counter_state *c = (struct counter_state *)argp;
741
+ c->count++;
742
+ return ST_CONTINUE;
743
+ }
744
+
745
+ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
746
+ /* prime the state: let libcurl act on timeouts to setup sockets */
747
+ CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
748
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
749
+ curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
750
+ rb_curl_multi_read_info(self, rbcm->handle);
751
+ if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
752
+
753
+ while (rbcm->running) {
754
+ struct timeval tv = {0, 0};
755
+ if (ctx->timeout_ms < 0) {
756
+ tv.tv_sec = cCurlMutiDefaulttimeout / 1000;
757
+ tv.tv_usec = (cCurlMutiDefaulttimeout % 1000) * 1000;
758
+ } else {
759
+ long t = ctx->timeout_ms;
760
+ if (t > cCurlMutiDefaulttimeout) t = cCurlMutiDefaulttimeout;
761
+ if (t < 0) t = 0;
762
+ tv.tv_sec = t / 1000;
763
+ tv.tv_usec = (t % 1000) * 1000;
764
+ }
765
+
766
+ /* Find a representative fd to wait on (if any). */
767
+ int wait_fd = -1;
768
+ int wait_what = 0;
769
+ if (ctx->sock_map) {
770
+ struct pick_one_state st = { -1, 0, 0 };
771
+ st_foreach(ctx->sock_map, st_pick_one_i, (st_data_t)&st);
772
+ if (st.found) { wait_fd = st.fd; wait_what = st.what; }
773
+ }
774
+
775
+ /* Count tracked fds for logging */
776
+ int count_tracked = 0;
777
+ if (ctx->sock_map) {
778
+ struct counter_state cs = { 0 };
779
+ st_foreach(ctx->sock_map, st_count_i, (st_data_t)&cs);
780
+ count_tracked = cs.count;
781
+ }
782
+
783
+ curb_debugf("[curb.socket] wait phase: tracked_fds=%d fd=%d what=%d tv=%ld.%06ld", count_tracked, wait_fd, wait_what, (long)tv.tv_sec, (long)tv.tv_usec);
784
+
785
+ int did_timeout = 0;
786
+ int any_ready = 0;
787
+
788
+ int handled_wait = 0;
789
+ if (count_tracked > 1) {
790
+ /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
791
+ rb_fdset_t rfds, wfds, efds;
792
+ rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
793
+ int maxfd = -1;
794
+ struct build_fdset_args a2; a2.r = &rfds; a2.w = &wfds; a2.e = &efds; a2.maxfd = -1;
795
+ st_foreach(ctx->sock_map, rb_fdset_from_sockmap_i, (st_data_t)&a2);
796
+ maxfd = a2.maxfd;
797
+ int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
798
+ curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
799
+ if (rc < 0) {
800
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
801
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
802
+ continue;
803
+ }
804
+ any_ready = (rc > 0);
805
+ did_timeout = (rc == 0);
806
+ if (any_ready) {
807
+ struct dispatch_args d; d.mh = rbcm->handle; d.running = &rbcm->running; d.mrc = CURLM_OK; d.r = &rfds; d.w = &wfds; d.e = &efds;
808
+ st_foreach(ctx->sock_map, dispatch_ready_fd_i, (st_data_t)&d);
809
+ if (d.mrc != CURLM_OK) {
810
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
811
+ raise_curl_multi_error_exception(d.mrc);
812
+ }
813
+ }
814
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
815
+ handled_wait = 1;
816
+ } else if (count_tracked == 1) {
817
+ #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
818
+ if (wait_fd >= 0) {
819
+ int ev = 0;
820
+ if (wait_what == CURL_POLL_IN) ev = RB_WAITFD_IN;
821
+ else if (wait_what == CURL_POLL_OUT) ev = RB_WAITFD_OUT;
822
+ else if (wait_what == CURL_POLL_INOUT) ev = RB_WAITFD_IN|RB_WAITFD_OUT;
823
+ int rc = rb_wait_for_single_fd(wait_fd, ev, &tv);
824
+ curb_debugf("[curb.socket] rb_wait_for_single_fd rc=%d fd=%d ev=%d", rc, wait_fd, ev);
825
+ if (rc < 0) {
826
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "wait_for_single_fd(): %s", strerror(errno));
827
+ continue;
828
+ }
829
+ any_ready = (rc != 0);
830
+ did_timeout = (rc == 0);
831
+ handled_wait = 1;
832
+ }
833
+ #endif
834
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
835
+ if (!handled_wait) {
836
+ VALUE scheduler = rb_fiber_scheduler_current();
837
+ if (scheduler != Qnil) {
838
+ int events = 0;
839
+ if (wait_fd >= 0) {
840
+ if (wait_what == CURL_POLL_IN) events = RB_WAITFD_IN;
841
+ else if (wait_what == CURL_POLL_OUT) events = RB_WAITFD_OUT;
842
+ else if (wait_what == CURL_POLL_INOUT) events = RB_WAITFD_IN|RB_WAITFD_OUT;
843
+ else events = RB_WAITFD_IN|RB_WAITFD_OUT;
844
+ }
845
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
846
+ VALUE timeout = rb_float_new(timeout_s);
847
+ if (wait_fd < 0) {
848
+ rb_thread_wait_for(tv);
849
+ did_timeout = 1;
850
+ } else {
851
+ const char *mode = (wait_what == CURL_POLL_IN) ? "r" : (wait_what == CURL_POLL_OUT) ? "w" : "r+";
852
+ VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(wait_fd), rb_str_new_cstr(mode));
853
+ rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
854
+ struct fiber_io_wait_args args = { scheduler, io, events, timeout };
855
+ int state = 0;
856
+ VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
857
+ if (state) {
858
+ did_timeout = 1; any_ready = 0;
859
+ } else {
860
+ any_ready = (ready != Qfalse);
861
+ did_timeout = !any_ready;
862
+ }
863
+ }
864
+ handled_wait = 1;
865
+ }
866
+ }
867
+ #endif
868
+ if (!handled_wait) {
869
+ /* Fallback: single-fd select. */
870
+ rb_fdset_t rfds, wfds, efds;
871
+ rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
872
+ int maxfd = -1;
873
+ if (wait_fd >= 0) {
874
+ if (wait_what == CURL_POLL_IN || wait_what == CURL_POLL_INOUT) rb_fd_set(wait_fd, &rfds);
875
+ if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) rb_fd_set(wait_fd, &wfds);
876
+ rb_fd_set(wait_fd, &efds);
877
+ maxfd = wait_fd;
878
+ }
879
+ int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
880
+ curb_debugf("[curb.socket] rb_thread_fd_select(single) rc=%d fd=%d", rc, wait_fd);
881
+ if (rc < 0) {
882
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
883
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
884
+ continue;
885
+ }
886
+ any_ready = (rc > 0);
887
+ did_timeout = (rc == 0);
888
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
889
+ }
890
+ } else { /* count_tracked == 0 */
891
+ rb_thread_wait_for(tv);
892
+ did_timeout = 1;
893
+ }
894
+
895
+ if (did_timeout) {
896
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
897
+ curb_debugf("[curb.socket] socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
898
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
899
+ } else if (any_ready) {
900
+ if (count_tracked == 1 && wait_fd >= 0) {
901
+ int flags = 0;
902
+ if (wait_what == CURL_POLL_IN || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
903
+ if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
904
+ flags |= CURL_CSELECT_ERR;
905
+ char b[32];
906
+ curb_debugf("[curb.socket] socket_action fd=%d flags=%s", wait_fd, cselect_flags_str(flags, b, sizeof(b)));
907
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)wait_fd, flags, &rbcm->running);
908
+ curb_debugf("[curb.socket] socket_action -> mrc=%d running=%d", mrc, rbcm->running);
909
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
910
+ }
911
+ }
912
+
913
+ rb_curl_multi_read_info(self, rbcm->handle);
914
+ curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
915
+ if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
916
+ }
917
+ }
918
+
919
+ struct socket_drive_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; VALUE block; };
920
+ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
921
+ struct socket_drive_args *a = (struct socket_drive_args *)argp;
922
+ rb_curl_multi_socket_drive(a->self, a->rbcm, a->ctx, a->block);
923
+ return Qtrue;
924
+ }
925
+ struct socket_cleanup_args { ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
926
+ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
927
+ struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
928
+ if (c->rbcm && c->rbcm->handle) {
929
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_SOCKETFUNCTION, NULL);
930
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_SOCKETDATA, NULL);
931
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_TIMERFUNCTION, NULL);
932
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_TIMERDATA, NULL);
933
+ }
934
+ if (c->ctx && c->ctx->sock_map) {
935
+ st_free_table(c->ctx->sock_map);
936
+ c->ctx->sock_map = NULL;
937
+ }
938
+ return Qnil;
939
+ }
940
+
941
+ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
942
+ ruby_curl_multi *rbcm;
943
+ VALUE block = Qnil;
944
+ rb_scan_args(argc, argv, "0&", &block);
945
+
946
+ Data_Get_Struct(self, ruby_curl_multi, rbcm);
947
+
948
+ multi_socket_ctx ctx;
949
+ ctx.sock_map = st_init_numtable();
950
+ ctx.timeout_ms = -1;
951
+
952
+ /* install socket/timer callbacks */
953
+ curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
954
+ curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETDATA, &ctx);
955
+ curl_multi_setopt(rbcm->handle, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
956
+ curl_multi_setopt(rbcm->handle, CURLMOPT_TIMERDATA, &ctx);
957
+
958
+ /* run using socket action loop with ensure-cleanup */
959
+ struct socket_drive_args body_args = { self, rbcm, &ctx, block };
960
+ struct socket_cleanup_args ensure_args = { rbcm, &ctx };
961
+ rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
962
+
963
+ /* finalize */
964
+ rb_curl_multi_read_info(self, rbcm->handle);
965
+ if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
966
+ if (cCurlMutiAutoClose == 1) rb_funcall(self, rb_intern("close"), 0);
967
+
968
+ return Qtrue;
969
+ }
970
+ #endif /* socket-action implementation */
971
+
469
972
  #ifdef _WIN32
470
973
  void create_crt_fd(fd_set *os_set, fd_set *crt_set)
471
974
  {
@@ -588,23 +1091,38 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
588
1091
  /* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
589
1092
  }
590
1093
 
591
- #ifdef HAVE_CURL_MULTI_WAIT
1094
+ #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
592
1095
  {
593
1096
  struct wait_args wait_args;
594
1097
  wait_args.handle = rbcm->handle;
595
1098
  wait_args.timeout_ms = timeout_milliseconds;
596
1099
  wait_args.numfds = 0;
1100
+ /*
1101
+ * When a Fiber scheduler is available (Ruby >= 3.x), rb_thread_fd_select
1102
+ * integrates with it. If we have rb_thread_fd_select available at build
1103
+ * time, we avoid curl_multi_wait entirely (see preprocessor guard above)
1104
+ * and use the fdset branch below. Otherwise, we use curl_multi_wait and
1105
+ * release the GVL so Ruby threads can continue to run.
1106
+ */
1107
+ CURLMcode wait_rc;
597
1108
  #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
598
- CURLMcode wait_rc = (CURLMcode)(intptr_t)
599
- rb_thread_call_without_gvl(curl_multi_wait_wrapper, &wait_args, RUBY_UBF_IO, NULL);
1109
+ wait_rc = (CURLMcode)(intptr_t)rb_thread_call_without_gvl(
1110
+ curl_multi_wait_wrapper, &wait_args, RUBY_UBF_IO, NULL
1111
+ );
600
1112
  #else
601
- CURLMcode wait_rc = curl_multi_wait(rbcm->handle, NULL, 0, timeout_milliseconds, &wait_args.numfds);
1113
+ wait_rc = curl_multi_wait(rbcm->handle, NULL, 0, timeout_milliseconds, &wait_args.numfds);
602
1114
  #endif
603
1115
  if (wait_rc != CURLM_OK) {
604
1116
  raise_curl_multi_error_exception(wait_rc);
605
1117
  }
606
1118
  if (wait_args.numfds == 0) {
1119
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1120
+ struct timeval tv_sleep = tv_100ms;
1121
+ /* Sleep in a scheduler-aware way. */
1122
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1123
+ #else
607
1124
  rb_thread_wait_for(tv_100ms);
1125
+ #endif
608
1126
  }
609
1127
  /* Process pending transfers after waiting */
610
1128
  rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
@@ -628,7 +1146,12 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
628
1146
 
629
1147
  if (maxfd == -1) {
630
1148
  /* libcurl recommends sleeping for 100ms */
1149
+ #if HAVE_RB_THREAD_FD_SELECT
1150
+ struct timeval tv_sleep = tv_100ms;
1151
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1152
+ #else
631
1153
  rb_thread_wait_for(tv_100ms);
1154
+ #endif
632
1155
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
633
1156
  rb_curl_multi_read_info( self, rbcm->handle );
634
1157
  if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
@@ -650,12 +1173,37 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
650
1173
  fdset_args.tv = &tv;
651
1174
  #endif
652
1175
 
653
- #ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
1176
+ #if HAVE_RB_THREAD_FD_SELECT
1177
+ /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
1178
+ {
1179
+ rb_fdset_t rfds, wfds, efds;
1180
+ rb_fd_init(&rfds);
1181
+ rb_fd_init(&wfds);
1182
+ rb_fd_init(&efds);
1183
+ #ifdef _WIN32
1184
+ /* On Windows, iterate explicit fd arrays for CRT fds. */
1185
+ int i;
1186
+ for (i = 0; i < crt_fdread.fd_count; i++) rb_fd_set(crt_fdread.fd_array[i], &rfds);
1187
+ for (i = 0; i < crt_fdwrite.fd_count; i++) rb_fd_set(crt_fdwrite.fd_array[i], &wfds);
1188
+ for (i = 0; i < crt_fdexcep.fd_count; i++) rb_fd_set(crt_fdexcep.fd_array[i], &efds);
1189
+ rc = rb_thread_fd_select(0, &rfds, &wfds, &efds, &tv);
1190
+ #else
1191
+ int fd;
1192
+ for (fd = 0; fd <= maxfd; fd++) {
1193
+ if (FD_ISSET(fd, &fdread)) rb_fd_set(fd, &rfds);
1194
+ if (FD_ISSET(fd, &fdwrite)) rb_fd_set(fd, &wfds);
1195
+ if (FD_ISSET(fd, &fdexcep)) rb_fd_set(fd, &efds);
1196
+ }
1197
+ rc = rb_thread_fd_select(maxfd+1, &rfds, &wfds, &efds, &tv);
1198
+ #endif
1199
+ rb_fd_term(&rfds);
1200
+ rb_fd_term(&wfds);
1201
+ rb_fd_term(&efds);
1202
+ }
1203
+ #elif defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
654
1204
  rc = (int)(VALUE) rb_thread_call_without_gvl((void *(*)(void *))curb_select, &fdset_args, RUBY_UBF_IO, 0);
655
1205
  #elif HAVE_RB_THREAD_BLOCKING_REGION
656
1206
  rc = rb_thread_blocking_region(curb_select, &fdset_args, RUBY_UBF_IO, 0);
657
- #elif HAVE_RB_THREAD_FD_SELECT
658
- rc = rb_thread_fd_select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv);
659
1207
  #else
660
1208
  rc = rb_thread_select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv);
661
1209
  #endif
@@ -679,7 +1227,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
679
1227
  if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
680
1228
  break;
681
1229
  }
682
- #endif /* HAVE_CURL_MULTI_WAIT */
1230
+ #endif /* disabled curl_multi_wait: use fdsets */
683
1231
  }
684
1232
 
685
1233
  } while( rbcm->running );
@@ -702,11 +1250,32 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
702
1250
  VALUE ruby_curl_multi_close(VALUE self) {
703
1251
  ruby_curl_multi *rbcm;
704
1252
  Data_Get_Struct(self, ruby_curl_multi, rbcm);
705
- curl_multi_cleanup(rbcm->handle);
1253
+ rb_curl_multi_detach_all(rbcm);
1254
+
1255
+ if (rbcm->handle) {
1256
+ curl_multi_cleanup(rbcm->handle);
1257
+ rbcm->handle = NULL;
1258
+ }
1259
+
706
1260
  ruby_curl_multi_init(rbcm);
707
1261
  return self;
708
1262
  }
709
1263
 
1264
+ /* GC mark: keep attached easy VALUEs alive while associated. */
1265
+ static int mark_attached_i(st_data_t key, st_data_t val, st_data_t arg) {
1266
+ VALUE easy = (VALUE)val;
1267
+ if (!NIL_P(easy)) rb_gc_mark(easy);
1268
+ return ST_CONTINUE;
1269
+ }
1270
+
1271
+ static void curl_multi_mark(void *ptr) {
1272
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
1273
+ if (!rbcm) return;
1274
+ if (rbcm->attached) {
1275
+ st_foreach(rbcm->attached, mark_attached_i, (st_data_t)0);
1276
+ }
1277
+ }
1278
+
710
1279
 
711
1280
  /* =================== INIT LIB =====================*/
712
1281
  void init_curb_multi() {
@@ -727,6 +1296,12 @@ void init_curb_multi() {
727
1296
  rb_define_method(cCurlMulti, "pipeline=", ruby_curl_multi_pipeline, 1);
728
1297
  rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
729
1298
  rb_define_method(cCurlMulti, "_remove", ruby_curl_multi_remove, 1);
1299
+ /* Prefer a socket-action based perform when supported and scheduler-aware. */
1300
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1301
+ extern VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self);
1302
+ rb_define_method(cCurlMulti, "perform", ruby_curl_multi_socket_perform, -1);
1303
+ #else
730
1304
  rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, -1);
1305
+ #endif
731
1306
  rb_define_method(cCurlMulti, "_close", ruby_curl_multi_close, 0);
732
1307
  }