process_executer 3.2.4 → 4.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +177 -134
  5. data/lib/process_executer/commands/run.rb +124 -0
  6. data/lib/process_executer/commands/run_with_capture.rb +148 -0
  7. data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
  8. data/lib/process_executer/commands.rb +11 -0
  9. data/lib/process_executer/destinations/child_redirection.rb +5 -4
  10. data/lib/process_executer/destinations/close.rb +5 -4
  11. data/lib/process_executer/destinations/destination_base.rb +73 -0
  12. data/lib/process_executer/destinations/file_descriptor.rb +10 -6
  13. data/lib/process_executer/destinations/file_path.rb +12 -6
  14. data/lib/process_executer/destinations/file_path_mode.rb +10 -6
  15. data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
  16. data/lib/process_executer/destinations/io.rb +10 -5
  17. data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
  18. data/lib/process_executer/destinations/stderr.rb +8 -4
  19. data/lib/process_executer/destinations/stdout.rb +8 -4
  20. data/lib/process_executer/destinations/tee.rb +24 -17
  21. data/lib/process_executer/destinations/writer.rb +12 -7
  22. data/lib/process_executer/destinations.rb +32 -17
  23. data/lib/process_executer/errors.rb +50 -26
  24. data/lib/process_executer/monitored_pipe.rb +128 -59
  25. data/lib/process_executer/options/base.rb +118 -82
  26. data/lib/process_executer/options/option_definition.rb +5 -1
  27. data/lib/process_executer/options/run_options.rb +13 -12
  28. data/lib/process_executer/options/run_with_capture_options.rb +156 -0
  29. data/lib/process_executer/options/spawn_options.rb +31 -30
  30. data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
  31. data/lib/process_executer/options.rb +3 -1
  32. data/lib/process_executer/result.rb +35 -77
  33. data/lib/process_executer/result_with_capture.rb +62 -0
  34. data/lib/process_executer/version.rb +2 -1
  35. data/lib/process_executer.rb +384 -346
  36. data/process_executer.gemspec +11 -2
  37. metadata +18 -8
  38. data/lib/process_executer/destination_base.rb +0 -83
  39. data/lib/process_executer/runner.rb +0 -144
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48ed2573bb17348a6b021a3354b360361ed39d4040bda74f6f2a9f1997d79616
4
- data.tar.gz: 8697e82bb6713eccbf0f4f58d0709c435b39fc62a53abece990070a53ab81a5d
3
+ metadata.gz: f8f23dd316f574cbf2194c9e75b825f03e34c86719d38ba626c22ce26c8d213b
4
+ data.tar.gz: ccc618656594d909774c872706fc27d8440bf6a26170d678ad3058fef3cf6310
5
5
  SHA512:
