curl_ffi 0.0.2
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.
- data/.gitignore +3 -0
- data/Gemfile +9 -0
- data/curl_ffi.gemspec +24 -0
- data/examples/evented_multi.rb +223 -0
- data/examples/perform_multi.rb +22 -0
- data/examples/select_multi.rb +219 -0
- data/lib/curl_ffi/curl_bindings.rb +985 -0
- data/lib/curl_ffi/easy.rb +86 -0
- data/lib/curl_ffi/multi.rb +66 -0
- data/lib/curl_ffi/version.rb +3 -0
- data/lib/curl_ffi.rb +13 -0
- data/spec/curl/easy_spec.rb +84 -0
- data/spec/curl/multi_spec.rb +81 -0
- data/spec/spec_helper.rb +3 -0
- metadata +108 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/curl_ffi.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "curl_ffi/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "curl_ffi"
|
7
|
+
s.version = CurlFFI::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Arthur Schreiber", "Scott Gonyea"]
|
10
|
+
s.email = ["schreiber.arthur@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/nokarma/curl-ffi"
|
12
|
+
s.summary = "An FFI based libCurl interface"
|
13
|
+
s.description = "An FFI based libCurl interface, intended to serve as a common backend for existing interfaces to libcurl"
|
14
|
+
|
15
|
+
s.required_rubygems_version = ">= 1.3.6"
|
16
|
+
s.rubyforge_project = "curl-ffi"
|
17
|
+
|
18
|
+
s.add_dependency "ffi"
|
19
|
+
s.add_development_dependency "rspec"
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
# Shows how to use the libcurl-ffi interface in combination with eventmachine.
|
2
|
+
# Inspired by the different hiperfifo examples on the libcurl site.
|
3
|
+
require "rubygems"
|
4
|
+
require "benchmark"
|
5
|
+
require "eventmachine"
|
6
|
+
|
7
|
+
require File.expand_path("../lib/curl", File.dirname(__FILE__))
|
8
|
+
|
9
|
+
if FFI::Platform.windows?
|
10
|
+
# Sockets returned by curl are WinSock SOCKETs
|
11
|
+
# As we want to wrap these sockets in Ruby's IO interface (using IO.for_fd),
|
12
|
+
# we have to first get the SOCKET's filehandle using _get_osfhandle.
|
13
|
+
#
|
14
|
+
# To later get the SOCKET again, we can use _open_osfhandle again.
|
15
|
+
module WinSock
|
16
|
+
extend FFI::Library
|
17
|
+
ffi_lib FFI::Library::LIBC
|
18
|
+
attach_function :_get_osfhandle, [:int], :long
|
19
|
+
attach_function :_open_osfhandle, [:long, :int], :int
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_socket(io)
|
23
|
+
WinSock._get_osfhandle(io.fileno)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_io(socket)
|
27
|
+
FFI::IO.for_fd(WinSock._open_osfhandle(socket, 0), "r")
|
28
|
+
end
|
29
|
+
else
|
30
|
+
def get_socket(io)
|
31
|
+
io.fileno
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_io(socket)
|
35
|
+
IO.for_fd(socket, "r")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
$sockets = {}
|
40
|
+
|
41
|
+
module CurlHandler
|
42
|
+
def notify_readable
|
43
|
+
begin
|
44
|
+
rc = $multi.socket_action(get_socket(@io), 1)
|
45
|
+
end while rc == :CALL_MULTI_PERFORM
|
46
|
+
mcode_or_die("event_cb: curl_multi_socket", rc)
|
47
|
+
check_run_count
|
48
|
+
if $multi.running <= 0
|
49
|
+
puts "last transfer done, kill timeout\n"
|
50
|
+
EM.stop
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def notify_writable
|
55
|
+
begin
|
56
|
+
rc = $multi.socket_action(get_socket(@io), 2)
|
57
|
+
end while rc == :CALL_MULTI_PERFORM
|
58
|
+
mcode_or_die("event_cb: curl_multi_socket", rc)
|
59
|
+
check_run_count
|
60
|
+
if $multi.running <= 0
|
61
|
+
puts "last transfer done, kill timeout\n"
|
62
|
+
EM.stop
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def addsock(socket, easy, action)
|
68
|
+
io = get_io(socket)
|
69
|
+
$sockets[socket] = {
|
70
|
+
:io => io,
|
71
|
+
:action => action,
|
72
|
+
:connection => EM.watch(io, CurlHandler)
|
73
|
+
}
|
74
|
+
setsock(socket, easy, action)
|
75
|
+
end
|
76
|
+
|
77
|
+
def setsock(socket, easy, action)
|
78
|
+
$sockets[socket][:action] = action
|
79
|
+
conn = $sockets[socket][:connection]
|
80
|
+
conn.notify_readable = action & 1 != 0
|
81
|
+
conn.notify_writable = action & 2 != 0
|
82
|
+
end
|
83
|
+
|
84
|
+
def remsock(socket)
|
85
|
+
puts "Removing Socket #{socket}"
|
86
|
+
$sockets[socket][:connection].detach
|
87
|
+
$sockets.delete(socket)
|
88
|
+
end
|
89
|
+
|
90
|
+
sock_callback = FFI::Function.new(:int, [:pointer, :int, :int]) do |easy_ptr, socket, what|
|
91
|
+
whatstr = [ "none", "IN", "OUT", "INOUT", "REMOVE" ]
|
92
|
+
puts("socket callback: s=%d e=%p what=%s " % [socket, easy_ptr, whatstr[what]])
|
93
|
+
|
94
|
+
if what == 4
|
95
|
+
remsock(socket)
|
96
|
+
puts ""
|
97
|
+
else
|
98
|
+
if $sockets[socket].nil?
|
99
|
+
puts "Adding data: %s\n" % whatstr[what]
|
100
|
+
addsock(socket, easy_ptr, what)
|
101
|
+
else
|
102
|
+
puts "Changing action from %s to %s\n" % [whatstr[$sockets[socket][:action]], whatstr[what]]
|
103
|
+
setsock(socket, easy_ptr, what)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
0
|
108
|
+
end
|
109
|
+
|
110
|
+
def mcode_or_die(where, code)
|
111
|
+
return if code == :OK
|
112
|
+
puts "ERROR: %s returns %s\n" % [where, code]
|
113
|
+
exit unless code == :BAD_SOCKET
|
114
|
+
end
|
115
|
+
|
116
|
+
$prev_running = 0
|
117
|
+
def check_run_count
|
118
|
+
if $prev_running > $multi.running
|
119
|
+
puts "REMAINING: %d\n" % $multi.running
|
120
|
+
|
121
|
+
while message = $multi.info_read_next
|
122
|
+
if message[:msg] == :DONE
|
123
|
+
puts "Done!"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
$prev_running = $multi.running
|
128
|
+
end
|
129
|
+
|
130
|
+
$timer = nil
|
131
|
+
|
132
|
+
multi_timer_callback = FFI::Function.new(:int, [:pointer, :long]) do |multi_ptr, timeout_ms|
|
133
|
+
puts "multi_timer_cb: Setting timeout to %d ms\n" % timeout_ms
|
134
|
+
|
135
|
+
$timer && $timer.cancel
|
136
|
+
$timer = EventMachine::Timer.new(timeout_ms / 1000.0) {
|
137
|
+
begin
|
138
|
+
rc = $multi.socket_action(CurlFFI::SOCKET_TIMEOUT, 0)
|
139
|
+
puts rc
|
140
|
+
end while rc == :CALL_MULTI_PERFORM
|
141
|
+
mcode_or_die("timer_cb: curl_multi_socket_action", rc)
|
142
|
+
check_run_count
|
143
|
+
}
|
144
|
+
|
145
|
+
0
|
146
|
+
end
|
147
|
+
|
148
|
+
def progress_callback(url_pointer, dltotal, dlnow, ultotal, ulnow)
|
149
|
+
puts "[#{Time.now}]Progress: %s (%g/%g)\n" % [url_pointer.read_string, dlnow, dltotal]
|
150
|
+
return 0
|
151
|
+
end
|
152
|
+
|
153
|
+
def write_callback(easy, size, nmemb, data)
|
154
|
+
realsize = size * nmemb
|
155
|
+
return realsize
|
156
|
+
end
|
157
|
+
|
158
|
+
PROGRESS_CALLBACK = FFI::Function.new(:int, [:pointer, :double, :double, :double, :double], &self.method(:progress_callback))
|
159
|
+
WRITE_CALLBACK = FFI::Function.new(:size_t, [:pointer, :size_t, :size_t, :pointer], &self.method(:write_callback))
|
160
|
+
|
161
|
+
$multi = CurlFFI::Multi.new
|
162
|
+
$multi.setopt(:SOCKETFUNCTION, sock_callback)
|
163
|
+
$multi.setopt(:TIMERFUNCTION, multi_timer_callback)
|
164
|
+
|
165
|
+
EventMachine::run {
|
166
|
+
[ "http://www.microsoft.com",
|
167
|
+
"http://www.opensource.org",
|
168
|
+
"http://www.google.com",
|
169
|
+
"http://www.yahoo.com",
|
170
|
+
"http://www.ibm.com",
|
171
|
+
"http://www.mysql.com",
|
172
|
+
"http://www.oracle.com",
|
173
|
+
"http://www.ripe.net",
|
174
|
+
"http://www.iana.org",
|
175
|
+
"http://www.amazon.com",
|
176
|
+
"http://www.netcraft.com",
|
177
|
+
"http://www.heise.de",
|
178
|
+
"http://www.chip.de",
|
179
|
+
"http://www.ca.com",
|
180
|
+
"http://www.cnet.com",
|
181
|
+
"http://www.news.com",
|
182
|
+
"http://www.cnn.com",
|
183
|
+
"http://www.wikipedia.org",
|
184
|
+
"http://www.dell.com",
|
185
|
+
"http://www.hp.com",
|
186
|
+
"http://www.cert.org",
|
187
|
+
"http://www.mit.edu",
|
188
|
+
"http://www.nist.gov",
|
189
|
+
"http://www.ebay.com",
|
190
|
+
"http://www.playstation.com",
|
191
|
+
"http://www.uefa.com",
|
192
|
+
"http://www.ieee.org",
|
193
|
+
"http://www.apple.com",
|
194
|
+
"http://www.sony.com",
|
195
|
+
"http://www.symantec.com",
|
196
|
+
"http://www.zdnet.com",
|
197
|
+
"http://www.fujitsu.com",
|
198
|
+
"http://www.supermicro.com",
|
199
|
+
"http://www.hotmail.com",
|
200
|
+
"http://www.ecma.com",
|
201
|
+
"http://www.bbc.co.uk",
|
202
|
+
"http://news.google.com",
|
203
|
+
"http://www.foxnews.com",
|
204
|
+
"http://www.msn.com",
|
205
|
+
"http://www.wired.com",
|
206
|
+
"http://www.sky.com",
|
207
|
+
"http://www.usatoday.com",
|
208
|
+
"http://www.cbs.com",
|
209
|
+
"http://www.nbc.com",
|
210
|
+
"http://slashdot.org",
|
211
|
+
"http://www.bloglines.com",
|
212
|
+
"http://www.techweb.com",
|
213
|
+
"http://www.newslink.org" ].each do |url|
|
214
|
+
e = CurlFFI::Easy.new
|
215
|
+
e.setopt(:PROXY, "")
|
216
|
+
e.setopt(:URL, url)
|
217
|
+
e.setopt(:NOPROGRESS, 0)
|
218
|
+
e.setopt(:PROGRESSFUNCTION, PROGRESS_CALLBACK)
|
219
|
+
e.setopt(:WRITEFUNCTION, WRITE_CALLBACK)
|
220
|
+
e.setopt(:PROGRESSDATA, url)
|
221
|
+
$multi.add_handle(e)
|
222
|
+
end
|
223
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
require File.expand_path("../lib/curl", File.dirname(__FILE__))
|
4
|
+
|
5
|
+
multi = CurlFFI::Multi.new
|
6
|
+
|
7
|
+
e = CurlFFI::Easy.new
|
8
|
+
e.setopt(:PROXY, "")
|
9
|
+
e.setopt(:URL, "http://www.un.org")
|
10
|
+
|
11
|
+
multi.add_handle(e)
|
12
|
+
|
13
|
+
|
14
|
+
e = CurlFFI::Easy.new
|
15
|
+
e.setopt(:PROXY, "")
|
16
|
+
e.setopt(:URL, "http://www.google.com")
|
17
|
+
|
18
|
+
multi.add_handle(e)
|
19
|
+
|
20
|
+
begin
|
21
|
+
multi.perform
|
22
|
+
end while multi.running != 0
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# Shows how to use the libcurl-ffi interface in combination with eventmachine.
|
2
|
+
# Inspired by the different hiperfifo examples on the libcurl site.
|
3
|
+
require "rubygems"
|
4
|
+
require "benchmark"
|
5
|
+
require "eventmachine"
|
6
|
+
|
7
|
+
require File.expand_path("../lib/curl", File.dirname(__FILE__))
|
8
|
+
|
9
|
+
if FFI::Platform.windows?
|
10
|
+
# Sockets returned by curl are WinSock SOCKETs
|
11
|
+
# As we want to wrap these sockets in Ruby's IO interface (using IO.for_fd),
|
12
|
+
# we have to first get the SOCKET's filehandle using _get_osfhandle.
|
13
|
+
#
|
14
|
+
# To later get the SOCKET again, we can use _open_osfhandle again.
|
15
|
+
module WinSock
|
16
|
+
extend FFI::Library
|
17
|
+
ffi_lib FFI::Library::LIBC
|
18
|
+
attach_function :_get_osfhandle, [:int], :long
|
19
|
+
attach_function :_open_osfhandle, [:long, :int], :int
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_socket(io)
|
23
|
+
WinSock._get_osfhandle(io.fileno)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_io(socket)
|
27
|
+
FFI::IO.for_fd(WinSock._open_osfhandle(socket, 0), "r")
|
28
|
+
end
|
29
|
+
else
|
30
|
+
def get_socket(io)
|
31
|
+
io.fileno
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_io(socket)
|
35
|
+
IO.for_fd(socket, "r")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
$sockets = { }
|
40
|
+
|
41
|
+
def addsock(socket, easy, action)
|
42
|
+
io = get_io(socket)
|
43
|
+
$sockets[socket] = {
|
44
|
+
:io => io,
|
45
|
+
:action => action,
|
46
|
+
}
|
47
|
+
setsock(socket, easy, action)
|
48
|
+
end
|
49
|
+
|
50
|
+
def setsock(socket, easy, action)
|
51
|
+
$sockets[socket][:action] = action
|
52
|
+
end
|
53
|
+
|
54
|
+
def remsock(socket)
|
55
|
+
puts "Removing Socket #{socket}"
|
56
|
+
$sockets.delete(socket)
|
57
|
+
end
|
58
|
+
|
59
|
+
def progress_callback(url_pointer, dltotal, dlnow, ultotal, ulnow)
|
60
|
+
puts "[#{Time.now}]Progress: %s (%g/%g)\n" % [url_pointer.read_string, dlnow, dltotal]
|
61
|
+
return 0
|
62
|
+
end
|
63
|
+
|
64
|
+
def write_callback(easy, size, nmemb, data)
|
65
|
+
realsize = size * nmemb
|
66
|
+
return realsize
|
67
|
+
end
|
68
|
+
|
69
|
+
def mcode_or_die(where, code)
|
70
|
+
return if code == :OK
|
71
|
+
puts "ERROR: %s returns %s\n" % [where, code]
|
72
|
+
exit unless code == :BAD_SOCKET
|
73
|
+
end
|
74
|
+
|
75
|
+
PROGRESS_CALLBACK = FFI::Function.new(:int, [:pointer, :double, :double, :double, :double], &self.method(:progress_callback))
|
76
|
+
WRITE_CALLBACK = FFI::Function.new(:size_t, [:pointer, :size_t, :size_t, :pointer], &self.method(:write_callback))
|
77
|
+
|
78
|
+
sock_callback = FFI::Function.new(:int, [:pointer, :int, :int]) do |easy_ptr, socket, what|
|
79
|
+
whatstr = [ "none", "IN", "OUT", "INOUT", "REMOVE" ]
|
80
|
+
puts("socket callback: s=%d e=%p what=%s " % [socket, easy_ptr, whatstr[what]])
|
81
|
+
|
82
|
+
if what == 4
|
83
|
+
remsock(socket)
|
84
|
+
puts ""
|
85
|
+
else
|
86
|
+
if $sockets[socket].nil?
|
87
|
+
puts "Adding data: %s\n" % whatstr[what]
|
88
|
+
addsock(socket, easy_ptr, what)
|
89
|
+
else
|
90
|
+
puts "Changing action from %s to %s\n" % [whatstr[$sockets[socket][:action]], whatstr[what]]
|
91
|
+
setsock(socket, easy_ptr, what)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
0
|
96
|
+
end
|
97
|
+
|
98
|
+
multi_timer_callback = FFI::Function.new(:int, [:pointer, :long]) do |multi_ptr, timeout_ms|
|
99
|
+
puts "multi_timer_cb: Setting timeout to %d ms\n" % timeout_ms
|
100
|
+
|
101
|
+
$timeout = timeout_ms
|
102
|
+
|
103
|
+
0
|
104
|
+
end
|
105
|
+
|
106
|
+
$prev_running = 0
|
107
|
+
def check_run_count
|
108
|
+
if $prev_running > $multi.running
|
109
|
+
puts "REMAINING: %d\n" % $multi.running
|
110
|
+
|
111
|
+
while message = $multi.info_read_next
|
112
|
+
if message[:msg] == :DONE
|
113
|
+
puts "Done!"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
$prev_running = $multi.running
|
118
|
+
end
|
119
|
+
|
120
|
+
$timeout = 0
|
121
|
+
$multi = CurlFFI::Multi.new
|
122
|
+
$multi.setopt(:SOCKETFUNCTION, sock_callback)
|
123
|
+
$multi.setopt(:TIMERFUNCTION, multi_timer_callback)
|
124
|
+
|
125
|
+
[ "http://www.microsoft.com",
|
126
|
+
"http://www.opensource.org",
|
127
|
+
"http://www.google.com",
|
128
|
+
"http://www.yahoo.com",
|
129
|
+
"http://www.ibm.com",
|
130
|
+
"http://www.mysql.com",
|
131
|
+
"http://www.oracle.com",
|
132
|
+
"http://www.ripe.net",
|
133
|
+
"http://www.iana.org",
|
134
|
+
"http://www.amazon.com",
|
135
|
+
"http://www.netcraft.com",
|
136
|
+
"http://www.heise.de",
|
137
|
+
"http://www.chip.de",
|
138
|
+
"http://www.ca.com",
|
139
|
+
"http://www.cnet.com",
|
140
|
+
"http://www.news.com",
|
141
|
+
"http://www.cnn.com",
|
142
|
+
"http://www.wikipedia.org",
|
143
|
+
"http://www.dell.com",
|
144
|
+
"http://www.hp.com",
|
145
|
+
"http://www.cert.org",
|
146
|
+
"http://www.mit.edu",
|
147
|
+
"http://www.nist.gov",
|
148
|
+
"http://www.ebay.com",
|
149
|
+
"http://www.playstation.com",
|
150
|
+
"http://www.uefa.com",
|
151
|
+
"http://www.ieee.org",
|
152
|
+
"http://www.apple.com",
|
153
|
+
"http://www.sony.com",
|
154
|
+
"http://www.symantec.com",
|
155
|
+
"http://www.zdnet.com",
|
156
|
+
"http://www.fujitsu.com",
|
157
|
+
"http://www.supermicro.com",
|
158
|
+
"http://www.hotmail.com",
|
159
|
+
"http://www.ecma.com",
|
160
|
+
"http://www.bbc.co.uk",
|
161
|
+
"http://news.google.com",
|
162
|
+
"http://www.foxnews.com",
|
163
|
+
"http://www.msn.com",
|
164
|
+
"http://www.wired.com",
|
165
|
+
"http://www.sky.com",
|
166
|
+
"http://www.usatoday.com",
|
167
|
+
"http://www.cbs.com",
|
168
|
+
"http://www.nbc.com",
|
169
|
+
"http://slashdot.org",
|
170
|
+
"http://www.bloglines.com",
|
171
|
+
"http://www.techweb.com",
|
172
|
+
"http://www.newslink.org" ].each do |url|
|
173
|
+
e = CurlFFI::Easy.new
|
174
|
+
e.setopt(:PROXY, "")
|
175
|
+
e.setopt(:URL, url)
|
176
|
+
e.setopt(:NOPROGRESS, 0)
|
177
|
+
e.setopt(:PROGRESSFUNCTION, PROGRESS_CALLBACK)
|
178
|
+
e.setopt(:WRITEFUNCTION, WRITE_CALLBACK)
|
179
|
+
e.setopt(:PROGRESSDATA, url)
|
180
|
+
$multi.add_handle(e)
|
181
|
+
end
|
182
|
+
|
183
|
+
begin
|
184
|
+
to_read = $sockets.select { |k, v| v[:action] & 1 != 0 }.map { |x| x[1][:io] }
|
185
|
+
to_write = $sockets.select { |k, v| v[:action] & 2 != 0 }.map { |x| x[1][:io] }
|
186
|
+
|
187
|
+
read, write, err = IO.select(to_read, to_write, [], $timeout / 1000.0)
|
188
|
+
|
189
|
+
if read
|
190
|
+
read.each do |io|
|
191
|
+
begin
|
192
|
+
rc = $multi.socket_action(get_socket(io), 1)
|
193
|
+
end while rc == :CALL_MULTI_PERFORM
|
194
|
+
mcode_or_die("event_cb: curl_multi_socket", rc)
|
195
|
+
check_run_count
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
if write
|
200
|
+
write.each do |io|
|
201
|
+
begin
|
202
|
+
rc = $multi.socket_action(get_socket(io), 2)
|
203
|
+
end while rc == :CALL_MULTI_PERFORM
|
204
|
+
mcode_or_die("event_cb: curl_multi_socket", rc)
|
205
|
+
check_run_count
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
if !read && !write
|
210
|
+
puts "!!! Socket timeout"
|
211
|
+
begin
|
212
|
+
rc = $multi.socket_action(CurlFFI::SOCKET_TIMEOUT, 0)
|
213
|
+
puts rc
|
214
|
+
end while rc == :CALL_MULTI_PERFORM
|
215
|
+
mcode_or_die("timer_cb: curl_multi_socket_action", rc)
|
216
|
+
check_run_count
|
217
|
+
end
|
218
|
+
|
219
|
+
end while $multi.running > 0
|