curb 1.2.2 → 1.3.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
@@ -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);
@@ -474,118 +665,291 @@ static VALUE find_easy_by_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
474
665
  return ctx.easy;
475
666
  }
476
667
 
477
- static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
478
- long response_code = -1;
668
+ static VALUE find_easy_value_for_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
479
669
  VALUE easy = Qnil;
480
670
  ruby_curl_easy *rbce = NULL;
481
- VALUE callargs;
482
- ruby_curl_multi *rbcm = NULL;
483
-
484
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
485
671
 
486
- /* Try to recover the ruby_curl_easy pointer stored via CURLOPT_PRIVATE. */
487
- CURLcode private_rc = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char**)&rbce);
672
+ CURLcode private_rc = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char **)&rbce);
488
673
  if (private_rc == CURLE_OK && rbce) {
489
674
  easy = rbce->self;
490
675
  }
491
676
 
492
- /* If PRIVATE is unavailable or invalid, fall back to scanning attachments. */
493
677
  if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
494
678
  easy = find_easy_by_handle(rbcm, easy_handle);
495
- if (!NIL_P(easy) && RB_TYPE_P(easy, T_DATA)) {
496
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
497
- }
498
679
  }
499
680
 
500
- /* If we still cannot identify the easy handle, remove it and bail. */
501
- if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA) || !rbce) {
502
- if (rbcm && rbcm->handle && easy_handle) {
503
- curl_multi_remove_handle(rbcm->handle, easy_handle);
504
- }
681
+ return easy;
682
+ }
683
+
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
+ };
692
+
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;
696
+
697
+ if (!rbce) {
698
+ return ST_CONTINUE;
699
+ }
700
+
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) {
505
747
  return;
506
748
  }
507
749
 
508
- rbce->last_result = result; /* save the last easy result code */
750
+ ctx.snapshot = snapshot;
751
+ ctx.easies = easies;
752
+ st_foreach(rbcm->attached, collect_new_attached_easies_i, (st_data_t)&ctx);
509
753
 
510
- /* Ensure any verbose output redirected via CURLOPT_STDERR is flushed
511
- * before we tear down handler state. */
512
- flush_stderr_if_any(rbce);
754
+ for (index = 0; index < RARRAY_LEN(easies); index++) {
755
+ VALUE easy = rb_ary_entry(easies, index);
756
+ ruby_curl_easy *rbce = NULL;
513
757
 
514
- // remove the easy handle from multi on completion so it can be reused again
515
- rb_funcall(self, rb_intern("remove"), 1, easy);
758
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
759
+ continue;
760
+ }
516
761
 
517
- /* after running a request cleanup the headers, these are set before each request */
518
- if (rbce->curl_headers) {
519
- curl_slist_free_all(rbce->curl_headers);
520
- rbce->curl_headers = NULL;
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
+ }
766
+
767
+ rb_curl_multi_remove_request_reference(self, easy);
768
+ rb_curl_multi_remove(rbcm, easy);
521
769
  }
522
770
 
523
- /* Flush again after removal to cover any last buffered data. */
524
- flush_stderr_if_any(rbce);
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;
525
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;
526
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
+ }
527
804
 
528
805
  if (!rb_easy_nil("complete_proc")) {
529
- callargs = rb_ary_new3(2, rb_easy_get("complete_proc"), easy);
530
- rbce->callback_active = 1;
806
+ callargs = rb_ary_new3(2, rb_easy_get("complete_proc"), args->easy);
807
+ args->rbce->callback_active = 1;
531
808
  rb_rescue(call_status_handler1, callargs, callback_exception, did_raise);
532
- rbce->callback_active = 0;
533
- 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);
534
811
  }
535
812
 
536
813
  #ifdef HAVE_CURLINFO_RESPONSE_CODE
537
- curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
814
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
538
815
  #else /* use fdsets path for waiting */
539
816
  // old libcurl
