ethon 0.18.0 → 0.19.1

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +705 -2
  3. data/README.md +183 -40
  4. data/lib/ethon/curl.rb +22 -0
  5. data/lib/ethon/curls/classes.rb +284 -12
  6. data/lib/ethon/curls/codes.rb +28 -9
  7. data/lib/ethon/curls/constants.rb +61 -4
  8. data/lib/ethon/curls/form_options.rb +2 -1
  9. data/lib/ethon/curls/functions.rb +60 -11
  10. data/lib/ethon/curls/infos.rb +104 -5
  11. data/lib/ethon/curls/introspection.rb +139 -0
  12. data/lib/ethon/curls/introspection_constants.rb +42 -0
  13. data/lib/ethon/curls/mime.rb +187 -0
  14. data/lib/ethon/curls/optional.rb +14 -0
  15. data/lib/ethon/curls/options.rb +298 -71
  16. data/lib/ethon/curls/settings.rb +12 -3
  17. data/lib/ethon/curls/share.rb +165 -0
  18. data/lib/ethon/curls/share_constants.rb +47 -0
  19. data/lib/ethon/curls/url.rb +171 -0
  20. data/lib/ethon/curls/url_constants.rb +78 -0
  21. data/lib/ethon/easy/callbacks.rb +177 -51
  22. data/lib/ethon/easy/features.rb +4 -1
  23. data/lib/ethon/easy/files.rb +113 -0
  24. data/lib/ethon/easy/form.rb +65 -6
  25. data/lib/ethon/easy/header.rb +57 -15
  26. data/lib/ethon/easy/headers_api.rb +79 -0
  27. data/lib/ethon/easy/http/actionable.rb +25 -3
  28. data/lib/ethon/easy/http/get.rb +5 -1
  29. data/lib/ethon/easy/http/post.rb +1 -0
  30. data/lib/ethon/easy/http/postable.rb +9 -2
  31. data/lib/ethon/easy/http/putable.rb +19 -4
  32. data/lib/ethon/easy/http.rb +16 -4
  33. data/lib/ethon/easy/informations.rb +51 -2
  34. data/lib/ethon/easy/mime.rb +29 -0
  35. data/lib/ethon/easy/mirror.rb +17 -4
  36. data/lib/ethon/easy/operations.rb +264 -11
  37. data/lib/ethon/easy/options.rb +327 -8
  38. data/lib/ethon/easy/queryable.rb +59 -14
  39. data/lib/ethon/easy/response_callbacks.rb +33 -12
  40. data/lib/ethon/easy/share.rb +28 -0
  41. data/lib/ethon/easy/url.rb +32 -0
  42. data/lib/ethon/easy/util.rb +19 -0
  43. data/lib/ethon/easy.rb +185 -10
  44. data/lib/ethon/errors/fork_safety.rb +10 -0
  45. data/lib/ethon/errors/form_add.rb +12 -0
  46. data/lib/ethon/errors/global_init.rb +1 -1
  47. data/lib/ethon/errors/header.rb +12 -0
  48. data/lib/ethon/errors/invalid_option.rb +3 -1
  49. data/lib/ethon/errors/invalid_value.rb +3 -1
  50. data/lib/ethon/errors/mime.rb +17 -0
  51. data/lib/ethon/errors/multi_add.rb +1 -1
  52. data/lib/ethon/errors/multi_assign.rb +12 -0
  53. data/lib/ethon/errors/multi_fdset.rb +1 -1
  54. data/lib/ethon/errors/multi_perform.rb +12 -0
  55. data/lib/ethon/errors/multi_poll.rb +12 -0
  56. data/lib/ethon/errors/multi_remove.rb +1 -1
  57. data/lib/ethon/errors/multi_timeout.rb +2 -2
  58. data/lib/ethon/errors/multi_wait.rb +12 -0
  59. data/lib/ethon/errors/multi_wakeup.rb +12 -0
  60. data/lib/ethon/errors/select.rb +1 -1
  61. data/lib/ethon/errors/set_option.rb +27 -0
  62. data/lib/ethon/errors/share.rb +17 -0
  63. data/lib/ethon/errors/unsupported_option.rb +23 -0
  64. data/lib/ethon/errors/url.rb +17 -0
  65. data/lib/ethon/errors.rb +13 -0
  66. data/lib/ethon/libc.rb +30 -1
  67. data/lib/ethon/loggable.rb +1 -1
  68. data/lib/ethon/multi/operations.rb +269 -25
  69. data/lib/ethon/multi/options.rb +114 -12
  70. data/lib/ethon/multi/stack.rb +181 -10
  71. data/lib/ethon/multi.rb +51 -15
  72. data/lib/ethon/version.rb +1 -1
  73. data/lib/ethon.rb +8 -9
  74. metadata +35 -6
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when curl_multi_poll fails.
6
+ class MultiPoll < EthonError
7
+ def initialize(code)
8
+ super("An error occurred polling the multi: #{code}")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -5,7 +5,7 @@ module Ethon
5
5
  # Raises when multi_remove_handle failed.
