tty-command 0.6.0 → 0.7.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: 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