curb 0.8.4 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +522 -0
  3. data/Rakefile +79 -26
  4. data/ext/banned.h +32 -0
  5. data/ext/curb.c +549 -230
  6. data/ext/curb.h +19 -10
  7. data/ext/curb_easy.c +1935 -366
  8. data/ext/curb_easy.h +16 -0
  9. data/ext/curb_errors.c +115 -18
  10. data/ext/curb_errors.h +8 -5
  11. data/ext/curb_macros.h +33 -21
  12. data/ext/curb_multi.c +1858 -272
  13. data/ext/curb_multi.h +10 -3
  14. data/ext/curb_postfield.c +149 -77
  15. data/ext/curb_postfield.h +1 -0
  16. data/ext/curb_upload.c +38 -11
  17. data/ext/curb_upload.h +2 -0
  18. data/ext/extconf.rb +353 -35
  19. data/lib/curb.rb +1 -0
  20. data/lib/curl/easy.rb +314 -78
  21. data/lib/curl/multi.rb +134 -28
  22. data/lib/curl.rb +217 -11
  23. data/tests/bug_crash_on_debug.rb +14 -28
  24. data/tests/bug_crash_on_progress.rb +32 -16
  25. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  26. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  27. data/tests/bug_follow_redirect_288.rb +83 -0
  28. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  29. data/tests/bug_issue102.rb +1 -1
  30. data/tests/bug_issue_noproxy.rb +56 -0
  31. data/tests/bug_issue_post_redirect.rb +93 -0
  32. data/tests/bug_issue_spnego.rb +41 -0
  33. data/tests/bug_multi_segfault.rb +1 -0
  34. data/tests/bug_raise_on_callback.rb +30 -0
  35. data/tests/helper.rb +400 -44
  36. data/tests/leak_trace.rb +237 -0
  37. data/tests/mem_check.rb +3 -0
  38. data/tests/tc_curl.rb +31 -1
  39. data/tests/tc_curl_download.rb +12 -7
  40. data/tests/tc_curl_easy.rb +911 -63
  41. data/tests/tc_curl_easy_cookielist.rb +277 -0
  42. data/tests/tc_curl_easy_request_target.rb +41 -0
  43. data/tests/tc_curl_easy_resolve.rb +48 -0
  44. data/tests/tc_curl_maxfilesize.rb +12 -0
  45. data/tests/tc_curl_multi.rb +855 -53
  46. data/tests/tc_curl_native_coverage.rb +145 -0
  47. data/tests/tc_curl_postfield.rb +207 -30
  48. data/tests/tc_curl_protocols.rb +37 -0
  49. data/tests/tc_fiber_scheduler.rb +543 -0
  50. data/tests/tc_ftp_options.rb +39 -0
  51. data/tests/tc_gc_compact.rb +223 -0
  52. data/tests/tc_test_server_methods.rb +110 -0
  53. data/tests/timeout.rb +30 -6
  54. metadata +59 -36
  55. data/README +0 -194
data/ext/curb_multi.c CHANGED
@@ -4,27 +4,77 @@
4
4
  *
5
5
  */
6
6
  #include "curb_config.h"
7
- #ifdef HAVE_RUBY19_ST_H
8
- #include <ruby.h>
7
+ #include <ruby.h>
8
+ #ifdef HAVE_RUBY_IO_H
9
+ #include <ruby/io.h>
10
+ #endif
11
+ #ifdef HAVE_RUBY_ST_H
9
12
  #include <ruby/st.h>
10
13
  #else
11
- #include <ruby.h>
12
14
  #include <st.h>
13
15
  #endif
16
+
17
+ #ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
18
+ #include <ruby/thread.h>
19
+ #endif
20
+ #ifdef HAVE_RUBY_FIBER_SCHEDULER_H
21
+ #include <ruby/fiber/scheduler.h>
22
+ #endif
23
+
14
24
  #include "curb_easy.h"
15
25
  #include "curb_errors.h"
16
26
  #include "curb_postfield.h"
17
27
  #include "curb_multi.h"
18
28
 
19
29
  #include <errno.h>
30
+ #include <fcntl.h>
31
+ #ifndef _WIN32
32
+ #include <sys/time.h>
33
+ #include <time.h>
34
+ #endif
35
+ #include <stdint.h>
36
+ #include <stdarg.h>
37
+
38
+ /*
39
+ * Optional socket-action debug logging. Enabled by defining CURB_SOCKET_DEBUG=1
40
+ * at compile time (e.g. via environment variable passed to extconf.rb).
41
+ */
42
+ #ifndef CURB_SOCKET_DEBUG
43
+ #define CURB_SOCKET_DEBUG 0
44
+ #endif
45
+ #if !CURB_SOCKET_DEBUG
46
+ #define curb_debugf(...) ((void)0)
47
+ #endif
48
+
49
+ #ifdef RBIMPL_ATTR_MAYBE_UNUSED
50
+ #define CURB_MAYBE_UNUSED_DECL RBIMPL_ATTR_MAYBE_UNUSED()
51
+ #else
52
+ #define CURB_MAYBE_UNUSED_DECL
53
+ #endif
20
54
 
21
55
  #ifdef _WIN32
22
56
  // for O_RDWR and O_BINARY
23
57
  #include <fcntl.h>
24
58
  #endif
25
59
 
60
+ #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
61
+ struct wait_args {
62
+ CURLM *handle;
63
+ long timeout_ms;
64
+ int numfds;
65
+ };
66
+ static void *curl_multi_wait_wrapper(void *p) {
67
+ struct wait_args *args = p;
68
+ CURLMcode code = curl_multi_wait(args->handle, NULL, 0, args->timeout_ms, &args->numfds);
69
+ return (void *)(intptr_t)code;
70
+ }
71
+ #endif
72
+
26
73
  extern VALUE mCurl;
27
74
  static VALUE idCall;
75
+ static ID id_deferred_exception_ivar;
76
+ static ID id_deferred_exception_source_id_ivar;
77
+ static ID id_socket_io_cache_ivar;
28
78
 
29
79
  #ifdef RDOC_NEVER_DEFINED
30
80
  mCurl = rb_define_module("Curl");
@@ -33,72 +83,353 @@ static VALUE idCall;
33
83
  VALUE cCurlMulti;
34
84
 
35
85
  static long cCurlMutiDefaulttimeout = 100; /* milliseconds */
86
+ static char cCurlMutiAutoClose = 0;
36
87
 
88
+ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result);
37
89
  static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
38
90
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
39
91
  static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running);
40
92
 
41
- static VALUE callback_exception(VALUE unused) {
42
- return Qfalse;
43
- }
93
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg);
94
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm);
95
+ static void curl_multi_mark(void *ptr);
96
+ static void ruby_curl_multi_ensure_handle(ruby_curl_multi *rbcm);
97
+ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce);
98
+ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
99
+ static VALUE ruby_curl_multi_mark_closed(VALUE self);
100
+ static VALUE ruby_curl_multi_alloc(VALUE klass);
101
+ static VALUE ruby_curl_multi_initialize(VALUE self);
102
+ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self);
103
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
104
+ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE self);
105
+ #endif
106
+
107
+ static ruby_curl_multi *ruby_curl_multi_pointer_if_compatible(VALUE multi_val) {
108
+ if (NIL_P(multi_val) || !RB_TYPE_P(multi_val, T_DATA)) {
109
+ return NULL;
110
+ }
111
+
112
+ #if defined(RTYPEDDATA_P) && defined(RTYPEDDATA_TYPE) && defined(RTYPEDDATA_DATA)
113
+ if (!RTYPEDDATA_P(multi_val)) {
114
+ return NULL;
115
+ }
116
+
117
+ if (RTYPEDDATA_TYPE(multi_val) != &ruby_curl_multi_data_type) {
118
+ return NULL;
119
+ }
120
+
121
+ return (ruby_curl_multi *)RTYPEDDATA_DATA(multi_val);
122
+ #else
123
+ if (!rb_typeddata_is_kind_of(multi_val, &ruby_curl_multi_data_type)) {
124
+ return NULL;
125
+ }
126
+
127
+ return DATA_PTR(multi_val);
128
+ #endif
129
+ }
44
130
 
45
- static void curl_multi_mark(ruby_curl_multi *rbcm) {
46
- rb_gc_mark(rbcm->requests);
131
+ static VALUE callback_exception(VALUE did_raise, VALUE exception) {
132
+ // TODO: we could have an option to enable exception reporting
133
+ /* VALUE ret = rb_funcall(exception, rb_intern("message"), 0);
134
+ VALUE trace = rb_funcall(exception, rb_intern("backtrace"), 0);
135
+ if (RB_TYPE_P(trace, T_ARRAY) && RARRAY_LEN(trace) > 0) {
136
+ printf("we got an exception: %s:%d\n", StringValueCStr(ret), RARRAY_LEN(trace));
137
+ VALUE sep = rb_str_new_cstr("\n");
138
+ VALUE trace_lines = rb_ary_join(trace, sep);
139
+ if (RB_TYPE_P(trace_lines, T_STRING)) {
140
+ printf("%s\n", StringValueCStr(trace_lines));
141
+ } else {
142
+ printf("trace is not a string??\n");
143
+ }
144
+ } else {
145
+ printf("we got an exception: %s\nno stack available\n", StringValueCStr(ret));
146
+ }
147
+ */
148
+ rb_hash_aset(did_raise, rb_easy_hkey("error"), exception);
149
+ return exception;
47
150
  }
48
151
 
49
- static void curl_multi_flush_easy(VALUE key, VALUE easy, ruby_curl_multi *rbcm) {
50
- CURLMcode result;
51
- ruby_curl_easy *rbce;
152
+ static VALUE take_easy_callback_error_if_any(VALUE easy) {
153
+ ruby_curl_easy *rbce = NULL;
52
154
 
53
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
54
- result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
55
- if (result != 0) {
56
- raise_curl_multi_error_exception(result);
155
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
156
+ return Qnil;
57
157
  }
158
+
159
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
160
+ return rb_curl_easy_take_callback_error(rbce);
161
+ }
162
+
163
+ static VALUE build_aborted_by_callback_exception(VALUE exception) {
164
+ VALUE message = rb_funcall(exception, rb_intern("message"), 0);
165
+ VALUE aborted_exception = rb_exc_new_str(eCurlErrAbortedByCallback, message);
166
+ VALUE backtrace = rb_funcall(exception, rb_intern("backtrace"), 0);
167
+ rb_funcall(aborted_exception, rb_intern("set_backtrace"), 1, backtrace);
168
+ return aborted_exception;
169
+ }
170
+
171
+ static void stash_multi_exception_if_unset(VALUE self, VALUE exception, VALUE source_easy) {
172
+ if (rb_ivar_defined(self, id_deferred_exception_ivar)) {
173
+ return;
174
+ }
175
+
176
+ rb_ivar_set(self, id_deferred_exception_ivar, exception);
177
+ if (!NIL_P(source_easy)) {
178
+ rb_ivar_set(self, id_deferred_exception_source_id_ivar, rb_obj_id(source_easy));
179
+ }
180
+ }
181
+
182
+ static void clear_multi_deferred_exception_source_id_if_any(VALUE self) {
183
+ if (rb_ivar_defined(self, id_deferred_exception_source_id_ivar)) {
184
+ rb_funcall(self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_deferred_exception_source_id_ivar));
185
+ }
186
+ }
187
+
188
+ static VALUE clear_multi_deferred_exception_if_any(VALUE self) {
189
+ VALUE exception = Qnil;
190
+
191
+ if (rb_ivar_defined(self, id_deferred_exception_ivar)) {
192
+ exception = rb_funcall(self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_deferred_exception_ivar));
193
+ }
194
+
195
+ return exception;
196
+ }
197
+
198
+ static void rb_curl_multi_yield_if_given(VALUE self, VALUE block) {
199
+ if (block == Qnil) {
200
+ return;
201
+ }
202
+
203
+ rb_funcall(block, idCall, 1, self);
204
+ }
205
+
206
+ static void raise_multi_deferred_exception_if_idle(VALUE self) {
207
+ ruby_curl_multi *rbcm = NULL;
208
+
209
+ if (!rb_ivar_defined(self, id_deferred_exception_ivar)) {
210
+ return;
211
+ }
212
+
213
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
214
+ if (rbcm && rbcm->active > 0) {
215
+ return;
216
+ }
217
+
218
+ VALUE exception = clear_multi_deferred_exception_if_any(self);
219
+ rb_exc_raise(exception);
220
+ }
221
+
222
+ static VALUE take_status_callback_exception_if_any(VALUE did_raise) {
223
+ VALUE exception = rb_hash_aref(did_raise, rb_easy_hkey("error"));
224
+
225
+ if (FIX2INT(rb_hash_size(did_raise)) > 0 && exception != Qnil) {
226
+ rb_hash_clear(did_raise);
227
+ return build_aborted_by_callback_exception(exception);
228
+ }
229
+
230
+ return Qnil;
231
+ }
232
+
233
+ static void raise_completion_callback_error_if_any(VALUE did_raise, VALUE easy_callback_error) {
234
+ if (!NIL_P(easy_callback_error)) {
235
+ rb_exc_raise(easy_callback_error);
236
+ }
237
+
238
+ VALUE exception = take_status_callback_exception_if_any(did_raise);
239
+ if (!NIL_P(exception)) {
240
+ rb_exc_raise(exception);
241
+ }
242
+ }
243
+
244
+ struct multi_handle_complete_args {
245
+ VALUE self;
246
+ CURL *easy_handle;
247
+ int result;
248
+ };
249
+
250
+ static VALUE rb_curl_mutli_handle_complete_protected(VALUE argp) {
251
+ struct multi_handle_complete_args *args = (struct multi_handle_complete_args *)argp;
252
+ rb_curl_mutli_handle_complete(args->self, args->easy_handle, args->result);
253
+ return Qnil;
254
+ }
255
+
256
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg) {
257
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)arg;
258
+ VALUE easy = (VALUE)val;
259
+ ruby_curl_easy *rbce = NULL;
260
+
261
+ if (RB_TYPE_P(easy, T_DATA)) {
262
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
263
+ }
264
+
265
+ if (!rbce) {
266
+ return ST_CONTINUE;
267
+ }
268
+
269
+ if (rbcm && rbcm->handle && rbce->curl) {
270
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
271
+ }
272
+
273
+ rbce->multi = Qnil;
274
+
275
+ return ST_CONTINUE;
276
+ }
277
+
278
+ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
279
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
280
+
281
+ if (!rbcm || !rbce || !rbcm->attached) {
282
+ return;
283
+ }
284
+
285
+ st_data_t key = (st_data_t)rbce;
286
+ st_delete(rbcm->attached, &key, NULL);
287
+ }
288
+
289
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
290
+ if (!rbcm || !rbcm->attached) {
291
+ return;
292
+ }
293
+
294
+ st_table *attached = rbcm->attached;
295
+ rbcm->attached = NULL;
296
+
297
+ st_foreach(attached, detach_easy_entry, (st_data_t)rbcm);
298
+
299
+ st_free_table(attached);
300
+
301
+ rbcm->active = 0;
302
+ rbcm->running = 0;
58
303
  }
