octocatalog-diff 1.5.1 → 2.0.0

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 (44) hide show
  1. checksums.yaml +5 -5
  2. data/.version +1 -1
  3. data/README.md +4 -4
  4. data/doc/CHANGELOG.md +49 -0
  5. data/doc/advanced-filter.md +23 -0
  6. data/doc/advanced-ignores.md +10 -0
  7. data/doc/advanced-puppet-master.md +23 -5
  8. data/doc/configuration-puppetdb.md +11 -0
  9. data/doc/dev/api/v1/calls/catalog-diff.md +6 -2
  10. data/doc/dev/api/v1/objects/diff.md +3 -3
  11. data/doc/dev/releasing.md +1 -1
  12. data/doc/limitations.md +9 -9
  13. data/doc/optionsref.md +167 -11
  14. data/doc/requirements.md +6 -2
  15. data/lib/octocatalog-diff/catalog-diff/differ.rb +29 -4
  16. data/lib/octocatalog-diff/catalog-diff/filter.rb +2 -1
  17. data/lib/octocatalog-diff/catalog-diff/filter/compilation_dir.rb +29 -25
  18. data/lib/octocatalog-diff/catalog-diff/filter/single_item_array.rb +44 -0
  19. data/lib/octocatalog-diff/catalog-util/builddir.rb +3 -3
  20. data/lib/octocatalog-diff/catalog-util/command.rb +25 -3
  21. data/lib/octocatalog-diff/catalog-util/fileresources.rb +1 -1
  22. data/lib/octocatalog-diff/catalog.rb +22 -4
  23. data/lib/octocatalog-diff/catalog/computed.rb +2 -1
  24. data/lib/octocatalog-diff/catalog/puppetmaster.rb +43 -5
  25. data/lib/octocatalog-diff/cli.rb +36 -5
  26. data/lib/octocatalog-diff/cli/options.rb +39 -3
  27. data/lib/octocatalog-diff/cli/options/hostname.rb +13 -2
  28. data/lib/octocatalog-diff/cli/options/pe_enc_token_file.rb +1 -1
  29. data/lib/octocatalog-diff/cli/options/puppet_master_api_version.rb +2 -2
  30. data/lib/octocatalog-diff/cli/options/puppet_master_token.rb +20 -0
  31. data/lib/octocatalog-diff/cli/options/puppet_master_token_file.rb +35 -0
  32. data/lib/octocatalog-diff/cli/options/puppet_master_update_catalog.rb +20 -0
  33. data/lib/octocatalog-diff/cli/options/puppet_master_update_facts.rb +20 -0
  34. data/lib/octocatalog-diff/cli/options/puppetdb_package_inventory.rb +18 -0
  35. data/lib/octocatalog-diff/cli/options/puppetdb_token.rb +17 -0
  36. data/lib/octocatalog-diff/cli/options/puppetdb_token_file.rb +21 -0
  37. data/lib/octocatalog-diff/facts/puppetdb.rb +43 -2
  38. data/lib/octocatalog-diff/puppetdb.rb +5 -1
  39. data/lib/octocatalog-diff/util/parallel.rb +20 -16
  40. data/lib/octocatalog-diff/util/util.rb +2 -0
  41. data/scripts/env/env.sh +1 -1
  42. data/scripts/git-extract/git-extract.sh +1 -1
  43. data/scripts/puppet/puppet.sh +1 -1
  44. metadata +37 -30
@@ -11,6 +11,7 @@ require_relative 'util/util'
11
11
  require_relative 'version'
12
12
 
13
13
  require 'logger'
14
+ require 'parallel'
14
15
  require 'socket'
15
16
 
16
17
  module OctocatalogDiff
@@ -116,16 +117,46 @@ module OctocatalogDiff
116
117
  end
117
118
 
118
119
  # Compile catalogs and do catalog-diff
