buildbox 0.2.3 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 29832c2693fb640f1003b5a010eeb170a792f9c2
4
- data.tar.gz: dd13bc62a427512418a1bdaa7dabce4098c009dd
3
+ metadata.gz: 943e5240e1dc1d41d6086cdf88dadc990477f165
4
+ data.tar.gz: 7f03003bb7c71db8971a5c313325fc5461cca2dc
5
5
  SHA512:
6
- metadata.gz: bc853759496d74190b5b5edf806325693bace3870809c3e78ef868a2e8bc45f33cadb3f3feab5268ec54f0d76ccbf600d682fea035fa863284b253ee1c6f1ded
7
- data.tar.gz: 2d728c74a1571837bd7d3c93f85b49574bc65fb393435c8b61b69f2987012c13129de744df72415cf201e5d3520527b4eb1eeb223956305401dd3cb5ff0cbbe0
6
+ metadata.gz: 581a02441d7887327dd69584eb0c476b90dfcd01899e81f3da7bafe03b7d505ffbf4e3af385154d17394fe0db255eed477d098f41fe7ff3ae18ce502252fafce
7
+ data.tar.gz: c9fec973a2d1814c2342a719d14861cb0478ecdcabf7723c5e3085144430e1fea8fc35e42082b40071a723beadaaedc3413e28b297902920cc30bb4977621b1d
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- buildbox (0.2.3)
4
+ buildbox (0.3)
5
5
  celluloid (~> 0.14)
6
6
  childprocess (~> 0.3)
7
7
  faraday (~> 0.8)
data/bin/buildbox-pty CHANGED
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ # Disable stdout,stderr buffering
4
+ STDERR.sync = STDOUT.sync = true
5
+
3
6
  require 'pty'
4
7
 
5
8
  # spawn the process in a pseudo terminal so colors out outputted
data/lib/buildbox.rb CHANGED
@@ -8,6 +8,7 @@ module Buildbox
8
8
  autoload :CLI, "buildbox/cli"
9
9
  autoload :Configuration, "buildbox/configuration"
10
10
  autoload :Monitor, "buildbox/monitor"
11
+ autoload :Platform, "buildbox/platform"
11
12
  autoload :Runner, "buildbox/runner"
12
13
  autoload :Script, "buildbox/script"
13
14
  autoload :Server, "buildbox/server"
@@ -23,6 +24,10 @@ module Buildbox
23
24
  @logger ||= Logger.new(STDOUT).tap { |logger| logger.level = Logger::INFO }
24
25
  end
25
26
 
27
+ def self.logger=(logger)
28
+ @logger = logger
29
+ end
30
+
26
31
  def self.gem_root
27
32
  path = File.expand_path(File.join(__FILE__, "..", ".."))
28
33
  end
@@ -1,56 +1,146 @@
1
1
  require 'childprocess'
2
2
  require 'pty'
3
3
 
4
+ # Inspiration from:
5
+ # https://github.com/mitchellh/vagrant/blob/master/lib/vagrant/util/subprocess.rb
6
+
4
7
  module Buildbox
5
8
  class Command
6
- class Result < Struct.new(:output, :exit_status)
7
- end
9
+ # The chunk size for reading from subprocess IO.
10
+ READ_CHUNK_SIZE = 4096
8
11
 
9
- def self.command(command, options = {}, &block)
10
- new(command, options).start(&block)
11
- end
12
+ # An error which occurs when the process doesn't end within
13
+ # the given timeout.
14
+ class TimeoutExceeded < StandardError; end
15
+
16
+ attr_reader :output, :exit_status
12
17
 
13
- def self.script(script, options = {}, &block)
14
- new(script, options).start(&block)
18
+ def self.run(*args, &block)
19
+ options = args.last.is_a?(Hash) ? args.pop : {}
20
+ arguments = args.dup
21
+
22
+ # Run the command
23
+ command = new(arguments, options, &block)
24
+ command.start(&block)
25
+ command
15
26
  end