59
304
 
60
- static int
61
- rb_hash_clear_i(VALUE key, VALUE value, VALUE dummy) {
62
- return ST_DELETE;
305
+ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce) {
306
+ st_data_t value = 0;
307
+
308
+ if (!rbcm || !rbce || !rbcm->attached) {
309
+ return 0;
310
+ }
311
+
312
+ return st_lookup(rbcm->attached, (st_data_t)rbce, &value);
63
313
  }
64
314
 
65
- static void curl_multi_free(ruby_curl_multi *rbcm) {
315
+ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
316
+ VALUE requests;
317
+
318
+ if (NIL_P(self) || NIL_P(easy)) {
319
+ return;
320
+ }
321
+
322
+ requests = rb_funcall(self, rb_intern("requests"), 0);
323
+ if (!RB_TYPE_P(requests, T_HASH)) {
324
+ return;
325
+ }
326
+
327
+ rb_hash_delete(requests, rb_obj_id(easy));
328
+ }
66
329
 
67
- if (rbcm && !rbcm->requests == Qnil && rb_type(rbcm->requests) == T_HASH && RHASH_LEN(rbcm->requests) > 0) {
330
+ /* TypedData-compatible free function */
331
+ static void curl_multi_free(void *ptr) {
332
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
333
+ if (!rbcm) {
334
+ return;
335
+ }
68
336
 
69
- rb_hash_foreach( rbcm->requests, (int (*)())curl_multi_flush_easy, (VALUE)rbcm );
337
+ rb_curl_multi_detach_all(rbcm);
70
338
 
71
- rb_hash_foreach(rbcm->requests, rb_hash_clear_i, 0); //rb_hash_clear(rbcm->requests);
72
- rbcm->requests = Qnil;
339
+ if (rbcm->handle) {
340
+ curl_multi_cleanup(rbcm->handle);
341
+ rbcm->handle = NULL;
73
342
  }
74
- curl_multi_cleanup(rbcm->handle);
343
+
75
344
  free(rbcm);
76
345
  }
77
346
 
78
- /*
79
- * call-seq:
80
- * Curl::Multi.new => #&lt;Curl::Easy...&gt;
81
- *
82
- * Create a new Curl::Multi instance
83
- */
84
- VALUE ruby_curl_multi_new(VALUE klass) {
85
- VALUE new_curlm;
347
+ static size_t curl_multi_memsize(const void *ptr) {
348
+ (void)ptr;
349
+ return sizeof(ruby_curl_multi);
350
+ }
86
351
 
87
- ruby_curl_multi *rbcm = ALLOC(ruby_curl_multi);
352
+ const rb_data_type_t ruby_curl_multi_data_type = {
353
+ "Curl::Multi",
354
+ {
355
+ curl_multi_mark,
356
+ curl_multi_free,
357
+ curl_multi_memsize,
358
+ #ifdef RUBY_TYPED_FREE_IMMEDIATELY
359
+ NULL, /* compact */
360
+ #endif
361
+ },
362
+ #ifdef RUBY_TYPED_FREE_IMMEDIATELY
363
+ NULL, NULL, /* parent, data */
364
+ RUBY_TYPED_FREE_IMMEDIATELY
365
+ #endif
366
+ };
88
367
 
368
+ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
89
369
  rbcm->handle = curl_multi_init();
90
370
  if (!rbcm->handle) {
91
371
  rb_raise(mCurlErrFailedInit, "Failed to initialize multi handle");
92
372
  }
93
373
 
94
- rbcm->requests = rb_hash_new();
95
-
96
374
  rbcm->active = 0;
97
375
  rbcm->running = 0;
376
+ rbcm->closed = 0;
377
+ rbcm->perform_active = 0;
378
+ rbcm->callback_active = 0;
379
+ rbcm->allow_close_during_perform = 0;
380
+
381
+ if (rbcm->attached) {
382
+ st_free_table(rbcm->attached);
383
+ rbcm->attached = NULL;
384
+ }
385
+
386
+ rbcm->attached = st_init_numtable();
387
+ if (!rbcm->attached) {
388
+ curl_multi_cleanup(rbcm->handle);
389
+ rbcm->handle = NULL;
390
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
391
+ }
392
+ }
393
+
394
+ static void ruby_curl_multi_ensure_handle(ruby_curl_multi *rbcm) {
395
+ if (!rbcm->handle) {
396
+ if (rbcm->closed) {
397
+ raise_curl_multi_error_exception(CURLM_BAD_HANDLE);
398
+ }
399
+ ruby_curl_multi_init(rbcm);
400
+ }
401
+ }
402
+
403
+ /*
404
+ * call-seq:
405
+ * Curl::Multi.new => #<Curl::Easy...>
406
+ *
407
+ * Create a new Curl::Multi instance
408
+ */
409
+ static VALUE ruby_curl_multi_alloc(VALUE klass) {
410
+ VALUE self;
411
+ ruby_curl_multi *rbcm;
98
412
 
99
- new_curlm = Data_Wrap_Struct(klass, curl_multi_mark, curl_multi_free, rbcm);
413
+ self = TypedData_Make_Struct(klass, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
414
+ MEMZERO(rbcm, ruby_curl_multi, 1);
100
415
 
101
- return new_curlm;
416
+ return self;
417
+ }
418
+
419
+ static VALUE ruby_curl_multi_initialize(VALUE self) {
420
+ ruby_curl_multi *rbcm;
421
+
422
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
423
+
424
+ ruby_curl_multi_init(rbcm);
425
+
426
+ /*
427
+ * The mark routine will be called by the garbage collector during its ``mark'' phase.
428
+ * If your structure references other Ruby objects, then your mark function needs to
429
+ * identify these objects using rb_gc_mark(value). If the structure doesn't reference
430
+ * other Ruby objects, you can simply pass 0 as a function pointer.
431
+ */
432
+ return self;
102
433
  }
103
434
 
104
435
  /*
@@ -110,7 +441,7 @@ VALUE ruby_curl_multi_new(VALUE klass) {
110
441
  *
111
442
  */
112
443
  VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
113
- cCurlMutiDefaulttimeout = FIX2LONG(timeout);
444
+ cCurlMutiDefaulttimeout = NUM2LONG(timeout);
114
445
  return timeout;
115
446
  }
116
447
 
@@ -122,55 +453,39 @@ VALUE ruby_curl_multi_set_default_timeout(VALUE klass, VALUE timeout) {
122
453
  *
123
454
  */
124
455
  VALUE ruby_curl_multi_get_default_timeout(VALUE klass) {
125
- return INT2FIX(cCurlMutiDefaulttimeout);
456
+ return LONG2NUM(cCurlMutiDefaulttimeout);
126
457
  }
127
458
 
128
- /* Hash#foreach callback for ruby_curl_multi_requests */
129
- static int ruby_curl_multi_requests_callback(VALUE key, VALUE value, VALUE result_array) {
130
- rb_ary_push(result_array, value);
131
-
132
- return ST_CONTINUE;
459
+ /*
460
+ * call-seq:
461
+ * Curl::Multi.autoclose = true => true
462
+ *
463
+ * Automatically close open connections after each request. Otherwise, the connection will remain open
464
+ * for reuse until the next GC
465
+ *
466
+ */
467
+ VALUE ruby_curl_multi_set_autoclose(VALUE klass, VALUE onoff) {
468
+ cCurlMutiAutoClose = ((onoff == Qtrue) ? 1 : 0);
469
+ return onoff;
133
470
  }
134
471
 
135
472
  /*
136
473
  * call-seq:
137
- * multi.requests => [#&lt;Curl::Easy...&gt;, ...]
138
- *
139
- * Returns an array containing all the active requests on this Curl::Multi object.
474
+ * Curl::Multi.autoclose => true|false
475
+ *
476
+ * Get the global default autoclose setting for all Curl::Multi Handles.
477
+ *
140
478
  */
141
- static VALUE ruby_curl_multi_requests(VALUE self) {
142
- ruby_curl_multi *rbcm;
143
- VALUE result_array;
144
-
145
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
146
-
147
- result_array = rb_ary_new();
148
-
149
- /* iterate over the requests hash, and stuff references into the array. */
150
- rb_hash_foreach(rbcm->requests, ruby_curl_multi_requests_callback, result_array);
151
-
152
- return result_array;
479
+ VALUE ruby_curl_multi_get_autoclose(VALUE klass) {
480
+ return cCurlMutiAutoClose == 1 ? Qtrue : Qfalse;
153
481
  }
154
482
 
155
483
  /*
156
484
  * call-seq:
157
- * multi.idle? => true or false
485
+ * multi.requests => [#<Curl::Easy...>, ...]
158
486
  *
159
- * Returns whether or not this Curl::Multi handle is processing any requests. E.g. this returns
160
- * true when multi.requests.length == 0.
487
+ * Returns an array containing all the active requests on this Curl::Multi object.
161
488
  */
162
- static VALUE ruby_curl_multi_idle(VALUE self) {
163
- ruby_curl_multi *rbcm;
164
-
165
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
166
-
167
- if ( FIX2INT( rb_funcall(rbcm->requests, rb_intern("length"), 0) ) == 0 ) {
168
- return Qtrue;
169
- } else {
170
- return Qfalse;
171
- }
172
- }
173
-
174
489
  /*
175
490
  * call-seq:
176
491
  * multi = Curl::Multi.new
@@ -182,8 +497,30 @@ static VALUE ruby_curl_multi_max_connects(VALUE self, VALUE count) {
182
497
  #ifdef HAVE_CURLMOPT_MAXCONNECTS
183
498
  ruby_curl_multi *rbcm;
184
499
 
185
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
186
- curl_multi_setopt(rbcm->handle, CURLMOPT_MAXCONNECTS, NUM2INT(count));
500
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
501
+ ruby_curl_multi_ensure_handle(rbcm);
502
+
503
+ curl_multi_setopt(rbcm->handle, CURLMOPT_MAXCONNECTS, NUM2LONG(count));
504
+ #endif
505
+
506
+ return count;
507
+ }
508
+
509
+ /*
510
+ * call-seq:
511
+ * multi = Curl::Multi.new
512
+ * multi.max_host_connections = 1
513
+ *
514
+ * Set the max number of connections per host
515
+ */
516
+ static VALUE ruby_curl_multi_max_host_connections(VALUE self, VALUE count) {
517
+ #ifdef HAVE_CURLMOPT_MAX_HOST_CONNECTIONS
518
+ ruby_curl_multi *rbcm;
519
+
520
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
521
+ ruby_curl_multi_ensure_handle(rbcm);
522
+
523
+ curl_multi_setopt(rbcm->handle, CURLMOPT_MAX_HOST_CONNECTIONS, NUM2LONG(count));
187
524
  #endif
188
525
 
189
526
  return count;
@@ -194,20 +531,32 @@ static VALUE ruby_curl_multi_max_connects(VALUE self, VALUE count) {
194
531
  * multi = Curl::Multi.new
195
532
  * multi.pipeline = true
196
533
  *
197
- * Pass a long set to 1 to enable or 0 to disable. Enabling pipelining on a multi handle will make it
198
- * attempt to perform HTTP Pipelining as far as possible for transfers using this handle. This means
199
- * that if you add a second request that can use an already existing connection, the second request will
200
- * be "piped" on the same connection rather than being executed in parallel. (Added in 7.16.0)
534
+ * Pass a long set to 1 for HTTP/1.1 pipelining, 2 for HTTP/2 multiplexing, or 0 to disable.
535
+ * Enabling pipelining on a multi handle will make it attempt to perform HTTP Pipelining as
536
+ * far as possible for transfers using this handle. This means that if you add a second request
537
+ * that can use an already existing connection, the second request will be "piped" on the same
538
+ * connection rather than being executed in parallel. (Added in 7.16.0, multiplex added in 7.43.0)
201
539
  *
202
540
  */
203
- static VALUE ruby_curl_multi_pipeline(VALUE self, VALUE onoff) {
541
+ static VALUE ruby_curl_multi_pipeline(VALUE self, VALUE method) {
204
542
  #ifdef HAVE_CURLMOPT_PIPELINING
205
543
  ruby_curl_multi *rbcm;
206
544
 
207
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
208
- curl_multi_setopt(rbcm->handle, CURLMOPT_PIPELINING, onoff == Qtrue ? 1 : 0);
545
+ long value;
546
+
547
+ if (method == Qtrue) {
548
+ value = 1;
549
+ } else if (method == Qfalse) {
550
+ value = 0;
551
+ } else {
552
+ value = NUM2LONG(method);
553
+ }
554
+
555
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
556
+ ruby_curl_multi_ensure_handle(rbcm);
557
+ curl_multi_setopt(rbcm->handle, CURLMOPT_PIPELINING, value);
209
558
  #endif
210
- return onoff;
559
+ return method == Qtrue ? 1 : 0;
211
560
  }
212
561
 
213
562
  /*
@@ -223,224 +572,1222 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
223
572
  CURLMcode mcode;
224
573
  ruby_curl_easy *rbce;
225
574
  ruby_curl_multi *rbcm;
575
+ ruby_curl_multi *existing_rbcm;
576
+
577
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
578
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
579
+ ruby_curl_multi_ensure_handle(rbcm);
580
+
581
+ if (rb_curl_multi_has_easy(rbcm, rbce)) {
582
+ return self;
583
+ }
584
+
585
+ existing_rbcm = ruby_curl_multi_pointer_if_compatible(rbce->multi);
586
+ if (existing_rbcm && existing_rbcm != rbcm && rb_curl_multi_has_easy(existing_rbcm, rbce)) {
587
+ rb_raise(rb_eRuntimeError, "Cannot add an active Curl::Easy handle to another Curl::Multi");
588
+ }
589
+
590
+ /* setup the easy handle */
591
+ ruby_curl_easy_setup( rbce );
592
+
593
+ mcode = curl_multi_add_handle(rbcm->handle, rbce->curl);
594
+ if (mcode != CURLM_CALL_MULTI_PERFORM && mcode != CURLM_OK) {
595
+ ruby_curl_easy_cleanup(easy, rbce);
596
+
597
+ raise_curl_multi_error_exception(mcode);
598
+ }
599
+
600
+ rbcm->active++;
601
+
602
+ /* Increase the running count, so that the perform loop keeps running.
603
+ * If this number is not correct, the next call to curl_multi_perform will correct it. */
604
+ rbcm->running++;
605
+
606
+ if (!rbcm->attached) {
607
+ rbcm->attached = st_init_numtable();
608
+ if (!rbcm->attached) {
609
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
610
+ ruby_curl_easy_cleanup(easy, rbce);
611
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
612
+ }
613
+ }
614
+
615
+ rbce->multi_attachment_generation++;
616
+ st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
617
+
618
+ /* track a reference to associated multi handle */
619
+ rbce->multi = self;
620
+
621
+ return self;
622
+ }
623
+
624
+ /*
625
+ * call-seq:
626
+ * multi = Curl::Multi.new
627
+ * easy = Curl::Easy.new('url')
628
+ *
629
+ * multi.add(easy)
630
+ *
631
+ * # sometime later
632
+ * multi.remove(easy)
633
+ *
634
+ * Remove an easy handle from a multi stack.
635
+ *
636
+ * Will raise an exception if the easy handle is not found
637
+ */
638
+ VALUE ruby_curl_multi_remove(VALUE self, VALUE rb_easy_handle) {
639
+ ruby_curl_multi *rbcm;
640
+
641
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
642
+
643
+ rb_curl_multi_remove(rbcm, rb_easy_handle);
644
+
645
+ return self;
646
+ }
647
+
648
+ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
649
+ CURLMcode result;
650
+ ruby_curl_easy *rbce;
651
+
652
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
653
+ result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
654
+ if (result != 0) {
655
+ raise_curl_multi_error_exception(result);
656
+ }
657
+
658
+ if (rbcm->active > 0) {
659
+ rbcm->active--;
660
+ }
661
+
662
+ rbce->multi = Qnil;
663
+ ruby_curl_easy_cleanup( easy, rbce );
664
+
665
+ rb_curl_multi_forget_easy(rbcm, rbce);
666
+ }
667
+
668
+ // on_success, on_failure, on_complete
669
+ static VALUE call_status_handler1(VALUE ary) {
670
+ return rb_funcall(rb_ary_entry(ary, 0), idCall, 1, rb_ary_entry(ary, 1));
671
+ }
672
+ static VALUE call_status_handler2(VALUE ary) {
673
+ return rb_funcall(rb_ary_entry(ary, 0), idCall, 2, rb_ary_entry(ary, 1), rb_ary_entry(ary, 2));
674
+ }
675
+
676
+ static void flush_stderr_if_any(ruby_curl_easy *rbce) {
677
+ VALUE stderr_io = rb_easy_get("stderr_io");
678
+ if (stderr_io != Qnil) {
679
+ /* Flush via Ruby IO API */
680
+ rb_funcall(stderr_io, rb_intern("flush"), 0);
681
+ #ifdef HAVE_RUBY_IO_H
682
+ /* Additionally flush underlying FILE* to be extra safe. */
683
+ rb_io_t *open_f_ptr;
684
+ if (RB_TYPE_P(stderr_io, T_FILE)) {
685
+ GetOpenFile(stderr_io, open_f_ptr);
686
+ FILE *fp = rb_io_stdio_file(open_f_ptr);
687
+ if (fp) fflush(fp);
688
+ }
689
+ #endif
690
+ }
691
+ }
692
+
693
+ /* Helper to locate the Ruby Easy VALUE from the attached table using the
694
+ * underlying CURL* handle when CURLINFO_PRIVATE is unavailable or stale. */
695
+ struct find_easy_ctx { CURL *handle; VALUE easy; };
696
+ static int find_easy_by_handle_i(st_data_t key, st_data_t val, st_data_t arg) {
697
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
698
+ struct find_easy_ctx *ctx = (struct find_easy_ctx *)arg;
699
+ if (rbce && rbce->curl == ctx->handle) {
700
+ ctx->easy = (VALUE)val;
701
+ return ST_STOP;
702
+ }
703
+ return ST_CONTINUE;
704
+ }
705
+
706
+ static VALUE find_easy_by_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
707
+ if (!rbcm || !rbcm->attached) return Qnil;
708
+ struct find_easy_ctx ctx; ctx.handle = easy_handle; ctx.easy = Qnil;
709
+ st_foreach(rbcm->attached, find_easy_by_handle_i, (st_data_t)&ctx);
710
+ return ctx.easy;
711
+ }
712
+
713
+ static VALUE find_easy_value_for_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
714
+ VALUE easy = Qnil;
715
+ ruby_curl_easy *rbce = NULL;
716
+
717
+ CURLcode private_rc = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char **)&rbce);
718
+ if (private_rc == CURLE_OK && rbce) {
719
+ easy = rbce->self;
720
+ }
721
+
722
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
723
+ easy = find_easy_by_handle(rbcm, easy_handle);
724
+ }
725
+
726
+ return easy;
727
+ }
728
+
729
+ struct multi_complete_callback_args {
730
+ VALUE self;
731
+ VALUE easy;
732
+ ruby_curl_multi *rbcm;
733
+ ruby_curl_easy *rbce;
734
+ int result;
735
+ st_table *attached_snapshot;
736
+ };
737
+
738
+ static int snapshot_attached_easy_i(st_data_t key, st_data_t val, st_data_t arg) {
739
+ st_table *snapshot = (st_table *)arg;
740
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
741
+
742
+ if (!rbce) {
743
+ return ST_CONTINUE;
744
+ }
745
+
746
+ st_insert(snapshot, key, (st_data_t)rbce->multi_attachment_generation);
747
+ return ST_CONTINUE;
748
+ }
749
+
750
+ static st_table *capture_attached_easy_snapshot(ruby_curl_multi *rbcm) {
751
+ st_table *snapshot = st_init_numtable();
752
+
753
+ if (!snapshot) {
754
+ rb_raise(rb_eNoMemError, "Failed to allocate multi callback snapshot table");
755
+ }
756
+
757
+ if (rbcm && rbcm->attached) {
758
+ st_foreach(rbcm->attached, snapshot_attached_easy_i, (st_data_t)snapshot);
759
+ }
760
+
761
+ return snapshot;
762
+ }
763
+
764
+ struct collect_new_attached_easies_ctx {
765
+ st_table *snapshot;
766
+ VALUE easies;
767
+ };
768
+
769
+ static int collect_new_attached_easies_i(st_data_t key, st_data_t val, st_data_t arg) {
770
+ st_data_t existing = 0;
771
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
772
+ struct collect_new_attached_easies_ctx *ctx = (struct collect_new_attached_easies_ctx *)arg;
773
+
774
+ if (!rbce) {
775
+ return ST_CONTINUE;
776
+ }
777
+
778
+ if (!ctx->snapshot || !st_lookup(ctx->snapshot, key, &existing) ||
779
+ existing != (st_data_t)rbce->multi_attachment_generation) {
780
+ rb_ary_push(ctx->easies, (VALUE)val);
781
+ }
782
+
783
+ return ST_CONTINUE;
784
+ }
785
+
786
+ static void rb_curl_multi_remove_added_easies_since_snapshot(VALUE self, ruby_curl_multi *rbcm, st_table *snapshot) {
787
+ struct collect_new_attached_easies_ctx ctx;
788
+ VALUE easies = rb_ary_new();
789
+ long index;
790
+
791
+ if (!rbcm || !snapshot || !rbcm->attached) {
792
+ return;
793
+ }
794
+
795
+ ctx.snapshot = snapshot;
796
+ ctx.easies = easies;
797
+ st_foreach(rbcm->attached, collect_new_attached_easies_i, (st_data_t)&ctx);
798
+
799
+ for (index = 0; index < RARRAY_LEN(easies); index++) {
800
+ VALUE easy = rb_ary_entry(easies, index);
801
+ ruby_curl_easy *rbce = NULL;
802
+
803
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
804
+ continue;
805
+ }
806
+
807
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
808
+ if (!rbce || rbce->multi != self || !rb_curl_multi_has_easy(rbcm, rbce)) {
809
+ continue;
810
+ }
811
+
812
+ rb_curl_multi_remove_request_reference(self, easy);
813
+ rb_curl_multi_remove(rbcm, easy);
814
+ }
815
+
816
+ RB_GC_GUARD(easies);
817
+ }
818
+
819
+ static void stash_and_raise_status_callback_error_if_unmasked(struct multi_complete_callback_args *args, VALUE did_raise, VALUE easy_callback_error) {
820
+ VALUE exception;
821
+
822
+ if (!NIL_P(easy_callback_error)) {
823
+ return;
824
+ }
825
+
826
+ exception = take_status_callback_exception_if_any(did_raise);
827
+ if (NIL_P(exception)) {
828
+ return;
829
+ }
830
+
831
+ stash_multi_exception_if_unset(args->self, exception, args->easy);
832
+ rb_curl_multi_remove_added_easies_since_snapshot(args->self, args->rbcm, args->attached_snapshot);
833
+ rb_exc_raise(exception);
834
+ }
835
+
836
+ static VALUE rb_curl_multi_run_completion_callbacks(VALUE argp) {
837
+ struct multi_complete_callback_args *args = (struct multi_complete_callback_args *)argp;
838
+ ruby_curl_easy *rbce = args->rbce;
839
+ long response_code = -1;
840
+ VALUE easy_callback_error = Qnil;
841
+ VALUE callargs;
842
+ VALUE did_raise = rb_hash_new();
843
+ long redirect_count;
844
+
845
+ args->rbcm->callback_active = 1;
846
+
847
+ easy_callback_error = take_easy_callback_error_if_any(args->easy);
848
+ if (!NIL_P(easy_callback_error)) {
849
+ stash_multi_exception_if_unset(args->self, easy_callback_error, args->easy);
850
+ }
851
+
852
+ if (!rb_easy_nil("complete_proc")) {
853
+ callargs = rb_ary_new3(2, rb_easy_get("complete_proc"), args->easy);
854
+ args->rbce->callback_active = 1;
855
+ rb_rescue(call_status_handler1, callargs, callback_exception, did_raise);
856
+ args->rbce->callback_active = 0;
857
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
858
+ }
859
+
860
+ #ifdef HAVE_CURLINFO_RESPONSE_CODE
861
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
862
+ #else /* use fdsets path for waiting */
863
+ // old libcurl
864
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_HTTP_CODE, &response_code);
865
+ #endif
866
+ curl_easy_getinfo(args->rbce->curl, CURLINFO_REDIRECT_COUNT, &redirect_count);
867
+
868
+ if (args->result != 0) {
869
+ if (!rb_easy_nil("failure_proc")) {
870
+ callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), args->easy, rb_curl_easy_error(args->result));
871
+ args->rbce->callback_active = 1;
872
+ rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
873
+ args->rbce->callback_active = 0;
874
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
875
+ }
876
+ } else if (!rb_easy_nil("success_proc") &&
877
+ ((response_code >= 200 && response_code < 300) || response_code == 0)) {
878
+ /* NOTE: we allow response_code == 0, in the case of non http requests e.g. reading from disk */
879
+ callargs = rb_ary_new3(2, rb_easy_get("success_proc"), args->easy);
880
+ args->rbce->callback_active = 1;
881
+ rb_rescue(call_status_handler1, callargs, callback_exception, did_raise);
882
+ args->rbce->callback_active = 0;
883
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
884
+
885
+ } else if (!rb_easy_nil("redirect_proc") && ((response_code >= 300 && response_code < 400) || redirect_count > 0) ) {
886
+ /* Skip on_redirect callback if follow_location is false AND max_redirects is 0 */
887
+ if (!args->rbce->follow_location && args->rbce->max_redirs == 0) {
888
+ // Do nothing - skip the callback
889
+ } else {
890
+ args->rbce->callback_active = 1;
891
+ callargs = rb_ary_new3(3, rb_easy_get("redirect_proc"), args->easy, rb_curl_easy_error(args->result));
892
+ rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
893
+ args->rbce->callback_active = 0;
894
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
895
+ }
896
+ } else if (!rb_easy_nil("missing_proc") &&
897
+ (response_code >= 400 && response_code < 500)) {
898
+ args->rbce->callback_active = 1;
899
+ callargs = rb_ary_new3(3, rb_easy_get("missing_proc"), args->easy, rb_curl_easy_error(args->result));
900
+ rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
901
+ args->rbce->callback_active = 0;
902
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
903
+ } else if (!rb_easy_nil("failure_proc") &&
904
+ (response_code >= 500 && response_code <= 999)) {
905
+ callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), args->easy, rb_curl_easy_error(args->result));
906
+ args->rbce->callback_active = 1;
907
+ rb_rescue(call_status_handler2, callargs, callback_exception, did_raise);
908
+ args->rbce->callback_active = 0;
909
+ stash_and_raise_status_callback_error_if_unmasked(args, did_raise, easy_callback_error);
910
+ }
911
+
912
+ raise_completion_callback_error_if_any(did_raise, easy_callback_error);
913
+ return Qnil;
914
+ }
915
+
916
+ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
917
+ struct multi_complete_callback_args *args = (struct multi_complete_callback_args *)argp;
918
+
919
+ if (!args->rbce) {
920
+ return Qnil;
921
+ }
922
+
923
+ if (args->rbce->callback_active) {
924
+ args->rbce->callback_active = 0;
925
+ }
926
+
927
+ if (args->rbcm) {
928
+ args->rbcm->callback_active = 0;
929
+ }
930
+
931
+ if (args->rbce->multi == args->self && !rb_curl_multi_has_easy(args->rbcm, args->rbce)) {
932
+ args->rbce->multi = Qnil;
933
+ }
934
+
935
+ if (args->attached_snapshot) {
936
+ st_free_table(args->attached_snapshot);
937
+ args->attached_snapshot = NULL;
938
+ }
939
+
940
+ return Qnil;
941
+ }
942
+
943
+ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
944
+ VALUE easy = Qnil;
945
+ ruby_curl_easy *rbce = NULL;
946
+ ruby_curl_multi *rbcm = NULL;
947
+ CURLMcode mcode;
948
+
949
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
950
+
951
+ easy = find_easy_value_for_handle(rbcm, easy_handle);
952
+ if (!NIL_P(easy) && RB_TYPE_P(easy, T_DATA)) {
953
+ TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
954
+ }
955
+
956
+ /* If we still cannot identify the easy handle, remove it and bail. */
957
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA) || !rbce) {
958
+ if (rbcm && rbcm->handle && easy_handle) {
959
+ curl_multi_remove_handle(rbcm->handle, easy_handle);
960
+ }
961
+ return;
962
+ }
963
+
964
+ rbce->last_result = result; /* save the last easy result code */
965
+
966
+ /* Ensure any verbose output redirected via CURLOPT_STDERR is flushed
967
+ * before we tear down handler state. */
968
+ flush_stderr_if_any(rbce);
969
+
970
+ /* Detach the easy from libcurl and the Ruby requests table before running
971
+ * status callbacks, but preserve easy.multi until the callbacks finish. */
972
+ mcode = curl_multi_remove_handle(rbcm->handle, rbce->curl);
973
+ if (mcode != CURLM_OK) {
974
+ raise_curl_multi_error_exception(mcode);
975
+ }
976
+
977
+ if (rbcm->active > 0) {
978
+ rbcm->active--;
979
+ }
980
+
981
+ rb_curl_multi_remove_request_reference(self, easy);
982
+ rb_curl_multi_forget_easy(rbcm, rbce);
983
+ ruby_curl_easy_cleanup(easy, rbce);
984
+
985
+ /* after running a request cleanup the headers, these are set before each request */
986
+ if (rbce->curl_headers) {
987
+ curl_slist_free_all(rbce->curl_headers);
988
+ rbce->curl_headers = NULL;
989
+ }
990
+
991
+ /* Flush again after removal to cover any last buffered data. */
992
+ flush_stderr_if_any(rbce);
993
+
994
+ struct multi_complete_callback_args args = {
995
+ self,
996
+ easy,
997
+ rbcm,
998
+ rbce,
999
+ result,
1000
+ capture_attached_easy_snapshot(rbcm)
1001
+ };
1002
+ rb_ensure(rb_curl_multi_run_completion_callbacks, (VALUE)&args,
1003
+ rb_curl_multi_finish_completion_callbacks, (VALUE)&args);
1004
+ }
1005
+
1006
+ static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
1007
+ int msgs_left;
1008
+
1009
+ CURLcode c_easy_result;
1010
+ CURLMsg *c_multi_result; // for picking up messages with the transfer status
1011
+ CURL *c_easy_handle;
1012
+ ruby_curl_multi *rbcm = NULL;
1013
+
1014
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1015
+
1016
+ /* Check for finished easy handles and remove from the multi handle.
1017
+ * curl_multi_info_read will query for messages from individual handles.
1018
+ *
1019
+ * The messages fetched with this function are removed from the curl internal
1020
+ * queue and when there are no messages left it will return NULL (and break
1021
+ * the loop effectively).
1022
+ */
1023
+ while ((c_multi_result = curl_multi_info_read(multi_handle, &msgs_left))) {
1024
+ // A message is there, but we really care only about transfer completetion.
1025
+ if (c_multi_result->msg != CURLMSG_DONE) continue;
1026
+
1027
+ c_easy_handle = c_multi_result->easy_handle;
1028
+ c_easy_result = c_multi_result->data.result; /* return code for transfer */
1029
+
1030
+ struct multi_handle_complete_args args = { self, c_easy_handle, c_easy_result };
1031
+ int state = 0;
1032
+ rb_protect(rb_curl_mutli_handle_complete_protected, (VALUE)&args, &state);
1033
+ if (state) {
1034
+ stash_multi_exception_if_unset(self, rb_errinfo(), find_easy_value_for_handle(rbcm, c_easy_handle));
1035
+ rb_set_errinfo(Qnil);
1036
+ }
1037
+ }
1038
+
1039
+ raise_multi_deferred_exception_if_idle(self);
1040
+ }
1041
+
1042
+ /* called within ruby_curl_multi_perform */
1043
+ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running) {
1044
+ CURLMcode mcode;
1045
+
1046
+ /*
1047
+ * curl_multi_perform will return CURLM_CALL_MULTI_PERFORM only when it wants to be called again immediately.
1048
+ * When things are fine and there is nothing immediate it wants done, it'll return CURLM_OK.
1049
+ *
1050
+ * It will perform all pending actions on all added easy handles attached to this multi handle. We will loop
1051
+ * here as long as mcode is CURLM_CALL_MULTIPERFORM.
1052
+ */
1053
+ do {
1054
+ mcode = curl_multi_perform(multi_handle, still_running);
1055
+ } while (mcode == CURLM_CALL_MULTI_PERFORM);
1056
+
1057
+ /*
1058
+ * Nothing more to do, check if an error occured in the loop above and raise an exception if necessary.
1059
+ */
1060
+
1061
+ if (mcode != CURLM_OK) {
1062
+ raise_curl_multi_error_exception(mcode);
1063
+ }
1064
+
1065
+ /*
1066
+ * Everything is ok, but this does not mean all the transfers are completed.
1067
+ * There is no data to read or write available for Curl at the moment.
1068
+ *
1069
+ * At this point we can return control to the caller to do something else while
1070
+ * curl is waiting for more actions to queue.
1071
+ */
1072
+ }
1073
+
1074
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1075
+ /* ---- socket-action implementation (scheduler-friendly) ---- */
1076
+ typedef struct {
1077
+ st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
1078
+ long long timeout_deadline_ms; /* absolute deadline for CURL_SOCKET_TIMEOUT */
1079
+ VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
1080
+ } multi_socket_ctx;
1081
+
1082
+ static long long multi_socket_current_time_ms(void) {
1083
+ #if defined(CLOCK_MONOTONIC)
1084
+ struct timespec ts;
1085
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
1086
+ return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000);
1087
+ }
1088
+ #endif
1089
+
1090
+ struct timeval tv;
1091
+ gettimeofday(&tv, NULL);
1092
+ return ((long long)tv.tv_sec * 1000) + (tv.tv_usec / 1000);
1093
+ }
1094
+
1095
+ static int multi_socket_timer_due(multi_socket_ctx *ctx) {
1096
+ return ctx && ctx->timeout_deadline_ms >= 0 &&
1097
+ multi_socket_current_time_ms() >= ctx->timeout_deadline_ms;
1098
+ }
1099
+
1100
+ #if CURB_SOCKET_DEBUG
1101
+ static void curb_debugf(const char *fmt, ...) {
1102
+ va_list ap;
1103
+ va_start(ap, fmt);
1104
+ vfprintf(stderr, fmt, ap);
1105
+ fputc('\n', stderr);
1106
+ fflush(stderr);
1107
+ va_end(ap);
1108
+ }
1109
+
1110
+ static const char *poll_what_str(int what, char *buf, size_t n) {
1111
+ /* what is one of CURL_POLL_*, not a bitmask except INOUT */
1112
+ if (what == CURL_POLL_REMOVE) snprintf(buf, n, "REMOVE");
1113
+ else if (what == CURL_POLL_IN) snprintf(buf, n, "IN");
1114
+ else if (what == CURL_POLL_OUT) snprintf(buf, n, "OUT");
1115
+ else if (what == CURL_POLL_INOUT) snprintf(buf, n, "INOUT");
1116
+ else snprintf(buf, n, "WHAT=%d", what);
1117
+ return buf;
1118
+ }
1119
+
1120
+ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
1121
+ char tmp[32]; tmp[0] = 0;
1122
+ int off = 0;
1123
+ if (flags & CURL_CSELECT_IN) off += snprintf(tmp+off, (size_t)(sizeof(tmp)-off), "%sIN", off?"|":"");
1124
+ if (flags & CURL_CSELECT_OUT) off += snprintf(tmp+off, (size_t)(sizeof(tmp)-off), "%sOUT", off?"|":"");
1125
+ if (flags & CURL_CSELECT_ERR) off += snprintf(tmp+off, (size_t)(sizeof(tmp)-off), "%sERR", off?"|":"");
1126
+ if (off == 0) snprintf(tmp, sizeof(tmp), "0");
1127
+ snprintf(buf, n, "%s", tmp);
1128
+ return buf;
1129
+ }
1130
+ #else
1131
+ #define poll_what_str(...) ""
1132
+ #define cselect_flags_str(...) ""
1133
+ #endif
226
1134
 
