fluentd 1.12.0.rc2 → 1.12.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.deepsource.toml +13 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +2 -2
  4. data/.github/workflows/linux-test.yaml +36 -0
  5. data/.github/workflows/macos-test.yaml +30 -0
  6. data/.github/workflows/windows-test.yaml +35 -0
  7. data/.gitlab-ci.yml +41 -19
  8. data/CHANGELOG.md +157 -0
  9. data/MAINTAINERS.md +5 -2
  10. data/README.md +7 -4
  11. data/fluentd.gemspec +5 -4
  12. data/lib/fluent/command/bundler_injection.rb +1 -1
  13. data/lib/fluent/command/ca_generate.rb +6 -3
  14. data/lib/fluent/command/cat.rb +0 -1
  15. data/lib/fluent/command/fluentd.rb +4 -0
  16. data/lib/fluent/command/plugin_config_formatter.rb +16 -1
  17. data/lib/fluent/command/plugin_generator.rb +31 -1
  18. data/lib/fluent/compat/parser.rb +2 -2
  19. data/lib/fluent/config/section.rb +2 -2
  20. data/lib/fluent/config/types.rb +2 -2
  21. data/lib/fluent/event.rb +3 -13
  22. data/lib/fluent/load.rb +0 -1
  23. data/lib/fluent/plugin/file_wrapper.rb +39 -3
  24. data/lib/fluent/plugin/formatter_ltsv.rb +2 -2
  25. data/lib/fluent/plugin/in_http.rb +1 -1
  26. data/lib/fluent/plugin/in_monitor_agent.rb +1 -1
  27. data/lib/fluent/plugin/in_tail.rb +35 -15
  28. data/lib/fluent/plugin/in_tail/position_file.rb +15 -1
  29. data/lib/fluent/plugin/in_tcp.rb +1 -0
  30. data/lib/fluent/plugin/out_copy.rb +18 -5
  31. data/lib/fluent/plugin/out_exec_filter.rb +3 -3
  32. data/lib/fluent/plugin/out_forward.rb +61 -28
  33. data/lib/fluent/plugin/out_http.rb +9 -2
  34. data/lib/fluent/plugin/output.rb +11 -9
  35. data/lib/fluent/plugin/parser_csv.rb +2 -2
  36. data/lib/fluent/plugin/parser_syslog.rb +2 -2
  37. data/lib/fluent/plugin/storage_local.rb +4 -4
  38. data/lib/fluent/plugin_helper/inject.rb +4 -2
  39. data/lib/fluent/plugin_helper/server.rb +4 -2
  40. data/lib/fluent/plugin_helper/socket_option.rb +2 -2
  41. data/lib/fluent/supervisor.rb +13 -5
  42. data/lib/fluent/system_config.rb +2 -1
  43. data/lib/fluent/time.rb +58 -1
  44. data/lib/fluent/version.rb +1 -1
  45. data/templates/new_gem/fluent-plugin.gemspec.erb +3 -3
  46. data/templates/plugin_config_formatter/param.md-table.erb +10 -0
  47. data/test/command/test_fluentd.rb +38 -0
  48. data/test/command/test_plugin_config_formatter.rb +67 -0
  49. data/test/config/test_configurable.rb +1 -1
  50. data/test/plugin/in_tail/test_position_file.rb +59 -5
  51. data/test/plugin/test_file_wrapper.rb +105 -0
  52. data/test/plugin/test_in_exec.rb +1 -1
  53. data/test/plugin/test_in_tail.rb +87 -26
  54. data/test/plugin/test_out_copy.rb +87 -0
  55. data/test/plugin/test_out_forward.rb +94 -6
  56. data/test/plugin/test_out_http.rb +20 -1
  57. data/test/plugin/test_output.rb +15 -3
  58. data/test/plugin/test_output_as_buffered_backup.rb +2 -0
  59. data/test/plugin/test_parser_csv.rb +14 -0
  60. data/test/plugin/test_parser_syslog.rb +16 -2
  61. data/test/plugin/test_sd_file.rb +1 -1
  62. data/test/plugin_helper/service_discovery/test_manager.rb +1 -1
  63. data/test/plugin_helper/test_child_process.rb +5 -2
  64. data/test/plugin_helper/test_http_server_helper.rb +4 -2
  65. data/test/plugin_helper/test_inject.rb +29 -0
  66. data/test/plugin_helper/test_server.rb +26 -7
  67. data/test/test_event.rb +16 -0
  68. data/test/test_formatter.rb +30 -0
  69. data/test/test_output.rb +2 -2
  70. data/test/test_supervisor.rb +31 -0
  71. data/test/test_time_parser.rb +109 -0
  72. metadata +36 -31
  73. data/.travis.yml +0 -77
  74. data/appveyor.yml +0 -31
