curb 1.2.1 → 1.3.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.
data/ext/curb_multi.c CHANGED
@@ -27,6 +27,7 @@
27
27
  #include "curb_multi.h"
28
28
 
29
29
  #include <errno.h>
30
+ #include <fcntl.h>
30
31
  #include <stdarg.h>
31
32
 
32
33
  /*
@@ -40,6 +41,12 @@
40
41
  #define curb_debugf(...) ((void)0)
41
42
  #endif
42
43
 
44
+ #ifdef RBIMPL_ATTR_MAYBE_UNUSED
45
+ #define CURB_MAYBE_UNUSED_DECL RBIMPL_ATTR_MAYBE_UNUSED()
46
+ #else
47
+ #define CURB_MAYBE_UNUSED_DECL
48
+ #endif
49
+
43
50
  #ifdef _WIN32
44
51
  // for O_RDWR and O_BINARY
45
52
  #include <fcntl.h>
@@ -62,6 +69,8 @@ static void *curl_multi_wait_wrapper(void *p) {
62
69
 
63
70
  extern VALUE mCurl;
64
71
  static VALUE idCall;
72
+ static ID id_deferred_exception_ivar;
73
+ static ID id_deferred_exception_source_id_ivar;
65
74
 
66
75
  #ifdef RDOC_NEVER_DEFINED
67
76
  mCurl = rb_define_module("Curl");
@@ -72,6 +81,7 @@ VALUE cCurlMulti;
72
81
  static long cCurlMutiDefaulttimeout = 100; /* milliseconds */
73
82
  static char cCurlMutiAutoClose = 0;
74
83
 
84
+ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result);
75
85
  static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
76
86
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
77
87
  static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running);
@@ -79,6 +89,12 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
79
89
  static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg);
80
90
  static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm);
81
91
  static void curl_multi_mark(void *ptr);
92
+ static void ruby_curl_multi_ensure_handle(ruby_curl_multi *rbcm);
93
+ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce);
94
+ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
95
+ static VALUE ruby_curl_multi_mark_closed(VALUE self);
96
+ static VALUE ruby_curl_multi_alloc(VALUE klass);
97
+ static VALUE ruby_curl_multi_initialize(VALUE self);
82
98
 