119
- catalog_diff = OctocatalogDiff::API::V1.catalog_diff(options.merge(logger: logger))
120
+ node_set = options.delete(:node)
121
+ node_set = [node_set] unless node_set.is_a?(Array)
122
+
123
+ # run multiple node diffs in parallel
124
+ catalog_diffs = if node_set.size == 1
125
+ [run_octocatalog_diff(node_set.first, options, logger)]
126
+ else
127
+ ::Parallel.map(node_set, in_threads: 4) { |node| run_octocatalog_diff(node, options, logger) }
128
+ end
129
+
130
+ # Return the resulting diff object if requested (generally for testing)
131
+ # or otherwise return exit code
132
+ return catalog_diffs.first if opts[:INTEGRATION]
133
+
134
+ all_diffs = catalog_diffs.map(&:diffs)
135
+
136
+ all_diffs.each do |diff|
137
+ next unless diff.any?
138
+ return EXITCODE_SUCCESS_WITH_DIFFS
139
+ end
140
+
141
+ EXITCODE_SUCCESS_NO_DIFFS
142
+ end
143
+
144
+ # Run the octocatalog-diff process for a given node. Return the diffs for a contribution to
145
+ # the final exit status.
146
+ # node - String with the node
147
+ # options - All of the currently defined options
148
+ # logger - Logger object
149
+ def self.run_octocatalog_diff(node, options, logger)
150
+ options_copy = options.merge(node: node)
151
+ catalog_diff = OctocatalogDiff::API::V1.catalog_diff(options_copy.merge(logger: logger))
120
152
  diffs = catalog_diff.diffs
121
153
 
122
154
  # Display diffs
123
- printer_obj = OctocatalogDiff::Cli::Printer.new(options, logger)
155
+ printer_obj = OctocatalogDiff::Cli::Printer.new(options_copy, logger)
124
156
  printer_obj.printer(diffs, catalog_diff.from.compilation_dir, catalog_diff.to.compilation_dir)
125
157
 
126
- # Return the resulting diff object if requested (generally for testing) or otherwise return exit code
127
- return catalog_diff if opts[:INTEGRATION]
128
- diffs.any? ? EXITCODE_SUCCESS_WITH_DIFFS : EXITCODE_SUCCESS_NO_DIFFS
158
+ # Return catalog-diff object.
159
+ catalog_diff
129
160
  end
130
161
 
131
162
  # Parse command line options with 'optparse'. Returns a hash with the parsed arguments.
@@ -11,7 +11,7 @@ module OctocatalogDiff
11
11
  # This class contains the option parser. 'parse_options' is the external entry point.
12
12
  class Options
13
13
  # The usage banner.
14
- BANNER = 'Usage: catalog-diff -n <hostname> [-f <from environment>] [-t <to environment>]'.freeze
14
+ BANNER = 'Usage: catalog-diff -n <hostname>[,<hostname>...] [-f <from environment>] [-t <to environment>]'.freeze
15
15
 
16
16
  # An error class specifically for passing information to the document build task.
17
17
  class DocBuildError < RuntimeError; end
@@ -23,7 +23,6 @@ module OctocatalogDiff
23
23
 
24
24
  # Define the Option class and newoption() method for use by cli/options/*.rb files
25
25
  class Option
26
- DEFAULT_WEIGHT = 999
27
26
  def self.has_weight(w) # rubocop:disable Style/PredicateName
28
27
  @weight = w
29
28
  end
@@ -38,7 +37,9 @@ module OctocatalogDiff
38
37
  elsif @weight
39
38
  @weight
40
39
  else
41
- DEFAULT_WEIGHT
40
+ # :nocov:
41
+ raise ArgumentError, "Option #{name} does not have a weight specified. Add 'has_weight NNN' to control ordering."
42
+ # :nocov:
42
43
  end
43
44
  end
44
45
 
@@ -102,6 +103,7 @@ module OctocatalogDiff
102
103
  datatype = opts.fetch(:datatype, '')
103
104
  return option_globally_or_per_branch_string(opts) if datatype.is_a?(String)
104
105
  return option_globally_or_per_branch_array(opts) if datatype.is_a?(Array)
106
+ return option_globally_or_per_branch_boolean(opts) if datatype.is_a?(TrueClass) || datatype.is_a?(FalseClass)
105
107
  raise ArgumentError, "option_globally_or_per_branch not equipped to handle #{datatype.class}"
106
108
  end
