process_executer 3.1.0 → 3.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6cedd27c98fa7e8e5d7ac1d134a514cdad1bac02fec17e88045729c644d978d
4
- data.tar.gz: 3c61d911e0134dd3d083be1773d44910b0218c268733e7bd10b547b83b67dfe4
3
+ metadata.gz: 0a9debe1b364e7755e593434f5b018b6ef39d64f509132b2f803ebe1be1bfe36
4
+ data.tar.gz: 780091f37f642962c1bbaf885cd22559defe52d568294884df23aac0eb575318
5
5
  SHA512:
6
- metadata.gz: 1963744fe4e899bc099f1641587e3e9d722bcf8f4ee1c88795918eadddc9b4f55324d59779f2be0804bf08a2c47267b0d28d6e625fba2588c1dedd075049f484
7
- data.tar.gz: 004562c12d9ba5ff76a6a9a0b028c0817631eb3fddbca2dac329a1d594ca843ba08e44b6b33cbaea7292b073d2bdd861bb47b738f7d4eaba506ead916606b5d4
6
+ metadata.gz: fc21311fca209fc28ce413c7562a8d80a3268c90bf34941525422db8cdd0656549c5e8ca285c4cfee4711f0d9ad8babe4de55676d4ed0fbf77f2001b4ebd17fe
7
+ data.tar.gz: 689feaf01cc890fcb978c78e24c16ad912e4e64cfa3494ecd6d825ceaf86662d46e8239528e4f985ed9a5365cc32498c7d618d1203aebf1da0a34bfedf868228
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to the process_executer gem will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v3.2.1 (2025-04-08)
9
+
10
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v3.2.0..v3.2.1)
11
+
12
+ Changes since v3.2.0:
13
+
14
+ * d1e19a5 test: assert that MonitoredPipe has no open instances after each test
15
+ * aa71f8e test: ensure MonitoredPipe cleans up open instances in specs
16
+ * 987b0c9 fix: ensure that all pipes are closed even when there is an IOError
17
+ * 65e8db0 fix: ensure that MonitoredPipe cleans up after itself even when there is IOError
18
+ * ed2454e chore: integrate track_open_instances gem to report on leaked MonitoredPipe instances
19
+ * f25c87d chore: release v3.2.0
20
+
21
+ ## v3.2.0 (2025-04-04)
22
+
23
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v3.1.0..v3.2.0)
24
+
25
+ Changes since v3.1.0:
26
+
27
+ * 272d246 test: fix flaky test that fails on windows
28
+ * 1e121d8 test: add test for raising a SpawnError when Process.spawn raises an error
29
+ * 2a2aaac refactor: improve synchronization of the monitored pipe state
30
+
8
31
  ## v3.1.0 (2025-04-01)
9
32
 