83
99
  static VALUE callback_exception(VALUE did_raise, VALUE exception) {
84
100
  // TODO: we could have an option to enable exception reporting
@@ -101,13 +117,117 @@ static VALUE callback_exception(VALUE did_raise, VALUE exception) {
101
117
  return exception;
102
118
  }
103
119
 
120
+ static VALUE take_easy_callback_error_if_any(VALUE easy) {
121
+ ruby_curl_easy *rbce = NULL;
122
+
123
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
124
+ return Qnil;
125
+ }
126
+
127
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
128
+ return rb_curl_easy_take_callback_error(rbce);
129
+ }
130
+
131
+ static VALUE build_aborted_by_callback_exception(VALUE exception) {
132
+ VALUE message = rb_funcall(exception, rb_intern("message"), 0);
133
+ VALUE aborted_exception = rb_exc_new_str(eCurlErrAbortedByCallback, message);
134
+ VALUE backtrace = rb_funcall(exception, rb_intern("backtrace"), 0);
135
+ rb_funcall(aborted_exception, rb_intern("set_backtrace"), 1, backtrace);
136
+ return aborted_exception;
137
+ }
138
+
139
+ static void stash_multi_exception_if_unset(VALUE self, VALUE exception, VALUE source_easy) {
140
+ if (rb_ivar_defined(self, id_deferred_exception_ivar)) {
141
+ return;
142
+ }
143
+
144
+ rb_ivar_set(self, id_deferred_exception_ivar, exception);
145
+ if (!NIL_P(source_easy)) {
146
+ rb_ivar_set(self, id_deferred_exception_source_id_ivar, rb_obj_id(source_easy));
147
+ }
148
+ }
149
+
150
+ static void clear_multi_deferred_exception_source_id_if_any(VALUE self) {
151
+ if (rb_ivar_defined(self, id_deferred_exception_source_id_ivar)) {
152
+ rb_funcall(self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_deferred_exception_source_id_ivar));
153
+ }
154
+ }
155
+
156
+ static VALUE clear_multi_deferred_exception_if_any(VALUE self) {
157
+ VALUE exception = Qnil;
158
+
159
+ if (rb_ivar_defined(self, id_deferred_exception_ivar)) {
160
+ exception = rb_funcall(self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_deferred_exception_ivar));
161
+ }
162
+
163
+ return exception;
164
+ }
165
+
166
+ static void rb_curl_multi_yield_if_given(VALUE self, VALUE block) {
167
+ if (block == Qnil) {
168
+ return;
169
+ }
170
+
171
+ rb_funcall(block, idCall, 1, self);
172
+ }
173
+
174
+ static void raise_multi_deferred_exception_if_idle(VALUE self) {
175
+ ruby_curl_multi *rbcm = NULL;
176
+
177
+ if (!rb_ivar_defined(self, id_deferred_exception_ivar)) {
178
+ return;
179
+ }
180
+
181
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
182
+ if (rbcm && rbcm->active > 0) {
183
+ return;
184
+ }
185
+
186
+ VALUE exception = clear_multi_deferred_exception_if_any(self);
187
+ rb_exc_raise(exception);
188
+ }
189
+
190
+ static VALUE take_status_callback_exception_if_any(VALUE did_raise) {
191
+ VALUE exception = rb_hash_aref(did_raise, rb_easy_hkey("error"));
192
+
193
+ if (FIX2INT(rb_hash_size(did_raise)) > 0 && exception != Qnil) {
194
+ rb_hash_clear(did_raise);
195
+ return build_aborted_by_callback_exception(exception);
196
+ }
197
+
198
+ return Qnil;
199
+ }
200
+
201
+ static void raise_completion_callback_error_if_any(VALUE did_raise, VALUE easy_callback_error) {
202
+ if (!NIL_P(easy_callback_error)) {
203
+ rb_exc_raise(easy_callback_error);
204
+ }
205
+
206
+ VALUE exception = take_status_callback_exception_if_any(did_raise);
207
+ if (!NIL_P(exception)) {
208
+ rb_exc_raise(exception);
209
+ }
210
+ }
211
+
212
+ struct multi_handle_complete_args {
213
+ VALUE self;
214
+ CURL *easy_handle;
215
+ int result;
216
+ };
217
+
218
+ static VALUE rb_curl_mutli_handle_complete_protected(VALUE argp) {
219
+ struct multi_handle_complete_args *args = (struct multi_handle_complete_args *)argp;
220
+ rb_curl_mutli_handle_complete(args->self, args->easy_handle, args->result);
221
+ return Qnil;
222
+ }
223
+
104
224
  static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg) {
105
225
  ruby_curl_multi *rbcm = (ruby_curl_multi *)arg;
106
226
  VALUE easy = (VALUE)val;
107
227
  ruby_curl_easy *rbce = NULL;
108
228
 
109
229
  if (RB_TYPE_P(easy, T_DATA)) {
110
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
230
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
111
231
  }
112
232
 
113
233
  if (!rbce) {
@@ -150,7 +270,34 @@ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
150
270
  rbcm->running = 0;
151
271
  }
152
272
 
153
- void curl_multi_free(ruby_curl_multi *rbcm) {
273
+ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce) {
274
+ st_data_t value = 0;
275
+
276
+ if (!rbcm || !rbce || !rbcm->attached) {
277
+ return 0;
278
+ }
279
+
280
+ return st_lookup(rbcm->attached, (st_data_t)rbce, &value);
281
+ }
282
+
283
+ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
284
+ VALUE requests;
285
+
286
+ if (NIL_P(self) || NIL_P(easy)) {
287
+ return;
288
+ }
289
+
290
+ requests = rb_funcall(self, rb_intern("requests"), 0);
291
+ if (!RB_TYPE_P(requests, T_HASH)) {
292
+ return;
293
+ }
294
+
295
+ rb_hash_delete(requests, rb_obj_id(easy));
296
+ }
297
+
298
+ /* TypedData-compatible free function */
299
+ static void curl_multi_free(void *ptr) {
300
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
154
301
  if (!rbcm) {
155
302
  return;
156
303
  }
@@ -165,6 +312,27 @@ void curl_multi_free(ruby_curl_multi *rbcm) {
165
312
  free(rbcm);
166
313
  }
167
314
 
315
+ static size_t curl_multi_memsize(const void *ptr) {
316
+ (void)ptr;
317
+ return sizeof(ruby_curl_multi);
318
+ }
319
+
320
+ const rb_data_type_t ruby_curl_multi_data_type = {
321
+ "Curl::Multi",
322
+ {
323
+ curl_multi_mark,
324
+ curl_multi_free,
325
+ curl_multi_memsize,
326
+ #ifdef RUBY_TYPED_FREE_IMMEDIATELY
327
+ NULL, /* compact */
328
+ #endif
329
+ },
330
+ #ifdef RUBY_TYPED_FREE_IMMEDIATELY
331
+ NULL, NULL, /* parent, data */
332
+ RUBY_TYPED_FREE_IMMEDIATELY
333
+ #endif
334
+ };
335
+
168
336
  static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
169
337
  rbcm->handle = curl_multi_init();
170
338
  if (!rbcm->handle) {
@@ -173,6 +341,7 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
173
341
 
174
342
  rbcm->active = 0;
175
343
  rbcm->running = 0;
344
+ rbcm->closed = 0;
176
345
 
177
346
  if (rbcm->attached) {
178
347
  st_free_table(rbcm->attached);
@@ -187,20 +356,36 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
187
356
  }
188
357
  }
189
358
 
359
+ static void ruby_curl_multi_ensure_handle(ruby_curl_multi *rbcm) {
360
+ if (!rbcm->handle) {
361
+ if (rbcm->closed) {
362
+ raise_curl_multi_error_exception(CURLM_BAD_HANDLE);
363
+ }
364
+ ruby_curl_multi_init(rbcm);
365
+ }
366
+ }
367
+
190
368
  /*
191
369
  * call-seq:
192
370
  * Curl::Multi.new => #<Curl::Easy...>
193
371
  *
194
372
  * Create a new Curl::Multi instance
195
373
  */
196
- VALUE ruby_curl_multi_new(VALUE klass) {
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
- }
374
+ static VALUE ruby_curl_multi_alloc(VALUE klass) {
375
+ VALUE self;
376
+ ruby_curl_multi *rbcm;
201
377
 
378
+ self = TypedData_Make_Struct(klass, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
202
379
  MEMZERO(rbcm, ruby_curl_multi, 1);
203
380
 
381
+ return self;
382
+ }
383
+
384
+ static VALUE ruby_curl_multi_initialize(VALUE self) {
385
+ ruby_curl_multi *rbcm;
386
+
387
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
388
+
204
389
  ruby_curl_multi_init(rbcm);
205
390
 
206
391
  /*
@@ -209,7 +394,7 @@ VALUE ruby_curl_multi_new(VALUE klass) {
209
394
  * identify these objects using rb_gc_mark(value). If the structure doesn't reference
210
395
  * other Ruby objects, you can simply pass 0 as a function pointer.
211
396
  */
212
- return Data_Wrap_Struct(klass, curl_multi_mark, curl_multi_free, rbcm);
397
+ return self;
213
398
  }
214
399
 
215
400
  /*
@@ -277,7 +462,8 @@ static VALUE ruby_curl_multi_max_connects(VALUE self, VALUE count) {
277
462
  #ifdef HAVE_CURLMOPT_MAXCONNECTS
278
463
  ruby_curl_multi *rbcm;
279
464
 
280
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
465
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
466
+ ruby_curl_multi_ensure_handle(rbcm);
281
467
 
282
468
  curl_multi_setopt(rbcm->handle, CURLMOPT_MAXCONNECTS, NUM2LONG(count));
283
469
  #endif
@@ -296,7 +482,8 @@ static VALUE ruby_curl_multi_max_host_connections(VALUE self, VALUE count) {
296
482
  #ifdef HAVE_CURLMOPT_MAX_HOST_CONNECTIONS
297
483
  ruby_curl_multi *rbcm;
298
484
 
299
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
485
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
486
+ ruby_curl_multi_ensure_handle(rbcm);
300
487
 
301
488
  curl_multi_setopt(rbcm->handle, CURLMOPT_MAX_HOST_CONNECTIONS, NUM2LONG(count));
302
489
  #endif
@@ -330,7 +517,8 @@ static VALUE ruby_curl_multi_pipeline(VALUE self, VALUE method) {
330
517
  value = NUM2LONG(method);
331
518
  }
332
519
 
333
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
520
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
521
+ ruby_curl_multi_ensure_handle(rbcm);
334
522
  curl_multi_setopt(rbcm->handle, CURLMOPT_PIPELINING, value);
335
523
  #endif
336
524
  return method == Qtrue ? 1 : 0;
@@ -350,8 +538,9 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
350
538
  ruby_curl_easy *rbce;
351
539
  ruby_curl_multi *rbcm;
352
540
 
353
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
354
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
541
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
542
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
543
+ ruby_curl_multi_ensure_handle(rbcm);
355
544
 
356
545
  /* setup the easy handle */
357
546
  ruby_curl_easy_setup( rbce );
@@ -378,6 +567,7 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
378
567
  }
379
568
  }
