rubysh 0.0.4 → 0.0.5
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.
- data/lib/rubysh/error.rb +1 -0
- data/lib/rubysh/redirect.rb +4 -2
- data/lib/rubysh/runner.rb +13 -2
- data/lib/rubysh/subprocess/parallel_io.rb +21 -4
- data/lib/rubysh/subprocess/pid_aware_parallel_io.rb +103 -0
- data/lib/rubysh/subprocess.rb +8 -1
- data/lib/rubysh/triple_less_than.rb +4 -1
- data/lib/rubysh/version.rb +1 -1
- data/lib/rubysh.rb +39 -9
- metadata +3 -2
data/lib/rubysh/error.rb
CHANGED
data/lib/rubysh/redirect.rb
CHANGED
@@ -15,7 +15,7 @@ module Rubysh
|
|
15
15
|
|
16
16
|
attr_accessor :source, :direction, :target
|
17
17
|
|
18
|
-
def initialize(source, direction, target)
|
18
|
+
def initialize(source, direction, target, opts=nil)
|
19
19
|
unless VALID_DIRECTIONS.include?(direction)
|
20
20
|
raise Rubysh::Error::BaseError.new("Direction must be one of #{VALID_DIRECTIONS.join(', ')}, not #{direction.inspect}")
|
21
21
|
end
|
@@ -31,6 +31,7 @@ module Rubysh
|
|
31
31
|
@source = source
|
32
32
|
@target = target
|
33
33
|
@direction = direction
|
34
|
+
@opts = opts || {}
|
34
35
|
end
|
35
36
|
|
36
37
|
def printable_source
|
@@ -124,7 +125,8 @@ module Rubysh
|
|
124
125
|
:buffer => StringIO.new,
|
125
126
|
:target_name => target_name,
|
126
127
|
:read_pos => 0,
|
127
|
-
:subprocess_fd_number => Util.to_fileno(source)
|
128
|
+
:subprocess_fd_number => Util.to_fileno(source),
|
129
|
+
:tee => @opts[:tee]
|
128
130
|
}
|
129
131
|
end
|
130
132
|
|
data/lib/rubysh/runner.rb
CHANGED
@@ -108,8 +108,8 @@ module Rubysh
|
|
108
108
|
# API for running/waiting
|
109
109
|
def run_async
|
110
110
|
raise Rubysh::Error::AlreadyRunError.new("You have already run this #{self.class} instance. Cannot run again. You can run its command directly though, which will create a fresh #{self.class} instance.") unless @runner_state == :initialized
|
111
|
-
prepare_io
|
112
111
|
@command.start_async(self)
|
112
|
+
prepare_io
|
113
113
|
@runner_state = :started
|
114
114
|
self
|
115
115
|
end
|
@@ -218,6 +218,14 @@ module Rubysh
|
|
218
218
|
end
|
219
219
|
end
|
220
220
|
|
221
|
+
def subprocesses
|
222
|
+
# Not everything with state has a subprocess
|
223
|
+
@state.map do |object, target|
|
224
|
+
next unless object.kind_of?(BaseCommand)
|
225
|
+
target.fetch(:subprocess)
|
226
|
+
end.compact
|
227
|
+
end
|
228
|
+
|
221
229
|
def do_wait
|
222
230
|
return unless @runner_state == :parallel_io_ran
|
223
231
|
@command.wait(self)
|
@@ -239,7 +247,7 @@ module Rubysh
|
|
239
247
|
# Can't build this in the prepare stage because pipes aren't built
|
240
248
|
# there.
|
241
249
|
def prepare_io
|
242
|
-
@parallel_io = Subprocess::
|
250
|
+
@parallel_io = Subprocess::PidAwareParallelIO.new(readers, writers, subprocesses)
|
243
251
|
@parallel_io.on_read do |target_name, data|
|
244
252
|
state = @targets[target_name]
|
245
253
|
buffer = state[:buffer]
|
@@ -248,6 +256,9 @@ module Rubysh
|
|
248
256
|
buffer.close_write
|
249
257
|
else
|
250
258
|
Rubysh.log.debug("Just read #{data.inspect} on #{target_name.inspect}")
|
259
|
+
tee = state[:tee]
|
260
|
+
tee.write(data) if tee
|
261
|
+
|
251
262
|
# Seek to end
|
252
263
|
buffer.pos = buffer.length
|
253
264
|
buffer.write(data)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class Rubysh::Subprocess
|
2
2
|
class ParallelIO
|
3
3
|
module EOF; end
|
4
|
+
class NothingAvailable < StandardError; end
|
4
5
|
|
5
6
|
# readers/writers should be hashes mapping {fd => name}
|
6
7
|
def initialize(readers, writers)
|
@@ -65,16 +66,25 @@ class Rubysh::Subprocess
|
|
65
66
|
end
|
66
67
|
end
|
67
68
|
|
68
|
-
|
69
|
+
# This method is a stub so it can be extended in subclasses
|
70
|
+
def run_once(timeout=nil)
|
71
|
+
run_select_loop(timeout)
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_select_loop(timeout)
|
69
75
|
potential_readers = available_readers
|
70
76
|
potential_writers = available_writers
|
71
77
|
|
72
78
|
begin
|
73
|
-
|
79
|
+
selected = IO.select(potential_readers, potential_writers, nil, timeout)
|
74
80
|
rescue Errno::EINTR
|
75
81
|
retry
|
82
|
+
else
|
83
|
+
raise NothingAvailable unless selected
|
76
84
|
end
|
77
85
|
|
86
|
+
ready_readers, ready_writers, _ = selected
|
87
|
+
|
78
88
|
ready_readers.each do |reader|
|
79
89
|
read_available(reader)
|
80
90
|
end
|
@@ -84,6 +94,13 @@ class Rubysh::Subprocess
|
|
84
94
|
end
|
85
95
|
end
|
86
96
|
|
97
|
+
def consume_all_available
|
98
|
+
begin
|
99
|
+
loop {run_select_loop(0)}
|
100
|
+
rescue NothingAvailable
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
87
104
|
def read_available(reader)
|
88
105
|
begin
|
89
106
|
data = reader.read_nonblock(4096)
|
@@ -106,7 +123,7 @@ class Rubysh::Subprocess
|
|
106
123
|
def issue_reader_callback(reader, data)
|
107
124
|
if @on_read
|
108
125
|
name = reader_name(reader)
|
109
|
-
@on_read.call(name, data)
|
126
|
+
@on_read.call(name, data) if name
|
110
127
|
end
|
111
128
|
end
|
112
129
|
|
@@ -168,7 +185,7 @@ class Rubysh::Subprocess
|
|
168
185
|
def issue_writer_callback(writer, data, remaining)
|
169
186
|
if @on_write
|
170
187
|
name = writer_name(writer)
|
171
|
-
@on_write.call(name, data, remaining)
|
188
|
+
@on_write.call(name, data, remaining) if name
|
172
189
|
end
|
173
190
|
end
|
174
191
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
class Rubysh::Subprocess
|
5
|
+
# We can't actually rely on an EOF once our subprocess has died,
|
6
|
+
# since it may have forked and a child inherited the parent's
|
7
|
+
# fds. (This happens, for example, when using SSH's ControlPersist.)
|
8
|
+
#
|
9
|
+
# E.g. try Rubysh.run('ruby', 'bad.rb', Rubysh.>) with:
|
10
|
+
# # cat bad.rb
|
11
|
+
# fork {sleep 1000}
|
12
|
+
class PidAwareParallelIO < ParallelIO
|
13
|
+
@pids_mutex = Mutex.new
|
14
|
+
@parallel_ios = {}
|
15
|
+
@old_sigchld_handler = nil
|
16
|
+
|
17
|
+
def self.register_parallel_io(parallel_io, breaker_writer)
|
18
|
+
@pids_mutex.synchronize do
|
19
|
+
register_sigchld_handler if @parallel_ios.length == 0
|
20
|
+
@parallel_ios[parallel_io] = breaker_writer
|
21
|
+
|
22
|
+
# This is needed in case the SIGCHLD is handled before the
|
23
|
+
# writer is stored.
|
24
|
+
trigger_breaker(breaker_writer)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.deregister_parallel_io(parallel_io)
|
29
|
+
@pids_mutex.synchronize do
|
30
|
+
@parallel_ios.delete(parallel_io)
|
31
|
+
deregister_sigchld_handler if @parallel_ios.length == 0
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.handle_sigchld
|
36
|
+
# It's ok for this operation to race against other
|
37
|
+
# threads. Break loop on all currently active selectors. This
|
38
|
+
# could in theory cause a thundering herd, but it's probably not
|
39
|
+
# worth the work to defend against.
|
40
|
+
@parallel_ios.values.each {|writer| trigger_breaker(writer)}
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.trigger_breaker(writer)
|
44
|
+
begin
|
45
|
+
writer.write_nonblock('a') unless writer.closed?
|
46
|
+
rescue Errno::EAGAIN, Errno::EPIPE
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.register_sigchld_handler
|
51
|
+
@old_sigchld_handler = Signal.trap('CHLD') {handle_sigchld}
|
52
|
+
# MRI returns nil for a DEFAULT handler, but it also treats nil
|
53
|
+
# as IGNORE.
|
54
|
+
@old_sigchld_handler ||= 'DEFAULT'
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.deregister_sigchld_handler
|
58
|
+
Signal.trap('CHLD', @old_sigchld_handler)
|
59
|
+
end
|
60
|
+
|
61
|
+
# readers/writers should be hashes mapping {fd => name}
|
62
|
+
def initialize(readers, writers, subprocesses)
|
63
|
+
@breaker_reader, @breaker_writer = IO.pipe
|
64
|
+
@subprocesses = subprocesses
|
65
|
+
|
66
|
+
readers = readers.dup
|
67
|
+
readers[@breaker_reader] = nil
|
68
|
+
super(readers, writers)
|
69
|
+
|
70
|
+
register_subprocesses
|
71
|
+
end
|
72
|
+
|
73
|
+
def register_subprocesses
|
74
|
+
self.class.register_parallel_io(self, @breaker_writer)
|
75
|
+
end
|
76
|
+
|
77
|
+
def run_once(timeout=nil)
|
78
|
+
@subprocesses.each do |subprocess|
|
79
|
+
subprocess.wait(true)
|
80
|
+
end
|
81
|
+
|
82
|
+
# All subprocesses have exited! We're done here.
|
83
|
+
if @subprocesses.all?(&:status)
|
84
|
+
finalize_all
|
85
|
+
return
|
86
|
+
end
|
87
|
+
|
88
|
+
super
|
89
|
+
end
|
90
|
+
|
91
|
+
def finalize_all
|
92
|
+
@breaker_writer.close
|
93
|
+
|
94
|
+
# We're guaranteed that if a process exited, all of its bytes
|
95
|
+
# are immediately available to us.
|
96
|
+
consume_all_available
|
97
|
+
|
98
|
+
available_readers.each {|reader| reader.close}
|
99
|
+
available_writers.each {|writer| writer.close}
|
100
|
+
self.class.deregister_parallel_io(self)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/rubysh/subprocess.rb
CHANGED
@@ -4,6 +4,7 @@ require 'timeout'
|
|
4
4
|
require 'thread'
|
5
5
|
|
6
6
|
require 'rubysh/subprocess/parallel_io'
|
7
|
+
require 'rubysh/subprocess/pid_aware_parallel_io'
|
7
8
|
require 'rubysh/subprocess/pipe_wrapper'
|
8
9
|
|
9
10
|
module Rubysh
|
@@ -73,7 +74,13 @@ module Rubysh
|
|
73
74
|
|
74
75
|
def do_wait(nonblock=false)
|
75
76
|
flags = nonblock ? Process::WNOHANG : nil
|
76
|
-
|
77
|
+
begin
|
78
|
+
result = Process.waitpid2(@pid, flags)
|
79
|
+
rescue Errno::ECHILD => e
|
80
|
+
raise Rubysh::Error::ECHILDError.new("No unreaped process #{@pid}. This could indicate a bug in Rubysh, but more likely means you have something in your codebase which is wait(2)ing on subprocesses.")
|
81
|
+
end
|
82
|
+
|
83
|
+
return unless result
|
77
84
|
|
78
85
|
pid, @status = result
|
79
86
|
Rubysh.assert(pid == @pid,
|
@@ -3,9 +3,12 @@ require 'tempfile'
|
|
3
3
|
module Rubysh
|
4
4
|
# Looks like bash always buffers <<< to disk
|
5
5
|
class TripleLessThan < BaseDirective
|
6
|
+
attr_reader :fd, :literal
|
7
|
+
|
6
8
|
class Shell < BaseDirective
|
7
|
-
def initialize(fd)
|
9
|
+
def initialize(fd, opts)
|
8
10
|
@fd = fd
|
11
|
+
@opts = opts
|
9
12
|
end
|
10
13
|
|
11
14
|
def <(literal=:stdin)
|
data/lib/rubysh/version.rb
CHANGED
data/lib/rubysh.rb
CHANGED
@@ -94,8 +94,13 @@ module Rubysh
|
|
94
94
|
command.run(&blk)
|
95
95
|
end
|
96
96
|
|
97
|
+
def self.run_async(*args, &blk)
|
98
|
+
command = Rubysh::Command.new(args)
|
99
|
+
command.run_async(&blk)
|
100
|
+
end
|
101
|
+
|
97
102
|
def self.check_call(*args, &blk)
|
98
|
-
command = Rubysh::Command.new(
|
103
|
+
command = Rubysh::Command.new(args)
|
99
104
|
command.check_call(&blk)
|
100
105
|
end
|
101
106
|
|
@@ -123,22 +128,47 @@ module Rubysh
|
|
123
128
|
FD.new(2)
|
124
129
|
end
|
125
130
|
|
126
|
-
def self.>(target
|
127
|
-
|
131
|
+
def self.>(target=nil, opts=nil)
|
132
|
+
# Might want to DRY this logic up at some point. Right now seems
|
133
|
+
# like it'd just sacrifice clarity though.
|
134
|
+
if !opts && target.kind_of?(Hash)
|
135
|
+
opts = target
|
136
|
+
target = nil
|
137
|
+
end
|
138
|
+
target = :stdout
|
139
|
+
|
140
|
+
Redirect.new(1, '>', target, opts)
|
128
141
|
end
|
129
142
|
|
130
|
-
def self.>>(target
|
131
|
-
|
143
|
+
def self.>>(target=nil, opts=nil)
|
144
|
+
if !opts && target.kind_of?(Hash)
|
145
|
+
opts = target
|
146
|
+
target = nil
|
147
|
+
end
|
148
|
+
target = :stdout
|
149
|
+
|
150
|
+
Redirect.new(1, '>>', target, opts)
|
132
151
|
end
|
133
152
|
|
134
|
-
def self.<(target
|
135
|
-
|
153
|
+
def self.<(target=nil, opts=nil)
|
154
|
+
if !opts && target.kind_of?(Hash)
|
155
|
+
opts = target
|
156
|
+
target = nil
|
157
|
+
end
|
158
|
+
target = :stdin
|
159
|
+
|
160
|
+
Redirect.new(0, '<', target, opts)
|
136
161
|
end
|
137
162
|
|
138
163
|
# Hack to implement <<<
|
139
|
-
def self.<<(fd=nil)
|
164
|
+
def self.<<(fd=nil, opts=nil)
|
165
|
+
if !opts && fd.kind_of?(Hash)
|
166
|
+
opts = fd
|
167
|
+
fd = nil
|
168
|
+
end
|
169
|
+
|
140
170
|
fd ||= FD.new(0)
|
141
|
-
TripleLessThan::Shell.new(fd)
|
171
|
+
TripleLessThan::Shell.new(fd, opts)
|
142
172
|
end
|
143
173
|
|
144
174
|
# Internal utility methods
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubysh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-08-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -68,6 +68,7 @@ files:
|
|
68
68
|
- lib/rubysh/runner.rb
|
69
69
|
- lib/rubysh/subprocess.rb
|
70
70
|
- lib/rubysh/subprocess/parallel_io.rb
|
71
|
+
- lib/rubysh/subprocess/pid_aware_parallel_io.rb
|
71
72
|
- lib/rubysh/subprocess/pipe_wrapper.rb
|
72
73
|
- lib/rubysh/triple_less_than.rb
|
73
74
|
- lib/rubysh/util.rb
|