6
6
  class MultiRemove < EthonError
7
7
  def initialize(code, easy)
8
- super("An error occured removing the easy handle: #{easy} from the multi: #{code}")
8
+ super("An error occurred removing the easy handle: #{easy} from the multi: #{code}")
9
9
  end
10
10
  end
11
11
  end
@@ -5,8 +5,8 @@ module Ethon
5
5
  # Raised when multi_timeout failed.
6
6
  class MultiTimeout < EthonError
7
7
  def initialize(code)
8
- super("An error occured getting the timeout: #{code}")
9
- # "An error occured getting the timeout: #{code}: #{Curl.multi_strerror(code)}"
8
+ super("An error occurred getting the timeout: #{code}")
9
+ # "An error occurred getting the timeout: #{code}: #{Curl.multi_strerror(code)}"
10
10
  end
11
11
  end
12
12
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when curl_multi_wait fails.
6
+ class MultiWait < EthonError
7
+ def initialize(code)
8
+ super("An error occurred waiting on the multi: #{code}")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when curl_multi_wakeup fails.
6
+ class MultiWakeup < EthonError
7
+ def initialize(code)
8
+ super("An error occurred waking up the multi: #{code}")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -5,7 +5,7 @@ module Ethon
5
5
  # Raised when select failed.
6
6
  class Select < EthonError
7
7
  def initialize(errno)
8
- super("An error occured on select: #{errno}")
8
+ super("An error occurred on select: #{errno}")
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when libcurl rejects an option value.
6
+ class SetOption < EthonError
7
+ def initialize(option, code, type = :easy)
8
+ message = begin
9
+ strerror(type, code)
10
+ rescue StandardError
11
+ code.to_s
12
+ end
13
+ # Inspect so control characters in an untrusted option name cannot
14
+ # inject lines into logs that record the message.
15
+ super("libcurl failed to set the option: #{option.inspect}: #{message} (#{code})")
16
+ end
17
+
18
+ private
19
+
20
+ # Multi and easy codes overlap, so decode with the matching strerror.
21
+ def strerror(type, code)
22
+ func = type == :multi ? :multi_strerror : :easy_strerror
23
+ Ethon::Curl.respond_to?(func) ? Ethon::Curl.public_send(func, code) : code.to_s
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when a curl share API call fails.
6
+ class Share < EthonError
7
+ def initialize(code)
8
+ message = if Ethon::Curl.respond_to?(:share_strerror)
9
+ Ethon::Curl.share_strerror(code)
10
+ else
11
+ code.to_s
12
+ end
13
+ super("An error occurred handling the share: #{message}")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when libcurl does not recognize an option.
6
+ #
7
+ # This happens when Ethon knows an option that the loaded libcurl is too
8
+ # old to support, for example an option added in a newer libcurl release.
9
+ class UnsupportedOption < EthonError
10
+ def initialize(option)
11
+ version = begin
12
+ Ethon::Curl.version_info[:version]
13
+ rescue StandardError
14
+ nil
15
+ end
16
+ version = Ethon::Curl.version if version.nil?
17
+ # Inspect so control characters in an untrusted option name cannot
18
+ # inject lines into logs that record the message.
19
+ super("libcurl #{version} does not support the option: #{option.inspect}")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ module Ethon
3
+ module Errors
4
+
5
+ # Raised when a curl URL API call fails.
6
+ class Url < EthonError
7
+ def initialize(code)
8
+ message = if Ethon::Curl.respond_to?(:url_strerror)
9
+ Ethon::Curl.url_strerror(code)
10
+ else
11
+ code.to_s
12
+ end
13
+ super("An error occurred handling the URL: #{message}")
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/ethon/errors.rb CHANGED
@@ -1,13 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
  require 'ethon/errors/ethon_error'