107
109
 
@@ -176,6 +178,40 @@ module OctocatalogDiff
176
178
  end
177
179
  end
178
180
 
181
+ # See description of `option_globally_or_per_branch`. This implements the logic for a boolean value.
182
+ # @param :parser [OptionParser object] The OptionParser argument
183
+ # @param :options [Hash] Options hash being constructed; this is modified in this method.
184
+ # @param :cli_name [String] Name of option on command line (e.g. puppet-binary)
185
+ # @param :option_name [Symbol] Name of option in the options hash (e.g. :puppet_binary)
186
+ # @param :desc [String] Description of option on the command line; will have "for the XX branch" appended
187
+ def self.option_globally_or_per_branch_boolean(opts)
188
+ parser = opts.fetch(:parser)
189
+ options = opts.fetch(:options)
190
+ cli_name = opts.fetch(:cli_name)
191
+ option_name = opts.fetch(:option_name)
192
+ desc = opts.fetch(:desc)
193
+
194
+ flag = cli_name
195
+ from_option = "from_#{option_name}".to_sym
196
+ to_option = "to_#{option_name}".to_sym
197
+ parser.on("--[no-]#{flag}", "#{desc} globally") do |x|
198
+ translated = translate_option(opts[:translator], x)
199
+ options[to_option] = translated
200
+ options[from_option] = translated
201
+ post_process(opts[:post_process], options)
202
+ end
203
+ parser.on("--[no-]to-#{flag}", "#{desc} for the to branch") do |x|
204
+ translated = translate_option(opts[:translator], x)
205
+ options[to_option] = translated
206
+ post_process(opts[:post_process], options)
207
+ end
208
+ parser.on("--[no-]from-#{flag}", "#{desc} for the from branch") do |x|
209
+ translated = translate_option(opts[:translator], x)
210
+ options[from_option] = translated
211
+ post_process(opts[:post_process], options)
212
+ end
213
+ end
214
+
179
215
  # If a validator was provided, run the validator on the supplied value. The validator is expected to
180
216
  # throw an error if there is a problem. Note that the validator runs *before* the translator if both
181
217
  # a validator and translator are supplied.
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Set hostname, which is used to look up facts in PuppetDB, and in the header of diff display.
4
+ # This option can recieve a single hostname, or a comma separated list of
5
+ # multiple hostnames, which are split into an Array. Multiple hostnames do not
6
+ # work with the `catalog-only` or `bootstrap-then-exit` options.
4
7
  # @param parser [OptionParser object] The OptionParser argument
5
8
  # @param options [Hash] Options hash being constructed; this is modified in this method.
6
9
 
@@ -8,8 +11,16 @@ OctocatalogDiff::Cli::Options::Option.newoption(:hostname) do
8
11
  has_weight 1
9
12
 
10
13
  def parse(parser, options)
11
- parser.on('--hostname HOSTNAME', '-n', 'Use PuppetDB facts from last run of hostname') do |hostname|
12
- options[:node] = hostname
14
+ parser.on(
15
+ '--hostname HOSTNAME1[,HOSTNAME2[,...]]',
16
+ '-n',
17
+ 'Use PuppetDB facts from last run of a hostname or a comma separated list of multiple hostnames'
18
+ ) do |hostname|
19
+ options[:node] = if hostname.include?(',')
20
+ hostname.split(',')
21
+ else
22
+ hostname
23
+ end
13
24
  end
14
25
  end
15
26
  end
@@ -12,7 +12,7 @@ OctocatalogDiff::Cli::Options::Option.newoption(:pe_enc_token_file) do
12
12
  def parse(parser, options)
13
13
  parser.on('--pe-enc-token-file PATH', 'Path containing token for PE node classifier, relative or absolute') do |x|
14
14
  proposed_token_path = x.start_with?('/') ? x : File.join(options[:basedir], x)
15
- raise Errno::ENOENT, "Provided token (#{proposed_token_path}) does not exist" unless File.file?(proposed_token_path)
15
+ raise Errno::ENOENT, "Provided PE ENC token (#{proposed_token_path}) does not exist" unless File.file?(proposed_token_path)
16
16
  options[:pe_enc_token] = File.read(proposed_token_path)
