logstash-core 5.0.0.alpha4.snapshot3-java → 5.0.0.alpha5.snapshot1-java
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of logstash-core might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/logstash/agent.rb +6 -2
- data/lib/logstash/api/app_helpers.rb +15 -0
- data/lib/logstash/api/command_factory.rb +3 -1
- data/lib/logstash/api/commands/base.rb +3 -5
- data/lib/logstash/api/commands/default_metadata.rb +27 -0
- data/lib/logstash/api/commands/hot_threads_reporter.rb +61 -0
- data/lib/logstash/api/commands/node.rb +9 -63
- data/lib/logstash/api/commands/stats.rb +5 -61
- data/lib/logstash/api/commands/system/basicinfo_command.rb +3 -6
- data/lib/logstash/api/modules/base.rb +3 -1
- data/lib/logstash/api/modules/node.rb +8 -18
- data/lib/logstash/api/modules/node_stats.rb +5 -41
- data/lib/logstash/api/modules/stats.rb +13 -33
- data/lib/logstash/build.rb +6 -0
- data/lib/logstash/environment.rb +9 -0
- data/lib/logstash/filter_delegator.rb +1 -1
- data/lib/logstash/instrument/metric.rb +7 -6
- data/lib/logstash/instrument/metric_type/base.rb +1 -4
- data/lib/logstash/instrument/namespaced_metric.rb +1 -1
- data/lib/logstash/instrument/null_metric.rb +6 -1
- data/lib/logstash/output_delegator.rb +2 -0
- data/lib/logstash/pipeline.rb +62 -93
- data/lib/logstash/pipeline_reporter.rb +14 -13
- data/lib/logstash/plugin.rb +8 -2
- data/lib/logstash/runner.rb +7 -1
- data/lib/logstash/settings.rb +17 -7
- data/lib/logstash/util/wrapped_synchronous_queue.rb +220 -0
- data/lib/logstash/version.rb +1 -1
- data/lib/logstash/webserver.rb +4 -0
- data/lib/logstash-core/version.rb +1 -1
- data/locales/en.yml +4 -0
- data/logstash-core.gemspec +2 -2
- data/spec/api/lib/api/node_spec.rb +0 -1
- data/spec/api/lib/api/node_stats_spec.rb +36 -34
- data/spec/api/lib/api/support/resource_dsl_methods.rb +15 -0
- data/spec/api/spec_helper.rb +5 -2
- data/spec/logstash/inputs/metrics_spec.rb +1 -1
- data/spec/logstash/instrument/metric_type/counter_spec.rb +1 -6
- data/spec/logstash/instrument/metric_type/gauge_spec.rb +1 -4
- data/spec/logstash/instrument/namespaced_metric_spec.rb +61 -2
- data/spec/logstash/instrument/null_metric_spec.rb +7 -9
- data/spec/logstash/pipeline_spec.rb +7 -7
- data/spec/logstash/plugin_spec.rb +73 -0
- data/spec/logstash/settings/string_spec.rb +21 -0
- data/spec/logstash/util/wrapped_synchronous_queue_spec.rb +70 -22
- data/spec/support/shared_examples.rb +98 -0
- metadata +11 -4
@@ -9,20 +9,25 @@ module LogStash module Instrument
|
|
9
9
|
attr_reader :namespace_name, :collector
|
10
10
|
|
11
11
|
def increment(key, value = 1)
|
12
|
+
Metric.validate_key!(key)
|
12
13
|
end
|
13
14
|
|
14
|
-
def decrement(
|
15
|
+
def decrement(key, value = 1)
|
16
|
+
Metric.validate_key!(key)
|
15
17
|
end
|
16
18
|
|
17
19
|
def gauge(key, value)
|
20
|
+
Metric.validate_key!(key)
|
18
21
|
end
|
19
22
|
|
20
23
|
def report_time(key, duration)
|
24
|
+
Metric.validate_key!(key)
|
21
25
|
end
|
22
26
|
|
23
27
|
# We have to manually redefine this method since it can return an
|
24
28
|
# object this object also has to be implemented as a NullObject
|
25
29
|
def time(key)
|
30
|
+
Metric.validate_key!(key)
|
26
31
|
if block_given?
|
27
32
|
yield
|
28
33
|
else
|
@@ -27,6 +27,8 @@ module LogStash class OutputDelegator
|
|
27
27
|
|
28
28
|
# Scope the metrics to the plugin
|
29
29
|
namespaced_metric = metric.namespace(output.plugin_unique_name.to_sym)
|
30
|
+
output.metric = namespaced_metric
|
31
|
+
|
30
32
|
@metric_events = namespaced_metric.namespace(:events)
|
31
33
|
namespaced_metric.gauge(:name, config_name)
|
32
34
|
|
data/lib/logstash/pipeline.rb
CHANGED
@@ -33,7 +33,9 @@ module LogStash; class Pipeline
|
|
33
33
|
:thread,
|
34
34
|
:config_str,
|
35
35
|
:settings,
|
36
|
-
:metric
|
36
|
+
:metric,
|
37
|
+
:filter_queue_client,
|
38
|
+
:input_queue_client
|
37
39
|
|
38
40
|
MAX_INFLIGHT_WARN_THRESHOLD = 10_000
|
39
41
|
|
@@ -82,14 +84,18 @@ module LogStash; class Pipeline
|
|
82
84
|
raise
|
83
85
|
end
|
84
86
|
|
85
|
-
|
87
|
+
queue = LogStash::Util::WrappedSynchronousQueue.new
|
88
|
+
@input_queue_client = queue.write_client
|
89
|
+
@filter_queue_client = queue.read_client
|
90
|
+
# Note that @infilght_batches as a central mechanism for tracking inflight
|
91
|
+
# batches will fail if we have multiple read clients here.
|
92
|
+
@filter_queue_client.set_events_metric(metric.namespace([:stats, :events]))
|
93
|
+
@filter_queue_client.set_pipeline_metric(
|
94
|
+
metric.namespace([:stats, :pipelines, pipeline_id.to_s.to_sym, :events])
|
95
|
+
)
|
86
96
|
@events_filtered = Concurrent::AtomicFixnum.new(0)
|
87
97
|
@events_consumed = Concurrent::AtomicFixnum.new(0)
|
88
98
|
|
89
|
-
# We generally only want one thread at a time able to access pop/take/poll operations
|
90
|
-
# from this queue. We also depend on this to be able to block consumers while we snapshot
|
91
|
-
# in-flight buffers
|
92
|
-
@input_queue_pop_mutex = Mutex.new
|
93
99
|
@input_threads = []
|
94
100
|
# @ready requires thread safety since it is typically polled from outside the pipeline thread
|
95
101
|
@ready = Concurrent::AtomicBoolean.new(false)
|
@@ -176,8 +182,6 @@ module LogStash; class Pipeline
|
|
176
182
|
end
|
177
183
|
|
178
184
|
def start_workers
|
179
|
-
@inflight_batches = {}
|
180
|
-
|
181
185
|
@worker_threads.clear # In case we're restarting the pipeline
|
182
186
|
begin
|
183
187
|
start_inputs
|
@@ -187,13 +191,14 @@ module LogStash; class Pipeline
|
|
187
191
|
pipeline_workers = safe_pipeline_worker_count
|
188
192
|
batch_size = @settings.get("pipeline.batch.size")
|
189
193
|
batch_delay = @settings.get("pipeline.batch.delay")
|
194
|
+
|
190
195
|
max_inflight = batch_size * pipeline_workers
|
191
196
|
|
192
|
-
config_metric = metric.namespace([:stats, :pipelines, pipeline_id.to_s.to_sym, :config])
|
197
|
+
config_metric = metric.namespace([:stats, :pipelines, pipeline_id.to_s.to_sym, :config])
|
193
198
|
config_metric.gauge(:workers, pipeline_workers)
|
194
199
|
config_metric.gauge(:batch_size, batch_size)
|
195
200
|
config_metric.gauge(:batch_delay, batch_delay)
|
196
|
-
|
201
|
+
|
197
202
|
@logger.info("Starting pipeline",
|
198
203
|
"id" => self.pipeline_id,
|
199
204
|
"pipeline.workers" => pipeline_workers,
|
@@ -211,7 +216,7 @@ module LogStash; class Pipeline
|
|
211
216
|
end
|
212
217
|
end
|
213
218
|
ensure
|
214
|
-
# it is important to
|
219
|
+
# it is important to guarantee @ready to be true after the startup sequence has been completed
|
215
220
|
# to potentially unblock the shutdown method which may be waiting on @ready to proceed
|
216
221
|
@ready.make_true
|
217
222
|
end
|
@@ -222,73 +227,39 @@ module LogStash; class Pipeline
|
|
222
227
|
def worker_loop(batch_size, batch_delay)
|
223
228
|
running = true
|
224
229
|
|
225
|
-
|
226
|
-
namespace_pipeline = metric.namespace([:stats, :pipelines, pipeline_id.to_s.to_sym, :events])
|
230
|
+
@filter_queue_client.set_batch_dimensions(batch_size, batch_delay)
|
227
231
|
|
228
232
|
while running
|
229
|
-
|
230
|
-
|
231
|
-
running = false if
|
232
|
-
|
233
|
-
@events_consumed.increment(input_batch.size)
|
234
|
-
namespace_events.increment(:in, input_batch.size)
|
235
|
-
namespace_pipeline.increment(:in, input_batch.size)
|
233
|
+
batch = @filter_queue_client.take_batch
|
234
|
+
@events_consumed.increment(batch.size)
|
235
|
+
running = false if batch.shutdown_signal_received?
|
236
|
+
filter_batch(batch)
|
236
237
|
|
237
|
-
|
238
|
-
|
239
|
-
if signal # Flush on SHUTDOWN or FLUSH
|
240
|
-
flush_options = (signal == LogStash::SHUTDOWN) ? {:final => true} : {}
|
241
|
-
flush_filters_to_batch(filtered_batch, flush_options)
|
238
|
+
if batch.shutdown_signal_received? || batch.flush_signal_received?
|
239
|
+
flush_filters_to_batch(batch)
|
242
240
|
end
|
243
241
|
|
244
|
-
|
245
|
-
|
246
|
-
namespace_events.increment(:filtered, filtered_batch.size)
|
247
|
-
namespace_pipeline.increment(:filtered, filtered_batch.size)
|
248
|
-
|
249
|
-
output_batch(filtered_batch)
|
250
|
-
|
251
|
-
namespace_events.increment(:out, filtered_batch.size)
|
252
|
-
namespace_pipeline.increment(:out, filtered_batch.size)
|
253
|
-
|
254
|
-
inflight_batches_synchronize { set_current_thread_inflight_batch(nil) }
|
242
|
+
output_batch(batch)
|
243
|
+
@filter_queue_client.close_batch(batch)
|
255
244
|
end
|
256
245
|
end
|
257
246
|
|
258
|
-
def take_batch(batch_size, batch_delay)
|
259
|
-
batch = []
|
260
|
-
# Since this is externally synchronized in `worker_look` wec can guarantee that the visibility of an insight batch
|
261
|
-
# guaranteed to be a full batch not a partial batch
|
262
|
-
set_current_thread_inflight_batch(batch)
|
263
|
-
|
264
|
-
signal = false
|
265
|
-
batch_size.times do |t|
|
266
|
-
event = (t == 0) ? @input_queue.take : @input_queue.poll(batch_delay)
|
267
|
-
|
268
|
-
if event.nil?
|
269
|
-
next
|
270
|
-
elsif event == LogStash::SHUTDOWN || event == LogStash::FLUSH
|
271
|
-
# We MUST break here. If a batch consumes two SHUTDOWN events
|
272
|
-
# then another worker may have its SHUTDOWN 'stolen', thus blocking
|
273
|
-
# the pipeline. We should stop doing work after flush as well.
|
274
|
-
signal = event
|
275
|
-
break
|
276
|
-
else
|
277
|
-
batch << event
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
[batch, signal]
|
282
|
-
end
|
283
|
-
|
284
247
|
def filter_batch(batch)
|
285
|
-
batch.
|
286
|
-
if
|
287
|
-
filtered = filter_func(
|
288
|
-
filtered.each
|
248
|
+
batch.each do |event|
|
249
|
+
if event.is_a?(LogStash::Event)
|
250
|
+
filtered = filter_func(event)
|
251
|
+
filtered.each do |e|
|
252
|
+
#these are both original and generated events
|
253
|
+
if e.cancelled?
|
254
|
+
batch.cancel(e)
|
255
|
+
else
|
256
|
+
batch.merge(e)
|
257
|
+
end
|
258
|
+
end
|
289
259
|
end
|
290
|
-
acc
|
291
260
|
end
|
261
|
+
@filter_queue_client.add_filtered_metrics(batch)
|
262
|
+
@events_filtered.increment(batch.size)
|
292
263
|
rescue Exception => e
|
293
264
|
# Plugins authors should manage their own exceptions in the plugin code
|
294
265
|
# but if an exception is raised up to the worker thread they are considered
|
@@ -304,31 +275,21 @@ module LogStash; class Pipeline
|
|
304
275
|
# Take an array of events and send them to the correct output
|
305
276
|
def output_batch(batch)
|
306
277
|
# Build a mapping of { output_plugin => [events...]}
|
307
|
-
|
278
|
+
output_events_map = Hash.new { |h, k| h[k] = [] }
|
279
|
+
batch.each do |event|
|
308
280
|
# We ask the AST to tell us which outputs to send each event to
|
309
281
|
# Then, we stick it in the correct bin
|
310
282
|
|
311
283
|
# output_func should never return anything other than an Array but we have lots of legacy specs
|
312
284
|
# that monkeypatch it and return nil. We can deprecate "|| []" after fixing these specs
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
acc
|
285
|
+
(output_func(event) || []).each do |output|
|
286
|
+
output_events_map[output].push(event)
|
287
|
+
end
|
317
288
|
end
|
318
|
-
|
319
289
|
# Now that we have our output to event mapping we can just invoke each output
|
320
290
|
# once with its list of events
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
def set_current_thread_inflight_batch(batch)
|
325
|
-
@inflight_batches[Thread.current] = batch
|
326
|
-
end
|
327
|
-
|
328
|
-
def inflight_batches_synchronize
|
329
|
-
@input_queue_pop_mutex.synchronize do
|
330
|
-
yield(@inflight_batches)
|
331
|
-
end
|
291
|
+
output_events_map.each { |output, events| output.multi_receive(events) }
|
292
|
+
@filter_queue_client.add_output_metrics(batch)
|
332
293
|
end
|
333
294
|
|
334
295
|
def wait_inputs
|
@@ -359,7 +320,7 @@ module LogStash; class Pipeline
|
|
359
320
|
def inputworker(plugin)
|
360
321
|
LogStash::Util::set_thread_name("[#{pipeline_id}]<#{plugin.class.config_name}")
|
361
322
|
begin
|
362
|
-
plugin.run(@
|
323
|
+
plugin.run(@input_queue_client)
|
363
324
|
rescue => e
|
364
325
|
if plugin.stop?
|
365
326
|
@logger.debug("Input plugin raised exception during shutdown, ignoring it.",
|
@@ -413,7 +374,7 @@ module LogStash; class Pipeline
|
|
413
374
|
# Each worker thread will receive this exactly once!
|
414
375
|
@worker_threads.each do |t|
|
415
376
|
@logger.debug("Pushing shutdown", :thread => t.inspect)
|
416
|
-
@
|
377
|
+
@input_queue_client.push(LogStash::SHUTDOWN)
|
417
378
|
end
|
418
379
|
|
419
380
|
@worker_threads.each do |t|
|
@@ -437,7 +398,11 @@ module LogStash; class Pipeline
|
|
437
398
|
elsif plugin_type == "filter"
|
438
399
|
LogStash::FilterDelegator.new(@logger, klass, pipeline_scoped_metric.namespace(:filters), *args)
|
439
400
|
else
|
440
|
-
klass.new(*args)
|
401
|
+
new_plugin = klass.new(*args)
|
402
|
+
inputs_metric = pipeline_scoped_metric.namespace(:inputs)
|
403
|
+
namespaced_metric = inputs_metric.namespace(new_plugin.plugin_unique_name.to_sym)
|
404
|
+
new_plugin.metric = namespaced_metric
|
405
|
+
new_plugin
|
441
406
|
end
|
442
407
|
end
|
443
408
|
|
@@ -449,7 +414,7 @@ module LogStash; class Pipeline
|
|
449
414
|
end
|
450
415
|
|
451
416
|
|
452
|
-
# perform filters flush and
|
417
|
+
# perform filters flush and yield flushed event to the passed block
|
453
418
|
# @param options [Hash]
|
454
419
|
# @option options [Boolean] :final => true to signal a final shutdown flush
|
455
420
|
def flush_filters(options = {}, &block)
|
@@ -479,7 +444,7 @@ module LogStash; class Pipeline
|
|
479
444
|
def flush
|
480
445
|
if @flushing.compare_and_set(false, true)
|
481
446
|
@logger.debug? && @logger.debug("Pushing flush onto pipeline")
|
482
|
-
@
|
447
|
+
@input_queue_client.push(LogStash::FLUSH)
|
483
448
|
end
|
484
449
|
end
|
485
450
|
|
@@ -493,18 +458,22 @@ module LogStash; class Pipeline
|
|
493
458
|
end
|
494
459
|
|
495
460
|
# perform filters flush into the output queue
|
461
|
+
#
|
462
|
+
# @param batch [ReadClient::ReadBatch]
|
496
463
|
# @param options [Hash]
|
497
|
-
# @option options [Boolean] :final => true to signal a final shutdown flush
|
498
464
|
def flush_filters_to_batch(batch, options = {})
|
465
|
+
options[:final] = batch.shutdown_signal_received?
|
499
466
|
flush_filters(options) do |event|
|
500
|
-
|
467
|
+
if event.cancelled?
|
468
|
+
batch.cancel(event)
|
469
|
+
else
|
501
470
|
@logger.debug? and @logger.debug("Pushing flushed events", :event => event)
|
502
|
-
batch
|
471
|
+
batch.merge(event)
|
503
472
|
end
|
504
473
|
end
|
505
474
|
|
506
475
|
@flushing.set(false)
|
507
|
-
end #
|
476
|
+
end # flush_filters_to_batch
|
508
477
|
|
509
478
|
def plugin_threads_info
|
510
479
|
input_threads = @input_threads.select {|t| t.alive? }
|
@@ -39,7 +39,7 @@ module LogStash; class PipelineReporter
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
def initialize(logger,pipeline)
|
42
|
+
def initialize(logger, pipeline)
|
43
43
|
@logger = logger
|
44
44
|
@pipeline = pipeline
|
45
45
|
end
|
@@ -52,7 +52,8 @@ module LogStash; class PipelineReporter
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def to_hash
|
55
|
-
pipeline.
|
55
|
+
# pipeline.filter_queue_client.inflight_batches is synchronized
|
56
|
+
pipeline.filter_queue_client.inflight_batches do |batch_map|
|
56
57
|
worker_states_snap = worker_states(batch_map) # We only want to run this once
|
57
58
|
inflight_count = worker_states_snap.map {|s| s[:inflight_count] }.reduce(0, :+)
|
58
59
|
|
@@ -83,17 +84,17 @@ module LogStash; class PipelineReporter
|
|
83
84
|
pipeline.plugin_threads
|
84
85
|
end
|
85
86
|
|
86
|
-
# Not threadsafe!
|
87
|
+
# Not threadsafe! ensure synchronization
|
87
88
|
def worker_states(batch_map)
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
89
|
+
pipeline.worker_threads.map.with_index do |thread, idx|
|
90
|
+
status = thread.status || "dead"
|
91
|
+
inflight_count = batch_map[thread] ? batch_map[thread].size : 0
|
92
|
+
{
|
93
|
+
:status => status,
|
94
|
+
:alive => thread.alive?,
|
95
|
+
:index => idx,
|
96
|
+
:inflight_count => inflight_count
|
97
|
+
}
|
97
98
|
end
|
98
99
|
end
|
99
100
|
|
@@ -111,4 +112,4 @@ module LogStash; class PipelineReporter
|
|
111
112
|
}
|
112
113
|
end
|
113
114
|
end
|
114
|
-
end end
|
115
|
+
end end
|
data/lib/logstash/plugin.rb
CHANGED
@@ -105,9 +105,15 @@ class LogStash::Plugin
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def metric
|
108
|
-
|
108
|
+
# We can disable metric per plugin if we want in the configuration
|
109
|
+
# we will use the NullMetric in this case.
|
110
|
+
@metric_plugin ||= if @enable_metric
|
111
|
+
# Fallback when testing plugin and no metric collector are correctly configured.
|
112
|
+
@metric.nil? ? LogStash::Instrument::NullMetric.new : @metric
|
113
|
+
else
|
114
|
+
LogStash::Instrument::NullMetric.new
|
115
|
+
end
|
109
116
|
end
|
110
|
-
|
111
117
|
# return the configured name of this plugin
|
112
118
|
# @return [String] The name of the plugin defined by `config_name`
|
113
119
|
def config_name
|
data/lib/logstash/runner.rb
CHANGED
@@ -16,6 +16,7 @@ require "logstash/config/defaults"
|
|
16
16
|
require "logstash/shutdown_watcher"
|
17
17
|
require "logstash/patches/clamp"
|
18
18
|
require "logstash/settings"
|
19
|
+
require "logstash/version"
|
19
20
|
|
20
21
|
class LogStash::Runner < Clamp::StrictCommand
|
21
22
|
# The `path.settings` need to be defined in the runner instead of the `logstash-core/lib/logstash/environment.rb`
|
@@ -63,6 +64,12 @@ class LogStash::Runner < Clamp::StrictCommand
|
|
63
64
|
:attribute_name => "pipeline.unsafe_shutdown",
|
64
65
|
:default => LogStash::SETTINGS.get_default("pipeline.unsafe_shutdown")
|
65
66
|
|
67
|
+
# Data Path Setting
|
68
|
+
option ["--path.data"] , "PATH",
|
69
|
+
I18n.t("logstash.runner.flag.datapath"),
|
70
|
+
:attribute_name => "path.data",
|
71
|
+
:default => LogStash::SETTINGS.get_default("path.data")
|
72
|
+
|
66
73
|
# Plugins Settings
|
67
74
|
option ["-p", "--path.plugins"] , "PATH",
|
68
75
|
I18n.t("logstash.runner.flag.pluginpath"),
|
@@ -256,7 +263,6 @@ class LogStash::Runner < Clamp::StrictCommand
|
|
256
263
|
end # def show_version
|
257
264
|
|
258
265
|
def show_version_logstash
|
259
|
-
require "logstash/version"
|
260
266
|
puts "logstash #{LOGSTASH_VERSION}"
|
261
267
|
end # def show_version_logstash
|
262
268
|
|
data/lib/logstash/settings.rb
CHANGED
@@ -210,12 +210,6 @@ module LogStash
|
|
210
210
|
end
|
211
211
|
end
|
212
212
|
|
213
|
-
class String < Setting
|
214
|
-
def initialize(name, default=nil, strict=true)
|
215
|
-
super(name, ::String, default, strict)
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
213
|
class Numeric < Setting
|
220
214
|
def initialize(name, default=nil, strict=true)
|
221
215
|
super(name, ::Numeric, default, strict)
|
@@ -241,11 +235,15 @@ module LogStash
|
|
241
235
|
|
242
236
|
class String < Setting
|
243
237
|
def initialize(name, default=nil, strict=true, possible_strings=[])
|
238
|
+
@possible_strings = possible_strings
|
244
239
|
super(name, ::String, default, strict)
|
245
240
|
end
|
246
241
|
|
247
242
|
def validate(value)
|
248
|
-
super(value)
|
243
|
+
super(value)
|
244
|
+
unless @possible_strings.empty? || @possible_strings.include?(value)
|
245
|
+
raise ArgumentError.new("invalid value \"#{value}\". Options are: #{@possible_strings.inspect}")
|
246
|
+
end
|
249
247
|
end
|
250
248
|
end
|
251
249
|
|
@@ -261,6 +259,18 @@ module LogStash
|
|
261
259
|
end
|
262
260
|
end
|
263
261
|
|
262
|
+
class WritableDirectory < Setting
|
263
|
+
def initialize(name, default=nil, strict=true)
|
264
|
+
super(name, ::String, default, strict) do |path|
|
265
|
+
if ::File.directory?(path) && ::File.writable?(path)
|
266
|
+
true
|
267
|
+
else
|
268
|
+
raise ::ArgumentError.new("Path \"#{path}\" is not a directory or not writable.")
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
264
274
|
end
|
265
275
|
|
266
276
|
SETTINGS = Settings.new
|