tty-command 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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