tty-command 0.6.0 → 0.7.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: c3d696d9fa5255f95622561988eddfcb26650824
4
- data.tar.gz: cf0fe3027e2e2958b55a1c99ce1f9769ccbd717a
3
+ metadata.gz: fb721a5620ec91bddc77643358783a38fb76631f
4
+ data.tar.gz: b0f3485e21f80e9172d1651eb661360fb3972d6f
5
5
  SHA512:
6
- metadata.gz: 06bdcfbea782615ef663e866bc11862f7cc9046a98ce6748418d83c4d539597e93f6d31bd928477b91a08370c9db9cef33094252110e08439a06cc02bac7b60d
7
- data.tar.gz: 94da004a191721e5d45e26f62587d5bfadb767fc083af00fa3eeac4d7e5520b7ec354578597b6c563bfb423adcb6d018e41d9215e8cf592099f79475dedf1e2c
6
+ metadata.gz: 3184987586dff4f957bbeee34cf0811f60c10db0f533fe48e957f171a3a1b22485f1e4970a7b4867466deb73ad95e9b3ae30c4194753724471756aece30446de
7
+ data.tar.gz: 1500b854c1a69a788e49226d28403f92ee28258b608939596e1da92296ff815623ef70885e0c54751e3a8769539fc5f6c8dfd973f69c43c87f112cbf575707ce
@@ -2,13 +2,14 @@
2
2
  language: ruby
3
3
  sudo: false
4
4
  cache: bundler
5
+ before_install: "gem update bundler"
5
6
  script: "bundle exec rake ci"
6
7
  rvm:
7
8
  - 2.0.0
8
9
  - 2.1.10
9
- - 2.2.6
10
- - 2.3.3
11
- - 2.4.1
10
+ - 2.2.8
11
+ - 2.3.5
12
+ - 2.4.2
12
13
  - ruby-head
13
14
  - jruby-9.1.1.0
14
15
  - jruby-head
@@ -1,5 +1,27 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.7.0] - 2017-11-19
4
+
5
+ ### Added
6
+ * Add :binmode option to allow configuring input & ouput as binary
7
+ * Add :pty option to allow runnig commands in PTY(pseudo terminal)
8
+
9
+ ### Changed
10
+ * Change Command to remove threads synchronization to leave it up to client to handle
11
+ * Change Cmd to allow updating options
12
+ * Change Command to accept options for all commands such as :timeout, :binmode etc...
13
+ * Change Execute to ChildProcess module
14
+ * Change ChildProcess to skip spawn redirect close options on Windows platform
15
+ * Change to enforce UTF-8 encoding for process pipes to be cross platform
16
+ * Change ProcessRunner to stop rescuing runtime failures
17
+ * Change to stop mutating String instances
18
+
19
+ ### Fixed
20
+ * Fix ProcessRunner threads deadlocking on exclusive mutex
21
+ * Fix :timeout option to raise TimeoutExceeded error
22
+ * Fix test suite to work on Windows
23
+ * Fix Cmd arguments escaping
24
+
3
25
  ## [v0.6.0] - 2017-07-22
4
26
 
5
27
  ### Added
@@ -74,6 +96,9 @@
74
96
 
75
97
  * Initial implementation and release
76
98
 
99
+ [v0.7.0]: https://github.com/piotrmurach/tty-command/compare/v0.6.0...v0.7.0
100
+ [v0.6.0]: https://github.com/piotrmurach/tty-command/compare/v0.5.0...v0.6.0
101
+ [v0.5.0]: https://github.com/piotrmurach/tty-command/compare/v0.4.0...v0.5.0
77
102
  [v0.4.0]: https://github.com/piotrmurach/tty-command/compare/v0.3.3...v0.4.0
78
103
  [v0.3.3]: https://github.com/piotrmurach/tty-command/compare/v0.3.2...v0.3.3
79
104
  [v0.3.2]: https://github.com/piotrmurach/tty-command/compare/v0.3.1...v0.3.2
data/Gemfile CHANGED
@@ -6,3 +6,9 @@ group :test do
6
6
  gem 'simplecov', '~> 0.12.0'
7
7
  gem 'coveralls', '~> 0.8.17'
8
8
  end