227
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
228
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
1135
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1136
+ /* Protected call to rb_fiber_scheduler_io_wait to avoid unwinding into C on TypeError. */
1137
+ struct fiber_io_wait_args { VALUE scheduler; VALUE io; VALUE events; VALUE timeout; };
1138
+ static VALUE fiber_io_wait_protected(VALUE argp) {
1139
+ struct fiber_io_wait_args *a = (struct fiber_io_wait_args *)argp;
1140
+ return rb_fiber_scheduler_io_wait(a->scheduler, a->io, a->events, a->timeout);
1141
+ }
1142
+ #endif
229
1143
 
230
- /* setup the easy handle */
231
- ruby_curl_easy_setup( rbce );
1144
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1145
+ struct fiber_io_select_args {
1146
+ VALUE scheduler;
1147
+ VALUE readables;
1148
+ VALUE writables;
1149
+ VALUE exceptables;
1150
+ VALUE timeout;
1151
+ };
232
1152
 
233
- mcode = curl_multi_add_handle(rbcm->handle, rbce->curl);
234
- if (mcode != CURLM_CALL_MULTI_PERFORM && mcode != CURLM_OK) {
235
- raise_curl_multi_error_exception(mcode);
236
- }
1153
+ static VALUE fiber_io_select_protected(VALUE argp) {
1154
+ struct fiber_io_select_args *a = (struct fiber_io_select_args *)argp;
1155
+ return rb_fiber_scheduler_io_select(a->scheduler, a->readables, a->writables, a->exceptables, a->timeout);
1156
+ }
1157
+ #endif
237
1158
 
