process_executer 1.1.2 → 1.3.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.
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+ require_relative 'result'
5
+
6
+ module ProcessExecuter
7
+ module Command
8
+ # The `Runner` class executes subprocess commands and captures their status and output.
9
+ #
10
+ # It does the following:
11
+ # - Run commands (`call`) with options for capturing output, handling timeouts, and merging stdout/stderr.
12
+ # - Process command results, including logging and error handling.
13
+ # - Raise detailed exceptions for common command failures, such as timeouts or subprocess errors.
14
+ #
15
+ # This class is used internally by {ProcessExecuter.run}.
16
+ #
17
+ # @api public
18
+ #
19
+ class Runner
20
+ # Create a new RunCommand instance
21
+ #
22
+ # @example
23
+ # runner = Runner.new()
24
+ # status = runner.call('echo', 'hello')
25
+ #
26
+ # @param logger [Logger] The logger to use. Defaults to a no-op logger if nil.
27
+ #
28
+ def initialize(logger)
29
+ @logger = logger || Logger.new(nil)
30
+ end
31
+
32
+ # The logger to use
33
+ # @example
34
+ # runner.logger #=> #<Logger:0x00007f9b1b8b3d20>
35
+ # @return [Logger]
36
+ attr_reader :logger
37
+
38
+ # rubocop:disable Metrics/ParameterLists
39
+
40
+ # Run a command and return the status including stdout and stderr output
41
+ #
42
+ # @example
43
+ # command = %w[git status]
44
+ # status = run(command)
45
+ # status.success? # => true
46
+ # status.exitstatus # => 0
47
+ # status.out # => "On branch master\nnothing to commit, working tree clean\n"
48
+ # status.err # => ""
49
+ #
50
+ # @param command [Array<String>] The command to run
51
+ # @param out [#write] The object to which stdout is written
52
+ # @param err [#write] The object to which stderr is written
53
+ # @param merge [Boolean] Write both stdout and stderr into the buffer for stdout
54
+ # @param raise_errors [Boolean] Raise an exception if the command fails
55
+ # @param options_hash [Hash] Additional options to pass to Process.spawn
56
+ #
57
+ # See {ProcessExecuter.run} for a full list of options.
58
+ #
59
+ # @return [ProcessExecuter::Command::Result] The status of the subprocess and captured stdout and stderr output
60
+ #
61
+ def call(*command, out: nil, err: nil, merge: false, raise_errors: true, **options_hash)
62
+ out ||= StringIO.new
63
+ err ||= (merge ? out : StringIO.new)
64
+
65
+ status = spawn(command, out:, err:, **options_hash)
66
+
67
+ process_result(command, status, out, err, options_hash[:timeout], raise_errors)
68
+ end
69
+
70
+ # rubocop:enable Metrics/ParameterLists
71
+
72
+ private
73
+
74
+ # Wrap the output buffers in pipes and then execute the command
75
+ #
76
+ # @param command [Array<String>] The command to execute
77
+ # @param out [#write] The object to which stdout is written
78
+ # @param err [#write] The object to which stderr is written
79
+ # @param options_hash [Hash] Additional options to pass to Process.spawn
80
+ #
81
+ # See {ProcessExecuter.run} for a full list of options.
82
+ #
83
+ # @raise [ProcessExecuter::Command::ProcessIOError] If an exception was raised while collecting subprocess output
84
+ # @raise [ProcessExecuter::Command::TimeoutError] If the command times out
85
+ #
86
+ # @return [ProcessExecuter::Status] The status of the completed subprocess
87
+ #
88
+ # @api private
89
+ #
90
+ def spawn(command, out:, err:, **options_hash)
91
+ out = [out] unless out.is_a?(Array)
92
+ err = [err] unless err.is_a?(Array)
93
+ out_pipe = ProcessExecuter::MonitoredPipe.new(*out)
94
+ err_pipe = ProcessExecuter::MonitoredPipe.new(*err)
95
+ ProcessExecuter.spawn(*command, out: out_pipe, err: err_pipe, **options_hash)
96
+ ensure
97
+ out_pipe.close
98
+ err_pipe.close
99
+ raise_pipe_error(command, :stdout, out_pipe) if out_pipe.exception
100
+ raise_pipe_error(command, :stderr, err_pipe) if err_pipe.exception
101
+ end
102
+
103
+ # rubocop:disable Metrics/ParameterLists
104
+
105
+ # Process the result of the command and return a ProcessExecuter::Command::Result
106
+ #
107
+ # Log the command and result, and raise an error if the command failed.
108
+ #
109
+ # @param command [Array<String>] The git command that was executed
110
+ # @param status [Process::Status] The status of the completed subprocess
111
+ # @param out [#write] The object that stdout was written to
112
+ # @param err [#write] The object that stderr was written to
113
+ # @param timeout [Numeric, nil] The maximum seconds to wait for the command to complete
114
+ # @param raise_errors [Boolean] Raise an exception if the command fails
115
+ #
116
+ # @return [ProcessExecuter::Command::Result] The status of the subprocess and captured stdout and stderr output
117
+ #
118
+ # @raise [ProcessExecuter::Command::FailedError] If the command failed
119
+ # @raise [ProcessExecuter::Command::SignaledError] If the command was signaled
120
+ # @raise [ProcessExecuter::Command::TimeoutError] If the command times out
121
+ # @raise [ProcessExecuter::Command::ProcessIOError] If an exception was raised while collecting subprocess output
122
+ #
123
+ # @api private
124
+ #
125
+ def process_result(command, status, out, err, timeout, raise_errors)
126
+ Result.new(command, status, out, err).tap do |result|
127
+ log_result(result)
128
+
129
+ if raise_errors
130
+ raise TimeoutError.new(result, timeout) if status.timeout?
131
+ raise SignaledError, result if status.signaled?
132
+ raise FailedError, result unless status.success?
133
+ end
134
+ end
135
+ end
136
+
137
+ # rubocop:enable Metrics/ParameterLists
138
+
139
+ # Log the command and result of the subprocess
140
+ # @param result [ProcessExecuter::Command::Result] the result of the command including
141
+ # the command, status, stdout, and stderr
142
+ # @return [void]
143
+ # @api private
144
+ def log_result(result)
145
+ logger.info { "#{result.command} exited with status #{result}" }
146
+ logger.debug { "stdout:\n#{result.stdout_to_s.inspect}\nstderr:\n#{result.stderr_to_s.inspect}" }
147
+ end
148
+
149
+ # Raise an error when there was exception while collecting the subprocess output
150
+ #
151
+ # @param command [Array<String>] The command that was executed
152
+ # @param pipe_name [Symbol] The name of the pipe that raised the exception
153
+ # @param pipe [ProcessExecuter::MonitoredPipe] The pipe that raised the exception
154
+ #
155
+ # @raise [ProcessExecuter::Command::ProcessIOError]
156
+ #
157
+ # @return [void] This method always raises an error
158
+ #
159
+ # @api private
160
+ #
161
+ def raise_pipe_error(command, pipe_name, pipe)
162
+ error = ProcessExecuter::Command::ProcessIOError.new("Pipe Exception for #{command}: #{pipe_name}")
163
+ raise(error, cause: pipe.exception)
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ProcessExecuter
4
+ # This module contains classes for implementing ProcessExecuter.run_command
5
+ module Command; end
6
+ end
7
+
8
+ require_relative 'command/errors'
9
+ require_relative 'command/result'
10
+ require_relative 'command/runner'
11
+
12
+ # Runs a command and returns the result
@@ -281,19 +281,56 @@ module ProcessExecuter
281
281
  # @api private
282
282
  def monitor_pipe
283
283
  new_data = pipe_reader.read_nonblock(chunk_size)
284
- # SimpleCov under JRuby reports the begin statement as not covered, but it is
285
- # :nocov:
286
- begin
287
- # :nocov:
288
- writers.each { |w| w.write(new_data) }
289
- rescue StandardError => e
290
- @exception = e
291
- @state = :closing
292
- end
284
+ write_data(new_data)
293
285
  rescue IO::WaitReadable
294
286
  pipe_reader.wait_readable(0.001)
295
287
  end
296
288
 
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
+
296
+ # Write the data read from the pipe to all destinations
297
+ #
298
+ # If an exception is raised by a writer, set the state to `:closing`
299
+ # so that the pipe can be closed.
300
+ #
301
+ # @param data [String] the data read from the pipe
302
+ # @return [void]
303
+ # @api private
304
+ def write_data(data)
305
+ writers.each do |w|
306
+ file_descriptor?(w) ? write_data_to_fd(w, data) : w.write(data)
307
+ end
308
+ rescue StandardError => e
309
+ @exception = e
310
+ @state = :closing
311
+ end
312
+
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
+
297
334
  # Read any remaining data from the pipe and close it
298
335
  #
299
336
  # @return [void]
@@ -15,6 +15,7 @@ module ProcessExecuter
15
15
  #
16
16
  # @param status [Process::Status] the status to delegate to
17
17
  # @param timeout [Boolean] true if the process timed out
18
+ # @param timeout_duration [Numeric, nil] The secs the command ran before being killed OR o or nil for no timeout
18
19
  #
19
20
  # @example
20
21
  # status = Process.wait2(pid).last
@@ -23,23 +24,47 @@ module ProcessExecuter
23
24
  #
24
25
  # @api public
25
26
  #
26
- def initialize(status, timeout)
27
+ def initialize(status, timeout, timeout_duration)
27
28
  super(status)
28
29
  @timeout = timeout
30
+ @timeout_duration = timeout_duration
29
31
  end
30
32
 
33
+ # The secs the command ran before being killed OR o or nil for no timeout
34
+ # @example
35
+ # status.timeout_duration #=> 10
36
+ # @return [Numeric, nil]
37
+ attr_reader :timeout_duration
38
+
31
39
  # @!attribute [r] timeout?
32
- #
33
40
  # True if the process timed out and was sent the SIGKILL signal
34
- #
35
41
  # @example
36
42
  # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
37
43
  # status.timeout? # => true
38
- #
39
44
  # @return [Boolean]
40
45
  #
41
- # @api public
42
- #
43
46
  def timeout? = @timeout
47
+
48
+ # Overrides the default success? method to return nil if the process timed out
49
+ #
50
+ # This is because when a timeout occurs, Windows will still return true
51
+ # @example
52
+ # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
53
+ # status.success? # => nil
54
+ # @return [Boolean, nil]
55
+ #
56
+ def success?
57
+ return nil if timeout? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
58
+
59
+ super
60
+ end
61
+
62
+ # Return a string representation of the status
63
+ # @example
64
+ # status.to_s #=> "pid 70144 SIGKILL (signal 9) timed out after 10s"
65
+ # @return [String]
66
+ def to_s
67
+ "#{super}#{timeout? ? " timed out after #{timeout_duration}s" : ''}"
68
+ end
44
69
  end
45
70
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module ProcessExecuter
4
4
  # The current Gem version
5
- VERSION = '1.1.2'
5
+ VERSION = '1.3.0'
6
6
  end
@@ -2,27 +2,43 @@
2
2
 
3
3
  require 'process_executer/monitored_pipe'
4
4
  require 'process_executer/options'
5
+ require 'process_executer/command'
5
6
  require 'process_executer/status'
6
7
 
8
+ require 'logger'
7
9
  require 'timeout'
8
10
 
9
- # Execute a command in a subprocess and optionally capture its output
11
+ # The `ProcessExecuter` module provides methods to execute subprocess commands
12
+ # with enhanced features such as output capture, timeout handling, and custom
13
+ # environment variables.
14
+ #
15
+ # Methods:
16
+ # * {run}: Executes a command and captures its output and status in a result object.
17
+ # * {spawn}: Executes a command and returns its exit status.
18
+ #
19
+ # Features:
20
+ # * Supports executing commands via a shell or directly.
21
+ # * Captures stdout and stderr to buffers, files, or custom objects.
22
+ # * Optionally enforces timeouts and terminates long-running commands.
23
+ # * Provides detailed status information, including success, failure, or timeout states.
10
24
  #
11
25
  # @api public
12
26
  #
13
27
  module ProcessExecuter
14
- # Execute the specified command as a subprocess and return the exit status
28
+ # Execute the given command as a subprocess and return the exit status
15
29
  #
16
- # This is a convenience method that calls Process.spawn and blocks until the
17
- # command has terminated.
30
+ # This is a convenience method that calls
31
+ # [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn)
32
+ # and blocks until the command terminates.
18
33
  #
19
- # The command will be send the SIGKILL signal if it does not terminate within
34
+ # The command will be sent the SIGKILL signal if it does not terminate within
20
35
  # the specified timeout.
21
36
  #
22
37
  # @example
23
38
  # status = ProcessExecuter.spawn('echo hello')
24
39
  # status.exited? # => true
25
40
  # status.success? # => true
41
+ # status.timeout? # => false
26
42
  #
27
43
  # @example with a timeout
28
44
  # status = ProcessExecuter.spawn('sleep 10', timeout: 0.01)
@@ -30,6 +46,7 @@ module ProcessExecuter
30
46
  # status.success? # => nil
31
47
  # status.signaled? # => true
32
48
  # status.termsig # => 9
49
+ # status.timeout? # => true
33
50
  #
34
51
  # @example capturing stdout to a string
35
52
  # stdout = StringIO.new
@@ -42,10 +59,10 @@ module ProcessExecuter
42
59
  # @see ProcessExecuter::Options#initialize See ProcessExecuter::Options#initialize
43
60
  # for options that may be specified
44
61
  #
45
- # @param command [Array<String>] the command to execute
46
- # @param options_hash [Hash] the options to use when exectuting the command
62
+ # @param command [Array<String>] The command to execute
63
+ # @param options_hash [Hash] The options to use when executing the command
47
64
  #
48
- # @return [Process::Status] the exit status of the proceess
65
+ # @return [Process::Status] the exit status of the process
49
66
  #
50
67
  def self.spawn(*command, **options_hash)
51
68
  options = ProcessExecuter::Options.new(**options_hash)
@@ -53,23 +70,210 @@ module ProcessExecuter
53
70
  wait_for_process(pid, options)
54
71
  end
55
72
 
73
+ # Execute the given command as a subprocess, blocking until it finishes
74
+ #
75
+ # Returns a result object which includes the process's status and output.
76
+ #
77
+ # Supports the same features as
78
+ # [Process.spawn](https://docs.ruby-lang.org/en/3.3/Process.html#method-c-spawn).
79
+ # In addition, it:
80
+ #
81
+ # 1. Blocks until the command exits
82
+ # 2. Captures stdout and stderr to a buffer or file
83
+ # 3. Optionally kills the command if it exceeds a timeout
84
+ #
85
+ # This method takes two forms:
86
+ #
87
+ # 1. The command is executed via a shell when the command is given as a single
88
+ # string:
89
+ #
90
+ # `ProcessExecuter.run([env, ] command_line, options = {}) ->` {ProcessExecuter::Command::Result}
91
+ #
92
+ # 2. The command is executed directly (bypassing the shell) when the command and it
93
+ # arguments are given as an array of strings:
94
+ #
95
+ # `ProcessExecuter.run([env, ] exe_path, *args, options = {}) ->` {ProcessExecuter::Command::Result}
96
+ #
97
+ # Optional argument `env` is a hash that affects ENV for the new process; see
98
+ # [Execution
99
+ # Environment](https://docs.ruby-lang.org/en/3.3/Process.html#module-Process-label-Execution+Environment).
100
+ #
101
+ # Argument `options` is a hash of options for the new process. See the options listed below.
102
+ #
103
+ # @example Run a command given as a single string (uses shell)
104
+ # # The command must be properly shell escaped when passed as a single string.
105
+ # command = 'echo "stdout: `pwd`"" && echo "stderr: $HOME" 1>&2'
106
+ # result = ProcessExecuter.run(command)
107
+ # result.success? #=> true
108
+ # result.stdout.string #=> "stdout: /Users/james/projects/main-branch/process_executer\n"
109
+ # result.stderr.string #=> "stderr: /Users/james\n"
110
+ #
111
+ # @example Run a command given as an array of strings (does not use shell)
112
+ # # The command and its args must be provided as separate strings in the array.
113
+ # # Shell expansions and redirections are not supported.
114
+ # command = ['git', 'clone', 'https://github.com/main-branch/process_executer']
115
+ # result = ProcessExecuter.run(*command)
116
+ # result.success? #=> true
117
+ # result.stdout.string #=> ""
118
+ # result.stderr.string #=> "Cloning into 'process_executer'...\n"
119
+ #
120
+ # @example Run a command with a timeout
121
+ # command = ['sleep', '1']
122
+ # result = ProcessExecuter.run(*command, timeout: 0.01)
123
+ # #=> raises ProcessExecuter::Command::TimeoutError which contains the command result
124
+ #
125
+ # @example Run a command which fails
126
+ # command = ['exit 1']
127
+ # result = ProcessExecuter.run(*command)
128
+ # #=> raises ProcessExecuter::Command::FailedError which contains the command result
129
+ #
130
+ # @example Run a command which exits due to an unhandled signal
131
+ # command = ['kill -9 $$']
132
+ # result = ProcessExecuter.run(*command)
133
+ # #=> raises ProcessExecuter::Command::SignaledError which contains the command result
134
+ #
135
+ # @example Return a result instead of raising an error when `raise_errors` is `false`
136
+ # # By setting `raise_errors` to `false`, exceptions will not be raised even
137
+ # # if the command fails.
138
+ # command = ['echo "Some error" 1>&2 && exit 1']
139
+ # result = ProcessExecuter.run(*command, raise_errors: false)
140
+ # # An error is not raised
141
+ # result.success? #=> false
142
+ # result.exitstatus #=> 1
143
+ # result.stdout.string #=> ""
144
+ # result.stderr.string #=> "Some error\n"
145
+ #
146
+ # @example Set environment variables
147
+ # env = { 'FOO' => 'foo', 'BAR' => 'bar' }
148
+ # command = 'echo "$FOO$BAR"'
149
+ # result = ProcessExecuter.run(env, *command)
150
+ # result.stdout.string #=> "foobar\n"
151
+ #
152
+ # @example Set environment variables when using a command array
153
+ # env = { 'GIT_DIR' => '/path/to/.git' }
154
+ # command = ['git', 'status']
155
+ # result = ProcessExecuter.run(env, *command)
156
+ # result.stdout.string #=> "On branch main\nYour branch is ..."
157
+ #
158
+ # @example Unset environment variables
159
+ # env = { 'GIT_DIR' => nil } # setting to nil unsets the variable in the environment
160
+ # command = ['git', 'status']
161
+ # result = ProcessExecuter.run(env, *command)
162
+ # result.stdout.string #=> "On branch main\nYour branch is ..."
163
+ #
164
+ # @example Reset existing environment variables and add new ones
165
+ # env = { 'PATH' => '/bin' }
166
+ # result = ProcessExecuter.run(env, 'echo "Home: $HOME" && echo "Path: $PATH"', unsetenv_others: true)
167
+ # result.stdout.string #=> "Home: \n/Path: /bin\n"
168
+ #
169
+ # @example Run command in a different directory
170
+ # command = ['pwd']
171
+ # result = ProcessExecuter.run(*command, chdir: '/tmp')
172
+ # result.stdout.string #=> "/tmp\n"
173
+ #
174
+ # @example Capture stdout and stderr into a single buffer
175
+ # command = ['echo "stdout" && echo "stderr" 1>&2']
176
+ # result = ProcessExecuter.run(*command, merge: true)
177
+ # result.stdout.string #=> "stdout\nstderr\n"
178
+ # result.stdout.object_id == result.stderr.object_id #=> true
179
+ #
180
+ # @example Capture to an explicit buffer
181
+ # out = StringIO.new
182
+ # err = StringIO.new
183
+ # command = ['echo "stdout" && echo "stderr" 1>&2']
184
+ # result = ProcessExecuter.run(*command, out: out, err: err)
185
+ # out.string #=> "stdout\n"
186
+ # err.string #=> "stderr\n"
187
+ # result.stdout.object_id == out.object_id #=> true
188
+ # result.stderr.object_id == err.object_id #=> true
189
+ #
190
+ # @example Capture to a file
191
+ # # Same technique can be used for stderr
192
+ # out = File.open('stdout.txt', 'w')
193
+ # command = ['echo "stdout" && echo "stderr" 1>&2']
194
+ # result = ProcessExecuter.run(*command, out: out, err: err)
195
+ # out.close
196
+ # File.read('stdout.txt') #=> "stdout\n"
197
+ # # stderr is still captured to a StringIO buffer internally
198
+ # result.stderr.string #=> "stderr\n"
199
+ #
200
+ # @example Capture to multiple writers (e.g. files, buffers, STDOUT, etc.)
201
+ # # Same technique can be used for stderr
202
+ # out_buffer = StringIO.new
203
+ # out_file = File.open('stdout.txt', 'w')
204
+ # command = ['echo "stdout" && echo "stderr" 1>&2']
205
+ # result = ProcessExecuter.run(*command, out: [out_buffer, out_file])
206
+ # # You must manage closing resources you create yourself
207
+ # out_file.close
208
+ # out_buffer.string #=> "stdout\n"
209
+ # File.read('stdout.txt') #=> "stdout\n"
210
+ #
211
+ # @param command [Array<String>] The command to run
212
+ #
213
+ # If the first element of command is a Hash, it is added to the ENV of
214
+ # the new process. See [Execution Environment](https://ruby-doc.org/3.3.6/Process.html#module-Process-label-Execution+Environment)
215
+ # for more details. The env hash is then removed from the command array.
216
+ #
217
+ # If the first and only (remaining) command element is a string, it is passed to
218
+ # a subshell if it begins with a shell reserved word, contains special built-ins,
219
+ # or includes shell metacharacters.
220
+ #
221
+ # Care must be taken to properly escape shell metacharacters in the command string.
222
+ #
223
+ # Otherwise, the command is run bypassing the shell. When bypassing the shell, shell expansions
224
+ # and redirections are not supported.
225
+ #
226
+ # @param logger [Logger] The logger to use
227
+ # @param options_hash [Hash] Additional options
228
+ # @option options_hash [Numeric] :timeout The maximum seconds to wait for the command to complete
229
+ #
230
+ # If timeout is zero or nil, the command will not time out. If the command
231
+ # times out, it is killed via a SIGKILL signal and {ProcessExecuter::Command::TimeoutError} is raised.
232
+ #
233
+ # If the command does not exit when receiving the SIGKILL signal, this method may hang indefinitely.
234
+ #
235
+ # @option options_hash [#write] :out (nil) The object to write stdout to
236
+ # @option options_hash [#write] :err (nil) The object to write stderr to
237
+ # @option options_hash [Boolean] :merge (false) If true, stdout and stderr are written to the same capture buffer
238
+ # @option options_hash [Boolean] :raise_errors (true) Raise an exception if the command fails
239
+ # @option options_hash [Boolean] :unsetenv_others (false) If true, unset all environment variables before
240
+ # applying the new ones
241
+ # @option options_hash [true, Integer, nil] :pgroup (nil) true or 0: new process group; non-zero: join
242
+ # the group, nil: existing group
243
+ # @option options_hash [Boolean] :new_pgroup (nil) Create a new process group (Windows only)
244
+ # @option options_hash [Integer] :rlimit_resource_name (nil) Set resource limits (see Process.setrlimit)
245
+ # @option options_hash [Integer] :umask (nil) Set the umask (see File.umask)
246
+ # @option options_hash [Boolean] :close_others (false) If true, close non-standard file descriptors
247
+ # @option options_hash [String] :chdir (nil) The directory to run the command in
248
+ #
249
+ # @raise [ProcessExecuter::Command::FailedError] if the command returned a non-zero exit status
250
+ # @raise [ProcessExecuter::Command::SignaledError] if the command exited because of an unhandled signal
251
+ # @raise [ProcessExecuter::Command::TimeoutError] if the command timed out
252
+ # @raise [ProcessExecuter::Command::ProcessIOError] if an exception was raised while collecting subprocess output
253
+ #
254
+ # @return [ProcessExecuter::Command::Result] A result object containing the process status and captured output
255
+ #
256
+ def self.run(*command, logger: Logger.new(nil), **options_hash)
257
+ ProcessExecuter::Command::Runner.new(logger).call(*command, **options_hash)
258
+ end
259
+
56
260
  # Wait for process to terminate