9
+
10
+ if RUBY_VERSION > '2.1.0'
11
+ group :perf do
12
+ gem 'memory_profiler', '~> 0.9.8'
13
+ end
14
+ end
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # TTY::Command [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
2
+
2
3
  [![Gem Version](https://badge.fury.io/rb/tty-command.svg)][gem]
3
4
  [![Build Status](https://secure.travis-ci.org/piotrmurach/tty-command.svg?branch=master)][travis]
5
+ [![Build status](https://ci.appveyor.com/api/projects/status/0150ync7bdkfhmsv?svg=true)][appveyor]
4
6
  [![Code Climate](https://codeclimate.com/github/piotrmurach/tty-command/badges/gpa.svg)][codeclimate]
5
7
  [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-command/badge.svg)][coverage]
6
8
  [![Inline docs](http://inch-ci.org/github/piotrmurach/tty-command.svg?branch=master)][inchpages]
@@ -8,6 +10,7 @@
8
10
  [gitter]: https://gitter.im/piotrmurach/tty
9
11
  [gem]: http://badge.fury.io/rb/tty-command
10
12
  [travis]: http://travis-ci.org/piotrmurach/tty-command
13
+ [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-command
11
14
  [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-command
12
15
  [coverage]: https://coveralls.io/github/piotrmurach/tty-command
13
16
  [inchpages]: http://inch-ci.org/github/piotrmurach/tty-command
@@ -56,14 +59,16 @@ Or install it yourself as:
56
59
  * [3. Advanced Interface](#3-advanced-interface)
57
60
  * [3.1. Environment variables](#31-environment-variables)
58
61
  * [3.2. Options](#32-options)
59
- * [3.2.1. Current directory](#321-current-directory)
60
- * [3.2.2. Redirection](#322-redirection)
61
- * [3.2.3. Handling input](#323-handling-input)
62
- * [3.2.4. Timeout](#324-timeout)
62
+ * [3.2.1. Redirection](#321-redirection)
63
+ * [3.2.2. Handling input](#322-handling-input)
64
+ * [3.2.3. Timeout](#323-timeout)
65
+ * [3.2.4. Binary mode](#324-binary-mode)
63
66
  * [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)
67
+ * [3.2.6. PTY(pseudo-terminal)](#326-ptypseudo-terminal)
68
+ * [3.2.7. Current directory](#327-current-directory)
69
+ * [3.2.8. User](#328-user)
70
+ * [3.2.9. Group](#329-group)
71
+ * [3.2.10. Umask](#3210-umask)
67
72
  * [3.3. Result](#33-result)
68
73
  * [3.3.1. success?](#331-success)
69
74
  * [3.3.2. failure?](#332-failure)
@@ -266,15 +271,7 @@ cmd.run(:echo, 'hello', env: {foo: 'bar', baz: nil})
266
271
 
267
272
  When a hash is given in the last argument (options), it allows to specify a current directory, umask, user, group and and zero or more fd redirects for the child process.
268
273
 
269
- #### 3.2.1 Current directory
270
-
271
- To change directory in which the command is run pass the `:chidir` option:
272
-
273
- ```ruby
274
- cmd.run(:echo, 'hello', chdir: '/var/tmp')
275
- ```
276
-
277
- #### 3.2.2 Redirection
274
+ #### 3.2.1 Redirection
278
275
 
279
276
  There are few ways you can redirect commands output.
280
277
 
@@ -328,7 +325,7 @@ You can, for example, read data from one source and output to another:
328
325
  cmd.run("cat", :in => "Gemfile", :out => 'gemfile.log')
329
326
  ```
330
327
 
331
- #### 3.2.3 Handling Input
328
+ #### 3.2.2 Handling Input
332
329
 
333
330
  You can provide input to stdin stream using the `:input` key. For instance, given the following executable called `cli` that expects name from `stdin`:
334
331
 
@@ -357,7 +354,7 @@ in_stream.rewind
357
354
  cmd.run("my_cli_program", "login", in: in_stream).out
358
355
  ```
359
356
 
360
- #### 3.2.4 Timeout
357
+ #### 3.2.3 Timeout
361
358
 
362
359
  You can timeout command execuation by providing the `:timeout` option in seconds:
363
360
 
@@ -365,8 +362,28 @@ You can timeout command execuation by providing the `:timeout` option in seconds
365
362
  cmd.run("while test 1; sleep 1; done", timeout: 5)
366
363
  ```
367
364
 
365
+ And to set it for all commands do:
366
+
367
+ ```ruby
368
+ cmd = TTY::Command.new(timeout: 5)
369
+ ```
370
+
368
371
  Please run `examples/timeout.rb` to see timeout in action.
369
372
 
373
+ #### 3.2.4 Binary mode
374
+
375
+ By default the standard input, output and error are non-binary. However, you can change to read and write in binary mode by using the `:binmode` option like so:
376
+
377
+ ```ruby
378
+ cmd.run("echo 'hello'", binmode: true)
379
+ ```
380
+
381
+ To set all commands to be run in binary mode do:
382
+
383
+ ```ruby
384
+ cmd = TTY::Command.new(binmode: true)
385
+ ```
386
+
370
387
  #### 3.2.5 Signal
371
388
 
372
389
  You can specify process termination signal other than the defaut `SIGTERM`:
@@ -375,7 +392,47 @@ You can specify process termination signal other than the defaut `SIGTERM`:
375
392
  cmd.run("whilte test1; sleep1; done", timeout: 5, signal: :KILL)
376
393
  ```
377
394
 
378
- #### 3.2.6 User
395
+ #### 3.2.6 PTY(pseudo terminal)
396
+
397
+ The `:pty` configuration option causes the command to be executed in subprocess where each stream is a pseudo terminal. By default this options is set to `false`. However, some comamnds may require a terminal like device to work correctly. For example, a command may emit colored output only if it is running via terminal.
398
+
399
+ In order to run command in pseudo terminal, either set the flag globally for all commands:
400
+
401
+ ```ruby
402
+ cmd = TTY::Command.new(pty: true)
403
+ ```
404
+
405
+ or for each executed command individually:
406
+
407
+ ```ruby
408
+ cmd.run("echo 'hello'", pty: true)
409
+ ```
410
+
411
+ Please note though, that setting `:pty` to `true` may change how the command behaves. For instance, on unix like systems the line feed character `\n` in output will be prefixed with carriage return `\r`:
412
+
413
+ ```ruby
414
+ out, _ = cmd.run("echo 'hello'")
415
+ out # => "hello\n"
416
+ ```
417
+
418
+ and with `:pty` option:
419
+
420
+ ```ruby
421
+ out, _ = cmd.run("echo 'hello'", pty: true)
422
+ out # => "hello\r\n"
423
+ ```
424
+
425
+ In addition, any input to command may be echoed to the standard output.
426
+
427
+ #### 3.2.7 Current directory
428
+
429
+ To change directory in which the command is run pass the `:chdir` option:
430
+
431
+ ```ruby
432
+ cmd.run(:echo, 'hello', chdir: '/var/tmp')
433
+ ```
434
+
435
+ #### 3.2.8 User
379
436
 
380
437
  To run command as a given user do:
381
438
 
@@ -383,7 +440,7 @@ To run command as a given user do:
383
440
  cmd.run(:echo, 'hello', user: 'piotr')
384
441
  ```
385
442
 
386
- #### 3.2.7 Group
443
+ #### 3.2.9 Group
387
444
 
388
445
  To run command as part of group do:
389
446
 
@@ -391,7 +448,7 @@ To run command as part of group do:
391
448
  cmd.run(:echo, 'hello', group: 'devs')
392
449
  ```
393
450
 
394
- #### 3.2.8 Umask
451
+ #### 3.2.10 Umask
395
452
 
396
453
  To run command with umask do:
397
454
 
@@ -0,0 +1,24 @@
1
+ ---
2
+ install:
3
+ - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
4
+ - ruby --version
5
+ - gem --version
6
+ - bundle install
7
+ build: off
8
+ test_script:
9
+ - bundle exec rake ci
10
+ environment:
11
+ matrix:
12
+ - ruby_version: "200"
13
+ - ruby_version: "200-x64"
14
+ - ruby_version: "21"
15
+ - ruby_version: "21-x64"
16
+ - ruby_version: "22"
17
+ - ruby_version: "22-x64"
18
+ - ruby_version: "23"
19
+ - ruby_version: "23-x64"
20
+ - ruby_version: "24"
21
+ - ruby_version: "24-x64"
22
+ matrix:
23
+ allow_failures:
24
+ - ruby_version: "193"
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'memory_profiler'
4
+ require 'tty-command'
5
+
6
+ report = MemoryProfiler.report do
7
+ cmd = TTY::Command.new(color: false)
8
+ cmd.run("echo 'hello world!'")
9
+ end
10
+
11
+ report.pretty_print(to_file: 'memory_report.txt')
@@ -0,0 +1,7 @@
1
+ require 'tty-command'
2
+
3
+ cmd = TTY::Command.new
4
+
5
+ path = File.expand_path("../spec/fixtures/color", __dir__)
6
+
7
+ cmd.run(path, pty: true)
@@ -0,0 +1,12 @@
1
+ require 'tty-command'
2
+
3
+ cmd = TTY::Command.new
4
+
5
+ threads = []
6
+ 3.times do |i|
7
+ th = Thread.new do
8
+ 10.times { cmd.run("echo th#{i}; sleep 0.1") }
9
+ end
10
+ threads << th
11
+ end
12
+ threads.each(&:join)
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'rbconfig'
@@ -25,6 +24,8 @@ module TTY
25
24
  RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']
26
25
  )
27
26
 
27
+ WIN_PLATFORMS = /cygwin|mswin|mingw|bccwin|wince|emx/.freeze
28
+
28
29
  def self.record_separator
29
30
  @record_separator ||= $/
30
31
  end
@@ -33,6 +34,10 @@ module TTY
33
34
  @record_separator = sep
34
35
  end
35
36
 
37
+ def self.windows?
38
+ !!(RbConfig::CONFIG['host_os'] =~ WIN_PLATFORMS)
39
+ end
40
+
36
41
  attr_reader :printer
37
42
 
38
43
  # Initialize a Command object
@@ -46,13 +51,17 @@ module TTY
46
51
  # the mode for executing command
47
52
  #
48
53
  # @api public
49
- def initialize(options = {})
54
+ def initialize(**options)
50
55
  @output = options.fetch(:output) { $stdout }
51
56
  @color = options.fetch(:color) { true }
52
57
  @uuid = options.fetch(:uuid) { true }
53
58
  @printer_name = options.fetch(:printer) { :pretty }
54
59
  @dry_run = options.fetch(:dry_run) { false }
55
60
  @printer = use_printer(@printer_name, color: @color, uuid: @uuid)
61
+ @cmd_options = {}
62
+ @cmd_options[:pty] = true if options[:pty]
63
+ @cmd_options[:binmode] = true if options[:binmode]
64
+ @cmd_options[:timeout] = options[:timeout] if options[:timeout]
56
65
  end
57
66
 
58
67
  # Start external executable in a child process
@@ -163,15 +172,16 @@ module TTY
163
172
 
164
173
  # @api private
165
174
  def command(*args)
166
- Cmd.new(*args)
175
+ cmd = Cmd.new(*args)
176
+ cmd.update(@cmd_options)
177
+ cmd
167
178
  end
168
179
 
169
180
  # @api private
170
181
  def execute_command(cmd, &block)
171
- mutex = Mutex.new
172
182
  dry_run = @dry_run || cmd.options[:dry_run] || false
173
- @runner = select_runner(dry_run).new(cmd, @printer)
174
- mutex.synchronize { @runner.run!(&block) }
183
+ @runner = select_runner(dry_run).new(cmd, @printer, &block)
184
+ @runner.run!
175
185
  end
176
186
 
177
187
  # @api private
@@ -3,10 +3,11 @@
3
3
 
4
4
  require 'tempfile'
5
5
  require 'securerandom'
6
+ require 'io/console'
6
7
 
7
8
  module TTY
8
9
  class Command
9
- module Execute
10
+ module ChildProcess
10
11
  # Execute command in a child process with all IO streams piped
11
12
  # in and out. The interface is similar to Process.spawn
12
13
  #
@@ -22,19 +23,45 @@ module TTY
22
23
  # @api public
23
24
  def spawn(cmd)
24
25
  process_opts = normalize_redirect_options(cmd.options)
26
+ binmode = cmd.options[:binmode] || false
27
+ pty = cmd.options[:pty] || false
28
+
29
+ pty = try_loading_pty if pty
30
+ require('pty') if pty # load within this scope
25
31
 
26
32
  # Create pipes
27
- in_rd, in_wr = IO.pipe # reading
28
- out_rd, out_wr = IO.pipe # writing
29
- err_rd, err_wr = IO.pipe # error
33
+ in_rd, in_wr = pty ? PTY.open : IO.pipe('utf-8') # reading
34
+ out_rd, out_wr = pty ? PTY.open : IO.pipe('utf-8') # writing
35
+ err_rd, err_wr = pty ? PTY.open : IO.pipe('utf-8') # error
30
36
  in_wr.sync = true
31
37
 
38
+ if binmode
39
+ in_wr.binmode
40
+ out_rd.binmode
41
+ err_rd.binmode
42
+ end
43
+
44
+ if pty
45
+ in_wr.raw!
46
+ out_wr.raw!
47
+ err_wr.raw!
48
+ end
49
+
32
50
  # redirect fds
33
51
  opts = {
34
- :in => in_rd, in_wr => :close,
35
- :out => out_wr, out_rd => :close,
36
- :err => err_wr, err_rd => :close
37
- }.merge(process_opts)
52
+ in: in_rd,
53
+ out: out_wr,
54
+ err: err_wr
55
+ }
56
+ unless TTY::Command.windows?
57
+ close_child_fds = {
58
+ in_wr => :close,
59
+ out_rd => :close,
60
+ err_rd => :close
61
+ }
62
+ opts.merge!(close_child_fds)
63
+ end
64
+ opts.merge!(process_opts)
38
65
 
39
66
  pid = Process.spawn(cmd.to_command, opts)
40
67
 
@@ -47,14 +74,28 @@ module TTY
47
74
  begin
48
75
  return yield(*tuple)
49
76
  ensure
77
+ # ensure parent pipes are closed
50
78
  [in_wr, out_rd, err_rd].each { |fd| fd.close if fd && !fd.closed? }
51
79
  end
52
80
  else
53
81
  tuple
54
82
  end
55
83
  end
84
+ module_function :spawn
56
85
 
57
- private
86
+ # Try loading pty module
87
+ #
88
+ # @return [Boolean]
89
+ #
90
+ # @api private
91
+ def try_loading_pty
92
+ require 'pty'
93
+ true
94
+ rescue LoadError
95
+ warn("Requested PTY device but the system doesn't support it.")
96
+ false
97
+ end
98
+ module_function :try_loading_pty
58
99
 
59
100
  # Normalize spawn fd into :in, :out, :err keys.
60
101
  #
@@ -75,6 +116,7 @@ module TTY
75
116
  opts
76
117
  end
77
118
  end
119
+ module_function :normalize_redirect_options
78
120
 
79
121
  # Convert option pari to recognized spawn option pair
80
122
  #
@@ -93,6 +135,7 @@ module TTY
93
135
  end
94
136
  [key, value]
95
137
  end
138
+ module_function :convert
96
139
 
97
140
  # Determine if object is a fd
98
141
  #
@@ -110,6 +153,7 @@ module TTY
110
153
  respond_to?(:to_i) && !object.to_io.nil?
111
154
  end
112
155
  end
156
+ module_function :fd?
113
157
 
114
158
  # Convert fd to name :in, :out, :err
115
159
  #
@@ -132,6 +176,7 @@ module TTY
132
176
  raise ExecuteError, "Wrong execute redirect: #{object.inspect}"
133
177
  end
134
178
  end
179
+ module_function :fd_to_process_key
135
180
 
136
181
  # Convert file name to file handle
137
182
  #
@@ -149,6 +194,7 @@ module TTY
149
194
  tmp.rewind
150
195
  tmp
151
196
  end
197
+ module_function :convert_to_fd
152
198
 
153
199
  # Attempts to read object content
154
200
  #
@@ -162,6 +208,7 @@ module TTY
162
208
  object
163
209
  end
164
210
  end
165
- end # Execute
211
+ module_function :try_reading
212
+ end # ChildProcess
166
213
  end # Command
167
214
  end # TTY
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'securerandom'
4
+ require 'shellwords'
4
5
 
5
6
  module TTY
6
7
  class Command
@@ -46,7 +47,7 @@ module TTY
46
47
  else
47
48
  @command = sanitize(command)
48
49
  end
49
- @argv = args.map { |i| shell_escape(i) }
50
+ @argv = args.map { |i| Shellwords.escape(i) }
50
51
  end
51
52
  @env ||= {}
52
53
  @options = opts
@@ -55,6 +56,13 @@ module TTY
55
56
  freeze
56
57
  end
57
58
 
59
+ # Extend command options if keys don't already exist
60
+ #
61
+ # @api public
62
+ def update(**options)
63
+ @options.update(options.update(@options))
64
+ end
65
+
58
66
  # The shell environment variables
59
67
  #
60
68
  # @api public
@@ -131,20 +139,6 @@ module TTY
131
139
  def sanitize(value)
132
140
  value.to_s.dup
133
141
  end
134
-
135
- # Enclose argument in quotes if it contains
136
- # characters that require escaping
137
- #
138
- # @param [String] arg
139
- # the argument to escape
140
- #
141
- # @api private
142
- def shell_escape(arg)
143
- str = arg.to_s.dup
144
- return str if str =~ /^[0-9A-Za-z+,.\/:=@_-]+$/
145
- str.gsub!("'", "'\\''")
146
- "'#{str}'"
147
- end
148
142
  end # Cmd
149
143
  end # Command
150
144
  end # TTY
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
- require 'tty/command/printers/abstract'
4
+ require_relative 'abstract'
4
5
 
5
6
  module TTY
6
7
  class Command
@@ -1,16 +1,20 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pastel'
4
- require 'tty/command/printers/abstract'
5
+
6
+ require_relative 'abstract'
5
7
 
6
8
  module TTY
7
9
  class Command
8
10
  module Printers
9
11
  class Pretty < Abstract
12
+ TIME_FORMAT = "%5.3f %s".freeze
13
+
10
14
  def print_command_start(cmd, *args)
11
- message = "Running #{decorate(cmd.to_command, :yellow, :bold)}"
15
+ message = ["Running #{decorate(cmd.to_command, :yellow, :bold)}"]
12
16
  message << args.map(&:chomp).join(' ') unless args.empty?
13
- write(message, cmd.uuid)
17
+ write(message.join, cmd.uuid)
14
18
  end
15
19
 
16
20
  def print_command_out_data(cmd, *args)
@@ -24,11 +28,11 @@ module TTY
24
28
  end
25
29
 
26
30
  def print_command_exit(cmd, status, runtime, *args)
27
- runtime = "%5.3f %s" % [runtime, pluralize(runtime, 'second')]
28
- message = "Finished in #{runtime}"
31
+ runtime = TIME_FORMAT % [runtime, pluralize(runtime, 'second')]
32
+ message = ["Finished in #{runtime}"]
29
33
  message << " with exit status #{status}" if status
30
34
  message << " (#{success_or_failure(status)})"
31
- write(message, cmd.uuid)
35
+ write(message.join, cmd.uuid)
32
36
  end
33
37
 
34
38
  # Write message out to output
@@ -36,12 +40,12 @@ module TTY
36
40
  # @api private
37
41
  def write(message, uuid = nil)
38
42
  uuid_needed = options.fetch(:uuid) { true }
39
- out = ''
43
+ out = []
40
44
  if uuid_needed
41
45
  out << "[#{decorate(uuid, :green)}] " unless uuid.nil?
42
46
  end
43
47
  out << "#{message}\n"
44
- output << out
48
+ output << out.join
45
49
  end
46
50
 
47
51
  private
@@ -1,7 +1,8 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pastel'
4
- require 'tty/command/printers/abstract'
5
+ require_relative 'abstract'
5
6
 
6
7
  module TTY
7
8
  class Command
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'tty/command/printers/abstract'
3
+ require_relative 'abstract'
4
4
 
5
5
  module TTY
6
6
  class Command
@@ -3,15 +3,13 @@
3
3
 
4
4
  require 'thread'
5
5
 
6
- require_relative 'execute'
6
+ require_relative 'child_process'
7
7
  require_relative 'result'
8
8
  require_relative 'truncator'
9
9
 
10
10
  module TTY
11
11
  class Command
12
12
  class ProcessRunner
13
- include Execute
14
-
15
13
  # the command to be spawned
16
14
  attr_reader :cmd
17
15
 
@@ -21,28 +19,45 @@ module TTY
21
19
  # the printer to use for logging
22
20
  #
23
21
  # @api private
24
- def initialize(cmd, printer)
22
+ def initialize(cmd, printer, &block)
25
23
  @cmd = cmd
26
24
  @timeout = cmd.options[:timeout]
27
25
  @input = cmd.options[:input]
28
26
  @signal = cmd.options[:signal] || :TERM
29
27
  @printer = printer
30
- @threads = []
31
- @lock = Mutex.new
28
+ @block = block
32
29
  end
33
30
 
34
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
+ #
35
39
  # @api public
36
- def run!(&block)
40
+ def run!
37
41
  @printer.print_command_start(cmd)
38
42
  start = Time.now
39
43
  runtime = 0.0
40
44
 
41
- pid, stdin, stdout, stderr = spawn(cmd)
45
+ pid, stdin, stdout, stderr = ChildProcess.spawn(cmd)
46
+
47
+ # no input to write, close child's stdin pipe
48
+ stdin.close if (@input.nil? || @input.empty?) && !stdin.nil?
42
49
 
43
- # write and read streams
44
- write_stream(stdin)
45
- stdout_data, stderr_data = read_streams(stdout, stderr, &block)
50
+ readers = [stdout, stderr]
51
+ writers = [@input && stdin].compact
52
+
53
+ while writers.any?
54
+ ready_readers, ready_writers = IO.select(readers, writers, [], @timeout)
55
+ raise TimeoutExceeded if ready_readers.nil? || ready_writers.nil?
56
+
57
+ write_stream(ready_writers, writers)
58
+ end
59
+
60
+ stdout_data, stderr_data = read_streams(stdout, stderr)
46
61
 
47
62
  status = waitpid(pid)
48
63
  runtime = Time.now - start
@@ -50,9 +65,6 @@ module TTY
50
65
  @printer.print_command_exit(cmd, status, runtime)
51
66
 
52
67
  Result.new(status, stdout_data, stderr_data, runtime)
53
- rescue
54
- terminate(pid)
55
- Result.new(-1, stdout_data, stderr_data)
56
68
  ensure
57
69
  [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? }
58
70
  end
@@ -76,32 +88,31 @@ module TTY
76
88
  raise TimeoutExceeded if t < 0.0
77
89
  end
78
90
 
91
+ # Write the input to the process stdin
92
+ #
79
93
  # @api private
80
- def write_stream(stdin)
81
- return unless @input
82
- writers = [stdin]
94
+ def write_stream(ready_writers, writers)
83
95
  start = Time.now
84
-
85
- # wait when ready for writing to pipe
86
- _, writable = IO.select(nil, writers, writers, @timeout)
87
- raise TimeoutExceeded if writable.nil?
88
-
89
- while writers.any?
90
- writable.each do |fd|
91
- begin
92
- err = nil
93
- size = fd.write(@input)
94
- @input = @input.byteslice(size..-1)
95
- rescue Errno::EPIPE => err
96
- end
97
- if err || @input.bytesize == 0
98
- writers.delete(stdin)
99
- end
100
-
101
- # control total time spent writing
102
- runtime = Time.now - start
103
- handle_timeout(runtime)
96
+ ready_writers.each do |fd|
97
+ begin
98
+ err = nil
99
+ size = fd.write(@input)
100
+ @input = @input.byteslice(size..-1)
101
+ rescue IO::WaitWritable
102
+ rescue Errno::EPIPE => err
103
+ # The pipe closed before all input written
104
+ # Probably process exited prematurely
105
+ fd.close
106
+ writers.delete(fd)
107
+ end
108
+ if err || @input.bytesize == 0
109
+ fd.close
110
+ writers.delete(fd)
104
111
  end
112
+
113
+ # control total time spent writing
114
+ runtime = Time.now - start
115
+ handle_timeout(runtime)
105
116
  end
106
117
  end
107
118
 
@@ -111,45 +122,45 @@ module TTY
111
122
  # @param [IO] stderr
112
123
  #
113
124
  # @api private
114
- def read_streams(stdout, stderr, &block)
125
+ def read_streams(stdout, stderr)
115
126
  stdout_data = []
116
127
  stderr_data = Truncator.new
117
128
 
118
- print_out = -> (cmd, line) { @printer.print_command_out_data(cmd, line) }
119
- print_err = -> (cmd, line) { @printer.print_command_err_data(cmd, line) }
129
+ out_buffer = -> (line) {
130
+ stdout_data << line
131
+ @printer.print_command_out_data(cmd, line)
132
+ @block.(line, nil) if @block
133
+ }
120
134
 
121
- stdout_yield = -> (line) { block.(line, nil) if block }
122
- stderr_yield = -> (line) { block.(nil, line) if block }
135
+ err_buffer = -> (line) {
136
+ stderr_data << line
137
+ @printer.print_command_err_data(cmd, line)
138
+ @block.(nil, line) if @block
139
+ }
123
140
 
124
- @threads << read_stream(stdout, stdout_data, print_out, stdout_yield)
125
- @threads << read_stream(stderr, stderr_data, print_err, stderr_yield)
141
+ stdout_thread = read_stream(stdout, out_buffer)
142
+ stderr_thread = read_stream(stderr, err_buffer)
126
143
 
127
- @threads.each do |th|
128
- result = th.join(@timeout)
129
- if result.nil?
130
- @threads[0].raise
131
- @threads[1].raise
132
- end
133
- end
144
+ stdout_thread.join
145
+ stderr_thread.join
134
146
 
135
147
  [stdout_data.join, stderr_data.read]
136
148
  end
137
149
 
138
- def read_stream(stream, data, print_callback, callback)
150
+ def read_stream(stream, buffer)
139
151
  Thread.new do
140
152
  Thread.current[:cmd_start] = Time.now
141
153
  begin
142
154
  while (line = stream.gets)
143
- @lock.synchronize do
144
- data << line
145
- callback.(line)
146
- print_callback.(cmd, line)
147
- end
155
+ buffer.(line)
148
156
 
149
157
  # control total time spent reading
150
158
  runtime = Time.now - Thread.current[:cmd_start]
151
159
  handle_timeout(runtime)
152
160
  end
161
+ rescue Errno::EIO
162
+ # GNU/Linux `gets` raises when PTY slave is closed
163
+ nil
153
164
  rescue => err
154
165
  raise err
155
166
  ensure
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Command
5
- VERSION = '0.6.0'
5
+ VERSION = '0.7.0'
6
6
  end # Command
7
7
  end # TTY
@@ -4,7 +4,8 @@ desc 'Load gem inside irb console'
4
4
  task :console do
5
5
  require 'irb'
6
6
  require 'irb/completion'
7
- require File.join(__FILE__, '../../lib/tty-command')
7
+ require_relative '../lib/tty-command'
8
8
  ARGV.clear
9
9
  IRB.start
10
10
  end
11
+ task :c => :console
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.6.0
4
+ version: 0.7.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-07-22 00:00:00.000000000 Z
11
+ date: 2017-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel
@@ -90,6 +90,8 @@ files:
90
90
  - LICENSE.txt
91
91
  - README.md
92
92
  - Rakefile
93
+ - appveyor.yml
94
+ - benchmarks/memory.rb
93
95
  - bin/console
94
96
  - bin/setup
95
97
  - examples/bash.rb
@@ -98,17 +100,19 @@ files:
98
100
  - examples/env.rb
99
101
  - examples/logger.rb
100
102
  - examples/output.rb
103
+ - examples/pty.rb
101
104
  - examples/redirect_stderr.rb
102
105
  - examples/redirect_stdin.rb
103
106
  - examples/redirect_stdout.rb
104
107
  - examples/stdin_input.rb
108
+ - examples/threaded.rb
105
109
  - examples/timeout.rb
106
110
  - examples/wait.rb
107
111
  - lib/tty-command.rb
108
112
  - lib/tty/command.rb
113
+ - lib/tty/command/child_process.rb
109
114
  - lib/tty/command/cmd.rb
110
115
  - lib/tty/command/dry_runner.rb
111
- - lib/tty/command/execute.rb
112
116
  - lib/tty/command/exit_error.rb
113
117
  - lib/tty/command/printers/abstract.rb
114
118
  - lib/tty/command/printers/null.rb