cool.io 1.9.4 → 1.9.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/.github/workflows/valgrind.yaml +44 -0
- data/Rakefile +6 -19
- data/cool.io.gemspec +1 -3
- data/ext/cool.io/buffer.c +50 -39
- data/ext/cool.io/cool.io.h +6 -0
- data/ext/cool.io/extconf.rb +0 -1
- data/ext/cool.io/loop.c +15 -15
- data/ext/cool.io/stat_watcher.c +15 -2
- data/ext/cool.io/watcher.c +13 -1
- data/ext/libev/ev.c +7 -1
- data/ext/libev/ev_select.c +10 -0
- data/gems.rb +4 -0
- data/lib/cool.io/dns_resolver.rb +52 -8
- data/lib/cool.io/version.rb +2 -2
- data/libev_win_select.diff +25 -9
- data/spec/async_watcher_spec.rb +3 -1
- data/spec/detach_race_condition_spec.rb +44 -22
- data/spec/dns_spec.rb +186 -0
- data/spec/iobuffer_spec.rb +31 -0
- data/spec/stat_watcher_spec.rb +31 -1
- data/spec/tcp_server_spec.rb +28 -4
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3da0843b8a62fd01aab297ce31f885a5856b4326bef2fb3c90d363f9576fe9a3
|
|
4
|
+
data.tar.gz: d073dd104500ccc2c1b62411c8f02c083df004353f9d2798ccf8ae41166effbd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3f2fab2151abdbd3e0762aa04675cab8b4d196990227bf5d068f12360a62c87e669480f50e70b47b289a9c0f29deeddb28836590422faafe5d0a5ecaf6173fb
|
|
7
|
+
data.tar.gz: 233ea14784f4390b6b1f1b8fb131d5b1d20d14d0b78f9f1fd3b39f89bd25d34be505267d23d1b5647be2febcd5216e33a459c0e135ee49737334a06a0626bb47
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Memcheck
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: ${{matrix.ruby}} on ${{matrix.os}}
|
|
11
|
+
runs-on: ${{matrix.os}}-latest
|
|
12
|
+
continue-on-error: ${{matrix.experimental}}
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
os:
|
|
17
|
+
- ubuntu
|
|
18
|
+
|
|
19
|
+
ruby:
|
|
20
|
+
- "3.2"
|
|
21
|
+
- "3.3"
|
|
22
|
+
- "3.4"
|
|
23
|
+
- "4.0"
|
|
24
|
+
|
|
25
|
+
experimental: [false]
|
|
26
|
+
|
|
27
|
+
include:
|
|
28
|
+
- os: ubuntu
|
|
29
|
+
ruby: head
|
|
30
|
+
experimental: true
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v6
|
|
34
|
+
- uses: ruby/setup-ruby@v1
|
|
35
|
+
with:
|
|
36
|
+
ruby-version: ${{matrix.ruby}}
|
|
37
|
+
bundler-cache: true
|
|
38
|
+
- name: Install Valgrind
|
|
39
|
+
run: |
|
|
40
|
+
sudo apt-get update
|
|
41
|
+
sudo apt-get install -y valgrind
|
|
42
|
+
- name: Run tests
|
|
43
|
+
timeout-minutes: 10
|
|
44
|
+
run: bundle exec rake spec:valgrind
|
data/Rakefile
CHANGED
|
@@ -22,7 +22,7 @@ end
|
|
|
22
22
|
|
|
23
23
|
require 'rake/extensiontask'
|
|
24
24
|
|
|
25
|
-
spec =
|
|
25
|
+
spec = Gem::Specification.load("cool.io.gemspec")
|
|
26
26
|
|
|
27
27
|
def configure_cross_compilation(ext)
|
|
28
28
|
unless RUBY_PLATFORM =~ /mswin|mingw/
|
|
@@ -48,24 +48,11 @@ namespace :build do
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
require
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
files << f if File.basename(f) =~ /.*spec.*\.rb$/
|
|
57
|
-
end
|
|
58
|
-
cmdline = "#{RUBY} -I.:lib:ext:spec \
|
|
59
|
-
-e '%w[#{files.join(' ')}].each { |f| require f }'"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
namespace :spec do
|
|
63
|
-
desc "run specs with valgrind"
|
|
64
|
-
task :valgrind => :compile do
|
|
65
|
-
system "valgrind --num-callers=15 \
|
|
66
|
-
--partial-loads-ok=yes --undef-value-errors=no \
|
|
67
|
-
--tool=memcheck --leak-check=yes --track-fds=yes \
|
|
68
|
-
--show-reachable=yes #{specs_command}"
|
|
51
|
+
if RUBY_PLATFORM.include?('linux')
|
|
52
|
+
require 'ruby_memcheck'
|
|
53
|
+
require 'ruby_memcheck/rspec/rake_task'
|
|
54
|
+
namespace :spec do
|
|
55
|
+
RubyMemcheck::RSpec::RakeTask.new(valgrind: :compile)
|
|
69
56
|
end
|
|
70
57
|
end
|
|
71
58
|
|
data/cool.io.gemspec
CHANGED
data/ext/cool.io/buffer.c
CHANGED
|
@@ -28,17 +28,22 @@
|
|
|
28
28
|
|
|
29
29
|
/* Default number of bytes in each node's buffer. Should be >= MTU */
|
|
30
30
|
#define DEFAULT_NODE_SIZE 16384
|
|
31
|
-
static
|
|
31
|
+
static size_t default_node_size = DEFAULT_NODE_SIZE;
|
|
32
32
|
|
|
33
|
+
/*
|
|
34
|
+
* Byte counts are kept in size_t so the accounting cannot wrap while the
|
|
35
|
+
* buffer itself still holds the data. node_size is capped at MAX_BUFFER_SIZE,
|
|
36
|
+
* but size grows with whatever the buffer is asked to hold.
|
|
37
|
+
*/
|
|
33
38
|
struct buffer {
|
|
34
|
-
|
|
39
|
+
size_t size, node_size;
|
|
35
40
|
struct buffer_node *head, *tail;
|
|
36
41
|
struct buffer_node *pool_head, *pool_tail;
|
|
37
42
|
|
|
38
43
|
};
|
|
39
44
|
|
|
40
45
|
struct buffer_node {
|
|
41
|
-
|
|
46
|
+
size_t start, end;
|
|
42
47
|
struct buffer_node *next;
|
|
43
48
|
unsigned char data[0];
|
|
44
49
|
};
|
|
@@ -68,13 +73,13 @@ static struct buffer *buffer_init(struct buffer *);
|
|
|
68
73
|
static void buffer_clear(struct buffer * buf);
|
|
69
74
|
static void buffer_free(struct buffer * buf);
|
|
70
75
|
static void buffer_free_pool(struct buffer * buf);
|
|
71
|
-
static void buffer_prepend(struct buffer * buf, char *str,
|
|
72
|
-
static void buffer_append(struct buffer * buf, char *str,
|
|
73
|
-
static void buffer_read(struct buffer * buf, char *str,
|
|
76
|
+
static void buffer_prepend(struct buffer * buf, char *str, size_t len);
|
|
77
|
+
static void buffer_append(struct buffer * buf, char *str, size_t len);
|
|
78
|
+
static void buffer_read(struct buffer * buf, char *str, size_t len);
|
|
74
79
|
static int buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark);
|
|
75
|
-
static void buffer_copy(struct buffer * buf, char *str,
|
|
76
|
-
static
|
|
77
|
-
static
|
|
80
|
+
static void buffer_copy(struct buffer * buf, char *str, size_t len);
|
|
81
|
+
static ssize_t buffer_read_from(struct buffer * buf, int fd);
|
|
82
|
+
static ssize_t buffer_write_to(struct buffer * buf, int fd);
|
|
78
83
|
|
|
79
84
|
/*
|
|
80
85
|
* High-performance I/O buffer intended for use in non-blocking programs
|
|
@@ -145,21 +150,21 @@ Coolio_Buffer_free(void * buf)
|
|
|
145
150
|
|
|
146
151
|
/**
|
|
147
152
|
* call-seq:
|
|
148
|
-
* Coolio::Buffer.default_node_size ->
|
|
153
|
+
* Coolio::Buffer.default_node_size -> 16384
|
|
149
154
|
*
|
|
150
155
|
* Retrieves the current value of the default node size.
|
|
151
156
|
*/
|
|
152
157
|
static VALUE
|
|
153
158
|
Coolio_Buffer_default_node_size(VALUE klass)
|
|
154
159
|
{
|
|
155
|
-
return
|
|
160
|
+
return SIZET2NUM(default_node_size);
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
/*
|
|
159
164
|
* safely converts node sizes from Ruby numerics to C and raising
|
|
160
165
|
* ArgumentError or RangeError on invalid sizes
|
|
161
166
|
*/
|
|
162
|
-
static
|
|
167
|
+
static size_t
|
|
163
168
|
convert_node_size(VALUE size)
|
|
164
169
|
{
|
|
165
170
|
if (
|
|
@@ -168,7 +173,7 @@ convert_node_size(VALUE size)
|
|
|
168
173
|
)
|
|
169
174
|
rb_raise(rb_eArgError, "invalid buffer size");
|
|
170
175
|
|
|
171
|
-
return (
|
|
176
|
+
return NUM2SIZET(size);
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
/**
|
|
@@ -241,7 +246,7 @@ Coolio_Buffer_size(VALUE self)
|
|
|
241
246
|
struct buffer *buf;
|
|
242
247
|
TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
|
|
243
248
|
|
|
244
|
-
return
|
|
249
|
+
return SIZET2NUM(buf->size);
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
/**
|
|
@@ -311,15 +316,18 @@ static VALUE
|
|
|
311
316
|
Coolio_Buffer_read(int argc, VALUE * argv, VALUE self)
|
|
312
317
|
{
|
|
313
318
|
VALUE length_obj, str;
|
|
314
|
-
|
|
319
|
+
size_t length;
|
|
315
320
|
struct buffer *buf;
|
|
316
321
|
|
|
317
322
|
TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
|
|
318
323
|
|
|
319
324
|
if (rb_scan_args(argc, argv, "01", &length_obj) == 1) {
|
|
320
|
-
|
|
321
|
-
|
|
325
|
+
/* Read signed so a negative argument still raises ArgumentError here
|
|
326
|
+
* rather than RangeError out of the conversion */
|
|
327
|
+
long requested = NUM2LONG(length_obj);
|
|
328
|
+
if(requested < 1)
|
|
322
329
|
rb_raise(rb_eArgError, "length must be greater than zero");
|
|
330
|
+
length = (size_t) requested;
|
|
323
331
|
if(length > buf->size)
|
|
324
332
|
length = buf->size;
|
|
325
333
|
} else
|
|
@@ -328,7 +336,7 @@ Coolio_Buffer_read(int argc, VALUE * argv, VALUE self)
|
|
|
328
336
|
if(buf->size == 0)
|
|
329
337
|
return rb_str_new2("");
|
|
330
338
|
|
|
331
|
-
str = rb_str_new(0, length);
|
|
339
|
+
str = rb_str_new(0, (long) length);
|
|
332
340
|
buffer_read(buf, RSTRING_PTR(str), length);
|
|
333
341
|
|
|
334
342
|
return str;
|
|
@@ -352,6 +360,9 @@ Coolio_Buffer_read_frame(VALUE self, VALUE data, VALUE mark)
|
|
|
352
360
|
|
|
353
361
|
TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
|
|
354
362
|
|
|
363
|
+
StringValue(data);
|
|
364
|
+
rb_str_modify(data);
|
|
365
|
+
|
|
355
366
|
if (buffer_read_frame(buf, data, mark_c)) {
|
|
356
367
|
return Qtrue;
|
|
357
368
|
} else {
|
|
@@ -373,7 +384,7 @@ Coolio_Buffer_to_str(VALUE self)
|
|
|
373
384
|
|
|
374
385
|
TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
|
|
375
386
|
|
|
376
|
-
str = rb_str_new(0, buf->size);
|
|
387
|
+
str = rb_str_new(0, (long) buf->size);
|
|
377
388
|
buffer_copy(buf, RSTRING_PTR(str), buf->size);
|
|
378
389
|
|
|
379
390
|
return str;
|
|
@@ -391,7 +402,7 @@ static VALUE
|
|
|
391
402
|
Coolio_Buffer_read_from(VALUE self, VALUE io)
|
|
392
403
|
{
|
|
393
404
|
struct buffer *buf;
|
|
394
|
-
|
|
405
|
+
ssize_t ret;
|
|
395
406
|
#if defined(HAVE_RB_IO_T) || defined(HAVE_RB_IO_DESCRIPTOR)
|
|
396
407
|
rb_io_t *fptr;
|
|
397
408
|
#else
|
|
@@ -408,7 +419,7 @@ Coolio_Buffer_read_from(VALUE self, VALUE io)
|
|
|
408
419
|
#else
|
|
409
420
|
ret = buffer_read_from(buf, FPTR_TO_FD(fptr));
|
|
410
421
|
#endif
|
|
411
|
-
return ret == -1 ? Qnil :
|
|
422
|
+
return ret == -1 ? Qnil : SSIZET2NUM(ret);
|
|
412
423
|
}
|
|
413
424
|
|
|
414
425
|
/**
|
|
@@ -435,9 +446,9 @@ Coolio_Buffer_write_to(VALUE self, VALUE io)
|
|
|
435
446
|
rb_io_set_nonblock(fptr);
|
|
436
447
|
|
|
437
448
|
#ifdef HAVE_RB_IO_DESCRIPTOR
|
|
438
|
-
return
|
|
449
|
+
return SSIZET2NUM(buffer_write_to(buf, rb_io_descriptor(io)));
|
|
439
450
|
#else
|
|
440
|
-
return
|
|
451
|
+
return SSIZET2NUM(buffer_write_to(buf, FPTR_TO_FD(fptr)));
|
|
441
452
|
#endif
|
|
442
453
|
}
|
|
443
454
|
|
|
@@ -535,7 +546,7 @@ buffer_node_free(struct buffer * buf, struct buffer_node * node)
|
|
|
535
546
|
|
|
536
547
|
/* Prepend data to the front of the buffer */
|
|
537
548
|
static void
|
|
538
|
-
buffer_prepend(struct buffer * buf, char *str,
|
|
549
|
+
buffer_prepend(struct buffer * buf, char *str, size_t len)
|
|
539
550
|
{
|
|
540
551
|
struct buffer_node *node, *tmp;
|
|
541
552
|
buf->size += len;
|
|
@@ -576,9 +587,9 @@ buffer_prepend(struct buffer * buf, char *str, unsigned len)
|
|
|
576
587
|
|
|
577
588
|
/* Append data to the front of the buffer */
|
|
578
589
|
static void
|
|
579
|
-
buffer_append(struct buffer * buf, char *str,
|
|
590
|
+
buffer_append(struct buffer * buf, char *str, size_t len)
|
|
580
591
|
{
|
|
581
|
-
|
|
592
|
+
size_t nbytes;
|
|
582
593
|
buf->size += len;
|
|
583
594
|
|
|
584
595
|
/* If it fits in the remaining space in the tail */
|
|
@@ -613,9 +624,9 @@ buffer_append(struct buffer * buf, char *str, unsigned len)
|
|
|
613
624
|
|
|
614
625
|
/* Read data from the buffer (and clear what we've read) */
|
|
615
626
|
static void
|
|
616
|
-
buffer_read(struct buffer * buf, char *str,
|
|
627
|
+
buffer_read(struct buffer * buf, char *str, size_t len)
|
|
617
628
|
{
|
|
618
|
-
|
|
629
|
+
size_t nbytes;
|
|
619
630
|
struct buffer_node *tmp;
|
|
620
631
|
|
|
621
632
|
while (buf->size > 0 && len > 0) {
|
|
@@ -649,7 +660,7 @@ buffer_read(struct buffer * buf, char *str, unsigned len)
|
|
|
649
660
|
static int
|
|
650
661
|
buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark)
|
|
651
662
|
{
|
|
652
|
-
|
|
663
|
+
size_t nbytes = 0;
|
|
653
664
|
struct buffer_node *tmp;
|
|
654
665
|
|
|
655
666
|
while (buf->size > 0) {
|
|
@@ -664,7 +675,7 @@ buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark)
|
|
|
664
675
|
}
|
|
665
676
|
|
|
666
677
|
/* Copy less than everything if we found a frame byte */
|
|
667
|
-
rb_str_cat(str, s, nbytes);
|
|
678
|
+
rb_str_cat(str, s, (long) nbytes);
|
|
668
679
|
|
|
669
680
|
/* Fixup the buffer pointers to indicate the bytes were consumed */
|
|
670
681
|
head->start += nbytes;
|
|
@@ -688,9 +699,9 @@ buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark)
|
|
|
688
699
|
|
|
689
700
|
/* Copy data from the buffer without clearing it */
|
|
690
701
|
static void
|
|
691
|
-
buffer_copy(struct buffer * buf, char *str,
|
|
702
|
+
buffer_copy(struct buffer * buf, char *str, size_t len)
|
|
692
703
|
{
|
|
693
|
-
|
|
704
|
+
size_t nbytes;
|
|
694
705
|
struct buffer_node *node;
|
|
695
706
|
|
|
696
707
|
node = buf->head;
|
|
@@ -709,10 +720,10 @@ buffer_copy(struct buffer * buf, char *str, unsigned len)
|
|
|
709
720
|
}
|
|
710
721
|
|
|
711
722
|
/* Write data from the buffer to a file descriptor */
|
|
712
|
-
static
|
|
723
|
+
static ssize_t
|
|
713
724
|
buffer_write_to(struct buffer * buf, int fd)
|
|
714
725
|
{
|
|
715
|
-
|
|
726
|
+
ssize_t bytes_written, total_bytes_written = 0;
|
|
716
727
|
struct buffer_node *tmp;
|
|
717
728
|
|
|
718
729
|
while (buf->head) {
|
|
@@ -730,7 +741,7 @@ buffer_write_to(struct buffer * buf, int fd)
|
|
|
730
741
|
buf->size -= bytes_written;
|
|
731
742
|
|
|
732
743
|
/* If the write blocked... */
|
|
733
|
-
if (bytes_written < buf->head->end - buf->head->start) {
|
|
744
|
+
if ((size_t) bytes_written < buf->head->end - buf->head->start) {
|
|
734
745
|
buf->head->start += bytes_written;
|
|
735
746
|
return total_bytes_written;
|
|
736
747
|
}
|
|
@@ -748,11 +759,11 @@ buffer_write_to(struct buffer * buf, int fd)
|
|
|
748
759
|
|
|
749
760
|
/* Read data from a file descriptor to a buffer */
|
|
750
761
|
/* Append data to the front of the buffer */
|
|
751
|
-
static
|
|
762
|
+
static ssize_t
|
|
752
763
|
buffer_read_from(struct buffer * buf, int fd)
|
|
753
764
|
{
|
|
754
|
-
|
|
755
|
-
|
|
765
|
+
ssize_t bytes_read, total_bytes_read = 0;
|
|
766
|
+
size_t nbytes;
|
|
756
767
|
|
|
757
768
|
/* Empty list needs initialized */
|
|
758
769
|
if (!buf->head) {
|
|
@@ -782,7 +793,7 @@ buffer_read_from(struct buffer * buf, int fd)
|
|
|
782
793
|
buf->tail->next = buffer_node_new(buf);
|
|
783
794
|
buf->tail = buf->tail->next;
|
|
784
795
|
}
|
|
785
|
-
} while (bytes_read == nbytes);
|
|
796
|
+
} while ((size_t) bytes_read == nbytes);
|
|
786
797
|
|
|
787
798
|
return total_bytes_read;
|
|
788
799
|
}
|
data/ext/cool.io/cool.io.h
CHANGED
|
@@ -55,6 +55,12 @@ struct Coolio_Watcher
|
|
|
55
55
|
int enabled;
|
|
56
56
|
VALUE loop;
|
|
57
57
|
|
|
58
|
+
/* Stable copy of the watched path for ev_stat watchers. libev retains the
|
|
59
|
+
* path pointer passed to ev_stat_init() for the lifetime of the watcher, so
|
|
60
|
+
* it must not point into a Ruby String whose buffer GC.compact may relocate.
|
|
61
|
+
* NULL for watcher types that don't use it. */
|
|
62
|
+
char *stat_path;
|
|
63
|
+
|
|
58
64
|
void (*dispatch_callback)(VALUE self, int revents);
|
|
59
65
|
};
|
|
60
66
|
|
data/ext/cool.io/extconf.rb
CHANGED
data/ext/cool.io/loop.c
CHANGED
|
@@ -110,8 +110,7 @@ void Coolio_Loop_process_event(VALUE watcher, int revents)
|
|
|
110
110
|
struct Coolio_Loop *loop_data;
|
|
111
111
|
struct Coolio_Watcher *watcher_data;
|
|
112
112
|
|
|
113
|
-
/* The Global VM
|
|
114
|
-
* we can still do this safely */
|
|
113
|
+
/* The Global VM Lock is held here, see the explanation below */
|
|
115
114
|
watcher_data = Coolio_Watcher_ptr(watcher);
|
|
116
115
|
|
|
117
116
|
if (watcher_data->enabled == 0) {
|
|
@@ -126,13 +125,14 @@ void Coolio_Loop_process_event(VALUE watcher, int revents)
|
|
|
126
125
|
*
|
|
127
126
|
* Our call path up to here looks a little something like:
|
|
128
127
|
*
|
|
129
|
-
* -> release GVL -> event syscall -> libev callback
|
|
130
|
-
* (GVL = Global VM Lock)
|
|
128
|
+
* -> release GVL -> event syscall -> reacquire GVL -> libev callback
|
|
129
|
+
* (GVL = Global VM Lock) ^^^ You are here
|
|
131
130
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
131
|
+
* libev is patched (see ev.c) to release the GVL around the blocking
|
|
132
|
+
* system call (one of epoll, kqueue, port, poll, or select, depending
|
|
133
|
+
* on the platform) so other Ruby threads can run while we wait there.
|
|
134
|
+
* Only that call runs without the GVL: libev invokes the events it
|
|
135
|
+
* collected after the call has returned, so we hold the GVL here.
|
|
136
136
|
*
|
|
137
137
|
* More specifically, this is a libev callback abstraction
|
|
138
138
|
* called from a real libev callback in every watcher,
|
|
@@ -150,19 +150,19 @@ void Coolio_Loop_process_event(VALUE watcher, int revents)
|
|
|
150
150
|
* event fired, why the hell is it telling the loop? Why
|
|
151
151
|
* doesn't it just rb_funcall() the appropriate callback?
|
|
152
152
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* it back we have to:
|
|
153
|
+
* Because Ruby code doesn't run from inside libev's own event
|
|
154
|
+
* invocation. Instead:
|
|
156
155
|
*
|
|
157
|
-
* stash event and return ->
|
|
156
|
+
* stash event and return -> ev_loop() returns -> dispatch to Ruby
|
|
158
157
|
*
|
|
159
|
-
* Which is kinda ugly and confusing, but still gives us
|
|
158
|
+
* Which is kinda ugly and confusing, but still gives us
|
|
160
159
|
* an O(1) event loop whose heart is in the kernel itself. w00t!
|
|
161
160
|
*
|
|
162
161
|
* So, stash the event in the loop's data struct. When we return
|
|
163
162
|
* the ev_loop() call being made in the Coolio_Loop_run_once_blocking()
|
|
164
|
-
* function below will also return,
|
|
165
|
-
*
|
|
163
|
+
* function below will also return, and Coolio_Loop_dispatch_events()
|
|
164
|
+
* walks what we stashed and calls out to Ruby. A watcher detached
|
|
165
|
+
* along the way nils its own stashed entries, which that walk skips */
|
|
166
166
|
|
|
167
167
|
/* Grow the event buffer if it's too small */
|
|
168
168
|
if(loop_data->events_received >= loop_data->eventbuf_size) {
|
data/ext/cool.io/stat_watcher.c
CHANGED
|
@@ -79,7 +79,9 @@ void Init_coolio_stat_watcher()
|
|
|
79
79
|
*/
|
|
80
80
|
static VALUE Coolio_StatWatcher_initialize(int argc, VALUE *argv, VALUE self)
|
|
81
81
|
{
|
|
82
|
-
|
|
82
|
+
VALUE path, interval;
|
|
83
|
+
const char *path_str;
|
|
84
|
+
long path_len;
|
|
83
85
|
struct Coolio_Watcher *watcher_data;
|
|
84
86
|
|
|
85
87
|
rb_scan_args(argc, argv, "11", &path, &interval);
|
|
@@ -87,15 +89,26 @@ static VALUE Coolio_StatWatcher_initialize(int argc, VALUE *argv, VALUE self)
|
|
|
87
89
|
interval = rb_convert_type(interval, T_FLOAT, "Float", "to_f");
|
|
88
90
|
|
|
89
91
|
path = rb_String(path);
|
|
92
|
+
path_str = StringValueCStr(path);
|
|
90
93
|
rb_iv_set(self, "@path", path);
|
|
91
94
|
|
|
92
95
|
watcher_data = Coolio_Watcher_ptr(self);
|
|
93
96
|
|
|
97
|
+
/* libev keeps the path pointer passed to ev_stat_init() for the lifetime of
|
|
98
|
+
* the watcher. RSTRING_PTR(path) points into a Ruby String whose buffer
|
|
99
|
+
* GC.compact may relocate, leaving libev with a dangling pointer. Keep an
|
|
100
|
+
* owned copy instead; it is released in Coolio_Watcher_free(). */
|
|
101
|
+
path_len = strlen(path_str);
|
|
102
|
+
if(watcher_data->stat_path)
|
|
103
|
+
xfree(watcher_data->stat_path);
|
|
104
|
+
watcher_data->stat_path = xmalloc(path_len + 1);
|
|
105
|
+
memcpy(watcher_data->stat_path, path_str, path_len + 1);
|
|
106
|
+
|
|
94
107
|
watcher_data->dispatch_callback = Coolio_StatWatcher_dispatch_callback;
|
|
95
108
|
ev_stat_init(
|
|
96
109
|
&watcher_data->event_types.ev_stat,
|
|
97
110
|
Coolio_StatWatcher_libev_callback,
|
|
98
|
-
|
|
111
|
+
watcher_data->stat_path,
|
|
99
112
|
interval == Qnil ? 0 : NUM2DBL(interval)
|
|
100
113
|
);
|
|
101
114
|
watcher_data->event_types.ev_stat.data = (void *)self;
|
data/ext/cool.io/watcher.c
CHANGED
|
@@ -14,6 +14,7 @@ static VALUE cCoolio_Watcher = Qnil;
|
|
|
14
14
|
|
|
15
15
|
static VALUE Coolio_Watcher_allocate(VALUE klass);
|
|
16
16
|
static void Coolio_Watcher_mark(void *data);
|
|
17
|
+
static void Coolio_Watcher_free(void *data);
|
|
17
18
|
|
|
18
19
|
static VALUE Coolio_Watcher_initialize(VALUE self);
|
|
19
20
|
static VALUE Coolio_Watcher_attach(VALUE self, VALUE loop);
|
|
@@ -56,7 +57,7 @@ static const rb_data_type_t Coolio_Watcher_type = {
|
|
|
56
57
|
"Coolio::Watcher",
|
|
57
58
|
{
|
|
58
59
|
Coolio_Watcher_mark,
|
|
59
|
-
|
|
60
|
+
Coolio_Watcher_free,
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
|
|
@@ -75,6 +76,7 @@ static VALUE Coolio_Watcher_allocate(VALUE klass)
|
|
|
75
76
|
|
|
76
77
|
watcher_data->loop = Qnil;
|
|
77
78
|
watcher_data->enabled = 0;
|
|
79
|
+
watcher_data->stat_path = NULL;
|
|
78
80
|
|
|
79
81
|
return watcher;
|
|
80
82
|
}
|
|
@@ -87,6 +89,16 @@ static void Coolio_Watcher_mark(void *data)
|
|
|
87
89
|
rb_gc_mark(watcher_data->loop);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
static void Coolio_Watcher_free(void *data)
|
|
93
|
+
{
|
|
94
|
+
struct Coolio_Watcher *watcher_data = data;
|
|
95
|
+
|
|
96
|
+
if(watcher_data->stat_path)
|
|
97
|
+
xfree(watcher_data->stat_path);
|
|
98
|
+
|
|
99
|
+
xfree(watcher_data);
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
static VALUE Coolio_Watcher_initialize(VALUE self)
|
|
91
103
|
{
|
|
92
104
|
rb_raise(rb_eRuntimeError, "watcher base class should not be initialized directly");
|
data/ext/libev/ev.c
CHANGED
|
@@ -210,7 +210,13 @@
|
|
|
210
210
|
#else
|
|
211
211
|
# include <io.h>
|
|
212
212
|
# define WIN32_LEAN_AND_MEAN
|
|
213
|
-
|
|
213
|
+
/* ruby.h above already pulled in winsock2.h, so fd_set may be dimensioned
|
|
214
|
+
* already. Defining FD_SETSIZE unconditionally here would only move the bound
|
|
215
|
+
* used by EV_WIN_FD_SET, not the array it indexes. Take whatever is in effect
|
|
216
|
+
* and only supply a default when nothing has been decided yet. */
|
|
217
|
+
# ifndef FD_SETSIZE
|
|
218
|
+
# define FD_SETSIZE 1024
|
|
219
|
+
# endif
|
|
214
220
|
# include <winsock2.h>
|
|
215
221
|
# include <windows.h>
|
|
216
222
|
# ifndef EV_SELECT_IS_WINSOCKET
|
data/ext/libev/ev_select.c
CHANGED
|
@@ -107,6 +107,16 @@ if (__i == ((fd_set *)(set))->fd_count) {\
|
|
|
107
107
|
#define EV_WIN_FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
|
|
108
108
|
#define EV_WIN_FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set *)(set))
|
|
109
109
|
#define EV_WIN_FD_COUNT(set) (((fd_set *)(set))->fd_count)
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
fd_set is dimensioned by whatever FD_SETSIZE was in effect when winsock2.h was
|
|
113
|
+
first pulled in, but EV_WIN_FD_SET and select_modify bound-check against the
|
|
114
|
+
FD_SETSIZE visible here. The two are decided by separate paths, so if the bound
|
|
115
|
+
ever exceeds the declared array we would write past the end of the allocation in
|
|
116
|
+
select_init. Catch that at build time instead.
|
|
117
|
+
*/
|
|
118
|
+
typedef char coolio_fd_setsize_matches_fd_set[
|
|
119
|
+
(sizeof (((fd_set *)0)->fd_array) / sizeof (SOCKET) >= (size_t)FD_SETSIZE) ? 1 : -1];
|
|
110
120
|
/* ######################################## */
|
|
111
121
|
#else
|
|
112
122
|
#define EV_WIN_FD_CLR FD_CLR
|
data/gems.rb
CHANGED
data/lib/cool.io/dns_resolver.rb
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
#++
|
|
19
19
|
|
|
20
20
|
require 'resolv'
|
|
21
|
+
require 'securerandom'
|
|
22
|
+
require 'socket'
|
|
21
23
|
|
|
22
24
|
module Coolio
|
|
23
25
|
# A non-blocking DNS resolver. It provides interfaces for querying both
|
|
@@ -67,14 +69,22 @@ module Coolio
|
|
|
67
69
|
# list of nameservers to query. By default the resolver will
|
|
68
70
|
# use nameservers listed in /etc/resolv.conf
|
|
69
71
|
def initialize(hostname, *nameservers)
|
|
72
|
+
nameservers = reject_ipv6_nameservers(nameservers)
|
|
70
73
|
if nameservers.empty?
|
|
71
|
-
nameservers = Resolv::DNS::Config.default_config_hash[:nameserver]
|
|
74
|
+
nameservers = reject_ipv6_nameservers(Resolv::DNS::Config.default_config_hash[:nameserver])
|
|
72
75
|
raise RuntimeError, "no nameservers found" if nameservers.empty? # TODO just call resolve_failed, not raise [also handle Errno::ENOENT)]
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
@nameservers = nameservers.dup
|
|
76
79
|
@question = request_question hostname
|
|
77
80
|
|
|
81
|
+
# A guessable ID would let an off-path attacker forge a response
|
|
82
|
+
@request_id = SecureRandom.random_number(1 << 16)
|
|
83
|
+
|
|
84
|
+
# Numeric addresses this query was sent to, and the lookups behind them
|
|
85
|
+
@queried_addresses = []
|
|
86
|
+
@numeric_addresses = {}
|
|
87
|
+
|
|
78
88
|
@socket = UDPSocket.new
|
|
79
89
|
@timer = Timeout.new(self)
|
|
80
90
|
|
|
@@ -113,27 +123,44 @@ module Coolio
|
|
|
113
123
|
|
|
114
124
|
# Send a request to the DNS server
|
|
115
125
|
def send_request
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
@nameservers.rotate!
|
|
127
|
+
|
|
128
|
+
# Send to the numeric address, so we know where a response must come from
|
|
129
|
+
address = numeric_address(@nameservers.first)
|
|
130
|
+
@queried_addresses << address unless @queried_addresses.include?(address)
|
|
131
|
+
|
|
118
132
|
begin
|
|
119
|
-
@socket.send request_message, 0,
|
|
133
|
+
@socket.send request_message, 0, address, DNS_PORT
|
|
120
134
|
rescue Errno::EHOSTUNREACH # TODO figure out why it has to be wrapper here, when the other wrapper should be wrapping this one!
|
|
121
135
|
end
|
|
122
136
|
end
|
|
123
137
|
|
|
124
138
|
# Called by the subclass when the DNS response is available
|
|
125
139
|
def on_readable
|
|
126
|
-
datagram = nil
|
|
140
|
+
datagram = sender = nil
|
|
127
141
|
begin
|
|
128
|
-
datagram = @socket.recvfrom_nonblock(DATAGRAM_SIZE)
|
|
142
|
+
datagram, sender = @socket.recvfrom_nonblock(DATAGRAM_SIZE)
|
|
129
143
|
rescue Errno::ECONNREFUSED
|
|
130
144
|
end
|
|
131
145
|
|
|
146
|
+
# Ignore anything we didn't ask for, rather than resolving or failing on it.
|
|
147
|
+
# The query stays outstanding, so the retry timer still bounds us.
|
|
148
|
+
return if datagram and not solicited_response?(datagram, sender)
|
|
149
|
+
|
|
132
150
|
address = response_address datagram rescue nil
|
|
133
151
|
address ? on_success(address) : on_failure
|
|
134
152
|
detach
|
|
135
153
|
end
|
|
136
154
|
|
|
155
|
+
# Is this a reply to our query, from an address we sent it to?
|
|
156
|
+
# Retries rotate through @nameservers, so any address already queried counts.
|
|
157
|
+
def solicited_response?(datagram, sender)
|
|
158
|
+
return false unless datagram.size >= 12
|
|
159
|
+
return false unless sender and sender[1] == DNS_PORT and @queried_addresses.include?(sender[3])
|
|
160
|
+
|
|
161
|
+
datagram[0..1].unpack('n').first.to_i == @request_id
|
|
162
|
+
end
|
|
163
|
+
|
|
137
164
|
def request_question(hostname)
|
|
138
165
|
raise ArgumentError, "hostname cannot be nil" if hostname.nil?
|
|
139
166
|
|
|
@@ -151,7 +178,7 @@ module Coolio
|
|
|
151
178
|
|
|
152
179
|
def request_message
|
|
153
180
|
# Standard query header
|
|
154
|
-
message = [
|
|
181
|
+
message = [@request_id, 1, 0].pack('nCC')
|
|
155
182
|
|
|
156
183
|
# One entry
|
|
157
184
|
qdcount = 1
|
|
@@ -166,7 +193,7 @@ module Coolio
|
|
|
166
193
|
def response_address(message)
|
|
167
194
|
# Confirm the ID field
|
|
168
195
|
id = message[0..1].unpack('n').first.to_i
|
|
169
|
-
return unless id ==
|
|
196
|
+
return unless id == @request_id
|
|
170
197
|
|
|
171
198
|
# Check the QR value and confirm this message is a response
|
|
172
199
|
qr = message[2..2].unpack('B1').first.to_i
|
|
@@ -204,6 +231,23 @@ module Coolio
|
|
|
204
231
|
nil
|
|
205
232
|
end
|
|
206
233
|
|
|
234
|
+
private
|
|
235
|
+
|
|
236
|
+
def reject_ipv6_nameservers(nameservers)
|
|
237
|
+
nameservers.reject { |ns| ns.include?(':') }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# The address of a nameserver, which may be given as a hostname.
|
|
241
|
+
# Only successful lookups are cached, so a transient failure is looked up again.
|
|
242
|
+
def numeric_address(nameserver)
|
|
243
|
+
@numeric_addresses[nameserver] ||= begin
|
|
244
|
+
addrinfo = Addrinfo.getaddrinfo(nameserver, nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM).first
|
|
245
|
+
raise SocketError, "getaddrinfo: no IPv4 address for #{nameserver}" if addrinfo.nil?
|
|
246
|
+
|
|
247
|
+
addrinfo.ip_address
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
207
251
|
class Timeout < TimerWatcher
|
|
208
252
|
def initialize(resolver)
|
|
209
253
|
@resolver = resolver
|
data/lib/cool.io/version.rb
CHANGED
data/libev_win_select.diff
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
diff --git a/ext/libev/ev.c b/ext/libev/ev.c
|
|
2
|
-
index
|
|
2
|
+
index a59efb2..5c18fd6 100644
|
|
3
3
|
--- a/ext/libev/ev.c
|
|
4
4
|
+++ b/ext/libev/ev.c
|
|
5
|
-
@@ -
|
|
5
|
+
@@ -210,6 +210,13 @@
|
|
6
6
|
#else
|
|
7
7
|
# include <io.h>
|
|
8
8
|
# define WIN32_LEAN_AND_MEAN
|
|
9
|
-
|
|
9
|
+
+/* ruby.h above already pulled in winsock2.h, so fd_set may be dimensioned
|
|
10
|
+
+ * already. Defining FD_SETSIZE unconditionally here would only move the bound
|
|
11
|
+
+ * used by EV_WIN_FD_SET, not the array it indexes. Take whatever is in effect
|
|
12
|
+
+ * and only supply a default when nothing has been decided yet. */
|
|
13
|
+
+# ifndef FD_SETSIZE
|
|
14
|
+
+# define FD_SETSIZE 1024
|
|
15
|
+
+# endif
|
|
10
16
|
# include <winsock2.h>
|
|
11
17
|
# include <windows.h>
|
|
12
18
|
# ifndef EV_SELECT_IS_WINSOCKET
|
|
13
19
|
diff --git a/ext/libev/ev_select.c b/ext/libev/ev_select.c
|
|
14
|
-
index
|
|
20
|
+
index ed1fc7a..eccff2b 100644
|
|
15
21
|
--- a/ext/libev/ev_select.c
|
|
16
22
|
+++ b/ext/libev/ev_select.c
|
|
17
|
-
@@ -67,6 +67,
|
|
23
|
+
@@ -67,6 +67,64 @@
|
|
18
24
|
|
|
19
25
|
#include <string.h>
|
|
20
26
|
|
|
@@ -58,6 +64,16 @@ index f38d6ca..7050778 100644
|
|
|
58
64
|
+#define EV_WIN_FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
|
|
59
65
|
+#define EV_WIN_FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set *)(set))
|
|
60
66
|
+#define EV_WIN_FD_COUNT(set) (((fd_set *)(set))->fd_count)
|
|
67
|
+
+
|
|
68
|
+
+/*
|
|
69
|
+
+fd_set is dimensioned by whatever FD_SETSIZE was in effect when winsock2.h was
|
|
70
|
+
+first pulled in, but EV_WIN_FD_SET and select_modify bound-check against the
|
|
71
|
+
+FD_SETSIZE visible here. The two are decided by separate paths, so if the bound
|
|
72
|
+
+ever exceeds the declared array we would write past the end of the allocation in
|
|
73
|
+
+select_init. Catch that at build time instead.
|
|
74
|
+
+*/
|
|
75
|
+
+typedef char coolio_fd_setsize_matches_fd_set[
|
|
76
|
+
+ (sizeof (((fd_set *)0)->fd_array) / sizeof (SOCKET) >= (size_t)FD_SETSIZE) ? 1 : -1];
|
|
61
77
|
+/* ######################################## */
|
|
62
78
|
+#else
|
|
63
79
|
+#define EV_WIN_FD_CLR FD_CLR
|
|
@@ -69,7 +85,7 @@ index f38d6ca..7050778 100644
|
|
|
69
85
|
static void
|
|
70
86
|
select_modify (EV_P_ int fd, int oev, int nev)
|
|
71
87
|
{
|
|
72
|
-
@@ -91,17 +
|
|
88
|
+
@@ -91,17 +149,17 @@ select_modify (EV_P_ int fd, int oev, int nev)
|
|
73
89
|
if ((oev ^ nev) & EV_READ)
|
|
74
90
|
#endif
|
|
75
91
|
if (nev & EV_READ)
|
|
@@ -91,7 +107,7 @@ index f38d6ca..7050778 100644
|
|
|
91
107
|
|
|
92
108
|
#else
|
|
93
109
|
|
|
94
|
-
@@ -197,8 +
|
|
110
|
+
@@ -197,8 +255,8 @@ select_poll (EV_P_ ev_tstamp timeout)
|
|
95
111
|
{
|
|
96
112
|
if (timeout)
|
|
97
113
|
{
|
|
@@ -102,7 +118,7 @@ index f38d6ca..7050778 100644
|
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
return;
|
|
105
|
-
@@ -230,10 +
|
|
121
|
+
@@ -230,10 +288,10 @@ select_poll (EV_P_ ev_tstamp timeout)
|
|
106
122
|
int handle = fd;
|
|
107
123
|
#endif
|
|
108
124
|
|
|
@@ -116,7 +132,7 @@ index f38d6ca..7050778 100644
|
|
|
116
132
|
#endif
|
|
117
133
|
|
|
118
134
|
if (expect_true (events))
|
|
119
|
-
@@ -
|
|
135
|
+
@@ -280,9 +338,9 @@ select_init (EV_P_ int flags)
|
|
120
136
|
backend_poll = select_poll;
|
|
121
137
|
|
|
122
138
|
#if EV_SELECT_USE_FD_SET
|
data/spec/async_watcher_spec.rb
CHANGED
|
@@ -30,7 +30,9 @@ describe Cool.io::AsyncWatcher, :env => :exclude_win do
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# ensure children are ready
|
|
33
|
-
|
|
33
|
+
# rd may be O_NONBLOCK on macOS Ruby 3.2+ (IO.pipe sets O_NONBLOCK); use read
|
|
34
|
+
# instead of sysread so EAGAIN on an empty pipe is retried transparently.
|
|
35
|
+
nr_fork.times { expect(rd.read(1)).to eq('.') }
|
|
34
36
|
|
|
35
37
|
# send our signals
|
|
36
38
|
nr_signal.times { aw.signal }
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
3
|
describe Cool.io::Loop do
|
|
4
|
+
# An IOWatcher that drains its pipe and then runs a user-supplied block,
|
|
5
|
+
# receiving itself as the argument.
|
|
4
6
|
class Victim < Cool.io::IOWatcher
|
|
5
|
-
def initialize(io)
|
|
6
|
-
super
|
|
7
|
+
def initialize(io, &on_readable)
|
|
8
|
+
super(io)
|
|
7
9
|
@io = io
|
|
10
|
+
@on_readable = on_readable
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
def on_readable
|
|
@@ -12,38 +15,57 @@ describe Cool.io::Loop do
|
|
|
12
15
|
@io.read_nonblock(1024)
|
|
13
16
|
rescue IO::WaitReadable, EOFError
|
|
14
17
|
end
|
|
18
|
+
@on_readable.call(self) if @on_readable
|
|
15
19
|
end
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
# https://github.com/socketry/cool.io/issues/87
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
#
|
|
24
|
+
# Several watchers have an event pending in the same loop iteration. When the
|
|
25
|
+
# first one dispatched detaches the others, the loop must skip their now-stale
|
|
26
|
+
# pending events instead of dispatching them to a detached watcher. Before the
|
|
27
|
+
# fix that raised "TypeError: wrong argument type nil (expected Coolio::Loop)"
|
|
28
|
+
# and could crash the VM.
|
|
29
|
+
#
|
|
30
|
+
# This is exercised deterministically within a single thread (a preceding
|
|
31
|
+
# callback detaching another watcher in the same loop cycle). The original
|
|
32
|
+
# reproduction detached from a separate thread while the loop was polling,
|
|
33
|
+
# which is an unsupported concurrent mutation of libev (not thread-safe) and
|
|
34
|
+
# crashed intermittently on macOS.
|
|
35
|
+
it "does not raise when a watcher with a pending event is detached during dispatch" do
|
|
22
36
|
iterations = 200
|
|
23
37
|
|
|
24
38
|
expect {
|
|
25
39
|
iterations.times do
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
coolio_loop = Cool.io::Loop.new
|
|
41
|
+
pipes = []
|
|
42
|
+
watchers = []
|
|
43
|
+
|
|
44
|
+
5.times do
|
|
45
|
+
r, w = IO.pipe
|
|
46
|
+
pipes << [r, w]
|
|
47
|
+
|
|
48
|
+
watcher = Victim.new(r) do |fired|
|
|
49
|
+
# Detach every other watcher whose event is already queued for this
|
|
50
|
+
# same loop iteration.
|
|
51
|
+
watchers.each do |other|
|
|
52
|
+
other.detach if !other.equal?(fired) && other.attached?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
watcher.attach(coolio_loop)
|
|
56
|
+
watchers << watcher
|
|
57
|
+
|
|
58
|
+
w.write("dummy\n") # make the read end readable so an event is pending
|
|
38
59
|
end
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
coolio_loop.run_once
|
|
41
62
|
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
# Only the first dispatched watcher runs; it detaches the other four,
|
|
64
|
+
# whose pending events are then skipped.
|
|
65
|
+
expect(watchers.count(&:attached?)).to eq(1)
|
|
44
66
|
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
watchers.each { |watcher| watcher.detach if watcher.attached? }
|
|
68
|
+
pipes.each { |r, w| r.close; w.close }
|
|
47
69
|
end
|
|
48
70
|
}.not_to raise_error
|
|
49
71
|
end
|
data/spec/dns_spec.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'tempfile'
|
|
2
3
|
|
|
3
4
|
VALID_DOMAIN = "google.com"
|
|
4
5
|
INVALID_DOMAIN = "gibidigibigididibitidibigitibidigitidididi.com"
|
|
@@ -55,4 +56,189 @@ describe "DNS" do
|
|
|
55
56
|
expect( Coolio::DNSResolver.hosts("localhost", file.path)).to eq @preferred_localhost_address
|
|
56
57
|
end
|
|
57
58
|
end
|
|
59
|
+
|
|
60
|
+
describe "IPv6 nameserver filtering" do
|
|
61
|
+
it "ignores IPv6 nameservers provided in arguments" do
|
|
62
|
+
resolver = Coolio::DNSResolver.new("example.com", "8.8.8.8", "2001:4860:4860::8888", "1.1.1.1")
|
|
63
|
+
|
|
64
|
+
nameservers = resolver.instance_variable_get(:@nameservers)
|
|
65
|
+
expect(nameservers).to eq(["8.8.8.8", "1.1.1.1"])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "falls back to default IPv4 config if only IPv6 addresses are provided" do
|
|
69
|
+
allow(Resolv::DNS::Config).to receive(:default_config_hash).and_return({
|
|
70
|
+
nameserver: ["8.8.4.4", "2001:4860:4860::8844"]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
resolver = Coolio::DNSResolver.new("example.com", "2001:4860:4860::8888")
|
|
74
|
+
|
|
75
|
+
nameservers = resolver.instance_variable_get(:@nameservers)
|
|
76
|
+
expect(nameservers).to eq(["8.8.4.4"])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "nameserver normalization" do
|
|
81
|
+
let(:localhost_address) do
|
|
82
|
+
Addrinfo.getaddrinfo("localhost", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM).first.ip_address
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "keeps the nameserver list as given" do
|
|
86
|
+
resolver = Coolio::DNSResolver.new("example.com", "localhost")
|
|
87
|
+
|
|
88
|
+
expect(resolver.instance_variable_get(:@nameservers)).to eq(["localhost"])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "queries the numeric address of a nameserver given as a hostname" do
|
|
92
|
+
resolver = Coolio::DNSResolver.new("example.com", "localhost")
|
|
93
|
+
resolver.__send__(:send_request)
|
|
94
|
+
|
|
95
|
+
expect(resolver.instance_variable_get(:@queried_addresses)).to eq([localhost_address])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "accepts responses from a nameserver which was given as a hostname" do
|
|
99
|
+
resolver = Coolio::DNSResolver.new("example.com", "localhost")
|
|
100
|
+
resolver.__send__(:send_request)
|
|
101
|
+
response = dns_response_for(resolver)
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
resolver.__send__(:solicited_response?, response, ["AF_INET", 53, localhost_address, localhost_address])
|
|
105
|
+
).to be true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "looks a nameserver up once and reuses the result on retries" do
|
|
109
|
+
resolved = Addrinfo.getaddrinfo("127.0.0.1", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM)
|
|
110
|
+
resolver = Coolio::DNSResolver.new("example.com", "127.0.0.1")
|
|
111
|
+
|
|
112
|
+
expect(Addrinfo).to receive(:getaddrinfo).once.and_return(resolved)
|
|
113
|
+
|
|
114
|
+
3.times { resolver.__send__(:send_request) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "does not reject an unresolvable nameserver at construction" do
|
|
118
|
+
allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError, "getaddrinfo: Name or service not known")
|
|
119
|
+
|
|
120
|
+
expect do
|
|
121
|
+
Coolio::DNSResolver.new("example.com", "no-such-nameserver.invalid")
|
|
122
|
+
end.to_not raise_error
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "surfaces an unresolvable nameserver as a SocketError from the request" do
|
|
126
|
+
allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError, "getaddrinfo: Name or service not known")
|
|
127
|
+
resolver = Coolio::DNSResolver.new("example.com", "no-such-nameserver.invalid")
|
|
128
|
+
|
|
129
|
+
expect { resolver.attach(@loop) }.to raise_error(SocketError)
|
|
130
|
+
expect(@loop.watchers).to be_empty
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "recovers when a nameserver is only transiently unresolvable" do
|
|
134
|
+
resolved = Addrinfo.getaddrinfo("127.0.0.1", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM)
|
|
135
|
+
resolver = Coolio::DNSResolver.new("example.com", "ns.example.test")
|
|
136
|
+
|
|
137
|
+
attempts = 0
|
|
138
|
+
allow(Addrinfo).to receive(:getaddrinfo) do
|
|
139
|
+
attempts += 1
|
|
140
|
+
raise SocketError, "getaddrinfo: Name or service not known" if attempts == 1
|
|
141
|
+
|
|
142
|
+
resolved
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
expect { resolver.__send__(:send_request) }.to raise_error(SocketError)
|
|
146
|
+
expect { resolver.__send__(:send_request) }.to_not raise_error
|
|
147
|
+
expect(resolver.instance_variable_get(:@queried_addresses)).to eq(["127.0.0.1"])
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe "response validation" do
|
|
152
|
+
let(:nameserver) { "127.0.0.1" }
|
|
153
|
+
let(:sender) { ["AF_INET", 53, nameserver, nameserver] }
|
|
154
|
+
let(:resolver) do
|
|
155
|
+
Coolio::DNSResolver.new("example.com", nameserver).tap { |r| r.__send__(:send_request) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "uses an unpredictable transaction ID for each query" do
|
|
159
|
+
ids = 10.times.map do
|
|
160
|
+
request_id_of(Coolio::DNSResolver.new("example.com", nameserver))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
expect(ids.uniq.size).to be > 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "accepts a response carrying our transaction ID from the queried nameserver" do
|
|
167
|
+
expect(
|
|
168
|
+
resolver.__send__(:solicited_response?, dns_response_for(resolver), sender)
|
|
169
|
+
).to be true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "rejects a response carrying a different transaction ID" do
|
|
173
|
+
forged = dns_response_for(resolver, id: (request_id_of(resolver) + 1) % 65536)
|
|
174
|
+
|
|
175
|
+
expect(resolver.__send__(:solicited_response?, forged, sender)).to be false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "rejects a response from a source address we did not query" do
|
|
179
|
+
response = dns_response_for(resolver)
|
|
180
|
+
|
|
181
|
+
expect(
|
|
182
|
+
resolver.__send__(:solicited_response?, response, ["AF_INET", 53, "10.11.12.13", "10.11.12.13"])
|
|
183
|
+
).to be false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "rejects a response arriving before the request was sent" do
|
|
187
|
+
unsent = Coolio::DNSResolver.new("example.com", nameserver)
|
|
188
|
+
|
|
189
|
+
expect(unsent.__send__(:solicited_response?, dns_response_for(unsent), sender)).to be false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "rejects a response from a source port other than the DNS port" do
|
|
193
|
+
response = dns_response_for(resolver)
|
|
194
|
+
|
|
195
|
+
expect(
|
|
196
|
+
resolver.__send__(:solicited_response?, response, ["AF_INET", 4444, nameserver, nameserver])
|
|
197
|
+
).to be false
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "rejects a truncated datagram" do
|
|
201
|
+
expect(resolver.__send__(:solicited_response?, "\0\0", sender)).to be false
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "resolves from a response sent by the queried nameserver" do
|
|
205
|
+
response = dns_response_for(resolver, address: "1.2.3.4")
|
|
206
|
+
allow(resolver.instance_variable_get(:@socket)).to receive(:recvfrom_nonblock).and_return([response, sender])
|
|
207
|
+
|
|
208
|
+
expect(resolver).to receive(:on_success).with("1.2.3.4")
|
|
209
|
+
expect(resolver).to receive(:detach)
|
|
210
|
+
|
|
211
|
+
resolver.__send__(:on_readable)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it "ignores a spoofed response instead of resolving or failing it" do
|
|
215
|
+
forged = dns_response_for(resolver, address: "6.6.6.6")
|
|
216
|
+
allow(resolver.instance_variable_get(:@socket)).to receive(:recvfrom_nonblock)
|
|
217
|
+
.and_return([forged, ["AF_INET", 53, "10.11.12.13", "10.11.12.13"]])
|
|
218
|
+
|
|
219
|
+
expect(resolver).to_not receive(:on_success)
|
|
220
|
+
expect(resolver).to_not receive(:on_failure)
|
|
221
|
+
expect(resolver).to_not receive(:detach)
|
|
222
|
+
|
|
223
|
+
resolver.__send__(:on_readable)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def request_id_of(resolver)
|
|
228
|
+
resolver.__send__(:request_message)[0..1].unpack('n').first
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# A response to the resolver's own query: header plus the echoed question,
|
|
232
|
+
# and an A record when an address is given.
|
|
233
|
+
def dns_response_for(resolver, id: request_id_of(resolver), address: nil)
|
|
234
|
+
question = resolver.instance_variable_get(:@question)
|
|
235
|
+
answer = if address
|
|
236
|
+
# Compressed name pointer, type A, class IN, TTL, RDLENGTH, RDATA
|
|
237
|
+
[0xc00c, 1, 1, 60, 4].pack('nnnNn') + address.split('.').map(&:to_i).pack('CCCC')
|
|
238
|
+
else
|
|
239
|
+
""
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
[id, 0x81, 0x80, 1, answer.empty? ? 0 : 1, 0, 0].pack('nCCnnnn') + question + answer
|
|
243
|
+
end
|
|
58
244
|
end
|
data/spec/iobuffer_spec.rb
CHANGED
|
@@ -31,6 +31,18 @@ describe Cool.io::Buffer do
|
|
|
31
31
|
expect(buffer << "baz").to eq "baz"
|
|
32
32
|
expect(buffer.read 3).to eq "arb"
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
it "raises ArgumentError for a length below one" do
|
|
36
|
+
buffer << "foo"
|
|
37
|
+
expect { buffer.read 0 }.to raise_error ArgumentError
|
|
38
|
+
expect { buffer.read(-1) }.to raise_error ArgumentError
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "clamps a length which does not fit in a C int to the buffer size" do
|
|
42
|
+
buffer << "foobar"
|
|
43
|
+
expect(buffer.read 2**31).to eq "foobar"
|
|
44
|
+
expect(buffer.size).to eq 0
|
|
45
|
+
end
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
describe "provides methods for performing non-blocking I/O" do
|
|
@@ -142,6 +154,25 @@ describe Cool.io::Buffer do
|
|
|
142
154
|
expect(data).to eq "foo\nbarbaz"
|
|
143
155
|
expect(buffer.to_str).to eq ""
|
|
144
156
|
end
|
|
157
|
+
|
|
158
|
+
it "raises TypeError instead of crashing when data is not a String" do
|
|
159
|
+
buffer << "hello world"
|
|
160
|
+
expect { buffer.read_frame 12345, " ".ord }.to raise_error(TypeError)
|
|
161
|
+
expect { buffer.read_frame nil, " ".ord }.to raise_error(TypeError)
|
|
162
|
+
expect { buffer.read_frame [], " ".ord }.to raise_error(TypeError)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "raises FrozenError when data is a frozen String" do
|
|
166
|
+
buffer << "hello world"
|
|
167
|
+
expect { buffer.read_frame "frozen".freeze, " ".ord }.to raise_error(FrozenError)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "coerces objects responding to #to_str" do
|
|
171
|
+
buffer << "foo\nbar"
|
|
172
|
+
convertible = Object.new
|
|
173
|
+
def convertible.to_str; +""; end
|
|
174
|
+
expect(buffer.read_frame convertible, "\n".ord).to eq true
|
|
175
|
+
end
|
|
145
176
|
end
|
|
146
177
|
|
|
147
178
|
end
|
data/spec/stat_watcher_spec.rb
CHANGED
|
@@ -18,12 +18,31 @@ class MyStatWatcher < Cool.io::StatWatcher
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def run_with_file_change(path)
|
|
21
|
+
def run_with_file_change(path, compact: false)
|
|
22
22
|
reactor = Cool.io::Loop.new
|
|
23
23
|
|
|
24
24
|
sw = MyStatWatcher.new(path)
|
|
25
25
|
sw.attach(reactor)
|
|
26
26
|
|
|
27
|
+
# libev retains the path pointer passed to ev_stat_init() for the lifetime of
|
|
28
|
+
# the watcher. If the watcher held RSTRING_PTR(@path) directly, a compaction
|
|
29
|
+
# that relocates the @path String would leave libev dereferencing a stale
|
|
30
|
+
# address on every stat. Force a maximal relocation here so the watcher keeps
|
|
31
|
+
# operating against a path buffer it owns rather than Ruby-managed memory.
|
|
32
|
+
if compact
|
|
33
|
+
if GC.respond_to?(:verify_compaction_references)
|
|
34
|
+
# verify_compaction_references moves every movable object to a fresh slot.
|
|
35
|
+
# Its keyword arguments have varied across Ruby versions, so fall back.
|
|
36
|
+
begin
|
|
37
|
+
GC.verify_compaction_references(expand_heap: true, toplevel: true)
|
|
38
|
+
rescue ArgumentError
|
|
39
|
+
GC.verify_compaction_references(expand_heap: true)
|
|
40
|
+
end
|
|
41
|
+
elsif GC.respond_to?(:compact)
|
|
42
|
+
GC.compact
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
tw = Cool.io::TimerWatcher.new(INTERVAL, true)
|
|
28
47
|
tw.on_timer do
|
|
29
48
|
reactor.stop if sw.accessed
|
|
@@ -69,6 +88,17 @@ describe Cool.io::StatWatcher do
|
|
|
69
88
|
expect(watcher.previous.ino).to eq(watcher.current.ino)
|
|
70
89
|
end
|
|
71
90
|
|
|
91
|
+
it "keeps firing on_change after GC compaction relocates objects" do
|
|
92
|
+
skip "GC compaction not available" unless GC.respond_to?(:compact)
|
|
93
|
+
|
|
94
|
+
watcher = run_with_file_change(TEMP_FILE_PATH, compact: true)
|
|
95
|
+
expect(watcher.accessed).to eq(true)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "raises ArgumentError when the path contains a null byte" do
|
|
99
|
+
expect { MyStatWatcher.new("foo\0bar") }.to raise_error(ArgumentError)
|
|
100
|
+
end
|
|
101
|
+
|
|
72
102
|
it "should raise when the handler does not take 2 parameters" do
|
|
73
103
|
class MyStatWatcher < Cool.io::StatWatcher
|
|
74
104
|
remove_method :on_change
|
data/spec/tcp_server_spec.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require File.expand_path('../spec_helper', __FILE__)
|
|
2
|
+
require 'timeout'
|
|
2
3
|
|
|
3
4
|
TIMEOUT = 0.010
|
|
4
5
|
HOST = '127.0.0.1'
|
|
@@ -39,13 +40,31 @@ def on_message(data)
|
|
|
39
40
|
@data = data
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
# The reactor can be an order of magnitude slower under valgrind, so sleeping a
|
|
44
|
+
# fixed interval and assuming the event was processed by then makes these specs
|
|
45
|
+
# fail intermittently. Wait for the result instead. The timeout is generous
|
|
46
|
+
# enough that a slow machine still passes, and a genuine hang raises
|
|
47
|
+
# Timeout::Error here instead of blocking the suite forever.
|
|
48
|
+
WAIT_TIMEOUT = 5.0
|
|
49
|
+
|
|
50
|
+
def wait_until(timeout = WAIT_TIMEOUT)
|
|
51
|
+
Timeout.timeout(timeout) do
|
|
52
|
+
sleep 0.001 until yield
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
42
56
|
def test_run(data = nil)
|
|
57
|
+
@data = ""
|
|
43
58
|
reactor = Coolio::Loop.new
|
|
44
59
|
server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
|
|
45
60
|
reactor.attach(server)
|
|
46
61
|
thread = Thread.new { reactor.run }
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
if data
|
|
63
|
+
send_data(data)
|
|
64
|
+
wait_until { @data == data }
|
|
65
|
+
else
|
|
66
|
+
sleep TIMEOUT
|
|
67
|
+
end
|
|
49
68
|
reactor.stop
|
|
50
69
|
server.detach
|
|
51
70
|
send_data('') # to leave from blocking loop
|
|
@@ -86,6 +105,7 @@ ensure
|
|
|
86
105
|
end
|
|
87
106
|
|
|
88
107
|
def test_run_timeout(data = nil, timeout = TIMEOUT)
|
|
108
|
+
@data = ""
|
|
89
109
|
reactor = Coolio::Loop.new
|
|
90
110
|
server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
|
|
91
111
|
reactor.attach(server)
|
|
@@ -95,8 +115,12 @@ def test_run_timeout(data = nil, timeout = TIMEOUT)
|
|
|
95
115
|
reactor.run_once(timeout)
|
|
96
116
|
end
|
|
97
117
|
end
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
if data
|
|
119
|
+
send_data(data)
|
|
120
|
+
wait_until { @data == data }
|
|
121
|
+
else
|
|
122
|
+
sleep timeout
|
|
123
|
+
end
|
|
100
124
|
server.detach
|
|
101
125
|
running = false # another send is not required
|
|
102
126
|
thread.join
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cool.io
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tony Arcieri
|
|
@@ -77,6 +77,7 @@ extensions:
|
|
|
77
77
|
extra_rdoc_files: []
|
|
78
78
|
files:
|
|
79
79
|
- ".github/workflows/test.yaml"
|
|
80
|
+
- ".github/workflows/valgrind.yaml"
|
|
80
81
|
- ".gitignore"
|
|
81
82
|
- ".rspec"
|
|
82
83
|
- CHANGES.md
|
|
@@ -169,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
169
170
|
- !ruby/object:Gem::Version
|
|
170
171
|
version: '0'
|
|
171
172
|
requirements: []
|
|
172
|
-
rubygems_version: 4.0.
|
|
173
|
+
rubygems_version: 4.0.16
|
|
173
174
|
specification_version: 4
|
|
174
175
|
summary: A cool framework for doing high performance I/O in Ruby
|
|
175
176
|
test_files:
|