380
569
 
570
+ rbce->multi_attachment_generation++;
381
571
  st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
382
572
 
383
573
  /* track a reference to associated multi handle */
@@ -403,7 +593,7 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
403
593
  VALUE ruby_curl_multi_remove(VALUE self, VALUE rb_easy_handle) {
404
594
  ruby_curl_multi *rbcm;
405
595
 
406
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
596
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
407
597
 
408
598
  rb_curl_multi_remove(rbcm, rb_easy_handle);
409
599
 
@@ -414,7 +604,7 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
414
604
  CURLMcode result;
415
605
  ruby_curl_easy *rbce;
416
606
 
417
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
607
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
418
608
  result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
419
609
  if (result != 0) {
420
610
  raise_curl_multi_error_exception(result);
@@ -424,6 +614,7 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
424
614
  rbcm->active--;
425
615
  }
426
616
 
617
+ rbce->multi = Qnil;
427
618
  ruby_curl_easy_cleanup( easy, rbce );
428
619
 
429
620
  rb_curl_multi_forget_easy(rbcm, rbce);
@@ -454,101 +645,311 @@ static void flush_stderr_if_any(ruby_curl_easy *rbce) {
454
645
  }
455
646
  }
456
647
 
457
- static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
458
- long response_code = -1;
459
- VALUE easy;
648
+ /* Helper to locate the Ruby Easy VALUE from the attached table using the
649
+ * underlying CURL* handle when CURLINFO_PRIVATE is unavailable or stale. */
650
+ struct find_easy_ctx { CURL *handle; VALUE easy; };
651
+ static int find_easy_by_handle_i(st_data_t key, st_data_t val, st_data_t arg) {
652
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
653
+ struct find_easy_ctx *ctx = (struct find_easy_ctx *)arg;
654
+ if (rbce && rbce->curl == ctx->handle) {
655
+ ctx->easy = (VALUE)val;
656
+ return ST_STOP;
657
+ }
658
+ return ST_CONTINUE;
659
+ }
660
+
661
+ static VALUE find_easy_by_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
662
+ if (!rbcm || !rbcm->attached) return Qnil;
663
+ struct find_easy_ctx ctx; ctx.handle = easy_handle; ctx.easy = Qnil;
664
+ st_foreach(rbcm->attached, find_easy_by_handle_i, (st_data_t)&ctx);
665
+ return ctx.easy;
666
+ }
667
+
668
+ static VALUE find_easy_value_for_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
669
+ VALUE easy = Qnil;
460
670
  ruby_curl_easy *rbce = NULL;
461
- VALUE callargs;
462
671
 
463
- CURLcode ecode = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char**)&easy);
672
+ CURLcode private_rc = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char **)&rbce);
673
+ if (private_rc == CURLE_OK && rbce) {
674
+ easy = rbce->self;
675
+ }
464
676
 
465
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
677
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
678
+ easy = find_easy_by_handle(rbcm, easy_handle);
679
+ }
466
680
 
467
- rbce->last_result = result; /* save the last easy result code */
681
+ return easy;
682
+ }
468
683
 
469
- /* Ensure any verbose output redirected via CURLOPT_STDERR is flushed
470
- * before we tear down handler state. */
471
- flush_stderr_if_any(rbce);
684
+ struct multi_complete_callback_args {
685
+ VALUE self;
686
+ VALUE easy;
687
+ ruby_curl_multi *rbcm;
688
+ ruby_curl_easy *rbce;
689
+ int result;
690
+ st_table *attached_snapshot;
691
+ };
472
692
 
473
- // remove the easy handle from multi on completion so it can be reused again
474
- rb_funcall(self, rb_intern("remove"), 1, easy);
693
+ static int snapshot_attached_easy_i(st_data_t key, st_data_t val, st_data_t arg) {
694
+ st_table *snapshot = (st_table *)arg;
695
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
475
696
 
476
- /* after running a request cleanup the headers, these are set before each request */
477
- if (rbce->curl_headers) {
478
- curl_slist_free_all(rbce->curl_headers);
479
- rbce->curl_headers = NULL;
697
+ if (!rbce) {
698
+ return ST_CONTINUE;
480
699
  }
481
700
 
482
- /* Flush again after removal to cover any last buffered data. */
483
- flush_stderr_if_any(rbce);
701
+ st_insert(snapshot, key, (st_data_t)rbce->multi_attachment_generation);
702
+ return ST_CONTINUE;
703
+ }
704
+
705
+ static st_table *capture_attached_easy_snapshot(ruby_curl_multi *rbcm) {
706
+ st_table *snapshot = st_init_numtable();
707
+
708
+ if (!snapshot) {
709
+ rb_raise(rb_eNoMemError, "Failed to allocate multi callback snapshot table");
710
+ }
711
+
712
+ if (rbcm && rbcm->attached) {
713
+ st_foreach(rbcm->attached, snapshot_attached_easy_i, (st_data_t)snapshot);
714
+ }
715
+
716
+ return snapshot;
717
+ }
718
+
719
+ struct collect_new_attached_easies_ctx {
720
+ st_table *snapshot;
721
+ VALUE easies;
722
+ };
723
+
724
+ static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t arg) {
725
+ st_data_t existing = 0;
726
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
727
+ struct collect_new_attached_easies_ctx *ctx = (struct collect_new_attached_easies_ctx *)arg;
728
+
729
+ if (!rbce) {
730
+ return ST_CONTINUE;
731
+ }
732
+
733
+ if (!ctx->snapshot || !st_lookup(ctx->snapshot, key, &existing) ||
734
+ existing != (st_data_t)rbce->multi_attachment_generation) {
735
+ rb_ary_push(ctx->easies, (VALUE)val);
736
+ }
737
+
738
+ return ST_CONTINUE;
739
+ }
740
+
741
+ static void rb_curl_multi_remove_added_easies_since_snapshot(VALUE self, ruby_curl_multi *rbcm, st_table *snapshot) {
742
+ struct collect_new_attached_easies_ctx ctx;
743
+ VALUE easies = rb_ary_new();
744
+ long index;
745
+
746
+ if (!rbcm || !snapshot || !rbcm->attached) {
747
+ return;
748
+ }
749
+
750
+ ctx.snapshot = snapshot;
751
+ ctx.easies = easies;
752
+ st_foreach(rbcm->attached, collect_new_attached_easies_i, (st_data_t)&ctx);
753
+
754
+ for (index = 0; index < RARRAY_LEN(easies); index++) {
755
+ VALUE easy = rb_ary_entry(easies, index);
756
+ ruby_curl_easy *rbce = NULL;
757
+
758
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
759
+ continue;
760
+ }
761
+
762
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
763
+ if (!rbce || rbce->multi != self || !rb_curl_multi_has_easy(rbcm, rbce)) {
764
+ continue;
765
+ }
484
766
 
485
- if (ecode != 0) {
486
- raise_curl_easy_error_exception(ecode);
767
+ rb_curl_multi_remove_request_reference(self, easy);
768
+ rb_curl_multi_remove(rbcm, easy);
487
769
  }
488
770
 
771
+ RB_GC_GUARD(easies);
772
+ }
773
+
774
+ static void stash_and_raise_status_callback_error_if_unmasked(struct multi_complete_callback_args *args, VALUE did_raise, VALUE easy_callback_error) {
775
+ VALUE exception;
776
+
777
+ if (!NIL_P(easy_callback_error)) {
778
+ return;
779
+ }
780
+
781
+ exception = take_status_callback_exception_if_any(did_raise);
782
+ if (NIL_P(exception)) {
783
+ return;
784
+ }
785
+
786
+ stash_multi_exception_if_unset(args->self, exception, args->easy);
787
+ rb_curl_multi_remove_added_easies_since_snapshot(args->self, args->rbcm, args->attached_snapshot);
788
+ rb_exc_raise(exception);
789
+ }
790
+
791
+ static VALUE rb_curl_multi_run_completion_callbacks(VALUE argp) {
792
+ struct multi_complete_callback_args *args = (struct multi_complete_callback_args *)argp;
793
+ ruby_curl_easy *rbce = args->rbce;
794
+ long response_code = -1;
795
+ VALUE easy_callback_error = Qnil;
796
+ VALUE callargs;
489
797
  VALUE did_raise = rb_hash_new();
798
+ long redirect_count;
799
+
800
+ easy_callback_error = take_easy_callback_error_if_any(args->easy);
801
+ if (!NIL_P(easy_callback_error)) {
802
+ stash_multi_exception_if_unset(args->self, easy_callback_error, args->easy);
803
+ }
490
804
 
491
805
  if (!rb_easy_nil("complete_proc")) {
492
- callargs = rb_ary_new3(2, rb_easy_get("complete_proc"), easy);
493
- rbce->callback_active = 1;
806
+ callargs = rb_ary_new3(2, rb_easy_get("complete_proc"), args->easy);
807
+ args->rbce->callback_active = 1;
494
808
  rb_rescue(call_status_handler1, callargs, callback_exception, did_raise);
495
- rbce->callback_active = 0;
496
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
809
+ args->rbce->callback_active = 0;
810
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
497
811
  }
498
812
 
499
813
  #ifdef HAVE_CURLINFO_RESPONSE_CODE
500
- curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
814
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
501
815
  #else /* use fdsets path for waiting */
502
816
  // old libcurl
503
- curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CODE, &response_code);
817
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_HTTP_CODE, &response_code);
504
818
  #endif
505
- long redirect_count;
506
- curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
819
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
507
820
 
508
- if (result != 0) {
821
+ if (args->result != 0) {
509
822
  if (!rb_easy_nil("failure_proc")) {
510
- callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), easy, rb_curl_easy_error(result));
511
- rbce->callback_active = 1;
823
+ callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), args->easy, rb_curl_easy_error(args->result));
824
+ args->rbce->callback_active = 1;
512
825
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
513
- rbce->callback_active = 0;
514
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
826
+ args->rbce->callback_active = 0;
827
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
515
828
  }
516
829
  } else if (!rb_easy_nil("success_proc") &&
517
830
  ((response_code >= 200 && response_code < 300) || response_code == 0)) {
518
831
  /* NOTE: we allow response_code == 0, in the case of non http requests e.g. reading from disk */
519
- callargs = rb_ary_new3(2, rb_easy_get("success_proc"), easy);
520
- rbce->callback_active = 1;
832
+ callargs = rb_ary_new3(2, rb_easy_get("success_proc"), args->easy);
833
+ args->rbce->callback_active = 1;
521
834
  rb_rescue(call_status_handler1, callargs, callback_exception, did_raise);
522
- rbce->callback_active = 0;
523
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
835
+ args->rbce->callback_active = 0;
836
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
524
837
 
525
838
  } else if (!rb_easy_nil("redirect_proc") && ((response_code >= 300 && response_code < 400) || redirect_count > 0) ) {
526
839
  /* Skip on_redirect callback if follow_location is false AND max_redirects is 0 */
527
- if (!rbce->follow_location && rbce->max_redirs == 0) {
840
+ if (!args->rbce->follow_location && args->rbce->max_redirs == 0) {
528
841
  // Do nothing - skip the callback
529
842
  } 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;
843
+ args->rbce->callback_active = 1;
844
+ callargs = rb_ary_new3(3, rb_easy_get("redirect_proc"), args->easy, rb_curl_easy_error(args->result));
533
845
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
534
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
846
+ args->rbce->callback_active = 0;
847
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
535
848
  }
536
849
  } else if (!rb_easy_nil("missing_proc") &&
537
850
  (response_code >= 400 && response_code < 500)) {
538
- rbce->callback_active = 1;
539
- callargs = rb_ary_new3(3, rb_easy_get("missing_proc"), easy, rb_curl_easy_error(result));
540
- rbce->callback_active = 0;
851
+ args->rbce->callback_active = 1;
852
+ callargs = rb_ary_new3(3, rb_easy_get("missing_proc"), args->easy, rb_curl_easy_error(args->result));
541
853
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
542
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
854
+ args->rbce->callback_active = 0;
855
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
543
856
  } else if (!rb_easy_nil("failure_proc") &&
544
857
  (response_code >= 500 && response_code <= 999)) {
545
- callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), easy, rb_curl_easy_error(result));
546
- rbce->callback_active = 1;
858
+ callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), args->easy, rb_curl_easy_error(args->result));
859
+ args->rbce->callback_active = 1;
547
860
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
548
- rbce->callback_active = 0;
549
- CURB_CHECK_RB_CALLBACK_RAISE(did_raise);
861
+ args->rbce->callback_active = 0;
862
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
550
863
  }