10
33
  [Full Changelog](https://github.com/main-branch/process_executer/compare/v3.0.0..v3.1.0)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'stringio'
4
4
  require 'io/wait'
5
+ require 'track_open_instances'
5
6
 
6
7
  module ProcessExecuter
7
8
  # Write data sent through a pipe to a destination
@@ -41,6 +42,8 @@ module ProcessExecuter
41
42
  # @api public
42
43
  #
43
44
  class MonitoredPipe
45
+ include TrackOpenInstances
46
+
44
47
  # Create a new monitored pipe
45
48
  #
46
49
  # Creates a IO.pipe and starts a monitoring thread to read data written to the pipe.
@@ -58,14 +61,14 @@ module ProcessExecuter
58
61
 
59
62
  assert_destination_is_compatible_with_monitored_pipe
60
63
 
64
+ @mutex = Mutex.new
65
+ @condition_variable = ConditionVariable.new
61
66
  @chunk_size = chunk_size
62
67
  @pipe_reader, @pipe_writer = IO.pipe
63
68
  @state = :open
64
- @thread = Thread.new do
65
- Thread.current.report_on_exception = false
66
- Thread.current.abort_on_exception = false
67
- monitor
68
- end
69
+ @thread = start_monitoring_thread
70
+
71
+ self.class.add_open_instance(self)
69
72
  end
70
73
 
71
74
  # Set the state to `:closing` and wait for the state to be set to `:closed`
@@ -84,12 +87,16 @@ module ProcessExecuter
84
87
  # @return [void]
85
88
  #
86
89
  def close
87
- return unless state == :open
88
-
89
- @state = :closing
90
- sleep 0.001 until state == :closed
90
+ mutex.synchronize do
91
+ if state == :open
92
+ @state = :closing
93
+ condition_variable.wait(mutex) while @state != :closed
94
+ end
95
+ end
91
96
 
97
+ thread.join
92
98
  destination.close
99
+ self.class.remove_open_instance(self)
93
100
  end
94
101
 
95
102
  # Return the write end of the pipe so that data can be written to it
@@ -152,9 +159,11 @@ module ProcessExecuter
152
159
  # @api private
153
160
  #
154
161
  def write(data)
155
- raise IOError, 'closed stream' unless state == :open
162
+ mutex.synchronize do
163
+ raise IOError, 'closed stream' unless state == :open
156
164
 
157
- pipe_writer.write(data)
165
+ pipe_writer.write(data)
166
+ end
158
167
  end
159
168
 
160
169
  # @!attribute [r]
@@ -255,6 +264,28 @@ module ProcessExecuter
255
264
 
256
265
  private
257
266
 
267
+ # @!attribute [r]
268
+ #
269
+ # The mutex used to synchronize access to the state variable
270
+ #
271
+ # @return [Mutex]
272
+ #
273
+ # @api private
274
+ #
275
+ attr_reader :mutex
276
+
277
+ # @!attribute [r]
278
+ #
279
+ # The condition variable used to synchronize access to the state
280
+ #
281
+ # In particular, it is used while waiting for the state to change to :closed
282
+ #
283
+ # @return [ConditionVariable]
284
+ #
285
+ # @api private
286
+ #
287
+ attr_reader :condition_variable
288
+
258
289
  # Raise an error if the destination is not compatible with MonitoredPipe
259
290
  # @return [void]
260
291
  # @raise [ArgumentError] if the destination is not compatible with MonitoredPipe
@@ -265,6 +296,17 @@ module ProcessExecuter
265
296
  raise ArgumentError, "Destination #{destination.destination} is not compatible with MonitoredPipe"
266
297
  end
267
298
 
299
+ # Start the thread to monitor the pipe and write data to the destination
300
+ # @return [void]
301
+ # @api private
302
+ def start_monitoring_thread
303
+ Thread.new do
304
+ Thread.current.report_on_exception = false
305
+ Thread.current.abort_on_exception = false
306
+ monitor
307
+ end
308
+ end
309
+
268
310
  # Read data from the pipe until `#state` is changed to `:closing`
269
311
  #
270
312
  # The state is changed to `:closed` by calling `#close`.
@@ -275,8 +317,12 @@ module ProcessExecuter
275
317
  # @api private
276
318
  def monitor
277
319
  monitor_pipe until state == :closing
320
+ ensure
278
321
  close_pipe
279
- @state = :closed
322
+ mutex.synchronize do
323
+ @state = :closed
324
+ condition_variable.signal
325
+ end
280
326
  end
281
327
 
282
328
  # Read data from the pipe until `#state` is changed to `:closing`
@@ -310,8 +356,10 @@ module ProcessExecuter
310
356
  def write_data(data)
311
357
  destination.write(data)
312
358
  rescue StandardError => e
313
- @exception = e
314
- @state = :closing
359
+ mutex.synchronize do
360
+ @exception = e
361
+ @state = :closing
362
+ end
315
363
  end
316
364
 
317
365
  # Read any remaining data from the pipe and close it
@@ -52,7 +52,8 @@ module ProcessExecuter
52
52
  opened_pipes = wrap_stdout_stderr(options)
53
53
  ProcessExecuter.spawn_and_wait_with_options(command, options)
54
54
  ensure
55
- opened_pipes.each { |key, value| close_pipe(command, key, value) }
55
+ opened_pipes.each_value(&:close)
56
+ opened_pipes.each { |option_key, pipe| raise_pipe_error(command, option_key, pipe) }
56
57
  end
57
58
 
58
59
  # Wrap the stdout and stderr redirection options with a MonitoredPipe
@@ -81,16 +82,6 @@ module ProcessExecuter
81
82
  ProcessExecuter::Destinations.compatible_with_monitored_pipe?(value)
82
83
  end
83
84
 
84
- # Close the pipe and raise an error if the pipe raised an exception
85
- # @return [void]
86
- # @raise [ProcessExecuter::ProcessIOError] If an exception was raised while
87
- # collecting subprocess output
88
- # @api private
89
- def close_pipe(command, option_key, pipe)
90
- pipe.close
91
- raise_pipe_error(command, option_key, pipe) if pipe.exception
92
- end
93
-
94
85
  # Process the result of the command and return a ProcessExecuter::Result
95
86
  #
96
87
  # Log the command and result, and raise an error if the command failed.
@@ -144,6 +135,8 @@ module ProcessExecuter
144
135
  # @api private
145
136
  #
146
137
  def raise_pipe_error(command, option_key, pipe)
138
+ return unless pipe.exception
139
+
147
140
  error = ProcessExecuter::ProcessIOError.new("Pipe Exception for #{command}: #{option_key.inspect}")
148
141
  raise(error, cause: pipe.exception)
149
142
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ProcessExecuter
4
4
  # The current Gem version
5
- VERSION = '3.1.0'
5
+ VERSION = '3.2.1'
6
6
  end
@@ -37,6 +37,8 @@ Gem::Specification.new do |spec|
37
37
  'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
38
38
  ]
39
39
 
40
+ spec.add_dependency 'track_open_instances', '~> 0.1'
41
+
40
42
  spec.add_development_dependency 'bundler-audit', '~> 0.9'
41
43
  spec.add_development_dependency 'create_github_release', '~> 2.1'
42
44
  spec.add_development_dependency 'main_branch_shared_rubocop_config', '~> 0.1'
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_executer
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-01 00:00:00.000000000 Z
10
+ date: 2025-04-09 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: track_open_instances
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: bundler-audit
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -250,8 +264,8 @@ metadata:
250
264
  allowed_push_host: https://rubygems.org
251
265
  homepage_uri: https://github.com/main-branch/process_executer
252
266
  source_code_uri: https://github.com/main-branch/process_executer
253
- documentation_uri: https://rubydoc.info/gems/process_executer/3.1.0
254
- changelog_uri: https://rubydoc.info/gems/process_executer/3.1.0/file/CHANGELOG.md
267
+ documentation_uri: https://rubydoc.info/gems/process_executer/3.2.1
268
+ changelog_uri: https://rubydoc.info/gems/process_executer/3.2.1/file/CHANGELOG.md
255
269
  rubygems_mfa_required: 'true'
256
270
  rdoc_options: []
257
271
  require_paths: