rubysh 0.0.1
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/.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
|