238
- rbcm->active++;
1159
+ #if defined(RB_INTEGER_TYPE_P)
1160
+ #define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
1161
+ #else
1162
+ #define CURB_INTEGER_P(value) (FIXNUM_P(value) || RB_TYPE_P((value), T_BIGNUM))
1163
+ #endif
239
1164
 
240
- /* Increase the running count, so that the perform loop keeps running.
241
- * If this number is not correct, the next call to curl_multi_perform will correct it. */
242
- rbcm->running++;
1165
+ static int multi_socket_wait_events_for_curl_poll(int what) {
1166
+ if (what == CURL_POLL_IN) return RB_WAITFD_IN;
1167
+ if (what == CURL_POLL_OUT) return RB_WAITFD_OUT;
1168
+ if (what == CURL_POLL_INOUT) return RB_WAITFD_IN | RB_WAITFD_OUT;
1169
+ return RB_WAITFD_IN | RB_WAITFD_OUT;
1170
+ }
243
1171
 
244
- rb_hash_aset( rbcm->requests, easy, easy );
1172
+ static int multi_socket_cselect_flags_for_curl_poll(int what) {
1173
+ int flags = 0;
245
1174
 
246
- return self;
1175
+ if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
1176
+ if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
1177
+ if (flags == 0) flags = CURL_CSELECT_IN | CURL_CSELECT_OUT;
1178
+
1179
+ return flags;
247
1180
  }
248
1181
 
249
- /*
250
- * call-seq:
251
- * multi = Curl::Multi.new
252
- * easy = Curl::Easy.new('url')
253
- *
254
- * multi.add(easy)
255
- *
256
- * # sometime later
257
- * multi.remove(easy)
258
- *
259
- * Remove an easy handle from a multi stack.
260
- *
261
- * Will raise an exception if the easy handle is not found
262
- */
263
- VALUE ruby_curl_multi_remove(VALUE self, VALUE easy) {
264
- ruby_curl_multi *rbcm;
265
- ruby_curl_easy *rbce;
1182
+ static int multi_socket_cselect_flags_for_wait_events(int events) {
1183
+ int flags = 0;
266
1184
 
267
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
1185
+ if (events & RB_WAITFD_IN) flags |= CURL_CSELECT_IN;
1186
+ if (events & RB_WAITFD_OUT) flags |= CURL_CSELECT_OUT;
1187
+ #ifdef RB_WAITFD_PRI
1188
+ if (events & RB_WAITFD_PRI) flags |= CURL_CSELECT_ERR;
1189
+ #endif
268
1190
 
269
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
1191
+ return flags;
1192
+ }
270
1193
 
271
- rb_curl_multi_remove(rbcm,easy);
1194
+ static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
1195
+ st_data_t key = (st_data_t)fd;
1196
+ st_data_t rec;
272
1197
 
273
- return self;
1198
+ if (!ctx) return;
1199
+ if (ctx->sock_map) st_delete(ctx->sock_map, &key, &rec);
1200
+ if (!NIL_P(ctx->io_cache)) rb_hash_delete(ctx->io_cache, INT2NUM(fd));
274
1201
  }
275
- static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
276
- CURLMcode result;
277
- ruby_curl_easy *rbce;
278
- VALUE r;
279
1202
 
280
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
1203
+ static int multi_socket_fd_valid_p(int fd) {
1204
+ if (fd < 0) return 0;
281
1205
 
282
- result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
283
- if (result != 0) {
284
- raise_curl_multi_error_exception(result);
285
- }
1206
+ errno = 0;
1207
+ return fcntl(fd, F_GETFD) != -1 || errno != EBADF;
1208
+ }
286
1209
 
287
- rbcm->active--;
1210
+ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp) {
1211
+ multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
1212
+ (void)easy; (void)socketp;
1213
+ int fd = (int)s;
288
1214
 
289
- ruby_curl_easy_cleanup( easy, rbce );
1215
+ if (!ctx || !ctx->sock_map) return 0;
1216
+ if (fd < 0) return 0;
290
1217
 
291
- // active should equal INT2FIX(RHASH(rbcm->requests)->tbl->num_entries)
292
- r = rb_hash_delete( rbcm->requests, easy );
293
- if( r != easy || r == Qnil ) {
294
- rb_warn("Possibly lost track of Curl::Easy VALUE, it may not be reclaimed by GC");
1218
+ if (what == CURL_POLL_REMOVE) {
1219
+ multi_socket_forget_fd(ctx, fd);
1220
+ #if CURB_SOCKET_DEBUG
1221
+ {
1222
+ char b[16];
1223
+ curb_debugf("[curb.socket] sock_cb fd=%d what=%s (removed)", fd, poll_what_str(what, b, sizeof(b)));
1224
+ }
1225
+ #endif
1226
+ } else {
1227
+ /* store current interest mask for this fd */
1228
+ st_data_t key = (st_data_t)fd;
1229
+ st_data_t old_what;
1230
+ if (st_lookup(ctx->sock_map, key, &old_what) && (int)old_what != what && !NIL_P(ctx->io_cache)) {
1231
+ rb_hash_delete(ctx->io_cache, INT2NUM(fd));
1232
+ }
1233
+ st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
1234
+ #if CURB_SOCKET_DEBUG
1235
+ {
1236
+ char b[16];
1237
+ curb_debugf("[curb.socket] sock_cb fd=%d what=%s (tracked)", fd, poll_what_str(what, b, sizeof(b)));
1238
+ }
1239
+ #endif
1240
+ }
1241
+ return 0;
1242
+ }
1243
+
1244
+ static int multi_timer_cb(CURLM *multi, long timeout_ms, void *userp) {
1245
+ (void)multi;
1246
+ multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
1247
+ if (ctx) {
1248
+ ctx->timeout_deadline_ms = timeout_ms < 0 ? -1 : multi_socket_current_time_ms() + timeout_ms;
295
1249
  }
1250
+ curb_debugf("[curb.socket] timer_cb timeout_ms=%ld", timeout_ms);
1251
+ return 0;
296
1252
  }
297
1253
 
298
- /* Hash#foreach callback for ruby_curl_multi_cancel */
299
- static int ruby_curl_multi_cancel_callback(VALUE key, VALUE value, ruby_curl_multi *rbcm) {
300
- rb_curl_multi_remove(rbcm, value);
301
-
1254
+ struct build_fdset_args { rb_fdset_t *r; rb_fdset_t *w; rb_fdset_t *e; int maxfd; };
1255
+ static int rb_fdset_from_sockmap_i(st_data_t key, st_data_t val, st_data_t argp) {
1256
+ struct build_fdset_args *a = (struct build_fdset_args *)argp;
1257
+ int fd = (int)key;
1258
+ int what = (int)val;
1259
+ if (what & CURL_POLL_IN) rb_fd_set(fd, a->r);
1260
+ if (what & CURL_POLL_OUT) rb_fd_set(fd, a->w);
1261
+ rb_fd_set(fd, a->e);
1262
+ if (fd > a->maxfd) a->maxfd = fd;
302
1263
  return ST_CONTINUE;
303
1264
  }
1265
+ CURB_MAYBE_UNUSED_DECL
1266
+ static void rb_fdset_from_sockmap(st_table *map, rb_fdset_t *rfds, rb_fdset_t *wfds, rb_fdset_t *efds, int *maxfd_out) {
1267
+ if (!map) { *maxfd_out = -1; return; }
1268
+ struct build_fdset_args a; a.r = rfds; a.w = wfds; a.e = efds; a.maxfd = -1;
1269
+ st_foreach(map, rb_fdset_from_sockmap_i, (st_data_t)&a);
1270
+ *maxfd_out = a.maxfd;
1271
+ }
304
1272
 
305
- /*
306
- * call-seq:
307
- * multi.cancel!
308
- *
309
- * Cancels all requests currently being made on this Curl::Multi handle.
310
- */
311
- static VALUE ruby_curl_multi_cancel(VALUE self) {
312
- ruby_curl_multi *rbcm;
1273
+ struct ready_fd {
1274
+ int fd;
1275
+ int flags;
1276
+ };
313
1277
 
314
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
315
-
316
- rb_hash_foreach( rbcm->requests, ruby_curl_multi_cancel_callback, (VALUE)rbcm );
317
-
318
- /* for chaining */
319
- return self;
1278
+ struct collect_ready_fd_args {
1279
+ rb_fdset_t *r;
1280
+ rb_fdset_t *w;
1281
+ rb_fdset_t *e;
1282
+ struct ready_fd *fds;
1283
+ int capacity;
1284
+ int count;
1285
+ };
1286
+
1287
+ static int collect_ready_fd_i(st_data_t key, st_data_t val, st_data_t argp) {
1288
+ (void)val;
1289
+ struct collect_ready_fd_args *a = (struct collect_ready_fd_args *)argp;
1290
+ int fd = (int)key;
1291
+ int flags = 0;
1292
+ if (rb_fd_isset(fd, a->r)) flags |= CURL_CSELECT_IN;
1293
+ if (rb_fd_isset(fd, a->w)) flags |= CURL_CSELECT_OUT;
1294
+ if (rb_fd_isset(fd, a->e)) flags |= CURL_CSELECT_ERR;
1295
+ if (flags && a->count < a->capacity) {
1296
+ a->fds[a->count].fd = fd;
1297
+ a->fds[a->count].flags = flags;
1298
+ a->count++;
1299
+ }
1300
+ return ST_CONTINUE;
320
1301
  }
321
1302
 
322
- // on_success, on_failure, on_complete
323
- static VALUE call_status_handler1(VALUE ary) {
324
- return rb_funcall(rb_ary_entry(ary, 0), idCall, 1, rb_ary_entry(ary, 1));
1303
+ /* Helpers used with st_foreach to avoid compiler-specific nested functions. */
1304
+ struct pick_one_state { int fd; int what; int found; };
1305
+ static int st_pick_one_i(st_data_t key, st_data_t val, st_data_t argp) {
1306
+ struct pick_one_state *s = (struct pick_one_state *)argp;
1307
+ s->fd = (int)key;
1308
+ s->what = (int)val;
1309
+ s->found = 1;
1310
+ return ST_STOP;
325
1311
  }
326
- static VALUE call_status_handler2(VALUE ary) {
327
- return rb_funcall(rb_ary_entry(ary, 0), idCall, 2, rb_ary_entry(ary, 1), rb_ary_entry(ary, 2));
1312
+ struct counter_state { int count; };
1313
+ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
1314
+ (void)k; (void)v;
1315
+ struct counter_state *c = (struct counter_state *)argp;
1316
+ c->count++;
1317
+ return ST_CONTINUE;
328
1318
  }
329
1319
 
330
- static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
331
-
332
- long response_code = -1;
333
- VALUE easy;
334
- ruby_curl_easy *rbce = NULL;
335
- VALUE callargs, val = Qtrue;
336
-
337
- CURLcode ecode = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char**)&easy);
1320
+ static const char *multi_socket_io_mode_for_curl_poll(int what) {
1321
+ if (what == CURL_POLL_IN) return "r";
1322
+ if (what == CURL_POLL_OUT) return "w";
1323
+ return "r+";
1324
+ }
338
1325
 
339
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
1326
+ static VALUE multi_socket_io_for_fd(multi_socket_ctx *ctx, int fd, int what) {
1327
+ VALUE key = INT2NUM(fd);
1328
+ VALUE io = rb_hash_aref(ctx->io_cache, key);
1329
+ if (NIL_P(io)) {
1330
+ io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, key, rb_str_new_cstr(multi_socket_io_mode_for_curl_poll(what)));
1331
+ rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
1332
+ rb_hash_aset(ctx->io_cache, key, io);
1333
+ }
1334
+ return io;
1335
+ }
340
1336
 
341
- rbce->last_result = result; /* save the last easy result code */
1337
+ struct io_for_fd_args { multi_socket_ctx *ctx; int fd; int what; };
1338
+ static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
1339
+ struct io_for_fd_args *a = (struct io_for_fd_args *)argp;
1340
+ return multi_socket_io_for_fd(a->ctx, a->fd, a->what);
1341
+ }
342
1342
 
343
- ruby_curl_multi_remove( self, easy );
1343
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1344
+ struct build_io_select_arrays_args {
1345
+ multi_socket_ctx *ctx;
1346
+ VALUE readables;
1347
+ VALUE writables;
1348
+ VALUE exceptables;
1349
+ int failed;
1350
+ };
344
1351
 
345
- /* after running a request cleanup the headers, these are set before each request */
346
- if (rbce->curl_headers) {
347
- curl_slist_free_all(rbce->curl_headers);
348
- rbce->curl_headers = NULL;
1352
+ static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp) {
1353
+ struct build_io_select_arrays_args *a = (struct build_io_select_arrays_args *)argp;
1354
+ int fd = (int)key;
1355
+ int what = (int)val;
1356
+ struct io_for_fd_args io_args = { a->ctx, fd, what };
1357
+ int io_state = 0;
1358
+ VALUE io;
1359
+
1360
+ if (!multi_socket_fd_valid_p(fd)) {
1361
+ a->failed = 1;
1362
+ return ST_STOP;
349
1363
  }
350
1364
 
351
- if (ecode != 0) {
352
- raise_curl_easy_error_exception(ecode);
1365
+ io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1366
+ if (io_state || NIL_P(io)) {
1367
+ if (io_state) {
1368
+ #if CURB_SOCKET_DEBUG
1369
+ VALUE err = rb_errinfo();
1370
+ VALUE msg = rb_obj_as_string(err);
1371
+ curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1372
+ #endif
1373
+ rb_set_errinfo(Qnil);
1374
+ }
1375
+ a->failed = 1;
1376
+ return ST_STOP;
353
1377
  }
354
1378
 
355
- if (!rb_easy_nil("complete_proc")) {
356
- callargs = rb_ary_new3(2, rb_easy_get("complete_proc"), easy);
357
- rbce->callback_active = 1;
358
- val = rb_rescue(call_status_handler1, callargs, callback_exception, Qnil);
359
- rbce->callback_active = 0;
360
- //rb_funcall( rb_easy_get("complete_proc"), idCall, 1, easy );
361
- }
1379
+ if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) rb_ary_push(a->readables, io);
1380
+ if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) rb_ary_push(a->writables, io);
1381
+ rb_ary_push(a->exceptables, io);
362
1382
 