57
261
  #
58
- # If a timeout is speecified in options, kill the process after options.timeout seconds.
262
+ # If a timeout is specified in options, terminate the process after options.timeout seconds.
59
263
  #
60
- # @param pid [Integer] the process id
264
+ # @param pid [Integer] the process ID
61
265
  # @param options [ProcessExecuter::Options] the options used
62
266
  #
63
- # @return [ProcessExecuter::Status] the status of the process
267
+ # @return [ProcessExecuter::Status] the process status including Process::Status attributes and a timeout flag
64
268
  #
65
269
  # @api private
66
270
  #
67
271
  private_class_method def self.wait_for_process(pid, options)
68
272
  Timeout.timeout(options.timeout) do
69
- ProcessExecuter::Status.new(Process.wait2(pid).last, false)
273
+ ProcessExecuter::Status.new(Process.wait2(pid).last, false, options.timeout)
70
274
  end
71
275
  rescue Timeout::Error
72
276
  Process.kill('KILL', pid)
73
- ProcessExecuter::Status.new(Process.wait2(pid).last, true)
277
+ ProcessExecuter::Status.new(Process.wait2(pid).last, true, options.timeout)
74
278
  end
75
279
  end
data/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "devDependencies": {
3
+ "@commitlint/cli": "^19.5.0",
4
+ "@commitlint/config-conventional": "^19.5.0",
5
+ "husky": "^9.1.0"
6
+ },
7
+ "scripts": {
8
+ "postinstall": "husky",
9
+ "prepare": "husky"
10
+ }
11
+ }
@@ -10,16 +10,17 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = 'An API for executing commands in a subprocess'
12
12
  spec.description = 'An API for executing commands in a subprocess'
13
- spec.homepage = 'https://github.com/main-branch/process_executer'
14
13
  spec.license = 'MIT'
15
- spec.required_ruby_version = '>= 3.0.0'
14
+ spec.required_ruby_version = '>= 3.1.0'
16
15
 
17
16
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
17
 
18
+ # Project links
19
+ spec.homepage = "https://github.com/main-branch/#{spec.name}"
19
20
  spec.metadata['homepage_uri'] = spec.homepage
20
- spec.metadata['source_code_uri'] = 'https://github.com/main-branch/process_executer'
21
- spec.metadata['changelog_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md"
21
+ spec.metadata['source_code_uri'] = spec.homepage
22
22
  spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}"
23
+ spec.metadata['changelog_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md"
23
24
 
24
25
  # Specify which files should be added to the gem when it is released.
25
26
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -31,15 +32,21 @@ Gem::Specification.new do |spec|
31
32
  spec.bindir = 'exe'
32
33
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
34
  spec.require_paths = ['lib']
35
+ spec.requirements = [
36
+ 'Platform: Mac, Linux, or Windows',
37
+ 'Ruby: MRI 3.1 or later, TruffleRuby 24 or later, or JRuby 9.4 or later'
38
+ ]
34
39
 
35
40
  spec.add_development_dependency 'bundler-audit', '~> 0.9'
36
- spec.add_development_dependency 'create_github_release', '~> 1.1'
37
- spec.add_development_dependency 'rake', '~> 13.1'
38
- spec.add_development_dependency 'rspec', '~> 3.12'
39
- spec.add_development_dependency 'rubocop', '~> 1.59'
41
+ spec.add_development_dependency 'create_github_release', '~> 2.1'
42
+ spec.add_development_dependency 'main_branch_shared_rubocop_config', '~> 0.1'
43
+ spec.add_development_dependency 'rake', '~> 13.2'
44
+ spec.add_development_dependency 'rspec', '~> 3.13'
45
+ spec.add_development_dependency 'rubocop', '~> 1.66'
40
46
  spec.add_development_dependency 'semverify', '~> 0.3'
41
47
  spec.add_development_dependency 'simplecov', '~> 0.22'
42
48
  spec.add_development_dependency 'simplecov-lcov', '~> 0.8'
49
+ spec.add_development_dependency 'simplecov-rspec', '~> 0.3'
43
50
 
44
51
  unless RUBY_PLATFORM == 'java'
45
52
  spec.add_development_dependency 'redcarpet', '~> 3.6'