rack-webdav 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 56d779afdbd6da5c459d33219b508ed4b141a564
4
+ data.tar.gz: 1bba827edea7b0d09f7b22ecb013660966c23ee8
5
+ SHA512:
6
+ metadata.gz: 888809b2b3deebd298ab26810d980686df9cf912ef3f8d557de2e5dfd45a3d0a3fa110dcd8562c4c3c38e6df1137901128eba54021e431139352e1cf0ba1e954
7
+ data.tar.gz: b545e17b78a6ec5c5c8ff1ae9e6d7cb4e682c152ed1d484ca8b5dd92a6be6599f0b4b68bd23bb72e84ae8013724ea2d280f9800ccc329369eea3ee5762bb2e4a
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .*
2
+ !.gitignore
3
+ *.gem
4
+ *~
5
+ doc/*
6
+ pkg/*
7
+ test/repo
8
+ /vendor/bundle
data/LICENSE ADDED
@@ -0,0 +1,42 @@
1
+ Current RackWebDAV license:
2
+
3
+ Copyright (c) 2010 Chris Roberts <chrisroberts.code@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
23
+ Original rack_dav source license:
24
+
25
+ Copyright (c) 2009 Matthias Georgi <http://www.matthias-georgi.de>
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to
29
+ deal in the Software without restriction, including without limitation the
30
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
31
+ sell copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in
35
+ all copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
40
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
41
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
42
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,336 @@
1
+ = RackWebDAV - Web Authoring for Rack
2
+
3
+ RackWebDAV based by (dav4rack)[https://github.com/chrisroberts/dav4rack]
4
+
5
+ == Motivaion
6
+
7
+ * dav4rack no maintained
8
+ * I want to implment the following functions
9
+ * WebDAV RFC 3253 ( https://www.ietf.org/rfc/rfc3253.txt )
10
+ * because support Subversion (SVN)
11
+ * Rails 5 support
12
+ * Rack 2.0 support
13
+
14
+ == Current Feature
15
+
16
+ === From DAV4Rack
17
+ * Better resource support for building fully virtualized resource structures
18
+ * Generic locking as well as Resource level specific locking
19
+ * Interceptor middleware to provide virtual mapping to resources
20
+ * Mapped resource paths
21
+ * Authentication support
22
+ * Resource callbacks
23
+ * Remote file proxying (including sendfile support for remote files)
24
+
25
+ === RackWebDAV original
26
+ * Rack 2.0 support
27
+ * serveral bugfix
28
+
29
+ == Install
30
+ === RubyGems
31
+
32
+ gem install rack-webdav
33
+
34
+ == Documentation
35
+
36
+ * TODO
37
+
38
+ == Quickstart
39
+
40
+ If you just want to share a folder over WebDAV, you can just start a
41
+ simple server with:
42
+
43
+ rack-webdav
44
+
45
+ This will start a Unicorn, Mongrel or WEBrick server on port 3000, which you can connect
46
+ to without authentication. Unicorn and Mongrel will be much more responsive than WEBrick,
47
+ so if you are having slowness issues, install one of them and restart the rack-webdav process.
48
+ The simple file resource allows very basic authentication which is used for an example. To enable it:
49
+
50
+ rack-webdav --username=user --password=pass
51
+
52
+ == Rack Handler
53
+
54
+ Using RackWebDAV within a rack application is pretty simple. A very slim
55
+ rackup script would look something like this:
56
+
57
+ require 'rubygems'
58
+ require 'rack-webdav'
59
+
60
+ use Rack::CommonLogger
61
+ run RackWebDAV::Handler.new(:root => '/path/to/public/fileshare')
62
+
63
+ This will use the included FileResource and set the share path. However,
64
+ RackWebDAV has some nifty little extras that can be enabled in the rackup script. First,
65
+ an example of how to use a custom resource:
66
+
67
+ run RackWebDAV::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')
68
+
69
+ Next, lets venture into mapping a path for our WebDAV access. In this example, we
70
+ will use default FileResource like in the first example, but instead of the WebDAV content
71
+ being available at the root directory, we will map it to a specific directory: /webdav/share/
72
+
73
+ require 'rubygems'
74
+ require 'rack-webdav'
75
+
76
+ use Rack::CommonLogger
77
+
78
+ app = Rack::Builder.new{
79
+ map '/webdav/share/' do
80
+ run RackWebDAV::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
81
+ end
82
+ }.to_app
83
+ run app
84
+
85
+ Aside from the Builder#map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
86
+ RackWebDAV receives a request, it will automatically convert the request to the proper path and pass it to
87
+ the resource.
88
+
89
+ Another tool available when building the rackup script is the Interceptor. The Interceptor's job is to simply
90
+ intecept WebDAV requests received up the path hierarchy where no resources are currently mapped. For example,
91
+ lets continue with the last example but this time include the interceptor:
92
+
93
+ require 'rubygems'
94
+ require 'rack-webdav'
95
+
96
+ use Rack::CommonLogger
97
+ app = Rack::Builder.new{
98
+ map '/webdav/share/' do
99
+ run RackWebDAV::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
100
+ end
101
+ map '/webdav/share2/' do
102
+ run RackWebDAV::Handler.new(:resource_class => CustomResource, :root_uri_path => '/webdav/share2/')
103
+ end
104
+ map '/' do
105
+ use RackWebDAV::Interceptor, :mappings => {
106
+ '/webdav/share/' => {:resource_class => FileResource, :custom => 'option'},
107
+ '/webdav/share2/' => {:resource_class => CustomResource}
108
+ }
109
+ use Rails::Rack::Static
110
+ run ActionController::Dispatcher.new
111
+ end
112
+ }.to_app
113
+ run app
114
+
115
+ In this example we have two WebDAV resources restricted by path. This means those resources will handle requests to /webdav/share/*
116
+ and /webdav/share2/* but nothing above that. To allow webdav to respond, we provide the Interceptor. The Interceptor does not
117
+ provide any authentication support. It simply creates a virtual file system view to the provided mapped paths. Once the actual
118
+ resources have been reached, authentication will be enforced based on the requirements defined by the individual resource. Also
119
+ note in the root map you can see we are running a Rails application. This is how you can easily enable RackWebDAV with your Rails
120
+ application.
121
+
122
+ === Enabling Logging
123
+
124
+ RackWebDAV provides some simple logging in a Rails style format (simply for consistency) so the output should look some what familiar.
125
+
126
+ RackWebDAV::Handler.new(:resource_class => CustomResource, :log_to => '/my/log/file')
127
+
128
+ You can even specify the level of logging:
129
+
130
+ RackWebDAV::Handler.new(:resource_class => CustomResource, :log_to => ['/my/log/file', Logger::DEBUG])
131
+
132
+ == Custom Resources
133
+
134
+ Creating your own resource is easy. Simply inherit the RackWebDAV::Resource class, and start redefining all the methods
135
+ you want to customize. The RackWebDAV::Resource class only has implementations for methods that can be provided extremely
136
+ generically. This means that most things will require at least some sort of implementation. However, because the Resource
137
+ is defined so generically, and the Controller simply passes the request on to the Resource, it is easy to create fully
138
+ virtualized resources.
139
+
140
+ == Helpers
141
+
142
+ There are some helpers worth mentioning that make things a little easier. RackWebDAV::Resource#accept_redirect? method is available to Resources.
143
+ If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly
144
+ support this, which can be a real pain when working with virtualized files that may be located some where else, like S3. To deal with
145
+ those clients that don't support redirects, a helper has been provided so resources don't have to deal with proxying themselves. The RackWebDAV::RemoteFile
146
+ is a modified Rack::File that can do some interesting things. First, lets look at its most basic use:
147
+
148
+ class MyResource < RackWebDAV::Resource
149
+ def setup
150
+ @item = method_to_fill_this_properly
151
+ end
152
+
153
+ def get
154
+ if(accept_redirect?)
155
+ response.redirect item[:url]
156
+ else
157
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type)
158
+ OK
159
+ end
160
+ end
161
+ end
162
+
163
+ This is a simple proxy. When Rack receives the RemoteFile, it will pull a chunk of data from object, which in turn pulls it from the socket, and
164
+ sends it to the user over and over again until the EOF is reached. This much the same method that Rack::File uses but instead we are pulling
165
+ from a socket rather than an actual file. Now, instead of proxying these files from a remote server every time, lets cache them:
166
+
167
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp')
168
+
169
+ Providing the :cache_directory will let RemoteFile cache the items locally, and then search for them on subsequent requests before heading out
170
+ to the network. The cached file name is based off the SHA1 hash of the file path, size and last modified time. It is important to note that for
171
+ services like S3, the path will often change, making this cache pretty worthless. To combat this, we can provide a reference to use instead:
172
+
173
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :cache_directory => '/tmp', :cache_ref => item[:static_url])
174
+
175
+ These methods will work just fine, but it would be really nice to just let someone else deal with the proxying and let the process get back
176
+ to dealing with actual requests. RemoteFile will happily do that as long as the frontend server is setup correctly. Using the sendfile approach
177
+ will tell the RemoteFile to simply pass the headers on and let the server deal with doing the actual proxying. First, lets look at an implementation
178
+ using all the features, and then degrade that down to the bare minimum. These examples are NGINX specific, but are based off the Rack::Sendfile implementation
179
+ and as such should be applicable to other servers. First, a simplified NGINX server block:
180
+
181
+ server {
182
+ listen 80;
183
+ location / {
184
+ proxy_set_header X-Real-IP $remote_addr;
185
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
186
+ proxy_set_header Host $http_host;
187
+ proxy_set_header X-Sendfile-Type X-Accel-Redirect;
188
+ proxy_set_header X-Accel-Remote-Mapping webdav_redirect
189
+ proxy_pass http://my_app_server;
190
+ }
191
+
192
+ location ~* /webdav_redirect {
193
+ internal;
194
+ resolver 127.0.0.1;
195
+ set $r_host $upstream_http_redirect_host;
196
+ set $r_url $upstream_http_redirect_url;
197
+ proxy_set_header Authorization '';
198
+ proxy_set_header Host $r_host;
199
+ proxy_max_temp_file_size 0;
200
+ proxy_pass $r_url;
201
+ }
202
+ }
203
+
204
+ With this in place, the parameters for the RemoteFile change slightly:
205
+
206
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true)
207
+
208
+ The RemoteFile will automatically take care of building out the correct path and sending the proper headers. If the X-Accel-Remote-Mapping header
209
+ is not available, you can simply pass the value:
210
+
211
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => true, :sendfile_prefix => 'webdav_redirect')
212
+
213
+ And if you don't have the X-Sendfile-Type header set, you can fix that by changing the value of :sendfile:
214
+
215
+ response.body = RackWebDAV::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type, :sendfile => 'X-Accel-Redirect', :sendfile_prefix => 'webdav_redirect')
216
+
217
+ And if you have none of the above because your server hasn't been configured for sendfile support, you're out of luck until it's configured.
218
+
219
+ == Authentication
220
+
221
+ Authentication is performed on a per Resource basis. The Controller object will check the Resource for a Resource#authenticate method. If it exists,
222
+ any authentication information will be passed to the method. Depending on the result, the Controller will either continue on with the request, or send
223
+ a 401 Unauthorized response.
224
+
225
+ As a nicety, Resource#authentication_realm will be checked for existence and the returning string will be used as the Realm. Resource#authentication_error_msg
226
+ will also be checked for existence and the returning string will be passed in the response upon authentication failure.
227
+
228
+ Authentication can also be implemented using callbacks, as discussed below.
229
+
230
+ == Callbacks
231
+
232
+ Resources can make use of callbacks to easily apply permissions, authentication or any other action that needs to be performed before or after any or all
233
+ actions. Callbacks are applied to all publicly available methods. This is important for methods used internally within the resource. Methods not meant
234
+ to be called by the Controller, or anyone else, should be scoped protected or private to reduce the interaction with callbacks.
235
+
236
+ Callbacks can be called before or after a method call. For example:
237
+
238
+ class MyResource < RackWebDAV::Resource
239
+ before do |resource, method_name|
240
+ resource.send(:my_authentication_method)
241
+ end
242
+
243
+ after do |resource, method_name|
244
+ puts "#{Time.now} -> Completed: #{resource}##{method_name}"
245
+ end
246
+
247
+ private
248
+
249
+ def my_authentication_method
250
+ true
251
+ end
252
+ end
253
+
254
+ In this example MyResource#my_authentication_method will be called before any public method is called. After any method has been called a status
255
+ line will be printed to STDOUT. Running callbacks before/after every method call is a bit much in most cases, so callbacks can be applied to specific
256
+ methods:
257
+
258
+ class MyResource < RackWebDAV::Resource
259
+ before_get do |resource|
260
+ puts "#{Time.now} -> Received GET request from resource: #{resource}"
261
+ end
262
+ end
263
+
264
+ In this example, a simple status line will be printed to STDOUT before the MyResource#get method is called. The current resource object is always
265
+ provided to callbacks. The method name is only provided to the generic before/after callbacks.
266
+
267
+ Something very handy for dealing with the mess of files OS X leaves on the system:
268
+
269
+ class MyResource < RackWebDAV::Resource
270
+ after_unlock do |resource|
271
+ resource.delete if resource.name[0,1] == '.'
272
+ end
273
+ end
274
+
275
+ Because OS X implements locking correctly, we can wait until it releases the lock on the file, and remove it if it's a hidden file.
276
+
277
+ Callbacks are called in the order they are defined, so you can easily build callbacks off each other. Like this example:
278
+
279
+ class MyResource < RackWebDAV::Resource
280
+ before do |resource, method_name|
281
+ resource.DAV_authenticate unless resource.user.is_a?(User)
282
+ raise Unauthorized unless resource.user.is_a?(User)
283
+ end
284
+ before do |resource, method_name|
285
+ resource.user.allowed?(method_name)
286
+ end
287
+ end
288
+
289
+ In this example, the second block checking User#allowed? can count on Resource#user being defined because the blocks are called in
290
+ order, and if the Resource#user is not a User type, an exception is raised.
291
+
292
+ === Avoiding callbacks
293
+
294
+ Something special to notice in the last example is the DAV_ prefix on authenticate. Providing the DAV_ prefix will prevent
295
+ any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource
296
+ without getting stuck in a loop.
297
+
298
+ == Software using RackWebDAV!
299
+
300
+ * {meishi}[https://github.com/inferiorhumanorgans/meishi] - Lightweight CardDAV implementation in Rails
301
+ * {rack-webdav_ext}[https://github.com/schmurfy/rack-webdav_ext] - CardDAV extension. (CalDAV planned)
302
+
303
+ == Issues/Bugs/Questions
304
+
305
+ === Known Issues
306
+
307
+ * OS X Finder PUT fails when using NGINX (this is due to NGINX's lack of chunked transfer encoding)
308
+ * Windows WebDAV mini-redirector fails (this client is very broken. patches welcome.)
309
+ * Lots of unimplemented parts of the webdav spec (patches always welcome)
310
+
311
+ === Unknown Issues
312
+
313
+ Please use the issues at github: http://github.com/chrisroberts/rack-webdav/issues
314
+
315
+ == Contributors
316
+
317
+ A big thanks to everyone contributing to help make this project better.
318
+
319
+ * {clyfe}[http://github.com/clyfe]
320
+ * {antiloopgmbh}[http://github.com/antiloopgmbh]
321
+ * {krug}[http://github.com/krug]
322
+ * {teefax}[http://github.com/teefax]
323
+ * {buffym}[https://github.com/buffym]
324
+ * {jbangert}[https://github.com/jbangert]
325
+ * {doxavore}[https://github.com/doxavore]
326
+ * {spicyj}[https://github.com/spicyj]
327
+ * {TurchenkoAlex}[https://github.com/TurchenkoAlex]
328
+ * {exabugs}[https://github.com/exabugs]
329
+ * {inferiorhumanorgans}[https://github.com/inferiorhumanorgans]
330
+ * {schmurfy}[https://github.com/schmurfy]
331
+ * {pifleo}[https://github.com/pifleo]
332
+ * {mlmorg}[https://github.com/mlmorg]
333
+
334
+ == License
335
+
336
+ Just like RackDAV before it, this software is distributed under the MIT license.
data/bin/rack-webdav ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rack-webdav/version'
5
+ require 'getoptlong'
6
+
7
+ def print_help_msg
8
+ print_version_info
9
+ puts "Usage: rack-webdav [opts]"
10
+ puts " --help Print help message"
11
+ puts " --version Print version information"
12
+ puts " --username name Set username"
13
+ puts " --password pass Set password"
14
+ puts " --root /share/path Set path to share directory"
15
+ puts " --log /path/to/log Set path for logging file"
16
+ puts " --verbosity opt Set logging verbosity. (Valid: debug,info,warn,fatal)"
17
+ end
18
+
19
+ def print_version_info
20
+ puts "DAV 4 Rack - Rack based WebDAV Framework - Version: #{RackWebDAV::VERSION}"
21
+ end
22
+
23
+ opts = GetoptLong.new(
24
+ ['--username', '-u', GetoptLong::REQUIRED_ARGUMENT],
25
+ ['--password', '-p', GetoptLong::REQUIRED_ARGUMENT],
26
+ ['--help', '-h', GetoptLong::NO_ARGUMENT],
27
+ ['--version', '-v', GetoptLong::NO_ARGUMENT],
28
+ ['--root', '-r', GetoptLong::REQUIRED_ARGUMENT],
29
+ ['--log', '-l', GetoptLong::OPTIONAL_ARGUMENT],
30
+ ['--verbosity', '-V', GetoptLong::REQUIRED_ARGUMENT],
31
+ ['--pretty-xml', '-P', GetoptLong::REQUIRED_ARGUMENT]
32
+ )
33
+
34
+ credentials = {}
35
+
36
+ opts.each do |opt,arg|
37
+ case opt
38
+ when '--help'
39
+ print_help_msg
40
+ exit(0)
41
+ when '--username'
42
+ credentials[:username] = arg
43
+ when '--password'
44
+ credentials[:password] = arg
45
+ when '--root'
46
+ credentials[:root] = arg
47
+ unless(File.exists?(arg) && File.directory?(arg))
48
+ puts "ERROR: Path provided is not a valid directory (#{arg})"
49
+ exit(-1)
50
+ end
51
+ when '--pretty-xml'
52
+ credentials[:pretty_xml] = true
53
+ when '--version'
54
+ print_version_info
55
+ exit(0)
56
+ when '--log'
57
+ require 'pathname'
58
+ require 'logger'
59
+ credentials[:log_to] = [nil, Logger::FATAL] unless credentials[:log_to]
60
+ if(arg && !arg.empty?)
61
+ dirname = Pathname.new(arg).dirname
62
+ if((File.exists?(arg) && File.writable?(arg)) || (File.exists?(dirname) && File.writable?(dirname)))
63
+ credentials[:log_to][0] = arg
64
+ else
65
+ puts "ERROR: Log file is not writable: #{arg}"
66
+ exit(-1)
67
+ end
68
+ else
69
+ credentials[:log_to][0] = $stdout
70
+ end
71
+ when '--verbosity'
72
+ require 'logger'
73
+ begin
74
+ credentials[:log_to] = [] unless credentials[:log_to]
75
+ credentials[:log_to][1] = Logger.const_get(arg.upcase)
76
+ rescue NameError
77
+ puts "ERROR: Unknown verbosity level given: #{arg}"
78
+ exit(-1)
79
+ end
80
+ else
81
+ puts "ERROR: Unknown option provided"
82
+ exit(-1)
83
+ end
84
+ end
85
+
86
+ require 'rack-webdav'
87
+
88
+ app = Rack::Builder.new do
89
+ use Rack::ShowExceptions
90
+ use Rack::CommonLogger
91
+ use Rack::Reloader
92
+ use Rack::Lint
93
+
94
+ run RackWebDAV::Handler.new(credentials)
95
+
96
+ end.to_app
97
+
98
+ runners = []
99
+ runners << lambda do |x|
100
+ print 'Looking for unicorn... '
101
+ require 'unicorn'
102
+ puts 'OK'
103
+ if(Unicorn.respond_to?(:run))
104
+ Unicorn.run(x, :listeners => ["0.0.0.0:3000"])
105
+ else
106
+ Unicorn::HttpServer.new(x, :listeners => ["0.0.0.0:3000"]).start.join
107
+ end
108
+ end
109
+ runners << lambda do |x|
110
+ print 'Looking for mongrel... '
111
+ require 'mongrel'
112
+ puts 'OK'
113
+ Rack::Handler::Mongrel.run(x, :Port => 3000)
114
+ end
115
+ runners << lambda do |x|
116
+ puts 'Loading WEBrick'
117
+ Rack::Handler::WEBrick.run(x, :Port => 3000)
118
+ end
119
+
120
+ begin
121
+ runner = runners.shift
122
+ runner.call(app)
123
+ rescue LoadError
124
+ puts 'FAILED'
125
+ retry unless runners.empty?
126
+ end