540
- curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CODE, &response_code);
817
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_HTTP_CODE, &response_code);
541
818
  #endif
542
- long redirect_count;
543
- curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
819
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
544
820
 
545
- if (result != 0) {
821
+ if (args->result != 0) {
546
822
  if (!rb_easy_nil("failure_proc")) {
547
- callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), easy, rb_curl_easy_error(result));
548
- 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;
549
825
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
550
- rbce->callback_active = 0;
551
- 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);
552
828
  }
553
829
  } else if (!rb_easy_nil("success_proc") &&
554
830
  ((response_code >= 200 && response_code < 300) || response_code == 0)) {
555
831
  /* NOTE: we allow response_code == 0, in the case of non http requests e.g. reading from disk */
556
- callargs = rb_ary_new3(2, rb_easy_get("success_proc"), easy);
557
- rbce->callback_active = 1;
832
+ callargs = rb_ary_new3(2, rb_easy_get("success_proc"), args->easy);
833
+ args->rbce->callback_active = 1;
558
834
  rb_rescue(call_status_handler1, callargs, callback_exception, did_raise);
559
- rbce->callback_active = 0;
560
- 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);
561
837
 
562
838
  } else if (!rb_easy_nil("redirect_proc") && ((response_code >= 300 && response_code < 400) || redirect_count > 0) ) {
563
839
  /* Skip on_redirect callback if follow_location is false AND max_redirects is 0 */
564
- if (!rbce->follow_location && rbce->max_redirs == 0) {
840
+ if (!args->rbce->follow_location && args->rbce->max_redirs == 0) {
565
841
  // Do nothing - skip the callback
566
842
  } else {
567
- rbce->callback_active = 1;
568
- callargs = rb_ary_new3(3, rb_easy_get("redirect_proc"), easy, rb_curl_easy_error(result));
569
- 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));
570
845
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
571
- 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);
572
848
  }
573
849
  } else if (!rb_easy_nil("missing_proc") &&
574
850
  (response_code >= 400 && response_code < 500)) {
575
- rbce->callback_active = 1;
576
- callargs = rb_ary_new3(3, rb_easy_get("missing_proc"), easy, rb_curl_easy_error(result));
577
- 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));
578
853
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
579
- 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);
580
856
  } else if (!rb_easy_nil("failure_proc") &&
581
857
  (response_code >= 500 && response_code <= 999)) {
582
- callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), easy, rb_curl_easy_error(result));
583
- 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;
584
860
  rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
585
- rbce->callback_active = 0;
586
- 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);
863
+ }
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;
587
882
  }
588
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);
589
953
  }
590
954
 
591
955
  static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
@@ -594,6 +958,9 @@ static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
594
958
  CURLcode c_easy_result;
595
959
  CURLMsg *c_multi_result; // for picking up messages with the transfer status
596
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);
597
964
 
598
965
  /* Check for finished easy handles and remove from the multi handle.
599
966
  * curl_multi_info_read will query for messages from individual handles.
@@ -609,8 +976,16 @@ static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
609
976
  c_easy_handle = c_multi_result->easy_handle;
610
977
  c_easy_result = c_multi_result->data.result; /* return code for transfer */
611
978
 
612
- 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
+ }
613
986
  }
987
+
988
+ raise_multi_deferred_exception_if_idle(self);
614
989
  }
615
990
 
616
991
  /* called within ruby_curl_multi_perform */
@@ -650,6 +1025,7 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
650
1025
  typedef struct {
651
1026
  st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
652
1027
  long timeout_ms; /* last timeout set by libcurl timer callback */
1028
+ VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
653
1029
  } multi_socket_ctx;
654
1030
 
655
1031
  #if CURB_SOCKET_DEBUG
@@ -687,12 +1063,30 @@ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
687
1063
  #define cselect_flags_str(...) ""
688
1064
  #endif
689
1065
 