16
27
 
17
28
  def initialize(arguments, options = {})
18
- @arguments = arguments
19
- @environment = options[:environment] || {}
20
- @directory = options[:directory] || "."
29
+ @arguments = arguments
30
+ @options = options
31
+ @logger = Buildbox.logger
21
32
  end
22
33
 
23
34
  def start(&block)
24
- read_io, write_io = IO.pipe
35
+ # Get the timeout, if we have one
36
+ timeout = @options[:timeout]
37
+
38
+ # Build the command we're going to run
39
+ arguments = [ *runner, *@arguments ].compact.map(&:to_s) # all arguments must be a string
40
+
41
+ # Build the ChildProcess
42
+ @logger.info("Starting process: #{arguments}")
25
43
 
26
- arguments = [ *runner, *@arguments ].compact.map(&:to_s) # all arguments must be a string
27
- process = ChildProcess.build(*arguments)
28
- process.cwd = expanded_directory
29
- process.io.stdout = process.io.stderr = write_io
44
+ process = ChildProcess.build(*arguments)
45
+ process.cwd = File.expand_path(@options[:directory] || Dir.pwd)
30
46
 
31
- @environment.each_pair do |key, value|
32
- process.environment[key] = value
47
+ # Create the pipes so we can read the output in real tim
48
+ read_pipe, write_pipe = IO.pipe
49
+ process.io.stdout = write_pipe
50
+ process.io.stderr = write_pipe
51
+ process.duplex = true
52
+
53
+ # Set the environment on the process
54
+ if @options[:environment]
55
+ @options[:environment].each_pair do |key, value|
56
+ process.environment[key] = value
57
+ end
33
58
  end
34
59
 
60
+ # Start the process
35
61
  process.start
36
- write_io.close
37
62
 
63
+ # Make sure the stdin does not buffer
64
+ process.io.stdin.sync = true
65
+
66
+ if RUBY_PLATFORM != "java"
67
+ # On Java, we have to close after. See down the method...
68
+ # Otherwise, we close the writer right here, since we're
69
+ # not on the writing side.
70
+ write_pipe.close
71
+ end
72
+
73
+ # Record the start time for timeout purposes
74
+ start_time = Time.now.to_i
75
+
76
+ # Track the output as it goes
38
77
  output = ""
39
- begin
40
- loop do
41
- chunk = read_io.readpartial(10240)
42
- cleaned_chunk = UTF8.clean(chunk)
43
78
 
44
- output << chunk
45
- yield cleaned_chunk if block_given?
79
+ @logger.debug("Selecting on IO")
80
+ while true
81
+ results = IO.select([read_pipe], nil, nil, timeout || 0.1) || []
82
+ readers = results[0]
83
+
84
+ # Check if we have exceeded our timeout
85
+ raise TimeoutExceeded if timeout && (Time.now.to_i - start_time) > timeout
86
+ # Kill the process and wait a bit for it to disappear
87
+ # Process.kill('KILL', process.pid)
88
+ # Process.waitpid2(process.pid)
89
+
90
+ # Check the readers to see if they're ready
91
+ if readers && !readers.empty?
92
+ readers.each do |r|
93
+ # Read from the IO object
94
+ data = read_io(r)
95
+
96
+ # We don't need to do anything if the data is empty
97
+ next if data.empty?
98
+
99
+ output << cleaned_data = UTF8.clean(data)
100
+ yield cleaned_data if block_given?
101
+ end
46
102
  end
47
- rescue EOFError
103
+
104
+ # Break out if the process exited. We have to do this before
105
+ # attempting to write to stdin otherwise we'll get a broken pipe
106
+ # error.
107
+ break if process.exited?
108
+ end
109
+
110
+ # Wait for the process to end.
111
+ begin
112
+ remaining = (timeout || 32000) - (Time.now.to_i - start_time)
113
+ remaining = 0 if remaining < 0
114
+ @logger.debug("Waiting for process to exit. Remaining to timeout: #{remaining}")
115
+
116
+ process.poll_for_exit(remaining)
117
+ rescue ChildProcess::TimeoutError
118
+ raise TimeoutExceeded
119
+ end
120
+
121
+ @logger.debug("Exit status: #{process.exit_code}")
122
+
123
+ # Read the final output data, since it is possible we missed a small
124
+ # amount of text between the time we last read data and when the
125
+ # process exited.
126
+
127
+ # Read the extra data
128
+ extra_data = read_io(read_pipe)
129
+
130
+ # If there's some that we missed
131
+ if extra_data != ""
132
+ output << cleaned_data = UTF8.clean(extra_data)
133
+ yield cleaned_data if block_given?
48
134
  end
49
135
 
50
- process.wait
136
+ if RUBY_PLATFORM == "java"
137
+ # On JRuby, we need to close the writers after the process,
138
+ # for some reason. See https://github.com/mitchellh/vagrant/pull/711
139
+ write_pipe.close
140
+ end
51
141
 
52
- # the final result!
53
- Result.new(output.chomp, process.exit_code)
142
+ @output = output.chomp
143
+ @exit_status = process.exit_code
54
144
  end
55
145
 
56
146
  private
@@ -67,8 +157,63 @@ module Buildbox
67
157
  [ "bash", "-c" ]
68
158
  end
69
159
 
70
- def expanded_directory
71
- File.expand_path(@directory)
160
+ # Reads data from an IO object while it can, returning the data it reads.
161
+ # When it encounters a case when it can't read anymore, it returns the
162
+ # data.
163
+ #
164
+ # @return [String]
165
+ def read_io(io)
166
+ data = ""
167
+
168
+ while true
169
+ begin
170
+ if Platform.windows?
171
+ # Windows doesn't support non-blocking reads on
172
+ # file descriptors or pipes so we have to get
173
+ # a bit more creative.
174
+
175
+ # Check if data is actually ready on this IO device.
176
+ # We have to do this since `readpartial` will actually block
177
+ # until data is available, which can cause blocking forever
178
+ # in some cases.
179
+ results = IO.select([io], nil, nil, 0.1)
180
+ break if !results || results[0].empty?
181
+
182
+ # Read!
183
+ data << io.readpartial(READ_CHUNK_SIZE)
184
+ else
185
+ # Do a simple non-blocking read on the IO object
186
+ data << io.read_nonblock(READ_CHUNK_SIZE)
187
+ end
188
+ rescue Exception => e
189
+ # The catch-all rescue here is to support multiple Ruby versions,
190
+ # since we use some Ruby 1.9 specific exceptions.
191
+
192
+ breakable = false
193
+ if e.is_a?(EOFError)
194
+ # An `EOFError` means this IO object is done!
195
+ breakable = true
196
+ elsif defined?(IO::WaitReadable) && e.is_a?(IO::WaitReadable)
197
+ # IO::WaitReadable is only available on Ruby 1.9+
198
+
199
+ # An IO::WaitReadable means there may be more IO but this
200
+ # IO object is not ready to be read from yet. No problem,
201
+ # we read as much as we can, so we break.
202
+ breakable = true
203
+ elsif e.is_a?(Errno::EAGAIN)
204
+ # Otherwise, we just look for the EAGAIN error which should be
205
+ # all that IO::WaitReadable does in Ruby 1.9.
206
+ breakable = true
207
+ end
208
+
209
+ # Break out if we're supposed to. Otherwise re-raise the error
210
+ # because it is a real problem.
211
+ break if breakable
212
+ raise
213
+ end
214
+ end
215
+
216
+ data
72
217
  end
73
218
  end
74
219
  end
@@ -0,0 +1,23 @@
1
+ module Buildbox
2
+ class Platform
3
+ class << self
4
+ [:cygwin, :darwin, :bsd, :freebsd, :linux, :solaris].each do |type|
5
+ define_method("#{type}?") do
6
+ platform.include?(type.to_s)
7
+ end
8
+ end
9
+
10
+ def windows?
11
+ %W[mingw mswin].each do |text|
12
+ return true if platform.include?(text)
13
+ end
14
+
15
+ false
16
+ end
17
+
18
+ def platform
19
+ RbConfig::CONFIG["host_os"].downcase
20
+ end
21
+ end
22
+ end
23
+ end
@@ -23,8 +23,8 @@ module Buildbox
23
23
  info "Running script: #{script_path}"
24
24
 
25
25
  build.output = ""
26
- result = Command.script(script_path, :environment => @build.env,
27
- :directory => directory_path) do |chunk|
26
+ result = Command.run(script_path, :environment => @build.env,
27
+ :directory => directory_path) do |chunk|
28
28
  build.output << chunk
29
29
  end
30
30
 
@@ -1,3 +1,3 @@
1
1
  module Buildbox
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3"
3
3
  end
@@ -5,34 +5,34 @@ require "spec_helper"
5
5
  describe Buildbox::Command do
6
6
  describe "#run" do
7
7
  it "is run within a tty" do
8
- result = Buildbox::Command.command(%{ruby -e "puts STDOUT.tty?"})
8
+ result = Buildbox::Command.run(%{ruby -e "puts STDOUT.tty?"})
9
9
 
10
10
  result.output.should == "true"
11
11
  end
12
12
 
13
13
  it "successfully runs and returns the output from a simple comment" do
14
- result = Buildbox::Command.command('echo hello world')
14
+ result = Buildbox::Command.run('echo hello world')
15
15
 
16
16
  result.exit_status.should == 0
17
17
  result.output.should == "hello world"
18
18
  end
19
19
 
20
20
  it "redirects stdout to stderr" do
21
- result = Buildbox::Command.command('echo hello world 1>&2')
21
+ result = Buildbox::Command.run('echo hello world 1>&2')
22
22
 
23
23
  result.exit_status.should == 0
24
24
  result.output.should == "hello world"
25
25
  end
26
26
 
27
27
  it "handles commands that fail and returns the correct status" do
28
- result = Buildbox::Command.command('(exit 1)')
28
+ result = Buildbox::Command.run('(exit 1)')
29
29
 
30
30
  result.exit_status.should_not == 0
31
31
  result.output.should == ''
32
32
  end
33
33
 
34
34
  it "handles running malformed commands" do
35
- result = Buildbox::Command.command('if (')
35
+ result = Buildbox::Command.run('if (')
36
36
 
37
37
  result.exit_status.should_not == 0
38
38
  # bash 3.2.48 prints "syntax error" in lowercase.
@@ -45,7 +45,7 @@ describe Buildbox::Command do
45
45
 
46
46
  it "can collect output in chunks" do
47
47
  chunked_output = ''
48
- result = Buildbox::Command.command('echo hello world') do |chunk|
48
+ result = Buildbox::Command.run('echo hello world') do |chunk|
49
49
  unless chunk.nil?
50
50
  chunked_output += chunk
51
51
  end
@@ -60,8 +60,8 @@ describe Buildbox::Command do
60
60
  result = nil
61
61
  second_result = nil
62
62
  thread = Thread.new do
63
- result = Buildbox::Command.command('sillycommandlololol')
64
- second_result = Buildbox::Command.command('export FOO=bar; doesntexist.rb')
63
+ result = Buildbox::Command.run('sillycommandlololol')
64
+ second_result = Buildbox::Command.run('export FOO=bar; doesntexist.rb')
65
65
  end
66
66
  thread.join
67
67
 
@@ -76,7 +76,7 @@ describe Buildbox::Command do
76
76
 
77
77
  it "captures color'd output from a command" do
78
78
  chunked_output = ''
79
- result = Buildbox::Command.command("rspec #{FIXTURES_PATH.join('rspec', 'test_spec.rb')}") do |chunk|
79
+ result = Buildbox::Command.run("rspec #{FIXTURES_PATH.join('rspec', 'test_spec.rb')}") do |chunk|
80
80
  chunked_output += chunk unless chunk.nil?
81
81
  end
82
82
 
@@ -87,7 +87,7 @@ describe Buildbox::Command do
87
87
 
88
88
  it "runs scripts in a tty" do
89
89
  chunked_output = ''
90
- result = Buildbox::Command.script(FIXTURES_PATH.join('tty_script')) do |chunk|
90
+ result = Buildbox::Command.run(FIXTURES_PATH.join('tty_script')) do |chunk|
91
91
  chunked_output += chunk unless chunk.nil?
92
92
  end
93
93
 
@@ -97,19 +97,29 @@ describe Buildbox::Command do
97
97
 
98
98
  it "still runs even if pty isn't available" do
99
99
  PTY.should_receive(:spawn).and_raise(RuntimeError.new)
100
- result = Buildbox::Command.command('echo hello world')
100
+ result = Buildbox::Command.run('echo hello world')
101
101
 
102
102
  result.exit_status.should == 0
103
103
  result.output.should == "hello world"
104
104
  end
105
105
 
106
106
  it "supports utf8 characters" do
107
- result = Buildbox::Command.command('echo "hello"; echo "\xE2\x98\xA0"')
107
+ result = Buildbox::Command.run('bash', '-c', 'echo "hello"; echo "\xE2\x98\xA0"')
108
108
 
109
109
  result.exit_status.should == 0
110
110
  # just trying to interact with the string that has utf8 in it to make sure that it
111
111
  # doesn't blow up like it doesn on osx. this is hacky - need a better test.
112
112
  added = result.output + "hello"
113
113
  end
114
+
115
+ it "can collect chunks from within a thread" do
116
+ chunks = []
117
+
118
+ result = Buildbox::Command.run(FIXTURES_PATH.join('sleep_script')) do |chunk|
119
+ chunks << chunk
120
+ end
121
+
122
+ chunks.should == ["0\r\n", "1\r\n", "2\r\n"]
123
+ end
114
124
  end
115
125
  end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ 3.times do |i|
4
+ puts i
5
+ sleep 1
6
+ end
@@ -8,5 +8,6 @@ RSpec.configure do |config|
8
8
  logger = Logger.new(StringIO.new)
9
9
 
10
10
  Celluloid.logger = logger
11
+ Buildbox.logger = logger
11
12
  end
12
13
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: buildbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Pitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-09-08 00:00:00.000000000 Z
11
+ date: 2013-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -121,6 +121,7 @@ files:
121
121
  - lib/buildbox/command.rb
122
122
  - lib/buildbox/configuration.rb
123
123
  - lib/buildbox/monitor.rb
124
+ - lib/buildbox/platform.rb
124
125
  - lib/buildbox/runner.rb
125
126
  - lib/buildbox/server.rb
126
127
  - lib/buildbox/utf8.rb
@@ -157,6 +158,7 @@ files:
157
158
  - spec/fixtures/repo.git/objects/e9/8d8a9be514ef53609a52c9e1b820dbcc8e6603
158
159
  - spec/fixtures/repo.git/refs/heads/master
159
160
  - spec/fixtures/rspec/test_spec.rb
161
+ - spec/fixtures/sleep_script
160
162
  - spec/fixtures/tty_script
161
163
  - spec/integration/running_a_build_spec.rb
162
164
  - spec/spec_helper.rb
@@ -219,6 +221,7 @@ test_files:
219
221
  - spec/fixtures/repo.git/objects/e9/8d8a9be514ef53609a52c9e1b820dbcc8e6603
220
222
  - spec/fixtures/repo.git/refs/heads/master
221
223
  - spec/fixtures/rspec/test_spec.rb
224
+ - spec/fixtures/sleep_script
222
225
  - spec/fixtures/tty_script
223
226
  - spec/integration/running_a_build_spec.rb
224
227
  - spec/spec_helper.rb