363
- curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &response_code);
1383
+ return ST_CONTINUE;
1384
+ }
364
1385
 
365
- if (result != 0) {
366
- if (!rb_easy_nil("failure_proc")) {
367
- callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), easy, rb_curl_easy_error(result));
368
- rbce->callback_active = 1;
369
- val = rb_rescue(call_status_handler2, callargs, callback_exception, Qnil);
370
- rbce->callback_active = 0;
371
- //rb_funcall( rb_easy_get("failure_proc"), idCall, 2, easy, rb_curl_easy_error(result) );
372
- }
373
- }
374
- else if (!rb_easy_nil("success_proc") &&
375
- ((response_code >= 200 && response_code < 300) || response_code == 0)) {
376
- /* NOTE: we allow response_code == 0, in the case of non http requests e.g. reading from disk */
377
- callargs = rb_ary_new3(2, rb_easy_get("success_proc"), easy);
378
- rbce->callback_active = 1;
379
- val = rb_rescue(call_status_handler1, callargs, callback_exception, Qnil);
380
- rbce->callback_active = 0;
381
- //rb_funcall( rb_easy_get("success_proc"), idCall, 1, easy );
382
- }
383
- else if (!rb_easy_nil("redirect_proc") &&
384
- (response_code >= 300 && response_code < 400)) {
385
- rbce->callback_active = 1;
386
- callargs = rb_ary_new3(3, rb_easy_get("redirect_proc"), easy, rb_curl_easy_error(result));
387
- rbce->callback_active = 0;
388
- val = rb_rescue(call_status_handler2, callargs, callback_exception, Qnil);
389
- }
390
- else if (!rb_easy_nil("missing_proc") &&
391
- (response_code >= 400 && response_code < 500)) {
392
- rbce->callback_active = 1;
393
- callargs = rb_ary_new3(3, rb_easy_get("missing_proc"), easy, rb_curl_easy_error(result));
394
- rbce->callback_active = 0;
395
- val = rb_rescue(call_status_handler2, callargs, callback_exception, Qnil);
396
- }
397
- else if (!rb_easy_nil("failure_proc") &&
398
- (response_code >= 500 && response_code <= 999)) {
399
- callargs = rb_ary_new3(3, rb_easy_get("failure_proc"), easy, rb_curl_easy_error(result));
400
- rbce->callback_active = 1;
401
- val = rb_rescue(call_status_handler2, callargs, callback_exception, Qnil);
402
- rbce->callback_active = 0;
403
- //rb_funcall( rb_easy_get("failure_proc"), idCall, 2, easy, rb_curl_easy_error(result) );
404
- }
1386
+ struct collect_io_select_ready_args {
1387
+ multi_socket_ctx *ctx;
1388
+ VALUE readables;
1389
+ VALUE writables;
1390
+ VALUE exceptables;
1391
+ struct ready_fd *fds;
1392
+ int capacity;
1393
+ int count;
1394
+ };
405
1395
 
406
- if (val == Qfalse) {
407
- rb_warn("uncaught exception from callback");
408
- // exception was raised?
409
- //fprintf(stderr, "exception raised from callback\n");
1396
+ static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t argp) {
1397
+ (void)val;
1398
+ struct collect_io_select_ready_args *a = (struct collect_io_select_ready_args *)argp;
1399
+ VALUE io = rb_hash_aref(a->ctx->io_cache, INT2NUM((int)key));
1400
+ int flags = 0;
1401
+
1402
+ if (NIL_P(io)) return ST_CONTINUE;
1403
+ if (RTEST(rb_ary_includes(a->readables, io))) flags |= CURL_CSELECT_IN;
1404
+ if (RTEST(rb_ary_includes(a->writables, io))) flags |= CURL_CSELECT_OUT;
1405
+ if (RTEST(rb_ary_includes(a->exceptables, io))) flags |= CURL_CSELECT_ERR;
1406
+
1407
+ if (flags && a->count < a->capacity) {
1408
+ a->fds[a->count].fd = (int)key;
1409
+ a->fds[a->count].flags = flags;
1410
+ a->count++;
410
1411
  }
411
1412
 
1413
+ return ST_CONTINUE;
412
1414
  }
1415
+ #endif
413
1416
 
414
- static void rb_curl_multi_read_info(VALUE self, CURLM *multi_handle) {
415
- int msgs_left, result;
416
- CURLMsg *msg;
417
- CURL *easy_handle;
1417
+ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
1418
+ /* prime the state: let libcurl act on timeouts to setup sockets */
1419
+ CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1420
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1421
+ curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1422
+ rb_curl_multi_read_info(self, rbcm->handle);
1423
+ rb_curl_multi_yield_if_given(self, block);
1424
+
1425
+ while (rbcm->running) {
1426
+ struct timeval tv = {0, 0};
1427
+ long wait_ms = cCurlMutiDefaulttimeout;
1428
+
1429
+ if (multi_socket_timer_due(ctx)) {
1430
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1431
+ curb_debugf("[curb.socket] socket_action timeout(due) -> mrc=%d running=%d", mrc, rbcm->running);
1432
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1433
+ rb_curl_multi_read_info(self, rbcm->handle);
1434
+ rb_curl_multi_yield_if_given(self, block);
1435
+ continue;
1436
+ }
1437
+
1438
+ if (ctx->timeout_deadline_ms >= 0) {
1439
+ long long remaining_ms = ctx->timeout_deadline_ms - multi_socket_current_time_ms();
1440
+ if (remaining_ms < wait_ms) wait_ms = remaining_ms < 0 ? 0 : (long)remaining_ms;
1441
+ }
1442
+ tv.tv_sec = wait_ms / 1000;
1443
+ tv.tv_usec = (wait_ms % 1000) * 1000;
1444
+
1445
+ /* Find a representative fd to wait on (if any). */
1446
+ int wait_fd = -1;
1447
+ int wait_what = 0;
1448
+ if (ctx->sock_map) {
1449
+ struct pick_one_state st = { -1, 0, 0 };
1450
+ st_foreach(ctx->sock_map, st_pick_one_i, (st_data_t)&st);
1451
+ if (st.found) { wait_fd = st.fd; wait_what = st.what; }
1452
+ }
1453
+
1454
+ /* Count tracked fds for logging */
1455
+ int count_tracked = 0;
1456
+ if (ctx->sock_map) {
1457
+ struct counter_state cs = { 0 };
1458
+ st_foreach(ctx->sock_map, st_count_i, (st_data_t)&cs);
1459
+ count_tracked = cs.count;
1460
+ }
1461
+
1462
+ curb_debugf("[curb.socket] wait phase: tracked_fds=%d fd=%d what=%d tv=%ld.%06ld", count_tracked, wait_fd, wait_what, (long)tv.tv_sec, (long)tv.tv_usec);
1463
+
1464
+ int did_timeout = 0;
1465
+ int any_ready = 0;
1466
+ int ready_flags = 0;
1467
+
1468
+ int handled_wait = 0;
1469
+ if (count_tracked > 1) {
1470
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1471
+ {
1472
+ VALUE scheduler = rb_fiber_scheduler_current();
1473
+ if (scheduler != Qnil) {
1474
+ VALUE readables = rb_ary_new();
1475
+ VALUE writables = rb_ary_new();
1476
+ VALUE exceptables = rb_ary_new();
1477
+ struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0 };
1478
+ st_foreach(ctx->sock_map, build_io_select_arrays_i, (st_data_t)&build_args);
1479
+ if (!build_args.failed) {
1480
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1481
+ VALUE timeout = rb_float_new(timeout_s);
1482
+ struct fiber_io_select_args select_args = { scheduler, readables, writables, exceptables, timeout };
1483
+ int state = 0;
1484
+ VALUE ready = rb_protect(fiber_io_select_protected, (VALUE)&select_args, &state);
1485
+ if (state) {
1486
+ #if CURB_SOCKET_DEBUG
1487
+ VALUE err = rb_errinfo();
1488
+ VALUE msg = rb_obj_as_string(err);
1489
+ curb_debugf("[curb.socket] scheduler io_select failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1490
+ #endif
1491
+ rb_set_errinfo(Qnil);
1492
+ } else {
1493
+ handled_wait = 1;
1494
+ any_ready = RB_TYPE_P(ready, T_ARRAY);
1495
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1496
+ if (any_ready) {
1497
+ VALUE ready_readables = rb_ary_entry(ready, 0);
1498
+ VALUE ready_writables = rb_ary_entry(ready, 1);
1499
+ VALUE ready_exceptables = rb_ary_entry(ready, 2);
1500
+ struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
1501
+ struct collect_io_select_ready_args d;
1502
+ int i;
1503
+ if (!RB_TYPE_P(ready_readables, T_ARRAY)) ready_readables = rb_ary_new();
1504
+ if (!RB_TYPE_P(ready_writables, T_ARRAY)) ready_writables = rb_ary_new();
1505
+ if (!RB_TYPE_P(ready_exceptables, T_ARRAY)) ready_exceptables = rb_ary_new();
1506
+ d.ctx = ctx;
1507
+ d.readables = ready_readables;
1508
+ d.writables = ready_writables;
1509
+ d.exceptables = ready_exceptables;
1510
+ d.fds = ready_fds;
1511
+ d.capacity = count_tracked;
1512
+ d.count = 0;
1513
+ st_foreach(ctx->sock_map, collect_io_select_ready_i, (st_data_t)&d);
1514
+ any_ready = (d.count > 0);
1515
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1516
+ for (i = 0; i < d.count; i++) {
1517
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
1518
+ if (mrc != CURLM_OK) {
1519
+ xfree(ready_fds);
1520
+ raise_curl_multi_error_exception(mrc);
1521
+ }
1522
+ }
1523
+ xfree(ready_fds);
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+ #endif
1530
+ if (!handled_wait) {
1531
+ /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1532
+ rb_fdset_t rfds, wfds, efds;
1533
+ rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1534
+ int maxfd = -1;
1535
+ rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
1536
+ int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
1537
+ curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
1538
+ if (rc < 0) {
1539
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1540
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
1541
+ continue;
1542
+ }
1543
+ any_ready = (rc > 0);
1544
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1545
+ if (any_ready) {
1546
+ struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
1547
+ struct collect_ready_fd_args d;
1548
+ int i;
1549
+ d.r = &rfds;
1550
+ d.w = &wfds;
1551
+ d.e = &efds;
1552
+ d.fds = ready_fds;
1553
+ d.capacity = count_tracked;
1554
+ d.count = 0;
1555
+ st_foreach(ctx->sock_map, collect_ready_fd_i, (st_data_t)&d);
1556
+ for (i = 0; i < d.count; i++) {
1557
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
1558
+ if (mrc != CURLM_OK) {
1559
+ xfree(ready_fds);
1560
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1561
+ raise_curl_multi_error_exception(mrc);
1562
+ }
1563
+ }
1564
+ xfree(ready_fds);
1565
+ }
1566
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1567
+ handled_wait = 1;
1568
+ }
1569
+ } else if (count_tracked == 1) {
1570
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1571
+ {
1572
+ VALUE scheduler = rb_fiber_scheduler_current();
1573
+ if (scheduler != Qnil) {
1574
+ int scheduler_wait_handled = 0;
1575
+ int events = 0;
1576
+ if (wait_fd >= 0) {
1577
+ events = multi_socket_wait_events_for_curl_poll(wait_what);
1578
+ }
1579
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1580
+ VALUE timeout = rb_float_new(timeout_s);
1581
+ if (wait_fd < 0) {
1582
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1583
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1584
+ #else
1585
+ rb_thread_wait_for(tv);
1586
+ #endif
1587
+ did_timeout = multi_socket_timer_due(ctx);
1588
+ scheduler_wait_handled = 1;
1589
+ } else if (!multi_socket_fd_valid_p(wait_fd)) {
1590
+ multi_socket_forget_fd(ctx, wait_fd);
1591
+ did_timeout = 1;
1592
+ scheduler_wait_handled = 1;
1593
+ } else {
1594
+ struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1595
+ int io_state = 0;
1596
+ VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1597
+ if (io_state || NIL_P(io)) {
1598
+ if (io_state) {
1599
+ #if CURB_SOCKET_DEBUG
1600
+ VALUE err = rb_errinfo();
1601
+ VALUE msg = rb_obj_as_string(err);
1602
+ curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1603
+ #endif
1604
+ rb_set_errinfo(Qnil);
1605
+ }
1606
+ any_ready = 0;
1607
+ } else {
1608
+ struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
1609
+ int state = 0;
1610
+ VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
1611
+ if (state) {
1612
+ #if CURB_SOCKET_DEBUG
1613
+ VALUE err = rb_errinfo();
1614
+ VALUE msg = rb_obj_as_string(err);
1615
+ curb_debugf("[curb.socket] scheduler io_wait failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1616
+ #endif
1617
+ rb_set_errinfo(Qnil);
1618
+ any_ready = 0;
1619
+ } else {
1620
+ scheduler_wait_handled = 1;
1621
+ any_ready = (ready != Qfalse && !NIL_P(ready));
1622
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1623
+ if (any_ready) {
1624
+ if (ready == Qtrue) {
1625
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1626
+ } else if (CURB_INTEGER_P(ready)) {
1627
+ ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1628
+ if (ready_flags == 0) {
1629
+ any_ready = 0;
1630
+ did_timeout = multi_socket_timer_due(ctx);
1631
+ }
1632
+ } else {
1633
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1634
+ }
1635
+ }
1636
+ }
1637
+ }
1638
+ }
1639
+ if (scheduler_wait_handled) handled_wait = 1;
1640
+ }
1641
+ }
1642
+ #endif
1643
+ #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
1644
+ if (!handled_wait && wait_fd >= 0) {
1645
+ int ev = multi_socket_wait_events_for_curl_poll(wait_what);
1646
+ int rc = rb_wait_for_single_fd(wait_fd, ev, &tv);
1647
+ curb_debugf("[curb.socket] rb_wait_for_single_fd rc=%d fd=%d ev=%d", rc, wait_fd, ev);
1648
+ if (rc < 0) {
1649
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "wait_for_single_fd(): %s", strerror(errno));
1650
+ continue;
1651
+ }
1652
+ any_ready = (rc != 0);
1653
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1654
+ if (any_ready) ready_flags = multi_socket_cselect_flags_for_wait_events(rc);
1655
+ handled_wait = 1;
1656
+ }
1657
+ #endif
1658
+ if (!handled_wait) {
1659
+ /* Fallback: single-fd select. */
1660
+ rb_fdset_t rfds, wfds, efds;
1661
+ rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1662
+ int maxfd = -1;
1663
+ if (wait_fd >= 0) {
1664
+ if (wait_what == CURL_POLL_IN || wait_what == CURL_POLL_INOUT) rb_fd_set(wait_fd, &rfds);
1665
+ if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) rb_fd_set(wait_fd, &wfds);
1666
+ rb_fd_set(wait_fd, &efds);
1667
+ maxfd = wait_fd;
1668
+ }
1669
+ int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
1670
+ curb_debugf("[curb.socket] rb_thread_fd_select(single) rc=%d fd=%d", rc, wait_fd);
1671
+ if (rc < 0) {
1672
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1673
+ if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
1674
+ continue;
1675
+ }
1676
+ any_ready = (rc > 0);
1677
+ did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
1678
+ if (any_ready && wait_fd >= 0) {
1679
+ if (rb_fd_isset(wait_fd, &rfds)) ready_flags |= CURL_CSELECT_IN;
1680
+ if (rb_fd_isset(wait_fd, &wfds)) ready_flags |= CURL_CSELECT_OUT;
1681
+ if (rb_fd_isset(wait_fd, &efds)) ready_flags |= CURL_CSELECT_ERR;
1682
+ }
1683
+ rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1684
+ }
1685
+ } else { /* count_tracked == 0 */
1686
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1687
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1688
+ #else
1689
+ rb_thread_wait_for(tv);
1690
+ #endif
1691
+ did_timeout = multi_socket_timer_due(ctx);
1692
+ }
418
1693
 
419
- /* check for finished easy handles and remove from the multi handle */
420
- while ((msg = curl_multi_info_read(multi_handle, &msgs_left))) {
421
- if (msg->msg == CURLMSG_DONE) {
422
- easy_handle = msg->easy_handle;
423
- result = msg->data.result;
424
- if (easy_handle) {
425
- rb_curl_mutli_handle_complete(self, easy_handle, result);
1694
+ if (did_timeout) {
1695
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1696
+ curb_debugf("[curb.socket] socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1697
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1698
+ } else if (any_ready) {
1699
+ if (count_tracked == 1 && wait_fd >= 0) {
1700
+ int flags = ready_flags;
1701
+ if (flags == 0) flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1702
+ #if CURB_SOCKET_DEBUG
1703
+ {
1704
+ char b[32];
1705
+ curb_debugf("[curb.socket] socket_action fd=%d flags=%s", wait_fd, cselect_flags_str(flags, b, sizeof(b)));
1706
+ }
1707
+ #endif
1708
+ mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)wait_fd, flags, &rbcm->running);
1709
+ curb_debugf("[curb.socket] socket_action -> mrc=%d running=%d", mrc, rbcm->running);
1710
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
426
1711
  }
427
1712
  }
1713
+
1714
+ rb_curl_multi_read_info(self, rbcm->handle);
1715
+ curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
1716
+ rb_curl_multi_yield_if_given(self, block);
428
1717
  }
429
1718
  }
430
1719
 
431
- /* called within ruby_curl_multi_perform */
432
- static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running) {
433
- CURLMcode mcode;
1720
+ struct socket_drive_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; VALUE block; };
1721
+ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
1722
+ struct socket_drive_args *a = (struct socket_drive_args *)argp;
1723
+ rb_curl_multi_socket_drive(a->self, a->rbcm, a->ctx, a->block);
1724
+ return Qtrue;
1725
+ }
1726
+ struct socket_cleanup_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
1727
+ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1728
+ struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
1729
+ if (c->rbcm && c->rbcm->handle) {
1730
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_SOCKETFUNCTION, NULL);
1731
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_SOCKETDATA, NULL);
1732
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_TIMERFUNCTION, NULL);
1733
+ curl_multi_setopt(c->rbcm->handle, CURLMOPT_TIMERDATA, NULL);
1734
+ }
1735
+ if (c->ctx && c->ctx->sock_map) {
1736
+ st_free_table(c->ctx->sock_map);
1737
+ c->ctx->sock_map = NULL;
1738
+ }
1739
+ if (c->ctx) {
1740
+ if (!NIL_P(c->ctx->io_cache)) {
1741
+ rb_hash_clear(c->ctx->io_cache);
1742
+ }
1743
+ c->ctx->io_cache = Qnil;
1744
+ }
1745
+ if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_io_cache_ivar)) {
1746
+ rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_io_cache_ivar));
1747
+ }
1748
+ return Qnil;
1749
+ }
434
1750
 
