curb 1.3.4 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +18 -1
- data/ext/curb.h +3 -3
- data/ext/curb_multi.c +284 -74
- data/ext/extconf.rb +1 -0
- data/tests/tc_curl_multi.rb +35 -0
- data/tests/tc_fiber_scheduler.rb +23 -5
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a502fb4afad21a24a20302b7522b616b08afbbe3d4b5b2a3eeee46d83b53c50d
|
|
4
|
+
data.tar.gz: 5e68fd3f3380f2045dfc6d4e8e157ebbb9fc56e4b76601574c92a755665eeef2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 940f343ad1d926ddcf27b994bca191e131176e766dce3d7f211f9ed5b28917ffb9ebfa5b8f71247ac8ca8a0b916e6565b60fb2ae57a964770b3273d1bd7d5d69
|
|
7
|
+
data.tar.gz: 77a44dd831eea50e786d4b4871c2f1702e3bee2c74d0484f7b0812a91d19d4bcdd79c12e20dce7cd8ffcbaf0e06ae77cc6cc7734b7c71fa2feabc636af82b0d8
|
data/Rakefile
CHANGED
|
@@ -106,7 +106,24 @@ else
|
|
|
106
106
|
task :alltests => [:unittests, :bugtests]
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
ruby_memcheck_config = { binary_name: 'curb_core' }
|
|
110
|
+
|
|
111
|
+
if RUBY_ENGINE == 'ruby' && RUBY_VERSION == '4.0.4'
|
|
112
|
+
# Ruby 4.0.4 reports fiber/block-handler VM stack accesses under Valgrind.
|
|
113
|
+
# Keep reporting errors that originate in curb_core, but filter Ruby-side noise.
|
|
114
|
+
ruby_memcheck_config[:filter_all_errors] = true
|
|
115
|
+
if RubyMemcheck::Configuration.instance_method(:initialize).parameters.any? { |type, name|
|
|
116
|
+
type == :key && name == :use_only_ruby_free_at_exit
|
|
117
|
+
}
|
|
118
|
+
ruby_memcheck_config[:use_only_ruby_free_at_exit] = false
|
|
119
|
+
end
|
|
120
|
+
ruby_memcheck_config[:skipped_ruby_functions] =
|
|
121
|
+
RubyMemcheck::Configuration::DEFAULT_SKIPPED_RUBY_FUNCTIONS + [
|
|
122
|
+
/\Arb_vm_frame_block_handler\z/
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
RubyMemcheck.config(**ruby_memcheck_config)
|
|
110
127
|
namespace :test do
|
|
111
128
|
RubyMemcheck::TestTask.new(valgrind: :compile) do|t|
|
|
112
129
|
t.test_files = FileList['tests/tc_*.rb']
|
data/ext/curb.h
CHANGED
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
#include "curb_macros.h"
|
|
29
29
|
|
|
30
30
|
// These should be managed from the Rake 'release' task.
|
|
31
|
-
#define CURB_VERSION "1.3.
|
|
32
|
-
#define CURB_VER_NUM
|
|
31
|
+
#define CURB_VERSION "1.3.5"
|
|
32
|
+
#define CURB_VER_NUM 1035
|
|
33
33
|
#define CURB_VER_MAJ 1
|
|
34
34
|
#define CURB_VER_MIN 3
|
|
35
|
-
#define CURB_VER_MIC
|
|
35
|
+
#define CURB_VER_MIC 5
|
|
36
36
|
#define CURB_VER_PATCH 0
|
|
37
37
|
|
|
38
38
|
|
data/ext/curb_multi.c
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
|
|
29
29
|
#include <errno.h>
|
|
30
30
|
#include <fcntl.h>
|
|
31
|
+
#ifndef _WIN32
|
|
32
|
+
#include <sys/time.h>
|
|
33
|
+
#include <time.h>
|
|
34
|
+
#endif
|
|
31
35
|
#include <stdint.h>
|
|
32
36
|
#include <stdarg.h>
|
|
33
37
|
|
|
@@ -70,6 +74,7 @@ extern VALUE mCurl;
|
|
|
70
74
|
static VALUE idCall;
|
|
71
75
|
static ID id_deferred_exception_ivar;
|
|
72
76
|
static ID id_deferred_exception_source_id_ivar;
|
|
77
|
+
static ID id_socket_io_cache_ivar;
|
|
73
78
|
|
|
74
79
|
#ifdef RDOC_NEVER_DEFINED
|
|
75
80
|
mCurl = rb_define_module("Curl");
|
|
@@ -1070,10 +1075,28 @@ static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_runnin
|
|
|
1070
1075
|
/* ---- socket-action implementation (scheduler-friendly) ---- */
|
|
1071
1076
|
typedef struct {
|
|
1072
1077
|
st_table *sock_map; /* key: int fd, value: int 'what' (CURL_POLL_*) */
|
|
1073
|
-
long
|
|
1078
|
+
long long timeout_deadline_ms; /* absolute deadline for CURL_SOCKET_TIMEOUT */
|
|
1074
1079
|
VALUE io_cache; /* fd -> IO wrapper for fiber-scheduler waits */
|
|
1075
1080
|
} multi_socket_ctx;
|
|
1076
1081
|
|
|
1082
|
+
static long long multi_socket_current_time_ms(void) {
|
|
1083
|
+
#if defined(CLOCK_MONOTONIC)
|
|
1084
|
+
struct timespec ts;
|
|
1085
|
+
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
|
|
1086
|
+
return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000);
|
|
1087
|
+
}
|
|
1088
|
+
#endif
|
|
1089
|
+
|
|
1090
|
+
struct timeval tv;
|
|
1091
|
+
gettimeofday(&tv, NULL);
|
|
1092
|
+
return ((long long)tv.tv_sec * 1000) + (tv.tv_usec / 1000);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
static int multi_socket_timer_due(multi_socket_ctx *ctx) {
|
|
1096
|
+
return ctx && ctx->timeout_deadline_ms >= 0 &&
|
|
1097
|
+
multi_socket_current_time_ms() >= ctx->timeout_deadline_ms;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1077
1100
|
#if CURB_SOCKET_DEBUG
|
|
1078
1101
|
static void curb_debugf(const char *fmt, ...) {
|
|
1079
1102
|
va_list ap;
|
|
@@ -1118,6 +1141,21 @@ static VALUE fiber_io_wait_protected(VALUE argp) {
|
|
|
1118
1141
|
}
|
|
1119
1142
|
#endif
|
|
1120
1143
|
|
|
1144
|
+
#if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
|
|
1145
|
+
struct fiber_io_select_args {
|
|
1146
|
+
VALUE scheduler;
|
|
1147
|
+
VALUE readables;
|
|
1148
|
+
VALUE writables;
|
|
1149
|
+
VALUE exceptables;
|
|
1150
|
+
VALUE timeout;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
static VALUE fiber_io_select_protected(VALUE argp) {
|
|
1154
|
+
struct fiber_io_select_args *a = (struct fiber_io_select_args *)argp;
|
|
1155
|
+
return rb_fiber_scheduler_io_select(a->scheduler, a->readables, a->writables, a->exceptables, a->timeout);
|
|
1156
|
+
}
|
|
1157
|
+
#endif
|
|
1158
|
+
|
|
1121
1159
|
#if defined(RB_INTEGER_TYPE_P)
|
|
1122
1160
|
#define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
|
|
1123
1161
|
#else
|
|
@@ -1187,6 +1225,11 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
|
|
|
1187
1225
|
#endif
|
|
1188
1226
|
} else {
|
|
1189
1227
|
/* store current interest mask for this fd */
|
|
1228
|
+
st_data_t key = (st_data_t)fd;
|
|
1229
|
+
st_data_t old_what;
|
|
1230
|
+
if (st_lookup(ctx->sock_map, key, &old_what) && (int)old_what != what && !NIL_P(ctx->io_cache)) {
|
|
1231
|
+
rb_hash_delete(ctx->io_cache, INT2NUM(fd));
|
|
1232
|
+
}
|
|
1190
1233
|
st_insert(ctx->sock_map, (st_data_t)fd, (st_data_t)what);
|
|
1191
1234
|
#if CURB_SOCKET_DEBUG
|
|
1192
1235
|
{
|
|
@@ -1201,7 +1244,9 @@ static int multi_socket_cb(CURL *easy, curl_socket_t s, int what, void *userp, v
|
|
|
1201
1244
|
static int multi_timer_cb(CURLM *multi, long timeout_ms, void *userp) {
|
|
1202
1245
|
(void)multi;
|
|
1203
1246
|
multi_socket_ctx *ctx = (multi_socket_ctx *)userp;
|
|
1204
|
-
if (ctx)
|
|
1247
|
+
if (ctx) {
|
|
1248
|
+
ctx->timeout_deadline_ms = timeout_ms < 0 ? -1 : multi_socket_current_time_ms() + timeout_ms;
|
|
1249
|
+
}
|
|
1205
1250
|
curb_debugf("[curb.socket] timer_cb timeout_ms=%ld", timeout_ms);
|
|
1206
1251
|
return 0;
|
|
1207
1252
|
}
|
|
@@ -1272,22 +1317,102 @@ static int st_count_i(st_data_t k, st_data_t v, st_data_t argp) {
|
|
|
1272
1317
|
return ST_CONTINUE;
|
|
1273
1318
|
}
|
|
1274
1319
|
|
|
1275
|
-
static
|
|
1320
|
+
static const char *multi_socket_io_mode_for_curl_poll(int what) {
|
|
1321
|
+
if (what == CURL_POLL_IN) return "r";
|
|
1322
|
+
if (what == CURL_POLL_OUT) return "w";
|
|
1323
|
+
return "r+";
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
static VALUE multi_socket_io_for_fd(multi_socket_ctx *ctx, int fd, int what) {
|
|
1276
1327
|
VALUE key = INT2NUM(fd);
|
|
1277
1328
|
VALUE io = rb_hash_aref(ctx->io_cache, key);
|
|
1278
1329
|
if (NIL_P(io)) {
|
|
1279
|
-
io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, key, rb_str_new_cstr(
|
|
1330
|
+
io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, key, rb_str_new_cstr(multi_socket_io_mode_for_curl_poll(what)));
|
|
1280
1331
|
rb_funcall(io, rb_intern("autoclose="), 1, Qfalse);
|
|
1281
1332
|
rb_hash_aset(ctx->io_cache, key, io);
|
|
1282
1333
|
}
|
|
1283
1334
|
return io;
|
|
1284
1335
|
}
|
|
1285
1336
|
|
|
1286
|
-
struct io_for_fd_args { multi_socket_ctx *ctx; int fd; };
|
|
1337
|
+
struct io_for_fd_args { multi_socket_ctx *ctx; int fd; int what; };
|
|
1287
1338
|
static VALUE multi_socket_io_for_fd_protected(VALUE argp) {
|
|
1288
1339
|
struct io_for_fd_args *a = (struct io_for_fd_args *)argp;
|
|
1289
|
-
return multi_socket_io_for_fd(a->ctx, a->fd);
|
|
1340
|
+
return multi_socket_io_for_fd(a->ctx, a->fd, a->what);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
#if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
|
|
1344
|
+
struct build_io_select_arrays_args {
|
|
1345
|
+
multi_socket_ctx *ctx;
|
|
1346
|
+
VALUE readables;
|
|
1347
|
+
VALUE writables;
|
|
1348
|
+
VALUE exceptables;
|
|
1349
|
+
int failed;
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp) {
|
|
1353
|
+
struct build_io_select_arrays_args *a = (struct build_io_select_arrays_args *)argp;
|
|
1354
|
+
int fd = (int)key;
|
|
1355
|
+
int what = (int)val;
|
|
1356
|
+
struct io_for_fd_args io_args = { a->ctx, fd, what };
|
|
1357
|
+
int io_state = 0;
|
|
1358
|
+
VALUE io;
|
|
1359
|
+
|
|
1360
|
+
if (!multi_socket_fd_valid_p(fd)) {
|
|
1361
|
+
a->failed = 1;
|
|
1362
|
+
return ST_STOP;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
|
|
1366
|
+
if (io_state || NIL_P(io)) {
|
|
1367
|
+
if (io_state) {
|
|
1368
|
+
#if CURB_SOCKET_DEBUG
|
|
1369
|
+
VALUE err = rb_errinfo();
|
|
1370
|
+
VALUE msg = rb_obj_as_string(err);
|
|
1371
|
+
curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
|
|
1372
|
+
#endif
|
|
1373
|
+
rb_set_errinfo(Qnil);
|
|
1374
|
+
}
|
|
1375
|
+
a->failed = 1;
|
|
1376
|
+
return ST_STOP;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) rb_ary_push(a->readables, io);
|
|
1380
|
+
if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) rb_ary_push(a->writables, io);
|
|
1381
|
+
rb_ary_push(a->exceptables, io);
|
|
1382
|
+
|
|
1383
|
+
return ST_CONTINUE;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
struct collect_io_select_ready_args {
|
|
1387
|
+
multi_socket_ctx *ctx;
|
|
1388
|
+
VALUE readables;
|
|
1389
|
+
VALUE writables;
|
|
1390
|
+
VALUE exceptables;
|
|
1391
|
+
struct ready_fd *fds;
|
|
1392
|
+
int capacity;
|
|
1393
|
+
int count;
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t argp) {
|
|
1397
|
+
(void)val;
|
|
1398
|
+
struct collect_io_select_ready_args *a = (struct collect_io_select_ready_args *)argp;
|
|
1399
|
+
VALUE io = rb_hash_aref(a->ctx->io_cache, INT2NUM((int)key));
|
|
1400
|
+
int flags = 0;
|
|
1401
|
+
|
|
1402
|
+
if (NIL_P(io)) return ST_CONTINUE;
|
|
1403
|
+
if (RTEST(rb_ary_includes(a->readables, io))) flags |= CURL_CSELECT_IN;
|
|
1404
|
+
if (RTEST(rb_ary_includes(a->writables, io))) flags |= CURL_CSELECT_OUT;
|
|
1405
|
+
if (RTEST(rb_ary_includes(a->exceptables, io))) flags |= CURL_CSELECT_ERR;
|
|
1406
|
+
|
|
1407
|
+
if (flags && a->count < a->capacity) {
|
|
1408
|
+
a->fds[a->count].fd = (int)key;
|
|
1409
|
+
a->fds[a->count].flags = flags;
|
|
1410
|
+
a->count++;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
return ST_CONTINUE;
|
|
1290
1414
|
}
|
|
1415
|
+
#endif
|
|
1291
1416
|
|
|
1292
1417
|
static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
|
|
1293
1418
|
/* prime the state: let libcurl act on timeouts to setup sockets */
|
|
@@ -1299,17 +1424,24 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1299
1424
|
|
|
1300
1425
|
while (rbcm->running) {
|
|
1301
1426
|
struct timeval tv = {0, 0};
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
if (
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1427
|
+
long wait_ms = cCurlMutiDefaulttimeout;
|
|
1428
|
+
|
|
1429
|
+
if (multi_socket_timer_due(ctx)) {
|
|
1430
|
+
mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
|
|
1431
|
+
curb_debugf("[curb.socket] socket_action timeout(due) -> mrc=%d running=%d", mrc, rbcm->running);
|
|
1432
|
+
if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
|
|
1433
|
+
rb_curl_multi_read_info(self, rbcm->handle);
|
|
1434
|
+
rb_curl_multi_yield_if_given(self, block);
|
|
1435
|
+
continue;
|
|
1311
1436
|
}
|
|
1312
1437
|
|
|
1438
|
+
if (ctx->timeout_deadline_ms >= 0) {
|
|
1439
|
+
long long remaining_ms = ctx->timeout_deadline_ms - multi_socket_current_time_ms();
|
|
1440
|
+
if (remaining_ms < wait_ms) wait_ms = remaining_ms < 0 ? 0 : (long)remaining_ms;
|
|
1441
|
+
}
|
|
1442
|
+
tv.tv_sec = wait_ms / 1000;
|
|
1443
|
+
tv.tv_usec = (wait_ms % 1000) * 1000;
|
|
1444
|
+
|
|
1313
1445
|
/* Find a representative fd to wait on (if any). */
|
|
1314
1446
|
int wait_fd = -1;
|
|
1315
1447
|
int wait_what = 0;
|
|
@@ -1333,50 +1465,113 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1333
1465
|
int any_ready = 0;
|
|
1334
1466
|
int ready_flags = 0;
|
|
1335
1467
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1468
|
+
int handled_wait = 0;
|
|
1469
|
+
if (count_tracked > 1) {
|
|
1470
|
+
#if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
|
|
1471
|
+
{
|
|
1472
|
+
VALUE scheduler = rb_fiber_scheduler_current();
|
|
1473
|
+
if (scheduler != Qnil) {
|
|
1474
|
+
VALUE readables = rb_ary_new();
|
|
1475
|
+
VALUE writables = rb_ary_new();
|
|
1476
|
+
VALUE exceptables = rb_ary_new();
|
|
1477
|
+
struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0 };
|
|
1478
|
+
st_foreach(ctx->sock_map, build_io_select_arrays_i, (st_data_t)&build_args);
|
|
1479
|
+
if (!build_args.failed) {
|
|
1480
|
+
double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
|
|
1481
|
+
VALUE timeout = rb_float_new(timeout_s);
|
|
1482
|
+
struct fiber_io_select_args select_args = { scheduler, readables, writables, exceptables, timeout };
|
|
1483
|
+
int state = 0;
|
|
1484
|
+
VALUE ready = rb_protect(fiber_io_select_protected, (VALUE)&select_args, &state);
|
|
1485
|
+
if (state) {
|
|
1486
|
+
#if CURB_SOCKET_DEBUG
|
|
1487
|
+
VALUE err = rb_errinfo();
|
|
1488
|
+
VALUE msg = rb_obj_as_string(err);
|
|
1489
|
+
curb_debugf("[curb.socket] scheduler io_select failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
|
|
1490
|
+
#endif
|
|
1491
|
+
rb_set_errinfo(Qnil);
|
|
1492
|
+
} else {
|
|
1493
|
+
handled_wait = 1;
|
|
1494
|
+
any_ready = RB_TYPE_P(ready, T_ARRAY);
|
|
1495
|
+
did_timeout = !any_ready && multi_socket_timer_due(ctx);
|
|
1496
|
+
if (any_ready) {
|
|
1497
|
+
VALUE ready_readables = rb_ary_entry(ready, 0);
|
|
1498
|
+
VALUE ready_writables = rb_ary_entry(ready, 1);
|
|
1499
|
+
VALUE ready_exceptables = rb_ary_entry(ready, 2);
|
|
1500
|
+
struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
|
|
1501
|
+
struct collect_io_select_ready_args d;
|
|
1502
|
+
int i;
|
|
1503
|
+
if (!RB_TYPE_P(ready_readables, T_ARRAY)) ready_readables = rb_ary_new();
|
|
1504
|
+
if (!RB_TYPE_P(ready_writables, T_ARRAY)) ready_writables = rb_ary_new();
|
|
1505
|
+
if (!RB_TYPE_P(ready_exceptables, T_ARRAY)) ready_exceptables = rb_ary_new();
|
|
1506
|
+
d.ctx = ctx;
|
|
1507
|
+
d.readables = ready_readables;
|
|
1508
|
+
d.writables = ready_writables;
|
|
1509
|
+
d.exceptables = ready_exceptables;
|
|
1510
|
+
d.fds = ready_fds;
|
|
1511
|
+
d.capacity = count_tracked;
|
|
1512
|
+
d.count = 0;
|
|
1513
|
+
st_foreach(ctx->sock_map, collect_io_select_ready_i, (st_data_t)&d);
|
|
1514
|
+
any_ready = (d.count > 0);
|
|
1515
|
+
did_timeout = !any_ready && multi_socket_timer_due(ctx);
|
|
1516
|
+
for (i = 0; i < d.count; i++) {
|
|
1517
|
+
mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
|
|
1518
|
+
if (mrc != CURLM_OK) {
|
|
1519
|
+
xfree(ready_fds);
|
|
1520
|
+
raise_curl_multi_error_exception(mrc);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
xfree(ready_fds);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
#endif
|
|
1530
|
+
if (!handled_wait) {
|
|
1531
|
+
/* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
|
|
1532
|
+
rb_fdset_t rfds, wfds, efds;
|
|
1533
|
+
rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
|
|
1534
|
+
int maxfd = -1;
|
|
1535
|
+
rb_fdset_from_sockmap(ctx->sock_map, &rfds, &wfds, &efds, &maxfd);
|
|
1536
|
+
int rc = rb_thread_fd_select(maxfd + 1, &rfds, &wfds, &efds, &tv);
|
|
1537
|
+
curb_debugf("[curb.socket] rb_thread_fd_select(multi) rc=%d maxfd=%d", rc, maxfd);
|
|
1538
|
+
if (rc < 0) {
|
|
1539
|
+
rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
|
|
1540
|
+
if (errno != EINTR) rb_raise(rb_eRuntimeError, "select(): %s", strerror(errno));
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
any_ready = (rc > 0);
|
|
1544
|
+
did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
|
|
1545
|
+
if (any_ready) {
|
|
1546
|
+
struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
|
|
1547
|
+
struct collect_ready_fd_args d;
|
|
1548
|
+
int i;
|
|
1549
|
+
d.r = &rfds;
|
|
1550
|
+
d.w = &wfds;
|
|
1551
|
+
d.e = &efds;
|
|
1552
|
+
d.fds = ready_fds;
|
|
1553
|
+
d.capacity = count_tracked;
|
|
1554
|
+
d.count = 0;
|
|
1555
|
+
st_foreach(ctx->sock_map, collect_ready_fd_i, (st_data_t)&d);
|
|
1556
|
+
for (i = 0; i < d.count; i++) {
|
|
1557
|
+
mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
|
|
1558
|
+
if (mrc != CURLM_OK) {
|
|
1559
|
+
xfree(ready_fds);
|
|
1560
|
+
rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
|
|
1561
|
+
raise_curl_multi_error_exception(mrc);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
xfree(ready_fds);
|
|
1565
|
+
}
|
|
1566
|
+
rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
|
|
1567
|
+
handled_wait = 1;
|
|
1568
|
+
}
|
|
1569
|
+
} else if (count_tracked == 1) {
|
|
1376
1570
|
#if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
|
|
1377
1571
|
{
|
|
1378
1572
|
VALUE scheduler = rb_fiber_scheduler_current();
|
|
1379
1573
|
if (scheduler != Qnil) {
|
|
1574
|
+
int scheduler_wait_handled = 0;
|
|
1380
1575
|
int events = 0;
|
|
1381
1576
|
if (wait_fd >= 0) {
|
|
1382
1577
|
events = multi_socket_wait_events_for_curl_poll(wait_what);
|
|
@@ -1389,30 +1584,42 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1389
1584
|
#else
|
|
1390
1585
|
rb_thread_wait_for(tv);
|
|
1391
1586
|
#endif
|
|
1392
|
-
did_timeout =
|
|
1587
|
+
did_timeout = multi_socket_timer_due(ctx);
|
|
1588
|
+
scheduler_wait_handled = 1;
|
|
1393
1589
|
} else if (!multi_socket_fd_valid_p(wait_fd)) {
|
|
1394
1590
|
multi_socket_forget_fd(ctx, wait_fd);
|
|
1395
1591
|
did_timeout = 1;
|
|
1592
|
+
scheduler_wait_handled = 1;
|
|
1396
1593
|
} else {
|
|
1397
|
-
struct io_for_fd_args io_args = { ctx, wait_fd };
|
|
1594
|
+
struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
|
|
1398
1595
|
int io_state = 0;
|
|
1399
1596
|
VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
|
|
1400
1597
|
if (io_state || NIL_P(io)) {
|
|
1401
|
-
if (io_state)
|
|
1402
|
-
|
|
1403
|
-
|
|
1598
|
+
if (io_state) {
|
|
1599
|
+
#if CURB_SOCKET_DEBUG
|
|
1600
|
+
VALUE err = rb_errinfo();
|
|
1601
|
+
VALUE msg = rb_obj_as_string(err);
|
|
1602
|
+
curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
|
|
1603
|
+
#endif
|
|
1604
|
+
rb_set_errinfo(Qnil);
|
|
1605
|
+
}
|
|
1404
1606
|
any_ready = 0;
|
|
1405
1607
|
} else {
|
|
1406
1608
|
struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
|
|
1407
1609
|
int state = 0;
|
|
1408
1610
|
VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
|
|
1409
1611
|
if (state) {
|
|
1612
|
+
#if CURB_SOCKET_DEBUG
|
|
1613
|
+
VALUE err = rb_errinfo();
|
|
1614
|
+
VALUE msg = rb_obj_as_string(err);
|
|
1615
|
+
curb_debugf("[curb.socket] scheduler io_wait failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
|
|
1616
|
+
#endif
|
|
1410
1617
|
rb_set_errinfo(Qnil);
|
|
1411
|
-
did_timeout = 1;
|
|
1412
1618
|
any_ready = 0;
|
|
1413
1619
|
} else {
|
|
1620
|
+
scheduler_wait_handled = 1;
|
|
1414
1621
|
any_ready = (ready != Qfalse && !NIL_P(ready));
|
|
1415
|
-
did_timeout = !any_ready;
|
|
1622
|
+
did_timeout = !any_ready && multi_socket_timer_due(ctx);
|
|
1416
1623
|
if (any_ready) {
|
|
1417
1624
|
if (ready == Qtrue) {
|
|
1418
1625
|
ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
|
|
@@ -1420,7 +1627,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1420
1627
|
ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
|
|
1421
1628
|
if (ready_flags == 0) {
|
|
1422
1629
|
any_ready = 0;
|
|
1423
|
-
did_timeout =
|
|
1630
|
+
did_timeout = multi_socket_timer_due(ctx);
|
|
1424
1631
|
}
|
|
1425
1632
|
} else {
|
|
1426
1633
|
ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
|
|
@@ -1429,7 +1636,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1429
1636
|
}
|
|
1430
1637
|
}
|
|
1431
1638
|
}
|
|
1432
|
-
handled_wait = 1;
|
|
1639
|
+
if (scheduler_wait_handled) handled_wait = 1;
|
|
1433
1640
|
}
|
|
1434
1641
|
}
|
|
1435
1642
|
#endif
|
|
@@ -1443,7 +1650,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1443
1650
|
continue;
|
|
1444
1651
|
}
|
|
1445
1652
|
any_ready = (rc != 0);
|
|
1446
|
-
did_timeout = (rc == 0);
|
|
1653
|
+
did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
|
|
1447
1654
|
if (any_ready) ready_flags = multi_socket_cselect_flags_for_wait_events(rc);
|
|
1448
1655
|
handled_wait = 1;
|
|
1449
1656
|
}
|
|
@@ -1467,7 +1674,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1467
1674
|
continue;
|
|
1468
1675
|
}
|
|
1469
1676
|
any_ready = (rc > 0);
|
|
1470
|
-
did_timeout = (rc == 0);
|
|
1677
|
+
did_timeout = (rc == 0 && multi_socket_timer_due(ctx));
|
|
1471
1678
|
if (any_ready && wait_fd >= 0) {
|
|
1472
1679
|
if (rb_fd_isset(wait_fd, &rfds)) ready_flags |= CURL_CSELECT_IN;
|
|
1473
1680
|
if (rb_fd_isset(wait_fd, &wfds)) ready_flags |= CURL_CSELECT_OUT;
|
|
@@ -1481,7 +1688,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1481
1688
|
#else
|
|
1482
1689
|
rb_thread_wait_for(tv);
|
|
1483
1690
|
#endif
|
|
1484
|
-
did_timeout =
|
|
1691
|
+
did_timeout = multi_socket_timer_due(ctx);
|
|
1485
1692
|
}
|
|
1486
1693
|
|
|
1487
1694
|
if (did_timeout) {
|
|
@@ -1516,7 +1723,7 @@ static VALUE ruby_curl_multi_socket_drive_body(VALUE argp) {
|
|
|
1516
1723
|
rb_curl_multi_socket_drive(a->self, a->rbcm, a->ctx, a->block);
|
|
1517
1724
|
return Qtrue;
|
|
1518
1725
|
}
|
|
1519
|
-
struct socket_cleanup_args { ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
|
|
1726
|
+
struct socket_cleanup_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; };
|
|
1520
1727
|
static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
|
|
1521
1728
|
struct socket_cleanup_args *c = (struct socket_cleanup_args *)argp;
|
|
1522
1729
|
if (c->rbcm && c->rbcm->handle) {
|
|
@@ -1532,10 +1739,12 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
|
|
|
1532
1739
|
if (c->ctx) {
|
|
1533
1740
|
if (!NIL_P(c->ctx->io_cache)) {
|
|
1534
1741
|
rb_hash_clear(c->ctx->io_cache);
|
|
1535
|
-
rb_gc_unregister_address(&c->ctx->io_cache);
|
|
1536
1742
|
}
|
|
1537
1743
|
c->ctx->io_cache = Qnil;
|
|
1538
1744
|
}
|
|
1745
|
+
if (!NIL_P(c->self) && rb_ivar_defined(c->self, id_socket_io_cache_ivar)) {
|
|
1746
|
+
rb_funcall(c->self, rb_intern("remove_instance_variable"), 1, ID2SYM(id_socket_io_cache_ivar));
|
|
1747
|
+
}
|
|
1539
1748
|
return Qnil;
|
|
1540
1749
|
}
|
|
1541
1750
|
|
|
@@ -1552,9 +1761,9 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
|
|
|
1552
1761
|
|
|
1553
1762
|
multi_socket_ctx ctx;
|
|
1554
1763
|
ctx.sock_map = st_init_numtable();
|
|
1555
|
-
ctx.
|
|
1764
|
+
ctx.timeout_deadline_ms = -1;
|
|
1556
1765
|
ctx.io_cache = rb_hash_new();
|
|
1557
|
-
|
|
1766
|
+
rb_ivar_set(self, id_socket_io_cache_ivar, ctx.io_cache);
|
|
1558
1767
|
|
|
1559
1768
|
/* install socket/timer callbacks */
|
|
1560
1769
|
curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
|
|
@@ -1564,7 +1773,7 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
|
|
|
1564
1773
|
|
|
1565
1774
|
/* run using socket action loop with ensure-cleanup */
|
|
1566
1775
|
struct socket_drive_args body_args = { self, rbcm, &ctx, block };
|
|
1567
|
-
struct socket_cleanup_args ensure_args = { rbcm, &ctx };
|
|
1776
|
+
struct socket_cleanup_args ensure_args = { self, rbcm, &ctx };
|
|
1568
1777
|
rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
|
|
1569
1778
|
|
|
1570
1779
|
/* finalize */
|
|
@@ -1857,10 +2066,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
|
|
|
1857
2066
|
#endif /* disabled curl_multi_wait: use fdsets */
|
|
1858
2067
|
}
|
|
1859
2068
|
|
|
2069
|
+
rb_curl_multi_read_info( self, rbcm->handle );
|
|
2070
|
+
rb_curl_multi_yield_if_given(self, block);
|
|
1860
2071
|
} while( rbcm->running );
|
|
1861
2072
|
|
|
1862
|
-
rb_curl_multi_read_info( self, rbcm->handle );
|
|
1863
|
-
rb_curl_multi_yield_if_given(self, block);
|
|
1864
2073
|
if (cCurlMutiAutoClose == 1) {
|
|
1865
2074
|
rbcm->allow_close_during_perform = 1;
|
|
1866
2075
|
rb_funcall(self, rb_intern("_autoclose"), 0);
|
|
@@ -1981,6 +2190,7 @@ void init_curb_multi() {
|
|
|
1981
2190
|
idCall = rb_intern("call");
|
|
1982
2191
|
id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
|
|
1983
2192
|
id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
|
|
2193
|
+
id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
|
|
1984
2194
|
cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
|
|
1985
2195
|
|
|
1986
2196
|
rb_define_alloc_func(cCurlMulti, ruby_curl_multi_alloc);
|
data/ext/extconf.rb
CHANGED
|
@@ -691,6 +691,7 @@ have_func('rb_wait_for_single_fd', 'ruby/io.h')
|
|
|
691
691
|
have_header('ruby/fiber/scheduler.h')
|
|
692
692
|
have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
|
|
693
693
|
have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
|
|
694
|
+
have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
|
|
694
695
|
have_func('rb_io_stdio_file')
|
|
695
696
|
have_func('curl_multi_wait')
|
|
696
697
|
have_func('curl_multi_socket_action')
|
data/tests/tc_curl_multi.rb
CHANGED
|
@@ -249,6 +249,41 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
|
249
249
|
m.close if m
|
|
250
250
|
end
|
|
251
251
|
|
|
252
|
+
def test_multi_perform_runs_work_added_from_final_idle_yield
|
|
253
|
+
with_queue_refill_test_server do |port, hits|
|
|
254
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
255
|
+
Curl::Multi.autoclose = true
|
|
256
|
+
|
|
257
|
+
multi = Curl::Multi.new
|
|
258
|
+
slow = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
|
|
259
|
+
queued = Curl::Easy.new("http://127.0.0.1:#{port}/queued")
|
|
260
|
+
completions = []
|
|
261
|
+
empty_yields = 0
|
|
262
|
+
queued_added = false
|
|
263
|
+
|
|
264
|
+
slow.on_complete { completions << :slow }
|
|
265
|
+
queued.on_complete { completions << :queued }
|
|
266
|
+
|
|
267
|
+
multi.add(slow)
|
|
268
|
+
multi.perform do |performing_multi|
|
|
269
|
+
empty_yields += 1 if performing_multi.requests.empty?
|
|
270
|
+
if !queued_added && empty_yields >= 2
|
|
271
|
+
queued_added = true
|
|
272
|
+
performing_multi.add(queued)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
assert queued_added, "test should add queued work from the final idle yield"
|
|
277
|
+
assert_equal [:slow, :queued], completions
|
|
278
|
+
assert_equal 1, hits[:slow]
|
|
279
|
+
assert_equal 1, hits[:queued]
|
|
280
|
+
assert_equal 0, multi.requests.length
|
|
281
|
+
ensure
|
|
282
|
+
Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
|
|
283
|
+
multi.close if defined?(multi) && multi
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
252
287
|
def test_multi_easy_get
|
|
253
288
|
n = 1
|
|
254
289
|
urls = []
|
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -15,10 +15,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
15
15
|
include TestServerMethods
|
|
16
16
|
|
|
17
17
|
class RecordingScheduler
|
|
18
|
-
attr_reader :io_wait_events
|
|
18
|
+
attr_reader :io_wait_events, :io_select_calls
|
|
19
19
|
|
|
20
20
|
def initialize
|
|
21
21
|
@io_wait_events = []
|
|
22
|
+
@io_select_calls = 0
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def fiber(&block)
|
|
@@ -31,7 +32,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
31
32
|
|
|
32
33
|
readers = (events & IO::READABLE) != 0 ? [io] : nil
|
|
33
34
|
writers = (events & IO::WRITABLE) != 0 ? [io] : nil
|
|
34
|
-
readable, writable = IO.select(readers, writers, nil, timeout)
|
|
35
|
+
readable, writable = blocking_io { IO.select(readers, writers, nil, timeout) }
|
|
35
36
|
|
|
36
37
|
ready = 0
|
|
37
38
|
ready |= IO::READABLE if readable && !readable.empty?
|
|
@@ -39,6 +40,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
39
40
|
ready.zero? ? false : ready
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def io_select(readers, writers, excepts, timeout = nil)
|
|
44
|
+
@io_select_calls += 1
|
|
45
|
+
blocking_io { IO.select(readers, writers, excepts, timeout) }
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
def kernel_sleep(duration = nil)
|
|
43
49
|
sleep(duration || 0)
|
|
44
50
|
end
|
|
@@ -56,6 +62,16 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
56
62
|
|
|
57
63
|
def fiber_interrupt(*)
|
|
58
64
|
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def blocking_io(&block)
|
|
69
|
+
if Fiber.respond_to?(:blocking)
|
|
70
|
+
Fiber.blocking(&block)
|
|
71
|
+
else
|
|
72
|
+
block.call
|
|
73
|
+
end
|
|
74
|
+
end
|
|
59
75
|
end
|
|
60
76
|
|
|
61
77
|
ITERS = 4
|
|
@@ -208,9 +224,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
208
224
|
end
|
|
209
225
|
|
|
210
226
|
assert_equal 200, result
|
|
211
|
-
assert_operator scheduler.io_wait_events.length, :>=, 1
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
assert_operator scheduler.io_wait_events.length + scheduler.io_select_calls, :>=, 1
|
|
228
|
+
unless scheduler.io_wait_events.empty?
|
|
229
|
+
assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
|
|
230
|
+
assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
|
|
231
|
+
end
|
|
214
232
|
end
|
|
215
233
|
|
|
216
234
|
def test_multi_reuse_after_scheduler_perform
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: curb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ross Bamford
|
|
8
8
|
- Todd A. Fisher
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
|
|
14
14
|
for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
|
|
@@ -107,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
108
|
version: '0'
|
|
109
109
|
requirements: []
|
|
110
|
-
rubygems_version: 4.0.
|
|
110
|
+
rubygems_version: 4.0.10
|
|
111
111
|
specification_version: 4
|
|
112
112
|
summary: Ruby libcurl bindings
|
|
113
113
|
test_files:
|