data/fluentd.gemspec CHANGED
@@ -28,12 +28,14 @@ Gem::Specification.new do |gem|
28
28
  gem.add_runtime_dependency("tzinfo", [">= 1.0", "< 3.0"])
29
29
  gem.add_runtime_dependency("tzinfo-data", ["~> 1.0"])
30
30
  gem.add_runtime_dependency("strptime", [">= 0.2.2", "< 1.0.0"])
31
+ gem.add_runtime_dependency("webrick", [">= 1.4.2", "< 1.8.0"])
31
32
 
32
33
  # build gem for a certain platform. see also Rakefile
33
34
  fake_platform = ENV['GEM_BUILD_FAKE_PLATFORM'].to_s
34
35
  gem.platform = fake_platform unless fake_platform.empty?
35
36
  if /mswin|mingw/ =~ fake_platform || (/mswin|mingw/ =~ RUBY_PLATFORM && fake_platform.empty?)
36
- gem.add_runtime_dependency("win32-service", ["~> 2.1.5"])
37
+ gem.add_runtime_dependency("win32-api", [">= 1.10", "< 2.0.0"])
38
+ gem.add_runtime_dependency("win32-service", ["~> 2.2.0"])
37
39
  gem.add_runtime_dependency("win32-ipc", ["~> 0.7.0"])
38
40
  gem.add_runtime_dependency("win32-event", ["~> 0.6.3"])
39
41
  gem.add_runtime_dependency("windows-pr", ["~> 1.2.6"])
@@ -44,11 +46,10 @@ Gem::Specification.new do |gem|
44
46
  gem.add_development_dependency("flexmock", ["~> 2.0"])
45
47
  gem.add_development_dependency("parallel_tests", ["~> 0.15.3"])
46
48
  gem.add_development_dependency("simplecov", ["~> 0.7"])
47
- gem.add_development_dependency("rr", ["~> 1.0"])
49
+ gem.add_development_dependency("rr", ["~> 3.0"])
48
50
  gem.add_development_dependency("timecop", ["~> 0.9"])
49
51
  gem.add_development_dependency("test-unit", ["~> 3.3"])
50
52
  gem.add_development_dependency("test-unit-rr", ["~> 1.0"])
51
53
  gem.add_development_dependency("oj", [">= 2.14", "< 4"])
52
- gem.add_development_dependency("ext_monitor", [">= 0.1.2", "< 0.2"])
53
- gem.add_development_dependency("async-http")
54
+ gem.add_development_dependency("async-http", ">= 0.50.0")
54
55
  end
@@ -40,6 +40,6 @@ else
40
40
  File.expand_path(File.join(File.dirname(__FILE__), 'fluentd.rb')),
41
41
  ] + ARGV
42
42
 
43
- exec *cmdline
43
+ exec(*cmdline)
44
44
  exit! 127
45
45
  end
@@ -75,6 +75,8 @@ HELP
75
75
 
76
76
  digest = OpenSSL::Digest::SHA256.new
77
77
 
78
+ factory = OpenSSL::X509::ExtensionFactory.new
79
+
78
80
  cert = OpenSSL::X509::Certificate.new
79
81
  cert.not_before = Time.at(0)
80
82
  cert.not_after = Time.now + 5 * 365 * 86400 # 5 years after
@@ -82,7 +84,7 @@ HELP
82
84
  cert.serial = 1
83
85
  cert.issuer = issuer