551
864
 
865
+ raise_completion_callback_error_if_any(did_raise, easy_callback_error);
866
+ return Qnil;
867
+ }
868
+
869
+ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
870
+ struct multi_complete_callback_args *args = (struct multi_complete_callback_args *)argp;
871
+
872
+ if (!args->rbce) {
873
+ return Qnil;
874
+ }
875
+
876
+ if (args->rbce->callback_active) {
877
+ args->rbce->callback_active = 0;
878
+ }
879
+
880
+ if (args->rbce->multi == args->self && !rb_curl_multi_has_easy(args->rbcm, args->rbce)) {
881
+ args->rbce->multi = Qnil;
882
+ }
883
+
884
+ if (args->attached_snapshot) {
885
+ st_free_table(args->attached_snapshot);
886
+ args->attached_snapshot = NULL;
887
+ }
888
+
889
+ return Qnil;
890
+ }
891
+
892
+ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
893
+ VALUE easy = Qnil;
894
+ ruby_curl_easy *rbce = NULL;
895
+ ruby_curl_multi *rbcm = NULL;
896
+ CURLMcode mcode;
897
+
898
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
899
+
900
+ easy = find_easy_value_for_handle(rbcm, easy_handle);
901
+ if (!NIL_P(easy) && RB_TYPE_P(easy, T_DATA)) {
902
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
903
+ }
904
+
905
+ /* If we still cannot identify the easy handle, remove it and bail. */
906
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA) || !rbce) {
907
+ if (rbcm && rbcm->handle && easy_handle) {
908
+ curl_multi_remove_handle(rbcm->handle, easy_handle);
909
+ }
910
+ return;
911
+ }
912
+
913
+ rbce->last_result = result; /* save the last easy result code */
914
+
915
+ /* Ensure any verbose output redirected via CURLOPT_STDERR is flushed
916
+ * before we tear down handler state. */
917
+ flush_stderr_if_any(rbce);
918
+
919
+ /* Detach the easy from libcurl and the Ruby requests table before running
920
+ * status callbacks, but preserve easy.multi until the callbacks finish. */
921
+ mcode = curl_multi_remove_handle(rbcm->handle, rbce->curl);
922
+ if (mcode != CURLM_OK) {
923
+ raise_curl_multi_error_exception(mcode);
924
+ }
925
+
926
+ if (rbcm->active > 0) {
927
+ rbcm->active--;
928
+ }
929
+
930
+ rb_curl_multi_remove_request_reference(self, easy);
931
+ rb_curl_multi_forget_easy(rbcm, rbce);
932
+ ruby_curl_easy_cleanup(easy, rbce);
933
+
934
+ /* after running a request cleanup the headers, these are set before each request */
935
+ if (rbce->curl_headers) {
936
+ curl_slist_free_all(rbce->curl_headers);
937
+ rbce->curl_headers = NULL;
938
+ }
939
+
940
+ /* Flush again after removal to cover any last buffered data. */
941
+ flush_stderr_if_any(rbce);
942
+
943
+ struct multi_complete_callback_args args = {
944
+ self,
945
+ easy,
946
+ rbcm,
947
+ rbce,
948
+ result,
949
+ capture_attached_easy_snapshot(rbcm)
950
+ };
951
+ rb_ensure(rb_curl_multi_run_completion_callbacks, (VALUE)&args,
952
+ rb_curl_multi_finish_completion_callbacks, (VALUE)&args);
552
953
  }
553
954
 
554
955
  static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
@@ -557,6 +958,9 @@ static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
557
958
  CURLcode c_easy_result;
558
959
  CURLMsg *c_multi_result; // for picking up messages with the transfer status
559
960
  CURL *c_easy_handle;
961
+ ruby_curl_multi *rbcm = NULL;
962
+
963
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
560
964
 
561
965
  /* Check for finished easy handles and remove from the multi handle.
562
966
  * curl_multi_info_read will query for messages from individual handles.
@@ -572,8 +976,16 @@ static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
572
976
  c_easy_handle = c_multi_result->easy_handle;
573
977
  c_easy_result = c_multi_result->data.result; /* return code for transfer */
574
978
 
575
- rb_curl_mutli_handle_complete(self, c_easy_handle, c_easy_result);
979
+ struct multi_handle_complete_args args = { self, c_easy_handle, c_easy_result };
980
+ int state = 0;
981
+ rb_protect(rb_curl_mutli_handle_complete_protected, (VALUE)&args, &state);
982
+ if (state) {
983
+ stash_multi_exception_if_unset(self, rb_errinfo(), find_easy_value_for_handle(rbcm, c_easy_handle));
984
+ rb_set_errinfo(Qnil);
985
+ }
576
986
  }
987
+
988
+ raise_multi_deferred_exception_if_idle(self);
577
989
  }
578
990
 
579
991
  /* called within ruby_curl_multi_perform */
@@ -613,6 +1025,7 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
613
1025
  typedef struct {
614
1026
  st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
615
1027
  long timeout_ms; /* last timeout set by libcurl timer callback */
1028
+ VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
616
1029
  } multi_socket_ctx;
617
1030
 