3
+ require 'ethon/errors/url'
3
4
  require 'ethon/errors/global_init'
5
+ require 'ethon/errors/form_add'
4
6
  require 'ethon/errors/multi_timeout'
7
+ require 'ethon/errors/multi_assign'
5
8
  require 'ethon/errors/multi_fdset'
9
+ require 'ethon/errors/share'
6
10
  require 'ethon/errors/multi_add'
7
11
  require 'ethon/errors/multi_remove'
12
+ require 'ethon/errors/header'
13
+ require 'ethon/errors/multi_perform'
8
14
  require 'ethon/errors/select'
9
15
  require 'ethon/errors/invalid_option'
10
16
  require 'ethon/errors/invalid_value'
17
+ require 'ethon/errors/set_option'
18
+ require 'ethon/errors/unsupported_option'
19
+ require 'ethon/errors/mime'
20
+ require 'ethon/errors/fork_safety'
21
+ require 'ethon/errors/multi_wait'
22
+ require 'ethon/errors/multi_poll'
23
+ require 'ethon/errors/multi_wakeup'
11
24
 
12
25
  module Ethon
13
26
 
data/lib/ethon/libc.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'etc'
3
+
2
4
  module Ethon
3
5
 
4
6
  # FFI Wrapper module for Libc.
@@ -13,8 +15,35 @@ module Ethon
13
15
  Gem.win_platform?
14
16
  end
15
17
 
18
+ def self.open_max
19
+ value = Etc.sysconf(Etc::SC_OPEN_MAX)
20
+ return value if value && value > 0
21
+
22
+ Process.getrlimit(:NOFILE).first
23
+ rescue NameError, NotImplementedError
24
+ Process.getrlimit(:NOFILE).first
25
+ end
26
+
27
+ # Size of the native fd_set, rounded up to whole longs. An unaligned
28
+ # ETHON_FD_SIZE would make select write past the Ruby-side struct.
29
+ def self.fd_size(value = ENV['ETHON_FD_SIZE'])
30
+ value = value.to_s
31
+ size = if value.empty?
32
+ [open_max, 65_536].min
33
+ else
34
+ begin
35
+ Integer(value, 10)
36
+ rescue ArgumentError, TypeError
37
+ raise ArgumentError, "ETHON_FD_SIZE must be a positive integer"
38
+ end
39
+ end
40
+ raise ArgumentError, "ETHON_FD_SIZE must be a positive integer" unless size > 0
41
+
42
+ word = FFI::Type::LONG.size
43
+ ((size + word - 1) / word) * word
44
+ end
45
+
16
46
  unless windows?
17
- attach_function :getdtablesize, [], :int
18
47
  attach_function :free, [:pointer], :void
19
48
  end
20
49
  end
@@ -14,7 +14,7 @@ module Ethon
14
14
  #
15
15
  # @return [ Logger ] The logger.
16
16
  def logger
17
- return @logger if defined?(@logger)
17
+ return @logger if defined?(@logger) && @logger
18
18
  @logger = rails_logger || default_logger
