curl_ffi 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|