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 +4 -4
- data/.rspec +1 -0
- data/.travis.yml +1 -1
- data/CHANGELOG.md +17 -0
- data/README.md +88 -31
- data/examples/basic.rb +1 -1
- data/examples/cli +4 -0
- data/examples/output.rb +10 -0
- data/examples/redirect_stdin.rb +3 -1
- data/examples/stdin_input.rb +10 -0
- data/examples/timeout.rb +1 -1
- data/examples/wait.rb +21 -0
- data/lib/tty/command.rb +38 -17
- data/lib/tty/command/dry_runner.rb +8 -2
- data/lib/tty/command/execute.rb +10 -8
- data/lib/tty/command/exit_error.rb +2 -2
- data/lib/tty/command/printers/quiet.rb +1 -1
- data/lib/tty/command/process_runner.rb +107 -41
- data/lib/tty/command/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9086c13127af2f7d61b0577426a34ea6e71929c
|
4
|
+
data.tar.gz: fbb089e7cc7d6937571ab84c3700856bb6f7eac7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3eeb7cc3fc245a9bc126ee2948c9d02ebae2e88df4f83a4c3ead72e4ee8b1597ac087b039448f843ab4f305575957dbde65f86ff7813101678a801aabc3f37a3
|
7
|
+
data.tar.gz: 028e2be35a74088e0015bb60577a4cbdf462b31e727221aa7e3d72a7c2530c68f47bdff8ed1593e55d1af249f1e006d6078d15d55dbbc59d839fc35a5fac4a6b
|
data/.rspec
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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.
|
50
|
-
|
51
|
-
|
52
|
-
* [2.
|
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.
|
61
|
-
* [3.2.6.
|
62
|
-
* [3.2.7.
|
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
|
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:
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
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.
|
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.
|
373
|
+
#### 3.2.8 Umask
|
317
374
|
|
318
375
|
To run command with umask do:
|
319
376
|
|
data/examples/basic.rb
CHANGED
data/examples/cli
ADDED
data/examples/output.rb
ADDED
data/examples/redirect_stdin.rb
CHANGED
@@ -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(
|
14
|
+
out, _ = cmd.run(cli, :in => stdin)
|
13
15
|
|
14
16
|
puts "#{out}"
|
data/examples/timeout.rb
CHANGED
data/examples/wait.rb
ADDED
@@ -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/)
|
data/lib/tty/command.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
106
|
-
|
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
|
152
|
-
mutex.synchronize { @runner.run(
|
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
|
-
|
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(
|
203
|
+
def select_runner(dry_run)
|
183
204
|
if dry_run
|
184
|
-
DryRunner
|
205
|
+
DryRunner
|
185
206
|
else
|
186
|
-
ProcessRunner
|
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
|
-
|
8
|
+
attr_reader :cmd
|
9
|
+
|
10
|
+
def initialize(cmd, printer)
|
11
|
+
@cmd = cmd
|
9
12
|
@printer = printer
|
10
13
|
end
|
11
14
|
|
12
|
-
|
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)
|
data/lib/tty/command/execute.rb
CHANGED
@@ -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
|
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
|
-
|
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,
|
32
|
-
:out => out_wr
|
33
|
-
:err => err_wr
|
34
|
-
}).merge(
|
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
|
@@ -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(
|
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
|
-
|
35
|
-
|
36
|
-
|
42
|
+
# write and read streams
|
43
|
+
write_stream(stdin)
|
44
|
+
stdout_data, stderr_data = read_streams(stdout, stderr, &block)
|
37
45
|
|
38
|
-
|
46
|
+
status = waitpid(pid)
|
47
|
+
runtime = Time.now - start
|
39
48
|
|
40
|
-
|
41
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
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 =
|
76
|
-
|
77
|
-
|
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
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
data/lib/tty/command/version.rb
CHANGED
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
|
+
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-
|
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
|