tty-command 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 234c1a25407b1be27c06441a1d1d9ec6cce50593
4
- data.tar.gz: bd9ff0177f553972aede0ce9ef718b4731fb587d
3
+ metadata.gz: b9086c13127af2f7d61b0577426a34ea6e71929c
4
+ data.tar.gz: fbb089e7cc7d6937571ab84c3700856bb6f7eac7
5
5
  SHA512:
6
- metadata.gz: bddbfb2794b1c672147162e28e3b380bec7e3c1b511b46dee46c688d994b7735cebba6db865dddb89dd6dfffec3de8ffa672f1e15bdcde01292805f240072926
7
- data.tar.gz: aa19370eb59f7127bb8ffa3bf64e4a97f6be2c9abc88143b3a1818cd0100baa0457b5031d63186e05d0a571a41283d4c7964378d38f13cc155c6d1c25d5ed499
6
+ metadata.gz: 3eeb7cc3fc245a9bc126ee2948c9d02ebae2e88df4f83a4c3ead72e4ee8b1597ac087b039448f843ab4f305575957dbde65f86ff7813101678a801aabc3f37a3
7
+ data.tar.gz: 028e2be35a74088e0015bb60577a4cbdf462b31e727221aa7e3d72a7c2530c68f47bdff8ed1593e55d1af249f1e006d6078d15d55dbbc59d839fc35a5fac4a6b
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --require spec_helper
2
2
  --color
3
+ --format doc
3
4
  --warnings
@@ -8,7 +8,7 @@ rvm:
8
8
  - 2.1.10
9
9
  - 2.2.6
10
10
  - 2.3.3
11
- - 2.4.0
11
+ - 2.4.1
12
12
  - ruby-head
13
13
  - jruby-9.1.1.0
14
14
  - jruby-head
@@ -1,5 +1,22 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.5.0] - 2017-07-16
4
+
5
+ ### Added
6
+ * Add :signal option for timeout
7
+ * Add :input option for handling stdin input
8
+ * Add ability for Command#run to specify a callback that is invoked whenever stdout or stderr receive output
9
+ * Add Command#wait for polling a long running script for matching output
10
+
11
+ ### Changed
12
+ * Change ProcessRunner to immediately sync write pipe
13
+ * Change ProcessRunner to write to stdin stream when writable
14
+
15
+ ### Fixed
16
+ * Fix quiet printer write call by @jamesepatrick
17
+ * Fix to correctly close all pipe ends between parent and child process
18
+ * Fix timeout behaviour for writable and readable streams
19
+
3
20
  ## [v0.4.0] - 2017-02-22
4
21
 
5
22
  ### Changed