618
1031
  #if CURB_SOCKET_DEBUG
@@ -650,12 +1063,30 @@ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
650
1063
  #define cselect_flags_str(...) ""
651
1064
  #endif
652
1065
 
1066
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
653
1067
  /* 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; };
1068
+ struct fiber_io_wait_args { VALUE scheduler; VALUE io; VALUE events; VALUE timeout; };
655
1069
  static VALUE fiber_io_wait_protected(VALUE argp) {
656
1070
  struct fiber_io_wait_args *a = (struct fiber_io_wait_args *)argp;
657
1071
  return rb_fiber_scheduler_io_wait(a->scheduler, a->io, a->events, a->timeout);
658
1072
  }
1073
+ #endif
1074
+
1075
+ static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
1076
+ st_data_t key = (st_data_t)fd;
1077
+ st_data_t rec;
1078
+
1079
+ if (!ctx) return;
1080
+ if (ctx->sock_map) st_delete(ctx->sock_map, &key, &rec);
1081
+ if (!NIL_P(ctx->io_cache)) rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1082
+ }
1083
+
1084
+ static int multi_socket_fd_valid_p(int fd) {
1085
+ if (fd < 0) return 0;
1086
+
1087
+ errno = 0;
1088
+ return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
1089
+ }
659
1090
 
660
1091
  static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp) {
661
1092
  multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
@@ -663,22 +1094,25 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
663
1094
  int fd = (int)s;
664
1095
 
665
1096
  if (!ctx || !ctx->sock_map) return 0;
1097
+ if (fd < 0) return 0;
666
1098
 
667
1099
  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);
1100
+ multi_socket_forget_fd(ctx, fd);
1101
+ #if CURB_SOCKET_DEBUG
671
1102
  {
672
1103
  char b[16];
673
1104
  curb_debugf("[curb.socket] sock_cb fd=%d what=%s (removed)", fd, poll_what_str(what, b, sizeof(b)));
674
1105
  }
1106
+ #endif
675
1107
  } else {
676
1108
  /* store current interest mask for this fd */
677
1109
  st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1110
+ #if CURB_SOCKET_DEBUG
678
1111
  {
679
1112
  char b[16];
680
1113
  curb_debugf("[curb.socket] sock_cb fd=%d what=%s (tracked)", fd, poll_what_str(what, b, sizeof(b)));
681
1114
  }
1115
+ #endif
682
1116
  }
683
1117
  return 0;
684
1118
  }
@@ -702,6 +1136,7 @@ static int rb_fdset_from_sockmap_i(st_data_t key, st_data_t val, st_data_t argp)
702
1136
  if (fd > a->maxfd) a->maxfd = fd;
703
1137
  return ST_CONTINUE;
704
1138
  }
1139
+ CURB_MAYBE_UNUSED_DECL
705
1140
  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
1141
  if (!map) { *maxfd_out = -1; return; }
707
1142
  struct build_fdset_args a; a.r = rfds; a.w = wfds; a.e = efds; a.maxfd = -1;
@@ -742,13 +1177,30 @@ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
742
1177
  return ST_CONTINUE;
743
1178
  }
744
1179
 
1180
+ static VALUE multi_socket_io_for_fd(multi_socket_ctx *ctx, int fd) {
1181
+ VALUE key = INT2NUM(fd);
1182
+ VALUE io = rb_hash_aref(ctx->io_cache, key);
1183
+ if (NIL_P(io)) {
1184
+ io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, key, rb_str_new_cstr("r+"));
1185
+ rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
1186
+ rb_hash_aset(ctx->io_cache, key, io);
1187
+ }
1188
+ return io;
1189
+ }
1190
+
1191
+ struct io_for_fd_args { multi_socket_ctx *ctx; int fd; };
1192
+ static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1193
+ struct io_for_fd_args *a = (struct io_for_fd_args *)argp;
1194
+ return multi_socket_io_for_fd(a->ctx, a->fd);
1195
+ }
1196
+
745
1197
  static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
746
1198
  /* prime the state: let libcurl act on timeouts to setup sockets */
747
1199
  CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
748
1200
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
749
1201
  curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
750
1202
  rb_curl_multi_read_info(self, rbcm->handle);
751
- if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
1203
+ rb_curl_multi_yield_if_given(self, block);
752
1204
 
753
1205
  while (rbcm->running) {
754
1206
  struct timeval tv = {0, 0};
@@ -791,9 +1243,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
791
1243
  rb_fdset_t rfds, wfds, efds;
792
1244
  rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
793
1245
  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;
1246
+ rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
797
1247
  int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
798
1248
  curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
799
1249
  if (rc < 0) {
@@ -814,25 +1264,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
814
1264
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
815
1265
  handled_wait = 1;
816
1266
  } 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
1267
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
835
- if (!handled_wait) {
1268
+ {
836
1269
  VALUE scheduler = rb_fiber_scheduler_current();
837
1270
  if (scheduler != Qnil) {
838
1271
  int events = 0;
@@ -845,25 +1278,58 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
845
1278
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
846
1279
  VALUE timeout = rb_float_new(timeout_s);
847
1280
  if (wait_fd < 0) {
1281
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1282
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1283
+ #else
848
1284
  rb_thread_wait_for(tv);
1285
+ #endif
1286
+ did_timeout = 1;
1287
+ } else if (!multi_socket_fd_valid_p(wait_fd)) {
1288
+ multi_socket_forget_fd(ctx, wait_fd);
849
1289
  did_timeout = 1;
850
1290
  } 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;
1291
+ struct io_for_fd_args io_args = { ctx, wait_fd };
1292
+ int io_state = 0;
1293
+ VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1294
+ if (io_state || NIL_P(io)) {
1295
+ if (io_state) rb_set_errinfo(Qnil);
1296
+ multi_socket_forget_fd(ctx, wait_fd);
1297
+ did_timeout = 1;
1298
+ any_ready = 0;
859
1299
  } else {
860
- any_ready = (ready != Qfalse);
861
- did_timeout = !any_ready;
1300
+ struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
1301
+ int state = 0;
1302
+ VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
1303
+ if (state) {
1304
+ rb_set_errinfo(Qnil);
1305
+ did_timeout = 1;
1306
+ any_ready = 0;
1307
+ } else {
1308
+ any_ready = (ready != Qfalse);
1309
+ did_timeout = !any_ready;
1310
+ }
862
1311
  }
863
1312
  }
864
1313
  handled_wait = 1;
865
1314
  }
866
1315
  }
1316
+ #endif
1317
+ #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
1318
+ if (!handled_wait && wait_fd >= 0) {
1319
+ int ev = 0;
1320
+ if (wait_what == CURL_POLL_IN) ev = RB_WAITFD_IN;
1321
+ else if (wait_what == CURL_POLL_OUT) ev = RB_WAITFD_OUT;
1322
+ else if (wait_what == CURL_POLL_INOUT) ev = RB_WAITFD_IN|RB_WAITFD_OUT;
1323
+ int rc = rb_wait_for_single_fd(wait_fd, ev, &tv);
1324
+ curb_debugf("[curb.socket] rb_wait_for_single_fd rc=%d fd=%d ev=%d", rc, wait_fd, ev);
1325
+ if (rc < 0) {
1326
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "wait_for_single_fd(): %s", strerror(errno));
1327
+ continue;
1328
+ }
1329
+ any_ready = (rc != 0);
1330
+ did_timeout = (rc == 0);
1331
+ handled_wait = 1;
1332
+ }
867
1333
  #endif
