patron 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +99 -1
- data/ext/patron/session_ext.c +697 -71
- data/lib/patron/request.rb +26 -5
- data/lib/patron/response.rb +2 -54
- data/lib/patron/response_decoding.rb +1 -18
- data/lib/patron/response_head.rb +109 -0
- data/lib/patron/response_stream.rb +217 -0
- data/lib/patron/session.rb +150 -19
- data/lib/patron/sse_client.rb +327 -0
- data/lib/patron/transfer_stats.rb +62 -0
- data/lib/patron/version.rb +1 -1
- data/lib/patron.rb +1 -0
- metadata +6 -2
data/ext/patron/session_ext.c
CHANGED
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* Thread#raise, Timeout and interpreter exit return promptly from any phase of a
|
|
14
14
|
* transfer, including DNS and connect.
|
|
15
15
|
*
|
|
16
|
+
* The same drive loop serves both shapes of request. A buffered request runs to completion
|
|
17
|
+
* and appends the body to a String. A streamed one (Session#stream) is driven by its consumer:
|
|
18
|
+
* transfer_start runs until the response head is complete, and every transfer_read runs it
|
|
19
|
+
* again until some body bytes have landed in the String the caller handed in. Between two
|
|
20
|
+
* reads the transfer is either paused or simply not driven, so no Ruby code ever runs
|
|
21
|
+
* inside libcurl on behalf of the consumer.
|
|
22
|
+
*
|
|
16
23
|
* Requires Ruby >= 3.3 and libcurl >= 7.81.0.
|
|
17
24
|
*/
|
|
18
25
|
#include <ruby.h>
|
|
@@ -24,6 +31,7 @@
|
|
|
24
31
|
|
|
25
32
|
#include <stdio.h>
|
|
26
33
|
#include <string.h>
|
|
34
|
+
#include <time.h>
|
|
27
35
|
#include <signal.h>
|
|
28
36
|
#include <unistd.h>
|
|
29
37
|
#include <sys/stat.h>
|
|
@@ -38,6 +46,7 @@
|
|
|
38
46
|
|
|
39
47
|
#define INTERRUPT_ABORT 1
|
|
40
48
|
#define INTERRUPT_DOWNLOAD_OVERFLOW 2
|
|
49
|
+
#define INTERRUPT_REWIND_FAILED 3
|
|
41
50
|
|
|
42
51
|
/* How long one curl_multi_poll() may sleep. libcurl wakes us earlier on socket activity,
|
|
43
52
|
* on its own timers, and on curl_multi_wakeup(); this is only the upper bound. */
|
|
@@ -63,6 +72,7 @@ static VALUE eTooManyRedirects;
|
|
|
63
72
|
static VALUE eAborted;
|
|
64
73
|
static VALUE eDownloadLimitExceeded;
|
|
65
74
|
static VALUE eClientCertificateError;
|
|
75
|
+
static VALUE eFiberError;
|
|
66
76
|
|
|
67
77
|
/* Frozen, Ractor-shareable lookup tables exposed as Patron::Request::SSL_VERSIONS and
|
|
68
78
|
* Patron::Request::HTTP_VERSIONS: version name => libcurl constant. */
|
|
@@ -76,15 +86,16 @@ static VALUE hHTTPVersions;
|
|
|
76
86
|
X(insecure) X(ssl_version) X(http_version) X(ssl_cert) X(ssl_cert_type) \
|
|
77
87
|
X(ssl_key) X(ssl_key_password) X(cacert) X(buffer_size) X(download_byte_limit) \
|
|
78
88
|
X(progress_callback) X(automatic_content_encoding) X(force_ipv4) \
|
|
79
|
-
X(upload_data) X(file_name) X(multipart)
|
|
89
|
+
X(upload_data) X(file_name) X(multipart) X(read_timeout)
|
|
80
90
|
|
|
81
91
|
#define DECLARE_ID(name) static ID id_##name;
|
|
82
92
|
REQUEST_ATTRIBUTES(DECLARE_ID)
|
|
83
93
|
#undef DECLARE_ID
|
|
84
94
|
|
|
85
95
|
static ID id_to_path, id_to_s, id_response_class, id_default_response_charset;
|
|
96
|
+
static ID id_read, id_readpartial, id_size, id_pos, id_seek, id_rewind;
|
|
86
97
|
static ID id_get, id_post, id_put, id_patch, id_delete, id_head;
|
|
87
|
-
static ID id_ivar_curl_code;
|
|
98
|
+
static ID id_ivar_curl_code, id_raise;
|
|
88
99
|
static ID id_url_kw, id_status_kw, id_redirect_count_kw, id_header_data_kw, id_body_kw,
|
|
89
100
|
id_default_charset_kw, id_timings_kw;
|
|
90
101
|
|
|
@@ -103,14 +114,36 @@ struct patron_session {
|
|
|
103
114
|
CURL *easy;
|
|
104
115
|
curl_mime *mime;
|
|
105
116
|
struct curl_slist *headers;
|
|
106
|
-
FILE *download_file;
|
|
107
117
|
FILE *request_body_file;
|
|
118
|
+
curl_off_t request_content_length; /* from a Content-Length request header, or -1 */
|
|
119
|
+
int request_transfer_encoding; /* whether the request headers carry a Transfer-Encoding */
|
|
120
|
+
curl_off_t upload_io_origin; /* position of upload_io when the request started */
|
|
121
|
+
int upload_io_partial; /* whether upload_io gets #readpartial rather than #read */
|
|
108
122
|
char error_buf[CURL_ERROR_SIZE];
|
|
109
123
|
curl_off_t download_byte_limit;
|
|
110
124
|
VALUE progress_proc;
|
|
111
125
|
VALUE upload_data; /* pinned while libcurl reads it through CURLOPT_POSTFIELDS */
|
|
112
126
|
VALUE header_buf; /* binary String receiving the raw headers */
|
|
113
127
|
VALUE body_buf; /* binary String receiving the body, or nil when downloading to a file */
|
|
128
|
+
VALUE upload_io; /* object responding to #read the body is streamed from, or nil */
|
|
129
|
+
VALUE upload_buf; /* String upload_io reads into, copied into libcurl's buffer */
|
|
130
|
+
|
|
131
|
+
/* Per request, the state the drive loop and the streaming primitives share */
|
|
132
|
+
int streaming; /* the body goes to read_buf instead of body_buf */
|
|
133
|
+
int paused; /* body_write_cb answered CURL_WRITEFUNC_PAUSE */
|
|
134
|
+
int head_complete; /* the final response head is in header_buf */
|
|
135
|
+
int done; /* CURLMSG_DONE was seen, `result` holds its code */
|
|
136
|
+
int read_timed_out;
|
|
137
|
+
CURLcode result;
|
|
138
|
+
long head_length; /* header_buf bytes up to head-complete; trailers follow */
|
|
139
|
+
size_t redeliver_skip; /* bytes at the start of the next delivery already handed out */
|
|
140
|
+
long read_limit; /* length read_buf may grow to, -1 for unbounded */
|
|
141
|
+
double read_timeout; /* seconds allowed between two deliveries, 0 for none */
|
|
142
|
+
double last_activity; /* monotonic seconds of the last delivery */
|
|
143
|
+
VALUE read_buf; /* the caller's String, set only during transfer_read */
|
|
144
|
+
VALUE owner; /* the Fiber that started the transfer */
|
|
145
|
+
VALUE owner_thread; /* and its Thread */
|
|
146
|
+
int in_scheduler_wait; /* the owner is parked in the Fiber scheduler by wait_with_scheduler() */
|
|
114
147
|
|
|
115
148
|
/* A callback may not let a Ruby exception unwind through libcurl's frames. The tag and
|
|
116
149
|
* errinfo are parked here, the transfer is aborted, and perform() re-raises afterwards. */
|
|
@@ -128,7 +161,18 @@ static void session_free(void *ptr) {
|
|
|
128
161
|
struct patron_session *s = ptr;
|
|
129
162
|
int i;
|
|
130
163
|
|
|
131
|
-
/* A
|
|
164
|
+
/* A stream abandoned with its Fiber is still in flight here. No Ruby may run from this
|
|
165
|
+
* point on, and debug_cb would write to an IO that may have been collected already. */
|
|
166
|
+
s->debug_io = Qnil;
|
|
167
|
+
if (s->easy) {
|
|
168
|
+
curl_easy_setopt(s->easy, CURLOPT_VERBOSE, 0L);
|
|
169
|
+
curl_easy_setopt(s->easy, CURLOPT_NOPROGRESS, 1L);
|
|
170
|
+
if (s->multi) curl_multi_remove_handle(s->multi, s->easy);
|
|
171
|
+
curl_easy_cleanup(s->easy);
|
|
172
|
+
}
|
|
173
|
+
if (s->mime) curl_mime_free(s->mime);
|
|
174
|
+
if (s->headers) curl_slist_free_all(s->headers);
|
|
175
|
+
if (s->request_body_file) fclose(s->request_body_file);
|
|
132
176
|
if (s->multi) curl_multi_cleanup(s->multi);
|
|
133
177
|
if (s->base) curl_easy_cleanup(s->base);
|
|
134
178
|
if (s->share) curl_share_cleanup(s->share);
|
|
@@ -144,7 +188,12 @@ static void session_mark(void *ptr) {
|
|
|
144
188
|
rb_gc_mark_movable(s->progress_proc);
|
|
145
189
|
rb_gc_mark_movable(s->header_buf);
|
|
146
190
|
rb_gc_mark_movable(s->body_buf);
|
|
191
|
+
rb_gc_mark_movable(s->upload_io);
|
|
192
|
+
rb_gc_mark_movable(s->upload_buf);
|
|
147
193
|
rb_gc_mark_movable(s->pending_errinfo);
|
|
194
|
+
rb_gc_mark_movable(s->read_buf);
|
|
195
|
+
rb_gc_mark_movable(s->owner);
|
|
196
|
+
rb_gc_mark_movable(s->owner_thread);
|
|
148
197
|
/* libcurl holds RSTRING_PTR() of this one for the whole transfer, and an embedded
|
|
149
198
|
* String keeps its bytes inside the object slot, so it must not be moved. */
|
|
150
199
|
rb_gc_mark(s->upload_data);
|
|
@@ -158,7 +207,12 @@ static void session_compact(void *ptr) {
|
|
|
158
207
|
s->progress_proc = rb_gc_location(s->progress_proc);
|
|
159
208
|
s->header_buf = rb_gc_location(s->header_buf);
|
|
160
209
|
s->body_buf = rb_gc_location(s->body_buf);
|
|
210
|
+
s->upload_io = rb_gc_location(s->upload_io);
|
|
211
|
+
s->upload_buf = rb_gc_location(s->upload_buf);
|
|
161
212
|
s->pending_errinfo = rb_gc_location(s->pending_errinfo);
|
|
213
|
+
s->read_buf = rb_gc_location(s->read_buf);
|
|
214
|
+
s->owner = rb_gc_location(s->owner);
|
|
215
|
+
s->owner_thread = rb_gc_location(s->owner_thread);
|
|
162
216
|
}
|
|
163
217
|
|
|
164
218
|
static size_t session_memsize(const void *ptr) {
|
|
@@ -215,14 +269,18 @@ static VALUE error_class_for(CURLcode code) {
|
|
|
215
269
|
}
|
|
216
270
|
}
|
|
217
271
|
|
|
218
|
-
|
|
219
|
-
static void raise_curl_error(VALUE klass, CURLcode code, const char *message) {
|
|
272
|
+
static VALUE build_curl_error(VALUE klass, CURLcode code, const char *message) {
|
|
220
273
|
VALUE exc;
|
|
221
274
|
|
|
222
275
|
if (!message || !*message) message = curl_easy_strerror(code);
|
|
223
276
|
exc = rb_exc_new_cstr(klass, message);
|
|
224
277
|
rb_ivar_set(exc, id_ivar_curl_code, INT2NUM((int)code));
|
|
225
|
-
|
|
278
|
+
return exc;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
NORETURN(static void raise_curl_error(VALUE klass, CURLcode code, const char *message));
|
|
282
|
+
static void raise_curl_error(VALUE klass, CURLcode code, const char *message) {
|
|
283
|
+
rb_exc_raise(build_curl_error(klass, code, message));
|
|
226
284
|
}
|
|
227
285
|
|
|
228
286
|
/* curl_easy_setopt() only fails for options the installed libcurl cannot honour; that must
|
|
@@ -283,25 +341,211 @@ static size_t append_to_string(struct patron_session *s, VALUE str, const char *
|
|
|
283
341
|
return len;
|
|
284
342
|
}
|
|
285
343
|
|
|
344
|
+
static double monotonic_now(void) {
|
|
345
|
+
struct timespec ts;
|
|
346
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
347
|
+
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
static void mark_head_complete(struct patron_session *s) {
|
|
351
|
+
if (s->head_complete) return;
|
|
352
|
+
s->head_complete = 1;
|
|
353
|
+
s->head_length = NIL_P(s->header_buf) ? 0 : RSTRING_LEN(s->header_buf);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* libcurl drops the bodies of the redirects it follows and 1xx responses have none, so the
|
|
357
|
+
* first body byte always belongs to the final response: the head is complete by then.
|
|
358
|
+
*
|
|
359
|
+
* A streamed transfer writes into the caller's String. When a delivery does not fit, what
|
|
360
|
+
* fits is copied and the transfer paused; libcurl then delivers the very same bytes again
|
|
361
|
+
* after curl_easy_pause(CURLPAUSE_CONT), and redeliver_skip steps over the part handed out
|
|
362
|
+
* already. Outside of transfer_read there is no String to write to, so the transfer pauses
|
|
363
|
+
* without taking anything. */
|
|
286
364
|
static size_t body_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
287
365
|
struct patron_session *s = userdata;
|
|
288
366
|
size_t len = size * nmemb;
|
|
367
|
+
size_t taken = 0, room, n;
|
|
368
|
+
long cur;
|
|
369
|
+
|
|
370
|
+
mark_head_complete(s);
|
|
371
|
+
s->last_activity = monotonic_now();
|
|
372
|
+
if (!s->streaming) return append_to_string(s, s->body_buf, ptr, len);
|
|
373
|
+
|
|
374
|
+
if (s->redeliver_skip) {
|
|
375
|
+
if (s->redeliver_skip >= len) {
|
|
376
|
+
s->redeliver_skip -= len;
|
|
377
|
+
return len;
|
|
378
|
+
}
|
|
379
|
+
taken = s->redeliver_skip;
|
|
380
|
+
s->redeliver_skip = 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (NIL_P(s->read_buf)) {
|
|
384
|
+
s->redeliver_skip = taken;
|
|
385
|
+
s->paused = 1;
|
|
386
|
+
return CURL_WRITEFUNC_PAUSE;
|
|
387
|
+
}
|
|
289
388
|
|
|
290
|
-
if (s->
|
|
291
|
-
if (
|
|
389
|
+
if (s->read_limit < 0) {
|
|
390
|
+
if (append_to_string(s, s->read_buf, ptr + taken, len - taken) != len - taken) return 0;
|
|
292
391
|
return len;
|
|
293
392
|
}
|
|
294
|
-
|
|
393
|
+
|
|
394
|
+
cur = RSTRING_LEN(s->read_buf);
|
|
395
|
+
room = s->read_limit > cur ? (size_t)(s->read_limit - cur) : 0;
|
|
396
|
+
n = len - taken < room ? len - taken : room;
|
|
397
|
+
if (n) {
|
|
398
|
+
memcpy(RSTRING_PTR(s->read_buf) + cur, ptr + taken, n);
|
|
399
|
+
rb_str_set_len(s->read_buf, cur + (long)n);
|
|
400
|
+
}
|
|
401
|
+
if (taken + n < len) {
|
|
402
|
+
s->redeliver_skip = taken + n;
|
|
403
|
+
s->paused = 1;
|
|
404
|
+
return CURL_WRITEFUNC_PAUSE;
|
|
405
|
+
}
|
|
406
|
+
return len;
|
|
295
407
|
}
|
|
296
408
|
|
|
409
|
+
/* The early head-complete trigger: the blank line ending a 2xx head. A 2xx is never followed
|
|
410
|
+
* or retried, so nothing else can come before its body, and a long-poll or an event stream
|
|
411
|
+
* gets its head before the first event. 3xx, 401 and 407 wait for the first body byte. */
|
|
297
412
|
static size_t header_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
298
413
|
struct patron_session *s = userdata;
|
|
299
|
-
|
|
414
|
+
size_t len = size * nmemb;
|
|
415
|
+
size_t written = append_to_string(s, s->header_buf, ptr, len);
|
|
416
|
+
|
|
417
|
+
s->last_activity = monotonic_now();
|
|
418
|
+
if (!s->head_complete && ((len == 2 && ptr[0] == '\r' && ptr[1] == '\n') || (len == 1 && ptr[0] == '\n'))) {
|
|
419
|
+
long code = 0;
|
|
420
|
+
curl_easy_getinfo(s->easy, CURLINFO_RESPONSE_CODE, &code);
|
|
421
|
+
if (code >= 200 && code < 300) mark_head_complete(s);
|
|
422
|
+
}
|
|
423
|
+
return written;
|
|
300
424
|
}
|
|
301
425
|
|
|
302
426
|
static size_t file_read_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
|
|
303
|
-
|
|
304
|
-
|
|
427
|
+
struct patron_session *s = userdata;
|
|
428
|
+
if (s->interrupt) return CURL_READFUNC_ABORT;
|
|
429
|
+
return fread(buffer, 1, size * nitems, s->request_body_file);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* Same as io_seek_cb(): a failed rewind has to fail the request, not empty the body */
|
|
433
|
+
static int file_seek_cb(void *userdata, curl_off_t offset, int origin) {
|
|
434
|
+
struct patron_session *s = userdata;
|
|
435
|
+
|
|
436
|
+
if (fseeko(s->request_body_file, (off_t)offset, origin) == 0) return CURL_SEEKFUNC_OK;
|
|
437
|
+
s->interrupt = INTERRUPT_REWIND_FAILED;
|
|
438
|
+
return CURL_SEEKFUNC_FAIL;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
struct io_read_args {
|
|
442
|
+
struct patron_session *s;
|
|
443
|
+
size_t len;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
static VALUE io_readpartial(VALUE arg) {
|
|
447
|
+
struct io_read_args *a = (struct io_read_args *)arg;
|
|
448
|
+
VALUE argv[2] = { SIZET2NUM(a->len), a->s->upload_buf };
|
|
449
|
+
return rb_funcallv(a->s->upload_io, id_readpartial, 2, argv);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
static VALUE io_readpartial_eof(VALUE arg, VALUE err) {
|
|
453
|
+
return Qnil;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
static VALUE io_read_protected(VALUE arg) {
|
|
457
|
+
struct io_read_args *a = (struct io_read_args *)arg;
|
|
458
|
+
struct patron_session *s = a->s;
|
|
459
|
+
VALUE argv[2] = { SIZET2NUM(a->len), s->upload_buf };
|
|
460
|
+
VALUE chunk;
|
|
461
|
+
|
|
462
|
+
if (s->upload_io_partial) {
|
|
463
|
+
chunk = rb_rescue2(io_readpartial, arg, io_readpartial_eof, Qnil, rb_eEOFError, (VALUE)0);
|
|
464
|
+
} else {
|
|
465
|
+
chunk = rb_funcallv(s->upload_io, id_read, 2, argv);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (NIL_P(chunk)) return Qnil;
|
|
469
|
+
StringValue(chunk);
|
|
470
|
+
/* A readable that ignores the length would overflow libcurl's buffer */
|
|
471
|
+
if ((size_t)RSTRING_LEN(chunk) > a->len) {
|
|
472
|
+
raise_curl_error(ePatronError, CURLE_READ_ERROR, "The request body #read returned more bytes than asked for");
|
|
473
|
+
}
|
|
474
|
+
return chunk;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* Streams the body from upload_io. #readpartial(len, buf), or #read(len, buf) when there is
|
|
478
|
+
* no #readpartial, reads into upload_buf, which is then copied into libcurl's buffer: Ruby
|
|
479
|
+
* cannot be handed memory it does not own. #read on a pipe or a socket waits for all of `len`
|
|
480
|
+
* (libcurl asks for 64KB), #readpartial returns what the writer has written so far, and ends
|
|
481
|
+
* with an EOFError rather than nil. Under a Fiber scheduler a blocking read yields to it like
|
|
482
|
+
* any other IO in the fiber. */
|
|
483
|
+
static size_t io_read_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
|
|
484
|
+
struct patron_session *s = userdata;
|
|
485
|
+
struct io_read_args args = { s, size * nitems };
|
|
486
|
+
int tag = 0;
|
|
487
|
+
VALUE chunk;
|
|
488
|
+
|
|
489
|
+
if (s->interrupt) return CURL_READFUNC_ABORT;
|
|
490
|
+
chunk = rb_protect(io_read_protected, (VALUE)&args, &tag);
|
|
491
|
+
if (tag) {
|
|
492
|
+
park_exception(s, tag);
|
|
493
|
+
return CURL_READFUNC_ABORT;
|
|
494
|
+
}
|
|
495
|
+
if (NIL_P(chunk)) return 0;
|
|
496
|
+
memcpy(buffer, RSTRING_PTR(chunk), (size_t)RSTRING_LEN(chunk));
|
|
497
|
+
return (size_t)RSTRING_LEN(chunk);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
struct io_seek_args {
|
|
501
|
+
struct patron_session *s;
|
|
502
|
+
curl_off_t offset;
|
|
503
|
+
int origin;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/* IO::SEEK_SET and friends are the C SEEK_* values, so origin passes through as-is */
|
|
507
|
+
static VALUE io_seek_protected(VALUE arg) {
|
|
508
|
+
struct io_seek_args *a = (struct io_seek_args *)arg;
|
|
509
|
+
VALUE io = a->s->upload_io;
|
|
510
|
+
|
|
511
|
+
if (rb_respond_to(io, id_seek)) {
|
|
512
|
+
VALUE argv[2] = { OFFT2NUM(a->offset), INT2FIX(a->origin) };
|
|
513
|
+
rb_funcallv(io, id_seek, 2, argv);
|
|
514
|
+
return Qtrue;
|
|
515
|
+
}
|
|
516
|
+
/* Zlib::GzipReader and friends can only start over */
|
|
517
|
+
if (a->origin == SEEK_SET && a->offset == 0 && rb_respond_to(io, id_rewind)) {
|
|
518
|
+
rb_funcallv(io, id_rewind, 0, NULL);
|
|
519
|
+
return Qtrue;
|
|
520
|
+
}
|
|
521
|
+
return Qfalse;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* libcurl rewinds the body when it has to send it again: a 307/308 redirect, an auth
|
|
525
|
+
* round-trip. A pipe has #seek but raises ESPIPE, which only means "cannot seek".
|
|
526
|
+
* libcurl 8.7 logs a failed rewind and then follows the redirect with an empty body
|
|
527
|
+
* anyway, so we abort the transfer ourselves rather than trust it to. */
|
|
528
|
+
static int io_seek_cb(void *userdata, curl_off_t offset, int origin) {
|
|
529
|
+
struct patron_session *s = userdata;
|
|
530
|
+
struct io_seek_args args = { s, offset, origin };
|
|
531
|
+
int tag = 0;
|
|
532
|
+
VALUE seeked;
|
|
533
|
+
|
|
534
|
+
if (origin == SEEK_SET) args.offset += s->upload_io_origin;
|
|
535
|
+
seeked = rb_protect(io_seek_protected, (VALUE)&args, &tag);
|
|
536
|
+
if (tag) {
|
|
537
|
+
VALUE err = rb_errinfo();
|
|
538
|
+
if (rb_obj_is_kind_of(err, rb_eSystemCallError) || rb_obj_is_kind_of(err, rb_eIOError)) {
|
|
539
|
+
rb_set_errinfo(Qnil);
|
|
540
|
+
seeked = Qfalse;
|
|
541
|
+
} else {
|
|
542
|
+
park_exception(s, tag);
|
|
543
|
+
return CURL_SEEKFUNC_FAIL;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (RTEST(seeked)) return CURL_SEEKFUNC_OK;
|
|
547
|
+
s->interrupt = INTERRUPT_REWIND_FAILED;
|
|
548
|
+
return CURL_SEEKFUNC_CANTSEEK;
|
|
305
549
|
}
|
|
306
550
|
|
|
307
551
|
struct progress_args {
|
|
@@ -475,7 +719,14 @@ static VALUE session_alloc(VALUE klass) {
|
|
|
475
719
|
s->upload_data = Qnil;
|
|
476
720
|
s->header_buf = Qnil;
|
|
477
721
|
s->body_buf = Qnil;
|
|
722
|
+
s->upload_io = Qnil;
|
|
723
|
+
s->upload_buf = Qnil;
|
|
724
|
+
s->request_content_length = -1;
|
|
478
725
|
s->pending_errinfo = Qnil;
|
|
726
|
+
s->read_buf = Qnil;
|
|
727
|
+
s->owner = Qnil;
|
|
728
|
+
s->owner_thread = Qnil;
|
|
729
|
+
s->read_limit = -1;
|
|
479
730
|
init_handles(s);
|
|
480
731
|
return obj;
|
|
481
732
|
}
|
|
@@ -614,6 +865,15 @@ static int each_http_header(VALUE key, VALUE value, VALUE self) {
|
|
|
614
865
|
setopt_or_raise(s->easy, CURLOPT_ACCEPT_ENCODING, "gzip");
|
|
615
866
|
}
|
|
616
867
|
|
|
868
|
+
/* A streamed body of unknown size goes by these, see set_request_body_from_io() */
|
|
869
|
+
if (header_name_is(name, "Content-Length")) {
|
|
870
|
+
VALUE length = rb_obj_as_string(value);
|
|
871
|
+
char *end = NULL;
|
|
872
|
+
long long parsed = strtoll(StringValueCStr(length), &end, 10);
|
|
873
|
+
if (end && *end == '\0' && parsed >= 0 && RSTRING_LEN(length) > 0) s->request_content_length = (curl_off_t)parsed;
|
|
874
|
+
}
|
|
875
|
+
if (header_name_is(name, "Transfer-Encoding")) s->request_transfer_encoding = 1;
|
|
876
|
+
|
|
617
877
|
line = rb_sprintf("%"PRIsVALUE": %"PRIsVALUE, name, value);
|
|
618
878
|
s->headers = curl_slist_append(s->headers, StringValueCStr(line));
|
|
619
879
|
return ST_CONTINUE;
|
|
@@ -669,14 +929,57 @@ static void set_request_body_from_file(struct patron_session *s, VALUE path) {
|
|
|
669
929
|
|
|
670
930
|
setopt_or_raise(s->easy, CURLOPT_UPLOAD, 1L);
|
|
671
931
|
setopt_or_raise(s->easy, CURLOPT_READFUNCTION, file_read_cb);
|
|
672
|
-
setopt_or_raise(s->easy, CURLOPT_READDATA, s
|
|
932
|
+
setopt_or_raise(s->easy, CURLOPT_READDATA, s);
|
|
933
|
+
setopt_or_raise(s->easy, CURLOPT_SEEKFUNCTION, file_seek_cb);
|
|
934
|
+
setopt_or_raise(s->easy, CURLOPT_SEEKDATA, s);
|
|
673
935
|
setopt_or_raise(s->easy, CURLOPT_INFILESIZE_LARGE, (curl_off_t)st.st_size);
|
|
674
936
|
}
|
|
675
937
|
|
|
938
|
+
/* Size comes from #size when there is one (minus #pos, for a half-read StringIO), then from
|
|
939
|
+
* a Content-Length header, and failing both the body goes out chunked. libcurl would pick
|
|
940
|
+
* chunked by itself too, but it would then send it alongside an app-supplied Content-Length. */
|
|
941
|
+
static void set_request_body_from_io(struct patron_session *s, VALUE io) {
|
|
942
|
+
curl_off_t size = -1;
|
|
943
|
+
|
|
944
|
+
SESSION_WRITE(s, upload_io, io);
|
|
945
|
+
SESSION_WRITE(s, upload_buf, rb_str_buf_new(0));
|
|
946
|
+
s->upload_io_origin = 0;
|
|
947
|
+
s->upload_io_partial = rb_respond_to(io, id_readpartial);
|
|
948
|
+
|
|
949
|
+
if (rb_respond_to(io, id_size)) {
|
|
950
|
+
VALUE value = rb_funcallv(io, id_size, 0, NULL);
|
|
951
|
+
if (!NIL_P(value)) {
|
|
952
|
+
size = NUM2OFFT(value);
|
|
953
|
+
if (rb_respond_to(io, id_pos)) {
|
|
954
|
+
s->upload_io_origin = NUM2OFFT(rb_funcallv(io, id_pos, 0, NULL));
|
|
955
|
+
size = size > s->upload_io_origin ? size - s->upload_io_origin : 0;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (size < 0) size = s->request_content_length;
|
|
960
|
+
if (size < 0 && !s->request_transfer_encoding) {
|
|
961
|
+
s->headers = curl_slist_append(s->headers, "Transfer-Encoding: chunked");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
setopt_or_raise(s->easy, CURLOPT_UPLOAD, 1L);
|
|
965
|
+
setopt_or_raise(s->easy, CURLOPT_READFUNCTION, io_read_cb);
|
|
966
|
+
setopt_or_raise(s->easy, CURLOPT_READDATA, s);
|
|
967
|
+
setopt_or_raise(s->easy, CURLOPT_SEEKFUNCTION, io_seek_cb);
|
|
968
|
+
setopt_or_raise(s->easy, CURLOPT_SEEKDATA, s);
|
|
969
|
+
if (size >= 0) setopt_or_raise(s->easy, CURLOPT_INFILESIZE_LARGE, size);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/* #to_path wins over #read so that a File keeps going through fread(). Every IO has a
|
|
973
|
+
* #to_path since Ruby 3.2, which is nil for a pipe or a socket. */
|
|
676
974
|
static void set_request_body(struct patron_session *s, VALUE stringable_or_file) {
|
|
677
|
-
|
|
678
|
-
|
|
975
|
+
VALUE path = Qnil;
|
|
976
|
+
|
|
977
|
+
if (rb_respond_to(stringable_or_file, id_to_path)) path = rb_funcallv(stringable_or_file, id_to_path, 0, NULL);
|
|
978
|
+
|
|
979
|
+
if (!NIL_P(path)) {
|
|
679
980
|
set_request_body_from_file(s, rb_obj_as_string(path));
|
|
981
|
+
} else if (rb_respond_to(stringable_or_file, id_read)) {
|
|
982
|
+
set_request_body_from_io(s, stringable_or_file);
|
|
680
983
|
} else {
|
|
681
984
|
VALUE data = rb_obj_as_string(stringable_or_file);
|
|
682
985
|
SESSION_WRITE(s, upload_data, data);
|
|
@@ -744,10 +1047,8 @@ static void set_options_from_request(struct patron_session *s, VALUE request) {
|
|
|
744
1047
|
set_request_body(s, upload_data);
|
|
745
1048
|
setopt_or_raise(easy, CURLOPT_CUSTOMREQUEST, "GET");
|
|
746
1049
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
SESSION_WRITE(s, body_buf, Qnil);
|
|
750
|
-
}
|
|
1050
|
+
/* Downloads go through Session#get_file, which streams into the file from Ruby */
|
|
1051
|
+
if (RTEST(file_name)) rb_raise(rb_eArgError, "A GET with a file name has to go through Session#get_file");
|
|
751
1052
|
} else if (action == id_post || action == id_put || action == id_patch) {
|
|
752
1053
|
VALUE multipart = request_attr(request, id_multipart);
|
|
753
1054
|
const char *verb = action == id_post ? "POST" : action == id_put ? "PUT" : "PATCH";
|
|
@@ -867,6 +1168,9 @@ static void set_options_from_request(struct patron_session *s, VALUE request) {
|
|
|
867
1168
|
|
|
868
1169
|
value = request_attr(request, id_buffer_size);
|
|
869
1170
|
if (RTEST(value)) setopt_or_raise(easy, CURLOPT_BUFFERSIZE, NUM2LONG(value));
|
|
1171
|
+
|
|
1172
|
+
value = request_attr(request, id_read_timeout);
|
|
1173
|
+
s->read_timeout = RTEST(value) ? NUM2DBL(value) : 0.0;
|
|
870
1174
|
}
|
|
871
1175
|
|
|
872
1176
|
/*----------------------------------------------------------------------------*/
|
|
@@ -923,12 +1227,13 @@ static VALUE create_response(struct patron_session *s) {
|
|
|
923
1227
|
|
|
924
1228
|
struct poll_context {
|
|
925
1229
|
CURLM *multi;
|
|
1230
|
+
int timeout_ms;
|
|
926
1231
|
CURLMcode rc;
|
|
927
1232
|
};
|
|
928
1233
|
|
|
929
1234
|
static void *poll_without_gvl(void *ptr) {
|
|
930
1235
|
struct poll_context *ctx = ptr;
|
|
931
|
-
ctx->rc = curl_multi_poll(ctx->multi, NULL, 0,
|
|
1236
|
+
ctx->rc = curl_multi_poll(ctx->multi, NULL, 0, ctx->timeout_ms, NULL);
|
|
932
1237
|
return NULL;
|
|
933
1238
|
}
|
|
934
1239
|
|
|
@@ -941,8 +1246,8 @@ static void wakeup_poll(void *ptr) {
|
|
|
941
1246
|
}
|
|
942
1247
|
|
|
943
1248
|
/* Blocks the whole thread until libcurl has something to do, with the GVL released */
|
|
944
|
-
static void wait_without_scheduler(struct patron_session *s) {
|
|
945
|
-
struct poll_context ctx = { s->multi, CURLM_OK };
|
|
1249
|
+
static void wait_without_scheduler(struct patron_session *s, long max_ms) {
|
|
1250
|
+
struct poll_context ctx = { s->multi, (int)max_ms, CURLM_OK };
|
|
946
1251
|
/* Pending Ruby interrupts are raised right here as rb_nogvl returns; the rb_ensure
|
|
947
1252
|
* around the request takes care of the half-finished transfer. */
|
|
948
1253
|
rb_nogvl(poll_without_gvl, &ctx, wakeup_poll, s, RB_NOGVL_UBF_ASYNC_SAFE);
|
|
@@ -955,7 +1260,8 @@ static VALUE float_seconds(long milliseconds) {
|
|
|
955
1260
|
|
|
956
1261
|
/* An IO wrapping one of libcurl's sockets, for handing to the scheduler. FMODE_EXTERNAL keeps
|
|
957
1262
|
* Ruby from ever closing the descriptor; libcurl owns it. The wrappers are dropped whenever the
|
|
958
|
-
* set of sockets changes, so a descriptor number the kernel reused gets
|
|
1263
|
+
* set of sockets changes and after every request, so a descriptor number the kernel reused gets
|
|
1264
|
+
* a fresh one. */
|
|
959
1265
|
static VALUE socket_io(struct patron_session *s, int fd) {
|
|
960
1266
|
VALUE key = INT2FIX(fd);
|
|
961
1267
|
VALUE io = rb_hash_lookup2(s->socket_ios, key, Qnil);
|
|
@@ -972,7 +1278,7 @@ static VALUE socket_io(struct patron_session *s, int fd) {
|
|
|
972
1278
|
/* Yields to the Fiber scheduler until libcurl has something to do, so other fibers on this thread
|
|
973
1279
|
* run meanwhile. Uses io_wait for the usual single socket, io_select when libcurl juggles several
|
|
974
1280
|
* (happy eyeballs), and a plain sleep while it has none (threaded resolver at work). */
|
|
975
|
-
static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
|
|
1281
|
+
static void wait_with_scheduler(struct patron_session *s, VALUE scheduler, long max_ms) {
|
|
976
1282
|
fd_set rd, wr, ex;
|
|
977
1283
|
int maxfd = -1, fd, nfds = 0, i;
|
|
978
1284
|
int fds[MAX_WAIT_FDS], events[MAX_WAIT_FDS];
|
|
@@ -982,7 +1288,7 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
|
|
|
982
1288
|
FD_ZERO(&rd); FD_ZERO(&wr); FD_ZERO(&ex);
|
|
983
1289
|
curl_multi_fdset(s->multi, &rd, &wr, &ex, &maxfd);
|
|
984
1290
|
curl_multi_timeout(s->multi, &timeout_ms);
|
|
985
|
-
if (timeout_ms < 0 || timeout_ms >
|
|
1291
|
+
if (timeout_ms < 0 || timeout_ms > max_ms) timeout_ms = max_ms;
|
|
986
1292
|
|
|
987
1293
|
for (fd = 0; fd <= maxfd && nfds < MAX_WAIT_FDS; fd++) {
|
|
988
1294
|
int ev = 0;
|
|
@@ -998,7 +1304,9 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
|
|
|
998
1304
|
if (nfds == 0) {
|
|
999
1305
|
/* libcurl asks to be called back within 100 ms when it has no socket to wait on */
|
|
1000
1306
|
if (timeout_ms > 100) timeout_ms = 100;
|
|
1307
|
+
s->in_scheduler_wait = 1;
|
|
1001
1308
|
rb_fiber_scheduler_kernel_sleep(scheduler, float_seconds(timeout_ms));
|
|
1309
|
+
s->in_scheduler_wait = 0;
|
|
1002
1310
|
return;
|
|
1003
1311
|
}
|
|
1004
1312
|
|
|
@@ -1013,7 +1321,10 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
|
|
|
1013
1321
|
|
|
1014
1322
|
timeout = float_seconds(timeout_ms);
|
|
1015
1323
|
if (nfds == 1) {
|
|
1016
|
-
|
|
1324
|
+
VALUE io = socket_io(s, fds[0]);
|
|
1325
|
+
s->in_scheduler_wait = 1;
|
|
1326
|
+
rb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events[0]), timeout);
|
|
1327
|
+
s->in_scheduler_wait = 0;
|
|
1017
1328
|
return;
|
|
1018
1329
|
}
|
|
1019
1330
|
|
|
@@ -1027,79 +1338,148 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
|
|
|
1027
1338
|
if (events[i] & RUBY_IO_WRITABLE) rb_ary_push(writables, io);
|
|
1028
1339
|
if (events[i] & RUBY_IO_PRIORITY) rb_ary_push(exceptables, io);
|
|
1029
1340
|
}
|
|
1341
|
+
s->in_scheduler_wait = 1;
|
|
1030
1342
|
ready = rb_fiber_scheduler_io_select(scheduler, readables, writables, exceptables, timeout);
|
|
1031
1343
|
if (ready == Qundef) {
|
|
1032
1344
|
/* A scheduler without io_select; poll instead. Only ever happens while connecting. */
|
|
1033
1345
|
rb_fiber_scheduler_kernel_sleep(scheduler, float_seconds(timeout_ms > 50 ? 50 : timeout_ms));
|
|
1034
1346
|
}
|
|
1347
|
+
s->in_scheduler_wait = 0;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/* Where drive_transfer() may stop before the transfer is done */
|
|
1352
|
+
enum drive_until {
|
|
1353
|
+
UNTIL_DONE,
|
|
1354
|
+
UNTIL_HEAD, /* the final response head is complete */
|
|
1355
|
+
UNTIL_DATA, /* some bytes landed in read_buf, or it is full */
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
static void collect_done(struct patron_session *s) {
|
|
1359
|
+
CURLMsg *msg;
|
|
1360
|
+
int remaining = 0;
|
|
1361
|
+
|
|
1362
|
+
while ((msg = curl_multi_info_read(s->multi, &remaining))) {
|
|
1363
|
+
if (msg->msg == CURLMSG_DONE && msg->easy_handle == s->easy) {
|
|
1364
|
+
s->done = 1;
|
|
1365
|
+
s->result = msg->data.result;
|
|
1366
|
+
}
|
|
1035
1367
|
}
|
|
1036
1368
|
}
|
|
1037
1369
|
|
|
1038
|
-
static
|
|
1370
|
+
static int drive_should_stop(struct patron_session *s, enum drive_until until, long read_from) {
|
|
1371
|
+
switch (until) {
|
|
1372
|
+
case UNTIL_HEAD: return s->head_complete;
|
|
1373
|
+
case UNTIL_DATA: return s->paused || RSTRING_LEN(s->read_buf) > read_from;
|
|
1374
|
+
default: return 0;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/* Drives the transfer until it is done or `until` holds. read_timeout is a deadline counted
|
|
1379
|
+
* from the last delivery, so the waits are capped at what is left of it. `read_from` is the
|
|
1380
|
+
* read_buf length that does not count as new data. */
|
|
1381
|
+
static void drive_transfer(struct patron_session *s, enum drive_until until, long read_from) {
|
|
1039
1382
|
int running = 1;
|
|
1040
1383
|
CURLMcode mc;
|
|
1041
1384
|
/* Set per thread; a Session inside Async { } yields to it instead of blocking the thread */
|
|
1042
1385
|
VALUE scheduler = rb_fiber_scheduler_current();
|
|
1043
1386
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1387
|
+
for (;;) {
|
|
1388
|
+
long wait_ms = POLL_TIMEOUT_MS;
|
|
1046
1389
|
|
|
1047
|
-
while (running) {
|
|
1048
1390
|
mc = curl_multi_perform(s->multi, &running);
|
|
1049
1391
|
if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
|
|
1392
|
+
collect_done(s);
|
|
1050
1393
|
if (s->pending_tag || s->interrupt) break;
|
|
1051
|
-
if (!running)
|
|
1394
|
+
if (!running) s->done = 1;
|
|
1395
|
+
if (s->done || drive_should_stop(s, until, read_from)) break;
|
|
1396
|
+
|
|
1397
|
+
if (s->read_timeout > 0) {
|
|
1398
|
+
double left = s->last_activity + s->read_timeout - monotonic_now();
|
|
1399
|
+
if (left <= 0) {
|
|
1400
|
+
s->read_timed_out = 1;
|
|
1401
|
+
break;
|
|
1402
|
+
}
|
|
1403
|
+
if (left * 1000.0 < (double)wait_ms) wait_ms = (long)(left * 1000.0) + 1;
|
|
1404
|
+
}
|
|
1052
1405
|
|
|
1053
1406
|
if (NIL_P(scheduler)) {
|
|
1054
|
-
wait_without_scheduler(s);
|
|
1407
|
+
wait_without_scheduler(s, wait_ms);
|
|
1055
1408
|
} else {
|
|
1056
|
-
wait_with_scheduler(s, scheduler);
|
|
1409
|
+
wait_with_scheduler(s, scheduler, wait_ms);
|
|
1057
1410
|
}
|
|
1058
1411
|
if (s->interrupt) break;
|
|
1059
1412
|
}
|
|
1413
|
+
if (s->done) mark_head_complete(s);
|
|
1060
1414
|
}
|
|
1061
1415
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1416
|
+
/* Raises whatever ended the transfer early, and the transfer's own failure when
|
|
1417
|
+
* `include_result` is set and it is done. */
|
|
1418
|
+
static void raise_if_failed(struct patron_session *s, int include_result) {
|
|
1419
|
+
if (s->pending_tag) reraise_parked(s);
|
|
1066
1420
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1421
|
+
if (s->interrupt == INTERRUPT_DOWNLOAD_OVERFLOW) {
|
|
1422
|
+
raise_curl_error(eDownloadLimitExceeded, CURLE_ABORTED_BY_CALLBACK, "Download byte limit exceeded");
|
|
1423
|
+
}
|
|
1424
|
+
if (s->interrupt == INTERRUPT_REWIND_FAILED) {
|
|
1425
|
+
raise_curl_error(ePatronError, CURLE_SEND_FAIL_REWIND, "The request body has to be sent again, but it cannot be rewound");
|
|
1426
|
+
}
|
|
1427
|
+
if (s->interrupt) {
|
|
1428
|
+
raise_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
|
|
1429
|
+
}
|
|
1430
|
+
if (s->read_timed_out) {
|
|
1431
|
+
char message[128];
|
|
1432
|
+
snprintf(message, sizeof(message), "Read timeout: no data received for %.3g seconds", s->read_timeout);
|
|
1433
|
+
raise_curl_error(eTimeoutError, CURLE_OPERATION_TIMEDOUT, message);
|
|
1434
|
+
}
|
|
1435
|
+
if (include_result && s->done && s->result != CURLE_OK) {
|
|
1436
|
+
raise_curl_error(error_class_for(s->result), s->result, s->error_buf);
|
|
1069
1437
|
}
|
|
1070
|
-
return result;
|
|
1071
1438
|
}
|
|
1072
1439
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
static VALUE perform(VALUE ptr) {
|
|
1079
|
-
struct perform_args *args = (struct perform_args *)ptr;
|
|
1080
|
-
struct patron_session *s = args->s;
|
|
1081
|
-
CURLcode result;
|
|
1440
|
+
/* Today's request setup plus curl_multi_add_handle(). Everything it allocates is released
|
|
1441
|
+
* by cleanup(), which the caller must make sure runs. */
|
|
1442
|
+
static void begin_transfer(struct patron_session *s, VALUE request, int streaming) {
|
|
1443
|
+
CURLMcode mc;
|
|
1082
1444
|
|
|
1083
1445
|
s->interrupt = 0;
|
|
1446
|
+
s->in_scheduler_wait = 0;
|
|
1084
1447
|
s->pending_tag = 0;
|
|
1085
1448
|
s->error_buf[0] = '\0';
|
|
1449
|
+
s->streaming = streaming;
|
|
1450
|
+
s->paused = 0;
|
|
1451
|
+
s->head_complete = 0;
|
|
1452
|
+
s->head_length = 0;
|
|
1453
|
+
s->done = 0;
|
|
1454
|
+
s->result = CURLE_OK;
|
|
1455
|
+
s->read_timed_out = 0;
|
|
1456
|
+
s->redeliver_skip = 0;
|
|
1457
|
+
s->read_limit = -1;
|
|
1086
1458
|
SESSION_WRITE(s, header_buf, rb_str_buf_new(HEADER_BUFFER_INITIAL_CAPACITY));
|
|
1087
|
-
SESSION_WRITE(s, body_buf, rb_str_buf_new(BODY_BUFFER_INITIAL_CAPACITY));
|
|
1459
|
+
SESSION_WRITE(s, body_buf, streaming ? Qnil : rb_str_buf_new(BODY_BUFFER_INITIAL_CAPACITY));
|
|
1088
1460
|
|
|
1089
|
-
set_options_from_request(s,
|
|
1090
|
-
|
|
1461
|
+
set_options_from_request(s, request);
|
|
1462
|
+
/* A proxy's "200 Connection established" would otherwise trip the early head trigger;
|
|
1463
|
+
* HeaderParser keeps only the last response anyway */
|
|
1464
|
+
if (streaming) setopt_or_raise(s->easy, CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L);
|
|
1091
1465
|
|
|
1092
|
-
|
|
1466
|
+
s->last_activity = monotonic_now();
|
|
1467
|
+
mc = curl_multi_add_handle(s->multi, s->easy);
|
|
1468
|
+
if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
|
|
1469
|
+
}
|
|
1093
1470
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1471
|
+
struct perform_args {
|
|
1472
|
+
struct patron_session *s;
|
|
1473
|
+
VALUE request;
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
static VALUE perform(VALUE ptr) {
|
|
1477
|
+
struct perform_args *args = (struct perform_args *)ptr;
|
|
1478
|
+
struct patron_session *s = args->s;
|
|
1100
1479
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1480
|
+
begin_transfer(s, args->request, 0);
|
|
1481
|
+
drive_transfer(s, UNTIL_DONE, 0);
|
|
1482
|
+
raise_if_failed(s, 1);
|
|
1103
1483
|
|
|
1104
1484
|
/* Write the cookies accumulated in the share out to the jar, if one is configured */
|
|
1105
1485
|
curl_easy_setopt(s->easy, CURLOPT_COOKIELIST, "FLUSH");
|
|
@@ -1124,10 +1504,6 @@ static VALUE cleanup(VALUE self) {
|
|
|
1124
1504
|
curl_slist_free_all(s->headers);
|
|
1125
1505
|
s->headers = NULL;
|
|
1126
1506
|
}
|
|
1127
|
-
if (s->download_file) {
|
|
1128
|
-
fclose(s->download_file);
|
|
1129
|
-
s->download_file = NULL;
|
|
1130
|
-
}
|
|
1131
1507
|
if (s->request_body_file) {
|
|
1132
1508
|
fclose(s->request_body_file);
|
|
1133
1509
|
s->request_body_file = NULL;
|
|
@@ -1137,11 +1513,39 @@ static VALUE cleanup(VALUE self) {
|
|
|
1137
1513
|
SESSION_WRITE(s, upload_data, Qnil);
|
|
1138
1514
|
SESSION_WRITE(s, header_buf, Qnil);
|
|
1139
1515
|
SESSION_WRITE(s, body_buf, Qnil);
|
|
1516
|
+
SESSION_WRITE(s, upload_io, Qnil);
|
|
1517
|
+
SESSION_WRITE(s, upload_buf, Qnil);
|
|
1518
|
+
SESSION_WRITE(s, read_buf, Qnil);
|
|
1519
|
+
SESSION_WRITE(s, owner, Qnil);
|
|
1520
|
+
SESSION_WRITE(s, owner_thread, Qnil);
|
|
1521
|
+
s->in_scheduler_wait = 0;
|
|
1522
|
+
/* An aborted transfer closes its socket, and the next one tends to get the same number. Handed
|
|
1523
|
+
* the same IO again, io-event's epoll selector takes it for still registered and never wakes. */
|
|
1524
|
+
if (!NIL_P(s->socket_ios)) rb_hash_clear(s->socket_ios);
|
|
1525
|
+
s->upload_io_origin = 0;
|
|
1526
|
+
s->upload_io_partial = 0;
|
|
1527
|
+
s->request_content_length = -1;
|
|
1528
|
+
s->request_transfer_encoding = 0;
|
|
1140
1529
|
s->download_byte_limit = 0;
|
|
1530
|
+
s->streaming = 0;
|
|
1531
|
+
s->paused = 0;
|
|
1532
|
+
s->read_timeout = 0.0;
|
|
1141
1533
|
s->busy = 0;
|
|
1142
1534
|
return Qnil;
|
|
1143
1535
|
}
|
|
1144
1536
|
|
|
1537
|
+
static void raise_if_busy(struct patron_session *s) {
|
|
1538
|
+
if (s->busy) {
|
|
1539
|
+
rb_raise(ePatronError, "This Session is already performing a request in another thread or fiber; use one Session per thread or fiber");
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
static void claim(struct patron_session *s) {
|
|
1544
|
+
s->busy = 1;
|
|
1545
|
+
SESSION_WRITE(s, owner, rb_fiber_current());
|
|
1546
|
+
SESSION_WRITE(s, owner_thread, rb_thread_current());
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1145
1549
|
/*
|
|
1146
1550
|
* Performs the HTTP request described by +request+ and returns a Response built with
|
|
1147
1551
|
* #response_class. Raises a Patron::Error subclass when libcurl reports a failure.
|
|
@@ -1153,17 +1557,226 @@ static VALUE session_handle_request(VALUE self, VALUE request) {
|
|
|
1153
1557
|
struct patron_session *s = get_session(self);
|
|
1154
1558
|
struct perform_args args = { s, request };
|
|
1155
1559
|
|
|
1156
|
-
|
|
1157
|
-
rb_raise(ePatronError, "This Session is already performing a request in another thread or fiber; use one Session per thread or fiber");
|
|
1158
|
-
}
|
|
1560
|
+
raise_if_busy(s);
|
|
1159
1561
|
ensure_handles_for_this_process(s);
|
|
1160
|
-
s
|
|
1562
|
+
claim(s);
|
|
1161
1563
|
return rb_ensure(perform, (VALUE)&args, cleanup, self);
|
|
1162
1564
|
}
|
|
1163
1565
|
|
|
1566
|
+
/*----------------------------------------------------------------------------*/
|
|
1567
|
+
/* Streaming primitives. Session#stream calls them in order inside one begin/ensure:
|
|
1568
|
+
* transfer_start, any number of transfer_read, transfer_finish. */
|
|
1569
|
+
|
|
1570
|
+
static VALUE start_stream(VALUE ptr) {
|
|
1571
|
+
struct perform_args *args = (struct perform_args *)ptr;
|
|
1572
|
+
struct patron_session *s = args->s;
|
|
1573
|
+
CURL *easy;
|
|
1574
|
+
char *effective_url = NULL;
|
|
1575
|
+
long code = 0, redirect_count = 0;
|
|
1576
|
+
|
|
1577
|
+
begin_transfer(s, args->request, 1);
|
|
1578
|
+
drive_transfer(s, UNTIL_HEAD, 0);
|
|
1579
|
+
raise_if_failed(s, 1);
|
|
1580
|
+
|
|
1581
|
+
easy = s->easy;
|
|
1582
|
+
curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &effective_url);
|
|
1583
|
+
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &code);
|
|
1584
|
+
curl_easy_getinfo(easy, CURLINFO_REDIRECT_COUNT, &redirect_count);
|
|
1585
|
+
return rb_ary_new_from_args(4,
|
|
1586
|
+
LONG2NUM(code),
|
|
1587
|
+
effective_url ? rb_usascii_str_new_cstr(effective_url) : Qnil,
|
|
1588
|
+
LONG2NUM(redirect_count),
|
|
1589
|
+
rb_str_subseq(s->header_buf, 0, s->head_length));
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/*
|
|
1593
|
+
* Starts a streamed transfer and drives it until the head of the final response is complete.
|
|
1594
|
+
* Raises the usual Patron::Error when the transfer fails before that, having cleaned up.
|
|
1595
|
+
*
|
|
1596
|
+
* @api private
|
|
1597
|
+
* @param request [Patron::Request]
|
|
1598
|
+
* @return [Array] status, effective URL, redirect count and the raw header data
|
|
1599
|
+
*/
|
|
1600
|
+
static VALUE session_transfer_start(VALUE self, VALUE request) {
|
|
1601
|
+
struct patron_session *s = get_session(self);
|
|
1602
|
+
struct perform_args args = { s, request };
|
|
1603
|
+
int state = 0;
|
|
1604
|
+
VALUE head;
|
|
1605
|
+
|
|
1606
|
+
raise_if_busy(s);
|
|
1607
|
+
ensure_handles_for_this_process(s);
|
|
1608
|
+
claim(s);
|
|
1609
|
+
head = rb_protect(start_stream, (VALUE)&args, &state);
|
|
1610
|
+
if (state) {
|
|
1611
|
+
cleanup(self);
|
|
1612
|
+
rb_jump_tag(state);
|
|
1613
|
+
}
|
|
1614
|
+
return head;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
static struct patron_session *open_stream(VALUE self) {
|
|
1618
|
+
struct patron_session *s = get_session(self);
|
|
1619
|
+
|
|
1620
|
+
if (!s->easy || !s->streaming) rb_raise(ePatronError, "No streamed transfer is open on this Session");
|
|
1621
|
+
if (s->owner != rb_fiber_current()) rb_raise(ePatronError, "The streamed transfer belongs to another thread or fiber");
|
|
1622
|
+
return s;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
struct read_args {
|
|
1626
|
+
struct patron_session *s;
|
|
1627
|
+
long maxlen; /* -1 for whatever the next deliveries bring */
|
|
1628
|
+
long keep; /* bytes at the start of buf to leave alone */
|
|
1629
|
+
VALUE buf;
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
static VALUE read_into_buf(VALUE ptr) {
|
|
1633
|
+
struct read_args *a = (struct read_args *)ptr;
|
|
1634
|
+
struct patron_session *s = a->s;
|
|
1635
|
+
long placed;
|
|
1636
|
+
|
|
1637
|
+
if (s->paused) {
|
|
1638
|
+
CURLcode rc;
|
|
1639
|
+
s->paused = 0;
|
|
1640
|
+
s->last_activity = monotonic_now();
|
|
1641
|
+
/* This may deliver the paused bytes right here, before any curl_multi_perform() */
|
|
1642
|
+
rc = curl_easy_pause(s->easy, CURLPAUSE_CONT);
|
|
1643
|
+
if (rc != CURLE_OK) raise_curl_error(error_class_for(rc), rc, s->error_buf);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (!s->paused && RSTRING_LEN(a->buf) == a->keep && !s->done) {
|
|
1647
|
+
s->last_activity = monotonic_now();
|
|
1648
|
+
drive_transfer(s, UNTIL_DATA, a->keep);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
placed = RSTRING_LEN(a->buf) - a->keep;
|
|
1652
|
+
raise_if_failed(s, placed == 0);
|
|
1653
|
+
if (placed == 0 && s->done) return Qnil;
|
|
1654
|
+
return LONG2NUM(placed);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
static VALUE read_done(VALUE ptr) {
|
|
1658
|
+
struct read_args *a = (struct read_args *)ptr;
|
|
1659
|
+
SESSION_WRITE(a->s, read_buf, Qnil);
|
|
1660
|
+
/* An exception out of the scheduler wait leaves it set, and the stream's own
|
|
1661
|
+
* ensure blocks run before transfer_finish gets to cleanup() */
|
|
1662
|
+
a->s->in_scheduler_wait = 0;
|
|
1663
|
+
return Qnil;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/*
|
|
1667
|
+
* Receives the next bytes of the streamed body into +buf+, which libcurl writes into directly.
|
|
1668
|
+
* The String is only ever written to during this call.
|
|
1669
|
+
*
|
|
1670
|
+
* @api private
|
|
1671
|
+
* @overload transfer_read(maxlen, buf, keep = 0)
|
|
1672
|
+
* @param maxlen [Integer, nil] most bytes to place, nil for whatever the next deliveries bring
|
|
1673
|
+
* @param buf [String] truncated to +keep+ bytes and appended to, made binary
|
|
1674
|
+
* @param keep [Integer] bytes at the start of +buf+ to leave in place
|
|
1675
|
+
* @return [Integer, nil] the bytes placed, nil at the end of the body
|
|
1676
|
+
*/
|
|
1677
|
+
static VALUE session_transfer_read(int argc, VALUE *argv, VALUE self) {
|
|
1678
|
+
struct patron_session *s = open_stream(self);
|
|
1679
|
+
VALUE maxlen, buf, keep;
|
|
1680
|
+
struct read_args args;
|
|
1681
|
+
|
|
1682
|
+
rb_scan_args(argc, argv, "21", &maxlen, &buf, &keep);
|
|
1683
|
+
StringValue(buf);
|
|
1684
|
+
args.s = s;
|
|
1685
|
+
args.buf = buf;
|
|
1686
|
+
args.maxlen = NIL_P(maxlen) ? -1 : NUM2LONG(maxlen);
|
|
1687
|
+
args.keep = NIL_P(keep) ? 0 : NUM2LONG(keep);
|
|
1688
|
+
if (args.maxlen < -1) rb_raise(rb_eArgError, "negative length %ld given", args.maxlen);
|
|
1689
|
+
if (args.keep < 0 || args.keep > RSTRING_LEN(buf)) rb_raise(rb_eArgError, "keep out of range");
|
|
1690
|
+
|
|
1691
|
+
/* A frozen or shared String raises or gets its own copy here, rather than being scribbled on */
|
|
1692
|
+
rb_str_modify(buf);
|
|
1693
|
+
rb_str_set_len(buf, args.keep);
|
|
1694
|
+
if (ENCODING_GET(buf) != rb_ascii8bit_encindex()) rb_enc_associate_index(buf, rb_ascii8bit_encindex());
|
|
1695
|
+
if (args.maxlen == 0) return INT2FIX(0);
|
|
1696
|
+
if (args.maxlen > 0) rb_str_modify_expand(buf, args.maxlen);
|
|
1697
|
+
|
|
1698
|
+
s->read_limit = args.maxlen < 0 ? -1 : args.keep + args.maxlen;
|
|
1699
|
+
SESSION_WRITE(s, read_buf, buf);
|
|
1700
|
+
return rb_ensure(read_into_buf, (VALUE)&args, read_done, (VALUE)&args);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
static VALUE finish_stream(VALUE ptr) {
|
|
1704
|
+
struct patron_session *s = (struct patron_session *)ptr;
|
|
1705
|
+
curl_off_t downloaded = 0, uploaded = 0;
|
|
1706
|
+
int complete = s->done && s->result == CURLE_OK && !s->interrupt && !s->pending_tag;
|
|
1707
|
+
VALUE trailer_data;
|
|
1708
|
+
|
|
1709
|
+
curl_easy_getinfo(s->easy, CURLINFO_SIZE_DOWNLOAD_T, &downloaded);
|
|
1710
|
+
curl_easy_getinfo(s->easy, CURLINFO_SIZE_UPLOAD_T, &uploaded);
|
|
1711
|
+
if (complete && RSTRING_LEN(s->header_buf) > s->head_length) {
|
|
1712
|
+
trailer_data = rb_str_subseq(s->header_buf, s->head_length, RSTRING_LEN(s->header_buf) - s->head_length);
|
|
1713
|
+
} else {
|
|
1714
|
+
trailer_data = rb_str_new(NULL, 0);
|
|
1715
|
+
}
|
|
1716
|
+
curl_easy_setopt(s->easy, CURLOPT_COOKIELIST, "FLUSH");
|
|
1717
|
+
|
|
1718
|
+
return rb_ary_new_from_args(5, LL2NUM(downloaded), LL2NUM(uploaded), create_timings(s->easy),
|
|
1719
|
+
complete ? Qtrue : Qfalse, trailer_data);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/*
|
|
1723
|
+
* Ends the streamed transfer, whether or not its body was received in full. A no-op when
|
|
1724
|
+
* this thread or fiber has no streamed transfer open.
|
|
1725
|
+
*
|
|
1726
|
+
* @api private
|
|
1727
|
+
* @return [Array, nil] downloaded bytes, uploaded bytes, timings, whether the transfer completed,
|
|
1728
|
+
* and the raw trailer lines
|
|
1729
|
+
*/
|
|
1730
|
+
static VALUE session_transfer_finish(VALUE self) {
|
|
1731
|
+
struct patron_session *s = get_session(self);
|
|
1732
|
+
|
|
1733
|
+
if (!s->easy || !s->streaming || s->owner != rb_fiber_current()) return Qnil;
|
|
1734
|
+
return rb_ensure(finish_stream, (VALUE)s, cleanup, self);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
struct scheduler_raise_args {
|
|
1738
|
+
VALUE scheduler;
|
|
1739
|
+
VALUE fiber;
|
|
1740
|
+
VALUE exc;
|
|
1741
|
+
};
|
|
1742
|
+
|
|
1743
|
+
static VALUE scheduler_raise(VALUE ptr) {
|
|
1744
|
+
struct scheduler_raise_args *a = (struct scheduler_raise_args *)ptr;
|
|
1745
|
+
return rb_funcall(a->scheduler, id_raise, 2, a->fiber, a->exc);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/* curl_multi_wakeup() does not reach an owner parked in the Fiber scheduler: libcurl's wakeup
|
|
1749
|
+
* socket is not among the ones curl_multi_fdset() hands out. So raise into the owner instead,
|
|
1750
|
+
* through the scheduler's #raise, which is what Async::Task#stop uses. That queues the calling
|
|
1751
|
+
* fiber to be resumed; a bare Fiber#raise would transfer away from it for good. Without such a
|
|
1752
|
+
* scheduler the owner notices the flag at its next wait cap. */
|
|
1753
|
+
static void raise_into_waiting_owner(struct patron_session *s) {
|
|
1754
|
+
struct scheduler_raise_args args;
|
|
1755
|
+
int state = 0;
|
|
1756
|
+
|
|
1757
|
+
if (!s->in_scheduler_wait) return;
|
|
1758
|
+
if (s->owner_thread != rb_thread_current() || s->owner == rb_fiber_current()) return;
|
|
1759
|
+
if (!RTEST(rb_fiber_alive_p(s->owner))) return;
|
|
1760
|
+
args.scheduler = rb_fiber_scheduler_current();
|
|
1761
|
+
if (NIL_P(args.scheduler) || !rb_respond_to(args.scheduler, id_raise)) return;
|
|
1762
|
+
|
|
1763
|
+
args.fiber = s->owner;
|
|
1764
|
+
args.exc = build_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
|
|
1765
|
+
s->in_scheduler_wait = 0;
|
|
1766
|
+
rb_protect(scheduler_raise, (VALUE)&args, &state);
|
|
1767
|
+
if (state) {
|
|
1768
|
+
/* Could not raise into the owner, so the flag stops it at its next wait cap. Anything
|
|
1769
|
+
* else, such as this fiber being stopped while it waited to be resumed, is not ours. */
|
|
1770
|
+
if (!rb_obj_is_kind_of(rb_errinfo(), eFiberError)) rb_jump_tag(state);
|
|
1771
|
+
rb_set_errinfo(Qnil);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1164
1775
|
/*
|
|
1165
1776
|
* Aborts the request currently in flight on this Session, which then raises Patron::Aborted.
|
|
1166
|
-
* Safe to call from another thread.
|
|
1777
|
+
* Safe to call from another thread. Called from another fiber on the thread of the request,
|
|
1778
|
+
* under a Fiber scheduler with a #raise of its own (Async), it raises the Aborted into the
|
|
1779
|
+
* waiting fiber at once, the way Async::Task#stop does.
|
|
1167
1780
|
*
|
|
1168
1781
|
* @return self
|
|
1169
1782
|
*/
|
|
@@ -1172,6 +1785,7 @@ static VALUE session_interrupt(VALUE self) {
|
|
|
1172
1785
|
|
|
1173
1786
|
s->interrupt = INTERRUPT_ABORT;
|
|
1174
1787
|
if (s->multi) curl_multi_wakeup(s->multi);
|
|
1788
|
+
raise_into_waiting_owner(s);
|
|
1175
1789
|
return self;
|
|
1176
1790
|
}
|
|
1177
1791
|
|
|
@@ -1224,6 +1838,12 @@ void Init_session_ext(void) {
|
|
|
1224
1838
|
#undef DEFINE_ID
|
|
1225
1839
|
id_to_path = rb_intern_const("to_path");
|
|
1226
1840
|
id_to_s = rb_intern_const("to_s");
|
|
1841
|
+
id_read = rb_intern_const("read");
|
|
1842
|
+
id_readpartial = rb_intern_const("readpartial");
|
|
1843
|
+
id_size = rb_intern_const("size");
|
|
1844
|
+
id_pos = rb_intern_const("pos");
|
|
1845
|
+
id_seek = rb_intern_const("seek");
|
|
1846
|
+
id_rewind = rb_intern_const("rewind");
|
|
1227
1847
|
id_response_class = rb_intern_const("response_class");
|
|
1228
1848
|
id_default_response_charset = rb_intern_const("default_response_charset");
|
|
1229
1849
|
id_get = rb_intern_const("get");
|
|
@@ -1233,6 +1853,7 @@ void Init_session_ext(void) {
|
|
|
1233
1853
|
id_delete = rb_intern_const("delete");
|
|
1234
1854
|
id_head = rb_intern_const("head");
|
|
1235
1855
|
id_ivar_curl_code = rb_intern_const("@curl_code");
|
|
1856
|
+
id_raise = rb_intern_const("raise");
|
|
1236
1857
|
id_url_kw = rb_intern_const("url");
|
|
1237
1858
|
id_status_kw = rb_intern_const("status");
|
|
1238
1859
|
id_redirect_count_kw = rb_intern_const("redirect_count");
|
|
@@ -1261,6 +1882,7 @@ void Init_session_ext(void) {
|
|
|
1261
1882
|
rb_global_variable(&eAborted);
|
|
1262
1883
|
rb_global_variable(&eDownloadLimitExceeded);
|
|
1263
1884
|
rb_global_variable(&eClientCertificateError);
|
|
1885
|
+
rb_global_variable(&eFiberError);
|
|
1264
1886
|
|
|
1265
1887
|
ePatronError = rb_const_get(mPatron, rb_intern("Error"));
|
|
1266
1888
|
eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
|
|
@@ -1273,6 +1895,7 @@ void Init_session_ext(void) {
|
|
|
1273
1895
|
eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
|
|
1274
1896
|
eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));
|
|
1275
1897
|
eAborted = rb_const_get(mPatron, rb_intern("Aborted"));
|
|
1898
|
+
eFiberError = rb_const_get(rb_cObject, rb_intern("FiberError"));
|
|
1276
1899
|
eDownloadLimitExceeded = rb_const_get(mPatron, rb_intern("DownloadLimitExceeded"));
|
|
1277
1900
|
eClientCertificateError = rb_const_get(mPatron, rb_intern("ClientCertificateError"));
|
|
1278
1901
|
|
|
@@ -1294,6 +1917,9 @@ void Init_session_ext(void) {
|
|
|
1294
1917
|
rb_define_alias(cSession, "urldecode", "unescape");
|
|
1295
1918
|
|
|
1296
1919
|
rb_define_private_method(cSession, "handle_request", session_handle_request, 1);
|
|
1920
|
+
rb_define_private_method(cSession, "transfer_start", session_transfer_start, 1);
|
|
1921
|
+
rb_define_private_method(cSession, "transfer_read", session_transfer_read, -1);
|
|
1922
|
+
rb_define_private_method(cSession, "transfer_finish", session_transfer_finish, 0);
|
|
1297
1923
|
rb_define_private_method(cSession, "set_cookie_file", session_set_cookie_file, 1);
|
|
1298
1924
|
rb_define_private_method(cSession, "set_debug_io", session_set_debug_io, 1);
|
|
1299
1925
|
rb_define_method(cSession, "interrupt", session_interrupt, 0);
|