17
17
  end
18
18
  end
@@ -14,8 +14,8 @@ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_api_version) do
14
14
  options: options,
15
15
  cli_name: 'puppet-master-api-version',
16
16
  option_name: 'puppet_master_api_version',
17
- desc: 'Puppet Master API version (2 for Puppet 3.x, 3 for Puppet 4.x)',
18
- validator: ->(x) { x =~ /^[23]$/ || raise(ArgumentError, 'Only API versions 2 and 3 are supported') },
17
+ desc: 'Puppet Master API version (2 for Puppet 3.x, 3 for Puppet 4.x, 4 for Puppet Server >= 6.3.0)',
18
+ validator: ->(x) { x =~ /^[234]$/ || raise(ArgumentError, 'Only API versions 2, 3, and 4 are supported') },
19
19
  translator: ->(x) { x.to_i }
20
20
  )
21
21
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify a PE RBAC token used to authenticate to Puppetserver for v4
4
+ # catalog API calls.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_token) do
8
+ has_weight 310
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: '',
15
+ cli_name: 'puppet-master-token',
16
+ option_name: 'puppet_master_token',
17
+ desc: 'PE RBAC token to authenticate to the Puppetserver API v4'
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify a path to a file containing a PE RBAC token used to authenticate to the
4
+ # Puppetserver for a v4 catalog API call.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_token_file) do
8
+ has_weight 300
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: '',
15
+ cli_name: 'puppet-master-token-file',
16
+ option_name: 'puppet_master_token_file',
17
+ desc: 'File containing PE RBAC token to authenticate to the Puppetserver API v4',
18
+ translator: ->(x) { x.start_with?('/', '~') ? x : File.join(options[:basedir], x) },
19
+ post_process: lambda do |opts|
20
+ %w(to from).each do |prefix|
21
+ fileopt = "#{prefix}_puppet_master_token_file".to_sym
22
+ tokenopt = "#{prefix}_puppet_master_token".to_sym
23
+
24
+ tokenfile = opts[fileopt]
25
+ next if tokenfile.nil?
26
+
27
+ raise(Errno::ENOENT, "Token file #{tokenfile} is not readable") unless File.readable?(tokenfile)
28
+
29
+ token = File.read(tokenfile).strip
30
+ opts[tokenopt] ||= token
31
+ end
32
+ end
33
+ )
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify if, when using the Puppetserver v4 catalog API, the Puppetserver should
4
+ # update the catalog in PuppetDB.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_update_catalog) do
8
+ has_weight 320
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: false,
15
+ cli_name: 'puppet-master-update-catalog',
16
+ option_name: 'puppet_master_update_catalog',
17
+ desc: 'Update catalog in PuppetDB when using Puppetmaster API version 4'
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify if, when using the Puppetserver v4 catalog API, the Puppetserver should
4
+ # update the facts in PuppetDB.
5
+ # @param parser [OptionParser object] The OptionParser argument
6
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
7
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppet_master_update_facts) do
8
+ has_weight 320
9
+
10
+ def parse(parser, options)
11
+ OctocatalogDiff::Cli::Options.option_globally_or_per_branch(
12
+ parser: parser,
13
+ options: options,
14
+ datatype: false,
15
+ cli_name: 'puppet-master-update-facts',
16
+ option_name: 'puppet_master_update_facts',
17
+ desc: 'Update facts in PuppetDB when using Puppetmaster API version 4'
18
+ )
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When pulling facts from PuppetDB in a Puppet Enterprise environment, also include
4
+ # the Puppet Enterprise Package Inventory data in the fact results, if available.
5
+ # Generally you should not need to specify this, but including the package inventory
6
+ # data will produce a more accurate set of input facts for environments using
7
+ # package inventory.
8
+ # @param parser [OptionParser object] The OptionParser argument
9
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
10
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppetdb_package_inventory) do
11
+ has_weight 150
12
+
13
+ def parse(parser, options)
14
+ parser.on('--[no-]puppetdb-package-inventory', 'Include Puppet Enterprise package inventory data, if found') do |x|
15
+ options[:puppetdb_package_inventory] = x
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify the PE RBAC token to access the PuppetDB API. Refer to
4
+ # https://puppet.com/docs/pe/latest/rbac/rbac_token_auth_intro.html#generate-a-token-using-puppet-access
5
+ # for details on generating and obtaining a token. Use this option to specify the text
6
+ # of the token. (Use --puppetdb-token-file to read the content of the token from a file.)
7
+ # @param parser [OptionParser object] The OptionParser argument
8
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
9
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppetdb_token) do
10
+ has_weight 310
11
+
12
+ def parse(parser, options)
13
+ parser.on('--puppetdb-token TOKEN', 'Token to access the PuppetDB API') do |token|
14
+ options[:puppetdb_token] = token
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Specify the PE RBAC token to access the PuppetDB API. Refer to
4
+ # https://puppet.com/docs/pe/latest/rbac/rbac_token_auth_intro.html#generate-a-token-using-puppet-access
5
+ # for details on generating and obtaining a token. Use this option to specify the text
6
+ # in a file, to read the content of the token from the file.
7
+ # @param parser [OptionParser object] The OptionParser argument
8
+ # @param options [Hash] Options hash being constructed; this is modified in this method.
9
+ OctocatalogDiff::Cli::Options::Option.newoption(:puppetdb_token_file) do
10
+ has_weight 310
11
+
12
+ def parse(parser, options)
13
+ parser.on('--puppetdb-token-file PATH', 'Path containing token for PuppetDB API, relative or absolute') do |x|
14
+ proposed_token_path = x.start_with?('/') ? x : File.join(options[:basedir], x)
15
+ unless File.file?(proposed_token_path)
16
+ raise Errno::ENOENT, "Provided PuppetDB API token (#{proposed_token_path}) does not exist"
17
+ end
18
+ options[:puppetdb_token] = File.read(proposed_token_path)
19
+ end
20
+ end
21
+ end
@@ -36,6 +36,7 @@ module OctocatalogDiff
36
36
  exception_class = nil
37
37
  exception_message = nil
38
38
  obj_to_return = nil
39
+ packages = nil
39
40
  (retries + 1).times do
40
41
  begin
41
42
  result = puppetdb.get(uri)
@@ -61,8 +62,48 @@ module OctocatalogDiff
61
62
  exception_message = "Fact retrieval failed for node #{node} from PuppetDB (#{exc.message})"
62
63
  end
63
64
  end
64
- return obj_to_return unless obj_to_return.nil?
65
- raise exception_class, exception_message
65
+
66
+ raise exception_class, exception_message if obj_to_return.nil?
67
+
68
+ return obj_to_return if puppetdb_api_version < 4 || (!options[:puppetdb_package_inventory])
69
+
70
+ (retries + 1).times do
71
+ begin
72
+ result = puppetdb.get("/pdb/query/v4/package-inventory/#{node}")
73
+ packages = {}
74
+ result.each do |pkg|
75
+ key = "#{pkg['package_name']}+#{pkg['provider']}"
76
+ # Need to handle the situation where a package has multiple versions installed.
77
+ # The _puppet_inventory_1 hash lists them separated by "; ".
78
+ if packages.key?(key)
79
+ packages[key]['version'] += "; #{pkg['version']}"
80
+ else
81
+ packages[key] = pkg
82
+ end
83
+ end
84
+ break
85
+ rescue OctocatalogDiff::Errors::PuppetDBConnectionError => exc
86
+ exception_class = OctocatalogDiff::Errors::FactSourceError
87
+ exception_message = "Package inventory retrieval failed (#{exc.class}) (#{exc.message})"
88
+ # This is not expected to occur, but we'll leave it just in case. A query to package-inventory
89
+ # for a non-existant node returns a 200 OK with an empty list of packages:
90
+ rescue OctocatalogDiff::Errors::PuppetDBNodeNotFoundError
91
+ packages = {}
92
+ rescue OctocatalogDiff::Errors::PuppetDBGenericError => exc
93
+ exception_class = OctocatalogDiff::Errors::FactRetrievalError
94
+ exception_message = "Package inventory retrieval failed for node #{node} from PuppetDB (#{exc.message})"
95
+ end
96
+ end
97
+
98
+ raise exception_class, exception_message if packages.nil?
99
+
100
+ unless packages.empty?
101
+ obj_to_return['values']['_puppet_inventory_1'] = {
102
+ 'packages' => packages.values.map { |pkg| [pkg['package_name'], pkg['version'], pkg['provider']] }
103
+ }
104
+ end
105
+
106
+ obj_to_return
66
107
  end
