process_executer 2.0.0 → 3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +156 -46
- data/lib/process_executer/destination_base.rb +83 -0
- data/lib/process_executer/destinations/child_redirection.rb +23 -0
- data/lib/process_executer/destinations/close.rb +23 -0
- data/lib/process_executer/destinations/file_descriptor.rb +36 -0
- data/lib/process_executer/destinations/file_path.rb +56 -0
- data/lib/process_executer/destinations/file_path_mode.rb +60 -0
- data/lib/process_executer/destinations/file_path_mode_perms.rb +61 -0
- data/lib/process_executer/destinations/io.rb +33 -0
- data/lib/process_executer/destinations/monitored_pipe.rb +39 -0
- data/lib/process_executer/destinations/stderr.rb +31 -0
- data/lib/process_executer/destinations/stdout.rb +31 -0
- data/lib/process_executer/destinations/tee.rb +60 -0
- data/lib/process_executer/destinations/writer.rb +33 -0
- data/lib/process_executer/destinations.rb +70 -0
- data/lib/process_executer/monitored_pipe.rb +40 -57
- data/lib/process_executer/options/base.rb +240 -0
- data/lib/process_executer/options/option_definition.rb +56 -0
- data/lib/process_executer/options/run_options.rb +48 -0
- data/lib/process_executer/options/spawn_and_wait_options.rb +39 -0
- data/lib/process_executer/options/spawn_options.rb +143 -0
- data/lib/process_executer/options.rb +7 -166
- data/lib/process_executer/result.rb +13 -23
- data/lib/process_executer/runner.rb +58 -50
- data/lib/process_executer/version.rb +1 -1
- data/lib/process_executer.rb +130 -21
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a13a780c8c9064d19873068266b40be2a2a8ba7fe1d46866d6e8cb15806d5e53
|
4
|
+
data.tar.gz: e727aab59452dac6ef74819bff2462ad0e1eb56c5b0c1ff1f7e018aed5dbdf77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e349893ee5fbf19410e35e2a3a43907ccb1b1610f266788e0bd01b972ccc0e875fa2df95a9f9b90aefa0d8910f535a9d2ad34432332df07dbd58a33d299169d
|
7
|
+
data.tar.gz: 29d7455610df17c93b8616fcff42fe44077f46de3e855eba573b45071e8bd51322240763529e2c5b578191ff20ada709073bac2989e6bbfcca20e0687fac09f5
|
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.0.0 (2025-03-18)
|
9
|
+
|
10
|
+
[Full Changelog](https://github.com/main-branch/process_executer/compare/v2.0.0..v3.0.0)
|
11
|
+
|
12
|
+
Changes since v2.0.0:
|
13
|
+
|
14
|
+
* 3d337de feat: remove Options setter methods and add `with` method
|
15
|
+
* 706d78a docs: add a list the breaking changes for each major release in the README.md
|
16
|
+
* 2903c80 feat: report all option errors instead of just the first one
|
17
|
+
* 247150d feat!: do not capture stdout and stderr by default in `ProcessExecuter.run`
|
18
|
+
* 4b3ac02 feat: support redirection destinations in the form [:child, fd] and :close
|
19
|
+
* ed4620f docs: update README.md to highlight the important parts of this gem
|
20
|
+
* 48b4695 fix: allow Integer or IO are used as a redirection source
|
21
|
+
* 4424a44 feat!: remove the :merge option from ProcessExecuter.run
|
22
|
+
* 7257e5d chore: allow SpawnOptions to accept Integer and IO redirection sources
|
23
|
+
* 92441d0 chore: move all options related classes to a new Options module
|
24
|
+
* 92c096c chore: remove unneeded test file
|
25
|
+
* 91d0db3 feat: implement all possible redirection destinations
|
26
|
+
* a58af4a fix: fix complexity error reported by CodeClimate
|
27
|
+
* 66d97b7 chore: do not fail the CI build for low coverage on JRuby and TruffleRuby
|
28
|
+
* 2fb0ccf feat: refactor options classes
|
29
|
+
* bcf35d5 chore: do not fail the CI build for low coverage on JRuby and TruffleRuby
|
30
|
+
|
8
31
|
## v2.0.0 (2025-03-03)
|
9
32
|
|
10
33
|
[Full Changelog](https://github.com/main-branch/process_executer/compare/v1.3.0..v2.0.0)
|
data/README.md
CHANGED
@@ -10,10 +10,41 @@
|
|
10
10
|
Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org)
|
11
11
|
[](https://main-branch.slack.com/archives/C07NG2BPG8Y)
|
12
12
|
|
13
|
+
ProcessExecuter provides an enhanced API for executing commands in subprocesses,
|
14
|
+
extending Ruby's built-in `Process.spawn` functionality.
|
15
|
+
|
16
|
+
It has additional features like capturing output, handling timeouts, streaming output
|
17
|
+
to multiple destinations, and providing detailed result information.
|
18
|
+
|
19
|
+
This README documents the HEAD version of process_executer which may contain
|
20
|
+
unrelease information. To see the README for the version you are using, consult
|
21
|
+
RubyGems.org. Go to the [process_executer page in
|
22
|
+
RubyGems.org](https://rubygems.org/gems/process_executer), select your version, and
|
23
|
+
then click the "Documentation" link.
|
24
|
+
|
25
|
+
## Requirements
|
26
|
+
|
27
|
+
* Ruby 3.1.0 or later
|
28
|
+
* Compatible with MRI 3.1+, TruffleRuby 24+, and JRuby 9.4+
|
29
|
+
* Works on Mac, Linux, and Windows platforms
|
30
|
+
|
31
|
+
## Table of Contents
|
32
|
+
|
33
|
+
* [Requirements](#requirements)
|
34
|
+
* [Table of Contents](#table-of-contents)
|
13
35
|
* [Usage](#usage)
|
14
|
-
* [ProcessExecuter.run](#processexecuterrun)
|
15
36
|
* [ProcessExecuter::MonitoredPipe](#processexecutermonitoredpipe)
|
37
|
+
* [ProcessExecuter::Result](#processexecuterresult)
|
16
38
|
* [ProcessExecuter.spawn\_and\_wait](#processexecuterspawn_and_wait)
|
39
|
+
* [ProcessExecuter.run](#processexecuterrun)
|
40
|
+
* [Breaking Changes](#breaking-changes)
|
41
|
+
* [2.x](#2x)
|
42
|
+
* [`ProcessExecuter.spawn`](#processexecuterspawn)
|
43
|
+
* [`ProcessExecuter.run`](#processexecuterrun-1)
|
44
|
+
* [`ProcessExecuter::Result`](#processexecuterresult-1)
|
45
|
+
* [Other](#other)
|
46
|
+
* [3.x](#3x)
|
47
|
+
* [`ProcessExecuter.run`](#processexecuterrun-2)
|
17
48
|
* [Installation](#installation)
|
18
49
|
* [Contributing](#contributing)
|
19
50
|
* [Reporting Issues](#reporting-issues)
|
@@ -25,53 +56,43 @@ Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?log
|
|
25
56
|
|
26
57
|
## Usage
|
27
58
|
|
28
|
-
[Full YARD documentation](https://rubydoc.info/gems/process_executer/) for this
|
29
|
-
|
30
|
-
|
31
|
-
This gem contains the following important classes:
|
32
|
-
|
33
|
-
### ProcessExecuter.run
|
34
|
-
|
35
|
-
`ProcessExecuter.run` execute the given command as a subprocess blocking until it is finished.
|
36
|
-
|
37
|
-
A Result object is returned which includes the process's status and output.
|
59
|
+
[Full YARD documentation](https://rubydoc.info/gems/process_executer/) for this gem
|
60
|
+
is hosted on RubyGems.org. Read below for an overview and several examples.
|
38
61
|
|
39
|
-
|
40
|
-
[Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn).
|
41
|
-
In addition, it (1) blocks until the command has exited, (2) captures stdout and
|
42
|
-
stderr to a buffer or file, and (3) can optionally kill the command if it exceeds
|
43
|
-
a given timeout duration.
|
62
|
+
This gem contains two public classes and two public methods:
|
44
63
|
|
45
|
-
|
64
|
+
Classes:
|
46
65
|
|
47
|
-
|
66
|
+
* `ProcessExecuter::MonitoredPipe`: allows use of any object with a `#write` method
|
67
|
+
or an array of objects as a redirection destination in `Process.spawn`
|
68
|
+
* `ProcessExecuter::Result`: an extension of `Process::Status` that includes more
|
69
|
+
information about the subprocess including timeout status, the command that was
|
70
|
+
run, the subprocess options given, and (in some cases) stdout and stderr captured
|
71
|
+
from the subprocess.
|
48
72
|
|
49
|
-
|
73
|
+
Methods:
|
50
74
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
[Execution
|
57
|
-
Environment](https://docs.ruby-lang.org/en/3.3/Process.html#module-Process-label-Execution+Environment).
|
58
|
-
|
59
|
-
Argument options is a hash of options for the new process; see the options listed below.
|
60
|
-
|
61
|
-
See comprehensive examples in the YARD documentation for this method.
|
75
|
+
* `ProcessExecuter.spawn_and_wait`: execute a subprocess and wait for it to exit with
|
76
|
+
an optional timeout. Supports the same interface and features as `Process.spawn`.
|
77
|
+
* `ProcessExecuter.run`: builds upon `.spawn_and_wait` adding (1) automatically
|
78
|
+
wrapping stdout and stderr destinations (if given) in a `MonitoredPipe` and (2)
|
79
|
+
raises errors for any problem executing the subprocess (can be turned off).
|
62
80
|
|
63
81
|
### ProcessExecuter::MonitoredPipe
|
64
82
|
|
65
|
-
`ProcessExecuter::MonitoredPipe`
|
83
|
+
`ProcessExecuter::MonitoredPipe` objects can be used as a redirection destination for
|
84
|
+
`Process.spawn` to stream output from a subprocess to one or more destinations.
|
85
|
+
Destinations are given in this class's initializer.
|
66
86
|
|
67
|
-
|
68
|
-
|
87
|
+
The destinations are all the redirection destinations allowed by `Process.spawn` plus
|
88
|
+
the following:
|
69
89
|
|
70
|
-
|
71
|
-
|
90
|
+
* Any object with a #write method even if it does not have a file descriptor (like
|
91
|
+
instances of StringIO)
|
92
|
+
* An array of destinations so that output can be tee'd to several sources
|
72
93
|
|
73
|
-
|
74
|
-
|
94
|
+
Example of capturing stdout to a StringIO (which is not directly possible with
|
95
|
+
`Process.spawn`):
|
75
96
|
|
76
97
|
```ruby
|
77
98
|
require 'stringio'
|
@@ -80,11 +101,15 @@ require 'process_executer'
|
|
80
101
|
output_buffer = StringIO.new
|
81
102
|
out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer)
|
82
103
|
pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
|
104
|
+
out_pipe.close # Close the pipe so all the data is flushed and resources are not leaked
|
83
105
|
output_buffer.string #=> "Hello World\n"
|
84
106
|
```
|
85
107
|
|
86
|
-
|
87
|
-
|
108
|
+
Any object that implements `#write` can be used as a destination (not just StringIO).
|
109
|
+
For instance, you can use it to parse process output as a stream which might be useful
|
110
|
+
for long XML or JSON output.
|
111
|
+
|
112
|
+
Example of tee'ing stdout to multiple destinations:
|
88
113
|
|
89
114
|
```ruby
|
90
115
|
require 'stringio'
|
@@ -92,24 +117,42 @@ require 'process_executer'
|
|
92
117
|
|
93
118
|
output_buffer = StringIO.new
|
94
119
|
output_file = File.open('process.out', 'w')
|
95
|
-
out_pipe = ProcessExecuter::MonitoredPipe.new(output_buffer, output_file)
|
120
|
+
out_pipe = ProcessExecuter::MonitoredPipe.new([:tee, output_buffer, output_file])
|
96
121
|
pid, status = Process.wait2(Process.spawn('echo "Hello World"', out: out_pipe))
|
122
|
+
out_pipe.close
|
97
123
|
output_file.close
|
98
124
|
output_buffer.string #=> "Hello World\n"
|
99
125
|
File.read('process.out') #=> "Hello World\n"
|
100
126
|
```
|
101
127
|
|
102
|
-
|
103
|
-
|
104
|
-
|
128
|
+
### ProcessExecuter::Result
|
129
|
+
|
130
|
+
An instance of this class is returned from both `.spawn_and_wait` and `.run`.
|
131
|
+
|
132
|
+
This class is an extension of
|
133
|
+
[Process::Status](https://docs.ruby-lang.org/en/3.3/Process/Status.html) so it
|
134
|
+
supports the same interface with the following additions:
|
135
|
+
|
136
|
+
* `#command`: the command given to `.spawn_and_wait` or `.run`
|
137
|
+
* `#options`: the options given to `.spawn_and_wait` or `.run` (possibly with some
|
138
|
+
changes)
|
139
|
+
* `#timed_out?`: true if the process was killed after running for `:timeout_after`
|
140
|
+
seconds
|
141
|
+
* `#elapsed_time`: the number of seconds the process was running
|
142
|
+
* `#stdout`: the captured stdout from the subprocess (if the stdout destination was
|
143
|
+
wrapped by a `MonitoredPipe`)
|
144
|
+
* `#stderr`: the captured stderr from the subprocess (if the stderr destination was
|
145
|
+
wrapped by a `MonitoredPipe`)
|
105
146
|
|
106
147
|
### ProcessExecuter.spawn_and_wait
|
107
148
|
|
108
|
-
`ProcessExecuter.
|
109
|
-
|
149
|
+
`ProcessExecuter.spawn_and_wait` has the same interface and features as
|
150
|
+
[Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
|
151
|
+
with the following differences:
|
110
152
|
|
111
|
-
1. It
|
153
|
+
1. It waits for the subprocess to exit
|
112
154
|
2. A timeout can be specified using the `:timeout_after` option
|
155
|
+
3. It returns a `ProcessExecuter::Result` instead of a `Process::Status`
|
113
156
|
|
114
157
|
If the command does not terminate before the number of seconds specified by
|
115
158
|
`:timeout_after`, the process is killed by sending it the SIGKILL signal. The
|
@@ -122,6 +165,73 @@ result.termsig #=> 9
|
|
122
165
|
result.timed_out? #=> true
|
123
166
|
```
|
124
167
|
|
168
|
+
If the destination for stdout and stderr are wrapped by a
|
169
|
+
ProcessExecuter::MonitoredPipe, the result will return the stdout and stderr
|
170
|
+
subprocess output from its `#stdout` and `#stderr` methods.
|
171
|
+
|
172
|
+
### ProcessExecuter.run
|
173
|
+
|
174
|
+
`ProcessExecuter.run` builds upon `ProcessExecuter.spawn_and_wait` adding the
|
175
|
+
following features:
|
176
|
+
|
177
|
+
* It automatically wraps any given stdout and stderr destination with a
|
178
|
+
MonitoredPipe. The pipe will be closed when the command exits.
|
179
|
+
* It raises an error if there is any problem with the subprocess. This behavior can
|
180
|
+
be turned off with the `raise_errors: false` option.
|
181
|
+
|
182
|
+
```ruby
|
183
|
+
result = ProcessExecuter.run('echo "Hello World"', out: StringIO.new)
|
184
|
+
result.stdout #=> "Hello World\n"
|
185
|
+
```
|
186
|
+
|
187
|
+
## Breaking Changes
|
188
|
+
|
189
|
+
### 2.x
|
190
|
+
|
191
|
+
This major release focused on changes to the interface to make it more understandable.
|
192
|
+
|
193
|
+
#### `ProcessExecuter.spawn`
|
194
|
+
|
195
|
+
* This method was renamed to `ProcessExecuter.spawn_and_wait`
|
196
|
+
* The `:timeout` option was renamed to `:timeout_after`
|
197
|
+
|
198
|
+
#### `ProcessExecuter.run`
|
199
|
+
|
200
|
+
* The `:timeout` option was renamed to `:timeout_after`
|
201
|
+
|
202
|
+
#### `ProcessExecuter::Result`
|
203
|
+
|
204
|
+
* The `#timeout` method was renamed to `#timed_out`
|
205
|
+
|
206
|
+
#### Other
|
207
|
+
|
208
|
+
* Dropped support for Ruby 3.0
|
209
|
+
|
210
|
+
### 3.x
|
211
|
+
|
212
|
+
#### `ProcessExecuter.run`
|
213
|
+
|
214
|
+
* The `:merge` option was removed
|
215
|
+
|
216
|
+
This was removed because `Process.spawn` already provides this functionality but in
|
217
|
+
a different way. To merge, you will need to define a redirection where the source
|
218
|
+
is an array of the file descriptors you want to merge. For instance:
|
219
|
+
|
220
|
+
```Ruby
|
221
|
+
[:out, :err] => 'output.txt'
|
222
|
+
```
|
223
|
+
|
224
|
+
will merge stdout and stderr from the subprocess into the file output.txt.
|
225
|
+
|
226
|
+
* Stdout and stderr redirections are no longer default to a new instance of StringIO
|
227
|
+
|
228
|
+
Calls to `ProcessExecuter.run` that do not define a redirection for stdout or
|
229
|
+
stderr will have to add explicit redirection(s) in order to capture the output.
|
230
|
+
|
231
|
+
This is to align with the functionality in `Process.spawn`. In `Process.spawn`, when
|
232
|
+
an explicit redirection is not given for stdout and stderr, this output will be
|
233
|
+
passed through to the parent process's stdout and stderr.
|
234
|
+
|
125
235
|
## Installation
|
126
236
|
|
127
237
|
Install the gem and add to the application's Gemfile by executing:
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
# Base class for all destination handlers
|
5
|
+
#
|
6
|
+
# Provides the common interface and functionality for all destination
|
7
|
+
# classes that handle different types of output redirection.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class DestinationBase
|
11
|
+
# Initializes a new destination handler
|
12
|
+
#
|
13
|
+
# @param destination [Object] the destination to write to
|
14
|
+
# @return [DestinationBase] a new destination handler instance
|
15
|
+
def initialize(destination)
|
16
|
+
@destination = destination
|
17
|
+
@data_written = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# The destination object this handler manages
|
21
|
+
#
|
22
|
+
# @return [Object] the destination object
|
23
|
+
attr_reader :destination
|
24
|
+
|
25
|
+
# The data written to the destination
|
26
|
+
#
|
27
|
+
# @return [Array<String>] the data written to the destination
|
28
|
+
attr_reader :data_written
|
29
|
+
|
30
|
+
# The data written to the destination as a single string
|
31
|
+
# @return [String]
|
32
|
+
def string
|
33
|
+
data_written.join
|
34
|
+
end
|
35
|
+
|
36
|
+
# Writes data to the destination
|
37
|
+
#
|
38
|
+
# This is an abstract method that must be implemented by subclasses.
|
39
|
+
#
|
40
|
+
# @param data [String] the data to write
|
41
|
+
# @return [void]
|
42
|
+
# @raise [NotImplementedError] if the subclass doesn't implement this method
|
43
|
+
def write(data)
|
44
|
+
@data_written << data
|
45
|
+
end
|
46
|
+
|
47
|
+
# Closes the destination if necessary
|
48
|
+
#
|
49
|
+
# By default, this method does nothing. Subclasses should override
|
50
|
+
# this method if they need to perform cleanup.
|
51
|
+
#
|
52
|
+
# @return [void]
|
53
|
+
def close; end
|
54
|
+
|
55
|
+
# Determines if this class can handle the given destination
|
56
|
+
#
|
57
|
+
# This is an abstract class method that must be implemented by subclasses.
|
58
|
+
#
|
59
|
+
# @param destination [Object] the destination to check
|
60
|
+
# @return [Boolean] true if this class can handle the destination
|
61
|
+
# @raise [NotImplementedError] if the subclass doesn't implement this method
|
62
|
+
def self.handles?(destination)
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
|
66
|
+
# Determines if this destination class can be wrapped by MonitoredPipe
|
67
|
+
#
|
68
|
+
# All destination types can be wrapped by MonitoredPipe unless they explicitly
|
69
|
+
# opt out.
|
70
|
+
#
|
71
|
+
# @return [Boolean]
|
72
|
+
# @api private
|
73
|
+
def self.compatible_with_monitored_pipe? = true
|
74
|
+
|
75
|
+
# Determines if this destination instance can be wrapped by MonitoredPipe
|
76
|
+
#
|
77
|
+
# @return [Boolean]
|
78
|
+
# @api private
|
79
|
+
def compatible_with_monitored_pipe?
|
80
|
+
self.class.compatible_with_monitored_pipe?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles generic objects that respond to write
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class ChildRedirection < ProcessExecuter::DestinationBase
|
9
|
+
# Determines if this class can handle the given destination
|
10
|
+
#
|
11
|
+
# @param destination [Object] the destination to check
|
12
|
+
# @return [Boolean] true if destination responds to write but is not an IO with fileno
|
13
|
+
def self.handles?(destination)
|
14
|
+
destination.is_a?(Array) && destination.size == 2 && destination[0] == :child
|
15
|
+
end
|
16
|
+
|
17
|
+
# This class should not be wrapped in a monitored pipe
|
18
|
+
# @return [Boolean]
|
19
|
+
# @api private
|
20
|
+
def self.compatible_with_monitored_pipe? = false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles generic objects that respond to write
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Close < ProcessExecuter::DestinationBase
|
9
|
+
# Determines if this class can handle the given destination
|
10
|
+
#
|
11
|
+
# @param destination [Object] the destination to check
|
12
|
+
# @return [Boolean] true if destination responds to write but is not an IO with fileno
|
13
|
+
def self.handles?(destination)
|
14
|
+
destination == :close
|
15
|
+
end
|
16
|
+
|
17
|
+
# This class should not be wrapped in a monitored pipe
|
18
|
+
# @return [Boolean]
|
19
|
+
# @api private
|
20
|
+
def self.compatible_with_monitored_pipe? = false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'process_executer/destination_base'
|
4
|
+
|
5
|
+
module ProcessExecuter
|
6
|
+
module Destinations
|
7
|
+
# Handles numeric file descriptors
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class FileDescriptor < ProcessExecuter::DestinationBase
|
11
|
+
# Writes data to the file descriptor
|
12
|
+
#
|
13
|
+
# @param data [String] the data to write
|
14
|
+
# @return [Integer] the number of bytes written
|
15
|
+
# @raise [SystemCallError] if the file descriptor is invalid
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# fd_handler = ProcessExecuter::Destinations::FileDescriptor.new(3)
|
19
|
+
# fd_handler.write("Hello world")
|
20
|
+
def write(data)
|
21
|
+
super
|
22
|
+
io = ::IO.open(destination, mode: 'a', autoclose: false)
|
23
|
+
io.write(data)
|
24
|
+
io.close
|
25
|
+
end
|
26
|
+
|
27
|
+
# Determines if this class can handle the given destination
|
28
|
+
#
|
29
|
+
# @param destination [Object] the destination to check
|
30
|
+
# @return [Boolean] true if destination is an Integer that's not stdout/stderr
|
31
|
+
def self.handles?(destination)
|
32
|
+
destination.is_a?(Integer) && ![:out, 1, :err, 2].include?(destination)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles file path destinations
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class FilePath < ProcessExecuter::DestinationBase
|
9
|
+
# Initializes a new file path destination handler
|
10
|
+
#
|
11
|
+
# Opens the file at the given path for writing.
|
12
|
+
#
|
13
|
+
# @param destination [String] the file path to write to
|
14
|
+
# @return [FilePath] a new file path destination handler
|
15
|
+
# @raise [Errno::ENOENT] if the file path is invalid
|
16
|
+
def initialize(destination)
|
17
|
+
super
|
18
|
+
@file = File.open(destination, 'w', 0o644)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The opened file object
|
22
|
+
#
|
23
|
+
# @return [File] the opened file
|
24
|
+
attr_reader :file
|
25
|
+
|
26
|
+
# Writes data to the file
|
27
|
+
#
|
28
|
+
# @param data [String] the data to write
|
29
|
+
# @return [Integer] the number of bytes written
|
30
|
+
# @raise [IOError] if the file is closed
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# file_handler = ProcessExecuter::Destinations::FilePath.new("output.log")
|
34
|
+
# file_handler.write("Log entry")
|
35
|
+
def write(data)
|
36
|
+
super
|
37
|
+
file.write data
|
38
|
+
end
|
39
|
+
|
40
|
+
# Closes the file if it's open
|
41
|
+
#
|
42
|
+
# @return [void]
|
43
|
+
def close
|
44
|
+
file.close unless file.closed?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Determines if this class can handle the given destination
|
48
|
+
#
|
49
|
+
# @param destination [Object] the destination to check
|
50
|
+
# @return [Boolean] true if destination is a String
|
51
|
+
def self.handles?(destination)
|
52
|
+
destination.is_a? String
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles file paths with specific open modes
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class FilePathMode < ProcessExecuter::DestinationBase
|
9
|
+
# Initializes a new file path with mode destination handler
|
10
|
+
#
|
11
|
+
# Opens the file at the given path with the specified mode.
|
12
|
+
#
|
13
|
+
# @param destination [Array<String, String>] array with file path and mode
|
14
|
+
# @return [FilePathMode] a new file path with mode destination handler
|
15
|
+
# @raise [Errno::ENOENT] if the file path is invalid
|
16
|
+
# @raise [ArgumentError] if the mode is invalid
|
17
|
+
def initialize(destination)
|
18
|
+
super
|
19
|
+
@file = File.open(destination[0], destination[1], 0o644)
|
20
|
+
end
|
21
|
+
|
22
|
+
# The opened file object
|
23
|
+
#
|
24
|
+
# @return [File] the opened file
|
25
|
+
attr_reader :file
|
26
|
+
|
27
|
+
# Writes data to the file
|
28
|
+
#
|
29
|
+
# @param data [String] the data to write
|
30
|
+
# @return [Integer] the number of bytes written
|
31
|
+
# @raise [IOError] if the file is closed
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# mode_handler = ProcessExecuter::Destinations::FilePathMode.new(["output.log", "a"])
|
35
|
+
# mode_handler.write("Appended log entry")
|
36
|
+
def write(data)
|
37
|
+
super
|
38
|
+
file.write data
|
39
|
+
end
|
40
|
+
|
41
|
+
# Closes the file if it's open
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def close
|
45
|
+
file.close unless file.closed?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Determines if this class can handle the given destination
|
49
|
+
#
|
50
|
+
# @param destination [Object] the destination to check
|
51
|
+
# @return [Boolean] true if destination is an Array with path and mode
|
52
|
+
def self.handles?(destination)
|
53
|
+
destination.is_a?(Array) &&
|
54
|
+
destination.size == 2 &&
|
55
|
+
destination[0].is_a?(String) &&
|
56
|
+
destination[1].is_a?(String)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles file paths with specific open modes and permissions
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class FilePathModePerms < ProcessExecuter::DestinationBase
|
9
|
+
# Initializes a new file path with mode and permissions destination handler
|
10
|
+
#
|
11
|
+
# Opens the file at the given path with the specified mode and permissions.
|
12
|
+
#
|
13
|
+
# @param destination [Array<String, String, Integer>] array with file path, mode, and permissions
|
14
|
+
# @return [FilePathModePerms] a new handler instance
|
15
|
+
# @raise [Errno::ENOENT] if the file path is invalid
|
16
|
+
# @raise [ArgumentError] if the mode is invalid
|
17
|
+
def initialize(destination)
|
18
|
+
super
|
19
|
+
@file = File.open(destination[0], destination[1], destination[2])
|
20
|
+
end
|
21
|
+
|
22
|
+
# The opened file object
|
23
|
+
#
|
24
|
+
# @return [File] the opened file
|
25
|
+
attr_reader :file
|
26
|
+
|
27
|
+
# Writes data to the file
|
28
|
+
#
|
29
|
+
# @param data [String] the data to write
|
30
|
+
# @return [Integer] the number of bytes written
|
31
|
+
# @raise [IOError] if the file is closed
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# perms_handler = ProcessExecuter::Destinations::FilePathModePerms.new(["output.log", "w", 0644])
|
35
|
+
# perms_handler.write("Log entry with specific permissions")
|
36
|
+
def write(data)
|
37
|
+
super
|
38
|
+
file.write data
|
39
|
+
end
|
40
|
+
|
41
|
+
# Closes the file if it's open
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def close
|
45
|
+
file.close unless file.closed?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Determines if this class can handle the given destination
|
49
|
+
#
|
50
|
+
# @param destination [Object] the destination to check
|
51
|
+
# @return [Boolean] true if destination is an Array with path, mode, and permissions
|
52
|
+
def self.handles?(destination)
|
53
|
+
destination.is_a?(Array) &&
|
54
|
+
destination.size == 3 &&
|
55
|
+
destination[0].is_a?(String) &&
|
56
|
+
destination[1].is_a?(String) &&
|
57
|
+
destination[2].is_a?(Integer)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|