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
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles IO objects
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class IO < ProcessExecuter::DestinationBase
|
9
|
+
# Writes data to the IO object
|
10
|
+
#
|
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
|
+
# @example
|
16
|
+
# io = File.open('file.txt', 'w')
|
17
|
+
# io_handler = ProcessExecuter::Destinations::IO.new(io)
|
18
|
+
# io_handler.write("Hello world")
|
19
|
+
def write(data)
|
20
|
+
super
|
21
|
+
destination.write data
|
22
|
+
end
|
23
|
+
|
24
|
+
# Determines if this class can handle the given destination
|
25
|
+
#
|
26
|
+
# @param destination [Object] the destination to check
|
27
|
+
# @return [Boolean] true if destination is an IO with a valid file descriptor
|
28
|
+
def self.handles?(destination)
|
29
|
+
destination.is_a?(::IO) && destination.respond_to?(:fileno) && destination.fileno
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles monitored pipes
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class MonitoredPipe < ProcessExecuter::DestinationBase
|
9
|
+
# Writes data to the monitored pipe
|
10
|
+
#
|
11
|
+
# @param data [String] the data to write
|
12
|
+
# @return [Object] the return value of the pipe's write method
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# pipe = ProcessExecuter::MonitoredPipe.new
|
16
|
+
# pipe_handler = ProcessExecuter::Destinations::MonitoredPipe.new(pipe)
|
17
|
+
# pipe_handler.write("Data to pipe")
|
18
|
+
def write(data)
|
19
|
+
super
|
20
|
+
destination.write data
|
21
|
+
end
|
22
|
+
|
23
|
+
# Closes the pipe if it's open
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def close
|
27
|
+
destination.close if destination.state == :open
|
28
|
+
end
|
29
|
+
|
30
|
+
# Determines if this class can handle the given destination
|
31
|
+
#
|
32
|
+
# @param destination [Object] the destination to check
|
33
|
+
# @return [Boolean] true if destination is a ProcessExecuter::MonitoredPipe
|
34
|
+
def self.handles?(destination)
|
35
|
+
destination.is_a? ProcessExecuter::MonitoredPipe
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles standard error redirection
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Stderr < ProcessExecuter::DestinationBase
|
9
|
+
# Writes data to standard error
|
10
|
+
#
|
11
|
+
# @param data [String] the data to write
|
12
|
+
# @return [Integer] the number of bytes written
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# stderr_handler = ProcessExecuter::Destinations::Stderr.new(:err)
|
16
|
+
# stderr_handler.write("Error message")
|
17
|
+
def write(data)
|
18
|
+
super
|
19
|
+
$stderr.write data
|
20
|
+
end
|
21
|
+
|
22
|
+
# Determines if this class can handle the given destination
|
23
|
+
#
|
24
|
+
# @param destination [Object] the destination to check
|
25
|
+
# @return [Boolean] true if destination is :err or 2
|
26
|
+
def self.handles?(destination)
|
27
|
+
[:err, 2].include?(destination)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles standard output redirection
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Stdout < ProcessExecuter::DestinationBase
|
9
|
+
# Writes data to standard output
|
10
|
+
#
|
11
|
+
# @param data [String] the data to write
|
12
|
+
# @return [Integer] the number of bytes written
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# stdout_handler = ProcessExecuter::Destinations::Stdout.new(:out)
|
16
|
+
# stdout_handler.write("Hello world")
|
17
|
+
def write(data)
|
18
|
+
super
|
19
|
+
$stdout.write data
|
20
|
+
end
|
21
|
+
|
22
|
+
# Determines if this class can handle the given destination
|
23
|
+
#
|
24
|
+
# @param destination [Object] the destination to check
|
25
|
+
# @return [Boolean] true if destination is :out or 1
|
26
|
+
def self.handles?(destination)
|
27
|
+
[:out, 1].include?(destination)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProcessExecuter
|
4
|
+
module Destinations
|
5
|
+
# Handles destination for writing to multiple destinations
|
6
|
+
#
|
7
|
+
# The destination is an array with the first element being :tee and the rest
|
8
|
+
# being the destinations.
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Tee < ProcessExecuter::DestinationBase
|
12
|
+
# Initializes a new file path with mode and permissions destination handler
|
13
|
+
#
|
14
|
+
# Opens the file at the given path with the specified mode and permissions.
|
15
|
+
#
|
16
|
+
# @param destination [Array<String, String, Integer>] array with file path, mode, and permissions
|
17
|
+
# @return [FilePathModePerms] a new handler instance
|
18
|
+
# @raise [Errno::ENOENT] if the file path is invalid
|
19
|
+
# @raise [ArgumentError] if the mode is invalid
|
20
|
+
def initialize(destination)
|
21
|
+
super
|
22
|
+
@child_destinations = destination[1..].map { |dest| ProcessExecuter::Destinations.factory(dest) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# The opened file object
|
26
|
+
#
|
27
|
+
# @return [File] the opened file
|
28
|
+
attr_reader :child_destinations
|
29
|
+
|
30
|
+
# Writes data to the file
|
31
|
+
#
|
32
|
+
# @param data [String] the data to write
|
33
|
+
# @return [Integer] the number of bytes written
|
34
|
+
# @raise [IOError] if the file is closed
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# perms_handler = ProcessExecuter::Destinations::FilePathModePerms.new(["output.log", "w", 0644])
|
38
|
+
# perms_handler.write("Log entry with specific permissions")
|
39
|
+
def write(data)
|
40
|
+
super
|
41
|
+
child_destinations.each { |dest| dest.write(data) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Closes the file if it's open
|
45
|
+
#
|
46
|
+
# @return [void]
|
47
|
+
def close
|
48
|
+
child_destinations.each(&:close)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Determines if this class can handle the given destination
|
52
|
+
#
|
53
|
+
# @param destination [Object] the destination to check
|
54
|
+
# @return [Boolean] true if destination is an Array with path, mode, and permissions
|
55
|
+
def self.handles?(destination)
|
56
|
+
destination.is_a?(Array) && destination.size > 1 && destination[0] == :tee
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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 Writer < ProcessExecuter::DestinationBase
|
9
|
+
# Writes data to the destination object
|
10
|
+
#
|
11
|
+
# @param data [String] the data to write
|
12
|
+
# @return [Object] the return value of the destination's write method
|
13
|
+
# @raise [NoMethodError] if the destination doesn't respond to write
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# buffer = StringIO.new
|
17
|
+
# writer_handler = ProcessExecuter::Destinations::Writer.new(buffer)
|
18
|
+
# writer_handler.write("Hello world")
|
19
|
+
def write(data)
|
20
|
+
super
|
21
|
+
destination.write data
|
22
|
+
end
|
23
|
+
|
24
|
+
# Determines if this class can handle the given destination
|
25
|
+
#
|
26
|
+
# @param destination [Object] the destination to check
|
27
|
+
# @return [Boolean] true if destination responds to write but is not an IO with fileno
|
28
|
+
def self.handles?(destination)
|
29
|
+
destination.respond_to?(:write) && (!destination.respond_to?(:fileno) || destination.fileno.nil?)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'destinations/child_redirection'
|
4
|
+
require_relative 'destinations/file_descriptor'
|
5
|
+
require_relative 'destinations/file_path'
|
6
|
+
require_relative 'destinations/file_path_mode'
|
7
|
+
require_relative 'destinations/file_path_mode_perms'
|
8
|
+
require_relative 'destinations/io'
|
9
|
+
require_relative 'destinations/monitored_pipe'
|
10
|
+
require_relative 'destinations/stderr'
|
11
|
+
require_relative 'destinations/stdout'
|
12
|
+
require_relative 'destinations/tee'
|
13
|
+
require_relative 'destinations/writer'
|
14
|
+
|
15
|
+
module ProcessExecuter
|
16
|
+
# Collection of destination handler implementations
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
module Destinations
|
20
|
+
# Creates appropriate destination objects based on the given destination
|
21
|
+
#
|
22
|
+
# This factory method dynamically finds and instantiates the appropriate
|
23
|
+
# destination class for handling the provided destination.
|
24
|
+
#
|
25
|
+
# @param destination [Object] the destination to create a handler for
|
26
|
+
# @return [DestinationBase] an instance of the appropriate destination handler
|
27
|
+
# @raise [ArgumentError] if no matching destination class is found
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# ProcessExecuter.destination_factory(1) #=> Returns a Stdout instance
|
31
|
+
# ProcessExecuter.destination_factory("output.log") #=> Returns a FilePath instance
|
32
|
+
def self.factory(destination)
|
33
|
+
matching_class = matching_destination_class(destination)
|
34
|
+
return matching_class.new(destination) if matching_class
|
35
|
+
|
36
|
+
raise ArgumentError, 'wrong exec redirect action'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Determines if the given destination is compatible with a monitored pipe
|
40
|
+
#
|
41
|
+
# If true, this destination should not be wrapped in a monitored pipe.
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# ProcessExecuter::Destinations.compatible_with_monitored_pipe?(1) #=> true
|
45
|
+
# ProcessExecuter::Destinations.compatible_with_monitored_pipe?([:child, 6]) #=> false
|
46
|
+
# ProcessExecuter::Destinations.compatible_with_monitored_pipe?([:close]) #=> false
|
47
|
+
#
|
48
|
+
# @param destination [Object] the destination to check
|
49
|
+
# @return [Boolean, nil] true if the destination is compatible with a monitored pipe
|
50
|
+
# @raise [ArgumentError] if no matching destination class is found
|
51
|
+
# @api public
|
52
|
+
def self.compatible_with_monitored_pipe?(destination)
|
53
|
+
matching_class = matching_destination_class(destination)
|
54
|
+
matching_class&.compatible_with_monitored_pipe?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Determines the destination class that can handle the given destination
|
58
|
+
# @param destination [Object] the destination to check
|
59
|
+
# @return [Class] the destination class that can handle the given destination
|
60
|
+
# @api private
|
61
|
+
def self.matching_destination_class(destination)
|
62
|
+
destination_classes =
|
63
|
+
ProcessExecuter::Destinations.constants
|
64
|
+
.map { |const| ProcessExecuter::Destinations.const_get(const) }
|
65
|
+
.select { |const| const.is_a?(Class) }
|
66
|
+
|
67
|
+
destination_classes.find { |klass| klass.handles?(destination) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -4,19 +4,16 @@ require 'stringio'
|
|
4
4
|
require 'io/wait'
|
5
5
|
|
6
6
|
module ProcessExecuter
|
7
|
-
#
|
7
|
+
# Write data sent through a pipe to a destination
|
8
8
|
#
|
9
9
|
# When a new MonitoredPipe is created, a pipe is created (via IO.pipe) and
|
10
10
|
# a thread is created to read data written to the pipe.
|
11
11
|
#
|
12
|
-
#
|
13
|
-
# `#initialize`.
|
14
|
-
#
|
15
|
-
# If any of the writers raise an exception, the monitoring thread will exit, the
|
12
|
+
# If the destination raises an exception, the monitoring thread will exit, the
|
16
13
|
# pipe will be closed, and the exception will be saved in `#exception`.
|
17
14
|
#
|
18
15
|
# `#close` must be called to ensure that (1) the pipe is closed, (2) all data is
|
19
|
-
# read from the pipe and written to the
|
16
|
+
# read from the pipe and written to the destination, and (3) the monitoring thread is
|
20
17
|
# killed.
|
21
18
|
#
|
22
19
|
# @example Collect pipe data into a string
|
@@ -52,11 +49,15 @@ module ProcessExecuter
|
|
52
49
|
# data_collector = StringIO.new
|
53
50
|
# pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
|
54
51
|
#
|
55
|
-
# @param
|
52
|
+
# @param redirection_destination [Array<#write>] as data is read from the pipe,
|
53
|
+
# it is written to this destination
|
56
54
|
# @param chunk_size [Integer] the size of the chunks to read from the pipe
|
57
55
|
#
|
58
|
-
def initialize(
|
59
|
-
@
|
56
|
+
def initialize(redirection_destination, chunk_size: 100_000)
|
57
|
+
@destination = Destinations.factory(redirection_destination)
|
58
|
+
|
59
|
+
assert_destination_is_compatible_with_monitored_pipe
|
60
|
+
|
60
61
|
@chunk_size = chunk_size
|
61
62
|
@pipe_reader, @pipe_writer = IO.pipe
|
62
63
|
@state = :open
|
@@ -87,12 +88,14 @@ module ProcessExecuter
|
|
87
88
|
|
88
89
|
@state = :closing
|
89
90
|
sleep 0.001 until state == :closed
|
91
|
+
|
92
|
+
destination.close
|
90
93
|
end
|
91
94
|
|
92
95
|
# Return the write end of the pipe so that data can be written to it
|
93
96
|
#
|
94
97
|
# Data written to this end of the pipe will be read by the monitor thread and
|
95
|
-
# written to the
|
98
|
+
# written to the destination.
|
96
99
|
#
|
97
100
|
# This is so we can provide a MonitoredPipe to Process.spawn as a FD
|
98
101
|
#
|
@@ -170,24 +173,17 @@ module ProcessExecuter
|
|
170
173
|
|
171
174
|
# @!attribute [r]
|
172
175
|
#
|
173
|
-
#
|
176
|
+
# The redirection destination to write data that is read from the pipe
|
174
177
|
#
|
175
|
-
# @example
|
178
|
+
# @example
|
176
179
|
# require 'stringio'
|
177
180
|
# data_collector = StringIO.new
|
178
181
|
# pipe = ProcessExecuter::MonitoredPipe.new(data_collector)
|
179
|
-
# pipe.
|
182
|
+
# pipe.destination #=>
|
180
183
|
#
|
181
|
-
# @
|
182
|
-
# require 'stringio'
|
183
|
-
# data_collector1 = StringIO.new
|
184
|
-
# data_collector2 = StringIO.new
|
185
|
-
# pipe = ProcessExecuter::MonitoredPipe.new(data_collector1, data_collector2)
|
186
|
-
# pipe.writers #=> [data_collector1, data_collector2]]
|
187
|
-
#
|
188
|
-
# @return [Array<#write>]
|
184
|
+
# @return [Array<ProcessExecuter::Destination::Base>]
|
189
185
|
#
|
190
|
-
attr_reader :
|
186
|
+
attr_reader :destination
|
191
187
|
|
192
188
|
# @!attribute [r]
|
193
189
|
#
|
@@ -246,19 +242,29 @@ module ProcessExecuter
|
|
246
242
|
|
247
243
|
# @!attribute [r]
|
248
244
|
#
|
249
|
-
# The exception raised by a
|
245
|
+
# The exception raised by a destination
|
250
246
|
#
|
251
|
-
# If an exception is raised by a
|
247
|
+
# If an exception is raised by a destination, it is stored here. Otherwise, it is `nil`.
|
252
248
|
#
|
253
249
|
# @example
|
254
250
|
# pipe.exception #=> nil
|
255
251
|
#
|
256
|
-
# @return [Exception, nil] the exception raised by a
|
252
|
+
# @return [Exception, nil] the exception raised by a destination or `nil` if no exception was raised
|
257
253
|
#
|
258
254
|
attr_reader :exception
|
259
255
|
|
260
256
|
private
|
261
257
|
|
258
|
+
# Raise an error if the destination is not compatible with MonitoredPipe
|
259
|
+
# @return [void]
|
260
|
+
# @raise [ArgumentError] if the destination is not compatible with MonitoredPipe
|
261
|
+
# @api private
|
262
|
+
def assert_destination_is_compatible_with_monitored_pipe
|
263
|
+
return if destination.compatible_with_monitored_pipe?
|
264
|
+
|
265
|
+
raise ArgumentError, "Destination #{destination.destination} is not compatible with MonitoredPipe"
|
266
|
+
end
|
267
|
+
|
262
268
|
# Read data from the pipe until `#state` is changed to `:closing`
|
263
269
|
#
|
264
270
|
# The state is changed to `:closed` by calling `#close`.
|
@@ -275,7 +281,7 @@ module ProcessExecuter
|
|
275
281
|
|
276
282
|
# Read data from the pipe until `#state` is changed to `:closing`
|
277
283
|
#
|
278
|
-
# Data read from the pipe is written to the
|
284
|
+
# Data read from the pipe is written to the destination.
|
279
285
|
#
|
280
286
|
# @return [void]
|
281
287
|
# @api private
|
@@ -286,14 +292,14 @@ module ProcessExecuter
|
|
286
292
|
pipe_reader.wait_readable(0.001)
|
287
293
|
end
|
288
294
|
|
289
|
-
# Check if the writer is a file descriptor
|
290
|
-
#
|
291
|
-
# @param writer [#write] the writer to check
|
292
|
-
# @return [Boolean] true if the writer is a file descriptor
|
293
|
-
# @api private
|
294
|
-
def file_descriptor?(writer) = writer.is_a?(Integer) || writer.is_a?(Symbol)
|
295
|
+
# # Check if the writer is a file descriptor
|
296
|
+
# #
|
297
|
+
# # @param writer [#write] the writer to check
|
298
|
+
# # @return [Boolean] true if the writer is a file descriptor
|
299
|
+
# # @api private
|
300
|
+
# def file_descriptor?(writer) = writer.is_a?(Integer) || writer.is_a?(Symbol)
|
295
301
|
|
296
|
-
# Write the data read from the pipe to
|
302
|
+
# Write the data read from the pipe to the destination
|
297
303
|
#
|
298
304
|
# If an exception is raised by a writer, set the state to `:closing`
|
299
305
|
# so that the pipe can be closed.
|
@@ -302,35 +308,12 @@ module ProcessExecuter
|
|
302
308
|
# @return [void]
|
303
309
|
# @api private
|
304
310
|
def write_data(data)
|
305
|
-
|
306
|
-
file_descriptor?(w) ? write_data_to_fd(w, data) : w.write(data)
|
307
|
-
end
|
311
|
+
destination.write(data)
|
308
312
|
rescue StandardError => e
|
309
313
|
@exception = e
|
310
314
|
@state = :closing
|
311
315
|
end
|
312
316
|
|
313
|
-
# Write data to the given file_descriptor correctly handling stdout and stderr
|
314
|
-
# @param file_descriptor [Integer, Symbol] the file descriptor to write to (either an integer or :out or :err)
|
315
|
-
# @param data [String] the data to write
|
316
|
-
# @return [void]
|
317
|
-
# @api private
|
318
|
-
def write_data_to_fd(file_descriptor, data)
|
319
|
-
# The case line is not marked as not covered only when using TruffleRuby
|
320
|
-
# :nocov:
|
321
|
-
case file_descriptor
|
322
|
-
# :nocov:
|
323
|
-
when :out, 1
|
324
|
-
$stdout.write data
|
325
|
-
when :err, 2
|
326
|
-
$stderr.write data
|
327
|
-
else
|
328
|
-
io = IO.open(file_descriptor, mode: 'a', autoclose: false)
|
329
|
-
io.write(data)
|
330
|
-
io.close
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
317
|
# Read any remaining data from the pipe and close it
|
335
318
|
#
|
336
319
|
# @return [void]
|