iodine 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +3 -2
- data/CHANGELOG.md +8 -0
- data/SPEC-Websocket-Draft.md +3 -3
- data/ext/iodine/iodine_http.c +8 -8
- data/ext/iodine/iodine_websocket.c +20 -6
- data/ext/iodine/libasync.c +18 -13
- data/ext/iodine/libserver.c +3 -3
- data/ext/iodine/rb-call.c +40 -56
- data/ext/iodine/rb-call.c_old +127 -0
- data/ext/iodine/rb-registry.c +47 -54
- data/ext/iodine/rb-registry_old.c_old +213 -0
- data/ext/iodine/websockets.c +10 -4
- data/iodine.gemspec +1 -1
- data/lib/iodine/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 765b6fa27478c971c926fe4010ebde8c95d970a3
|
4
|
+
data.tar.gz: 740963e010629bb812d6de4e4167375bed0b0bf3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ebd38a0c3442d787af596ca73a26481673697727e891026355722a33e897d3a631c4569669bfb041c61653df0c2248a3b51a20780002c9603e36fa538ea67e0
|
7
|
+
data.tar.gz: 2e31973b2bc6ec931001c8cdac8a09bd3e3d3ac6fb74e8c7749d80f0daf4419057f95db0151c3205eb441c4a3ac07c3938ea5e9a52c98944ea1b39c8a86328fc
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -8,6 +8,14 @@ Please notice that this change log contains changes for upcoming releases as wel
|
|
8
8
|
|
9
9
|
***
|
10
10
|
|
11
|
+
Change log v.0.2.5
|
12
|
+
|
13
|
+
**Fix:**: fix for issue #9 (credit to Jack Christensen for exposing the issue) caused by an unlocked critical section's "window of opportunity" that allowed asynchronous Websocket `each` blocks to run during the tail of the Websocket handshake (while the `on_open` callback was running in parallel).
|
14
|
+
|
15
|
+
**Minor Fix**: Fix Iodine::Rack's startup message's `fprint` call to fit correct argument sizes (Linux warnings).
|
16
|
+
|
17
|
+
***
|
18
|
+
|
11
19
|
Change log v.0.2.4
|
12
20
|
|
13
21
|
**Minor Fix**: Patched Iodine against Apple's broken `getrlimit` on macOS. This allows correct auto-setting of open file limits for the socket layer.
|
data/SPEC-Websocket-Draft.md
CHANGED
@@ -60,11 +60,11 @@ Server settings **MAY** (not required) be provided to allow for customization an
|
|
60
60
|
|
61
61
|
## Upgrading
|
62
62
|
|
63
|
-
* **Server**: When an upgrade request is received, the server will set the `env['upgrade.
|
63
|
+
* **Server**: When an upgrade request is received, the server will set the `env['upgrade.websocket?']` flag to `true`, indicating that: 1. this specific request is upgradable; and 2. this server supports specification.
|
64
64
|
|
65
|
-
* **Client**: When a client decides to upgrade a request, they will place a Websocket Callback Object (either a class or an instance) in the `env['upgrade.
|
65
|
+
* **Client**: When a client decides to upgrade a request, they will place a Websocket Callback Object (either a class or an instance) in the `env['upgrade.websocket']` Hash key.
|
66
66
|
|
67
|
-
* **Server**: The server will review the `env` Hash *before* sending the response. If the `env['upgrade.
|
67
|
+
* **Server**: The server will review the `env` Hash *before* sending the response. If the `env['upgrade.websocket']` was set, the server will perform the upgrade.
|
68
68
|
|
69
69
|
* **Server**: The server will send the correct response status and headers, as will as any headers present in the response. The server will also perform any required housekeeping, such as closing the response body, if exists.
|
70
70
|
|
data/ext/iodine/iodine_http.c
CHANGED
@@ -553,22 +553,22 @@ int iodine_http_review(void) {
|
|
553
553
|
if (public_folder)
|
554
554
|
fprintf(stderr, "Starting up Iodine Http Server:\n"
|
555
555
|
" * Ruby v.%s\n * Iodine v.%s \n"
|
556
|
-
" * %
|
557
|
-
" * %
|
556
|
+
" * %lu processes X %lu thread%s\n"
|
557
|
+
" * %lu max concurrent connections / open files\n"
|
558
558
|
" * Serving static files from:\n"
|
559
559
|
" %s\n\n",
|
560
560
|
StringValueCStr(ruby_version), StringValueCStr(iodine_version),
|
561
|
-
processes, threads, (threads > 1 ? "s" : ""),
|
562
|
-
public_folder);
|
561
|
+
(size_t)processes, (size_t)threads, (threads > 1 ? "s" : ""),
|
562
|
+
(size_t)sock_max_capacity(), public_folder);
|
563
563
|
else
|
564
564
|
fprintf(stderr, "Starting up Iodine Http Server:\n"
|
565
565
|
" * Ruby v.%s\n * Iodine v.%s \n"
|
566
|
-
" * %
|
567
|
-
" * %
|
566
|
+
" * %lu processes X %lu thread%s\n"
|
567
|
+
" * %lu max concurrent connections / open files\n"
|
568
568
|
"\n",
|
569
569
|
StringValueCStr(ruby_version), StringValueCStr(iodine_version),
|
570
|
-
processes, threads, (threads > 1 ? "s" : ""),
|
571
|
-
sock_max_capacity());
|
570
|
+
(size_t)processes, (size_t)threads, (threads > 1 ? "s" : ""),
|
571
|
+
(size_t)sock_max_capacity());
|
572
572
|
|
573
573
|
// listen
|
574
574
|
return http1_listen(port, address, .on_request = on_rack_request,
|
@@ -1,10 +1,10 @@
|
|
1
|
+
#include "iodine_websocket.h"
|
1
2
|
#include "iodine_core.h"
|
2
3
|
#include "iodine_http.h"
|
3
|
-
#include "iodine_websocket.h"
|
4
4
|
#include "rb-call.h"
|
5
5
|
#include "rb-registry.h"
|
6
|
-
#include <ruby/io.h>
|
7
6
|
#include <arpa/inet.h>
|
7
|
+
#include <ruby/io.h>
|
8
8
|
|
9
9
|
/* *****************************************************************************
|
10
10
|
Core helpers and data
|
@@ -31,6 +31,8 @@ inline static intptr_t get_uuid(VALUE obj) {
|
|
31
31
|
|
32
32
|
inline static ws_s *get_ws(VALUE obj) {
|
33
33
|
VALUE i = rb_ivar_get(obj, ws_var_id);
|
34
|
+
if (i == Qnil)
|
35
|
+
return NULL;
|
34
36
|
return (ws_s *)FIX2ULONG(i);
|
35
37
|
}
|
36
38
|
|
@@ -126,8 +128,14 @@ static VALUE iodine_ws_close(VALUE self) {
|
|
126
128
|
* global `write` buffer is full, `write` will block until a buffer "packet"
|
127
129
|
* becomes available and can be assigned to the socket. */
|
128
130
|
static VALUE iodine_ws_write(VALUE self, VALUE data) {
|
131
|
+
Check_Type(data, T_STRING);
|
129
132
|
ws_s *ws = get_ws(self);
|
130
|
-
if ((
|
133
|
+
// if ((void *)ws == (void *)0x04 || (void *)data == (void *)0x04 ||
|
134
|
+
// RSTRING_PTR(data) == (void *)0x04)
|
135
|
+
// fprintf(stderr, "iodine_ws_write: self = %p ; data = %p\n"
|
136
|
+
// "\t\tString ptr: %p, String length: %lu\n",
|
137
|
+
// (void *)ws, (void *)data, RSTRING_PTR(data), RSTRING_LEN(data));
|
138
|
+
if (!ws || ((protocol_s *)ws)->service != WEBSOCKET_ID_STR)
|
131
139
|
return Qfalse;
|
132
140
|
websocket_write(ws, RSTRING_PTR(data), RSTRING_LEN(data),
|
133
141
|
rb_enc_get(data) == UTF8Encoding);
|
@@ -252,6 +260,11 @@ i.e.:
|
|
252
260
|
msg = data.dup; # data will be overwritten once the function exists.
|
253
261
|
each {|ws| ws.write msg}
|
254
262
|
end
|
263
|
+
|
264
|
+
|
265
|
+
The block of code will be executed asynchronously, to avoid having two blocks
|
266
|
+
of code running at the same time and minimizing race conditions when using
|
267
|
+
multilple threads.
|
255
268
|
*/
|
256
269
|
static VALUE iodine_ws_each(VALUE self) {
|
257
270
|
// requires a block to be passed
|
@@ -268,7 +281,8 @@ static VALUE iodine_ws_each(VALUE self) {
|
|
268
281
|
/**
|
269
282
|
Runs the required block for each dynamic protocol connection.
|
270
283
|
|
271
|
-
Tasks will be performed within each connections lock, so no
|
284
|
+
Tasks will be performed asynchronously, within each connections lock, so no
|
285
|
+
connection will have
|
272
286
|
more then one task being performed at the same time (similar to {#defer}).
|
273
287
|
|
274
288
|
Also, unlike {Iodine.run}, the block will **not** be called unless the
|
@@ -425,8 +439,8 @@ static VALUE empty_func(VALUE self) { return Qnil; }
|
|
425
439
|
// initialize the class and the whole of the Iodine/http library
|
426
440
|
void Init_iodine_websocket(void) {
|
427
441
|
// get IDs and data that's used often
|
428
|
-
ws_var_id = rb_intern("
|
429
|
-
dup_func_id = rb_intern("dup");
|
442
|
+
ws_var_id = rb_intern("iodine_ws_ptr"); // when upgrading
|
443
|
+
dup_func_id = rb_intern("dup"); // when upgrading
|
430
444
|
|
431
445
|
// the Ruby websockets protocol class.
|
432
446
|
rWebsocket = rb_define_module_under(Iodine, "Websocket");
|
data/ext/iodine/libasync.c
CHANGED
@@ -11,17 +11,17 @@ Feel free to copy, use and enjoy according to the license provided.
|
|
11
11
|
|
12
12
|
#include "libasync.h"
|
13
13
|
|
14
|
-
#include <stdlib.h>
|
15
|
-
#include <stdio.h>
|
16
14
|
#include <errno.h>
|
17
|
-
#include <signal.h>
|
18
|
-
#include <unistd.h>
|
19
15
|
#include <execinfo.h>
|
20
|
-
#include <pthread.h>
|
21
16
|
#include <fcntl.h>
|
17
|
+
#include <pthread.h>
|
22
18
|
#include <sched.h>
|
23
|
-
#include <
|
19
|
+
#include <signal.h>
|
20
|
+
#include <stdio.h>
|
21
|
+
#include <stdlib.h>
|
24
22
|
#include <string.h>
|
23
|
+
#include <sys/mman.h>
|
24
|
+
#include <unistd.h>
|
25
25
|
|
26
26
|
/* *****************************************************************************
|
27
27
|
Performance options.
|
@@ -43,7 +43,7 @@ Performance options.
|
|
43
43
|
|
44
44
|
/* Sentinal thread to respawn crashed threads - limited crash resistance. */
|
45
45
|
#ifndef ASYNC_USE_SENTINEL
|
46
|
-
#define ASYNC_USE_SENTINEL
|
46
|
+
#define ASYNC_USE_SENTINEL 1
|
47
47
|
#endif
|
48
48
|
|
49
49
|
/* *****************************************************************************
|
@@ -457,11 +457,13 @@ Test
|
|
457
457
|
|
458
458
|
#define ASYNC_SPEED_TEST_THREAD_COUNT 120
|
459
459
|
|
460
|
-
static
|
460
|
+
static spn_lock_i i_lock = SPN_LOCK_INIT;
|
461
|
+
static size_t i_count = 0;
|
461
462
|
|
462
463
|
static void sample_task(void *_) {
|
463
|
-
|
464
|
-
|
464
|
+
spn_lock(&i_lock);
|
465
|
+
i_count++;
|
466
|
+
spn_unlock(&i_lock);
|
465
467
|
}
|
466
468
|
|
467
469
|
static void sched_sample_task(void *_) {
|
@@ -471,8 +473,9 @@ static void sched_sample_task(void *_) {
|
|
471
473
|
}
|
472
474
|
|
473
475
|
static void text_task_text(void *_) {
|
474
|
-
|
476
|
+
spn_lock(&i_lock);
|
475
477
|
fprintf(stderr, "this text should print before async_finish returns\n");
|
478
|
+
spn_unlock(&i_lock);
|
476
479
|
}
|
477
480
|
|
478
481
|
static void text_task(void *_) {
|
@@ -491,7 +494,9 @@ static void evil_task(void *_) {
|
|
491
494
|
#endif
|
492
495
|
|
493
496
|
void async_test_library_speed(void) {
|
494
|
-
|
497
|
+
spn_lock(&i_lock);
|
498
|
+
i_count = 0;
|
499
|
+
spn_unlock(&i_lock);
|
495
500
|
time_t start, end;
|
496
501
|
fprintf(stderr, "Starting Async testing\n");
|
497
502
|
if (async_start(ASYNC_SPEED_TEST_THREAD_COUNT) == 0) {
|
@@ -506,7 +511,7 @@ void async_test_library_speed(void) {
|
|
506
511
|
async_finish();
|
507
512
|
end = clock();
|
508
513
|
fprintf(stderr, "Async performance test %lu cycles with i_count = %lu\n",
|
509
|
-
end - start,
|
514
|
+
end - start, i_count);
|
510
515
|
} else {
|
511
516
|
fprintf(stderr, "Async test couldn't be initialized\n");
|
512
517
|
exit(-1);
|
data/ext/iodine/libserver.c
CHANGED
@@ -8,12 +8,12 @@ Feel free to copy, use and enjoy according to the license provided.
|
|
8
8
|
#define _GNU_SOURCE
|
9
9
|
#endif
|
10
10
|
#include "libserver.h"
|
11
|
-
#include <
|
12
|
-
#include <signal.h>
|
11
|
+
#include <errno.h>
|
13
12
|
#include <pthread.h>
|
13
|
+
#include <signal.h>
|
14
|
+
#include <string.h>
|
14
15
|
#include <sys/mman.h>
|
15
16
|
#include <sys/wait.h>
|
16
|
-
#include <errno.h>
|
17
17
|
|
18
18
|
/* *****************************************************************************
|
19
19
|
Connection Data
|
data/ext/iodine/rb-call.c
CHANGED
@@ -1,18 +1,15 @@
|
|
1
1
|
#include "rb-call.h"
|
2
|
+
#include <pthread.h>
|
2
3
|
#include <ruby.h>
|
3
4
|
#include <ruby/thread.h>
|
4
|
-
|
5
|
+
|
6
|
+
#if __STDC_VERSION__ < 201112L || __STDC_NO_THREADS__
|
7
|
+
#define _Thread_local __thread
|
8
|
+
#endif
|
5
9
|
|
6
10
|
///////////////
|
7
11
|
// this is a simple helper that calls Ruby methods on Ruby objects while within
|
8
12
|
// a non-GVL ruby thread zone.
|
9
|
-
|
10
|
-
// a structure for Ruby API calls
|
11
|
-
struct RubySimpleCall {
|
12
|
-
VALUE obj;
|
13
|
-
VALUE returned;
|
14
|
-
ID method;
|
15
|
-
};
|
16
13
|
struct RubyArgCall {
|
17
14
|
VALUE obj;
|
18
15
|
int argc;
|
@@ -21,25 +18,10 @@ struct RubyArgCall {
|
|
21
18
|
ID method;
|
22
19
|
};
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
// a thread specific global variable that lets us know if we're in the GVL
|
29
|
-
static _Thread_local char in_gvl = 0;
|
30
|
-
static char check_in_gvl(void) { return in_gvl; }
|
31
|
-
|
32
|
-
////////////////////////////////////////////////////////////////////////////
|
33
|
-
// Calling C functions.
|
34
|
-
static void *call_c(void *(*func)(void *), void *arg) {
|
35
|
-
if (in_gvl) {
|
36
|
-
return func(arg);
|
37
|
-
}
|
38
|
-
void *ret;
|
39
|
-
in_gvl = 1;
|
40
|
-
ret = rb_thread_call_with_gvl(func, arg);
|
41
|
-
in_gvl = 0;
|
42
|
-
return ret;
|
21
|
+
// running the actual method call
|
22
|
+
static VALUE run_ruby_method_unsafe(VALUE _tsk) {
|
23
|
+
struct RubyArgCall *task = (void *)_tsk;
|
24
|
+
return rb_funcall2(task->obj, task->method, task->argc, task->argv);
|
43
25
|
}
|
44
26
|
|
45
27
|
////////////////////////////////////////////////////////////////////////////
|
@@ -67,18 +49,9 @@ static void *handle_exception(void *_) {
|
|
67
49
|
return (void *)Qnil;
|
68
50
|
}
|
69
51
|
|
70
|
-
////////////////////////////////////////////////////////////////////////////
|
71
|
-
// A simple (and a bit lighter) design for when there's no need for arguments.
|
72
|
-
|
73
|
-
// running the actual method call
|
74
|
-
static VALUE run_ruby_method_unsafe(VALUE _tsk) {
|
75
|
-
struct RubySimpleCall *task = (void *)_tsk;
|
76
|
-
return rb_funcall2(task->obj, task->method, 0, NULL);
|
77
|
-
}
|
78
|
-
|
79
52
|
// GVL gateway
|
80
53
|
static void *run_ruby_method_within_gvl(void *_tsk) {
|
81
|
-
struct
|
54
|
+
struct RubyArgCall *task = _tsk;
|
82
55
|
int state = 0;
|
83
56
|
task->returned = rb_protect(run_ruby_method_unsafe, (VALUE)(task), &state);
|
84
57
|
if (state)
|
@@ -86,9 +59,32 @@ static void *run_ruby_method_within_gvl(void *_tsk) {
|
|
86
59
|
return task;
|
87
60
|
}
|
88
61
|
|
62
|
+
////////////////////////////////////////////////////////////////////////////
|
63
|
+
// GVL state.
|
64
|
+
|
65
|
+
// a thread specific global variable that lets us know if we're in the GVL
|
66
|
+
static _Thread_local char in_gvl = 0;
|
67
|
+
static char check_in_gvl(void) { return in_gvl; }
|
68
|
+
|
69
|
+
////////////////////////////////////////////////////////////////////////////
|
70
|
+
// Calling C functions.
|
71
|
+
static void *call_c(void *(*func)(void *), void *arg) {
|
72
|
+
if (in_gvl) {
|
73
|
+
return func(arg);
|
74
|
+
}
|
75
|
+
void *ret;
|
76
|
+
in_gvl = 1;
|
77
|
+
ret = rb_thread_call_with_gvl(func, arg);
|
78
|
+
in_gvl = 0;
|
79
|
+
return ret;
|
80
|
+
}
|
81
|
+
|
82
|
+
////////////////////////////////////////////////////////////////////////////
|
83
|
+
// A simple (and a bit lighter) design for when there's no need for arguments.
|
84
|
+
|
89
85
|
// wrapping any API calls for exception management AND GVL entry
|
90
86
|
static VALUE call(VALUE obj, ID method) {
|
91
|
-
struct
|
87
|
+
struct RubyArgCall task = {.obj = obj, .method = method};
|
92
88
|
call_c(run_ruby_method_within_gvl, &task);
|
93
89
|
return task.returned;
|
94
90
|
}
|
@@ -96,32 +92,20 @@ static VALUE call(VALUE obj, ID method) {
|
|
96
92
|
////////////////////////////////////////////////////////////////////////////
|
97
93
|
// A heavier (memory) design for when we're passing arguments around.
|
98
94
|
|
99
|
-
// running the actual method call
|
100
|
-
static VALUE run_argv_method_unsafe(VALUE _tsk) {
|
101
|
-
struct RubyArgCall *task = (void *)_tsk;
|
102
|
-
return rb_funcall2(task->obj, task->method, task->argc, task->argv);
|
103
|
-
}
|
104
|
-
|
105
|
-
// GVL gateway
|
106
|
-
static void *run_argv_method_within_gvl(void *_tsk) {
|
107
|
-
struct RubyArgCall *task = _tsk;
|
108
|
-
int state = 0;
|
109
|
-
task->returned = rb_protect(run_argv_method_unsafe, (VALUE)(task), &state);
|
110
|
-
if (state)
|
111
|
-
handle_exception(NULL);
|
112
|
-
return task;
|
113
|
-
}
|
114
|
-
|
115
95
|
// wrapping any API calls for exception management AND GVL entry
|
116
96
|
static VALUE call_arg(VALUE obj, ID method, int argc, VALUE *argv) {
|
117
97
|
struct RubyArgCall task = {
|
118
98
|
.obj = obj, .method = method, .argc = argc, .argv = argv};
|
119
|
-
call_c(
|
99
|
+
call_c(run_ruby_method_within_gvl, &task);
|
120
100
|
return task.returned;
|
121
101
|
}
|
122
102
|
|
123
103
|
////////////////////////////////////////////////////////////////////////////
|
124
104
|
// the API interface
|
125
105
|
struct _Ruby_Method_Caller_Class_ RubyCaller = {
|
126
|
-
.call = call,
|
106
|
+
.call = call,
|
107
|
+
.call2 = call_arg,
|
108
|
+
.call_c = call_c,
|
109
|
+
// .leave_gvl = leave_gvl,
|
110
|
+
.in_gvl = check_in_gvl,
|
127
111
|
};
|
@@ -0,0 +1,127 @@
|
|
1
|
+
#include "rb-call.h"
|
2
|
+
#include <ruby.h>
|
3
|
+
#include <ruby/thread.h>
|
4
|
+
#include <pthread.h>
|
5
|
+
|
6
|
+
///////////////
|
7
|
+
// this is a simple helper that calls Ruby methods on Ruby objects while within
|
8
|
+
// a non-GVL ruby thread zone.
|
9
|
+
|
10
|
+
// a structure for Ruby API calls
|
11
|
+
struct RubySimpleCall {
|
12
|
+
VALUE obj;
|
13
|
+
VALUE returned;
|
14
|
+
ID method;
|
15
|
+
};
|
16
|
+
struct RubyArgCall {
|
17
|
+
VALUE obj;
|
18
|
+
int argc;
|
19
|
+
VALUE *argv;
|
20
|
+
VALUE returned;
|
21
|
+
ID method;
|
22
|
+
};
|
23
|
+
|
24
|
+
#if __STDC_VERSION__ < 201112L || __STDC_NO_THREADS__
|
25
|
+
#define _Thread_local __thread
|
26
|
+
#endif
|
27
|
+
|
28
|
+
// a thread specific global variable that lets us know if we're in the GVL
|
29
|
+
static _Thread_local char in_gvl = 0;
|
30
|
+
static char check_in_gvl(void) { return in_gvl; }
|
31
|
+
|
32
|
+
////////////////////////////////////////////////////////////////////////////
|
33
|
+
// Calling C functions.
|
34
|
+
static void *call_c(void *(*func)(void *), void *arg) {
|
35
|
+
if (in_gvl) {
|
36
|
+
return func(arg);
|
37
|
+
}
|
38
|
+
void *ret;
|
39
|
+
in_gvl = 1;
|
40
|
+
ret = rb_thread_call_with_gvl(func, arg);
|
41
|
+
in_gvl = 0;
|
42
|
+
return ret;
|
43
|
+
}
|
44
|
+
|
45
|
+
////////////////////////////////////////////////////////////////////////////
|
46
|
+
// Handling exceptions (printing the backtrace doesn't really work well).
|
47
|
+
static void *handle_exception(void *_) {
|
48
|
+
VALUE exc = rb_errinfo();
|
49
|
+
if (exc != Qnil) {
|
50
|
+
VALUE msg = RubyCaller.call(exc, rb_intern("message"));
|
51
|
+
VALUE exc_class = rb_class_name(CLASS_OF(exc));
|
52
|
+
VALUE bt = RubyCaller.call(exc, rb_intern("backtrace"));
|
53
|
+
if (TYPE(bt) == T_ARRAY) {
|
54
|
+
bt = rb_ary_join(bt, rb_str_new_literal("\n"));
|
55
|
+
fprintf(stderr, "Iodine caught an unprotected exception - %.*s: %.*s\n%s",
|
56
|
+
(int)RSTRING_LEN(exc_class), RSTRING_PTR(exc_class),
|
57
|
+
(int)RSTRING_LEN(msg), RSTRING_PTR(msg), StringValueCStr(bt));
|
58
|
+
} else {
|
59
|
+
fprintf(stderr, "Iodine caught an unprotected exception - %.*s: %.*s\n"
|
60
|
+
"No backtrace available.\n",
|
61
|
+
(int)RSTRING_LEN(exc_class), RSTRING_PTR(exc_class),
|
62
|
+
(int)RSTRING_LEN(msg), RSTRING_PTR(msg));
|
63
|
+
}
|
64
|
+
rb_backtrace();
|
65
|
+
rb_set_errinfo(Qnil);
|
66
|
+
}
|
67
|
+
return (void *)Qnil;
|
68
|
+
}
|
69
|
+
|
70
|
+
////////////////////////////////////////////////////////////////////////////
|
71
|
+
// A simple (and a bit lighter) design for when there's no need for arguments.
|
72
|
+
|
73
|
+
// running the actual method call
|
74
|
+
static VALUE run_ruby_method_unsafe(VALUE _tsk) {
|
75
|
+
struct RubySimpleCall *task = (void *)_tsk;
|
76
|
+
return rb_funcall2(task->obj, task->method, 0, NULL);
|
77
|
+
}
|
78
|
+
|
79
|
+
// GVL gateway
|
80
|
+
static void *run_ruby_method_within_gvl(void *_tsk) {
|
81
|
+
struct RubySimpleCall *task = _tsk;
|
82
|
+
int state = 0;
|
83
|
+
task->returned = rb_protect(run_ruby_method_unsafe, (VALUE)(task), &state);
|
84
|
+
if (state)
|
85
|
+
handle_exception(NULL);
|
86
|
+
return task;
|
87
|
+
}
|
88
|
+
|
89
|
+
// wrapping any API calls for exception management AND GVL entry
|
90
|
+
static VALUE call(VALUE obj, ID method) {
|
91
|
+
struct RubySimpleCall task = {.obj = obj, .method = method};
|
92
|
+
call_c(run_ruby_method_within_gvl, &task);
|
93
|
+
return task.returned;
|
94
|
+
}
|
95
|
+
|
96
|
+
////////////////////////////////////////////////////////////////////////////
|
97
|
+
// A heavier (memory) design for when we're passing arguments around.
|
98
|
+
|
99
|
+
// running the actual method call
|
100
|
+
static VALUE run_argv_method_unsafe(VALUE _tsk) {
|
101
|
+
struct RubyArgCall *task = (void *)_tsk;
|
102
|
+
return rb_funcall2(task->obj, task->method, task->argc, task->argv);
|
103
|
+
}
|
104
|
+
|
105
|
+
// GVL gateway
|
106
|
+
static void *run_argv_method_within_gvl(void *_tsk) {
|
107
|
+
struct RubyArgCall *task = _tsk;
|
108
|
+
int state = 0;
|
109
|
+
task->returned = rb_protect(run_argv_method_unsafe, (VALUE)(task), &state);
|
110
|
+
if (state)
|
111
|
+
handle_exception(NULL);
|
112
|
+
return task;
|
113
|
+
}
|
114
|
+
|
115
|
+
// wrapping any API calls for exception management AND GVL entry
|
116
|
+
static VALUE call_arg(VALUE obj, ID method, int argc, VALUE *argv) {
|
117
|
+
struct RubyArgCall task = {
|
118
|
+
.obj = obj, .method = method, .argc = argc, .argv = argv};
|
119
|
+
call_c(run_argv_method_within_gvl, &task);
|
120
|
+
return task.returned;
|
121
|
+
}
|
122
|
+
|
123
|
+
////////////////////////////////////////////////////////////////////////////
|
124
|
+
// the API interface
|
125
|
+
struct _Ruby_Method_Caller_Class_ RubyCaller = {
|
126
|
+
.call = call, .call2 = call_arg, .call_c = call_c, .in_gvl = check_in_gvl,
|
127
|
+
};
|
data/ext/iodine/rb-registry.c
CHANGED
@@ -1,11 +1,20 @@
|
|
1
1
|
#include "rb-registry.h"
|
2
|
-
#include <ruby.h>
|
3
2
|
#include "spnlock.h"
|
3
|
+
#include <ruby.h>
|
4
4
|
|
5
5
|
// #define RUBY_REG_DBG
|
6
6
|
|
7
|
+
#define REGISTRY_POOL_SIZE 1024
|
8
|
+
// the references struct (bin-tree)
|
9
|
+
struct Object {
|
10
|
+
struct Object *next;
|
11
|
+
VALUE obj;
|
12
|
+
int count;
|
13
|
+
};
|
14
|
+
|
7
15
|
// the registry global
|
8
16
|
static struct Registry {
|
17
|
+
struct Object pool_mem[REGISTRY_POOL_SIZE];
|
9
18
|
struct Object *obj_pool;
|
10
19
|
struct Object *first;
|
11
20
|
VALUE owner;
|
@@ -17,28 +26,14 @@ static struct Registry {
|
|
17
26
|
#define unlock_registry() spn_unlock(®istry.lock)
|
18
27
|
#define lock_registry() spn_lock(®istry.lock)
|
19
28
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
}
|
26
|
-
|
27
|
-
// manage existing objects - add a reference
|
28
|
-
int add_reference(VALUE obj) {
|
29
|
-
struct Object *line;
|
30
|
-
lock_registry();
|
31
|
-
line = registry.first;
|
32
|
-
while (line) {
|
33
|
-
if (line->obj == obj) {
|
34
|
-
line->count++;
|
35
|
-
unlock_registry();
|
36
|
-
return 1;
|
37
|
-
}
|
38
|
-
line = line->next;
|
29
|
+
inline static void free_node(struct Object *to_free) {
|
30
|
+
if (to_free >= registry.pool_mem &&
|
31
|
+
(intptr_t)to_free <= (intptr_t)(®istry.obj_pool)) {
|
32
|
+
to_free->next = registry.obj_pool;
|
33
|
+
registry.obj_pool = to_free;
|
34
|
+
} else {
|
35
|
+
free(to_free);
|
39
36
|
}
|
40
|
-
unlock_registry();
|
41
|
-
return 0;
|
42
37
|
}
|
43
38
|
|
44
39
|
// add an object to the registry
|
@@ -47,25 +42,30 @@ int add_reference(VALUE obj) {
|
|
47
42
|
static VALUE register_object(VALUE obj) {
|
48
43
|
if (!obj || obj == Qnil)
|
49
44
|
return 0;
|
50
|
-
|
51
|
-
return obj;
|
52
|
-
struct Object *line;
|
45
|
+
struct Object *line = registry.first;
|
53
46
|
lock_registry();
|
47
|
+
while (line) {
|
48
|
+
if (line->obj == obj) {
|
49
|
+
line->count++;
|
50
|
+
goto finish;
|
51
|
+
}
|
52
|
+
line = line->next;
|
53
|
+
}
|
54
54
|
if (registry.obj_pool) {
|
55
55
|
line = registry.obj_pool;
|
56
56
|
registry.obj_pool = registry.obj_pool->next;
|
57
57
|
} else {
|
58
58
|
line = malloc(sizeof(struct Object));
|
59
59
|
}
|
60
|
-
if (
|
61
|
-
perror("No Memory");
|
62
|
-
|
63
|
-
return 0;
|
60
|
+
if (line == NULL) {
|
61
|
+
perror("No Memory!");
|
62
|
+
exit(1);
|
64
63
|
}
|
65
64
|
line->obj = obj;
|
66
65
|
line->next = registry.first;
|
67
66
|
line->count = 1;
|
68
67
|
registry.first = line;
|
68
|
+
finish:
|
69
69
|
unlock_registry();
|
70
70
|
return obj;
|
71
71
|
}
|
@@ -77,24 +77,18 @@ static void unregister_object(VALUE obj) {
|
|
77
77
|
if (!obj || obj == Qnil)
|
78
78
|
return;
|
79
79
|
lock_registry();
|
80
|
-
struct Object
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
line->count
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
prev->next = line->next;
|
90
|
-
// move the object container to the discarded object pool
|
91
|
-
line->next = registry.obj_pool;
|
92
|
-
registry.obj_pool = line;
|
80
|
+
struct Object **line = ®istry.first;
|
81
|
+
while (*line) {
|
82
|
+
if ((*line)->obj == obj) {
|
83
|
+
(*line)->count -= 1;
|
84
|
+
if ((*line)->count <= 0) {
|
85
|
+
struct Object *to_free = *line;
|
86
|
+
*line = (*line)->next;
|
87
|
+
free_node(to_free);
|
88
|
+
goto finish;
|
93
89
|
}
|
94
|
-
goto finish;
|
95
90
|
}
|
96
|
-
|
97
|
-
line = line->next;
|
91
|
+
line = &((*line)->next);
|
98
92
|
}
|
99
93
|
finish:
|
100
94
|
unlock_registry();
|
@@ -148,17 +142,9 @@ static void registry_clear(void *ignore) {
|
|
148
142
|
while (line) {
|
149
143
|
to_free = line;
|
150
144
|
line = line->next;
|
151
|
-
|
145
|
+
free_node(to_free);
|
152
146
|
}
|
153
147
|
registry.first = NULL;
|
154
|
-
// free container pool
|
155
|
-
line = registry.obj_pool;
|
156
|
-
while (line) {
|
157
|
-
to_free = line;
|
158
|
-
line = line->next;
|
159
|
-
free(to_free);
|
160
|
-
}
|
161
|
-
registry.obj_pool = NULL;
|
162
148
|
registry.owner = 0;
|
163
149
|
unlock_registry();
|
164
150
|
}
|
@@ -174,6 +160,7 @@ static struct rb_data_type_struct my_registry_type_struct = {
|
|
174
160
|
// initialize the registry
|
175
161
|
static void init(VALUE owner) {
|
176
162
|
lock_registry();
|
163
|
+
// only one registry
|
177
164
|
if (registry.owner)
|
178
165
|
goto finish;
|
179
166
|
if (!owner)
|
@@ -184,6 +171,12 @@ static void init(VALUE owner) {
|
|
184
171
|
VALUE r_registry =
|
185
172
|
TypedData_Wrap_Struct(rReferences, &my_registry_type_struct, ®istry);
|
186
173
|
rb_ivar_set(owner, rb_intern("registry"), r_registry);
|
174
|
+
// initialize memory pool
|
175
|
+
for (size_t i = 0; i < REGISTRY_POOL_SIZE - 1; i++) {
|
176
|
+
registry.pool_mem[i].next = registry.pool_mem + i + 1;
|
177
|
+
}
|
178
|
+
registry.pool_mem[REGISTRY_POOL_SIZE - 1].next = NULL;
|
179
|
+
registry.obj_pool = registry.pool_mem;
|
187
180
|
finish:
|
188
181
|
unlock_registry();
|
189
182
|
}
|
@@ -0,0 +1,213 @@
|
|
1
|
+
#include "rb-registry.h"
|
2
|
+
#include "spnlock.h"
|
3
|
+
#include <ruby.h>
|
4
|
+
|
5
|
+
// #define RUBY_REG_DBG
|
6
|
+
|
7
|
+
// the registry global
|
8
|
+
static struct Registry {
|
9
|
+
struct Object *obj_pool;
|
10
|
+
struct Object *first;
|
11
|
+
VALUE owner;
|
12
|
+
spn_lock_i lock;
|
13
|
+
} registry = {
|
14
|
+
.obj_pool = NULL, .first = NULL, .owner = 0, .lock = SPN_LOCK_INIT};
|
15
|
+
|
16
|
+
#define try_lock_registry() spn_trylock(®istry.lock)
|
17
|
+
#define unlock_registry() spn_unlock(®istry.lock)
|
18
|
+
#define lock_registry() spn_lock(®istry.lock)
|
19
|
+
|
20
|
+
// the references struct (bin-tree)
|
21
|
+
struct Object {
|
22
|
+
struct Object *next;
|
23
|
+
VALUE obj;
|
24
|
+
int count;
|
25
|
+
};
|
26
|
+
|
27
|
+
// manage existing objects - add a reference
|
28
|
+
int add_reference(VALUE obj) {
|
29
|
+
struct Object *line;
|
30
|
+
lock_registry();
|
31
|
+
line = registry.first;
|
32
|
+
while (line) {
|
33
|
+
if (line->obj == obj) {
|
34
|
+
line->count++;
|
35
|
+
unlock_registry();
|
36
|
+
return 1;
|
37
|
+
}
|
38
|
+
line = line->next;
|
39
|
+
}
|
40
|
+
unlock_registry();
|
41
|
+
return 0;
|
42
|
+
}
|
43
|
+
|
44
|
+
// add an object to the registry
|
45
|
+
//
|
46
|
+
// allow multiple registrartions (bag)
|
47
|
+
static VALUE register_object(VALUE obj) {
|
48
|
+
if (!obj || obj == Qnil)
|
49
|
+
return 0;
|
50
|
+
if (add_reference(obj))
|
51
|
+
return obj;
|
52
|
+
struct Object *line;
|
53
|
+
lock_registry();
|
54
|
+
if (registry.obj_pool) {
|
55
|
+
line = registry.obj_pool;
|
56
|
+
registry.obj_pool = registry.obj_pool->next;
|
57
|
+
} else {
|
58
|
+
line = malloc(sizeof(struct Object));
|
59
|
+
}
|
60
|
+
if (!line) {
|
61
|
+
perror("No Memory!");
|
62
|
+
unlock_registry();
|
63
|
+
return 0;
|
64
|
+
}
|
65
|
+
line->obj = obj;
|
66
|
+
line->next = registry.first;
|
67
|
+
line->count = 1;
|
68
|
+
registry.first = line;
|
69
|
+
unlock_registry();
|
70
|
+
return obj;
|
71
|
+
}
|
72
|
+
|
73
|
+
// free a single registry
|
74
|
+
//
|
75
|
+
// free only one.
|
76
|
+
static void unregister_object(VALUE obj) {
|
77
|
+
if (!obj || obj == Qnil)
|
78
|
+
return;
|
79
|
+
lock_registry();
|
80
|
+
struct Object *line = registry.first;
|
81
|
+
struct Object *prev = NULL;
|
82
|
+
while (line) {
|
83
|
+
if (line->obj == obj) {
|
84
|
+
line->count--;
|
85
|
+
if (!line->count) {
|
86
|
+
if (line == registry.first)
|
87
|
+
registry.first = line->next;
|
88
|
+
else if (prev) // must be true, really
|
89
|
+
prev->next = line->next;
|
90
|
+
// move the object container to the discarded object pool
|
91
|
+
line->next = registry.obj_pool;
|
92
|
+
registry.obj_pool = line;
|
93
|
+
}
|
94
|
+
goto finish;
|
95
|
+
}
|
96
|
+
prev = line;
|
97
|
+
line = line->next;
|
98
|
+
}
|
99
|
+
finish:
|
100
|
+
unlock_registry();
|
101
|
+
}
|
102
|
+
|
103
|
+
// // Replaces one registry object with another,
|
104
|
+
// // allowing updates to the Registry with no memory allocations.
|
105
|
+
// //
|
106
|
+
// // returns 0 if all OK, returns -1 if it couldn't replace the object.
|
107
|
+
// static int replace_object(VALUE obj, VALUE new_obj) {
|
108
|
+
// int ret = -1;
|
109
|
+
// if (obj == new_obj)
|
110
|
+
// return 0;
|
111
|
+
// pthread_mutex_lock(®istry_lock);
|
112
|
+
// struct Object* line = registry.first;
|
113
|
+
// while (line) {
|
114
|
+
// if (line->obj == obj) {
|
115
|
+
// line->obj = new_obj;
|
116
|
+
// ret = 0;
|
117
|
+
// goto finish;
|
118
|
+
// }
|
119
|
+
// line = line->next;
|
120
|
+
// }
|
121
|
+
// finish:
|
122
|
+
// pthread_mutex_unlock(®istry_lock);
|
123
|
+
// return ret;
|
124
|
+
// }
|
125
|
+
|
126
|
+
// a callback for the GC (marking active objects)
|
127
|
+
static void registry_mark(void *ignore) {
|
128
|
+
#ifdef RUBY_REG_DBG
|
129
|
+
Registry.print();
|
130
|
+
#endif
|
131
|
+
lock_registry();
|
132
|
+
struct Object *line = registry.first;
|
133
|
+
while (line) {
|
134
|
+
if (line->obj)
|
135
|
+
rb_gc_mark(line->obj);
|
136
|
+
line = line->next;
|
137
|
+
}
|
138
|
+
unlock_registry();
|
139
|
+
}
|
140
|
+
|
141
|
+
// clear the registry (end of lifetime)
|
142
|
+
static void registry_clear(void *ignore) {
|
143
|
+
lock_registry();
|
144
|
+
struct Object *line;
|
145
|
+
struct Object *to_free;
|
146
|
+
// free active object references
|
147
|
+
line = registry.first;
|
148
|
+
while (line) {
|
149
|
+
to_free = line;
|
150
|
+
line = line->next;
|
151
|
+
free(to_free);
|
152
|
+
}
|
153
|
+
registry.first = NULL;
|
154
|
+
// free container pool
|
155
|
+
line = registry.obj_pool;
|
156
|
+
while (line) {
|
157
|
+
to_free = line;
|
158
|
+
line = line->next;
|
159
|
+
free(to_free);
|
160
|
+
}
|
161
|
+
registry.obj_pool = NULL;
|
162
|
+
registry.owner = 0;
|
163
|
+
unlock_registry();
|
164
|
+
}
|
165
|
+
|
166
|
+
// the data-type used to identify the registry
|
167
|
+
// this sets the callbacks.
|
168
|
+
static struct rb_data_type_struct my_registry_type_struct = {
|
169
|
+
.wrap_struct_name = "RubyReferencesIn_C_Land",
|
170
|
+
.function.dfree = (void (*)(void *))registry_clear,
|
171
|
+
.function.dmark = (void (*)(void *))registry_mark,
|
172
|
+
};
|
173
|
+
|
174
|
+
// initialize the registry
|
175
|
+
static void init(VALUE owner) {
|
176
|
+
lock_registry();
|
177
|
+
if (registry.owner)
|
178
|
+
goto finish;
|
179
|
+
if (!owner)
|
180
|
+
owner = rb_cObject;
|
181
|
+
registry.owner = owner;
|
182
|
+
VALUE rReferences =
|
183
|
+
rb_define_class_under(owner, "RubyObjectRegistry_for_C_land", rb_cData);
|
184
|
+
VALUE r_registry =
|
185
|
+
TypedData_Wrap_Struct(rReferences, &my_registry_type_struct, ®istry);
|
186
|
+
rb_ivar_set(owner, rb_intern("registry"), r_registry);
|
187
|
+
finish:
|
188
|
+
unlock_registry();
|
189
|
+
}
|
190
|
+
|
191
|
+
// print data, for testing
|
192
|
+
static void print(void) {
|
193
|
+
lock_registry();
|
194
|
+
struct Object *line = registry.first;
|
195
|
+
fprintf(stderr, "Registry owner is %lu\n", registry.owner);
|
196
|
+
long index = 0;
|
197
|
+
while (line) {
|
198
|
+
fprintf(stderr, "[%lu] => %d X obj %lu type %d at %p\n", index++,
|
199
|
+
line->count, line->obj, TYPE(line->obj), line);
|
200
|
+
line = line->next;
|
201
|
+
}
|
202
|
+
fprintf(stderr, "Total of %lu registered objects being marked\n", index);
|
203
|
+
unlock_registry();
|
204
|
+
}
|
205
|
+
|
206
|
+
////////////////////////////////////////////
|
207
|
+
// The API gateway
|
208
|
+
struct ___RegistryClass___ Registry = {
|
209
|
+
.init = init,
|
210
|
+
.remove = unregister_object,
|
211
|
+
.add = register_object,
|
212
|
+
.print = print,
|
213
|
+
};
|
data/ext/iodine/websockets.c
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
#include "libserver.h"
|
2
1
|
#include "websockets.h"
|
3
2
|
#include "bscrypt.h"
|
4
|
-
#include
|
3
|
+
#include "libserver.h"
|
4
|
+
#include <arpa/inet.h>
|
5
5
|
#include <stdio.h>
|
6
|
+
#include <stdlib.h>
|
6
7
|
#include <string.h>
|
7
|
-
#include <arpa/inet.h>
|
8
8
|
|
9
9
|
#if !defined(__BIG_ENDIAN__) && !defined(__LITTLE_ENDIAN__)
|
10
10
|
#include <endian.h>
|
@@ -593,8 +593,12 @@ refuse:
|
|
593
593
|
// set the negative response
|
594
594
|
response->status = 400;
|
595
595
|
cleanup:
|
596
|
-
http_response_finish(response);
|
597
596
|
if (response->status == 101) {
|
597
|
+
// set the protocol lock
|
598
|
+
ws->protocol.callback_lock = SPN_LOCK_INIT;
|
599
|
+
spn_lock(&ws->protocol.callback_lock);
|
600
|
+
// send the response
|
601
|
+
http_response_finish(response);
|
598
602
|
// update the protocol object, cleanning up the old one
|
599
603
|
server_switch_protocol(ws->fd, (void *)ws);
|
600
604
|
// we have an active websocket connection - prep the connection buffer
|
@@ -604,8 +608,10 @@ cleanup:
|
|
604
608
|
// call the on_open callback
|
605
609
|
if (settings.on_open)
|
606
610
|
server_task(ws->fd, on_open, settings.on_open, NULL);
|
611
|
+
spn_unlock(&ws->protocol.callback_lock);
|
607
612
|
return 0;
|
608
613
|
}
|
614
|
+
http_response_finish(response);
|
609
615
|
destroy_ws(ws);
|
610
616
|
return -1;
|
611
617
|
}
|
data/iodine.gemspec
CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.requirements << 'A Unix based system: Linux / macOS / BSD.'
|
38
38
|
spec.requirements << 'An updated C compiler.'
|
39
39
|
spec.requirements << 'Ruby >= 2.2.2'
|
40
|
-
spec.requirements << 'Ruby >= 2.
|
40
|
+
spec.requirements << 'Ruby >= 2.3.0 is recommended.'
|
41
41
|
|
42
42
|
spec.add_development_dependency 'bundler', '~> 1.10'
|
43
43
|
spec.add_development_dependency 'rake', '~> 10.0'
|
data/lib/iodine/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iodine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Boaz Segev
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-12-
|
11
|
+
date: 2016-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -150,12 +150,14 @@ files:
|
|
150
150
|
- ext/iodine/random.c
|
151
151
|
- ext/iodine/random.h
|
152
152
|
- ext/iodine/rb-call.c
|
153
|
+
- ext/iodine/rb-call.c_old
|
153
154
|
- ext/iodine/rb-call.h
|
154
155
|
- ext/iodine/rb-libasync.h
|
155
156
|
- ext/iodine/rb-rack-io.c
|
156
157
|
- ext/iodine/rb-rack-io.h
|
157
158
|
- ext/iodine/rb-registry.c
|
158
159
|
- ext/iodine/rb-registry.h
|
160
|
+
- ext/iodine/rb-registry_old.c_old
|
159
161
|
- ext/iodine/sha1.c
|
160
162
|
- ext/iodine/sha1.h
|
161
163
|
- ext/iodine/sha2.c
|
@@ -198,7 +200,7 @@ requirements:
|
|
198
200
|
- 'A Unix based system: Linux / macOS / BSD.'
|
199
201
|
- An updated C compiler.
|
200
202
|
- Ruby >= 2.2.2
|
201
|
-
- Ruby >= 2.
|
203
|
+
- Ruby >= 2.3.0 is recommended.
|
202
204
|
rubyforge_project:
|
203
205
|
rubygems_version: 2.5.2
|
204
206
|
signing_key:
|