rubysh 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -58,10 +58,18 @@ are arbitrary symbols):
58
58
  runner.data(:stdout) # "hi\n"
59
59
  runner.data(:stderr) # ""
60
60
 
61
- Support for controlled input isn't quite ready, but the syntax will be
62
- similar to the above. I want to support interactivity (so being able
63
- to write data, read some data, and then write more data), and haven't
64
- quite decided on the right API for this yet.
61
+ ## Controlled input
62
+
63
+ You can easily read and write data interactively:
64
+
65
+ >> runner = Rubysh('examples/dots.sh', Rubysh.>).run_async
66
+ => Rubysh::Runner: examples/dots.sh >:stdout (readers: :stdout, pid: 78296)
67
+ >> runner.read(:how => :partial) # block until some output available
68
+ => ".\n.\n.\n.\n.\n.\n.\n.\n.\n"
69
+ >> runner.read(:how => :nonblock)
70
+ => ""
71
+ >> runner.read # block until all output available
72
+ => [truncated]
65
73
 
66
74
  ## API
67
75
 
data/examples/dots.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+
3
+ for i in $(seq 1 100); do
4
+ echo .
5
+ sleep 1
6
+ done
@@ -9,6 +9,8 @@ module Rubysh
9
9
  case arg
10
10
  when BaseCommand, BaseDirective
11
11
  arg.stringify
12
+ when Hash
13
+ arg.inspect
12
14
  else
13
15
  Shellwords.shellescape(arg.to_s)
14
16
  end
@@ -26,12 +28,16 @@ module Rubysh
26
28
  Runner.new(self)
27
29
  end
28
30
 
29
- def run
30
- create_runner.run
31
+ def run(&blk)
32
+ create_runner.run(&blk)
31
33
  end
32
34
 
33
- def run_async
34
- create_runner.run_async
35
+ def run_async(&blk)
36
+ create_runner.run_async(&blk)
37
+ end
38
+
39
+ def check_call(&blk)
40
+ create_runner.check_call(&blk)
35
41
  end
36
42
 
37
43
  def |(other)
@@ -6,6 +6,7 @@ module Rubysh
6
6
  @raw_args = args
7
7
  @directives = []
8
8
  @args = nil
9
+ @opts = {}
9
10
 
10
11
  process_args
11
12
  end
@@ -18,6 +19,9 @@ module Rubysh
18
19
  when BaseDirective
19
20
  @directives << arg
20
21
  nil
22
+ when Hash
23
+ @opts.merge!(arg)
24
+ nil
21
25
  else
22
26
  arg.to_s
23
27
  end
@@ -87,6 +91,14 @@ module Rubysh
87
91
  state(runner)[:extra_directives] ||= []
88
92
  end
89
93
 
94
+ def base_post_forks
95
+ post_forks = []
96
+ if cwd = @opts[:cwd]
97
+ post_forks << Proc.new {Dir.chdir(cwd)}
98
+ end
99
+ post_forks
100
+ end
101
+
90
102
  def extra_post_forks(runner)
91
103
  state(runner)[:extra_post_forks] ||= []
92
104
  end
@@ -95,7 +107,7 @@ module Rubysh
95
107
  # extras first because they are currently only used for
96
108
  # pipeline, which should not win out over internal redirects.
97
109
  directives = extra_directives(runner) + @directives
98
- post_forks = extra_post_forks(runner)
110
+ post_forks = base_post_forks + extra_post_forks(runner)
99
111
  state(runner)[:subprocess] = Subprocess.new(args, directives, post_forks, runner)
100
112
  end
101
113
  end
data/lib/rubysh/error.rb CHANGED
@@ -16,5 +16,6 @@ module Rubysh
16
16
  class UnreachableError < BaseError; end
17
17
  class AlreadyClosedError < BaseError; end
18
18
  class AlreadyRunError < BaseError; end
19
+ class BadExitError < BaseError; end
19
20
  end
20
21
  end
@@ -1,3 +1,5 @@
1
+ require 'stringio'
2
+
1
3
  module Rubysh
2
4
  # Note that in bash, the semantics of redirection appear to be
3
5
  # following (tested empirically, rather than reading a spec):
@@ -119,7 +121,10 @@ module Rubysh
119
121
  :target_reading? => target_reading?,
120
122
  :target => target_reading? ? pipe.reader : pipe.writer,
121
123
  :complement => target_reading? ? pipe.writer : pipe.reader,
122
- :buffer => []
124
+ :buffer => StringIO.new,
125
+ :target_name => target_name,
126
+ :read_pos => 0,
127
+ :subprocess_fd_number => Util.to_fileno(source)
123
128
  }
124
129
  end
125
130
 
data/lib/rubysh/runner.rb CHANGED
@@ -14,10 +14,70 @@ module Rubysh
14
14
  prepare!