data/README.md CHANGED
@@ -46,20 +46,24 @@ Or install it yourself as:
46
46
  * [2. Interface](#2-interface)
47
47
  * [2.1. Run](#21-run)
48
48
  * [2.2. Run!](#22-run)
49
- * [2.3. Test](#23-test)
50
- * [2.4. Logging](#24-logging)
51
- * [2.5. Dry run](#25-dry-run)
52
- * [2.6. Ruby interpreter](#26-ruby-interpreter)
49
+ * [2.3. Logging](#23-logging)
50
+ * [2.3.1. Color](#231-color)
51
+ * [2.3.2. UUID](#232-uuid)
52
+ * [2.4. Dry run](#24-dry-run)
53
+ * [2.5. Wait](#25-wait)
54
+ * [2.6. Test](#26-test)
55
+ * [2.7. Ruby interpreter](#27-ruby-interpreter)
53
56
  * [3. Advanced Interface](#3-advanced-interface)
54
57
  * [3.1. Environment variables](#31-environment-variables)
55
58
  * [3.2. Options](#32-options)
56
59
  * [3.2.1. Current directory](#321-current-directory)
57
60
  * [3.2.2. Redirection](#322-redirection)
58
- * [3.2.3. Handling input](#323-handling-input)
59
- * [3.2.4. Timeout](#324-timeout)
60
- * [3.2.5. User](#324-user)
61
- * [3.2.6. Group](#325-group)
62
- * [3.2.7. Umask](#326-umask)
61
+ * [3.2.3. Handling input](#323-handling-input)
62
+ * [3.2.4. Timeout](#324-timeout)
63
+ * [3.2.5. Signal](#325-signal)
64
+ * [3.2.6. User](#326-user)
65
+ * [3.2.7. Group](#327-group)
66
+ * [3.2.8. Umask](#328-umask)
63
67
  * [3.3. Result](#33-result)
64
68
  * [3.3.1. success?](#331-success)
65
69
  * [3.3.2. failure?](#332-failure)
@@ -121,6 +125,15 @@ puts "The date is #{out}"
121
125
  # => "The date is Tue 10 May 2016 22:30:15 BST\n"
122
126
  ```
123
127
 
128
+ You can also pass a block that gets invoked anytime stdout and/or stderr receive output:
129
+
130
+ ```ruby
131
+ cmd.run('long running script') do |out, err|
132
+ output << out if out
133
+ errors << err if err
134
+ end
135
+ ```
136
+
124
137
  If the command fails (with a non-zero exit code), a `TTY::Command::ExitError` is raised. The `ExitError` message will include:
125
138
 
126
139
  * the name of command executed
@@ -140,20 +153,7 @@ if cmd.run!('which xyzzy').failure?
140
153
  end
141
154
  ```
142
155
 
143
- ### 2.3 Test
144
-
145
- To simulate classic bash test command you case use `test` method with expression to check as a first argument:
146
-
147
- ```ruby
148
- if cmd.test '-e /etc/passwd'
149
- puts "Sweet..."
150
- else
151
- puts "Ohh no! Where is it?"
152
- exit 1
153
- end
154
- ```
155
-
156
- ### 2.4 Logging
156
+ ### 2.3 Logging
157
157
 
158
158
  By default, when a command is run, the command and the output are printed to `stdout` using the `:pretty` printer. If you wish to change printer you can do so by passing a `:printer` option:
159
159
 
@@ -172,7 +172,7 @@ By default the printers log to `stdout` but this can be changed by passing an ob
172
172
 
173
173
  ```ruby
174
174
  logger = Logger.new('dev.log')
175
- cmd = TTY::Command.new(output: output)
175
+ cmd = TTY::Command.new(output: logger)
176
176
  ```
177
177
 
178
178
  You can force the printer to always in print in color by passing the `:color` option:
@@ -181,7 +181,21 @@ You can force the printer to always in print in color by passing the `:color` op
181
181
  cmd = TTY::Command.new(color: true)
182
182
  ```
183
183
 
184
- ### 2.5 Dry run
184
+ #### 2.3.1 Color
185
+
186
+ When using printers you can switch off coloring by using `color` option set to `false`.
187
+
188
+ #### 2.3.2 Uuid
189
+
190
+ By default when logging is enabled each log entry is prefixed by specific command run uuid number. This number can be switched off using `uuid` option:
191
+
192
+ ```ruby
193
+ cmd = TTY::Command.new uuid: false
194
+ cmd.run('rm -R all_my_files')
195
+ # => rm -r all_my_files
196
+ ```
197
+
198
+ ### 2.4 Dry run
185
199
 
186
200
  Sometimes it can be useful to put your script into a "dry run" mode that prints commands without actually running them. To simulate execution of the command use the `:dry_run` option:
187
201
 
@@ -197,7 +211,28 @@ To check what mode the command is in use the `dry_run?` query helper:
197
211
  cmd.dry_run? # => true
198
212
  ```
199
213
 
200
- ### 2.6 Ruby interpreter
214
+ ### 2.5 Wait
215
+
216
+ If you need to wait for a long running script and stop it when a given pattern has been matched use `wait` like so:
217
+
218
+ ```ruby
219
+ cmd.wait 'tail -f /var/log/production.log', /something happened/
220
+ ```
221
+
222
+ ### 2.6 Test
223
+
224
+ To simulate classic bash test command you case use `test` method with expression to check as a first argument:
225
+
226
+ ```ruby
227
+ if cmd.test '-e /etc/passwd'
228
+ puts "Sweet..."
229
+ else
230
+ puts "Ohh no! Where is it?"
231
+ exit 1
232
+ end
233
+ ```
234
+
235
+ ### 2.7 Ruby interpreter
201
236
 
202
237
  In order to run a command with Ruby interpreter do:
203
238
 
@@ -274,7 +309,21 @@ cmd.run(:ls, '-la', 2 => 1)
274
309
 
275
310
  #### 3.2.3 Handling Input
276
311
 
277
- You can pass input via the :in option, by passing a StringIO Object. This object might have more than one line, if the executed command reads more than once from STDIN.
312
+ You can provide input to stdin stream using the `:input` key. For instance, given the following executable called `cli` that expects name from `stdin`:
313
+
314
+ ```ruby
315
+ name = $stdin.gets
316
+ puts "Your name: #{name}"
317
+ ```
318
+
319
+ In order to execute `cli` with name input do:
320
+
321
+ ```ruby
322
+ cmd.run('cli', input: "Piotr\n")
323
+ # => Your name: Piotr
324
+ ```
325
+
326
+ Alternatively, you can pass input via the :in option, by passing a `StringIO` Object. This object might have more than one line, if the executed command reads more than once from STDIN.
278
327
 
279
328
  Assume you have run a program, that first asks for your email address and then for a password:
280
329
 
@@ -284,7 +333,7 @@ in_stream.puts "username@example.com"
284
333
  in_stream.puts "password"
285
334
  in_stream.rewind
286
335
 
287
- TTY::Command.new.run("my_cli_program", "login", in: in_stream).out
336
+ cmd.run("my_cli_program", "login", in: in_stream).out
288
337
  ```
289
338
 
290
339
  #### 3.2.4 Timeout
@@ -297,7 +346,15 @@ cmd.run("while test 1; sleep 1; done", timeout: 5)
297
346
 
298
347
  Please run `examples/timeout.rb` to see timeout in action.
299
348
 
300
- #### 3.2.5 User
349
+ #### 3.2.5 Signal
350
+
351
+ You can specify process termination signal other than the defaut `SIGTERM`:
352
+
353
+ ```ruby
354
+ cmd.run("whilte test1; sleep1; done", timeout: 5, signal: :KILL)
355
+ ```
356
+
357
+ #### 3.2.6 User
301
358
 
302
359
  To run command as a given user do:
303
360
 
@@ -305,7 +362,7 @@ To run command as a given user do:
305
362
  cmd.run(:echo, 'hello', user: 'piotr')
306
363
  ```
307
364
 
308
- #### 3.2.6 Group
365
+ #### 3.2.7 Group
309
366
 
310
367
  To run command as part of group do:
311
368
 
@@ -313,7 +370,7 @@ To run command as part of group do:
313
370
  cmd.run(:echo, 'hello', group: 'devs')
314
371
  ```
315
372
 
316
- #### 3.2.7 Umask
373
+ #### 3.2.8 Umask
317
374
 
318
375
  To run command with umask do:
319
376
 
@@ -4,6 +4,6 @@ require 'tty-command'
4
4
 
5
5
  cmd = TTY::Command.new
6
6
 
7
- out, err = cmd.execute(:echo, 'hello world!')
7
+ out, err = cmd.run(:echo, 'hello world!')
8
8
 
9
9
  puts "Result: #{out}"
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ name = $stdin.gets
4
+ puts "Your name: #{name}"
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+ cmd.run("i=0; while true; do i=$[$i+1]; echo 'hello '$i; sleep 1; done") do |out, err|
7
+ if out =~ /.*5.*/
8
+ raise ArgumentError, 'BOOM'
9
+ end
10
+ end
@@ -1,7 +1,9 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'tty-command'
4
+ require 'pathname'
4
5
 
6
+ cli = Pathname.new('examples/cli.rb')
5
7
  cmd = TTY::Command.new
6
8
 
7
9
  stdin = StringIO.new
@@ -9,6 +11,6 @@ stdin.puts "hello"
9
11
  stdin.puts "world"
10
12
  stdin.rewind
11
13
 
12
- out, _ = cmd.run(:cat, :in => stdin)
14
+ out, _ = cmd.run(cli, :in => stdin)
13
15
 
14
16
  puts "#{out}"
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+ require 'pathname'
5
+
6
+ cmd = TTY::Command.new
7
+ cli = Pathname.new('examples/cli')
8
+ out, _ = cmd.run(cli, input: "Piotr\n")
9
+
10
+ puts "#{out}"
@@ -4,4 +4,4 @@ require 'tty-command'
4
4
 
5
5
  cmd = TTY::Command.new
6
6
 
7
- cmd.run("while test 1; do echo 'hello'; sleep 1; done", timeout: 5)
7
+ cmd.run("while test 1; do echo 'hello'; sleep 1; done", timeout: 5, signal: :KILL)
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+ require 'logger'
5
+
6
+ logger = Logger.new('dev.log')
7
+ cmd = TTY::Command.new
8
+
9
+ Thread.new do
10
+ 10.times do |i|
11
+ sleep 1
12
+ if i == 5
13
+ logger << "error\n"
14
+ else
15
+ logger << "hello #{i}\n"
16
+ end
17
+ end
18
+ end
19
+
20
+
21
+ cmd.wait('tail -f dev.log', /error/)
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'rbconfig'
4
- require 'tty/command/version'
5
4
 
6
5
  require_relative 'command/cmd'
7
6
  require_relative 'command/exit_error'
@@ -11,6 +10,7 @@ require_relative 'command/printers/null'
11
10
  require_relative 'command/printers/pretty'
12
11
  require_relative 'command/printers/progress'
13
12
  require_relative 'command/printers/quiet'
13
+ require_relative 'command/version'
14
14
 
15
15
  module TTY
16
16
  class Command
@@ -21,7 +21,8 @@ module TTY
21
21
  # Path to the current Ruby
22
22
  RUBY = ENV['RUBY'] || ::File.join(
23
23
  RbConfig::CONFIG['bindir'],
24
- RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT'])
24
+ RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']
25
+ )
25
26
 
26
27
  def self.record_separator
27
28
  @record_separator ||= $/
@@ -71,23 +72,25 @@ module TTY
71
72
  #
72
73
  # @param [Hash] options
73
74
  # hash of operations to perform
74
- #
75
75
  # @option options [String] :chdir
76
76
  # The current directory.
77
- #
78
77
  # @option options [Integer] :timeout
79
78
  # Maximum number of seconds to allow the process
80
79
  # to run before aborting with a TimeoutExceeded
81
80
  # exception.
81
+ # @option options [Symbol] :signal
82
+ # Signal used on timeout, SIGKILL by default
83
+ #
84
+ # @yield [out, err]
85
+ # Yields stdout and stderr output whenever available
82
86
  #
83
87
  # @raise [ExitError]
84
88
  # raised when command exits with non-zero code
85
89
  #
86
90
  # @api public
87
- def run(*args)
91
+ def run(*args, &block)
88
92
  cmd = command(*args)
89
- yield(cmd) if block_given?
90
- result = execute_command(cmd)
93
+ result = execute_command(cmd, &block)
91
94
  if result && result.failure?
92
95
  raise ExitError.new(cmd.to_command, result)
93
96
  end
@@ -100,10 +103,28 @@ module TTY
100
103
  # cmd.run!(command, [argv1, ..., argvN], [options])
101
104
  #
102
105
  # @api public
103
- def run!(*args)
106
+ def run!(*args, &block)
104
107
  cmd = command(*args)
105
- yield(cmd) if block_given?
106
- execute_command(cmd)
108
+ execute_command(cmd, &block)
109
+ end
110
+
111
+ # Wait on long running script until output matches a specific pattern
112
+ #
113
+ # @example
114
+ # cmd.wait 'tail -f /var/log/php.log', /something happened/
115
+ #
116
+ # @api public
117
+ def wait(*args)
118
+ pattern = args.pop
119
+ unless pattern
120
+ raise ArgumentError, 'Please provide condition to wait for'
121
+ end
122
+
123
+ run(*args) do |out, _|
124
+ raise if out =~ /#{pattern}/
125
+ end
126
+ rescue ExitError
127
+ # noop
107
128
  end
108
129
 
109
130
  # Execute shell test command
@@ -145,11 +166,11 @@ module TTY
145
166
  end
146
167
 
147
168
  # @api private
148
- def execute_command(cmd)
169
+ def execute_command(cmd, &block)
149
170
  mutex = Mutex.new
150
171
  dry_run = @dry_run || cmd.options[:dry_run] || false
151
- @runner = select_runner(@printer, dry_run)
152
- mutex.synchronize { @runner.run(cmd) }
172
+ @runner = select_runner(dry_run).new(cmd, @printer)
173
+ mutex.synchronize { @runner.run!(&block) }
153
174
  end
154
175
 
155
176
  # @api private
@@ -173,17 +194,17 @@ module TTY
173
194
  def find_printer_class(name)
174
195
  const_name = name.to_s.capitalize.to_sym
175
196
  if const_name.empty? || !TTY::Command::Printers.const_defined?(const_name)
176
- fail ArgumentError, %(Unknown printer type "#{name}")
197
+ raise ArgumentError, %(Unknown printer type "#{name}")
177
198
  end
178
199
  TTY::Command::Printers.const_get(const_name)
179
200
  end
180
201
 
181
202
  # @api private
182
- def select_runner(printer, dry_run)
203
+ def select_runner(dry_run)
183
204
  if dry_run
184
- DryRunner.new(printer)
205
+ DryRunner
185
206
  else
186
- ProcessRunner.new(printer)
207
+ ProcessRunner
187
208
  end
188
209
  end
189
210
  end # Command
@@ -5,11 +5,17 @@ require_relative 'result'
5
5
  module TTY
6
6
  class Command
7
7
  class DryRunner
8
- def initialize(printer)
8
+ attr_reader :cmd
9
+
10
+ def initialize(cmd, printer)
11
+ @cmd = cmd
9
12
  @printer = printer
10
13
  end
11
14
 
12
- def run(cmd)
15
+ # Show command without running
16
+ #
17
+ # @api public
18
+ def run!(&block)
13
19
  cmd.to_command
14
20
  message = "#{@printer.decorate('(dry run)', :blue)} "
15
21
  message << @printer.decorate(cmd.to_command, :yellow, :bold)
@@ -6,37 +6,39 @@ require 'securerandom'
6
6
  module TTY
7
7
  class Command
8
8
  module Execute
9
- # Execute command in a child process
9
+ # Execute command in a child process with all IO streams piped
10
+ # in and out. The interface is similar to Process.spawn
10
11
  #
11
12
  # The caller should ensure that all IO objects are closed
12
13
  # when the child process is finished. However, when block
13
14
  # is provided this will be taken care of automatically.
14
15
  #
15
16
  # @param [Cmd] cmd
16
- # the command to execute
17
+ # the command to spawn
17
18
  #
18
19
  # @return [pid, stdin, stdout, stderr]
19
20
  #
20
21
  # @api public
21
22
  def spawn(cmd)
22
- @process_options = normalize_redirect_options(cmd.options)
23
+ process_opts = normalize_redirect_options(cmd.options)
23
24
 
24
25
  # Create pipes
25
26
  in_rd, in_wr = IO.pipe # reading
26
27
  out_rd, out_wr = IO.pipe # writing
27
28
  err_rd, err_wr = IO.pipe # error
29
+ in_wr.sync = true
28
30
 
29
31
  # redirect fds
30
32
  opts = ({
31
- :in => in_rd, # in_wr => :close,
32
- :out => out_wr,# out_rd => :close,
33
- :err => err_wr,# err_rd => :close
34
- }).merge(@process_options)
33
+ :in => in_rd, in_wr => :close,
34
+ :out => out_wr, out_rd => :close,
35
+ :err => err_wr, err_rd => :close
36
+ }).merge(process_opts)
35
37
 
36
38
  pid = Process.spawn(cmd.to_command, opts)
37
39
 
38
40
  # close in parent process
39
- [out_wr, err_wr].each { |fd| fd.close if fd }
41
+ [in_rd, out_wr, err_wr].each { |fd| fd.close if fd }
40
42
 
41
43
  tuple = [pid, in_wr, out_rd, err_rd]
42
44
 
@@ -20,8 +20,8 @@ module TTY
20
20
  message = ''
21
21
  message << "Running `#{cmd_name}` failed with\n"
22
22
  message << " exit status: #{result.exit_status}\n"
23
- message << " stdout: #{result.out.strip.empty? ? 'Nothing written' : result.out.strip}\n"
24
- message << " stderr: #{result.err.strip.empty? ? 'Nothing written' : result.err.strip}\n"
23
+ message << " stdout: #{(result.out || '').strip.empty? ? 'Nothing written' : result.out.strip}\n"
24
+ message << " stderr: #{(result.err || '').strip.empty? ? 'Nothing written' : result.err.strip}\n"
25
25
  end
26
26
  end # ExitError
27
27
  end # Command
@@ -17,7 +17,7 @@ module TTY
17
17
  end
18
18
 
19
19
  def write(message)
20
- output.print(message)
20
+ output << message
21
21
  end
22
22
  end # Progress
23
23
  end # Printers
@@ -11,84 +11,150 @@ module TTY
11
11
  class ProcessRunner
12
12
  include Execute
13
13
 
14
+ # the command to be spawned
15
+ attr_reader :cmd
16
+
14
17
  # Initialize a Runner object
15
18
  #
16
19
  # @param [Printer] printer
17
20
  # the printer to use for logging
18
21
  #
19
22
  # @api private
20
- def initialize(printer)
23
+ def initialize(cmd, printer)
24
+ @cmd = cmd
25
+ @timeout = cmd.options[:timeout]
26
+ @input = cmd.options[:input]
27
+ @signal = cmd.options[:signal] || :TERM
21
28
  @printer = printer
29
+ @threads = []
30
+ @lock = Mutex.new
22
31
  end
23
32
 
24
33
  # Execute child process
25
34
  # @api public
26
- def run(cmd)
27
- timeout = cmd.options[:timeout]
35
+ def run!(&block)
28
36
  @printer.print_command_start(cmd)
29
37
  start = Time.now
38
+ runtime = 0.0
30
39
 
31
- spawn(cmd) do |pid, stdin, stdout, stderr|
32
- stdout_data, stderr_data = read_streams(cmd, stdout, stderr)
40
+ pid, stdin, stdout, stderr = spawn(cmd) # do |pid, stdin, stdout, stderr|
33
41
 
34
- runtime = Time.now - start
35
- handle_timeout(timeout, runtime, pid)
36
- status = waitpid(pid)
42
+ # write and read streams
43
+ write_stream(stdin)
44
+ stdout_data, stderr_data = read_streams(stdout, stderr, &block)
37
45
 
38
- @printer.print_command_exit(cmd, status, runtime)
46
+ status = waitpid(pid)
47
+ runtime = Time.now - start
39
48
 
40
- Result.new(status, stdout_data, stderr_data)
41
- end
49
+ @printer.print_command_exit(cmd, status, runtime)
50
+
51
+ Result.new(status, stdout_data, stderr_data)
52
+ rescue
53
+ terminate(pid)
54
+ Result.new(-1, stdout_data, stderr_data)
55
+ ensure
56
+ [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? }
57
+ end
58
+
59
+ # Stop a process marked by pid
60
+ #
61
+ # @param [Integer] pid
62
+ #
63
+ # @api public
64
+ def terminate(pid)
65
+ ::Process.kill(@signal, pid)
42
66
  end
43
67
 
44
68
  private
45
69
 
46
70
  # @api private
47
- def handle_timeout(timeout, runtime, pid)
48
- return unless timeout
71
+ def handle_timeout(runtime)
72
+ return unless @timeout
49
73
 
50
- t = timeout - runtime
51
- if t < 0.0
52
- ::Process.kill(:KILL, pid)
74
+ t = @timeout - runtime
75
+ raise TimeoutExceeded if t < 0.0
76
+ end
77
+
78
+ # @api private
79
+ def write_stream(stdin)
80
+ return unless @input
81
+ writers = [stdin]
82
+ start = Time.now
83
+
84
+ # wait when ready for writing to pipe
85
+ _, writable = IO.select(nil, writers, writers, @timeout)
86
+ raise TimeoutExceeded if writable.nil?
87
+
88
+ while writers.any?
89
+ writable.each do |fd|
90
+ begin
91
+ err = nil
92
+ size = fd.write(@input)
93
+ @input = @input.byteslice(size..-1)
94
+ rescue Errno::EPIPE => err
95
+ end
96
+ if err || @input.bytesize == 0
97
+ writers.delete(stdin)
98
+ end
99
+
100
+ # control total time spent writing
101
+ runtime = Time.now - start
102
+ handle_timeout(runtime)
103
+ end
53
104
  end
54
105
  end
55
106
 
107
+ # Read stdout & stderr streams in the background
108
+ #
109
+ # @param [IO] stdout
110
+ # @param [IO] stderr
111
+ #
56
112
  # @api private
57
- def read_streams(cmd, stdout, stderr)
113
+ def read_streams(stdout, stderr, &block)
58
114
  stdout_data = ''
59
115
  stderr_data = Truncator.new
60
- timeout = cmd.options[:timeout]
61
116
 
62
- stdout_thread = Thread.new do
63
- begin
64
- while (line = stdout.gets)
65
- stdout_data << line
66
- @printer.print_command_out_data(cmd, line)
67
- end
68
- rescue TimeoutExceeded
69
- stdout.close
117
+ print_out = -> (cmd, line) { @printer.print_command_out_data(cmd, line) }
118
+ print_err = -> (cmd, line) { @printer.print_command_err_data(cmd, line) }
119
+
120
+ stdout_yield = -> (line) { block.(line, nil) if block }
121
+ stderr_yield = -> (line) { block.(nil, line) if block }
122
+
123
+ @threads << read_stream(stdout, stdout_data, print_out, stdout_yield)
124
+ @threads << read_stream(stderr, stderr_data, print_err, stderr_yield)
125
+
126
+ @threads.each do |th|
127
+ result = th.join(@timeout)
128
+ if result.nil?
129
+ @threads[0].raise
130
+ @threads[1].raise
70
131
  end
71
132
  end
72
133
 
73
- stderr_thread = Thread.new do
134
+ [stdout_data, stderr_data.read]
135
+ end
136
+
137
+ def read_stream(stream, data, print_callback, callback)
138
+ Thread.new do
139
+ Thread.current[:cmd_start] = Time.now
74
140
  begin
75
- while (line = stderr.gets)
76
- stderr_data << line
77
- @printer.print_command_err_data(cmd, line)
141
+ while (line = stream.gets)
142
+ @lock.synchronize do
143
+ data << line
144
+ callback.(line)
145
+ print_callback.(cmd, line)
146
+ end
147
+
148
+ # control total time spent reading
149
+ runtime = Time.now - Thread.current[:cmd_start]
150
+ handle_timeout(runtime)
78
151
  end
79
- rescue TimeoutExceeded
80
- stderr.close
81
- end
82
- end
83
-
84
- [stdout_thread, stderr_thread].each do |th|
85
- result = th.join(timeout)
86
- if result.nil?
87
- stdout_thread.raise(TimeoutExceeded)
88
- stderr_thread.raise(TimeoutExceeded)
152
+ rescue => err
153
+ raise err
154
+ ensure
155
+ stream.close
89
156
  end
90
157
  end
91
- [stdout_data, stderr_data.read]
92
158
  end
93
159
 
94
160
  # @api private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Command
5
- VERSION = '0.4.0'
5
+ VERSION = '0.5.0'
6
6
  end # Command
7
7
  end # TTY
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-command
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-02-22 00:00:00.000000000 Z
11
+ date: 2017-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel
@@ -94,12 +94,16 @@ files:
94
94
  - bin/setup
95
95
  - examples/bash.rb
96
96
  - examples/basic.rb
97
+ - examples/cli
97
98
  - examples/env.rb
98
99
  - examples/logger.rb
100
+ - examples/output.rb
99
101
  - examples/redirect_stderr.rb
100
102
  - examples/redirect_stdin.rb
101
103
  - examples/redirect_stdout.rb
104
+ - examples/stdin_input.rb
102
105
  - examples/timeout.rb
106
+ - examples/wait.rb
103
107
  - lib/tty-command.rb
104
108
  - lib/tty/command.rb
105
109
  - lib/tty/command/cmd.rb