84
86
  cert.subject = subject
85
- cert.add_extension OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(true)]))
87
+ cert.add_extension(factory.create_extension('basicConstraints', 'CA:TRUE'))
86
88
  cert.sign(key, digest)
87
89
 
88
90
  return cert, key
@@ -111,8 +113,9 @@ HELP
111
113
  cert.issuer = issuer
112
114
  cert.subject = subject
113
115
 
114
- cert.add_extension OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(false)]))
115
- cert.add_extension OpenSSL::X509::Extension.new('nsCertType', 'server')
116
+ factory = OpenSSL::X509::ExtensionFactory.new
117
+ server_cert.add_extension(factory.create_extension('basicConstraints', 'CA:FALSE'))
118
+ server_cert.add_extension(factory.create_extension('nsCertType', 'server'))
116
119
 
117
120
  cert.sign ca_key, digest
118
121
 
@@ -101,7 +101,6 @@ rescue
101
101
  usage $!.to_s
102
102
  end
103
103
 
104
- require 'thread'
105
104
  require 'socket'
106
105
  require 'yajl'
107
106
  require 'msgpack'
@@ -163,6 +163,10 @@ op.on('--conf-encoding ENCODING', "specify configuration file encoding") { |s|
163
163
  opts[:conf_encoding] = s
164
164
  }
165
165
 
166
+ op.on('--disable-shared-socket', "Don't open shared socket for multiple workers") { |b|
167
+ opts[:disable_shared_socket] = b
168
+ }
169
+
166
170
  if Fluent.windows?
167
171
  require 'windows/library'
168
172
  include Windows::Library
@@ -44,6 +44,7 @@ class FluentPluginConfigFormatter
44
44
  @verbose = false
45
45
  @libs = []
46
46
  @plugin_dirs = []
47
+ @table = false
47
48
  @options = {}
48
49
 
49
50
  prepare_option_parser
@@ -162,9 +163,20 @@ class FluentPluginConfigFormatter
162
163
  else
163
164
  sections, params = base_section.partition {|_name, value| value[:section] }
164
165
  end
166
+ if @table && (not params.empty?)
167
+ dumped << "### Configuration\n\n"
168
+ dumped << "|parameter|type|description|default|\n"
169
+ dumped << "|---|---|---|---|\n"
170
+ end
165
171
  params.each do |name, config|
166
172
  next if name == :section
167
- template_name = @compact ? "param.md-compact.erb" : "param.md.erb"
173
+ template_name = if @compact
174
+ "param.md-compact.erb"
175
+ elsif @table
176
+ "param.md-table.erb"
177
+ else
178
+ "param.md.erb"
179
+ end
168
180
  template = template_path(template_name).read
169
181
  dumped <<
170
182
  if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
@@ -257,6 +269,9 @@ BANNER
257
269
  @parser.on("-p", "--plugin=DIR", "Add plugin directory") do |s|
258
270
  @plugin_dirs << s
259
271
  end
272
+ @parser.on("-t", "--table", "Use table syntax to dump parameters") do
273
+ @table = true
274
+ end
260
275
  end
261
276
 
262
277
  def parse_options!
@@ -105,7 +105,7 @@ Generate a project skeleton for creating a Fluentd plugin
105
105
 
106
106
  Arguments:
107
107
  \ttype: #{SUPPORTED_TYPES.join(",")}
108
- \tname: Your plugin name
108
+ \tname: Your plugin name (fluent-plugin- prefix will be added to <name>)
109
109
 
110
110
  Options:
111
111
  BANNER
@@ -151,6 +151,36 @@ BANNER
151
151
  underscore_name
152
152
  end
153
153
 