1066
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
690
1067
  /* Protected call to rb_fiber_scheduler_io_wait to avoid unwinding into C on TypeError. */
691
- 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; };
692
1069
  static VALUE fiber_io_wait_protected(VALUE argp) {
693
1070
  struct fiber_io_wait_args *a = (struct fiber_io_wait_args *)argp;
694
1071
  return rb_fiber_scheduler_io_wait(a->scheduler, a->io, a->events, a->timeout);
695
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
+ }
696
1090
 
697
1091
  static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp) {
698
1092
  multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
@@ -700,22 +1094,25 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
700
1094
  int fd = (int)s;
701
1095
 
702
1096
  if (!ctx || !ctx->sock_map) return 0;
1097
+ if (fd < 0) return 0;
703
1098
 
704
1099
  if (what == CURL_POLL_REMOVE) {
705
- st_data_t k = (st_data_t)fd;
706
- st_data_t rec;
707
- st_delete(ctx->sock_map, &k, &rec);
1100
+ multi_socket_forget_fd(ctx, fd);
1101
+ #if CURB_SOCKET_DEBUG
708
1102
  {
709
1103
  char b[16];
710
1104
  curb_debugf("[curb.socket] sock_cb fd=%d what=%s (removed)", fd, poll_what_str(what, b, sizeof(b)));
711
1105
  }
1106
+ #endif
712
1107
  } else {
713
1108
  /* store current interest mask for this fd */
714
1109
  st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1110
+ #if CURB_SOCKET_DEBUG
715
1111
  {
716
1112
  char b[16];
717
1113
  curb_debugf("[curb.socket] sock_cb fd=%d what=%s (tracked)", fd, poll_what_str(what, b, sizeof(b)));
718
1114
  }
1115
+ #endif
719
1116
  }
720
1117
  return 0;
721
1118
  }
@@ -739,6 +1136,7 @@ static int rb_fdset_from_sockmap_i(st_data_t key, st_data_t val, st_data_t argp)
739
1136
  if (fd > a->maxfd) a->maxfd = fd;
740
1137
  return ST_CONTINUE;
741
1138
  }
1139
+ CURB_MAYBE_UNUSED_DECL
742
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) {
743
1141
  if (!map) { *maxfd_out = -1; return; }
744
1142
  struct build_fdset_args a; a.r = rfds; a.w = wfds; a.e = efds; a.maxfd = -1;
@@ -779,13 +1177,30 @@ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
779
1177
  return ST_CONTINUE;
780
1178
  }
781
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
+
782
1197
  static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
783
1198
  /* prime the state: let libcurl act on timeouts to setup sockets */
784
1199
  CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
785
1200
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
786
1201
  curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
787
1202
  rb_curl_multi_read_info(self, rbcm->handle);
788
- if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
1203
+ rb_curl_multi_yield_if_given(self, block);
789
1204
 
