octocatalog-diff 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'