process_executer 3.2.3 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.commitlintrc.yml +27 -6
  3. data/.release-please-manifest.json +1 -1
  4. data/CHANGELOG.md +49 -0
  5. data/README.md +177 -134
  6. data/lib/process_executer/commands/run.rb +124 -0
  7. data/lib/process_executer/commands/run_with_capture.rb +148 -0
  8. data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
  9. data/lib/process_executer/commands.rb +11 -0
  10. data/lib/process_executer/destinations/child_redirection.rb +5 -4
  11. data/lib/process_executer/destinations/close.rb +5 -4
  12. data/lib/process_executer/destinations/destination_base.rb +73 -0
  13. data/lib/process_executer/destinations/file_descriptor.rb +10 -6
  14. data/lib/process_executer/destinations/file_path.rb +12 -6
  15. data/lib/process_executer/destinations/file_path_mode.rb +10 -6
  16. data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
  17. data/lib/process_executer/destinations/io.rb +10 -5
  18. data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
  19. data/lib/process_executer/destinations/stderr.rb +8 -4
  20. data/lib/process_executer/destinations/stdout.rb +8 -4
  21. data/lib/process_executer/destinations/tee.rb +24 -17
  22. data/lib/process_executer/destinations/writer.rb +12 -7
  23. data/lib/process_executer/destinations.rb +32 -17
  24. data/lib/process_executer/errors.rb +50 -26
  25. data/lib/process_executer/monitored_pipe.rb +128 -59
  26. data/lib/process_executer/options/base.rb +118 -82
  27. data/lib/process_executer/options/option_definition.rb +5 -1
  28. data/lib/process_executer/options/run_options.rb +13 -12
  29. data/lib/process_executer/options/run_with_capture_options.rb +156 -0
  30. data/lib/process_executer/options/spawn_options.rb +31 -30
  31. data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
  32. data/lib/process_executer/options.rb +3 -1
  33. data/lib/process_executer/result.rb +35 -77
  34. data/lib/process_executer/result_with_capture.rb +62 -0
  35. data/lib/process_executer/version.rb +2 -1
  36. data/lib/process_executer.rb +384 -346
  37. data/process_executer.gemspec +11 -2
  38. data/release-please-config.json +16 -2
  39. metadata +18 -8
  40. data/lib/process_executer/destination_base.rb +0 -83
  41. data/lib/process_executer/runner.rb +0 -144
@@ -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
@@ -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 generic objects that respond to write
7
+ # Handles [:child, fd] redirection options as supported by `Process.spawn`
6
8
  #
7
9
  # @api private
8
- class ChildRedirection < ProcessExecuter::DestinationBase
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 responds to write but is not an IO with fileno
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 generic objects that respond to write
7
+ # Handles the :close redirection option as supported by `Process.spawn`
6
8
  #
7
9
  # @api private
8
- class Close < ProcessExecuter::DestinationBase
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 responds to write but is not an IO with fileno
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
- require 'process_executer/destination_base'
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 < ProcessExecuter::DestinationBase
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
- # @return [Boolean] true if destination is an Integer that's not stdout/stderr
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) && ![:out, 1, :err, 2].include?(destination)
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 < ProcessExecuter::DestinationBase
10
+ class FilePath < DestinationBase
9
11
  # Initializes a new file path destination handler
10
12
  #
11
- # Opens the file at the given path for writing.
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
- # @return [FilePath] a new file path destination handler
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