process_executer 4.0.3 → 4.1.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 +4 -4
- data/.editorconfig +10 -0
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +13 -0
- data/.vscode/extensions.json +3 -0
- data/CHANGELOG.md +54 -0
- data/README.md +20 -0
- data/Rakefile +3 -0
- data/lib/process_executer/commands/run.rb +152 -10
- data/lib/process_executer/commands/run_with_capture.rb +41 -4
- data/lib/process_executer/commands/spawn_with_timeout.rb +185 -16
- data/lib/process_executer/destinations/monitored_pipe.rb +7 -4
- data/lib/process_executer/destinations.rb +1 -1
- data/lib/process_executer/errors.rb +2 -2
- data/lib/process_executer/monitored_pipe.rb +373 -44
- data/lib/process_executer/options/base.rb +34 -5
- data/lib/process_executer/options/run_with_capture_options.rb +70 -8
- data/lib/process_executer/options/spawn_options.rb +13 -0
- data/lib/process_executer/result.rb +13 -2
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +34 -2
- data/process_spawn_test/README.md +38 -1
- data/process_spawn_test/spec/spec_helper.rb +18 -0
- data/process_spawn_test/spec/test_spec.rb +21 -0
- data/release-please-config.json +1 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63acfcf3541790f98d140feac5a9badc901f8b4e475b3c97d125f6b549556bfc
|
|
4
|
+
data.tar.gz: 7c42a5cfa8b66fea530be0821443a974c1fabc1002fc2b3f2f35e533812c9c6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8d54aa2b9c5e1c153a4c92dbaeec6d96c3b80f31f6517ad2026519d070eff6d6c5b6fdc447cfbff481a6c15599c06ace734068f784e1f1ca8ba78831e84e0d7
|
|
7
|
+
data.tar.gz: fef70d1e6342139152e498dd72572f591f5f330f8617e7ad0603898638c08db8e2ec09aeb302e486949432d80a7635611654959869314946df13c1bf87ed4dd0
|
data/.editorconfig
ADDED
data/.rubocop.yml
CHANGED
|
@@ -5,3 +5,16 @@ AllCops:
|
|
|
5
5
|
# Pin this project to Ruby 3.1 in case the shared config above is upgraded to 3.2
|
|
6
6
|
# or later.
|
|
7
7
|
TargetRubyVersion: 3.1
|
|
8
|
+
|
|
9
|
+
# The default `native` style expects CRLF on Windows, but this project's files
|
|
10
|
+
# are LF everywhere. Pin the style so RuboCop gives the same result on Windows
|
|
11
|
+
# as it does on Linux and macOS.
|
|
12
|
+
Layout/EndOfLine:
|
|
13
|
+
EnforcedStyle: lf
|
|
14
|
+
|
|
15
|
+
# MonitoredPipe grew past the default limit when the close deadline handling
|
|
16
|
+
# was added for issue #164. Its pipe lifecycle logic (open, monitor, drain,
|
|
17
|
+
# close) is cohesive and would not be clearer split across classes.
|
|
18
|
+
Metrics/ClassLength:
|
|
19
|
+
Exclude:
|
|
20
|
+
- "lib/process_executer/monitored_pipe.rb"
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,60 @@ 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.1.0](https://github.com/main-branch/process_executer/compare/v4.0.4...v4.1.0) (2026-08-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Make timeout_after bound execution even when descendants hold stdout/stderr open ([a1298d3](https://github.com/main-branch/process_executer/commit/a1298d3f98fd8e57f58f50a0566dd3be78079189))
|
|
14
|
+
* **monitored_pipe:** Close acquired resources when #initialize fails partway ([8f10a86](https://github.com/main-branch/process_executer/commit/8f10a86b42c4d72f0f34004071cfeb91116d6bd4)), closes [#178](https://github.com/main-branch/process_executer/issues/178)
|
|
15
|
+
* **monitored_pipe:** Do not hold the mutex while writing to the pipe ([91d7fe8](https://github.com/main-branch/process_executer/commit/91d7fe88b9950d24f8c28ccbea400be0063ec99a)), closes [#165](https://github.com/main-branch/process_executer/issues/165)
|
|
16
|
+
* **monitored_pipe:** Handle a destination raising a non-StandardError ([a7de23b](https://github.com/main-branch/process_executer/commit/a7de23b5699ef4d32518e988ac76dd6a957277e3)), closes [#170](https://github.com/main-branch/process_executer/issues/170)
|
|
17
|
+
* Replace the monitor thread's 1ms poll with an event-driven wakeup pipe ([44068a3](https://github.com/main-branch/process_executer/commit/44068a35f4f87de9b7b46ce1deffda636f7c1138))
|
|
18
|
+
* Return a timed out result when the timeout races Process.wait2 reaping the child ([84a6a11](https://github.com/main-branch/process_executer/commit/84a6a11df7d736f26d98700e90a76f5d65e2d35c)), closes [#171](https://github.com/main-branch/process_executer/issues/171)
|
|
19
|
+
* **run_with_capture:** Capture combined [:out, :err] redirection into stdout ([17199b8](https://github.com/main-branch/process_executer/commit/17199b8ea332278680c46accce072763f0d67a4a)), closes [#167](https://github.com/main-branch/process_executer/issues/167)
|
|
20
|
+
* **run:** Close created pipes and keep the original error when pipe creation fails ([909eaa1](https://github.com/main-branch/process_executer/commit/909eaa177fa53ee4cd4e773595a8658ea94ca6a4)), closes [#166](https://github.com/main-branch/process_executer/issues/166)
|
|
21
|
+
* **run:** Stop mutating caller options and closing caller-owned pipes ([96d922b](https://github.com/main-branch/process_executer/commit/96d922b4bff5586189b30d9a1f83b0d47159710f)), closes [#169](https://github.com/main-branch/process_executer/issues/169)
|
|
22
|
+
* Validate options merged via Options::Base#merge! ([#173](https://github.com/main-branch/process_executer/issues/173)) ([da0a956](https://github.com/main-branch/process_executer/commit/da0a956bcb085378b1315dafb87351174500c966))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Other Changes
|
|
26
|
+
|
|
27
|
+
* Add failing tests for merge! bypassing option validation ([#173](https://github.com/main-branch/process_executer/issues/173)) ([325f16e](https://github.com/main-branch/process_executer/commit/325f16e589821571f3250e86a82f51f0e6b70f9a))
|
|
28
|
+
* Add failing tests for the monitor thread 1ms polling loop ([#172](https://github.com/main-branch/process_executer/issues/172)) ([e202909](https://github.com/main-branch/process_executer/commit/e202909f99074a8ceb331f13290c224c35904e7a))
|
|
29
|
+
* Add failing tests for the timeout vs Process.wait2 reap race ([#171](https://github.com/main-branch/process_executer/issues/171)) ([647739a](https://github.com/main-branch/process_executer/commit/647739ad7233059aafb54dad70eef14998892c5d))
|
|
30
|
+
* Add failing tests for timeout_after not bounding execution ([#164](https://github.com/main-branch/process_executer/issues/164)) ([8e5115c](https://github.com/main-branch/process_executer/commit/8e5115c9236347e77ba6e6d0631f5f1bfe826b26))
|
|
31
|
+
* **coverage:** Only enforce code coverage on MRI builds not on Windows ([160815e](https://github.com/main-branch/process_executer/commit/160815ecb091d1f155406f9a1b37f73e13690627))
|
|
32
|
+
* Enforce LF line endings on all platforms ([e94bfea](https://github.com/main-branch/process_executer/commit/e94bfeaf4bd93506090277b4020859cd23dfc625))
|
|
33
|
+
* Explain stdin redirection via the in: option ([f7bded7](https://github.com/main-branch/process_executer/commit/f7bded743a204a99c7f6e272b0330d4ba1e4a037)), closes [#159](https://github.com/main-branch/process_executer/issues/159)
|
|
34
|
+
* **monitored_pipe:** Accept the documented IOError and confine failures ([002274d](https://github.com/main-branch/process_executer/commit/002274d421afc84c7c441c6f56dbbc32506e1de2)), closes [#182](https://github.com/main-branch/process_executer/issues/182)
|
|
35
|
+
* **monitored_pipe:** Add a failing test for the #write deadlock ([9826558](https://github.com/main-branch/process_executer/commit/9826558035b03fe7becdc729053519141ba8d00d))
|
|
36
|
+
* **monitored_pipe:** Add failing tests for a destination raising a non-StandardError ([0177329](https://github.com/main-branch/process_executer/commit/0177329fb47f3c185d9ea768f9f80028c54fb7ab))
|
|
37
|
+
* **monitored_pipe:** Add failing tests for resource leaks when #initialize fails ([b2e3151](https://github.com/main-branch/process_executer/commit/b2e31519ea7a38f774b90f6e9e1f17fa0b31baa3)), closes [#178](https://github.com/main-branch/process_executer/issues/178)
|
|
38
|
+
* **monitored_pipe:** Make the writer-exception race deterministic ([c72f1e4](https://github.com/main-branch/process_executer/commit/c72f1e400ef3d78e4b092ecde0d0704cf79ebc36)), closes [#182](https://github.com/main-branch/process_executer/issues/182)
|
|
39
|
+
* **monitored_pipe:** Stub the monitor loop on the class, not the live instance ([799177f](https://github.com/main-branch/process_executer/commit/799177f6046d661013ce6b68b19c6b220294d00f))
|
|
40
|
+
* **process-spawn:** Check exit status and blocking, and add a subspawn backend ([99021d2](https://github.com/main-branch/process_executer/commit/99021d2144a21210f793b7ae94d36875eef504e6))
|
|
41
|
+
* **process-spawn:** Run the spawn tests against the subspawn backend ([573086f](https://github.com/main-branch/process_executer/commit/573086f194e790f25d916de32bb5e94437a5ae86))
|
|
42
|
+
* **rake:** Skip RuboCop in the default task on TruffleRuby ([3d2a118](https://github.com/main-branch/process_executer/commit/3d2a1186d65e76f170f5428d611822d95d8fdc2d))
|
|
43
|
+
* Release the hardening changes as v4.1.0 instead of v4.0.5 ([70b6327](https://github.com/main-branch/process_executer/commit/70b63278f334bd8b714c2fc28ded1520921add09))
|
|
44
|
+
* **run_with_capture:** Add failing tests for combined [:out, :err] redirection ([61e97cb](https://github.com/main-branch/process_executer/commit/61e97cb09c816cbe30d81dc33f268accf76ab396))
|
|
45
|
+
* **run:** Add failing tests for options mutation and caller-owned pipe closing ([cf32615](https://github.com/main-branch/process_executer/commit/cf326156381fe81562597906d43134a08ebbef17))
|
|
46
|
+
* **run:** Add failing tests for pipe creation failure in Run#call ([21521d9](https://github.com/main-branch/process_executer/commit/21521d9a9b5141d9bb6d22fb5fd3e7de0e497ca2))
|
|
47
|
+
* **run:** Update the JRuby expectation for an invalid chdir path ([2712f0b](https://github.com/main-branch/process_executer/commit/2712f0bdf060fa1831edb428183283dec5f2727a))
|
|
48
|
+
* Use RbConfig.ruby to locate the Ruby interpreter ([82e65e2](https://github.com/main-branch/process_executer/commit/82e65e202a4f884a3283b4af00fcd2b5eda3a394))
|
|
49
|
+
|
|
50
|
+
## [4.0.4](https://github.com/main-branch/process_executer/compare/v4.0.3...v4.0.4) (2026-04-24)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Bug Fixes
|
|
54
|
+
|
|
55
|
+
* **rubocop:** Fix offenses introduced by new rubocop cops ([b21ca99](https://github.com/main-branch/process_executer/commit/b21ca991641784146b0c92c1dd3205fc9d2c0f4f))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
### Other Changes
|
|
59
|
+
|
|
60
|
+
* **dependencies:** Update dependencies for all GitHub Actions workflows ([3524dbf](https://github.com/main-branch/process_executer/commit/3524dbf8b5945c233be96381f4555407b537f236))
|
|
61
|
+
|
|
8
62
|
## [4.0.3](https://github.com/main-branch/process_executer/compare/v4.0.2...v4.0.3) (2026-01-04)
|
|
9
63
|
|
|
10
64
|
|
data/README.md
CHANGED
|
@@ -26,6 +26,7 @@ then click the "Documentation" link.
|
|
|
26
26
|
- [Usage](#usage)
|
|
27
27
|
- [Key methods](#key-methods)
|
|
28
28
|
- [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
|
|
29
|
+
- [Redirecting stdin](#redirecting-stdin)
|
|
29
30
|
- [Encoding](#encoding)
|
|
30
31
|
- [Encoding summary](#encoding-summary)
|
|
31
32
|
- [Encoding details](#encoding-details)
|
|
@@ -119,6 +120,25 @@ supports these additional types of destinations:
|
|
|
119
120
|
be any value that `MonitoredPipe` itself supports (including another tee or
|
|
120
121
|
MonitoredPipe).
|
|
121
122
|
|
|
123
|
+
### Redirecting stdin
|
|
124
|
+
|
|
125
|
+
The `in:` option is passed directly to `Process.spawn` and is not wrapped in a
|
|
126
|
+
`MonitoredPipe`. It accepts any stdin redirection value that `Process.spawn`
|
|
127
|
+
supports, such as a filename (`in: 'input.txt'`) or an IO object with a file
|
|
128
|
+
descriptor. Unlike `out:` and `err:`, redirecting stdin from a `StringIO` is not
|
|
129
|
+
supported and will raise `ProcessExecuter::SpawnError`. To feed in-memory data
|
|
130
|
+
to a command's stdin, write it to a `Tempfile` and pass that:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
require 'tempfile'
|
|
134
|
+
|
|
135
|
+
Tempfile.create do |file|
|
|
136
|
+
file.write("HEAD\n")
|
|
137
|
+
file.rewind
|
|
138
|
+
result = ProcessExecuter.run('git cat-file --batch-check', in: file)
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
122
142
|
### Encoding
|
|
123
143
|
|
|
124
144
|
#### Encoding summary
|
data/Rakefile
CHANGED
|
@@ -6,6 +6,9 @@ desc 'Run the same tasks that the CI build will run'
|
|
|
6
6
|
|
|
7
7
|
if RUBY_PLATFORM == 'java'
|
|
8
8
|
task default: %w[spec rubocop bundle:audit build]
|
|
9
|
+
elsif RUBY_ENGINE == 'truffleruby'
|
|
10
|
+
# RuboCop raises internal errors when run on TruffleRuby, so skip it there
|
|
11
|
+
task default: %w[spec yard bundle:audit build]
|
|
9
12
|
else
|
|
10
13
|
task default: %w[spec rubocop yard bundle:audit build]
|
|
11
14
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'English'
|
|
4
|
+
|
|
3
5
|
require_relative '../errors'
|
|
4
6
|
require_relative 'spawn_with_timeout'
|
|
5
7
|
|
|
@@ -19,9 +21,29 @@ module ProcessExecuter
|
|
|
19
21
|
# destination. This means that you can redirect to a StringIO which is not possible
|
|
20
22
|
# with `Process.spawn`.
|
|
21
23
|
#
|
|
24
|
+
# The wrapper pipes are kept in an internal hash that is combined with the
|
|
25
|
+
# user's options only when `Process.spawn` is called. The options object the
|
|
26
|
+
# caller gave is never modified, so it can be reused for another run and
|
|
27
|
+
# `result.options` returns the destinations as the user configured them.
|
|
28
|
+
#
|
|
22
29
|
# @api private
|
|
23
30
|
#
|
|
24
31
|
class Run < SpawnWithTimeout
|
|
32
|
+
# Create a new Run instance
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# options = ProcessExecuter::Options::RunOptions.new(raise_errors: true)
|
|
36
|
+
# result = ProcessExecuter::Commands::Run.new('echo hello', options).call
|
|
37
|
+
# result.success? # => true
|
|
38
|
+
#
|
|
39
|
+
# @param command [Array<String>] The command to run in the subprocess
|
|
40
|
+
# @param options [ProcessExecuter::Options::RunOptions] The options to use when running the command
|
|
41
|
+
#
|
|
42
|
+
def initialize(command, options)
|
|
43
|
+
super
|
|
44
|
+
@redirection_overrides = {}
|
|
45
|
+
end
|
|
46
|
+
|
|
25
47
|
# Run a command and return the result
|
|
26
48
|
#
|
|
27
49
|
# Wrap the stdout and stderr redirection destinations in pipes and then execute
|
|
@@ -44,35 +66,130 @@ module ProcessExecuter
|
|
|
44
66
|
# @raise [ProcessExecuter::TimeoutError] If the command timed out
|
|
45
67
|
#
|
|
46
68
|
# @raise [ProcessExecuter::ProcessIOError] If there was an exception while
|
|
47
|
-
# collecting subprocess output
|
|
69
|
+
# collecting subprocess output, or the output was truncated because it
|
|
70
|
+
# could not be fully collected before the close timeout
|
|
48
71
|
#
|
|
49
72
|
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
|
50
73
|
#
|
|
51
74
|
def call
|
|
52
|
-
opened_pipes =
|
|
75
|
+
opened_pipes = {}
|
|
76
|
+
wrap_stdout_stderr(opened_pipes)
|
|
53
77
|
super.tap do
|
|
54
78
|
log_result
|
|
55
79
|
raise_errors if options.raise_errors
|
|
56
80
|
end
|
|
57
81
|
ensure
|
|
58
|
-
opened_pipes
|
|
59
|
-
opened_pipes.each { |option_key, pipe| raise_pipe_error(option_key, pipe) }
|
|
82
|
+
close_pipes_and_check_errors(opened_pipes, $ERROR_INFO)
|
|
60
83
|
end
|
|
61
84
|
|
|
62
85
|
private
|
|
63
86
|
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
# Redirection options to apply on top of the user's options at spawn time
|
|
88
|
+
#
|
|
89
|
+
# Holds the {MonitoredPipe} wrappers created by {#wrap_stdout_stderr} (and
|
|
90
|
+
# the capture redirections added by subclasses) keyed by the redirection
|
|
91
|
+
# option key. Keeping them here instead of writing them into {#options}
|
|
92
|
+
# leaves the caller's options object unmodified.
|
|
93
|
+
#
|
|
94
|
+
# @return [Hash<Object, Object>]
|
|
95
|
+
#
|
|
96
|
+
attr_reader :redirection_overrides
|
|
69
97
|
|
|
98
|
+
# The options to pass to Process.spawn
|
|
99
|
+
#
|
|
100
|
+
# The user's spawn options with the redirection destinations replaced by
|
|
101
|
+
# their internal {MonitoredPipe} wrappers.
|
|
102
|
+
#
|
|
103
|
+
# @return [Hash]
|
|
104
|
+
#
|
|
105
|
+
def spawn_options = super.merge(redirection_overrides)
|
|
106
|
+
|
|
107
|
+
# Wrap the stdout and stderr redirection options with a MonitoredPipe
|
|
108
|
+
#
|
|
109
|
+
# The wrapper pipes are recorded in {#redirection_overrides} instead of
|
|
110
|
+
# being written into {#options} so the caller's options object is not
|
|
111
|
+
# modified.
|
|
112
|
+
#
|
|
113
|
+
# Each pipe is added to `opened_pipes` as soon as it is created so that,
|
|
114
|
+
# if creating a later pipe raises, the caller's ensure block can close the
|
|
115
|
+
# pipes created so far.
|
|
116
|
+
#
|
|
117
|
+
# @param opened_pipes [Hash<Object, ProcessExecuter::MonitoredPipe>] an
|
|
118
|
+
# accumulator for the opened pipes (the Object is the option key)
|
|
119
|
+
#
|
|
120
|
+
# @return [Hash<Object, ProcessExecuter::MonitoredPipe>] the given `opened_pipes`
|
|
121
|
+
#
|
|
122
|
+
def wrap_stdout_stderr(opened_pipes)
|
|
123
|
+
effective_redirections.each do |key, value|
|
|
70
124
|
next unless should_wrap?(key, value)
|
|
71
125
|
|
|
72
126
|
wrapped_destination = ProcessExecuter::MonitoredPipe.new(value)
|
|
73
127
|
opened_pipes[key] = wrapped_destination
|
|
74
|
-
|
|
128
|
+
redirection_overrides[key] = wrapped_destination
|
|
75
129
|
end
|
|
130
|
+
opened_pipes
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The options as given by the user with {#redirection_overrides} applied
|
|
134
|
+
#
|
|
135
|
+
# @return [Hash<Object, Object>]
|
|
136
|
+
#
|
|
137
|
+
def effective_redirections = options.to_h.merge(redirection_overrides)
|
|
138
|
+
|
|
139
|
+
# Close the given pipes and raise any pipe error unless already unwinding
|
|
140
|
+
#
|
|
141
|
+
# When `in_flight_error` is set, `#call` is unwinding from an exception
|
|
142
|
+
# and that exception (not a pipe destination error or a pipe cleanup
|
|
143
|
+
# error) must be the one the caller sees, so nothing is raised here.
|
|
144
|
+
#
|
|
145
|
+
# @param opened_pipes [Hash<Object, ProcessExecuter::MonitoredPipe>] the pipes to close
|
|
146
|
+
#
|
|
147
|
+
# @param in_flight_error [Exception, nil] the exception `#call` is unwinding from, if any
|
|
148
|
+
#
|
|
149
|
+
# @raise [ProcessExecuter::ProcessIOError] if a pipe recorded a destination
|
|
150
|
+
# exception or gave up draining before reaching EOF (truncated output)
|
|
151
|
+
#
|
|
152
|
+
# @raise [StandardError] the first error raised while closing the pipes
|
|
153
|
+
#
|
|
154
|
+
# @return [void]
|
|
155
|
+
#
|
|
156
|
+
def close_pipes_and_check_errors(opened_pipes, in_flight_error)
|
|
157
|
+
close_error = close_pipes(opened_pipes)
|
|
158
|
+
return if in_flight_error
|
|
159
|
+
|
|
160
|
+
opened_pipes.each do |option_key, pipe|
|
|
161
|
+
raise_pipe_error(option_key, pipe)
|
|
162
|
+
raise_truncation_error(option_key, pipe)
|
|
163
|
+
end
|
|
164
|
+
raise close_error if close_error
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Close the given pipes, continuing if closing one of them raises
|
|
168
|
+
#
|
|
169
|
+
# Closing continues past a failure so that one pipe's error does not leak
|
|
170
|
+
# the monitoring threads and file descriptors of the pipes after it.
|
|
171
|
+
#
|
|
172
|
+
# All pipes share a single close deadline so that this method -- called
|
|
173
|
+
# from `#call`'s ensure block -- returns in bounded time even when a
|
|
174
|
+
# process outside this object's control (such as an orphaned descendant
|
|
175
|
+
# of a timed out command) still holds a pipe's write fd open. A pipe
|
|
176
|
+
# whose drain is cut short by the deadline records it via
|
|
177
|
+
# {MonitoredPipe#truncated?}.
|
|
178
|
+
#
|
|
179
|
+
# @param opened_pipes [Hash<Object, ProcessExecuter::MonitoredPipe>] the pipes to close
|
|
180
|
+
#
|
|
181
|
+
# @return [StandardError, nil] the first error raised while closing, or nil if none was raised
|
|
182
|
+
#
|
|
183
|
+
def close_pipes(opened_pipes)
|
|
184
|
+
first_close_error = nil
|
|
185
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + MonitoredPipe::DEFAULT_CLOSE_TIMEOUT
|
|
186
|
+
opened_pipes.each_value do |pipe|
|
|
187
|
+
remaining_time = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
188
|
+
pipe.close(timeout: [remaining_time, 0].max)
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
first_close_error ||= e
|
|
191
|
+
end
|
|
192
|
+
first_close_error
|
|
76
193
|
end
|
|
77
194
|
|
|
78
195
|
# Should the redirection option be wrapped by a MonitoredPipe
|
|
@@ -119,6 +236,31 @@ module ProcessExecuter
|
|
|
119
236
|
error = ProcessExecuter::ProcessIOError.new("Pipe Exception for #{command}: #{option_key.inspect}")
|
|
120
237
|
raise(error, cause: pipe.exception)
|
|
121
238
|
end
|
|
239
|
+
|
|
240
|
+
# Raises a ProcessIOError if the given pipe's output was truncated
|
|
241
|
+
#
|
|
242
|
+
# Truncation means the pipe gave up draining before reaching EOF
|
|
243
|
+
# ({MonitoredPipe#truncated?}): output the subprocess (or a descendant
|
|
244
|
+
# holding the inherited write fd) produced was discarded instead of
|
|
245
|
+
# being written to the destination. Raising makes that data loss loud
|
|
246
|
+
# rather than letting the command appear to succeed with silently
|
|
247
|
+
# incomplete output.
|
|
248
|
+
#
|
|
249
|
+
# @param option_key [Object] The redirection option key
|
|
250
|
+
#
|
|
251
|
+
# @param pipe [ProcessExecuter::MonitoredPipe] The pipe whose output was truncated
|
|
252
|
+
#
|
|
253
|
+
# @raise [ProcessExecuter::ProcessIOError] If the pipe's output was truncated
|
|
254
|
+
#
|
|
255
|
+
# @return [void]
|
|
256
|
+
#
|
|
257
|
+
def raise_truncation_error(option_key, pipe)
|
|
258
|
+
return unless pipe.truncated?
|
|
259
|
+
|
|
260
|
+
raise ProcessExecuter::ProcessIOError,
|
|
261
|
+
"Output truncated for #{command}: #{option_key.inspect} " \
|
|
262
|
+
'could not be fully collected before the close timeout'
|
|
263
|
+
end
|
|
122
264
|
end
|
|
123
265
|
end
|
|
124
266
|
end
|
|
@@ -50,7 +50,7 @@ module ProcessExecuter
|
|
|
50
50
|
@stderr_buffer = StringIO.new
|
|
51
51
|
stderr_buffer.set_encoding(options.effective_stderr_encoding)
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
add_capture_redirections
|
|
54
54
|
|
|
55
55
|
begin
|
|
56
56
|
super
|
|
@@ -89,15 +89,52 @@ module ProcessExecuter
|
|
|
89
89
|
)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
#
|
|
92
|
+
# Add the stdout and stderr capture redirections to {#redirection_overrides}
|
|
93
|
+
#
|
|
94
|
+
# The capture redirections are not written into {options} so the caller's
|
|
95
|
+
# options object is not modified.
|
|
96
|
+
#
|
|
97
|
+
# When the user gives a combined redirection whose key covers both stdout
|
|
98
|
+
# and stderr (e.g. `[:out, :err] => destination`), a single capture
|
|
99
|
+
# redirection is built for that key so both streams are interleaved into
|
|
100
|
+
# {#stdout_buffer} and {#stderr_buffer} is left empty, mirroring the
|
|
101
|
+
# `merge_output: true` contract.
|
|
102
|
+
#
|
|
103
|
+
# @return [Void]
|
|
104
|
+
#
|
|
105
|
+
def add_capture_redirections
|
|
106
|
+
if options.combined_stdout_and_stderr_redirection?
|
|
107
|
+
add_combined_capture_redirection
|
|
108
|
+
else
|
|
109
|
+
add_stdout_and_stderr_capture_redirections
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Add a single capture redirection for a combined stdout/stderr key
|
|
114
|
+
#
|
|
115
|
+
# Both streams are interleaved into {#stdout_buffer}; {#stderr_buffer} is
|
|
116
|
+
# left empty.
|
|
117
|
+
#
|
|
118
|
+
# @return [Void]
|
|
119
|
+
#
|
|
120
|
+
def add_combined_capture_redirection
|
|
121
|
+
redirection_overrides.merge!(
|
|
122
|
+
capture_option(:out, stdout_redirection_source, stdout_redirection_destination, stdout_buffer)
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Add separate capture redirections for stdout and stderr
|
|
127
|
+
#
|
|
128
|
+
# If `merge_output: true` was given, stderr is redirected into stdout so
|
|
129
|
+
# both streams are interleaved into {#stdout_buffer}.
|
|
93
130
|
#
|
|
94
131
|
# @return [Void]
|
|
95
132
|
#
|
|
96
|
-
def
|
|
133
|
+
def add_stdout_and_stderr_capture_redirections
|
|
97
134
|
out = stdout_buffer
|
|
98
135
|
err = options.merge_output ? [:child, 1] : stderr_buffer
|
|
99
136
|
|
|
100
|
-
|
|
137
|
+
redirection_overrides.merge!(
|
|
101
138
|
capture_option(:out, stdout_redirection_source, stdout_redirection_destination, out),
|
|
102
139
|
capture_option(:err, stderr_redirection_source, stderr_redirection_destination, err)
|
|
103
140
|
)
|