process_executer 3.0.0 → 3.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a13a780c8c9064d19873068266b40be2a2a8ba7fe1d46866d6e8cb15806d5e53
4
- data.tar.gz: e727aab59452dac6ef74819bff2462ad0e1eb56c5b0c1ff1f7e018aed5dbdf77
3
+ metadata.gz: 3af694308b0e9c5119b2ff63d11b6b351a25cc9f7d66ac2dd482c8525736595a
4
+ data.tar.gz: a669d2c3cceadeb7544be9765793eff22addc8c7bde10e13e4d36de447b26cb5
5
5
  SHA512:
6
- metadata.gz: 6e349893ee5fbf19410e35e2a3a43907ccb1b1610f266788e0bd01b972ccc0e875fa2df95a9f9b90aefa0d8910f535a9d2ad34432332df07dbd58a33d299169d
7
- data.tar.gz: 29d7455610df17c93b8616fcff42fe44077f46de3e855eba573b45071e8bd51322240763529e2c5b578191ff20ada709073bac2989e6bbfcca20e0687fac09f5
6
+ metadata.gz: 670dcd425f0879def69e0875a90edae0f79280b44d4ea73d117b3875ad3eeb1cd6329f4b2d8a6a8c0d601f7b79eb72989288255d57067714f2e830f4dde3ddb2
7
+ data.tar.gz: 6a18db9777ecb26ebaa7b30c3fbadb81ae8adba1dcd60c92bf6d30510f30275336ef34c4b1c05c5df6356bcd62cd5b783736adb25462dd55d9d87deec6015945
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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.0 (2025-04-04)
9
+
10
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v3.1.0..v3.2.0)
11
+
12
+ Changes since v3.1.0:
13
+
14
+ * 272d246 test: fix flaky test that fails on windows
15
+ * 1e121d8 test: add test for raising a SpawnError when Process.spawn raises an error
16
+ * 2a2aaac refactor: improve synchronization of the monitored pipe state
17
+
18
+ ## v3.1.0 (2025-04-01)
19
+
20
+ [Full Changelog](https://github.com/main-branch/process_executer/compare/v3.0.0..v3.1.0)
21
+
22
+ Changes since v3.0.0:
23
+
24
+ * acb6385 fix: give Windows enough time to release its file lock so tmpdir can be deleted
25
+ * 3fe114a feat: wrap errors raised by `Process.spawn` in a `ProcessExecuter::SpawnError`
26
+
8
27
  ## v3.0.0 (2025-03-18)
9
28
 
10
29
  [Full Changelog](https://github.com/main-branch/process_executer/compare/v2.0.0..v3.0.0)
data/README.md CHANGED
@@ -179,6 +179,9 @@ following features:
179
179
  * It raises an error if there is any problem with the subprocess. This behavior can
180
180
  be turned off with the `raise_errors: false` option.
181
181
 
182
+ ⚠️ `ProcessIOError` and `SpawnError` errors are not suppressed by giving the
183
+ `raise_errors: false` option.
184
+
182
185
  ```ruby
183
186
  result = ProcessExecuter.run('echo "Hello World"', out: StringIO.new)
184
187
  result.stdout #=> "Hello World\n"
@@ -17,7 +17,8 @@ module ProcessExecuter
17
17
  # │ ├─> FailedError
18
18
  # │ └─> SignaledError
19
19
  # │ └─> TimeoutError
20
- # └─> ProcessIOError
20
+ # ├─> ProcessIOError
21
+ # └─> SpawnError
21
22
  # ```
22
23
  #
23
24
  # | Error Class | Description |
@@ -28,6 +29,7 @@ module ProcessExecuter
28
29
  # | `SignaledError` | Raised when the command is terminated as a result of receiving a signal. This could happen if the process is forcibly terminated or if there is a serious system error. |
29
30
  # | `TimeoutError` | This is a specific type of `SignaledError` that is raised when the command times out and is killed via the SIGKILL signal. Raised when the operation takes longer than the specified timeout duration (if provided). |
30
31
  # | `ProcessIOError` | Raised when an error was encountered reading or writing to the command's subprocess. |
32
+ # | `SpawnError` | Raised when the process could not execute. Check the |
31
33
  #
32
34
  # @example Rescuing any error
33
35
  # begin
@@ -129,6 +131,14 @@ module ProcessExecuter
129
131
  # @api public
130
132
  #
131
133
  class ProcessIOError < ProcessExecuter::Error; end
134
+
135
+ # Raised when spawn could not execute the process
136
+ #
137
+ # See the `cause` for the exception that Process.spawn raised.
138
+ #
139
+ # @api public
140
+ #
141
+ class SpawnError < ProcessExecuter::Error; end
132
142
  end
133
143
 
134
144
  # rubocop:enable Layout/LineLength
@@ -58,14 +58,12 @@ module ProcessExecuter
58
58
 
59
59
  assert_destination_is_compatible_with_monitored_pipe
60
60
 
61
+ @mutex = Mutex.new
62
+ @condition_variable = ConditionVariable.new
61
63
  @chunk_size = chunk_size
62
64
  @pipe_reader, @pipe_writer = IO.pipe
63
65
  @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
66
+ @thread = start_monitoring_thread
69
67
  end
70
68
 
71
69
  # Set the state to `:closing` and wait for the state to be set to `:closed`
@@ -84,10 +82,17 @@ module ProcessExecuter
84
82
  # @return [void]
85
83
  #
86
84
  def close
87
- return unless state == :open
85
+ mutex.synchronize do
86
+ return unless state == :open
88
87
 
89
- @state = :closing
90
- sleep 0.001 until state == :closed
88
+ @state = :closing
89
+ end
90
+
91
+ mutex.synchronize do
92
+ condition_variable.wait(mutex) while @state != :closed
93
+ end
94
+
95
+ thread.join
91
96
 
92
97
  destination.close
93
98
  end
@@ -152,9 +157,11 @@ module ProcessExecuter
152
157
  # @api private
153
158
  #
154
159
  def write(data)
155
- raise IOError, 'closed stream' unless state == :open
160
+ mutex.synchronize do
161
+ raise IOError, 'closed stream' unless state == :open
156
162
 
157
- pipe_writer.write(data)
163
+ pipe_writer.write(data)
164
+ end
158
165
  end
159
166
 
160
167
  # @!attribute [r]
@@ -255,6 +262,28 @@ module ProcessExecuter
255
262
 
256
263
  private
257
264
 
265
+ # @!attribute [r]
266
+ #
267
+ # The mutex used to synchronize access to the state variable
268
+ #
269
+ # @return [Mutex]
270
+ #
271
+ # @api private
272
+ #
273
+ attr_reader :mutex
274
+
275
+ # @!attribute [r]
276
+ #
277
+ # The condition variable used to synchronize access to the state
278
+ #
279
+ # In particular, it is used while waiting for the state to change to :closed
280
+ #
281
+ # @return [ConditionVariable]
282
+ #
283
+ # @api private
284
+ #
285
+ attr_reader :condition_variable
286
+
258
287
  # Raise an error if the destination is not compatible with MonitoredPipe
259
288
  # @return [void]
260
289
  # @raise [ArgumentError] if the destination is not compatible with MonitoredPipe
@@ -265,6 +294,17 @@ module ProcessExecuter
265
294
  raise ArgumentError, "Destination #{destination.destination} is not compatible with MonitoredPipe"
266
295
  end
267
296
 
297
+ # Start the thread to monitor the pipe and write data to the destination
298
+ # @return [void]
299
+ # @api private
300
+ def start_monitoring_thread
301
+ Thread.new do
302
+ Thread.current.report_on_exception = false
303
+ Thread.current.abort_on_exception = false
304
+ monitor
305
+ end
306
+ end
307
+
268
308
  # Read data from the pipe until `#state` is changed to `:closing`
269
309
  #
270
310
  # The state is changed to `:closed` by calling `#close`.
@@ -275,8 +315,12 @@ module ProcessExecuter
275
315
  # @api private
276
316
  def monitor
277
317
  monitor_pipe until state == :closing
318
+ ensure
278
319
  close_pipe
279
- @state = :closed
320
+ mutex.synchronize do
321
+ @state = :closed
322
+ condition_variable.signal
323
+ end
280
324
  end
281
325
 
282
326
  # Read data from the pipe until `#state` is changed to `:closing`
@@ -310,8 +354,10 @@ module ProcessExecuter
310
354
  def write_data(data)
311
355
  destination.write(data)
312
356
  rescue StandardError => e
313
- @exception = e
314
- @state = :closing
357
+ mutex.synchronize do
358
+ @exception = e
359
+ @state = :closing
360
+ end
315
361
  end
316
362
 
317
363
  # Read any remaining data from the pipe and close it
@@ -42,8 +42,7 @@ module ProcessExecuter
42
42
  # @param command [Array<String>] The command to execute
43
43
  # @param options [ProcessExecuter::Options::RunOptions] Options for running the command
44
44
  #
45
- # @raise [ProcessExecuter::ProcessIOError] If an exception was raised while collecting subprocess output
46
- # @raise [ProcessExecuter::TimeoutError] If the command times out
45
+ # @raise [ProcessExecuter::Error] if the command could not be executed or failed
47
46
  #
48
47
  # @return [ProcessExecuter::Result] The result of the completed subprocess
49
48
  #
@@ -100,10 +99,7 @@ module ProcessExecuter
100
99
  #
101
100
  # @return [Void]
102
101
  #
103
- # @raise [ProcessExecuter::FailedError] If the command failed
104
- # @raise [ProcessExecuter::SignaledError] If the command was signaled
105
- # @raise [ProcessExecuter::TimeoutError] If the command times out
106
- # @raise [ProcessExecuter::ProcessIOError] If an exception was raised while collecting subprocess output
102
+ # @raise [ProcessExecuter::Error] if the command could not be executed or failed
107
103
  #
108
104
  # @api private
109
105
  #
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ProcessExecuter
4
4
  # The current Gem version
5
- VERSION = '3.0.0'
5
+ VERSION = '3.2.0'
6
6
  end
@@ -87,7 +87,11 @@ module ProcessExecuter
87
87
  # @return [ProcessExecuter::Result] The result of the completed subprocess
88
88
  # @api private
89
89
  def self.spawn_and_wait_with_options(command, options)
90
- pid = Process.spawn(*command, **options.spawn_options)
90
+ begin
91
+ pid = Process.spawn(*command, **options.spawn_options)
92
+ rescue StandardError => e
93
+ raise ProcessExecuter::SpawnError, "Failed to spawn process: #{e.message}"
94
+ end
91
95
  wait_for_process(pid, command, options)
92
96
  end
93
97
 
@@ -287,10 +291,7 @@ module ProcessExecuter
287
291
  # @option options_hash [String] :chdir (nil) The directory to run the command in
288
292
  # @option options_hash [Logger] :logger The logger to use
289
293
  #
290
- # @raise [ProcessExecuter::FailedError] if the command returned a non-zero exit status
291
- # @raise [ProcessExecuter::SignaledError] if the command exited because of an unhandled signal
292
- # @raise [ProcessExecuter::TimeoutError] if the command timed out
293
- # @raise [ProcessExecuter::ProcessIOError] if an exception was raised while collecting subprocess output
294
+ # @raise [ProcessExecuter::Error] if the command could not be executed or failed
294
295
  #
295
296
  # @return [ProcessExecuter::Result] The result of the completed subprocess
296
297
  #
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: process_executer
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-18 00:00:00.000000000 Z
10
+ date: 2025-04-05 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler-audit
@@ -250,8 +250,8 @@ metadata:
250
250
  allowed_push_host: https://rubygems.org
251
251
  homepage_uri: https://github.com/main-branch/process_executer
252
252
  source_code_uri: https://github.com/main-branch/process_executer
253
- documentation_uri: https://rubydoc.info/gems/process_executer/3.0.0
254
- changelog_uri: https://rubydoc.info/gems/process_executer/3.0.0/file/CHANGELOG.md
253
+ documentation_uri: https://rubydoc.info/gems/process_executer/3.2.0
254
+ changelog_uri: https://rubydoc.info/gems/process_executer/3.2.0/file/CHANGELOG.md
255
255
  rubygems_mfa_required: 'true'
256
256
  rdoc_options: []
257
257
  require_paths: