curb 0.8.5 → 1.3.6

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