790
1205
  while (rbcm->running) {
791
1206
  struct timeval tv = {0, 0};
@@ -828,9 +1243,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
828
1243
  rb_fdset_t rfds, wfds, efds;
829
1244
  rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
830
1245
  int maxfd = -1;
831
- struct build_fdset_args a2; a2.r = &rfds; a2.w = &wfds; a2.e = &efds; a2.maxfd = -1;
832
- st_foreach(ctx->sock_map, rb_fdset_from_sockmap_i, (st_data_t)&a2);
833
- maxfd = a2.maxfd;
1246
+ rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
834
1247
  int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
835
1248
  curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
836
1249
  if (rc < 0) {
@@ -851,25 +1264,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
851
1264
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
852
1265
  handled_wait = 1;
853
1266
  } else if (count_tracked == 1) {
854
- #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
855
- if (wait_fd >= 0) {
856
- int ev = 0;
857
- if (wait_what == CURL_POLL_IN) ev = RB_WAITFD_IN;
858
- else if (wait_what == CURL_POLL_OUT) ev = RB_WAITFD_OUT;
859
- else if (wait_what == CURL_POLL_INOUT) ev = RB_WAITFD_IN|RB_WAITFD_OUT;
860
- int rc = rb_wait_for_single_fd(wait_fd, ev, &tv);
861
- curb_debugf("[curb.socket] rb_wait_for_single_fd rc=%d fd=%d ev=%d", rc, wait_fd, ev);
862
- if (rc < 0) {
863
- if (errno != EINTR) rb_raise(rb_eRuntimeError, "wait_for_single_fd(): %s", strerror(errno));
864
- continue;
865
- }
866
- any_ready = (rc != 0);
867
- did_timeout = (rc == 0);
868
- handled_wait = 1;
869
- }
870
- #endif
871
1267
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
872
- if (!handled_wait) {
1268
+ {
873
1269
  VALUE scheduler = rb_fiber_scheduler_current();
874
1270
  if (scheduler != Qnil) {
875
1271
  int events = 0;
@@ -882,25 +1278,58 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
882
1278
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
883
1279
  VALUE timeout = rb_float_new(timeout_s);
884
1280
  if (wait_fd < 0) {
1281
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1282
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1283
+ #else
885
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);
886
1289
  did_timeout = 1;
887
1290
  } else {
888
- const char *mode = (wait_what == CURL_POLL_IN) ? "r" : (wait_what == CURL_POLL_OUT) ? "w" : "r+";
889
- VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(wait_fd), rb_str_new_cstr(mode));
890
- rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
891
- struct fiber_io_wait_args args = { scheduler, io, events, timeout };
892
- int state = 0;
893
- VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
894
- if (state) {
895
- 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;
896
1299
  } else {
897
- any_ready = (ready != Qfalse);
898
- 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
+ }
899
1311
  }
900
1312
  }
901
1313
  handled_wait = 1;
902
1314
  }
903
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
+ }
904
1333
  #endif
905
1334
  if (!handled_wait) {
906
1335
  /* Fallback: single-fd select. */
@@ -925,7 +1354,11 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
925
1354
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
926
1355
  }
927
1356
  } else { /* count_tracked == 0 */
1357
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1358
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1359
+ #else
928
1360
  rb_thread_wait_for(tv);
1361
+ #endif
929
1362
  did_timeout = 1;
930
1363
  }
931
1364
 
@@ -939,8 +1372,12 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
939
1372
  if (wait_what == CURL_POLL_IN || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
940
1373
  if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
941
1374
  flags |= CURL_CSELECT_ERR;
942
- char b[32];
943
- 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
944
1381
  mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)wait_fd, flags, &rbcm->running);
945
1382
  curb_debugf("[curb.socket] socket_action -> mrc=%d running=%d", mrc, rbcm->running);
946
1383
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
@@ -949,7 +1386,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
949
1386
 
950
1387
  rb_curl_multi_read_info(self, rbcm->handle);
951
1388
  curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
952
- if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
1389
+ rb_curl_multi_yield_if_given(self, block);
953
1390
  }
954
1391
  }
955
1392
 
@@ -972,6 +1409,9 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
972
1409
  st_free_table(c->ctx->sock_map);
973
1410
  c->ctx->sock_map = NULL;
974
1411
  }
1412
+ if (c->ctx) {
1413
+ c->ctx->io_cache = Qnil;
1414
+ }
975
1415
  return Qnil;
976
1416
  }
977
1417
 
@@ -980,11 +1420,16 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
980
1420
  VALUE block = Qnil;
981
1421
  rb_scan_args(argc, argv, "0&", &block);
982
1422
 
983
- 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
+ }
984
1428
 
985
1429
  multi_socket_ctx ctx;
986
1430
  ctx.sock_map = st_init_numtable();
987
1431
  ctx.timeout_ms = -1;
1432
+ ctx.io_cache = rb_hash_new();
988
1433
 
989
1434
  /* install socket/timer callbacks */
990
1435
  curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