868
1334
  if (!handled_wait) {
869
1335
  /* Fallback: single-fd select. */
@@ -888,7 +1354,11 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
888
1354
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
889
1355
  }
890
1356
  } else { /* count_tracked == 0 */
1357
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1358
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1359
+ #else
891
1360
  rb_thread_wait_for(tv);
1361
+ #endif
892
1362
  did_timeout = 1;
893
1363
  }
894
1364
 
@@ -902,8 +1372,12 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
902
1372
  if (wait_what == CURL_POLL_IN || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
903
1373
  if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
904
1374
  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)));
1375
+ #if CURB_SOCKET_DEBUG
1376
+ {
1377
+ char b[32];
1378
+ curb_debugf("[curb.socket] socket_action fd=%d flags=%s", wait_fd, cselect_flags_str(flags, b, sizeof(b)));
1379
+ }
1380
+ #endif
907
1381
  mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)wait_fd, flags, &rbcm->running);
908
1382
  curb_debugf("[curb.socket] socket_action -> mrc=%d running=%d", mrc, rbcm->running);
909
1383
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
@@ -912,7 +1386,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
912
1386
 
913
1387
  rb_curl_multi_read_info(self, rbcm->handle);
914
1388
  curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
915
- if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
1389
+ rb_curl_multi_yield_if_given(self, block);
916
1390
  }
917
1391
  }
918
1392
 
@@ -935,6 +1409,9 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
935
1409
  st_free_table(c->ctx->sock_map);
936
1410
  c->ctx->sock_map = NULL;
937
1411
  }
1412
+ if (c->ctx) {
1413
+ c->ctx->io_cache = Qnil;
1414
+ }
938
1415
  return Qnil;
939
1416
  }
940
1417
 
@@ -943,11 +1420,16 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
943
1420
  VALUE block = Qnil;
944
1421
  rb_scan_args(argc, argv, "0&", &block);
945
1422
 
946
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
1423
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1424
+ ruby_curl_multi_ensure_handle(rbcm);
1425
+ if (!rb_ivar_defined(self, id_deferred_exception_ivar)) {
1426
+ clear_multi_deferred_exception_source_id_if_any(self);
1427
+ }
947
1428
 
948
1429
  multi_socket_ctx ctx;
949
1430
  ctx.sock_map = st_init_numtable();
950
1431
  ctx.timeout_ms = -1;
1432
+ ctx.io_cache = rb_hash_new();
951
1433
 
952
1434
  /* install socket/timer callbacks */
953
1435
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -962,8 +1444,8 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
962
1444
 
963
1445
  /* finalize */
964
1446
  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);
1447
+ rb_curl_multi_yield_if_given(self, block);
1448
+ if (cCurlMutiAutoClose == 1) rb_funcall(self, rb_intern("_autoclose"), 0);
967
1449
 
968
1450
  return Qtrue;
969
1451
  }
@@ -998,7 +1480,8 @@ void cleanup_crt_fd(fd_set *os_set, fd_set *crt_set)
998
1480
  }
999
1481
  #endif
1000
1482
 
