outstand-tty-command 0.10.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'result'
4
+
5
+ module TTY
6
+ class Command
7
+ class DryRunner
8
+ attr_reader :cmd
9
+
10
+ def initialize(cmd, printer)
11
+ @cmd = cmd
12
+ @printer = printer
13
+ end
14
+
15
+ # Show command without running
16
+ #
17
+ # @api public
18
+ def run!(*)
19
+ cmd.to_command
20
+ message = "#{@printer.decorate('(dry run)', :blue)} " +
21
+ @printer.decorate(cmd.to_command, :yellow, :bold)
22
+ @printer.write(cmd, message, cmd.uuid)
23
+ Result.new(0, '', '')
24
+ end
25
+ end # DryRunner
26
+ end # Command
27
+ end # TTY
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Command
5
+ # An ExitError reports an unsuccessful exit by command.
6
+ #
7
+ # The error message includes:
8
+ # * the name of command executed
9
+ # * the exit status
10
+ # * stdout bytes
11
+ # * stderr bytes
12
+ #
13
+ # @api private
14
+ class ExitError < RuntimeError
15
+ def initialize(cmd_name, result)
16
+ super(info(cmd_name, result))
17
+ end
18
+
19
+ def info(cmd_name, result)
20
+ "Running `#{cmd_name}` failed with\n" \
21
+ " exit status: #{result.exit_status}\n" \
22
+ " stdout: #{extract_output(result.out)}\n" \
23
+ " stderr: #{extract_output(result.err)}\n"
24
+ end
25
+
26
+ def extract_output(value)
27
+ (value || '').strip.empty? ? 'Nothing written' : value.strip
28
+ end
29
+ end # ExitError
30
+ end # Command
31
+ end # TTY
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+
3
+ require 'pastel'
4
+
5
+ module TTY
6
+ class Command
7
+ module Printers
8
+ class Abstract
9
+ extend Forwardable
10
+
11
+ def_delegators :@color, :decorate
12
+
13
+ attr_reader :output, :options
14
+ attr_accessor :out_data, :err_data
15
+
16
+ # Initialize a Printer object
17
+ #
18
+ # @param [IO] output
19
+ # the printer output
20
+ #
21
+ # @api public
22
+ def initialize(output, options = {})
23
+ @output = output
24
+ @options = options
25
+ @enabled = options.fetch(:color) { true }
26
+ @color = ::Pastel.new(enabled: @enabled)
27
+
28
+ @out_data = ''
29
+ @err_data = ''
30
+ end
31
+
32
+ def print_command_start(cmd, *args)
33
+ write(cmd.to_command + "#{args.join}")
34
+ end
35
+
36
+ def print_command_out_data(cmd, *args)
37
+ write(args.join(' '))
38
+ end
39
+
40
+ def print_command_err_data(cmd, *args)
41
+ write(args.join(' '))
42
+ end
43
+
44
+ def print_command_exit(cmd, *args)
45
+ write(args.join(' '))
46
+ end
47
+
48
+ def write(cmd, message)
49
+ raise NotImplemented, "Abstract printer cannot be used"
50
+ end
51
+ end # Abstract
52
+ end # Printers
53
+ end # Command
54
+ end # TTY
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'abstract'
5
+
6
+ module TTY
7
+ class Command
8
+ module Printers
9
+ class Null < Abstract
10
+ def write(*)
11
+ # Do nothing
12
+ end
13
+ end # Null
14
+ end # Printers
15
+ end # Command
16
+ end # TTY
@@ -0,0 +1,83 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'pastel'
5
+
6
+ require_relative 'abstract'
7
+
8
+ module TTY
9
+ class Command
10
+ module Printers
11
+ class Pretty < Abstract
12
+ TIME_FORMAT = "%5.3f %s".freeze
13
+
14
+ def initialize(*)
15
+ super
16
+ @uuid = options.fetch(:uuid) { true }
17
+ end
18
+
19
+ def print_command_start(cmd, *args)
20
+ message = ["Running #{decorate(cmd.to_command, :yellow, :bold)}"]
21
+ message << args.map(&:chomp).join(' ') unless args.empty?
22
+ write(cmd, message.join)
23
+ end
24
+
25
+ def print_command_out_data(cmd, *args)
26
+ message = args.map(&:chomp).join(' ')
27
+ write(cmd, "\t#{message}", out_data)
28
+ end
29
+
30
+ def print_command_err_data(cmd, *args)
31
+ message = args.map(&:chomp).join(' ')
32
+ write(cmd, "\t" + decorate(message, :red), err_data)
33
+ end
34
+
35
+ def print_command_exit(cmd, status, runtime, *args)
36
+ if cmd.only_output_on_error && !status.zero?
37
+ output << out_data
38
+ output << err_data
39
+ end
40
+
41
+ runtime = TIME_FORMAT % [runtime, pluralize(runtime, 'second')]
42
+ message = ["Finished in #{runtime}"]
43
+ message << " with exit status #{status}" if status
44
+ message << " (#{success_or_failure(status)})"
45
+ write(cmd, message.join)
46
+ end
47
+
48
+ # Write message out to output
49
+ #
50
+ # @api private
51
+ def write(cmd, message, data = nil)
52
+ cmd_set_uuid = cmd.options.fetch(:uuid, true)
53
+ uuid_needed = cmd.options[:uuid].nil? ? @uuid : cmd_set_uuid
54
+ out = []
55
+ if uuid_needed
56
+ out << "[#{decorate(cmd.uuid, :green)}] " unless cmd.uuid.nil?
57
+ end
58
+ out << "#{message}\n"
59
+ target = (cmd.only_output_on_error && !data.nil?) ? data : output
60
+ target << out.join
61
+ end
62
+
63
+ private
64
+
65
+ # Pluralize word based on a count
66
+ #
67
+ # @api private
68
+ def pluralize(count, word)
69
+ "#{word}#{'s' unless count.to_f == 1}"
70
+ end
71
+
72
+ # @api private
73
+ def success_or_failure(status)
74
+ if status == 0
75
+ decorate('successful', :green, :bold)
76
+ else
77
+ decorate('failed', :red, :bold)
78
+ end
79
+ end
80
+ end # Pretty
81
+ end # Printers
82
+ end # Command
83
+ end # TTY
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'pastel'
5
+ require_relative 'abstract'
6
+
7
+ module TTY
8
+ class Command
9
+ module Printers
10
+ class Progress < Abstract
11
+
12
+ def print_command_exit(cmd, status, runtime, *args)
13
+ output.print(success_or_failure(status))
14
+ end
15
+
16
+ def write(*)
17
+ end
18
+
19
+ private
20
+
21
+ # @api private
22
+ def success_or_failure(status)
23
+ if status == 0
24
+ decorate('.', :green)
25
+ else
26
+ decorate('F', :red)
27
+ end
28
+ end
29
+ end # Progress
30
+ end # Printers
31
+ end # Command
32
+ end # TTY
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'abstract'
4
+
5
+ module TTY
6
+ class Command
7
+ module Printers
8
+ class Quiet < Abstract
9
+ attr_reader :output, :options
10
+
11
+ def print_command_start(cmd)
12
+ # quiet
13
+ end
14
+
15
+ def print_command_out_data(cmd, *args)
16
+ write(cmd, args.join(' '), out_data)
17
+ end
18
+
19
+ def print_command_err_data(cmd, *args)
20
+ write(cmd, args.join(' '), err_data)
21
+ end
22
+
23
+ def print_command_exit(cmd, status, *args)
24
+ unless !cmd.only_output_on_error || status.zero?
25
+ output << out_data
26
+ output << err_data
27
+ end
28
+
29
+ # quiet
30
+ end
31
+
32
+ def write(cmd, message, data = nil)
33
+ target = (cmd.only_output_on_error && !data.nil?) ? data : output
34
+ target << message
35
+ end
36
+ end # Progress
37
+ end # Printers
38
+ end # Command
39
+ end # TTY
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thread'
4
+
5
+ require_relative 'child_process'
6
+ require_relative 'result'
7
+ require_relative 'truncator'
8
+
9
+ module TTY
10
+ class Command
11
+ class ProcessRunner
12
+ # the command to be spawned
13
+ attr_reader :cmd
14
+
15
+ # Initialize a Runner object
16
+ #
17
+ # @param [Printer] printer
18
+ # the printer to use for logging
19
+ #
20
+ # @api private
21
+ def initialize(cmd, printer, &block)
22
+ @cmd = cmd
23
+ @timeout = cmd.options[:timeout]
24
+ @input = cmd.options[:input]
25
+ @signal = cmd.options[:signal] || "SIGKILL"
26
+ @binmode = cmd.options[:binmode]
27
+ @printer = printer
28
+ @block = block
29
+ end
30
+
31
+ # Execute child process
32
+ #
33
+ # Write the input if provided to the child's stdin and read
34
+ # the contents of both the stdout and stderr.
35
+ #
36
+ # If a block is provided then yield the stdout and stderr content
37
+ # as its being read.
38
+ #
39
+ # @api public
40
+ def run!
41
+ @printer.print_command_start(cmd)
42
+ start = Time.now
43
+
44
+ pid, stdin, stdout, stderr = ChildProcess.spawn(cmd)
45
+
46
+ write_stream(stdin, @input)
47
+
48
+ stdout_data, stderr_data = read_streams(stdout, stderr)
49
+
50
+ status = waitpid(pid)
51
+ runtime = Time.now - start
52
+
53
+ @printer.print_command_exit(cmd, status, runtime)
54
+
55
+ Result.new(status, stdout_data, stderr_data, runtime)
56
+ ensure
57
+ [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? }
58
+ if pid # Ensure no zombie processes
59
+ ::Process.detach(pid)
60
+ terminate(pid)
61
+ end
62
+ end
63
+
64
+ # Stop a process marked by pid
65
+ #
66
+ # @param [Integer] pid
67
+ #
68
+ # @api public
69
+ def terminate(pid)
70
+ ::Process.kill(@signal, pid) rescue nil
71
+ end
72
+
73
+ private
74
+
75
+ # The buffer size for reading stdout and stderr
76
+ BUFSIZE = 16 * 1024
77
+
78
+ # @api private
79
+ def handle_timeout(runtime)
80
+ return unless @timeout
81
+
82
+ t = @timeout - runtime
83
+ raise TimeoutExceeded if t < 0.0
84
+ end
85
+
86
+ # Write the input to the process stdin
87
+ #
88
+ # @api private
89
+ def write_stream(stream, input)
90
+ start = Time.now
91
+ writers = [input && stream].compact
92
+
93
+ while writers.any?
94
+ ready = IO.select(nil, writers, writers, @timeout)
95
+ raise TimeoutExceeded if ready.nil?
96
+
97
+ ready[1].each do |writer|
98
+ begin
99
+ err = nil
100
+ size = writer.write(@input)
101
+ input = input.byteslice(size..-1)
102
+ rescue IO::WaitWritable
103
+ rescue Errno::EPIPE => err
104
+ # The pipe closed before all input written
105
+ # Probably process exited prematurely
106
+ writer.close
107
+ writers.delete(writer)
108
+ end
109
+ if err || input.bytesize == 0
110
+ writer.close
111
+ writers.delete(writer)
112
+ end
113
+
114
+ # control total time spent writing
115
+ runtime = Time.now - start
116
+ handle_timeout(runtime)
117
+ end
118
+ end
119
+ end
120
+
121
+ # Read stdout & stderr streams in the background
122
+ #
123
+ # @param [IO] stdout
124
+ # @param [IO] stderr
125
+ #
126
+ # @api private
127
+ def read_streams(stdout, stderr)
128
+ stdout_data = []
129
+ stderr_data = Truncator.new
130
+
131
+ out_buffer = ->(line) {
132
+ stdout_data << line
133
+ @printer.print_command_out_data(cmd, line)
134
+ @block.(line, nil) if @block
135
+ }
136
+
137
+ err_buffer = ->(line) {
138
+ stderr_data << line
139
+ @printer.print_command_err_data(cmd, line)
140
+ @block.(nil, line) if @block
141
+ }
142
+
143
+ stdout_thread = read_stream(stdout, out_buffer)
144
+ stderr_thread = read_stream(stderr, err_buffer)
145
+
146
+ stdout_thread.join
147
+ stderr_thread.join
148
+
149
+ encoding = @binmode ? Encoding::BINARY : Encoding::UTF_8
150
+
151
+ [
152
+ stdout_data.join.force_encoding(encoding),
153
+ stderr_data.read.dup.force_encoding(encoding)
154
+ ]
155
+ end
156
+
157
+ def read_stream(stream, buffer)
158
+ Thread.new do
159
+ if Thread.current.respond_to?(:report_on_exception)
160
+ Thread.current.report_on_exception = false
161
+ end
162
+ Thread.current[:cmd_start] = Time.now
163
+ readers = [stream]
164
+
165
+ while readers.any?
166
+ ready = IO.select(readers, nil, readers, @timeout)
167
+ raise TimeoutExceeded if ready.nil?
168
+
169
+ ready[0].each do |reader|
170
+ begin
171
+ line = reader.readpartial(BUFSIZE)
172
+ buffer.(line)
173
+
174
+ # control total time spent reading
175
+ runtime = Time.now - Thread.current[:cmd_start]
176
+ handle_timeout(runtime)
177
+ rescue Errno::EAGAIN, Errno::EINTR
178
+ rescue EOFError, Errno::EPIPE, Errno::EIO # thrown by PTY
179
+ readers.delete(reader)
180
+ reader.close
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # @api private
188
+ def waitpid(pid)
189
+ _pid, status = ::Process.waitpid2(pid, ::Process::WUNTRACED)
190
+ status.exitstatus || status.termsig if _pid
191
+ rescue Errno::ECHILD
192
+ # In JRuby, waiting on a finished pid raises.
193
+ end
194
+ end # ProcessRunner
195
+ end # Command
196
+ end # TTY