@@ -999,8 +1444,8 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
999
1444
 
1000
1445
  /* finalize */
1001
1446
  rb_curl_multi_read_info(self, rbcm->handle);
1002
- if (block != Qnil) rb_funcall(block, rb_intern("call"), 1, self);
1003
- 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);
1004
1449
 
1005
1450
  return Qtrue;
1006
1451
  }
@@ -1035,7 +1480,8 @@ void cleanup_crt_fd(fd_set *os_set, fd_set *crt_set)
1035
1480
  }
1036
1481
  #endif
1037
1482
 
1038
- #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))
1039
1485
  struct _select_set {
1040
1486
  int maxfd;
1041
1487
  fd_set *fdread, *fdwrite, *fdexcep;
@@ -1076,13 +1522,17 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1076
1522
  struct timeval tv = {0, 0};
1077
1523
  struct timeval tv_100ms = {0, 100000};
1078
1524
  VALUE block = Qnil;
1079
- #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))
1080
1526
  struct _select_set fdset_args;
1081
1527
  #endif
1082
1528
 
1083
1529
  rb_scan_args(argc, argv, "0&", &block);
1084
1530
 
1085
- 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
+ }
1086
1536
 
1087
1537
  timeout_milliseconds = cCurlMutiDefaulttimeout;
1088
1538
 
@@ -1099,9 +1549,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1099
1549
  // There are no more messages to handle by curl and we can run the ruby block
1100
1550
  // passed to perform method.
1101
1551
  // When the block completes curl will resume.
1102
- if (block != Qnil) {
1103
- rb_funcall(block, rb_intern("call"), 1, self);
1104
- }
1552
+ rb_curl_multi_yield_if_given(self, block);
1105
1553
 
1106
1554
  do {
1107
1555
  while (rbcm->running) {
@@ -1119,7 +1567,12 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1119
1567
  if (timeout_milliseconds == 0) { /* no delay */
1120
1568
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1121
1569
  rb_curl_multi_read_info( self, rbcm->handle );
1122
- 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
1123
1576
  continue;
1124
1577
  }
1125
1578
 
@@ -1164,7 +1617,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1164
1617
  /* Process pending transfers after waiting */
1165
1618
  rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
1166
1619
  rb_curl_multi_read_info(self, rbcm->handle);
1167
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1620
+ rb_curl_multi_yield_if_given(self, block);
1168
1621
  }
1169
1622
  #else
1170
1623
 
@@ -1183,7 +1636,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1183
1636
 
1184
1637
  if (maxfd == -1) {
1185
1638
  /* libcurl recommends sleeping for 100ms */
1186
- #if HAVE_RB_THREAD_FD_SELECT
1639
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1187
1640
  struct timeval tv_sleep = tv_100ms;
1188
1641
  rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1189
1642
  #else
@@ -1191,7 +1644,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1191
1644
  #endif
1192
1645
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1193
1646
  rb_curl_multi_read_info( self, rbcm->handle );
1194
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1647
+ rb_curl_multi_yield_if_given(self, block);
1195
1648
  continue;
1196
1649
  }
1197
1650
 
@@ -1202,7 +1655,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1202
1655
  #endif
1203
1656
 
1204
1657
 
1205
- #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))
1206
1659
  fdset_args.maxfd = maxfd+1;
1207
1660
  fdset_args.fdread = &fdread;
1208
1661
  fdset_args.fdwrite = &fdwrite;
@@ -1210,7 +1663,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1210
1663
  fdset_args.tv = &tv;
1211
1664
  #endif
1212
1665
 
1213
- #if HAVE_RB_THREAD_FD_SELECT
1666
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1214
1667
  /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
1215
1668
  {
1216
1669
  rb_fdset_t rfds, wfds, efds;
@@ -1242,7 +1695,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1242
1695
  #elif HAVE_RB_THREAD_BLOCKING_REGION
1243
1696
  rc = rb_thread_blocking_region(curb_select, &fdset_args, RUBY_UBF_IO, 0);
1244
1697
  #else
1245
- rc = rb_thread_select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv);
1698
+ rc = select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv);
1246
1699
  #endif
1247
1700
 
1248
1701
  #ifdef _WIN32
@@ -1261,7 +1714,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1261
1714
  default: /* action */
1262
1715
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1263
1716
  rb_curl_multi_read_info( self, rbcm->handle );
1264
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1717
+ rb_curl_multi_yield_if_given(self, block);
1265
1718
  break;
1266
1719
  }
1267
1720
  #endif /* disabled curl_multi_wait: use fdsets */
@@ -1270,9 +1723,9 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1270
1723
  } while( rbcm->running );
1271
1724
 
1272
1725
  rb_curl_multi_read_info( self, rbcm->handle );
1273
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1726
+ rb_curl_multi_yield_if_given(self, block);
1274
1727
  if (cCurlMutiAutoClose == 1) {
1275
- rb_funcall(self, rb_intern("close"), 0);
1728
+ rb_funcall(self, rb_intern("_autoclose"), 0);
1276
1729
  }
1277
1730
  return Qtrue;
1278
1731
  }
@@ -1286,7 +1739,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1286
1739
  */
1287
1740
  VALUE ruby_curl_multi_close(VALUE self) {
1288
1741
  ruby_curl_multi *rbcm;
1289
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
1742
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1290
1743
  rb_curl_multi_detach_all(rbcm);
1291
1744
 
1292
1745
  if (rbcm->handle) {
@@ -1294,7 +1747,19 @@ VALUE ruby_curl_multi_close(VALUE self) {
1294
1747
  rbcm->handle = NULL;
1295
1748
  }
1296
1749
 
1297
- 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
+
1298
1763
  return self;
1299
1764
  }
1300
1765
 
@@ -1317,28 +1782,33 @@ static void curl_multi_mark(void *ptr) {
1317
1782
  /* =================== INIT LIB =====================*/
1318
1783
  void init_curb_multi() {
1319
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");
1320
1787
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
1321
1788
 
1322
- rb_undef_alloc_func(cCurlMulti);
1789
+ rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
1323
1790
 
1324
1791
  /* Class methods */
1325
- rb_define_singleton_method(cCurlMulti, "new", ruby_curl_multi_new, 0);
1326
1792
  rb_define_singleton_method(cCurlMulti, "default_timeout=", ruby_curl_multi_set_default_timeout, 1);
1327
1793
  rb_define_singleton_method(cCurlMulti, "default_timeout", ruby_curl_multi_get_default_timeout, 0);
1328
1794
  rb_define_singleton_method(cCurlMulti, "autoclose=", ruby_curl_multi_set_autoclose, 1);
1329
1795
  rb_define_singleton_method(cCurlMulti, "autoclose", ruby_curl_multi_get_autoclose, 0);
1330
1796
  /* Instance methods */
1797
+ rb_define_method(cCurlMulti, "initialize", ruby_curl_multi_initialize, 0);
1331
1798
  rb_define_method(cCurlMulti, "max_connects=", ruby_curl_multi_max_connects, 1);
1332
1799
  rb_define_method(cCurlMulti, "max_host_connections=", ruby_curl_multi_max_host_connections, 1);
1333
1800
  rb_define_method(cCurlMulti, "pipeline=", ruby_curl_multi_pipeline, 1);
1334
1801
  rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
1335
1802
  rb_define_method(cCurlMulti, "_remove", ruby_curl_multi_remove, 1);
1336
- /* Prefer a socket-action based perform when supported and scheduler-aware. */
1337
- #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1338
- extern VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self);
1339
- rb_define_method(cCurlMulti, "perform", ruby_curl_multi_socket_perform, -1);
1340
- #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
+ */
1341
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);
1342
1811
  #endif
1343
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);
1344
1814
  }