rubysh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/lib/rubysh/base_command.rb +65 -0
- data/lib/rubysh/base_directive.rb +24 -0
- data/lib/rubysh/command.rb +102 -0
- data/lib/rubysh/error.rb +20 -0
- data/lib/rubysh/fd.rb +43 -0
- data/lib/rubysh/pipe.rb +4 -0
- data/lib/rubysh/pipeline.rb +70 -0
- data/lib/rubysh/redirect.rb +181 -0
- data/lib/rubysh/runner.rb +156 -0
- data/lib/rubysh/subprocess/parallel_io.rb +184 -0
- data/lib/rubysh/subprocess/pipe_wrapper.rb +61 -0
- data/lib/rubysh/subprocess.rb +154 -0
- data/lib/rubysh/triple_less_than.rb +65 -0
- data/lib/rubysh/util.rb +55 -0
- data/lib/rubysh/version.rb +3 -0
- data/lib/rubysh.rb +149 -0
- data/rubysh.gemspec +26 -0
- data/test/_lib.rb +25 -0
- data/test/functional/_lib.rb +7 -0
- data/test/functional/lib/fd-lister +2 -0
- data/test/functional/lib/leaked_fds.rb +83 -0
- data/test/functional/lib/redirect_ordering.rb +15 -0
- data/test/functional/lib/triple_less_than.rb +16 -0
- data/test/integration/_lib.rb +7 -0
- data/test/integration/lib/rubysh.rb +6 -0
- data/test/rubysh +47 -0
- data/test/unit/_lib.rb +7 -0
- data/test/unit/lib/rubysh/command.rb +20 -0
- data/test/unit/lib/rubysh/pipeline.rb +108 -0
- data/test/unit/lib/rubysh/redirect.rb +44 -0
- data/test/unit/lib/rubysh/runner.rb +16 -0
- data/test/unit/lib/rubysh/subprocess/parallel_io.rb +233 -0
- data/test/unit/lib/rubysh/subprocess.rb +37 -0
- data/test/unit/lib/rubysh.rb +74 -0
- metadata +149 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
module Rubysh
|
2
|
+
class Runner
|
3
|
+
attr_accessor :command, :targets
|
4
|
+
|
5
|
+
def initialize(command)
|
6
|
+
@runner_state = :initialized
|
7
|
+
|
8
|
+
@command = command
|
9
|
+
@targets = {}
|
10
|
+
@state = {}
|
11
|
+
|
12
|
+
@parallel_io = nil
|
13
|
+
|
14
|
+
prepare!
|
15
|
+
end
|
16
|
+
|
17
|
+
def data(target_name)
|
18
|
+
state = target_state(target_name)
|
19
|
+
raise Rubysh::Error::BaseError.new("Can only access data for readable FDs") unless state[:target_reading?]
|
20
|
+
state[:buffer].join
|
21
|
+
end
|
22
|
+
|
23
|
+
# Ruby's Process::Status. Has fun things like pid and signaled?
|
24
|
+
def full_status(command=nil)
|
25
|
+
command ||= @command
|
26
|
+
@command.status(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def pid(command=nil)
|
30
|
+
command ||= @command
|
31
|
+
@command.pid(self)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convenience wrapper
|
35
|
+
def exitstatus(command=nil)
|
36
|
+
if st = full_status(command)
|
37
|
+
st.exitstatus
|
38
|
+
else
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# API for running/waiting
|
44
|
+
def run_async
|
45
|
+
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
|
46
|
+
prepare_io
|
47
|
+
@command.start_async(self)
|
48
|
+
@runner_state = :started
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def wait
|
53
|
+
run_io
|
54
|
+
do_wait
|
55
|
+
end
|
56
|
+
|
57
|
+
def run(input={})
|
58
|
+
run_async
|
59
|
+
run_io
|
60
|
+
do_wait
|
61
|
+
end
|
62
|
+
|
63
|
+
def readers
|
64
|
+
readers = {}
|
65
|
+
@targets.each do |target_name, target_state|
|
66
|
+
next unless target_state[:target_reading?]
|
67
|
+
target = target_state[:target]
|
68
|
+
readers[target] = target_name
|
69
|
+
end
|
70
|
+
readers
|
71
|
+
end
|
72
|
+
|
73
|
+
def writers
|
74
|
+
writers = {}
|
75
|
+
@targets.each do |target_name, target_state|
|
76
|
+
next if target_state[:target_reading?]
|
77
|
+
target = target_state[:target]
|
78
|
+
writers[target] = target_name
|
79
|
+
end
|
80
|
+
writers
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
inspect
|
85
|
+
end
|
86
|
+
|
87
|
+
def inspect
|
88
|
+
extras = []
|
89
|
+
valid_readers = readers.values.map(&:inspect).join(', ')
|
90
|
+
valid_writers = readers.values.map(&:inspect).join(', ')
|
91
|
+
|
92
|
+
extras << "readers: #{valid_readers}" if valid_readers.length > 0
|
93
|
+
extras << "writers: #{valid_writers}" if valid_writers.length > 0
|
94
|
+
if status = exitstatus
|
95
|
+
extras << "exitstatus: #{status}"
|
96
|
+
elsif mypid = pid
|
97
|
+
extras << "pid: #{pid}"
|
98
|
+
end
|
99
|
+
extra_display = extras.length > 0 ? " (#{extras.join(', ')})" : nil
|
100
|
+
|
101
|
+
"#{self.class}: #{command.stringify}#{extra_display}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Internal helpers
|
105
|
+
def state(object)
|
106
|
+
@state[object] ||= {}
|
107
|
+
end
|
108
|
+
|
109
|
+
# Internal helpers
|
110
|
+
def target_state(target_name)
|
111
|
+
@targets[target_name] || raise(Rubysh::Error::BaseError.new("Invalid target: #{target_name.inspect} (valid targets are: #{@targets.keys.inspect})"))
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def do_wait
|
117
|
+
raise Rubysh::Error::AlreadyRunError.new("You must run parallel io before waiting. (Perhaps you want to use the 'run' method, which takes care of the plumbing for you?)") unless @runner_state == :parallel_io_ran
|
118
|
+
@command.wait(self)
|
119
|
+
@runner_state = :waited
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
def run_io
|
124
|
+
raise Rubysh::Error::AlreadyRunError.new("You must start the subprocesses before running parallel io. (Perhaps you want to use the 'run' method, which takes care of the plumbing for you?)") unless @runner_state == :started
|
125
|
+
@parallel_io.run
|
126
|
+
@runner_state = :parallel_io_ran
|
127
|
+
self
|
128
|
+
end
|
129
|
+
|
130
|
+
def prepare!
|
131
|
+
@command.prepare!(self)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Can't build this in the prepare stage because pipes aren't built
|
135
|
+
# there.
|
136
|
+
def prepare_io
|
137
|
+
@parallel_io = Subprocess::ParallelIO.new(readers, writers)
|
138
|
+
@parallel_io.on_read do |target_name, data|
|
139
|
+
if data == Subprocess::ParallelIO::EOF
|
140
|
+
Rubysh.log.debug("EOF reached on #{target_name.inspect}")
|
141
|
+
else
|
142
|
+
Rubysh.log.debug("Just read #{data.inspect} on #{target_name.inspect}")
|
143
|
+
@targets[target_name][:buffer] << data
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
@parallel_io.on_write do |target_name, written, remaining|
|
148
|
+
if data == Subprocess::ParallelIO::EOF
|
149
|
+
Rubysh.log.debug("EOF reached on #{target_name.inspect}")
|
150
|
+
else
|
151
|
+
Rubysh.log.debug("Just wrote #{written.inspect} on #{target_name.inspect}")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
class Rubysh::Subprocess
|
2
|
+
class ParallelIO
|
3
|
+
module EOF; end
|
4
|
+
|
5
|
+
# readers/writers should be hashes mapping {fd => name}
|
6
|
+
def initialize(readers, writers)
|
7
|
+
@finished_readers = Set.new
|
8
|
+
@on_read = nil
|
9
|
+
@readers = readers
|
10
|
+
|
11
|
+
@writers = writers
|
12
|
+
@finished_writers = Set.new
|
13
|
+
@on_write = nil
|
14
|
+
@writer_buffers = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_read(&blk)
|
18
|
+
@on_read = blk
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_write(&blk)
|
22
|
+
@on_write = blk
|
23
|
+
end
|
24
|
+
|
25
|
+
def write(writer_name, data, close_on_complete=true)
|
26
|
+
writer = writer_by_name(writer_name)
|
27
|
+
buffer_state = @writer_buffers[writer] ||= {
|
28
|
+
:data => '',
|
29
|
+
:close_on_complete => nil
|
30
|
+
}
|
31
|
+
|
32
|
+
if buffer_state[:close_on_complete]
|
33
|
+
raise Rubysh::Error::AlreadyClosedError.new("You have already marked #{writer.inspect} as close_on_complete; can't write more data")
|
34
|
+
end
|
35
|
+
|
36
|
+
buffer_state[:close_on_complete] = close_on_complete
|
37
|
+
# XXX: unnecessary copy here
|
38
|
+
buffer_state[:data] += data
|
39
|
+
|
40
|
+
# Note that this leads to a bit of weird semantics if you try
|
41
|
+
# doing a write('') from within an on_write handler, since it'll
|
42
|
+
# call this synchronously. May want to change at some point.
|
43
|
+
finalize_writer_if_done(writer)
|
44
|
+
end
|
45
|
+
|
46
|
+
def close(writer_name)
|
47
|
+
writer = writer_by_name(writer_name)
|
48
|
+
writer.close
|
49
|
+
end
|
50
|
+
|
51
|
+
def available_readers
|
52
|
+
potential = @readers.keys - @finished_readers.to_a
|
53
|
+
potential.select {|reader| !reader.closed?}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Writers with a non-zero number of bytes remaining to write
|
57
|
+
def available_writers
|
58
|
+
potential = @writer_buffers.keys - @finished_writers.to_a
|
59
|
+
potential.select {|writer| !writer.closed? && get_data(writer).length > 0}
|
60
|
+
end
|
61
|
+
|
62
|
+
def run
|
63
|
+
while available_writers.length > 0 || available_readers.length > 0
|
64
|
+
run_once
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def run_once
|
71
|
+
potential_readers = available_readers
|
72
|
+
potential_writers = available_writers
|
73
|
+
|
74
|
+
begin
|
75
|
+
ready_readers, ready_writers, _ = IO.select(potential_readers, potential_writers)
|
76
|
+
rescue Errno::EINTR
|
77
|
+
next
|
78
|
+
end
|
79
|
+
|
80
|
+
ready_readers.each do |reader|
|
81
|
+
read_available(reader)
|
82
|
+
end
|
83
|
+
|
84
|
+
ready_writers.each do |writer|
|
85
|
+
write_available(writer)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def read_available(reader)
|
90
|
+
begin
|
91
|
+
data = reader.read_nonblock(4096)
|
92
|
+
rescue EOFError, Errno::EPIPE
|
93
|
+
finalize_reader(reader)
|
94
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR
|
95
|
+
else
|
96
|
+
issue_reader_callback(reader, data)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def finalize_reader(reader)
|
101
|
+
@finished_readers.add(reader)
|
102
|
+
issue_reader_callback(reader, EOF)
|
103
|
+
reader.close
|
104
|
+
end
|
105
|
+
|
106
|
+
def issue_reader_callback(reader, data)
|
107
|
+
if @on_read
|
108
|
+
name = reader_name(reader)
|
109
|
+
@on_read.call(name, data)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def reader_name(reader)
|
114
|
+
@readers.fetch(reader)
|
115
|
+
end
|
116
|
+
|
117
|
+
def write_available(writer)
|
118
|
+
data = get_data(writer)
|
119
|
+
begin
|
120
|
+
count = writer.write_nonblock(data)
|
121
|
+
rescue EOFError, Errno::EPIPE
|
122
|
+
finalize_writer(writer)
|
123
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::EINTR
|
124
|
+
else
|
125
|
+
# XXX: This may be a lot of copying. May want to think about
|
126
|
+
# how this scales.
|
127
|
+
written = data[0...count]
|
128
|
+
remaining = data[count..-1]
|
129
|
+
set_data(writer, remaining)
|
130
|
+
issue_writer_callback(writer, written, remaining)
|
131
|
+
end
|
132
|
+
finalize_writer_if_done(writer)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Will only schedule a writer if it has a nonzero number of bytes
|
136
|
+
# left to write, so we need to manually check if we're out after
|
137
|
+
# every run.
|
138
|
+
def finalize_writer_if_done(writer)
|
139
|
+
if !writer.closed? &&
|
140
|
+
buffer_state(writer)[:close_on_complete] &&
|
141
|
+
get_data(writer).length == 0
|
142
|
+
finalize_writer(writer)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def finalize_writer(writer)
|
147
|
+
# TODO: think about how we should deal with errors, maybe
|
148
|
+
remaining = get_data(writer)
|
149
|
+
@finished_writers.add(writer)
|
150
|
+
issue_writer_callback(writer, EOF, remaining)
|
151
|
+
writer.close if buffer_state(writer)[:close_on_complete]
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_data(writer)
|
155
|
+
buffer_state(writer)[:data]
|
156
|
+
end
|
157
|
+
|
158
|
+
def set_data(writer, data)
|
159
|
+
buffer_state(writer)[:data] = data
|
160
|
+
end
|
161
|
+
|
162
|
+
def buffer_state(writer)
|
163
|
+
buffer_state = @writer_buffers[writer]
|
164
|
+
Rubysh.assert(buffer_state, "No buffer state: #{writer.inspect}", true)
|
165
|
+
buffer_state
|
166
|
+
end
|
167
|
+
|
168
|
+
def issue_writer_callback(writer, data, remaining)
|
169
|
+
if @on_write
|
170
|
+
name = writer_name(writer)
|
171
|
+
@on_write.call(name, data, remaining)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def writer_name(writer)
|
176
|
+
@writers.fetch(writer)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Could make this fast, but don't think it matters enough.
|
180
|
+
def writer_by_name(writer_name)
|
181
|
+
@writers.detect {|writer, name| writer_name == name}.first
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Rubysh::Subprocess
|
2
|
+
class PipeWrapper
|
3
|
+
attr_accessor :reader, :writer
|
4
|
+
|
5
|
+
def initialize(reader_cloexec=true, writer_cloexec=true)
|
6
|
+
@reader, @writer = IO.pipe
|
7
|
+
set_reader_cloexec if reader_cloexec
|
8
|
+
set_writer_cloexec if writer_cloexec
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_only
|
12
|
+
@writer.close
|
13
|
+
end
|
14
|
+
|
15
|
+
def write_only
|
16
|
+
@reader.close
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
@writer.close
|
21
|
+
@reader.close
|
22
|
+
end
|
23
|
+
|
24
|
+
def set_reader_cloexec
|
25
|
+
@reader.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_writer_cloexec
|
29
|
+
@writer.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
|
30
|
+
end
|
31
|
+
|
32
|
+
def nonblock
|
33
|
+
[@reader, @writer].each do |fd|
|
34
|
+
fl = fd.fcntl(Fcntl::F_GETFL)
|
35
|
+
fd.fcntl(Fcntl::F_SETFL, fl | Fcntl::O_NONBLOCK)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump_yaml_and_close(msg)
|
40
|
+
begin
|
41
|
+
YAML.dump(msg, @writer)
|
42
|
+
ensure
|
43
|
+
@writer.close
|
44
|
+
Rubysh.assert(@reader.closed?, "Reader should already be closed")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def load_yaml_and_close
|
49
|
+
begin
|
50
|
+
YAML.load(@reader)
|
51
|
+
rescue ArgumentError => e
|
52
|
+
# e.g. ArgumentError: syntax error on line 0, col 2: `' (could
|
53
|
+
# happen if the subprocess was killed while writing a message)
|
54
|
+
raise Rubysh::Error::BaseError.new("Invalid message read from pipe: #{e}")
|
55
|
+
ensure
|
56
|
+
@reader.close
|
57
|
+
Rubysh.assert(@writer.closed?, "Writer should already be closed")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# Adapted from https://github.com/ahoward/open4
|
2
|
+
require 'fcntl'
|
3
|
+
require 'timeout'
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
# Using YAML to avoid the JSON dep. open4 uses Marshal to pass around
|
7
|
+
# the exception object, but I'm always a bit sketched by Marshal when
|
8
|
+
# it's not needed (i.e. don't want the subprocess to have the ability
|
9
|
+
# to execute code in the parent, even if it should lose that ability
|
10
|
+
# post-exec.)
|
11
|
+
require 'yaml'
|
12
|
+
|
13
|
+
require 'rubysh/subprocess/parallel_io'
|
14
|
+
require 'rubysh/subprocess/pipe_wrapper'
|
15
|
+
|
16
|
+
module Rubysh
|
17
|
+
class Subprocess
|
18
|
+
attr_accessor :command, :args, :directives, :runner
|
19
|
+
attr_accessor :pid, :status, :exec_error
|
20
|
+
|
21
|
+
# TODO: switch directives over to an OrderedHash of some form? Really
|
22
|
+
# want to preserve the semantics here.
|
23
|
+
def initialize(args, directives=[], post_fork=[], runner=nil)
|
24
|
+
raise ArgumentError.new("Must provide an array (#{args.inspect} provided)") unless args.kind_of?(Array)
|
25
|
+
raise ArgumentError.new("No command specified (#{args.inspect} provided)") unless args.length > 0
|
26
|
+
@command = args[0]
|
27
|
+
@args = args[1..-1]
|
28
|
+
@directives = directives
|
29
|
+
@runner = runner
|
30
|
+
|
31
|
+
Rubysh.assert(@directives.length == 0 || @runner, "Directives provided but no runner is", true)
|
32
|
+
|
33
|
+
@exec_status = nil
|
34
|
+
@post_fork = post_fork
|
35
|
+
|
36
|
+
@pid = nil
|
37
|
+
@status = nil
|
38
|
+
@exec_error = nil
|
39
|
+
|
40
|
+
Rubysh.log.debug("Just created: #{self}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s
|
44
|
+
"Subprocess: command=#{@command.inspect} args=#{@args.inspect} directives: #{@directives.inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def run
|
48
|
+
do_run unless @pid
|
49
|
+
@pid
|
50
|
+
end
|
51
|
+
|
52
|
+
def wait(nonblock=false)
|
53
|
+
do_wait(nonblock) unless @status
|
54
|
+
@status
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def do_run
|
60
|
+
# Create this here so as to not leave an open pipe hanging
|
61
|
+
# around for too long. Not sure what would happen if a child
|
62
|
+
# inherited it.
|
63
|
+
open_exec_status
|
64
|
+
@pid = fork do
|
65
|
+
do_run_child
|
66
|
+
end
|
67
|
+
do_run_parent
|
68
|
+
end
|
69
|
+
|
70
|
+
def open_exec_status
|
71
|
+
@exec_status = PipeWrapper.new
|
72
|
+
end
|
73
|
+
|
74
|
+
def do_run_parent
|
75
|
+
# nil in tests
|
76
|
+
@exec_status.read_only
|
77
|
+
apply_directives_parent
|
78
|
+
handle_exec_error
|
79
|
+
end
|
80
|
+
|
81
|
+
def do_wait(nonblock=false)
|
82
|
+
flags = nonblock ? Process::WNOHANG : nil
|
83
|
+
return nil unless result = Process.waitpid2(@pid, flags)
|
84
|
+
|
85
|
+
pid, @status = result
|
86
|
+
Rubysh.assert(pid == @pid,
|
87
|
+
"Process.waitpid2 returned #{pid} while waiting for #{@pid}",
|
88
|
+
true)
|
89
|
+
end
|
90
|
+
|
91
|
+
def do_run_child
|
92
|
+
# nil in tests
|
93
|
+
@exec_status.write_only
|
94
|
+
run_post_fork
|
95
|
+
apply_directives_child
|
96
|
+
exec_program
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_post_fork
|
100
|
+
@post_fork.each {|blk| blk.call}
|
101
|
+
end
|
102
|
+
|
103
|
+
def apply_directives_parent
|
104
|
+
apply_directives(true)
|
105
|
+
end
|
106
|
+
|
107
|
+
def apply_directives_child
|
108
|
+
apply_directives(false)
|
109
|
+
end
|
110
|
+
|
111
|
+
def apply_directives(is_parent)
|
112
|
+
@directives.each {|directive| apply_directive(directive, is_parent)}
|
113
|
+
end
|
114
|
+
|
115
|
+
def apply_directive(directive, is_parent)
|
116
|
+
if is_parent
|
117
|
+
directive.apply_parent!(runner)
|
118
|
+
else
|
119
|
+
directive.apply!(runner)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def exec_program
|
124
|
+
begin
|
125
|
+
Kernel.exec([command, command], *args)
|
126
|
+
raise Rubysh::Error::UnreachableError.new("This code should be unreachable. If you are seeing this exception, it means someone overrode Kernel.exec. That's not very nice of them.")
|
127
|
+
rescue Exception => e
|
128
|
+
msg = {
|
129
|
+
'message' => e.message,
|
130
|
+
'klass' => e.class.to_s,
|
131
|
+
# TODO: this may need coercion in Ruby1.9
|
132
|
+
'caller' => e.send(:caller)
|
133
|
+
}
|
134
|
+
@exec_status.dump_yaml_and_close(msg)
|
135
|
+
# Note: atexit handlers will fire in this case. May want to do
|
136
|
+
# something about that.
|
137
|
+
raise
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def handle_exec_error
|
142
|
+
msg = @exec_status.load_yaml_and_close
|
143
|
+
|
144
|
+
case msg
|
145
|
+
when false
|
146
|
+
# success!
|
147
|
+
when Hash
|
148
|
+
@exec_error = Rubysh::Error::ExecError.new("Failed to exec in subprocess: #{msg['message']}", msg['klass'], msg['caller'])
|
149
|
+
else
|
150
|
+
@exec_error = Rubysh::Error::BaseError.new("Invalid message received over the exec_status pipe: #{msg.inspect}")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Rubysh
|
4
|
+
# Looks like bash always buffers <<< to disk
|
5
|
+
class TripleLessThan < BaseDirective
|
6
|
+
class Shell < BaseDirective
|
7
|
+
def initialize(fd)
|
8
|
+
@fd = fd
|
9
|
+
end
|
10
|
+
|
11
|
+
def <(literal)
|
12
|
+
TripleLessThan.new(@fd, literal)
|
13
|
+
end
|
14
|
+
|
15
|
+
def prepare!
|
16
|
+
raise Rubysh::Error::BaseError.new("You have an incorrect usage of <<<, leading to a #{self.class} instance hanging around. Use it as either: Rubysh.<<< 'my string' or Rubysh::FD(3).<<< 'my string'.")
|
17
|
+
end
|
18
|
+
|
19
|
+
def stringify
|
20
|
+
" << #{fd.stringify} (INVALID SYNTAX)"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# TODO: support in-place strings
|
25
|
+
def initialize(fd, literal)
|
26
|
+
@fd = fd
|
27
|
+
@literal = literal
|
28
|
+
end
|
29
|
+
|
30
|
+
def prepare!(runner)
|
31
|
+
tempfile = Tempfile.new('buffer')
|
32
|
+
tempfile.delete
|
33
|
+
tempfile.write(@literal)
|
34
|
+
tempfile.flush
|
35
|
+
tempfile.rewind
|
36
|
+
|
37
|
+
Util.set_cloexec(tempfile)
|
38
|
+
|
39
|
+
state = state(runner)
|
40
|
+
state[:tempfile] = tempfile
|
41
|
+
state[:redirect] = Redirect.new(@fd, '<', tempfile)
|
42
|
+
end
|
43
|
+
|
44
|
+
def stringify
|
45
|
+
fd = Util.to_fileno(@fd)
|
46
|
+
beginning = fd == 0 ? '' : fd.to_s
|
47
|
+
"#{beginning}<<< #{@literal.inspect}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
"TripleLessThan: #{stringify}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def apply_parent!(runner)
|
55
|
+
state = state(runner)
|
56
|
+
state[:tempfile].close
|
57
|
+
state[:redirect].apply_parent!(runner)
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply!(runner)
|
61
|
+
state = state(runner)
|
62
|
+
state[:redirect].apply!(runner)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/rubysh/util.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Rubysh
|
4
|
+
module Util
|
5
|
+
def self.to_fileno(file)
|
6
|
+
if file.respond_to?(:fileno)
|
7
|
+
file.fileno
|
8
|
+
else
|
9
|
+
file
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Leaks memory (needed to avoid Ruby 1.8's IO autoclose behavior),
|
14
|
+
# and so you should only use it right before execing.
|
15
|
+
def self.io_without_autoclose(fd_num)
|
16
|
+
fd_num = to_fileno(fd_num)
|
17
|
+
io = IO.new(fd_num)
|
18
|
+
hold(io)
|
19
|
+
io
|
20
|
+
end
|
21
|
+
|
22
|
+
# Should really just shell out to dup2, but looks like we'd need a
|
23
|
+
# C extension to do so. The concurrency story here is a bit off,
|
24
|
+
# and this probably doesn't copy over all FD state
|
25
|
+
# properly. Should be fine for now.
|
26
|
+
def self.dup2(fildes, fildes2)
|
27
|
+
original = io_without_autoclose(fildes)
|
28
|
+
|
29
|
+
begin
|
30
|
+
copy = io_without_autoclose(fildes2)
|
31
|
+
rescue Errno::EBADF
|
32
|
+
else
|
33
|
+
copy.close
|
34
|
+
end
|
35
|
+
|
36
|
+
res = original.fcntl(Fcntl::F_DUPFD, fildes2)
|
37
|
+
Rubysh.assert(res == fildes2, "Tried to open #{fildes2} but ended up with #{res} instead", true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.set_cloexec(file, enable=true)
|
41
|
+
file = io_without_autoclose(file) unless file.kind_of?(IO)
|
42
|
+
value = enable ? Fcntl::FD_CLOEXEC : 0
|
43
|
+
file.fcntl(Fcntl::F_SETFD, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
@references = []
|
49
|
+
def self.hold(*references)
|
50
|
+
# Needed for Ruby 1.8, where we can't set IO objects to not
|
51
|
+
# close the underlying FD on destruction
|
52
|
+
@references += references
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|