154
+ def gem_file_path
155
+ File.expand_path(File.join(File.dirname(__FILE__),
156
+ "../../../",
157
+ "Gemfile"))
158
+ end
159
+
160
+ def lock_file_path
161
+ File.expand_path(File.join(File.dirname(__FILE__),
162
+ "../../../",
163
+ "Gemfile.lock"))
164
+ end
165
+
166
+ def locked_gem_version(gem_name)
167
+ d = Bundler::Definition.build(gem_file_path, lock_file_path, false)
168
+ d.locked_gems.dependencies[gem_name].requirement.requirements.first.last.version
169
+ end
170
+
171
+ def rake_version
172
+ locked_gem_version("rake")
173
+ end
174
+
175
+ def test_unit_version
176
+ locked_gem_version("test-unit")
177
+ end
178
+
179
+ def bundler_version
180
+ d = Bundler::Definition.build(gem_file_path, lock_file_path, false)
181
+ d.locked_gems.bundler_version.version
182
+ end
183
+
154
184
  def class_name
155
185
  "#{capitalized_name}#{type.capitalize}"
156
186
  end
@@ -244,10 +244,10 @@ module Fluent
244
244
  end
245
245
 
246
246
  def convert_value_to_nil(value)
247
- if value and @null_empty_string
247
+ if value && @null_empty_string
248
248
  value = (value == '') ? nil : value
249
249
  end
250
- if value and @null_value_pattern
250
+ if value && @null_value_pattern
251
251
  value = ::Fluent::StringUtil.match_regexp(@null_value_pattern, value) ? nil : value
252
252
  end
253
253
  value
@@ -179,7 +179,7 @@ module Fluent
179
179
  end
180
180
 
181
181
  if section_params[varname].nil?
182
- unless proxy.defaults.has_key?(varname) and proxy.defaults[varname].nil?
182
+ unless proxy.defaults.has_key?(varname) && proxy.defaults[varname].nil?
183
183
  logger.error "config error in:\n#{conf}" if logger
184
184
  raise ConfigError, "'#{name}' parameter is required but nil is specified"
185
185
  end
@@ -247,7 +247,7 @@ module Fluent
247
247
  def self.check_unused_section(proxy, conf, plugin_class)
248
248
  elems = conf.respond_to?(:elements) ? conf.elements : []