435
- do {
436
- mcode = curl_multi_perform(multi_handle, still_running);
437
- } while (mcode == CURLM_CALL_MULTI_PERFORM);
1751
+ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE self) {
1752
+ ruby_curl_multi *rbcm;
1753
+ VALUE block = Qnil;
1754
+ rb_scan_args(argc, argv, "0&", &block);
438
1755
 
439
- if (mcode != CURLM_OK) {
440
- raise_curl_multi_error_exception(mcode);
1756
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1757
+ ruby_curl_multi_ensure_handle(rbcm);
1758
+ if (!rb_ivar_defined(self, id_deferred_exception_ivar)) {
1759
+ clear_multi_deferred_exception_source_id_if_any(self);
441
1760
  }
442
-
1761
+
1762
+ multi_socket_ctx ctx;
1763
+ ctx.sock_map = st_init_numtable();
1764
+ ctx.timeout_deadline_ms = -1;
1765
+ ctx.io_cache = rb_hash_new();
1766
+ rb_ivar_set(self, id_socket_io_cache_ivar, ctx.io_cache);
1767
+
1768
+ /* install socket/timer callbacks */
1769
+ curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
1770
+ curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETDATA, &ctx);
1771
+ curl_multi_setopt(rbcm->handle, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
1772
+ curl_multi_setopt(rbcm->handle, CURLMOPT_TIMERDATA, &ctx);
1773
+
1774
+ /* run using socket action loop with ensure-cleanup */
1775
+ struct socket_drive_args body_args = { self, rbcm, &ctx, block };
1776
+ struct socket_cleanup_args ensure_args = { self, rbcm, &ctx };
1777
+ rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1778
+
1779
+ /* finalize */
1780
+ rb_curl_multi_read_info(self, rbcm->handle);
1781
+ rb_curl_multi_yield_if_given(self, block);
1782
+ if (cCurlMutiAutoClose == 1) {
1783
+ rbcm->allow_close_during_perform = 1;
1784
+ rb_funcall(self, rb_intern("_autoclose"), 0);
1785
+ rbcm->allow_close_during_perform = 0;
1786
+ }
1787
+
1788
+ return Qtrue;
443
1789
  }
1790
+ #endif /* socket-action implementation */
444
1791
 
445
1792
  #ifdef _WIN32
446
1793
  void create_crt_fd(fd_set *os_set, fd_set *crt_set)
@@ -471,7 +1818,8 @@ void cleanup_crt_fd(fd_set *os_set, fd_set *crt_set)
471
1818
  }
472
1819
  #endif
473
1820
 