19
19
  end
20
20
 
@@ -14,7 +14,68 @@ module Ethon
14
14
  #
15
15
  # @return [ FFI::Pointer ] The multi handle.
16
16
  def handle
17
- @handle ||= FFI::AutoPointer.new(Curl.multi_init, Curl.method(:multi_cleanup))
17
+ ensure_not_cleaned_up!
18
+
19
+ handle = @handle ||= begin
20
+ # Enforce fork safety before creating a handle for a Multi that was
21
+ # constructed before the fork but never touched.
22
+ Curl.check_pid!
23
+ pointer = Curl.multi_init
24
+ raise NoMemoryError, "curl_multi_init failed" if pointer.null?
25
+ Curl::MultiHandle.new(pointer)
26
+ end
27
+ handle.check_pid!
28
+ handle
29
+ end
30
+
31
+ # Clean up the multi.
32
+ #
33
+ # Removes all attached easy handles before freeing the native multi
34
+ # handle. Cleanup is terminal: the Multi cannot be used afterwards.
35
+ #
36
+ # @return [ nil ]
37
+ def cleanup
38
+ ensure_not_cleaned_up!
39
+ ensure_not_executing!
40
+ # A handle created before a fork must not be released in the child.
41
+ @handle.check_pid! if @handle
42
+
43
+ error = nil
44
+ begin
45
+ stack.dup.each do |easy|
46
+ begin
47
+ delete(easy)
48
+ rescue StandardError => e
49
+ # Keep detaching the other handles. A failed close or native
50
+ # detach must not strand every remaining handle on a Multi whose
51
+ # native handle is about to be freed.
52
+ error ||= e
53
+ end
54
+ end
55
+ ensure
56
+ # Cleanup is terminal. Release the native handle even when detaching
57
+ # one of the easy handles fails. Handles that could not be detached
58
+ # keep their multi ownership so they cannot be reused unsafely.
59
+ begin
60
+ @handle.free if @handle
61
+ ensure
62
+ stack.each do |easy|
63
+ begin
64
+ easy.finish_response_body_io
65
+ rescue StandardError
66
+ # Cleanup is best effort; keep tearing down the multi even if
67
+ # closing a streamed response fails.
68
+ end
69
+ end
70
+ stack.clear
71
+ @handle = nil
72
+ @running_count = nil
73
+ @assigned_sockets = nil
74
+ @procs = nil
75
+ @cleaned_up = true
76
+ end
77
+ end
78
+ raise error if error
18
79
  end
19
80
 
20
81
  # Initialize variables.
@@ -26,13 +87,26 @@ module Ethon
26
87
  def init_vars
27
88
  if @execution_mode == :perform
28
89
  @timeout = ::FFI::MemoryPointer.new(:long)
29
- @timeval = Curl::Timeval.new
30
- @fd_read = Curl::FDSet.new
31
- @fd_write = Curl::FDSet.new
32
- @fd_excep = Curl::FDSet.new
33
- @max_fd = ::FFI::MemoryPointer.new(:int)
90
+ @running_count_pointer = ::FFI::MemoryPointer.new(:int)
91
+ @msgs_left = ::FFI::MemoryPointer.new(:int)
92
+ @wait_strategy = if Curl.respond_to?(:multi_poll)
93
+ :poll
94
+ elsif Curl.respond_to?(:multi_wait)
95
+ :wait
96
+ else
97
+ :select
98
+ end
99
+
100
+ if @wait_strategy == :select
101
+ @timeval = Curl::Timeval.new
102
+ @fd_read = Curl::FDSet.new
103
+ @fd_write = Curl::FDSet.new
104
+ @fd_excep = Curl::FDSet.new
105
+ @max_fd = ::FFI::MemoryPointer.new(:int)
106
+ end
34
107
  elsif @execution_mode == :socket_action
35
108
  @running_count_pointer = FFI::MemoryPointer.new(:int)
109
+ @msgs_left = FFI::MemoryPointer.new(:int)
36
110
  end