249
249
  elems.each { |e|
250
- next if plugin_class.nil? && Fluent::Config::V1Parser::ELEM_SYMBOLS.include?(e.name) # skip pre-defined non-plugin elements because it doens't have proxy section
250
+ next if plugin_class.nil? && Fluent::Config::V1Parser::ELEM_SYMBOLS.include?(e.name) # skip pre-defined non-plugin elements because it doesn't have proxy section
251
251
  next if e.unused_in && e.unused_in.empty? # the section is used at least once
252
252
 
253
253
  if proxy.sections.any? { |name, subproxy| e.name == subproxy.name.to_s || e.name == subproxy.alias.to_s }
@@ -186,7 +186,7 @@ module Fluent
186
186
  return nil if val.nil?
187
187
 
188
188
  param = if val.is_a?(String)
189
- val.start_with?('{') ? JSON.load(val) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
189
+ val.start_with?('{') ? JSON.parse(val) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
190
190
  else
191
191
  val
192
192
  end
@@ -213,7 +213,7 @@ module Fluent
213
213
  return nil if val.nil?
214
214
 
215
215
  param = if val.is_a?(String)
216
- val.start_with?('[') ? JSON.load(val) : val.strip.split(/\s*,\s*/)
216
+ val.start_with?('[') ? JSON.parse(val) : val.strip.split(/\s*,\s*/)
217
217
  else
218
218
  val
219
219
  end
data/lib/fluent/event.rb CHANGED
@@ -254,19 +254,9 @@ module Fluent
254
254
  end
255
255
 
256
256
  def each(unpacker: nil, &block)
257
- if @unpacked_times
258
- @unpacked_times.each_with_index do |time, i|
259
- block.call(time, @unpacked_records[i])
260
- end
261
- else
262
- @unpacked_times = []
263
- @unpacked_records = []
264
- (unpacker || Fluent::MessagePackFactory.msgpack_unpacker).feed_each(@data) do |time, record|
265
- @unpacked_times << time
266
- @unpacked_records << record
267
- block.call(time, record)
268
- end
269
- @size = @unpacked_times.size
257
+ ensure_unpacked!(unpacker: unpacker)
258
+ @unpacked_times.each_with_index do |time, i|
259
+ block.call(time, @unpacked_records[i])
270
260
  end
271
261
  nil
272
262
  end
data/lib/fluent/load.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'thread'
2
1
  require 'socket'
3
2
  require 'fcntl'
4
3
  require 'time'
@@ -46,6 +46,42 @@ module Fluent
46
46
  end
47
47
  end
48
48
 
49
+ class Win32Error < StandardError
50
+ require 'windows/error'
51
+ include Windows::Error
52
+
53
+ attr_reader :errcode, :msg
54
+
55
+ def initialize(errcode, msg = nil)
56
+ @errcode = errcode
57
+ @msg = msg
58
+ end
59
+
60
+ def format_english_message(errcode)
61
+ buf = 0.chr * 260
62
+ flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY
63
+ english_lang_id = 1033 # The result of MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)
64
+ FormatMessageA.call(flags, 0, errcode, english_lang_id, buf, buf.size, 0)
65
+ buf.force_encoding(Encoding.default_external).strip
66
+ end
67
+
68
+ def to_s
69
+ msg = super
70
+ msg << ": code: #{@errcode}, #{format_english_message(@errcode)}"
71
+ msg << " - #{@msg}" if @msg
72
+ msg
73
+ end
74
+
75
+ def inspect
76
+ "#<#{to_s}>"
77
+ end
78
+
79
+ def ==(other)
80
+ return false if other.class != Win32Error
81
+ @errcode == other.errcode && @msg == other.msg
82
+ end
83
+ end
84
+
49
85
  # To open and get stat with setting FILE_SHARE_DELETE
50
86
  class WindowsFile
51
87
  require 'windows/file'
@@ -77,11 +113,11 @@ module Fluent
77
113
  @file_handle = CreateFile.call(@path, access, sharemode,
78
114
  0, creationdisposition, FILE_ATTRIBUTE_NORMAL, 0)
79
115
  if @file_handle == INVALID_HANDLE_VALUE
80
- err = GetLastError.call
116
+ err = Win32::API.last_error
81
117
  if err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND || err == ERROR_ACCESS_DENIED
82
- raise SystemCallError.new(2)
118
+ raise Errno::ENOENT
83
119
  end
84
- raise SystemCallError.new(err)
120
+ raise Win32Error.new(err, path)
85
121
  end
86
122
  end
87
123
 
@@ -27,14 +27,14 @@ module Fluent
27
27
 
28
28
  config_param :delimiter, :string, default: "\t".freeze
29
29
  config_param :label_delimiter, :string, default: ":".freeze
30
+ config_param :replacement, :string, default: " ".freeze
30
31
  config_param :add_newline, :bool, default: true
31
32
 
32
- # TODO: escaping for \t in values
33
33
  def format(tag, time, record)
34
34
  formatted = ""
35
35
  record.each do |label, value|
36
36
  formatted << @delimiter if formatted.length.nonzero?
37
- formatted << "#{label}#{@label_delimiter}#{value}"
37
+ formatted << "#{label}#{@label_delimiter}#{value.to_s.gsub(@delimiter, @replacement)}"
38
38
  end
39
39
  formatted << @newline if @add_newline
40
40
  formatted
@@ -503,7 +503,7 @@ module Fluent::Plugin
503
503
  # For every incoming request, we check if we have some CORS
504
504
  # restrictions and allow listed origins through @cors_allow_origins.
505
505
  unless @cors_allow_origins.nil?
506
- unless @cors_allow_origins.include?('*') or include_cors_allow_origin
506
+ unless @cors_allow_origins.include?('*') || include_cors_allow_origin
507
507
  send_response_and_close(RES_403_STATUS, {'Connection' => 'close'}, "")
508
508
  return
509
509
  end
@@ -338,7 +338,7 @@ module Fluent::Plugin
338
338
  obj.merge!(pe.statistics['output'] || {})
339
339
  end
340
340
 
341
- obj['retry'] = get_retry_info(pe.retry) if opts[:with_retry] and pe.instance_variable_defined?(:@retry)
341
+ obj['retry'] = get_retry_info(pe.retry) if opts[:with_retry] && pe.instance_variable_defined?(:@retry)
342
342
 
343
343
  # include all instance variables if :with_debug_info is set
344
344
  if opts[:with_debug_info]
@@ -313,11 +313,17 @@ module Fluent::Plugin
313
313
  (paths - excluded).select { |path|
314
314
  FileTest.exist?(path)
315
315
  }.each { |path|
316
- target_info = TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
317
- if @follow_inodes
318
- hash[target_info.ino] = target_info
319
- else
320
- hash[target_info.path] = target_info
316
+ # Even we just checked for existence, there is a race condition here as
317
+ # of which stat() might fail with ENOENT. See #3224.
318
+ begin
319
+ target_info = TargetInfo.new(path, Fluent::FileWrapper.stat(path).ino)
320
+ if @follow_inodes
321
+ hash[target_info.ino] = target_info
322
+ else
323
+ hash[target_info.path] = target_info
324
+ end
325
+ rescue Errno::ENOENT
326
+ $log.warn "expand_paths: stat() for #{path} failed with ENOENT. Skip file."
321
327
  end
322
328
  }
323
329
  hash
@@ -406,8 +412,16 @@ module Fluent::Plugin
406
412
  log.warn "Skip #{target_info.path} because unexpected setup error happens: #{e}"
407
413
  next
408
414
  end
409
- target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
410
- @tails[target_info] = tw
415
+
416
+ begin
417
+ target_info = TargetInfo.new(target_info.path, Fluent::FileWrapper.stat(target_info.path).ino)
418
+ @tails[target_info] = tw
419
+ rescue Errno::ENOENT
420
+ $log.warn "stat() for #{target_info.path} failed with ENOENT. Drop tail watcher for now."
421
+ # explicitly detach and unwatch watcher `tw`.
422
+ tw.unwatched = true
423
+ detach_watcher(tw, target_info.ino, false)
424
+ end
411
425
  }
