File: run_with_capture.rb

package info (click to toggle)
ruby-process-executer 4.0.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 408 kB
  • sloc: ruby: 873; makefile: 4
file content (148 lines) | stat: -rw-r--r-- 5,256 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# frozen_string_literal: true

require_relative '../errors'

module ProcessExecuter
  module Commands
    # Runs a subprocess, blocks until it completes, and returns the result
    #
    # Extends {ProcessExecuter::Commands::Run} to provide the core functionality for
    # {ProcessExecuter.run_with_capture}.
    #
    # It accepts all [Process.spawn execution
    # options](https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options)
    # plus the additional options `timeout_after`, `raise_errors`, `logger`, and
    # `merge_output`.
    #
    # Like {Run}, any stdout or stderr redirection destinations are wrapped in a
    # {MonitoredPipe}.
    #
    # @api private
    #
    class RunWithCapture < Run
      # Run a command and return the result which includes the captured output
      #
      # @example
      #   options = ProcessExecuter::Options::RunWithCaptureOptions.new(merge_output: false)
      #   result = ProcessExecuter::Commands::RunWithCapture.new('echo hello', options).call
      #   result.success? # => true
      #   result.exitstatus # => 0
      #   result.stdout # => "hello\n"
      #
      # @raise [ProcessExecuter::SpawnError] `Process.spawn` raised an error before the
      #   command was run
      #
      # @raise [ProcessExecuter::FailedError] If the command ran and failed
      #
      # @raise [ProcessExecuter::SignaledError] If the command ran and terminated due to
      #   an unhandled signal
      #
      # @raise [ProcessExecuter::TimeoutError] If the command timed out
      #
      # @raise [ProcessExecuter::ProcessIOError] If there was an exception while
      #   collecting subprocess output
      #
      # @return [ProcessExecuter::ResultWithCapture] The result of the completed subprocess
      #
      def call
        @stdout_buffer = StringIO.new
        stdout_buffer.set_encoding(options.effective_stdout_encoding)
        @stderr_buffer = StringIO.new
        stderr_buffer.set_encoding(options.effective_stderr_encoding)

        update_capture_options

        begin
          super
        ensure
          log_command_output
        end
      end

      # The buffer used to capture stdout
      #
      # @example
      #   run.stdout_buffer #=> StringIO
      #
      # @return [StringIO]
      #
      attr_reader :stdout_buffer

      # The buffer used to capture stderr
      #
      # @example
      #   run.stderr_buffer #=> StringIO
      #
      # @return [StringIO]
      #
      attr_reader :stderr_buffer

      private

      # Create a result object that includes the captured stdout and stderr
      #
      # @return [ProcessExecuter::ResultWithCapture] The result of the command with captured output
      #
      def create_result
        ProcessExecuter::ResultWithCapture.new(
          super, stdout_buffer:, stderr_buffer:
        )
      end

      # Updates {options} to include the stdout and stderr capture options
      #
      # @return [Void]
      #
      def update_capture_options
        out = stdout_buffer
        err = options.merge_output ? [:child, 1] : stderr_buffer

        options.merge!(
          capture_option(:out, stdout_redirection_source, stdout_redirection_destination, out),
          capture_option(:err, stderr_redirection_source, stderr_redirection_destination, err)
        )
      end

      # The source for stdout redirection
      # @return [Object]
      def stdout_redirection_source = options.stdout_redirection_source

      # The source for stderr redirection
      # @return [Object]
      def stderr_redirection_source = options.stderr_redirection_source

      # The destination for stdout redirection
      # @return [Object]
      def stdout_redirection_destination = options.stdout_redirection_destination

      # The destination for stderr redirection
      # @return [Object]
      def stderr_redirection_destination = options.stderr_redirection_destination

      # Add the capture redirection to existing options (if any)
      # @param redirection_source [Symbol, Integer] The source of the redirection (e.g., :out, :err)
      # @param given_source [Symbol, Integer, nil] The source provided by the user (if any)
      # @param given_destination [Object, nil] The destination provided by the user (if any)
      # @param capture_destination [Object] The additional destination to capture output to
      # @return [Hash] The option (including the capture_destination) to merge into options
      def capture_option(redirection_source, given_source, given_destination, capture_destination)
        if given_source
          if Destinations::Tee.handles?(given_destination)
            { given_source => given_destination + [capture_destination] }
          else
            { given_source => [:tee, given_destination, capture_destination] }
          end
        else
          { redirection_source => capture_destination }
        end
      end

      # Log the captured command output to the given logger at debug level
      # @return [Void]
      def log_command_output
        options.logger&.debug { "PID #{pid}: stdout: #{stdout_buffer.string.inspect}" }
        options.logger&.debug { "PID #{pid}: stderr: #{stderr_buffer.string.inspect}" }
      end
    end
  end
end