15
15
  end
16
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
17
+ def write(bytes, target=0)
18
+ raise Rubysh::Error::AlreadyRunError.new("Can only write to a runner in runner_state :started, not #{@runner_state.inspect}") unless @runner_state == :started
19
+ state = target_state(target, false)
20
+ target_name = state[:target_name]
21
+ @parallel_io.write(target_name, bytes, false)
22
+ end
23
+
24
+ # A bit of an unothordox read interface, not sure if I like
25
+ # it. Also, the target/opts magic is probably too magical (and not
26
+ # consistent with write!)
27
+ #
28
+ # You can do:
29
+ #
30
+ # read: finish the subprocess, and read from FD 1 in the child
31
+ # read(:how => :partial): wait until there are bytes on FD 1, and
32
+ # then return what you can
33
+ # read(2, :how => :partial): Do the same for FD 2
34
+ # read(:stdout, :how => :partial): Do the same with whatever the named
35
+ # descriptor :stdout
36
+ # read(:how => :nonblock): Return whatever is immediately available
37
+ def read(target=nil, opts=nil)
38
+ raise Rubysh::Error::AlreadyRunError.new("Can only read from a runner in runner_state :started or :waited, not #{@runner_state.inspect}") unless @runner_state == :started || @runner_state == :waited
39
+
40
+ if target.kind_of?(Hash)
41
+ opts = target
42
+ target = nil
43
+ end
44
+ target ||= 1
45
+ opts ||= {}
46
+
47
+ # TODO: add a stringio
48
+ state = target_state(target, true)
49
+ target_name = state[:target_name]
50
+
51
+ # Be nice to people and validate the hash
52
+ valid_keys = [:how]
53
+ extra_keys = opts.keys - valid_keys
54
+ raise raise Rubysh::Error::BaseError.new("Unrecognized keys #{extra_keys.inspect}. (Valid keys: #{valid_keys.inspect}") if extra_keys.length > 0
55
+
56
+ case how = opts[:how]
57
+ when :partial
58
+ # Read until we get some bytes
59
+ @parallel_io.run_once until state[:buffer].length != state[:read_pos]
60
+ when :nonblock
61
+ @parallel_io.read_available(state[:target])
62
+ when nil
63
+ communicate if @runner_state == :started
64
+ else
65
+ raise Rubysh::Error::BaseError.new("Invalid read directive #{how.inspect}")
66
+ end
67
+
68
+ state[:buffer].pos = state[:read_pos]
69
+ bytes = state[:buffer].read
70
+ # Could also increment by bytes, but meh.
71
+ state[:read_pos] = state[:buffer].pos
72
+ bytes
73
+ end
74
+
75
+ def communicate
76
+ raise Rubysh::Error::AlreadyRunError.new("Can only communicate with a runner in runner_state :started, not #{@runner_state.inspect}") unless @runner_state == :started
77
+ writers.each do |io, target_name|
78
+ @parallel_io.close(target_name) unless io.closed?
79
+ end
80
+ wait
21
81
  end
22
82
 
23
83
  # Ruby's Process::Status. Has fun things like pid and signaled?
@@ -40,6 +100,11 @@ module Rubysh
40
100
  end
41
101
  end
42
102
 
103
+ def exec_error
104
+ subprocess = state(@command)[:subprocess]
105
+ subprocess.exec_error
106
+ end
107
+
43
108
  # API for running/waiting
44
109
  def run_async
45
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
@@ -49,15 +114,22 @@ module Rubysh
49
114
  self
50
115
  end
51
116
 
52
- def wait
117
+ def run(input={}, &blk)
118
+ run_async
119
+ blk.call(self) if blk
53
120
  run_io
54
121
  do_wait
122
+ self
55
123
  end
56
124
 
57
- def run(input={})
58
- run_async
59
- run_io
60
- do_wait
125
+ def check_call(&blk)
126
+ run
127
+ status = full_status
128
+ unless status.success?
129
+ raise Rubysh::Error::BadExitError.new("#{@command} exited with #{rendered_status(status)}")
130
+ end
131
+ status
132
+ self
61
133
  end
62
134
 
63
135
  def readers
@@ -87,12 +159,12 @@ module Rubysh
87
159
  def inspect
88
160
  extras = []
89
161
  valid_readers = readers.values.map(&:inspect).join(', ')
90
- valid_writers = readers.values.map(&:inspect).join(', ')
162
+ valid_writers = writers.values.map(&:inspect).join(', ')
91
163
 
92
164
  extras << "readers: #{valid_readers}" if valid_readers.length > 0
93
165
  extras << "writers: #{valid_writers}" if valid_writers.length > 0
94
- if status = exitstatus
95
- extras << "exitstatus: #{status}"
166
+ if status = full_status
167
+ extras << rendered_status(status)
96
168
  elsif mypid = pid
97
169
  extras << "pid: #{pid}"
98
170
  end
@@ -107,21 +179,54 @@ module Rubysh
107
179
  end
108
180
 
109
181
  # 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})"))
182
+ def target_state(target_name, reading=nil)
183
+ case target_name
184
+ when Symbol
185
+ target_state = @targets[target_name]
186
+ raise Rubysh::Error::BaseError.new("Invalid target symbol: #{target_name.inspect} (valid target symbols are: #{@targets.keys.inspect})") unless target_state
187
+ when Fixnum
188
+ targets = targets_by_fd_numbers
189
+ target_state = targets[target_name]
190
+ raise Rubysh::Error::BaseError.new("Invalid target fd number: #{target_name.inspect} (valid target fd numbers are: #{targets.keys.inspect}})") unless target_state
191
+ else
192
+ raise Rubysh::Error::BaseError.new("Invalid type for target name: #{target_name.inspect} (#{target_name.class}). Valid types are Symbol and Fixnum.")
193
+ end
194
+
195
+ if reading.nil?
196
+ # No checking
197
+ elsif target_state[:target_reading?] && !reading
198
+ raise Rubysh::Error::BaseError.new("Trying to write to read pipe #{target_name}")
199
+ elsif !target_state[:target_reading?] && reading
200
+ raise Rubysh::Error::BaseError.new("Trying to read from write pipe #{target_name}")
201
+ end
202
+
203
+ target_state
112
204
  end
113
205
 
114
206
  private
115
207
 
208
+ def wait
209
+ run_io
210
+ do_wait
211
+ end
212
+
213
+ def targets_by_fd_numbers
214
+ @targets.inject({}) do |hash, (_, target_state)|
215
+ fd_num = target_state[:subprocess_fd_number]
216
+ hash[fd_num] = target_state
217
+ hash
218
+ end
219
+ end
220
+
116
221
  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
222
+ return unless @runner_state == :parallel_io_ran
118
223
  @command.wait(self)
119
224
  @runner_state = :waited
120
225
  self
121
226
  end
122
227
 
123
228
  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
229
+ return unless @runner_state == :started
125
230
  @parallel_io.run
126
231
  @runner_state = :parallel_io_ran
127
232
  self
@@ -136,11 +241,16 @@ module Rubysh
136
241
  def prepare_io
137
242
  @parallel_io = Subprocess::ParallelIO.new(readers, writers)
138
243
  @parallel_io.on_read do |target_name, data|
244
+ state = @targets[target_name]
245
+ buffer = state[:buffer]
139
246
  if data == Subprocess::ParallelIO::EOF
140
247
  Rubysh.log.debug("EOF reached on #{target_name.inspect}")
248
+ buffer.close_write
141
249
  else
142
250
  Rubysh.log.debug("Just read #{data.inspect} on #{target_name.inspect}")
143
- @targets[target_name][:buffer] << data
251
+ # Seek to end
252
+ buffer.pos = buffer.length
253
+ buffer.write(data)
144
254
  end
145
255
  end
146
256
 
@@ -152,5 +262,16 @@ module Rubysh
152
262
  end
153
263
  end
154
264
  end
265
+
266
+ def rendered_status(status)
267
+ if exitstatus = status.exitstatus
268
+ "exitstatus: #{exitstatus}"
269
+ elsif termsig = status.termsig
270
+ name, _ = Signal.list.detect {|name, number| number == termsig}
271
+ "termsig: #{name} [signal number #{termsig}]"
272
+ else
273
+ ''
274
+ end
275
+ end
155
276
  end
156
277
  end
@@ -65,8 +65,6 @@ class Rubysh::Subprocess
65
65
  end
66
66
  end
67
67
 
68
- private
69
-
70
68
  def run_once
71
69
  potential_readers = available_readers
72
70
  potential_writers = available_writers
@@ -97,6 +95,8 @@ class Rubysh::Subprocess
97
95
  end
98
96
  end
99
97
 
98
+ private
99
+
100
100
  def finalize_reader(reader)
101
101
  @finished_readers.add(reader)
102
102
  issue_reader_callback(reader, EOF)
@@ -1,5 +1,17 @@
1
1
  class Rubysh::Subprocess
2
2
  class PipeWrapper
3
+ begin
4
+ require 'json'
5
+ SERIALIZER = JSON
6
+ rescue LoadError => e
7
+ if ENV['RUBYSH_ENABLE_YAML']
8
+ require 'yaml'
9
+ SERIALIZER = YAML
10
+ else
11
+ raise LoadError.new("Could not import JSON (#{e}). You should either run in an environment with rubygems and JSON, or you can set the RUBYSH_ENABLE_YAML environment variable to allow Rubysh to internally use YAML for communication rather than JSON. This is believed safe, but YAML-parsing of untrusted input is bad, so only do this if you really can't get JSON.")
12
+ end
13
+ end
14
+
3
15
  attr_accessor :reader, :writer
4
16
 
5
17
  def initialize(reader_cloexec=true, writer_cloexec=true)
@@ -36,18 +48,22 @@ class Rubysh::Subprocess
36
48
  end
37
49
  end
38
50
 
39
- def dump_yaml_and_close(msg)
51
+ def dump_json_and_close(msg)
52
+ dumped = SERIALIZER.dump(msg)
40
53
  begin
41
- YAML.dump(msg, @writer)
54
+ @writer.write(dumped)
42
55
  ensure
43
56
  @writer.close
44
57
  Rubysh.assert(@reader.closed?, "Reader should already be closed")
45
58
  end
46
59
  end
47
60
 
48
- def load_yaml_and_close
61
+ def load_json_and_close
62
+ contents = @reader.read
63
+ return false if contents.length == 0
64
+
49
65
  begin
50
- YAML.load(@reader)
66
+ SERIALIZER.load(contents)
51
67
  rescue ArgumentError => e
52
68
  # e.g. ArgumentError: syntax error on line 0, col 2: `' (could
53
69
  # happen if the subprocess was killed while writing a message)
@@ -3,13 +3,6 @@ require 'fcntl'
3
3
  require 'timeout'
4
4
  require 'thread'
5
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
6
  require 'rubysh/subprocess/parallel_io'
14
7
  require 'rubysh/subprocess/pipe_wrapper'
15
8
 
@@ -131,7 +124,7 @@ module Rubysh
131
124
  # TODO: this may need coercion in Ruby1.9
132
125
  'caller' => e.send(:caller)
133
126
  }
134
- @exec_status.dump_yaml_and_close(msg)
127
+ @exec_status.dump_json_and_close(msg)
135
128
  # Note: atexit handlers will fire in this case. May want to do
136
129
  # something about that.
137
130
  raise
@@ -139,7 +132,7 @@ module Rubysh
139
132
  end
140
133
 
141
134
  def handle_exec_error
142
- msg = @exec_status.load_yaml_and_close
135
+ msg = @exec_status.load_json_and_close
143
136
 
144
137
  case msg
145
138
  when false
@@ -8,7 +8,7 @@ module Rubysh
8
8
  @fd = fd
9
9
  end
10
10
 
11
- def <(literal)
11
+ def <(literal=:stdin)
12
12
  TripleLessThan.new(@fd, literal)
13
13
  end
14
14
 
@@ -1,3 +1,3 @@
1
1
  module Rubysh
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
data/lib/rubysh.rb CHANGED
@@ -89,9 +89,14 @@ end
89
89
 
90
90
  module Rubysh
91
91
  # Convenience methods
92
- def self.run(command)
93
- command = Rubysh::Command.new(command) unless command.kind_of?(Rubysh::Command)
94
- command.run
92
+ def self.run(*args, &blk)
93
+ command = Rubysh::Command.new(args)
94
+ command.run(&blk)
95
+ end
96
+
97
+ def self.check_call(*args, &blk)
98
+ command = Rubysh::Command.new(*args)
99
+ command.check_call(&blk)
95
100
  end
96
101
 
97
102
  def self.Command(*args)
@@ -118,15 +123,15 @@ module Rubysh
118
123
  FD.new(2)
119
124
  end
120
125
 
121
- def self.>(target)
126
+ def self.>(target=:stdout)
122
127
  Redirect.new(1, '>', target)
123
128
  end
124
129
 
125
- def self.>>(target)
130
+ def self.>>(target=:stdout)
126
131
  Redirect.new(1, '>>', target)
127
132
  end
128
133
 
129
- def self.<(target)
134
+ def self.<(target=:stdin)
130
135
  Redirect.new(0, '<', target)
131
136
  end
132
137
 
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.3
4
+ version: 0.0.4
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: 2012-10-17 00:00:00.000000000 Z
12
+ date: 2013-02-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -55,6 +55,7 @@ files:
55
55
  - LICENSE
56
56
  - README.md
57
57
  - Rakefile
58
+ - examples/dots.sh
58
59
  - lib/rubysh.rb
59
60
  - lib/rubysh/base_command.rb
60
61
  - lib/rubysh/base_directive.rb
@@ -109,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
110
  version: '0'
110
111
  requirements: []
111
112
  rubyforge_project:
112
- rubygems_version: 1.8.24
113
+ rubygems_version: 1.8.23
113
114
  signing_key:
114
115
  specification_version: 3
115
116
  summary: ! 'Rubysh makes shelling out easy with a __sh__-like syntax layer for Ruby: irb