412
426
  end
413
427
 
@@ -767,16 +781,22 @@ module Fluent::Plugin
767
781
  end
768
782
 
769
783
  if watcher_needs_update
770
- # No need to update a watcher if stat is nil (file not present), because moving to inodes will create
771
- # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
772
- if stat
773
- target_info = TargetInfo.new(@path, stat.ino)
774
- if @follow_inodes
775
- # don't want to swap state because we need latest read offset in pos file even after rotate_wait
784
+ if @follow_inodes
785
+ # No need to update a watcher if stat is nil (file not present), because moving to inodes will create
786
+ # new watcher, and old watcher will be closed by stop_watcher in refresh_watchers method
787
+ # don't want to swap state because we need latest read offset in pos file even after rotate_wait
788
+ if stat
789
+ target_info = TargetInfo.new(@path, stat)
776
790
  @update_watcher.call(target_info, @pe)
777
- else
778
- @update_watcher.call(target_info, swap_state(@pe))
779
791
  end
792
+ else
793
+ # Permit to handle if stat is nil (file not present).
794
+ # If a file is mv-ed and a new file is created during
795
+ # calling `#refresh_watchers`s, and `#refresh_watchers` won't run `#start_watchers`
796
+ # and `#stop_watchers()` for the path because `target_paths_hash`
797
+ # always contains the path.
798
+ target_info = TargetInfo.new(@path, stat ? stat.ino : nil)
799
+ @update_watcher.call(target_info, swap_state(@pe))
780
800
  end
781
801
  else
782
802
  @log.info "detected rotation of #{@path}"
@@ -248,6 +248,20 @@ module Fluent::Plugin
248
248
  end
249
249
  end
250
250
 
251
- TargetInfo = Struct.new(:path, :ino)
251
+ TargetInfo = Struct.new(:path, :ino) do
252
+ def ==(other)
253
+ return false unless other.is_a?(TargetInfo)
254
+ self.path == other.path
255
+ end
256
+
257
+ def hash
258
+ self.path.hash
259
+ end
260
+
261
+ def eql?(other)
262
+ return false unless other.is_a?(TargetInfo)
263
+ self.path == other.path
264
+ end
265
+ end
252
266
  end
253
267
  end