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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +41 -0
- data/README.md +177 -134
- data/lib/process_executer/commands/run.rb +124 -0
- data/lib/process_executer/commands/run_with_capture.rb +148 -0
- data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
- data/lib/process_executer/commands.rb +11 -0
- data/lib/process_executer/destinations/child_redirection.rb +5 -4
- data/lib/process_executer/destinations/close.rb +5 -4
- data/lib/process_executer/destinations/destination_base.rb +73 -0
- data/lib/process_executer/destinations/file_descriptor.rb +10 -6
- data/lib/process_executer/destinations/file_path.rb +12 -6
- data/lib/process_executer/destinations/file_path_mode.rb +10 -6
- data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
- data/lib/process_executer/destinations/io.rb +10 -5
- data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
- data/lib/process_executer/destinations/stderr.rb +8 -4
- data/lib/process_executer/destinations/stdout.rb +8 -4
- data/lib/process_executer/destinations/tee.rb +24 -17
- data/lib/process_executer/destinations/writer.rb +12 -7
- data/lib/process_executer/destinations.rb +32 -17
- data/lib/process_executer/errors.rb +50 -26
- data/lib/process_executer/monitored_pipe.rb +128 -59
- data/lib/process_executer/options/base.rb +118 -82
- data/lib/process_executer/options/option_definition.rb +5 -1
- data/lib/process_executer/options/run_options.rb +13 -12
- data/lib/process_executer/options/run_with_capture_options.rb +156 -0
- data/lib/process_executer/options/spawn_options.rb +31 -30
- data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
- data/lib/process_executer/options.rb +3 -1
- data/lib/process_executer/result.rb +35 -77
- data/lib/process_executer/result_with_capture.rb +62 -0
- data/lib/process_executer/version.rb +2 -1
- data/lib/process_executer.rb +384 -346
- data/process_executer.gemspec +11 -2
- metadata +18 -8
- data/lib/process_executer/destination_base.rb +0 -83
- data/lib/process_executer/runner.rb +0 -144
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
module ProcessExecuter
|
6
|
+
module Commands
|
7
|
+
# Runs a subprocess, blocks until it completes, and returns the result
|
8
|
+
#
|
9
|
+
# Extends {ProcessExecuter::Commands::Run} to provide the core functionality for
|
10
|
+
# {ProcessExecuter.run_with_capture}.
|
11
|
+
#
|
12
|
+
# It accepts all [Process.spawn execution
|
13
|
+
# options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options)
|
14
|
+
# plus the additional options `timeout_after`, `raise_errors`, `logger`, and
|
15
|
+
# `merge_output`.
|
16
|
+
#
|
17
|
+
# Like {Run}, any stdout or stderr redirection destinations are wrapped in a
|
18
|
+
# {MonitoredPipe}.
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
#
|
22
|
+
class RunWithCapture < Run
|
23
|
+
# Run a command and return the result which includes the captured output
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# options = ProcessExecuter::Options::RunWithCaptureOptions.new(merge_output: false)
|
27
|
+
# result = ProcessExecuter::Commands::RunWithCapture.new('echo hello', options).call
|
28
|
+
# result.success? # => true
|
29
|
+
# result.exitstatus # => 0
|
30
|
+
# result.stdout # => "hello\n"
|
31
|
+
#
|
32
|
+
# @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
|
33
|
+
# command was run
|
34
|
+
#
|
35
|
+
# @raise [ProcessExecuter::FailedError] If the command ran and failed
|
36
|
+
#
|
37
|
+
# @raise [ProcessExecuter::SignaledError] If the command ran and terminated due to
|
38
|
+
# an unhandled signal
|
39
|
+
#
|
40
|
+
# @raise [ProcessExecuter::TimeoutError] If the command timed out
|
41
|
+
#
|
42
|
+
# @raise [ProcessExecuter::ProcessIOError] If there was an exception while
|
43
|
+
# collecting subprocess output
|
44
|
+
#
|
45
|
+
# @return [ProcessExecuter::ResultWithCapture] The result of the completed subprocess
|
46
|
+
#
|
47
|
+
def call
|
48
|
+
@stdout_buffer = StringIO.new
|
49
|
+
stdout_buffer.set_encoding(options.effective_stdout_encoding)
|
50
|
+
@stderr_buffer = StringIO.new
|
51
|
+
stderr_buffer.set_encoding(options.effective_stderr_encoding)
|
52
|
+
|
53
|
+
update_capture_options
|
54
|
+
|
55
|
+
begin
|
56
|
+
super
|
57
|
+
ensure
|
58
|
+
log_command_output
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# The buffer used to capture stdout
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# run.stdout_buffer #=> StringIO
|
66
|
+
#
|
67
|
+
# @return [StringIO]
|
68
|
+
#
|
69
|
+
attr_reader :stdout_buffer
|
70
|
+
|
71
|
+
# The buffer used to capture stderr
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# run.stderr_buffer #=> StringIO
|
75
|
+
#
|
76
|
+
# @return [StringIO]
|
77
|
+
#
|
78
|
+
attr_reader :stderr_buffer
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Create a result object that includes the captured stdout and stderr
|
83
|
+
#
|
84
|
+
# @return [ProcessExecuter::ResultWithCapture] The result of the command with captured output
|
85
|
+
#
|
86
|
+
def create_result
|
87
|
+
ProcessExecuter::ResultWithCapture.new(
|
88
|
+
super, stdout_buffer:, stderr_buffer:
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Updates {options} to include the stdout and stderr capture options
|
93
|
+
#
|
94
|
+
# @return [Void]
|
95
|
+
#
|
96
|
+
def update_capture_options
|
97
|
+
out = stdout_buffer
|
98
|
+
err = options.merge_output ? [:child, 1] : stderr_buffer
|
99
|
+
|
100
|
+
options.merge!(
|
101
|
+
capture_option(:out, stdout_redirection_source, stdout_redirection_destination, out),
|
102
|
+
capture_option(:err, stderr_redirection_source, stderr_redirection_destination, err)
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
# The source for stdout redirection
|
107
|
+
# @return [Object]
|
108
|
+
def stdout_redirection_source = options.stdout_redirection_source
|
109
|
+
|
110
|
+
# The source for stderr redirection
|
111
|
+
# @return [Object]
|
112
|
+
def stderr_redirection_source = options.stderr_redirection_source
|
113
|
+
|
114
|
+
# The destination for stdout redirection
|
115
|
+
# @return [Object]
|
116
|
+
def stdout_redirection_destination = options.stdout_redirection_destination
|
117
|
+
|
118
|
+
# The destination for stderr redirection
|
119
|
+
# @return [Object]
|
120
|
+
def stderr_redirection_destination = options.stderr_redirection_destination
|
121
|
+
|
122
|
+
# Add the capture redirection to existing options (if any)
|
123
|
+
# @param redirection_source [Symbol, Integer] The source of the redirection (e.g., :out, :err)
|
124
|
+
# @param given_source [Symbol, Integer, nil] The source provided by the user (if any)
|
125
|
+
# @param given_destination [Object, nil] The destination provided by the user (if any)
|
126
|
+
# @param capture_destination [Object] The additional destination to capture output to
|
127
|
+
# @return [Hash] The option (including the capture_destination) to merge into options
|
128
|
+
def capture_option(redirection_source, given_source, given_destination, capture_destination)
|
129
|
+
if given_source
|
130
|
+
if Destinations::Tee.handles?(given_destination)
|
131
|
+
{ given_source => given_destination + [capture_destination] }
|
132
|
+
else
|
133
|
+
{ given_source => [:tee, given_destination, capture_destination] }
|
134
|
+
end
|
135
|
+
else
|
136
|
+
{ redirection_source => capture_destination }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Log the captured command output to the given logger at debug level
|
141
|
+
# @return [Void]
|
142
|
+
def log_command_output
|
143
|
+
options.logger&.debug { "PID #{pid}: stdout: #{stdout_buffer.string.inspect}" }
|
144
|
+
options.logger&.debug { "PID #{pid}: stderr: #{stderr_buffer.string.inspect}" }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
|
5
|
+
module ProcessExecuter
|
6
|
+
module Commands
|
7
|
+
# Spawns a subprocess, waits until it completes, and returns the result
|
8
|
+
#
|
9
|
+
# Wraps `Process.spawn` to provide the core functionality for
|
10
|
+
# {ProcessExecuter.spawn_with_timeout}.
|
11
|
+
#
|
12
|
+
# It accepts all [Process.spawn execution
|
13
|
+
# options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options)
|
14
|
+
# plus the additional option `timeout_after`.
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
#
|
18
|
+
class SpawnWithTimeout
|
19
|
+
# Create a new SpawnWithTimeout instance
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# options = ProcessExecuter::Options::SpawnWithTimeoutOptions.new(timeout_after: 5)
|
23
|
+
# result = ProcessExecuter::Commands::SpawnWithTimeout.new('echo hello', options).call
|
24
|
+
# result.success? # => true
|
25
|
+
# result.exitstatus # => 0
|
26
|
+
#
|
27
|
+
# @param command [Array<String>] The command to run in the subprocess
|
28
|
+
# @param options [ProcessExecuter::Options::SpawnWithTimeoutOptions] The options to use when spawning the process
|
29
|
+
#
|
30
|
+
def initialize(command, options)
|
31
|
+
@command = command
|
32
|
+
@options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Run a command and return the result
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# options = ProcessExecuter::Options::SpawnWithTimeoutOptions.new(timeout_after: 5)
|
39
|
+
# result = ProcessExecuter::Commands::SpawnWithTimeout.new('echo hello', options).call
|
40
|
+
# result.success? # => true
|
41
|
+
# result.exitstatus # => 0
|
42
|
+
# result.timed_out? # => false
|
43
|
+
#
|
44
|
+
# @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
|
45
|
+
# command was run
|
46
|
+
#
|
47
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
48
|
+
#
|
49
|
+
def call
|
50
|
+
begin
|
51
|
+
@pid = Process.spawn(*command, **options.spawn_options)
|
52
|
+
rescue StandardError => e
|
53
|
+
raise ProcessExecuter::SpawnError, "Failed to spawn process: #{e.message}"
|
54
|
+
end
|
55
|
+
|
56
|
+
wait_for_process
|
57
|
+
end
|
58
|
+
|
59
|
+
# The command to be run in the subprocess
|
60
|
+
# @see Process.spawn
|
61
|
+
# @example
|
62
|
+
# spawn.command #=> ['echo', 'hello']
|
63
|
+
# @return [Array<String>]
|
64
|
+
attr_reader :command
|
65
|
+
|
66
|
+
# The options that were used to spawn the process
|
67
|
+
# @example
|
68
|
+
# spawn.options #=> ProcessExecuter::Options::SpawnWithTimeoutOptions
|
69
|
+
# @return [ProcessExecuter::Options::SpawnWithTimeoutOptions]
|
70
|
+
attr_reader :options
|
71
|
+
|
72
|
+
# The process ID of the spawned subprocess
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# spawn.pid #=> 12345
|
76
|
+
#
|
77
|
+
# @return [Integer]
|
78
|
+
#
|
79
|
+
attr_reader :pid
|
80
|
+
|
81
|
+
# The status returned by Process.wait2
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# spawn.status #=> #<Process::Status: pid 12345 exit 0>
|
85
|
+
#
|
86
|
+
# @return [Process::Status]
|
87
|
+
#
|
88
|
+
attr_reader :status
|
89
|
+
|
90
|
+
# Whether the process timed out
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# spawn.timed_out? #=> true
|
94
|
+
#
|
95
|
+
# @return [Boolean]
|
96
|
+
#
|
97
|
+
attr_reader :timed_out
|
98
|
+
|
99
|
+
alias timed_out? timed_out
|
100
|
+
|
101
|
+
# The elapsed time in seconds that the command ran
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# spawn.elapsed_time #=> 1.234
|
105
|
+
#
|
106
|
+
# @return [Numeric]
|
107
|
+
#
|
108
|
+
attr_reader :elapsed_time
|
109
|
+
|
110
|
+
# The result of the completed subprocess
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# spawn.result #=> ProcessExecuter::Result
|
114
|
+
#
|
115
|
+
# @return [ProcessExecuter::Result]
|
116
|
+
#
|
117
|
+
attr_reader :result
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Wait for process to terminate
|
122
|
+
#
|
123
|
+
# If a `:timeout_after` is specified in options, terminate the process after the
|
124
|
+
# specified number of seconds.
|
125
|
+
#
|
126
|
+
# @return [ProcessExecuter::Result] The result of the completed subprocess
|
127
|
+
#
|
128
|
+
def wait_for_process
|
129
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
130
|
+
@status, @timed_out = wait_for_process_raw
|
131
|
+
@elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
132
|
+
@result = create_result
|
133
|
+
end
|
134
|
+
|
135
|
+
# Create a result object that includes the status, command, and other details
|
136
|
+
#
|
137
|
+
# @return [ProcessExecuter::Result] The result of the command
|
138
|
+
#
|
139
|
+
def create_result
|
140
|
+
ProcessExecuter::Result.new(status, command:, options:, timed_out:, elapsed_time:)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Wait for a process to terminate returning the status and timed out flag
|
144
|
+
#
|
145
|
+
# @return [Array<Process::Status, Boolean>] an array containing the process status and a boolean
|
146
|
+
# indicating whether the process timed out
|
147
|
+
def wait_for_process_raw
|
148
|
+
timed_out = false
|
149
|
+
|
150
|
+
process_status =
|
151
|
+
begin
|
152
|
+
Timeout.timeout(options.timeout_after) { Process.wait2(pid).last }
|
153
|
+
rescue Timeout::Error
|
154
|
+
Process.kill('KILL', pid)
|
155
|
+
timed_out = true
|
156
|
+
Process.wait2(pid).last
|
157
|
+
end
|
158
|
+
|
159
|
+
[process_status, timed_out]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'commands/spawn_with_timeout'
|
4
|
+
require_relative 'commands/run'
|
5
|
+
require_relative 'commands/run_with_capture'
|
6
|
+
|
7
|
+
module ProcessExecuter
|
8
|
+
# Classes that implement commands for process execution
|
9
|
+
# @api private
|
10
|
+
module Commands; end
|
11
|
+
end
|
@@ -1,22 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'destination_base'
|
4
|
+
|
3
5
|
module ProcessExecuter
|
4
6
|
module Destinations
|
5
|
-
# Handles
|
7
|
+
# Handles [:child, fd] redirection options as supported by `Process.spawn`
|
6
8
|
#
|
7
9
|
# @api private
|
8
|
-
class ChildRedirection <
|
10
|
+
class ChildRedirection < DestinationBase
|
9
11
|
# Determines if this class can handle the given destination
|
10
12
|
#
|
11
13
|
# @param destination [Object] the destination to check
|
12
|
-
# @return [Boolean] true if destination
|
14
|
+
# @return [Boolean] true if the destination is an array in the format [:child, file_descriptor]
|
13
15
|
def self.handles?(destination)
|
14
16
|
destination.is_a?(Array) && destination.size == 2 && destination[0] == :child
|
15
17
|
end
|
16
18
|
|
17
19
|
# This class should not be wrapped in a monitored pipe
|
18
20
|
# @return [Boolean]
|
19
|
-
# @api private
|
20
21
|
def self.compatible_with_monitored_pipe? = false
|
21
22
|
end
|
22
23
|
end
|
@@ -1,22 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'destination_base'
|
4
|
+
|
3
5
|
module ProcessExecuter
|
4
6
|
module Destinations
|
5
|
-
# Handles
|
7
|
+
# Handles the :close redirection option as supported by `Process.spawn`
|
6
8
|
#
|
7
9
|
# @api private
|
8
|
-
class Close <
|
10
|
+
class Close < DestinationBase
|
9
11
|
# Determines if this class can handle the given destination
|
10
12
|
#
|
11
13
|
# @param destination [Object] the destination to check
|
12
|
-
# @return [Boolean] true if destination
|
14
|
+
# @return [Boolean] true if the destination is the symbol `:close`
|
13
15
|
def self.handles?(destination)
|
14
16
|
destination == :close
|
15
17
|
end
|
16
18
|
|
17
19
|
# This class should not be wrapped in a monitored pipe
|
18
20
|
# @return [Boolean]
|
19
|
-
# @api private
|
20
21
|
def self.compatible_with_monitored_pipe? = false
|
21
22
|
end
|
22
23
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Base class for all destination handlers
|
6
|
+
#
|
7
|
+
# Provides the common interface and functionality for all destination
|
8
|
+
# classes that handle different types of output redirection.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class DestinationBase
|
12
|
+
# Initializes a new destination handler
|
13
|
+
#
|
14
|
+
# @param destination [Object] the destination to write to
|
15
|
+
#
|
16
|
+
def initialize(destination)
|
17
|
+
@destination = destination
|
18
|
+
end
|
19
|
+
|
20
|
+
# The destination object this handler manages
|
21
|
+
#
|
22
|
+
# @return [Object] the destination object
|
23
|
+
attr_reader :destination
|
24
|
+
|
25
|
+
# Writes data to the destination
|
26
|
+
#
|
27
|
+
# Subclasses should override this method to provide specific write behavior.
|
28
|
+
# The base implementation is a no-op.
|
29
|
+
#
|
30
|
+
# @param _data [String] the data to write
|
31
|
+
#
|
32
|
+
# @return [Integer] the number of bytes written
|
33
|
+
#
|
34
|
+
def write(_data)
|
35
|
+
0
|
36
|
+
end
|
37
|
+
|
38
|
+
# Closes the destination if necessary
|
39
|
+
#
|
40
|
+
# By default, this method does nothing. Subclasses should override
|
41
|
+
# this method if they need to perform cleanup.
|
42
|
+
#
|
43
|
+
# @return [void]
|
44
|
+
def close; end
|
45
|
+
|
46
|
+
# Determines if this class can handle the given destination
|
47
|
+
#
|
48
|
+
# This is an abstract class method that must be implemented by subclasses.
|
49
|
+
#
|
50
|
+
# @param destination [Object] the destination to check
|
51
|
+
# @return [Boolean] true if this class can handle the destination
|
52
|
+
# @raise [NotImplementedError] if the subclass doesn't implement this method
|
53
|
+
def self.handles?(destination)
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
|
57
|
+
# Determines if this destination class can be wrapped by MonitoredPipe
|
58
|
+
#
|
59
|
+
# All destination types can be wrapped by MonitoredPipe unless they explicitly
|
60
|
+
# opt out.
|
61
|
+
#
|
62
|
+
# @return [Boolean]
|
63
|
+
def self.compatible_with_monitored_pipe? = true
|
64
|
+
|
65
|
+
# Determines if this destination instance can be wrapped by MonitoredPipe
|
66
|
+
#
|
67
|
+
# @return [Boolean]
|
68
|
+
def compatible_with_monitored_pipe?
|
69
|
+
self.class.compatible_with_monitored_pipe?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -1,35 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'destination_base'
|
4
4
|
|
5
5
|
module ProcessExecuter
|
6
6
|
module Destinations
|
7
7
|
# Handles numeric file descriptors
|
8
8
|
#
|
9
9
|
# @api private
|
10
|
-
class FileDescriptor <
|
10
|
+
class FileDescriptor < DestinationBase
|
11
11
|
# Writes data to the file descriptor
|
12
12
|
#
|
13
13
|
# @param data [String] the data to write
|
14
|
+
#
|
14
15
|
# @return [Integer] the number of bytes written
|
16
|
+
#
|
15
17
|
# @raise [SystemCallError] if the file descriptor is invalid
|
16
18
|
#
|
17
19
|
# @example
|
18
20
|
# fd_handler = ProcessExecuter::Destinations::FileDescriptor.new(3)
|
19
21
|
# fd_handler.write("Hello world")
|
22
|
+
#
|
20
23
|
def write(data)
|
21
24
|
super
|
22
25
|
io = ::IO.open(destination, mode: 'a', autoclose: false)
|
23
|
-
io.write(data)
|
24
|
-
io.close
|
26
|
+
io.write(data).tap { io.close }
|
25
27
|
end
|
26
28
|
|
27
29
|
# Determines if this class can handle the given destination
|
28
30
|
#
|
29
31
|
# @param destination [Object] the destination to check
|
30
|
-
#
|
32
|
+
#
|
33
|
+
# @return [Boolean] true if destination is a file descriptor that's not stdout or stderr
|
34
|
+
#
|
31
35
|
def self.handles?(destination)
|
32
|
-
destination.is_a?(Integer) && ![
|
36
|
+
destination.is_a?(Integer) && ![1, 2].include?(destination)
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
@@ -1,18 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'destination_base'
|
4
|
+
|
3
5
|
module ProcessExecuter
|
4
6
|
module Destinations
|
5
7
|
# Handles file path destinations
|
6
8
|
#
|
7
9
|
# @api private
|
8
|
-
class FilePath <
|
10
|
+
class FilePath < DestinationBase
|
9
11
|
# Initializes a new file path destination handler
|
10
12
|
#
|
11
|
-
#
|
13
|
+
# Redirects to the file at destination via `open(destination, 'w', 0644)`
|
12
14
|
#
|
13
15
|
# @param destination [String] the file path to write to
|
14
|
-
#
|
16
|
+
#
|
15
17
|
# @raise [Errno::ENOENT] if the file path is invalid
|
18
|
+
#
|
16
19
|
def initialize(destination)
|
17
20
|
super
|
18
21
|
@file = File.open(destination, 'w', 0o644)
|
@@ -25,13 +28,16 @@ module ProcessExecuter
|
|
25
28
|
|
26
29
|
# Writes data to the file
|
27
30
|
#
|
31
|
+
# @example
|
32
|
+
# file_handler = ProcessExecuter::Destinations::FilePath.new("output.log")
|
33
|
+
# file_handler.write("Log entry")
|
34
|
+
#
|
28
35
|
# @param data [String] the data to write
|
36
|
+
#
|
29
37
|
# @return [Integer] the number of bytes written
|
38
|
+
#
|
30
39
|
# @raise [IOError] if the file is closed
|
31
40
|
#
|
32
|
-
# @example
|
33
|
-
# file_handler = ProcessExecuter::Destinations::FilePath.new("output.log")
|
34
|
-
# file_handler.write("Log entry")
|
35
41
|
def write(data)
|
36
42
|
super
|
37
43
|
file.write data
|
@@ -1,17 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'destination_base'
|
4
|
+
|
3
5
|
module ProcessExecuter
|
4
6
|
module Destinations
|
5
7
|
# Handles file paths with specific open modes
|
6
8
|
#
|
7
9
|
# @api private
|
8
|
-
class FilePathMode <
|
10
|
+
class FilePathMode < DestinationBase
|
9
11
|
# Initializes a new file path with mode destination handler
|
10
12
|
#
|
11
|
-
#
|
13
|
+
# Redirects to the file at destination via `open(destination[0], destination[1], 0644)`
|
12
14
|
#
|
13
15
|
# @param destination [Array<String, String>] array with file path and mode
|
14
|
-
# @return [FilePathMode] a new file path with mode destination handler
|
15
16
|
# @raise [Errno::ENOENT] if the file path is invalid
|
16
17
|
# @raise [ArgumentError] if the mode is invalid
|
17
18
|
def initialize(destination)
|
@@ -26,13 +27,16 @@ module ProcessExecuter
|
|
26
27
|
|
27
28
|
# Writes data to the file
|
28
29
|
#
|
30
|
+
# @example
|
31
|
+
# mode_handler = ProcessExecuter::Destinations::FilePathMode.new(["output.log", "a"])
|
32
|
+
# mode_handler.write("Appended log entry")
|
33
|
+
#
|
29
34
|
# @param data [String] the data to write
|
35
|
+
#
|
30
36
|
# @return [Integer] the number of bytes written
|
37
|
+
#
|
31
38
|
# @raise [IOError] if the file is closed
|
32
39
|
#
|
33
|
-
# @example
|
34
|
-
# mode_handler = ProcessExecuter::Destinations::FilePathMode.new(["output.log", "a"])
|
35
|
-
# mode_handler.write("Appended log entry")
|
36
40
|
def write(data)
|
37
41
|
super
|
38
42
|
file.write data
|
@@ -1,19 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'destination_base'
|
4
|
+
|
3
5
|
module ProcessExecuter
|
4
6
|
module Destinations
|
5
7
|
# Handles file paths with specific open modes and permissions
|
6
8
|
#
|
7
9
|
# @api private
|
8
|
-
class FilePathModePerms <
|
10
|
+
class FilePathModePerms < DestinationBase
|
9
11
|
# Initializes a new file path with mode and permissions destination handler
|
10
12
|
#
|
11
13
|
# Opens the file at the given path with the specified mode and permissions.
|
12
14
|
#
|
13
15
|
# @param destination [Array<String, String, Integer>] array with file path, mode, and permissions
|
14
|
-
#
|
16
|
+
#
|
15
17
|
# @raise [Errno::ENOENT] if the file path is invalid
|
18
|
+
#
|
16
19
|
# @raise [ArgumentError] if the mode is invalid
|
20
|
+
#
|
17
21
|
def initialize(destination)
|
18
22
|
super
|
19
23
|
@file = File.open(destination[0], destination[1], destination[2])
|
@@ -26,13 +30,16 @@ module ProcessExecuter
|
|
26
30
|
|
27
31
|
# Writes data to the file
|
28
32
|
#
|
33
|
+
# @example
|
34
|
+
# perms_handler = ProcessExecuter::Destinations::FilePathModePerms.new(["output.log", "w", 0644])
|
35
|
+
# perms_handler.write("Log entry with specific permissions")
|
36
|
+
#
|
29
37
|
# @param data [String] the data to write
|
38
|
+
#
|
30
39
|
# @return [Integer] the number of bytes written
|
40
|
+
#
|
31
41
|
# @raise [IOError] if the file is closed
|
32
42
|
#
|
33
|
-
# @example
|
34
|
-
# perms_handler = ProcessExecuter::Destinations::FilePathModePerms.new(["output.log", "w", 0644])
|
35
|
-
# perms_handler.write("Log entry with specific permissions")
|
36
43
|
def write(data)
|
37
44
|
super
|
38
45
|
file.write data
|
@@ -1,21 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'destination_base'
|
4
|
+
|
3
5
|
module ProcessExecuter
|
4
6
|
module Destinations
|
5
7
|
# Handles IO objects
|
6
8
|
#
|
7
9
|
# @api private
|
8
|
-
class IO <
|
10
|
+
class IO < DestinationBase
|
9
11
|
# Writes data to the IO object
|
10
12
|
#
|
11
|
-
# @param data [String] the data to write
|
12
|
-
# @return [Integer] the number of bytes written
|
13
|
-
# @raise [IOError] if the IO object is closed
|
14
|
-
#
|
15
13
|
# @example
|
16
14
|
# io = File.open('file.txt', 'w')
|
17
15
|
# io_handler = ProcessExecuter::Destinations::IO.new(io)
|
18
16
|
# io_handler.write("Hello world")
|
17
|
+
#
|
18
|
+
# @param data [String] the data to write
|
19
|
+
#
|
20
|
+
# @return [Integer] the number of bytes written
|
21
|
+
#
|
22
|
+
# @raise [IOError] if the IO object is closed
|
23
|
+
#
|
19
24
|
def write(data)
|
20
25
|
super
|
21
26
|
destination.write data
|