474
- #ifdef HAVE_RB_THREAD_BLOCKING_REGION
1821
+ /* curb_select is only needed when rb_thread_fd_select is NOT available */
1822
+ #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
475
1823
  struct _select_set {
476
1824
  int maxfd;
477
1825
  fd_set *fdread, *fdwrite, *fdexcep;
@@ -483,6 +1831,14 @@ static VALUE curb_select(void *args) {
483
1831
  int rc = select(set->maxfd, set->fdread, set->fdwrite, set->fdexcep, set->tv);
484
1832
  return INT2FIX(rc);
485
1833
  }
1834
+
1835
+ #ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
1836
+ static void *curb_select_without_gvl(void *args) {
1837
+ struct _select_set* set = args;
1838
+ int rc = select(set->maxfd, set->fdread, set->fdwrite, set->fdexcep, set->tv);
1839
+ return (void *)(intptr_t)rc;
1840
+ }
1841
+ #endif
486
1842
  #endif
487
1843
 
488
1844
  /*
@@ -500,34 +1856,49 @@ static VALUE curb_select(void *args) {
500
1856
  *
501
1857
  * Run multi handles, looping selecting when data can be transfered
502
1858
  */
503
- VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1859
+ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
504
1860
  CURLMcode mcode;
505
1861
  ruby_curl_multi *rbcm;
506
- int maxfd, rc;
1862
+ int maxfd, rc = -1;
507
1863
  fd_set fdread, fdwrite, fdexcep;
508
1864
  #ifdef _WIN32
509
1865
  fd_set crt_fdread, crt_fdwrite, crt_fdexcep;
510
1866
  #endif
511
1867
  long timeout_milliseconds;
512
1868
  struct timeval tv = {0, 0};
1869
+ struct timeval tv_100ms = {0, 100000};
513
1870
  VALUE block = Qnil;
514
- #ifdef HAVE_RB_THREAD_BLOCKING_REGION
1871
+ #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
515
1872
  struct _select_set fdset_args;
516
1873
  #endif
517
1874
 
518
1875
  rb_scan_args(argc, argv, "0&", &block);
519
1876
 
520
- Data_Get_Struct(self, ruby_curl_multi, rbcm);
1877
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1878
+ ruby_curl_multi_ensure_handle(rbcm);
1879
+ if (!rb_ivar_defined(self, id_deferred_exception_ivar)) {
1880
+ clear_multi_deferred_exception_source_id_if_any(self);
1881
+ }
521
1882
 
522
1883
  timeout_milliseconds = cCurlMutiDefaulttimeout;
523
1884
 
1885
+ // Run curl_multi_perform for the first time to get the ball rolling
524
1886
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1887
+
1888
+ // Check the easy handles for new messages one more time before yielding
1889
+ // control to passed ruby block.
1890
+ //
1891
+ // This call will block until all queued messages are processed and if any
1892
+ // handle completed the transfer we will run the on_complete callback here too.
525
1893
  rb_curl_multi_read_info( self, rbcm->handle );
526
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
527
-
1894
+
1895
+ // There are no more messages to handle by curl and we can run the ruby block
1896
+ // passed to perform method.
1897
+ // When the block completes curl will resume.
1898
+ rb_curl_multi_yield_if_given(self, block);
1899
+
528
1900
  do {
529
1901
  while (rbcm->running) {
530
-
531
1902
  #ifdef HAVE_CURL_MULTI_TIMEOUT
532
1903
  /* get the curl suggested time out */
533
1904
  mcode = curl_multi_timeout(rbcm->handle, &timeout_milliseconds);
@@ -542,7 +1913,12 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
542
1913
  if (timeout_milliseconds == 0) { /* no delay */
543
1914
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
544
1915
  rb_curl_multi_read_info( self, rbcm->handle );
545
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
1916
+ rb_curl_multi_yield_if_given(self, block);
1917
+ #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1918
+ if (rb_fiber_scheduler_current() != Qnil) {
1919
+ rb_thread_schedule();
1920
+ }
1921
+ #endif
546
1922
  continue;
547
1923
  }
548
1924
 
@@ -551,6 +1927,46 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
551
1927
  /* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
552
1928
  }
553
1929
 
1930
+ #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
1931
+ {
1932
+ struct wait_args wait_args;
1933
+ wait_args.handle = rbcm->handle;
1934
+ wait_args.timeout_ms = timeout_milliseconds;
1935
+ wait_args.numfds = 0;
1936
+ /*
1937
+ * When a Fiber scheduler is available (Ruby >= 3.x), rb_thread_fd_select
1938
+ * integrates with it. If we have rb_thread_fd_select available at build
1939
+ * time, we avoid curl_multi_wait entirely (see preprocessor guard above)
1940
+ * and use the fdset branch below. Otherwise, we use curl_multi_wait and
1941
+ * release the GVL so Ruby threads can continue to run.
1942
+ */
1943
+ CURLMcode wait_rc;
1944
+ #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
1945
+ wait_rc = (CURLMcode)(intptr_t)rb_thread_call_without_gvl(
1946
+ curl_multi_wait_wrapper, &wait_args, RUBY_UBF_IO, NULL
1947
+ );
1948
+ #else
1949
+ wait_rc = curl_multi_wait(rbcm->handle, NULL, 0, timeout_milliseconds, &wait_args.numfds);
1950
+ #endif
1951
+ if (wait_rc != CURLM_OK) {
1952
+ raise_curl_multi_error_exception(wait_rc);
1953
+ }
1954
+ if (wait_args.numfds == 0) {
1955
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1956
+ struct timeval tv_sleep = tv_100ms;
1957
+ /* Sleep in a scheduler-aware way. */
1958
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1959
+ #else
1960
+ rb_thread_wait_for(tv_100ms);
1961
+ #endif
1962
+ }
1963
+ /* Process pending transfers after waiting */
1964
+ rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
1965
+ rb_curl_multi_read_info(self, rbcm->handle);
1966
+ rb_curl_multi_yield_if_given(self, block);
1967
+ }
1968
+ #else
1969
+
554
1970
  tv.tv_sec = 0; /* never wait longer than 1 second */
555
1971
  tv.tv_usec = (int)(timeout_milliseconds * 1000); /* XXX: int is the right type for OSX, what about linux? */
556
1972
 
@@ -564,21 +1980,68 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
564
1980
  raise_curl_multi_error_exception(mcode);
565
1981
  }
566
1982
 
1983
+ if (maxfd == -1) {
1984
+ /* libcurl recommends sleeping for 100ms */
1985
+ #ifdef HAVE_RB_THREAD_FD_SELECT
1986
+ struct timeval tv_sleep = tv_100ms;
1987
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1988
+ #else
1989
+ rb_thread_wait_for(tv_100ms);
1990
+ #endif
1991
+ rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1992
+ rb_curl_multi_read_info( self, rbcm->handle );
1993
+ rb_curl_multi_yield_if_given(self, block);
1994
+ continue;
1995
+ }
1996
+
567
1997
  #ifdef _WIN32
568
1998
  create_crt_fd(&fdread, &crt_fdread);
569
1999
  create_crt_fd(&fdwrite, &crt_fdwrite);
570
2000
  create_crt_fd(&fdexcep, &crt_fdexcep);
571
2001
  #endif
572
2002
 
573
- #ifdef HAVE_RB_THREAD_BLOCKING_REGION
2003
+
2004
+ #if !defined(HAVE_RB_THREAD_FD_SELECT) && (defined(HAVE_RB_THREAD_BLOCKING_REGION) || defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL))
574
2005
  fdset_args.maxfd = maxfd+1;
575
2006
  fdset_args.fdread = &fdread;
576
2007
  fdset_args.fdwrite = &fdwrite;
577
2008
  fdset_args.fdexcep = &fdexcep;
578
2009
  fdset_args.tv = &tv;
2010
+ #endif
2011
+
2012
+ #ifdef HAVE_RB_THREAD_FD_SELECT
2013
+ /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
2014
+ {
2015
+ rb_fdset_t rfds, wfds, efds;
2016
+ rb_fd_init(&rfds);
2017
+ rb_fd_init(&wfds);
2018
+ rb_fd_init(&efds);
2019
+ #ifdef _WIN32
2020
+ /* On Windows, iterate explicit fd arrays for CRT fds. */
2021
+ int i;
2022
+ for (i = 0; i < crt_fdread.fd_count; i++) rb_fd_set(crt_fdread.fd_array[i], &rfds);
2023
+ for (i = 0; i < crt_fdwrite.fd_count; i++) rb_fd_set(crt_fdwrite.fd_array[i], &wfds);
2024
+ for (i = 0; i < crt_fdexcep.fd_count; i++) rb_fd_set(crt_fdexcep.fd_array[i], &efds);
2025
+ rc = rb_thread_fd_select(0, &rfds, &wfds, &efds, &tv);
2026
+ #else
2027
+ int fd;
2028
+ for (fd = 0; fd <= maxfd; fd++) {
2029
+ if (FD_ISSET(fd, &fdread)) rb_fd_set(fd, &rfds);
2030
+ if (FD_ISSET(fd, &fdwrite)) rb_fd_set(fd, &wfds);
2031
+ if (FD_ISSET(fd, &fdexcep)) rb_fd_set(fd, &efds);
2032
+ }
2033
+ rc = rb_thread_fd_select(maxfd+1, &rfds, &wfds, &efds, &tv);
2034
+ #endif
2035
+ rb_fd_term(&rfds);
2036
+ rb_fd_term(&wfds);
2037
+ rb_fd_term(&efds);
2038
+ }
2039
+ #elif defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
2040
+ rc = (int)(intptr_t) rb_thread_call_without_gvl(curb_select_without_gvl, &fdset_args, RUBY_UBF_IO, 0);
2041
+ #elif HAVE_RB_THREAD_BLOCKING_REGION
579
2042
  rc = rb_thread_blocking_region(curb_select, &fdset_args, RUBY_UBF_IO, 0);
580
2043
  #else
581
- rc = rb_thread_select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv);
2044
+ rc = select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv);
582
2045
  #endif
583
2046
 
584
2047
  #ifdef _WIN32
@@ -597,39 +2060,162 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
597
2060
  default: /* action */
598
2061
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
599
2062
  rb_curl_multi_read_info( self, rbcm->handle );
600
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
2063
+ rb_curl_multi_yield_if_given(self, block);
601
2064
  break;
602
2065
  }
2066
+ #endif /* disabled curl_multi_wait: use fdsets */
603
2067
  }
604
2068
 
2069
+ rb_curl_multi_read_info( self, rbcm->handle );
2070
+ rb_curl_multi_yield_if_given(self, block);
605
2071
  } while( rbcm->running );
606
2072
 
607
- rb_curl_multi_read_info( self, rbcm->handle );
608
- if (block != Qnil) { rb_funcall(block, rb_intern("call"), 1, self); }
609
-
2073
+ if (cCurlMutiAutoClose == 1) {
2074
+ rbcm->allow_close_during_perform = 1;
2075
+ rb_funcall(self, rb_intern("_autoclose"), 0);
2076
+ rbcm->allow_close_during_perform = 0;
2077
+ }
610
2078
  return Qtrue;
611
2079
  }
612
2080
 
2081
+ struct multi_perform_call_args {
2082
+ int argc;
2083
+ VALUE *argv;
2084
+ VALUE self;
2085
+ VALUE result;
2086
+ VALUE (*func)(int, VALUE *, VALUE);
2087
+ };
2088
+
2089
+ static VALUE ruby_curl_multi_perform_guard_body(VALUE argp) {
2090
+ struct multi_perform_call_args *args = (struct multi_perform_call_args *)argp;
2091
+ args->result = args->func(args->argc, args->argv, args->self);
2092
+ return args->result;
2093
+ }
2094
+
2095
+ static VALUE ruby_curl_multi_perform_guard_ensure(VALUE self) {
2096
+ ruby_curl_multi *rbcm;
2097
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
2098
+ rbcm->perform_active = 0;
2099
+ rbcm->callback_active = 0;
2100
+ rbcm->allow_close_during_perform = 0;
2101
+ return Qnil;
2102
+ }
2103
+
2104
+ static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE self, VALUE (*func)(int, VALUE *, VALUE)) {
2105
+ ruby_curl_multi *rbcm;
2106
+ struct multi_perform_call_args args;
2107
+
2108
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
2109
+ if (rbcm->perform_active) {
2110
+ rb_raise(rb_eRuntimeError, "Cannot recursively perform an active Curl::Multi handle");
2111
+ }
2112
+
2113
+ rbcm->perform_active = 1;
2114
+ args.argc = argc;
2115
+ args.argv = argv;
2116
+ args.self = self;
2117
+ args.result = Qnil;
2118
+ args.func = func;
2119
+
2120
+ return rb_ensure(ruby_curl_multi_perform_guard_body, (VALUE)&args,
2121
+ ruby_curl_multi_perform_guard_ensure, self);
2122
+ }
2123
+
2124
+ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
2125
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
2126
+ }
2127
+
2128
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2129
+ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
2130
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
2131
+ }
2132
+ #endif
2133
+
2134
+ /*
2135
+ * call-seq:
2136
+ *
2137
+ * multi.close
2138
+ * after closing the multi handle all connections will be closed and the handle will no longer be usable
2139
+ *
2140
+ */
2141
+ VALUE ruby_curl_multi_close(VALUE self) {
2142
+ ruby_curl_multi *rbcm;
2143
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
2144
+
2145
+ if ((rbcm->perform_active || rbcm->callback_active) && !rbcm->allow_close_during_perform) {
2146
+ rb_raise(rb_eRuntimeError, "Cannot close an active Curl::Multi handle during perform");
2147
+ }
2148
+
2149
+ rb_curl_multi_detach_all(rbcm);
2150
+
2151
+ if (rbcm->handle) {
2152
+ curl_multi_cleanup(rbcm->handle);
2153
+ rbcm->handle = NULL;
2154
+ }
2155
+
2156
+ rbcm->active = 0;
2157
+ rbcm->running = 0;
2158
+ clear_multi_deferred_exception_if_any(self);
2159
+ clear_multi_deferred_exception_source_id_if_any(self);
2160
+ return self;
2161
+ }
2162
+
2163
+ static VALUE ruby_curl_multi_mark_closed(VALUE self) {
2164
+ ruby_curl_multi *rbcm;
2165
+
2166
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
2167
+ rbcm->closed = 1;
2168
+
2169
+ return self;
2170
+ }
2171
+
2172
+ /* GC mark: keep attached easy VALUEs alive while associated. */
2173
+ static int mark_attached_i(st_data_t key, st_data_t val, st_data_t arg) {
2174
+ VALUE easy = (VALUE)val;
2175
+ if (!NIL_P(easy)) rb_gc_mark(easy);
2176
+ return ST_CONTINUE;
2177
+ }
2178
+
2179
+ static void curl_multi_mark(void *ptr) {
2180
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
2181
+ if (!rbcm) return;
2182
+ if (rbcm->attached) {
2183
+ st_foreach(rbcm->attached, mark_attached_i, (st_data_t)0);
2184
+ }
2185
+ }
2186
+
2187
+
613
2188
  /* =================== INIT LIB =====================*/
614
2189
  void init_curb_multi() {
615
2190
  idCall = rb_intern("call");
616
-
2191
+ id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
2192
+ id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2193
+ id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
617
2194
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
618
2195
 
2196
+ rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
2197
+
619
2198
  /* Class methods */
620
- rb_define_singleton_method(cCurlMulti, "new", ruby_curl_multi_new, 0);
621
2199
  rb_define_singleton_method(cCurlMulti, "default_timeout=", ruby_curl_multi_set_default_timeout, 1);
622
2200
  rb_define_singleton_method(cCurlMulti, "default_timeout", ruby_curl_multi_get_default_timeout, 0);
623
-
624
- /* "Attributes" */
625
- rb_define_method(cCurlMulti, "requests", ruby_curl_multi_requests, 0);
626
- rb_define_method(cCurlMulti, "idle?", ruby_curl_multi_idle, 0);
627
-
628
- /* Instnace methods */
2201
+ rb_define_singleton_method(cCurlMulti, "autoclose=", ruby_curl_multi_set_autoclose, 1);
2202
+ rb_define_singleton_method(cCurlMulti, "autoclose", ruby_curl_multi_get_autoclose, 0);
2203
+ /* Instance methods */
2204
+ rb_define_method(cCurlMulti, "initialize", ruby_curl_multi_initialize, 0);
629
2205
  rb_define_method(cCurlMulti, "max_connects=", ruby_curl_multi_max_connects, 1);
2206
+ rb_define_method(cCurlMulti, "max_host_connections=", ruby_curl_multi_max_host_connections, 1);
630
2207
  rb_define_method(cCurlMulti, "pipeline=", ruby_curl_multi_pipeline, 1);
631
- rb_define_method(cCurlMulti, "add", ruby_curl_multi_add, 1);
632
- rb_define_method(cCurlMulti, "remove", ruby_curl_multi_remove, 1);
633
- rb_define_method(cCurlMulti, "cancel!", ruby_curl_multi_cancel, 0);
2208
+ rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
2209
+ rb_define_method(cCurlMulti, "_remove", ruby_curl_multi_remove, 1);
2210
+ /*
2211
+ * The legacy fdset loop is the stable default. The newer socket-action path
2212
+ * is kept in-tree, but it has shown scheduler regressions for one-handle
2213
+ * multi usage (for example Curl::Easy#perform under Async).
2214
+ */
634
2215
  rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, -1);
2216
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2217
+ rb_define_private_method(cCurlMulti, "_socket_perform", ruby_curl_multi_socket_perform, -1);
2218
+ #endif
2219
+ rb_define_method(cCurlMulti, "_close", ruby_curl_multi_close, 0);
2220
+ rb_define_private_method(cCurlMulti, "_mark_closed", ruby_curl_multi_mark_closed, 0);
635
2221
  }