67
108
  end
68
109
  end
@@ -42,6 +42,7 @@ module OctocatalogDiff
42
42
  # @param :puppetdb_ssl_client_p12 [String] pkcs12-encoded client key and certificate
43
43
  # @param :puppetdb_ssl_client_password [String] Path to file containing password for SSL client key (any format)
44
44
  # @param :puppetdb_ssl_client_auth [Boolean] Override the client-auth that is guessed from parameters
45
+ # @param :puppetdb_token [String] PE RBAC token to authenticate to PuppetDB API
45
46
  # @param :timeout [Integer] Connection timeout for PuppetDB (default=10)
46
47
  def initialize(options = {})
47
48
  @connections =
@@ -107,7 +108,10 @@ module OctocatalogDiff
107
108
  ].join('')
108
109
 
109
110
  begin
110
- more_options = { headers: { 'Accept' => 'application/json' }, timeout: @timeout }
111
+ headers = { 'Accept' => 'application/json' }
112
+ headers['X-Authentication'] = @options[:puppetdb_token] if @options[:puppetdb_token]
113
+ more_options = { headers: headers, timeout: @timeout }
114
+
111
115
  if connection[:username] || connection[:password]
112
116
  more_options[:basic_auth] = { username: connection[:username], password: connection[:password] }
113
117
  end
@@ -129,22 +129,26 @@ module OctocatalogDiff
129
129
 
130
130
  # Waiting for children and handling results
131
131
  while pidmap.any?
132
- this_pid, exit_obj = Process.wait2(0)
133
- next unless this_pid && pidmap.key?(this_pid)
134
- index = pidmap[this_pid][:index]
135
- exitstatus = exit_obj.exitstatus
136
- raise "PID=#{this_pid} exited abnormally: #{exit_obj.inspect}" if exitstatus.nil?
137
- raise "PID=#{this_pid} exited with status #{exitstatus}" unless exitstatus.zero?
138
-
139
- input = File.read(File.join(ipc_tempdir, "#{this_pid}.dat"))
140
- result[index] = Marshal.load(input) # rubocop:disable Security/MarshalLoad
141
- time_delta = Time.now - pidmap[this_pid][:start_time]
142
- pidmap.delete(this_pid)
143
-
144
- logger.debug "PID=#{this_pid} completed in #{time_delta} seconds, #{input.length} bytes"
145
-
146
- next if result[index].status
147
- return result[index].exception
132
+ pidmap.each do |pid|
133
+ status = Process.waitpid2(pid[0], Process::WNOHANG)
134
+ next if status.nil?
135
+ this_pid, exit_obj = status
136
+ next unless this_pid && pidmap.key?(this_pid)
137
+ index = pidmap[this_pid][:index]
138
+ exitstatus = exit_obj.exitstatus
139
+ raise "PID=#{this_pid} exited abnormally: #{exit_obj.inspect}" if exitstatus.nil?
140
+ raise "PID=#{this_pid} exited with status #{exitstatus}" unless exitstatus.zero?
141
+
142
+ input = File.read(File.join(ipc_tempdir, "#{this_pid}.dat"))
143
+ result[index] = Marshal.load(input) # rubocop:disable Security/MarshalLoad
144
+ time_delta = Time.now - pidmap[this_pid][:start_time]
145
+ pidmap.delete(this_pid)
146
+
147
+ logger.debug "PID=#{this_pid} completed in #{time_delta} seconds, #{input.length} bytes"
148
+
149
+ next if result[index].status
150
+ return result[index].exception
151
+ end
148
152
  end
149
153
 
150
154
  logger.debug 'All child processes completed with no exceptions raised'