1001
- #if defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
1483
+ /* curb_select is only needed when rb_thread_fd_select is NOT available */
1484
+ #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1002
1485
  struct _select_set {
1003
1486
  int maxfd;
1004
1487
  fd_set *fdread, *fdwrite, *fdexcep;
@@ -1039,13 +1522,17 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1039
1522
  struct timeval tv = {0, 0};
1040
1523
  struct timeval tv_100ms = {0, 100000};
1041
1524
  VALUE block = Qnil;
1042
- #if defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
1525
+ #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1043
1526
  struct _select_set fdset_args;
1044
1527
  #endif
1045
1528
 
1046
1529
  rb_scan_args(argc, argv, "0&", &block);
1047
1530
 
1048
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
1531
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1532
+ ruby_curl_multi_ensure_handle(rbcm);
1533
+ if (!rb_ivar_defined(self, id_deferred_exception_ivar)) {
1534
+ clear_multi_deferred_exception_source_id_if_any(self);
1535
+ }
1049
1536
 
1050
1537
  timeout_milliseconds = cCurlMutiDefaulttimeout;
1051
1538
 
@@ -1062,9 +1549,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1062
1549
  // There are no more messages to handle by curl and we can run the ruby block
1063
1550
  // passed to perform method.
1064
1551
  // When the block completes curl will resume.
1065
- if (block != Qnil) {
1066
- rb_funcall(block, rb_intern("call"), 1, self);
1067
- }
1552
+ rb_curl_multi_yield_if_given(self, block);
1068
1553
 
1069
1554
  do {
1070
1555
  while (rbcm->running) {
@@ -1082,7 +1567,12 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1082
1567
  if (timeout_milliseconds == 0) { /* no delay */
1083
1568
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1084
1569
  rb_curl_multi_read_info( self, rbcm->handle );
1085
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1570
+ rb_curl_multi_yield_if_given(self, block);
1571
+ #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1572
+ if (rb_fiber_scheduler_current() != Qnil) {
1573
+ rb_thread_schedule();
1574
+ }
1575
+ #endif
1086
1576
  continue;
1087
1577
  }
1088
1578
 
@@ -1127,7 +1617,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1127
1617
  /* Process pending transfers after waiting */
1128
1618
  rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
1129
1619
  rb_curl_multi_read_info(self, rbcm->handle);
1130
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1620
+ rb_curl_multi_yield_if_given(self, block);
1131
1621
  }
1132
1622
  #else
1133
1623
 
@@ -1146,7 +1636,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1146
1636
 
1147
1637
  if (maxfd == -1) {
1148
1638
  /* libcurl recommends sleeping for 100ms */
1149
- #if HAVE_RB_THREAD_FD_SELECT
1639
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1150
1640
  struct timeval tv_sleep = tv_100ms;
1151
1641
  rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1152
1642
  #else
@@ -1154,7 +1644,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1154
1644
  #endif
1155
1645
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1156
1646
  rb_curl_multi_read_info( self, rbcm->handle );
1157
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1647
+ rb_curl_multi_yield_if_given(self, block);
1158
1648
  continue;
1159
1649
  }
1160
1650
 
@@ -1165,7 +1655,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1165
1655
  #endif
1166
1656
 
1167
1657
 
1168
- #if (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1658
+ #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
1169
1659
  fdset_args.maxfd = maxfd+1;
1170
1660
  fdset_args.fdread = &fdread;
1171
1661
  fdset_args.fdwrite = &fdwrite;
@@ -1173,7 +1663,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1173
1663
  fdset_args.tv = &tv;
1174
1664
  #endif
1175
1665
 
1176
- #if HAVE_RB_THREAD_FD_SELECT
1666
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1177
1667
  /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
1178
1668
  {
1179
1669
  rb_fdset_t rfds, wfds, efds;
@@ -1224,7 +1714,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1224
1714
  default: /* action */
1225
1715
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1226
1716
  rb_curl_multi_read_info( self, rbcm->handle );
1227
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1717
+ rb_curl_multi_yield_if_given(self, block);
1228
1718
  break;
1229
1719
  }
1230
1720
  #endif /* disabled curl_multi_wait: use fdsets */
@@ -1233,9 +1723,9 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1233
1723
  } while( rbcm->running );
1234
1724
 
1235
1725
  rb_curl_multi_read_info( self, rbcm->handle );
1236
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1726
+ rb_curl_multi_yield_if_given(self, block);
1237
1727
  if (cCurlMutiAutoClose == 1) {
1238
- rb_funcall(self, rb_intern("close"), 0);
1728
+ rb_funcall(self, rb_intern("_autoclose"), 0);
1239
1729
  }
1240
1730
  return Qtrue;
1241
1731
  }
@@ -1249,7 +1739,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1249
1739
  */
1250
1740
  VALUE ruby_curl_multi_close(VALUE self) {
1251
1741
  ruby_curl_multi *rbcm;
1252
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
1742
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1253
1743
  rb_curl_multi_detach_all(rbcm);
1254
1744
 
1255
1745
  if (rbcm->handle) {
@@ -1257,7 +1747,19 @@ VALUE ruby_curl_multi_close(VALUE self) {
1257
1747
  rbcm->handle = NULL;
1258
1748
  }
1259
1749
 
1260
- ruby_curl_multi_init(rbcm);
1750
+ rbcm->active = 0;
1751
+ rbcm->running = 0;
1752
+ clear_multi_deferred_exception_if_any(self);
1753
+ clear_multi_deferred_exception_source_id_if_any(self);
1754
+ return self;
1755
+ }
1756
+
1757
+ static VALUE ruby_curl_multi_mark_closed(VALUE self) {
1758
+ ruby_curl_multi *rbcm;
1759
+
1760
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1761
+ rbcm->closed = 1;
1762
+
1261
1763
  return self;
1262
1764
  }
1263
1765
 
@@ -1280,28 +1782,33 @@ static void curl_multi_mark(void *ptr) {
1280
1782
  /* =================== INIT LIB =====================*/
1281
1783
  void init_curb_multi() {
1282
1784
  idCall = rb_intern("call");
1785
+ id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
1786
+ id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
1283
1787
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
1284
1788
 
1285
- rb_undef_alloc_func(cCurlMulti);
1789
+ rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
1286
1790
 
1287
1791
  /* Class methods */
1288
- rb_define_singleton_method(cCurlMulti, "new", ruby_curl_multi_new, 0);
1289
1792
  rb_define_singleton_method(cCurlMulti, "default_timeout=", ruby_curl_multi_set_default_timeout, 1);
1290
1793
  rb_define_singleton_method(cCurlMulti, "default_timeout", ruby_curl_multi_get_default_timeout, 0);
1291
1794
  rb_define_singleton_method(cCurlMulti, "autoclose=", ruby_curl_multi_set_autoclose, 1);
1292
1795
  rb_define_singleton_method(cCurlMulti, "autoclose", ruby_curl_multi_get_autoclose, 0);
1293
1796
  /* Instance methods */
1797
+ rb_define_method(cCurlMulti, "initialize", ruby_curl_multi_initialize, 0);
1294
1798
  rb_define_method(cCurlMulti, "max_connects=", ruby_curl_multi_max_connects, 1);
1295
1799
  rb_define_method(cCurlMulti, "max_host_connections=", ruby_curl_multi_max_host_connections, 1);
1296
1800
  rb_define_method(cCurlMulti, "pipeline=", ruby_curl_multi_pipeline, 1);
1297
1801
  rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
1298
1802
  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
1803
+ /*
1804
+ * The legacy fdset loop is the stable default. The newer socket-action path
1805
+ * is kept in-tree, but it has shown scheduler regressions for one-handle
1806
+ * multi usage (for example Curl::Easy#perform under Async).
1807
+ */
1304
1808
  rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, -1);
1809
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1810
+ rb_define_private_method(cCurlMulti, "_socket_perform", ruby_curl_multi_socket_perform, -1);
1305
1811
  #endif
1306
1812
  rb_define_method(cCurlMulti, "_close", ruby_curl_multi_close, 0);
1813
+ rb_define_private_method(cCurlMulti, "_mark_closed", ruby_curl_multi_mark_closed, 0);
1307
1814
  }