6
- metadata.gz: efbeab2d5a4608f322969a2b5c0f7a948ff25cd717a19d32c7bf0708652c5d923c638bb43bee27cea6ac5687d52c10252d6bf92b7a3f07a21f88f3e3d5b580ca
7
- data.tar.gz: '09de2958475e4581f86c27208fbb148afc1860f054c4c7651b7adc32255a89fd41072c327284e5afa93dcc0d6456c1d0e680358419304588cbc8250b88859cc1'
6
+ metadata.gz: 0e06d8087a256e103b0352bb4f6e4215cdd0d3e7810fb3206337c211aa45d309d7deaa8e760c41815fe81171ac9258a441c6a8f22b5a4c4fa2465c4c28f59314
7
+ data.tar.gz: 7dc33006e39f0f35399741017cb584f132ef89ea50c0dd8081556a12c2dd48ad19a0bf8adbe915f9be241d54374a3d0856ffe54d4c778d1b2b0bc9e4eeef44b3
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "3.2.4"
2
+ ".": "4.0.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,47 @@ 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
+ ## [4.0.0](https://github.com/main-branch/process_executer/compare/v3.2.4...v4.0.0) (2025-06-05)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * Users who call ProcessExecuter::Options::Base#with even if from a derived class will need to update to use #merge instead.
14
+ * Users depending on `Result#stdout` or `Result#stderr` will either have to capture this output manually themselves or change from `spawn_and_wait`/`run` to `run_with_capture`.
15
+ * calls to `ProcessExecuter.spawn_with_timeout_with_options` and `ProcessExecuter.run_with_options` have been removed. Use `ProcessExecuter.spawn_with_timeout` and `ProcessExecuter.run` instead.
16
+ * Users who use ProcessExecuter.spawn_and_wait will need to update their calls to spawn_with_timeout. In addition, the following items will need to be updated if used by the user of this gem:
17
+ * ProcessExecuter.spawn_and_wait_with_options
18
+ * ProcessExecuter::SpawnAndWaitOptions
19
+ * In places where users of this gem rescued ::ArgumentError, they will have to change the rescued class to ProcessExecuter::ArgumentError.
20
+
21
+ ### Features
22
+
23
+ * Add `ProcessExecuter.run_with_capture` ([d9e97fe](https://github.com/main-branch/process_executer/commit/d9e97fe7728a0c7fce9520ad5ba9568782243f70))
24
+ * Add encoding, stdout_encoding, stderr_encoding options to RunWithCaptureOptions ([83eaa93](https://github.com/main-branch/process_executer/commit/83eaa93417e810c1a1b569616c19d8facdc2b2f4))
25
+ * Add ProcessExecuter::ArgumentError and raise it instead of ::ArgumentError ([860fc5a](https://github.com/main-branch/process_executer/commit/860fc5a224f86dd4ff525de32b643fc261e456f6))
26
+ * Ensure that all data written by MonitoredPipe is ASCII-8BIT encoded ([8753006](https://github.com/main-branch/process_executer/commit/87530066280ed91afc208714514df845e4455b6b))
27
+ * Make run_with_capture encode captured stdout and stderr based on encoding options ([75c3d92](https://github.com/main-branch/process_executer/commit/75c3d922fa74a17b61b415fe50acd9763a00524f))
28
+ * Remove #spawn_with_timeout_with_options and #run_with_options methods ([446cb51](https://github.com/main-branch/process_executer/commit/446cb510a634ff5171df6b0fcb2d426cb3f9ed9e))
29
+ * Remove Result#stdout and Result#stderr ([2dcad47](https://github.com/main-branch/process_executer/commit/2dcad47bb921170070f8d4bae1ce07244547db5c))
30
+ * Rename ProcessExecuter::Options::Base#with to #merge ([7e8c28e](https://github.com/main-branch/process_executer/commit/7e8c28e33b99945187d272766134e30b5746ddc8))
31
+ * Rename ProcessExecuter.spawn_and_wait to spawn_with_timeout ([b9d19e7](https://github.com/main-branch/process_executer/commit/b9d19e792234996f78c7cd63b22047bb7474a06d))
32
+
33
+
34
+ ### Bug Fixes
35
+
36
+ * Fix new rubocop offense Style/EmptyStringInsideInterpolation ([bb610af](https://github.com/main-branch/process_executer/commit/bb610af96519cccd2fe1be62e61b1531711e5d9b))
37
+
38
+
39
+ ### Other Changes
40
+
41
+ * Add a JRuby 10 build to the continuous integration workflow ([7a939ba](https://github.com/main-branch/process_executer/commit/7a939ba5bf7b291555a4259db7040b6cb96f494b))
42
+ * Document the new encoding options on ProcessExecuter.run_with_capture ([c86ce62](https://github.com/main-branch/process_executer/commit/c86ce627b8c081981a92414c35eafb85b99e3201))
43
+ * Ensure that binary data is correctly written to file destinations ([0d2db54](https://github.com/main-branch/process_executer/commit/0d2db54d2b9c4354cb880a17a6b683dc8b5f8424))
44
+ * Fix indentation in README ([1837e7a](https://github.com/main-branch/process_executer/commit/1837e7a5cee56ccc7f3ebdc7f71aab43d1a56ab4))
45
+ * Internally refactor classes for clarity and update documentation ([da1db96](https://github.com/main-branch/process_executer/commit/da1db9697e5c2be371d308d5790e44dcccf8e40b))
46
+ * Remove unneeded :nocov: blocks ([7a1fcf5](https://github.com/main-branch/process_executer/commit/7a1fcf500b89d7fe8e7254ba4fa20e38f0b46d45))
47
+ * Update the README with all the changes for the latest release ([38206a5](https://github.com/main-branch/process_executer/commit/38206a57d26dcca3437611291ab3ccdc1d5a442f))
48
+
8
49
  ## [3.2.4](https://github.com/main-branch/process_executer/compare/v3.2.3...v3.2.4) (2025-04-18)
9
50
 
10
51
 
data/README.md CHANGED
@@ -15,176 +15,190 @@ It has additional features like capturing output, handling timeouts, streaming o
15
15
  to multiple destinations, and providing detailed result information.
16
16
 
17
17
  This README documents the HEAD version of process_executer which may contain
18
- unrelease information. To see the README for the version you are using, consult
18
+ unreleased information. To see the README for the version you are using, consult
19
19
  RubyGems.org. Go to the [process_executer page in
20
20
  RubyGems.org](https://rubygems.org/gems/process_executer), select your version, and
21
21
  then click the "Documentation" link.
22
22
 
23
23
  ## Requirements
24
24
 
25
- * Ruby 3.1.0 or later
26
- * Compatible with MRI 3.1+, TruffleRuby 24+, and JRuby 9.4+
27
- * Works on Mac, Linux, and Windows platforms
28
-
29
- ## Table of Contents
30
-
31
- * [Requirements](#requirements)
32
- * [Table of Contents](#table-of-contents)
33
- * [Usage](#usage)
34
- * [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
35
- * [ProcessExecuter::Result](#processexecuterresult)
36
- * [ProcessExecuter.spawn\_and\_wait](#processexecuterspawn_and_wait)
37
- * [ProcessExecuter.run](#processexecuterrun)
38
- * [Breaking Changes](#breaking-changes)
39
- * [2.x](#2x)
40
- * [`ProcessExecuter.spawn`](#processexecuterspawn)
41
- * [`ProcessExecuter.run`](#processexecuterrun-1)
42
- * [`ProcessExecuter::Result`](#processexecuterresult-1)
43
- * [Other](#other)
44
- * [3.x](#3x)
45
- * [`ProcessExecuter.run`](#processexecuterrun-2)
46
- * [Installation](#installation)
47
- * [Contributing](#contributing)
48
- * [Reporting Issues](#reporting-issues)
49
- * [Developing](#developing)
50
- * [Commit message guidelines](#commit-message-guidelines)
51
- * [Pull request guidelines](#pull-request-guidelines)
52
- * [Releasing](#releasing)
53
- * [License](#license)
25
+ - Ruby 3.1.0 or later
26
+ - Compatible with MRI 3.1+, TruffleRuby 24+, and JRuby 9.4+
27
+ - Works on Mac, Linux, and Windows platforms
28
+
29
+ ## Table of contents
30
+
31
+ - [Requirements](#requirements)
32
+ - [Table of contents](#table-of-contents)
33
+ - [Usage](#usage)
34
+ - [Key methods](#key-methods)
35
+ - [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
36
+ - [Encoding](#encoding)
37
+ - [Encoding summary](#encoding-summary)
38
+ - [Encoding details](#encoding-details)
39
+ - [Breaking Changes](#breaking-changes)
40
+ - [2.x](#2x)
41
+ - [`ProcessExecuter.spawn`](#processexecuterspawn)
42
+ - [`ProcessExecuter.run`](#processexecuterrun)
43
+ - [`ProcessExecuter::Result`](#processexecuterresult)
44
+ - [Other](#other)
45
+ - [3.x](#3x)
46
+ - [`ProcessExecuter.run`](#processexecuterrun-1)
47
+ - [4.x](#4x)
48
+ - [`ProcessExecuter.spawn_and_wait`](#processexecuterspawn_and_wait)
49
+ - [`ProcessExecuter::Result`](#processexecuterresult-1)
50
+ - [`ProcessExecuter.spawn_and_wait_with_options`](#processexecuterspawn_and_wait_with_options)
51
+ - [`ProcessExecuter.run_with_options`](#processexecuterrun_with_options)
52
+ - [Other](#other-1)
53
+ - [Installation](#installation)
54
+ - [Contributing](#contributing)
55
+ - [Reporting Issues](#reporting-issues)
56
+ - [Developing](#developing)
57
+ - [Commit message guidelines](#commit-message-guidelines)
58
+ - [Pull request guidelines](#pull-request-guidelines)
59
+ - [Releasing](#releasing)
60
+ - [License](#license)
54
61
 
55
62
  ## Usage
56
63
 
57
64
  [Full YARD documentation](https://rubydoc.info/gems/process_executer/) for this gem
58
65
  is hosted on RubyGems.org. Read below for an overview and several examples.
59
66
 
60
- This gem contains two public classes and two public methods:
67
+ ### Key methods
61
68
 
62
- Classes:
69
+ ℹ️ See [the ProcessExecuter module
70
+ documentation](https://rubydoc.info/gems/process_executer/ProcessExecuter) for
71
+ more details and examples of using the methods described here.
63
72
 
64
- * `ProcessExecuter::MonitoredPipe`: allows use of any object with a `#write` method
65
- or an array of objects as a redirection destination in `Process.spawn`
66
- * `ProcessExecuter::Result`: an extension of `Process::Status` that includes more
67
- information about the subprocess including timeout status, the command that was
68
- run, the subprocess options given, and (in some cases) stdout and stderr captured
69
- from the subprocess.
73
+ The `ProcessExecuter` module provides extended versions of
74
+ [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) that
75
+ block while the command is executing. These methods provide enhanced features such as
76
+ timeout handling, more flexible redirection options, logging, error raising, and
77
+ output capturing.
70
78
 
71
- Methods:
79
+ The interface of these methods is the same as the standard library
80
+ [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn)
81
+ method but with additional options.
72
82
 
73
- * `ProcessExecuter.spawn_and_wait`: execute a subprocess and wait for it to exit with
74
- an optional timeout. Supports the same interface and features as `Process.spawn`.
75
- * `ProcessExecuter.run`: builds upon `.spawn_and_wait` adding (1) automatically
76
- wrapping stdout and stderr destinations (if given) in a `MonitoredPipe` and (2)
77
- raises errors for any problem executing the subprocess (can be turned off).
83
+ These methods are:
84
+
85
+ | Method | Description |
86
+ |--------|-------------|
87
+ | `ProcessExecuter.spawn_with_timeout` | Extends [Process.spawn](https://docs.ruby-lang.org/en/3.4/Process.html#method-c-spawn) to run a command and wait (with timeout) for it to finish |
88
+ | `ProcessExecuter.run` | Extends `spawn_with_timeout` with more flexible redirection and other options. |
89
+ | `ProcessExecuter.run_with_capture` | Extends `run` with capture of stdout and stderr |
90
+
91
+ See the `ProcessExecuter::Error` class for the error architecture for this module.
78
92
 
79
93
  ### ProcessExecuter::MonitoredPipe
80
94
 
81
- `ProcessExecuter::MonitoredPipe` objects can be used as a redirection destination for
82
- `Process.spawn` to stream output from a subprocess to one or more destinations.
83
- Destinations are given in this class's initializer.
95
+ ℹ️ See [the ProcessExecuter::MonitoredPipe class
96
+ documentation](https://rubydoc.info/gems/process_executer/ProcessExecuter/MonitoredPipe)
97
+ for more details and examples of using this class.
84
98
 
85
- The destinations are all the redirection destinations allowed by `Process.spawn` plus
86
- the following:
99
+ `ProcessExecuter::MonitoredPipe` was created to expand the output redirection options
100
+ for `Process.spawn` and methods derived from it within the `ProcessExecuter` module.
87
101
 
88
- * Any object with a #write method even if it does not have a file descriptor (like
89
- instances of StringIO)
90
- * An array of destinations so that output can be tee'd to several sources
102
+ This class's initializer accepts any compatible redirection destination supported by
103
+ `Process.spawn` (this is the `value` part of the file redirection option described in
104
+ [the File Redirection section of
105
+ `Process.spawn`](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-File+Redirection+-28File+Descriptor-29).
91
106
 
92
- Example of capturing stdout to a StringIO (which is not directly possible with
93
- `Process.spawn`):
107
+ In addition to the standard redirection destinations, `MonitoredPipe` also
108
+ supports these additional types of destinations:
94
109
 
95
- ```ruby
96
- require 'stringio'
97
- require 'process_executer'
98
-
99
- output_buffer = StringIO.new
100
- out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer)
101
- pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
102
- out_pipe.close # Close the pipe so all the data is flushed and resources are not leaked
103
- output_buffer.string #=> "Hello World\n"
104
- ```
110
+ - **Arbitrary writers**
105
111
 
106
- Any object that implements `#write` can be used as a destination (not just StringIO).
107
- For instance, you can use it to parse process output as a stream which might be useful
108
- for long XML or JSON output.
112
+ You can redirect subprocess output to any Ruby object that implements the
113
+ `#write` method. This is particularly useful for:
109
114
 
110
- Example of tee'ing stdout to multiple destinations:
115
+ - capturing command output in in-memory buffers like `StringIO`,
116
+ - sending command output to custom logging objects that do not have a file descriptor, and
117
+ - processing with a streaming parser to parse and process command output as the
118
+ command runs
111
119
 
112
- ```ruby
113
- require 'stringio'
114
- require 'process_executer'
115
-
116
- output_buffer = StringIO.new
117
- output_file = File.open('process.out', 'w')
118
- out_pipe = ProcessExecuter::MonitoredPipe.new([:tee, output_buffer, output_file])
119
- pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
120
- out_pipe.close
121
- output_file.close
122
- output_buffer.string #=> "Hello World\n"
123
- File.read('process.out') #=> "Hello World\n"
124
- ```
120
+ - **Multiple destinations**
125
121
 
126
- ### ProcessExecuter::Result
122
+ MonitoredPipe supports duplicating (or "teeing") output to multiple
123
+ destinations simultaneously. This is achieved by providing an array in the
124
+ format `[:tee, destination1, destination2, ...]`, where each `destination` can
125
+ be any value that `MonitoredPipe` itself supports (including another tee or
126
+ MonitoredPipe).
127
127
 
128
- An instance of this class is returned from both `.spawn_and_wait` and `.run`.
128
+ ### Encoding
129
129
 
130
- This class is an extension of
131
- [Process::Status](https://docs.ruby-lang.org/en/3.3/Process/Status.html) so it
132
- supports the same interface with the following additions:
130
+ #### Encoding summary
133
131
 
134
- * `#command`: the command given to `.spawn_and_wait` or `.run`
135
- * `#options`: the options given to `.spawn_and_wait` or `.run` (possibly with some
136
- changes)
137
- * `#timed_out?`: true if the process was killed after running for `:timeout_after`
138
- seconds
139
- * `#elapsed_time`: the number of seconds the process was running
140
- * `#stdout`: the captured stdout from the subprocess (if the stdout destination was
141
- wrapped by a `MonitoredPipe`)
142
- * `#stderr`: the captured stderr from the subprocess (if the stderr destination was
143
- wrapped by a `MonitoredPipe`)
132
+ The gem's core (`MonitoredPipe`) passes through raw bytes from the subprocess without
133
+ attempting to interpret or transcode them. `ProcessExecuter.run_with_capture` allows
134
+ text encodings to be specified for the captured stdout and stderr (defaulting to
135
+ `UTF-8`). For these outputs, the raw bytes are interpreted as being in that specified
136
+ encoding. The original byte sequence is preserved and the resulting captured string
137
+ is tagged with the target encoding. No transcoding between different text encodings
138
+ (e.g., `Latin-1` to `UTF-8`) is performed.
144
139
 
145
- ### ProcessExecuter.spawn_and_wait
140
+ #### Encoding details
146
141
 
147
- `ProcessExecuter.spawn_and_wait` has the same interface and features as
148
- [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
149
- with the following differences:
142
+ `ProcessExecuter::MonitoredPipe` is encoding agnostic. Bytes pass through this class
143
+ from the subprocesses output to the destination object as a stream of unaltered
144
+ bytes. No transcoding is applied. Strings written to the destination are tagged for
145
+ the ASCII-8BIT (aka BINARY) encoding.
150
146
 
151
- 1. It waits for the subprocess to exit
152
- 2. A timeout can be specified using the `:timeout_after` option
153
- 3. It returns a `ProcessExecuter::Result` instead of a `Process::Status`
147
+ `ProcessExecuter` methods `.spawn_with_timeout`, `.run`, and `.run_with_capture` are
148
+ also encoding agnostic except with one exception: the user can specify the assumed
149
+ encoding for strings returned from `ResultWithCapture#stdout` and
150
+ `ResultWithCapture#stderr`.
154
151
 
155
- If the command does not terminate before the number of seconds specified by
156
- `:timeout_after`, the process is killed by sending it the SIGKILL signal. The
157
- returned Result object's `timed_out?` attribute will return `true`. For example:
152
+ As a convenience, the captured output is assumed to be UTF-8 by default:
158
153
 
159
154
  ```ruby
160
- result = ProcessExecuter.spawn_and_wait('sleep 10', timeout_after: 0.01)
161
- result.signaled? #=> true
162
- result.termsig #=> 9
163
- result.timed_out? #=> true
155
+ result = ProcessExecuter.run_with_capture('pwd')
156
+ result.stdout #=> "/Users/James/projects/process_executer\n"
157
+ result.stdout.encoding #=> #<Encoding::UTF-8>
164
158
  ```
165
159
 
166
- If the destination for stdout and stderr are wrapped by a
167
- ProcessExecuter::MonitoredPipe, the result will return the stdout and stderr
168
- subprocess output from its `#stdout` and `#stderr` methods.
160
+ You can changed the assumed encoding for the captured stdout and stderr via options
161
+ passed to `#run_with_capture`:
169
162
 
170
- ### ProcessExecuter.run
163
+ ```ruby
164
+ # Set the assumed encoding for both stdout and stderr
165
+ result = ProcessExecuter.run_with_capture('pwd', encoding: Encoding::BINARY)
166
+ result.stdout #=> "/Users/James/projects/process_executer\n"
167
+ result.stdout.encoding #=> #<Encoding:BINARY (ASCII-8BIT)>
168
+
169
+ # You can set the assumed encoding separately for stdout and stderr
170
+ # Encoding may be different for each
171
+ result = ProcessExecuter.run_with_capture('pwd', stdout_encoding: 'BINARY', stderr_encoding: 'UTF-8')
172
+ result.stdout.encoding #=> #<Encoding:BINARY (ASCII-8BIT)>
173
+ result.stderr.encoding #=> #<Encoding:UTF-8>
174
+ ```
171
175
 
172
- `ProcessExecuter.run` builds upon `ProcessExecuter.spawn_and_wait` adding the
173
- following features:
176
+ It is possible that the bytes captured are not valid in the given encoding. The user
177
+ will need to check the `#valid_encoding?` method to know for sure.
174
178
 
175
- * It automatically wraps any given stdout and stderr destination with a
176
- MonitoredPipe. The pipe will be closed when the command exits.
177
- * It raises an error if there is any problem with the subprocess. This behavior can
178
- be turned off with the `raise_errors: false` option.
179
+ ```ruby
180
+ File.binwrite('output.txt', "\xFF\xFE") # little-endian BOM marker is not valid UTF-8
181
+ result = ProcessExecuter.run_with_capture('cat output.txt')
182
+ result.stdout #=> "\xFF\xFE"
183
+ result.stdout.encoding #=> #<Encoding:UTF-8>
184
+ result.stdout.valid_encoding? #=> false
185
+ ```
179
186
 
180
- ⚠️ `ProcessIOError` and `SpawnError` errors are not suppressed by giving the
181
- `raise_errors: false` option.
187
+ Encoding options accept any encoding objects returned by `Encoding.list` or their
188
+ String equivalent given by `#to_s`:
182
189
 
183
190
  ```ruby
184
- result = ProcessExecuter.run('echo "Hello World"', out: StringIO.new)
185
- result.stdout #=> "Hello World\n"
191
+ Encoding::UTF_8.to_s #=> 'UTF-8'
186
192
  ```
187
193
 
194
+ Changing the assumed encoding DOES NOT cause transcoding. It simply interprets the
195
+ bytes captured as the given encoding.
196
+
197
+ These encoding options ONLY affect the internally captured stdout and stderr for
198
+ `ProcessExecuter::run_with_capture`. If you give an `out:` or `err:` option, these
199
+ will result in BINARY encoded strings and you will need to handle setting the right
200
+ encoding or transcoding after collecting the output.
201
+
188
202
  ## Breaking Changes
189
203
 
190
204
  ### 2.x
@@ -193,26 +207,26 @@ This major release focused on changes to the interface to make it more understan
193
207
 
194
208
  #### `ProcessExecuter.spawn`
195
209
 
196
- * This method was renamed to `ProcessExecuter.spawn_and_wait`
197
- * The `:timeout` option was renamed to `:timeout_after`
210
+ - This method was renamed to `ProcessExecuter.spawn_with_timeout`
211
+ - The `:timeout` option was renamed to `:timeout_after`
198
212
 
199
213
  #### `ProcessExecuter.run`
200
214
 
201
- * The `:timeout` option was renamed to `:timeout_after`
215
+ - The `:timeout` option was renamed to `:timeout_after`
202
216
 
203
217
  #### `ProcessExecuter::Result`
204
218
 
205
- * The `#timeout` method was renamed to `#timed_out`
219
+ - The `#timeout` method was renamed to `#timed_out`
206
220
 
207
221
  #### Other
208
222
 
209
- * Dropped support for Ruby 3.0
223
+ - Dropped support for Ruby 3.0
210
224
 
211
225
  ### 3.x
212
226
 
213
227
  #### `ProcessExecuter.run`
214
228
 
215
- * The `:merge` option was removed
229
+ - The `:merge` option was removed
216
230
 
217
231
  This was removed because `Process.spawn` already provides this functionality but in
218
232
  a different way. To merge, you will need to define a redirection where the source
@@ -224,7 +238,7 @@ This major release focused on changes to the interface to make it more understan
224
238
 
225
239
  will merge stdout and stderr from the subprocess into the file output.txt.
226
240
 
227
- * Stdout and stderr redirections are no longer default to a new instance of StringIO
241
+ - Stdout and stderr redirections no longer default to new instances of `StringIO`
228
242
 
229
243
  Calls to `ProcessExecuter.run` that do not define a redirection for stdout or
230
244
  stderr will have to add explicit redirection(s) in order to capture the output.
@@ -233,6 +247,35 @@ This major release focused on changes to the interface to make it more understan
233
247
  an explicit redirection is not given for stdout and stderr, this output will be
234
248
  passed through to the parent process's stdout and stderr.
235
249
 
250
+ ### 4.x
251
+
252
+ #### `ProcessExecuter.spawn_and_wait`
253
+
254
+ `ProcessExecuter.spawn_and_wait` has been renamed to `ProcessExecuter.spawn_with_timeout`.
255
+
256
+ #### `ProcessExecuter::Result`
257
+
258
+ `Result#stdout` and `Result#stderr` were removed. Users depending on these methods
259
+ will either have to capture this output themselves or change from using
260
+ `.spawn_and_wait`/`.run` to `.run_with_capture` which returns a `ResultWithCapture`
261
+ object.
262
+
263
+ #### `ProcessExecuter.spawn_and_wait_with_options`
264
+
265
+ `ProcessExecuter.spawn_and_wait_with_options` has been removed. Instead call
266
+ `ProcessExecuter.spawn_with_timeout` which is overloaded to take the same method
267
+ arguments.
268
+
269
+ #### `ProcessExecuter.run_with_options`
270
+
271
+ `ProcessExecuter.run_with_options` has been removed. Instead call
272
+ `ProcessExecuter.run` which is overloaded to take the same method arguments.
273
+
274
+ #### Other
275
+
276
+ In places where users of this gem rescued `::ArgumentError`, they will have to change
277
+ the rescued class to `ProcessExecuter::ArgumentError`.
278
+
236
279
  ## Installation
237
280
 
238
281
  Install the gem and add to the application's Gemfile by executing:
@@ -271,11 +314,11 @@ effectively.
271
314
 
272
315
  To ensure compliance, this project includes:
273
316
 
274
- * A git commit-msg hook that validates your commit messages before they are accepted.
317
+ - A git commit-msg hook that validates your commit messages before they are accepted.
275
318
 
276
- To activate the hook, you must have node installed and run `npm install`.
319
+ To activate the hook, you must have Node.js installed and run `npm install`.
277
320
 
278
- * A GitHub Actions workflow that will enforce the Conventional Commit standard as
321
+ - A GitHub Actions workflow that will enforce the Conventional Commit standard as
279
322
  part of the continuous integration pipeline.
280
323
 
281
324
  Any commit message that does not conform to the Conventional Commits standard will
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'spawn_with_timeout'
5
+
6
+ module ProcessExecuter
7
+ module Commands
8
+ # Run a command and return the {ProcessExecuter::Result}
9
+ #
10
+ # Extends {ProcessExecuter::Commands::SpawnWithTimeout} to provide the core functionality for
11
+ # {ProcessExecuter.run}.
12
+ #
13
+ # It accepts all [Process.spawn execution
14
+ # options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options)
15
+ # plus the additional options `timeout_after`, `raise_errors` and `logger`.
16
+ #
17
+ # This class wraps any stdout or stderr redirection destinations in a {MonitoredPipe}.
18
+ # This allows any class that implements `#write` to be used as an output redirection
19
+ # destination. This means that you can redirect to a StringIO which is not possible
20
+ # with `Process.spawn`.
21
+ #
22
+ # @api private
23
+ #
24
+ class Run < SpawnWithTimeout
25
+ # Run a command and return the result
26
+ #
27
+ # Wrap the stdout and stderr redirection destinations in pipes and then execute
28
+ # the command.
29
+ #
30
+ # @example
31
+ # options = ProcessExecuter::Options::RunOptions.new(raise_errors: true)
32
+ # result = ProcessExecuter::Commands::Run.new('echo hello', options).call
33
+ # result.success? # => true
34
+ # result.exitstatus # => 0
35
+ #
36
+ # @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
37
+ # command was run
38
+ #
39
+ # @raise [ProcessExecuter::FailedError] If the command ran and failed
40
+ #
41
+ # @raise [ProcessExecuter::SignaledError] If the command ran and terminated due to
42
+ # an unhandled signal
43
+ #
44
+ # @raise [ProcessExecuter::TimeoutError] If the command timed out
45
+ #
46
+ # @raise [ProcessExecuter::ProcessIOError] If there was an exception while
47
+ # collecting subprocess output
48
+ #
49
+ # @return [ProcessExecuter::Result] The result of the completed subprocess
50
+ #
51
+ def call
52
+ opened_pipes = wrap_stdout_stderr
53
+ super.tap do
54
+ log_result
55
+ raise_errors if options.raise_errors
56
+ end
57
+ ensure
58
+ opened_pipes.each_value(&:close)
59
+ opened_pipes.each { |option_key, pipe| raise_pipe_error(option_key, pipe) }
60
+ end
61
+
62
+ private
63
+
64
+ # Wrap the stdout and stderr redirection options with a MonitoredPipe
65
+ # @return [Hash<Object, ProcessExecuter::MonitoredPipe>] The opened pipes (the Object is the option key)
66
+ def wrap_stdout_stderr
67
+ options.each_with_object({}) do |key_value, opened_pipes|
68
+ key, value = key_value
69
+
70
+ next unless should_wrap?(key, value)
71
+
72
+ wrapped_destination = ProcessExecuter::MonitoredPipe.new(value)
73
+ opened_pipes[key] = wrapped_destination
74
+ options.merge!({ key => wrapped_destination })
75
+ end
76
+ end
77
+
78
+ # Should the redirection option be wrapped by a MonitoredPipe
79
+ # @param key [Object] The option key
80
+ # @param value [Object] The option value
81
+ # @return [Boolean] Whether the option should be wrapped
82
+ def should_wrap?(key, value)
83
+ (options.stdout_redirection?(key) || options.stderr_redirection?(key)) &&
84
+ ProcessExecuter::Destinations.compatible_with_monitored_pipe?(value)
85
+ end
86
+
87
+ # Raise an error if the command failed
88
+ # @return [void]
89
+ # @raise [ProcessExecuter::FailedError] If the command ran and failed
90
+ # @raise [ProcessExecuter::SignaledError] If the command ran and terminated due to an unhandled signal
91
+ # @raise [ProcessExecuter::TimeoutError] If the command timed out
92
+ def raise_errors
93
+ raise TimeoutError, result if result.timed_out?
94
+ raise SignaledError, result if result.signaled?
95
+ raise FailedError, result unless result.success?
96
+ end
97
+
98
+ # Log the result of running the command
99
+ # @return [void]
100
+ def log_result
101
+ options.logger.info { "PID #{pid}: #{command} exited with status #{result}" }
102
+ end
103
+
104
+ # Raises a ProcessIOError if the given pipe has a recorded exception
105
+ #
106
+ # @param option_key [Object] The redirection option key
107
+ #
108
+ # For example, `:out`, or an Array like `[:out, :err]` for merged streams.
109
+ #
110
+ # @param pipe [ProcessExecuter::MonitoredPipe] The pipe that raised the exception
111
+ #
112
+ # @raise [ProcessExecuter::ProcessIOError] If there was an exception while collecting subprocess output
113
+ #
114
+ # @return [void]
115
+ #
116
+ def raise_pipe_error(option_key, pipe)
117
+ return unless pipe.exception
118
+
119
+ error = ProcessExecuter::ProcessIOError.new("Pipe Exception for #{command}: #{option_key.inspect}")
120
+ raise(error, cause: pipe.exception)
121
+ end
122
+ end
123
+ end
124
+ end