37
111
  end
38
112
 
@@ -44,14 +118,28 @@ module Ethon
44
118
  # multi.perform
45
119
  def perform
46
120
  ensure_execution_mode(:perform)
121
+ ensure_not_cleaned_up!
122
+ ensure_not_executing!
47
123
 
48
124
  Ethon.logger.debug(STARTED_MULTI)
49
- while ongoing?
50
- run
51
- timeout = get_timeout
52
- next if timeout == 0
53
- reset_fds
54
- set_fds(timeout)
125
+ @executing = true
126
+ begin
127
+ while ongoing?
128
+ run
129
+ timeout = get_timeout
130
+ next if timeout == 0
131
+ case @wait_strategy
132
+ when :poll
133
+ poll(timeout)
134
+ when :wait
135
+ wait(timeout)
136
+ else
137
+ reset_fds
138
+ set_fds(timeout)
139
+ end
140
+ end
141
+ ensure
142
+ @executing = false
55
143
  end
56
144
  Ethon.logger.debug(PERFORMED_MULTI)
57
145
  nil
@@ -67,12 +155,63 @@ module Ethon
67
155
  # @deprecated It is no longer necessary to call prepare.
68
156
  def prepare
69
157
  Ethon.logger.warn(
70
- "ETHON: It is no longer necessay to call "+
71
- "Multi#prepare. Its going to be removed "+
158
+ "ETHON: It is no longer necessary to call "+
159
+ "Multi#prepare. It's going to be removed "+
72
160
  "in future versions."
73
161
  )
74
162
  end
75
163
 
164
+ # Wait for activity on the multi's sockets.
165
+ #
166
+ # @example Wait up to one second.
167
+ # multi.wait(1000)
168
+ #
169
+ # @param [ Integer ] timeout_ms Maximum time to wait in milliseconds.
170
+ #
171
+ # @return [ Integer ] Number of file descriptors with activity.
172
+ def wait(timeout_ms = 1000)
173
+ unless Curl.respond_to?(:multi_wait)
174
+ raise NotImplementedError, "curl_multi_wait is not supported by this libcurl"
175
+ end
176
+
177
+ numfds = (@numfds ||= ::FFI::MemoryPointer.new(:int))
178
+ code = Curl.multi_wait(handle, ::FFI::Pointer::NULL, 0, timeout_ms, numfds)
179
+ raise Errors::MultiWait.new(code) unless code == :ok
180
+
181
+ numfds.read_int
182
+ end
183
+
184
+ # Wait for activity using curl_multi_poll.
185
+ #
186
+ # @param [ Integer ] timeout_ms Maximum time to wait in milliseconds.
187
+ #
188
+ # @return [ Integer ] Number of file descriptors with activity.
189
+ def poll(timeout_ms = 1000)
190
+ unless Curl.respond_to?(:multi_poll)
191
+ raise NotImplementedError, "curl_multi_poll is not supported by this libcurl"
192
+ end
193
+
194
+ numfds = (@numfds ||= ::FFI::MemoryPointer.new(:int))
195
+ code = Curl.multi_poll(handle, ::FFI::Pointer::NULL, 0, timeout_ms, numfds)
196
+ raise Errors::MultiPoll.new(code) unless code == :ok
197
+
198
+ numfds.read_int
199
+ end
200
+
201
+ # Wake up a multi handle blocked in curl_multi_poll.
202
+ #
203
+ # @return [ Symbol ] The Curl multi return code.
204
+ def wakeup
205
+ unless Curl.respond_to?(:multi_wakeup)
206
+ raise NotImplementedError, "curl_multi_wakeup is not supported by this libcurl"
207
+ end
208
+
209
+ code = Curl.multi_wakeup(handle)
210
+ raise Errors::MultiWakeup.new(code) unless code == :ok
211
+
212
+ code
213
+ end
214
+
76
215
  # Continue execution with an external IO loop.
77
216
  #
78
217
  # @example When no sockets are ready yet, or to begin.
@@ -84,9 +223,16 @@ module Ethon
84
223
  # @example When a socket is readable and writable
85
224
  # multi.socket_action(io_object, [:in, :out])
86
225
  #
226
+ # @param [ IO, Integer, nil ] io The socket to act on, or nil for the
227
+ # timeout case.
228
+ # @param [ Integer, Symbol, Array ] readiness CURL_CSELECT_* readiness
229
+ # bits, either as a number or as symbols such as :in and :out.
230
+ #
87
231
  # @return [ Symbol ] The Curl.multi_socket_action return code.
88
232
  def socket_action(io = nil, readiness = 0)
89
233
  ensure_execution_mode(:socket_action)
234
+ ensure_not_cleaned_up!
235
+ ensure_not_executing!
90
236
 
91
237
  fd = if io.nil?
92
238
  ::Ethon::Curl::SOCKET_TIMEOUT
@@ -96,12 +242,17 @@ module Ethon
96
242
  io.fileno
97
243
  end
98
244
 
99
- code = Curl.multi_socket_action(handle, fd, readiness, @running_count_pointer)
100
- @running_count = @running_count_pointer.read_int
245
+ @executing = true
246
+ begin
247
+ code = Curl.multi_socket_action(handle, fd, readiness, @running_count_pointer)
248
+ @running_count = @running_count_pointer.read_int
101
249
 
102
- check
250
+ check
103
251
 
104
- code
252
+ code
253
+ ensure
254
+ @executing = false
255
+ end
105
256
  end
106
257
 
107
258
  # Return whether the multi still contains requests or not.
@@ -111,11 +262,86 @@ module Ethon
111
262
  #
112
263
  # @return [ Boolean ] True if ongoing, else false.
113
264
  def ongoing?
114
- easy_handles.size > 0 || (!defined?(@running_count) || running_count > 0)
265
+ stack.size > 0 || (running_count || 0) > 0
266
+ end
267
+
268
+ # Associate an arbitrary pointer with a socket in the multi handle.
269
+ #
270
+ # @param [ Integer, IO ] io The socket descriptor.
271
+ # @param [ FFI::Pointer, nil ] pointer The pointer to associate, or nil
272
+ # to clear it.
273
+ #
274
+ # @return [ Symbol ] The Curl multi return code.
275
+ def assign(io, pointer)
276
+ unless pointer.nil? || pointer.is_a?(::FFI::Pointer)
277
+ raise Errors::InvalidValue.new(:assign, pointer)
278
+ end
279
+ # A freed wrapper still looks like a pointer, but libcurl would keep
280
+ # the dangling address.
281
+ if pointer.is_a?(Ethon::Curl::OwnedPointer)
282
+ pointer.check_pid!
283
+ end
284
+
285
+ socket = io.respond_to?(:fileno) ? io.fileno : io
286
+ # FFI would reject a non-integer descriptor with a TypeError instead
287
+ # of the error Ethon uses for invalid option values.
288
+ unless socket.is_a?(Integer)
289
+ raise Errors::InvalidValue.new(:assign, io)
290
+ end
291
+ code = Curl.multi_assign(handle, socket, pointer)
292
+ raise Errors::MultiAssign.new(code) unless code == :ok
293
+
294
+ # libcurl keeps the pointer until the socket is removed or assigned
295
+ # again, so keep it alive from Ruby as well.
296
+ if pointer.nil? || pointer.null?
297
+ (@assigned_sockets ||= {}).delete(socket)
298
+ else
299
+ (@assigned_sockets ||= {})[socket] = pointer
300
+ end
301
+
302
+ code
303
+ end
304
+
305
+ # Return the easy handles currently attached natively.
306
+ #
307
+ # @return [ Array<FFI::Pointer> ] Native easy handles.
308
+ def native_handles
309
+ unless Curl.respond_to?(:multi_get_handles)
310
+ raise NotImplementedError, "curl_multi_get_handles is not supported by this libcurl"
311
+ end
312
+
313
+ pointer = Curl.multi_get_handles(handle)
314
+ raise NoMemoryError, "curl_multi_get_handles failed" if pointer.null?
315
+
316
+ begin
317
+ handles = []
318
+ offset = 0
319
+ loop do
320
+ easy = pointer.get_pointer(offset)
321
+ break if easy.null?
322
+
323
+ handles << easy
324
+ offset += ::FFI.type_size(:pointer)
325
+ end
326
+ handles
327
+ ensure
328
+ Curl.free(pointer)
329
+ end
115
330
  end
116
331
 
117
332
  private
118
333
 
334
+ def ensure_not_cleaned_up!
335
+ return unless @cleaned_up
336
+ raise Errors::EthonError, "Multi has been cleaned up"
337
+ end
338
+
339
+ def ensure_not_executing!
340
+ return unless @executing
341
+
342
+ raise Errors::EthonError, "Multi is currently executing; wait for it to finish"
343
+ end
344
+
119
345
  # Get timeout.
120
346
  #
121
347
  # @example Get timeout.
@@ -158,7 +384,8 @@ module Ethon
158
384
  raise Errors::MultiFdset.new(code) unless code == :ok
159
385
  max_fd = @max_fd.read_int
160
386
  if max_fd == -1
161
- sleep(0.001)
387
+ # No sockets are ready; wait for the timer instead of spinning.
388
+ sleep(timeout / 1000.0)
162
389
  else
163
390
  @timeval[:sec] = timeout / 1000
164
391
  @timeval[:usec] = (timeout * 1000) % 1000000
@@ -177,16 +404,31 @@ module Ethon
177
404
  #
178
405
  # @return [ void ]
179
406
  def check
180
- msgs_left = ::FFI::MemoryPointer.new(:int)
407
+ msgs_left = (@msgs_left ||= ::FFI::MemoryPointer.new(:int))
181
408
  while true
182
409
  msg = Curl.multi_info_read(handle, msgs_left)
183
410
  break if msg.null?
184
411
  next if msg[:code] != :done
185
- easy = easy_handles.find{ |e| e.handle == msg[:easy_handle] }
412
+ easy = stack.find{ |e| e.handle == msg[:easy_handle] }
413
+ # The handle may have been removed between the transfer finishing and
414
+ # the completion message being read.
415
+ next unless easy
186
416
  easy.return_code = msg[:data][:code]
187
417
  Ethon.logger.debug { "ETHON: performed #{easy.log_inspect}" }
188
- delete(easy)
418
+ # delete finishes the transfer's response stream. A failed close or
419
+ # flush must not keep on_complete from running; raise it afterwards.
420
+ teardown_error = nil
421
+ begin
422
+ delete(easy)
423
+ rescue Exception => exception
424
+ # A handle that libcurl still owns must stay tracked for cleanup.
425
+ raise if stack.include?(easy)
426
+
427
+ teardown_error = exception
428
+ end
189
429
  easy.complete
430
+ easy.raise_callback_error
431
+ raise teardown_error if teardown_error
190
432
  end
191
433
  end
192
434
 
@@ -197,8 +439,10 @@ module Ethon
197
439
  #
198
440
  # @return [ void ]
199
441
  def run
200
- running_count_pointer = FFI::MemoryPointer.new(:int)
442
+ running_count_pointer = (@running_count_pointer ||= ::FFI::MemoryPointer.new(:int))
201
443
  begin code = trigger(running_count_pointer) end while code == :call_multi_perform
444
+ raise Errors::MultiPerform.new(code) unless code == :ok
445
+
202
446
  check
203
447
  end
204
448
 
@@ -221,7 +465,7 @@ module Ethon
221
465
  #
222
466
  # @return [ Integer ] Number running requests.
223
467
  def running_count
224
- @running_count ||= nil
468
+ @running_count
225
469
  end
226
470
  end
227
471
  end