process_executer 3.2.4 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +177 -134
  5. data/lib/process_executer/commands/run.rb +124 -0
  6. data/lib/process_executer/commands/run_with_capture.rb +148 -0
  7. data/lib/process_executer/commands/spawn_with_timeout.rb +163 -0
  8. data/lib/process_executer/commands.rb +11 -0
  9. data/lib/process_executer/destinations/child_redirection.rb +5 -4
  10. data/lib/process_executer/destinations/close.rb +5 -4
  11. data/lib/process_executer/destinations/destination_base.rb +73 -0
  12. data/lib/process_executer/destinations/file_descriptor.rb +10 -6
  13. data/lib/process_executer/destinations/file_path.rb +12 -6
  14. data/lib/process_executer/destinations/file_path_mode.rb +10 -6
  15. data/lib/process_executer/destinations/file_path_mode_perms.rb +12 -5
  16. data/lib/process_executer/destinations/io.rb +10 -5
  17. data/lib/process_executer/destinations/monitored_pipe.rb +10 -5
  18. data/lib/process_executer/destinations/stderr.rb +8 -4
  19. data/lib/process_executer/destinations/stdout.rb +8 -4
  20. data/lib/process_executer/destinations/tee.rb +24 -17
  21. data/lib/process_executer/destinations/writer.rb +12 -7
  22. data/lib/process_executer/destinations.rb +32 -17
  23. data/lib/process_executer/errors.rb +50 -26
  24. data/lib/process_executer/monitored_pipe.rb +128 -59
  25. data/lib/process_executer/options/base.rb +118 -82
  26. data/lib/process_executer/options/option_definition.rb +5 -1
  27. data/lib/process_executer/options/run_options.rb +13 -12
  28. data/lib/process_executer/options/run_with_capture_options.rb +156 -0
  29. data/lib/process_executer/options/spawn_options.rb +31 -30
  30. data/lib/process_executer/options/{spawn_and_wait_options.rb → spawn_with_timeout_options.rb} +11 -7
  31. data/lib/process_executer/options.rb +3 -1
  32. data/lib/process_executer/result.rb +35 -77
  33. data/lib/process_executer/result_with_capture.rb +62 -0
  34. data/lib/process_executer/version.rb +2 -1
  35. data/lib/process_executer.rb +384 -346
  36. data/process_executer.gemspec +11 -2
  37. metadata +18 -8
  38. data/lib/process_executer/destination_base.rb +0 -83
  39. data/lib/process_executer/runner.rb +0 -144
@@ -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
@@ -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 < ProcessExecuter::DestinationBase
10
+ class FilePathMode < DestinationBase
9
11
  # Initializes a new file path with mode destination handler
10
12
  #
11
- # Opens the file at the given path with the specified mode.
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 < ProcessExecuter::DestinationBase
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
- # @return [FilePathModePerms] a new handler instance
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 < ProcessExecuter::DestinationBase
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