buildbox 0.3.3 → 0.3.4

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: 17c1b2a9d1b7bc186af0aa5a3d93d7dea50e9864
4
- data.tar.gz: dd7a3713cfed6a4f8f04eeb77fece760132833cd
3
+ metadata.gz: 0652374e7d5bf3b28c6d9a2af33a53c3882d6143
4
+ data.tar.gz: d76455f156b6fda572ea2d1276784f57005f9654
5
5
  SHA512:
6
- metadata.gz: b15ed4fb9913f0e48dae8b18d05695b556f0714cf9a042b7f08ffec7773945e2cee92554a0f8f7a4af8870ccdbc92b0159c16df970dcd94f80a3bd16b1260a44
7
- data.tar.gz: b266bba36f25497145ff582886caae1aa0da899119092deda3d4a304a24d4e35abc0c4caaca0a518859e19999038901c527f6bad9443ef83d6cb3c6d38af4356
6
+ metadata.gz: 45597206e22ef07fcd6948c9cd9071e66aabac2b44760fb9724ba5367785b626d3116b6c8c19e47e0c074916fb206b56c52d5ebf3ccf5580f25ebafcfe24e5f3
7
+ data.tar.gz: 9097f5b48ebbd24f4a90c70d9e6409bd518ec9884058cdbde153814c3a3ca196f2c7121d9d5cba6e43ce21109dbc2a147feded052077c5c7884611f2116a7408
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- buildbox (0.3.3)
4
+ buildbox (0.3.4)
5
5
  celluloid (~> 0.14)
6
6
  childprocess (~> 0.3)
7
7
  faraday (~> 0.8)
data/README.md CHANGED
@@ -5,14 +5,18 @@
5
5
  Install the gem
6
6
 
7
7
  $ gem install buildbox
8
+
9
+ Authenticate
10
+
11
+ $ buildbox auth:login [api_key]
8
12
 
9
13
  Add your worker tokens
10
14
 
11
- $ buildbox worker:add [token]
15
+ $ buildbox agent:setup [token]
12
16
 
13
17
  Then you can start monitoring for builds like so:
14
18
 
15
- $ buildbox worker:start
19
+ $ buildbox agent:start
16
20
 
17
21
  For more help with the command line interface
18
22
 
@@ -3,8 +3,18 @@
3
3
  # Disable stdout,stderr buffering
4
4
  STDERR.sync = STDOUT.sync = true
5
5
 
6
- dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
7
- $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
6
+ # Set the process name before we go to far in
7
+ $PROGRAM_NAME = 'buildbox'
8
+
9
+ # Add the buildbox lib directory to the load path
10
+ root_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
11
+ lib_dir = File.join(root_dir, 'lib')
12
+ $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
13
+
14
+ # Setup bundler correctly
15
+ ENV['BUNDLE_GEMFILE'] ||= File.join(root_dir, "Gemfile")
16
+ require 'bundler/setup'
8
17
 
9
18
  require 'buildbox'
19
+ Buildbox.logger.level = Logger::DEBUG if ENV['DEBUG'] == 'true'
10
20
  Buildbox::CLI.new(ARGV).parse
@@ -1,3 +1,4 @@
1
+ require 'rubygems'
1
2
  require 'pathname'
2
3
  require 'logger'
3
4
 
@@ -1,38 +1,39 @@
1
- require 'rubygems'
2
1
  require 'celluloid'
3
2
 
4
3
  module Buildbox
5
4
  class Agent
5
+ include Celluloid
6
6
  include Celluloid::Logger
7
7
 
8
- def initialize(access_token, api)
8
+ def initialize(access_token, api = Buildbox::API.new)
9
9
  @api = api
10
10
  @access_token = access_token
11
11
  end
12
12
 
13
13
  def work
14
- running_builds = scheduled_builds.map do |build|
15
- Monitor.new(build, @api).async.monitor
16
- Runner.new(build).future(:start)
14
+ builds = scheduled_builds
15
+
16
+ # Start off by letting each build know that it's been picked up
17
+ # by an agent.
18
+ builds.each do |build|
19
+ @api.update_build(build, :agent_accepted => @access_token)
17
20
  end
18
21
 
19
- # wait for all the running builds to finish
20
- running_builds.map(&:value)
22
+ # Run the builds one at a time
23
+ builds.each do |build|
24
+ Monitor.new(build, @api).async.monitor
25
+ Runner.new(build).start
26
+ end
21
27
  end
22
28
 
23
29
  private
24
30
 
25
- def projects
26
- @api.agent(@access_token, hostname).projects
27
- rescue Faraday::Error::ClientError
28
- warn "Agent #{@access_token} doesn't exist"
29
- [] # return empty array to avoid breakage
30
- end
31
-
32
31
  def scheduled_builds
33
- projects.map do |project|
34
- @api.scheduled_builds(project)
35
- end.flatten
32
+ agent = @api.agent(@access_token, hostname)
33
+ @api.scheduled_builds agent
34
+ rescue Buildbox::API::AgentNotFoundError
35
+ warn "Agent `#{@access_token}` doesn't exist"
36
+ [] # return empty array to avoid breakage
36
37
  end
37
38
 
38
39
  def hostname
@@ -1,32 +1,56 @@
1
- require 'rubygems'
2
1
  require 'faraday'
3
2
  require 'faraday_middleware'
4
3
  require 'hashie/mash'
4
+ require 'delegate'
5
5
 
6
6
  module Buildbox
7
7
  class API
8
- def initialize(config = Buildbox.config)
9
- @config = config
8
+ # Faraday uses debug to show response information, but when the agent is in
9
+ # DEBUG mode, it's kinda useless noise. So we use a ProxyLogger to only push
10
+ # the information we care about to the logger.
11
+ class ProxyLogger
12
+ def initialize(logger)
13
+ @logger = logger
14
+ end
15
+ def info(*args)
16
+ @logger.debug(*args)
17
+ end
18
+
19
+ def debug(*args)
20
+ # no-op
21
+ end
22
+ end
23
+
24
+ class AgentNotFoundError < Faraday::Error::ClientError; end
25
+ class ServerError < Faraday::Error::ClientError; end
26
+
27
+ def initialize(config = Buildbox.config, logger = Buildbox.logger)
28
+ @config = config
29
+ @logger = logger
10
30
  end
11
31
 
12
32
  def authenticate(api_key)
13
33
  @api_key = api_key
34
+
14
35
  get("user")
15
36
  end
16
37
 
17
38
  def agent(access_token, hostname)
18
39
  put("agents/#{access_token}", :hostname => hostname)
40
+ rescue Faraday::Error::ClientError => e
41
+ if e.response[:status] == 404
42
+ raise AgentNotFoundError.new(e, e.response)
43
+ else
44
+ raise ServerError.new(e, e.response)
45
+ end
19
46
  end
20
47
 
21
- def scheduled_builds(project)
22
- get(project.scheduled_builds_url).map { |build| Buildbox::Build.new(build) }
48
+ def scheduled_builds(agent)
49
+ get(agent.scheduled_builds_url).map { |build| Buildbox::Build.new(build) }
23
50
  end
24
51
 
25
- def update_build(build)
26
- put(build.url, :started_at => build.started_at,
27
- :finished_at => build.finished_at,
28
- :output => build.output,
29
- :exit_status => build.exit_status)
52
+ def update_build(build, options)
53
+ put(build.url, options)
30
54
  end
31
55
 
32
56
  private
@@ -34,24 +58,29 @@ module Buildbox
34
58
  def connection
35
59
  @connection ||= Faraday.new(:url => @config.api_endpoint) do |faraday|
36
60
  faraday.basic_auth @api_key || @config.api_key, ''
61
+ faraday.request :retry
62
+ faraday.request :json
37
63
 
38
- faraday.request :json
39
-
40
- faraday.response :logger, Buildbox.logger
64
+ faraday.response :logger, ProxyLogger.new(@logger)
41
65
  faraday.response :mashify
42
66
 
43
- # json needs to come after mashify as it needs to run before the mashify
67
+ # JSON needs to come after mashify as it needs to run before the mashify
44
68
  # middleware.
45
69
  faraday.response :json
46
70
  faraday.response :raise_error
47
71
 
48
72
  faraday.adapter Faraday.default_adapter
73
+
74
+ # Set some sensible defaults on the adapter.
75
+ faraday.options[:timeout] = 60
76
+ faraday.options[:open_timeout] = 60
49
77
  end
50
78
  end
51
79
 
52
80
  def post(path, body = {})
53
81
  connection.post(path) do |request|
54
- request.body = body
82
+ request.body = body
83
+ request.headers['Content-Type'] = 'application/json'
55
84
  end.body
56
85
  end
57
86
 
@@ -1,8 +1,9 @@
1
+ require 'celluloid'
2
+ require 'timeout'
3
+
1
4
  module Buildbox
2
5
  class Canceler
3
- def self.cancel(build)
4
- new(build).cancel
5
- end
6
+ include Celluloid
6
7
 
7
8
  def initialize(build)
8
9
  @build = build
@@ -11,8 +12,69 @@ module Buildbox
11
12
  def cancel
12
13
  @build.cancel_started = true
13
14
 
14
- # Kill that damn process, yo!
15
- Process.kill 'INT', @build.pid
15
+ # Store all the child processes before we stop so we can reap them after.
16
+ # A new process may start up between this and the process stopping,
17
+ # but that should be OK for now.
18
+ child_processes = process_map[@build.process.pid]
19
+
20
+ # Stop the process
21
+ Buildbox.logger.info "Cancelling build #{@build.namespace}/#{@build.id} with PID #{@build.process.pid}"
22
+ @build.process.stop
23
+
24
+ begin
25
+ @build.process.wait
26
+ rescue Errno::ECHILD
27
+ # Wow! That finished quickly...
28
+ end
29
+
30
+ kill_processes(child_processes)
31
+ end
32
+
33
+ private
34
+
35
+ def kill_processes(processes)
36
+ processes.each do |pid|
37
+ Buildbox.logger.debug "Sending a TERM signal to child process with PID #{pid}"
38
+
39
+ begin
40
+ Process.kill("TERM", pid)
41
+
42
+ # If the child process doesn't die within 5 seconds, try a more
43
+ # forceful kill command
44
+ begin
45
+ Timeout.timeout(5) do
46
+ Process.wait
47
+ end
48
+ rescue Timeout::Error
49
+ Buildbox.logger.debug "Sending a KILL signal to child process with PID #{pid}"
50
+
51
+ Process.kill("KILL", pid)
52
+ rescue Errno::ECHILD
53
+ # Killed already
54
+ end
55
+ rescue Errno::ESRCH
56
+ # No such process
57
+ end
58
+ end
59
+ end
60
+
61
+ # Generates a map of parent process and child processes. This method
62
+ # will currently only work on unix.
63
+ def process_map
64
+ output = `ps -eo ppid,pid`
65
+ processes = {}
66
+
67
+ output.split("\n").each do |line|
68
+ if result = line.match(/(\d+)\s(\d+)/)
69
+ parent = result[1].to_i
70
+ child = result[2].to_i
71
+
72
+ processes[parent] ||= []
73
+ processes[parent] << child
74
+ end
75
+ end
76
+
77
+ processes
16
78
  end
17
79
  end
18
80
  end
@@ -72,7 +72,8 @@ module Buildbox
72
72
  Buildbox.config.update(:agent_access_tokens => agent_access_tokens << access_token)
73
73
 
74
74
  puts "Successfully added agent access token"
75
- puts "You can now start the agent with: buildbox agent:start"
75
+ puts "You can now start the agent with: buildbox agent:start."
76
+ puts "If the agent is already running, you'll have to restart it for the new changes to take effect"
76
77
  elsif command == "auth:login"
77
78
  if @argv.length == 0
78
79
  puts "No api key provided"
@@ -13,35 +13,33 @@ module Buildbox
13
13
  # the given timeout.
14
14
  class TimeoutExceeded < StandardError; end
15
15
 
16
- attr_reader :pid, :output, :exit_status
16
+ attr_reader :output, :exit_status
17
17
 
18
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)
19
+ command = new(*args, &block)
24
20
  command.start(&block)
25
21
  command
26
22
  end
27
23
 
28
- def initialize(arguments, options = {})
29
- @arguments = arguments
30
- @options = options
24
+ def initialize(*args)
25
+ @options = args.last.is_a?(Hash) ? args.pop : {}
26
+ @arguments = args.dup
31
27
  @logger = Buildbox.logger
32
28
  end
33
29
 
30
+ def arguments
31
+ [ *@arguments ].compact.map(&:to_s) # all arguments must be a string
32
+ end
33
+
34
+ def process
35
+ @process ||= ChildProcess.build(*arguments)
36
+ end
37
+
34
38
  def start(&block)
35
39
  # Get the timeout, if we have one
36
40
  timeout = @options[:timeout]
37
41
 
38
- # Build the command we're going to run
39
- arguments = [ *@arguments ].compact.map(&:to_s) # all arguments must be a string
40
-
41
- # Build the ChildProcess
42
- @logger.info("Starting process: #{arguments}")
43
-
44
- process = ChildProcess.build(*arguments)
42
+ # Set the directory for the process
45
43
  process.cwd = File.expand_path(@options[:directory] || Dir.pwd)
46
44
 
47
45
  # Create the pipes so we can read the output in real time. PTY
@@ -70,6 +68,8 @@ module Buildbox
70
68
  # Make sure the stdin does not buffer
71
69
  process.io.stdin.sync = true
72
70
 
71
+ @logger.debug("Process #{arguments} started with PID: #{process.pid} and Group ID: #{Process.getpgid(process.pid)}")
72
+
73
73
  if RUBY_PLATFORM != "java"
74
74
  # On Java, we have to close after. See down the method...
75
75
  # Otherwise, we close the writer right here, since we're
@@ -77,9 +77,6 @@ module Buildbox
77
77
  write_pipe.close
78
78
  end
79
79
 
80
- # Store the process id for later cancelling!
81
- @pid = process.pid
82
-
83
80
  # Record the start time for timeout purposes
84
81
  start_time = Time.now.to_i
85
82
 
@@ -107,7 +104,7 @@ module Buildbox
107
104
  next if data.empty?
108
105
 
109
106
  output << cleaned_data = UTF8.clean(data)
110
- yield self, cleaned_data if block_given?
107
+ yield cleaned_data if block_given?
111
108
  end
112
109
  end
113
110
 
@@ -140,7 +137,7 @@ module Buildbox
140
137
  # If there's some that we missed
141
138
  if extra_data != ""
142
139
  output << cleaned_data = UTF8.clean(extra_data)
143
- yield self, cleaned_data if block_given?
140
+ yield cleaned_data if block_given?
144
141
  end
145
142
 
146
143
  if RUBY_PLATFORM == "java"
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'celluloid'
3
2
 
4
3
  module Buildbox
@@ -12,25 +11,25 @@ module Buildbox
12
11
 
13
12
  def monitor
14
13
  loop do
15
- # There is an edge case where the build finishes between making the
16
- # update_build http call, and breaking. So to make sure we're using the
17
- # same build object throughout this call, we can just deep dup it.
18
- build = Marshal.load(Marshal.dump(@build))
14
+ if @build.started?
15
+ # As the build can finish in between doing the update_build api_call
16
+ # and checking to see if the build has finished, we make sure we use the
17
+ # same finished_at timestamp throughout the entire method.
18
+ finished_at = @build.finished_at
19
19
 
20
- if build.started? || build.finished?
21
- new_build = @api.update_build(build)
20
+ updated_build = @api.update_build(@build, :started_at => @build.started_at,
21
+ :finished_at => finished_at,
22
+ :output => @build.output,
23
+ :exit_status => @build.exit_status)
22
24
 
23
- # Try and cancel the build if we haven't tried already
24
- if new_build.state == 'canceled' && !@build.cancelling?
25
- Buildbox::Canceler.cancel(@build)
25
+ if updated_build.state == 'canceled' && !@build.cancelling?
26
+ Buildbox::Canceler.new(@build).async.cancel
26
27
  end
27
- end
28
28
 
29
- if build.finished?
30
- break
31
- else
32
- sleep 2 # 2 seconds seems reasonable for now
29
+ break if finished_at
33
30
  end
31
+
32
+ sleep 1
34
33
  end
35
34
  end
36
35
  end
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'celluloid'
3
2
  require 'fileutils'
4
3
 
@@ -7,8 +6,6 @@ module Buildbox
7
6
  include Celluloid
8
7
  include Celluloid::Logger
9
8
 
10
- attr_reader :build
11
-
12
9
  def initialize(build)
13
10
  @build = build
14
11
  end
@@ -20,24 +17,22 @@ module Buildbox
20
17
  File.open(script_path, 'w+') { |file| file.write(@build.script) }
21
18
  File.chmod(0777, script_path)
22
19
 
23
- info "Running script: #{script_path}"
20
+ command = Command.new(script_path, :environment => @build.env, :directory => directory_path)
24
21
 
22
+ @build.output = ""
23
+ @build.process = command.process
25
24
  @build.started_at = Time.now.utc
26
25
 
27
- build.output = ""
28
- result = Command.run(script_path, :environment => @build.env, :directory => directory_path) do |command, chunk|
29
- build.pid = command.pid
30
- build.output << chunk
31
- end
26
+ command.start { |chunk| @build.output << chunk }
32
27
 
33
- build.output = result.output
34
- build.exit_status = result.exit_status
28
+ @build.output = command.output
29
+ @build.exit_status = command.exit_status
35
30
 
36
31
  File.delete(script_path)
37
32
 
38
33
  @build.finished_at = Time.now.utc
39
34
 
40
- info "#{@build.namespace} ##{@build.id} finished with exit status #{result.exit_status}"
35
+ info "#{@build.namespace} ##{@build.id} finished with exit status #{command.exit_status}"
41
36
  end
42
37
 
43
38
  private
@@ -1,3 +1,5 @@
1
+ require 'celluloid'
2
+
1
3
  module Buildbox
2
4
  class Server
3
5
  INTERVAL = 5
@@ -5,26 +7,33 @@ module Buildbox
5
7
  def initialize(config = Buildbox.config, logger = Buildbox.logger)
6
8
  @config = config
7
9
  @logger = logger
10
+ @supervisors = []
8
11
  end
9
12
 
10
13
  def start
11
- loop do
12
- @config.check
13
- @config.reload
14
+ Celluloid.logger = @logger
14
15
 
15
- agent_access_tokens.each do |access_token|
16
- Buildbox::Agent.new(access_token, api).work
16
+ agent_access_tokens.each do |access_token|
17
+ @supervisors << Buildbox::Agent.supervise(access_token)
18
+
19
+ @logger.info "Agent with access token `#{access_token}` has started."
20
+ end
21
+
22
+ loop do
23
+ @supervisors.each do |supervisor|
24
+ supervisor.actors.first.async.work
17
25
  end
18
26
 
19
- @logger.info "Sleeping for #{INTERVAL} seconds"
20
- sleep INTERVAL
27
+ wait INTERVAL
21
28
  end
22
29
  end
23
30
 
24
31
  private
25
32
 
26
- def api
27
- @api ||= Buildbox::API.new
33
+ def wait(interval)
34
+ @logger.debug "Sleeping for #{interval} seconds"
35
+
36
+ sleep interval
28
37
  end
29
38
 
30
39
  def agent_access_tokens
@@ -1,3 +1,3 @@
1
1
  module Buildbox
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.4"
3
3
  end
@@ -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.run('echo', 'hello world') do |command, chunk|
48
+ result = Buildbox::Command.run('echo', 'hello world') do |chunk|
49
49
  unless chunk.nil?
50
50
  chunked_output += chunk
51
51
  end
@@ -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.run('rspec', FIXTURES_PATH.join('rspec', 'test_spec.rb')) do |command, 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.run(FIXTURES_PATH.join('tty_script')) do |command, 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
 
@@ -115,11 +115,11 @@ describe Buildbox::Command do
115
115
  it "can collect chunks from within a thread" do
116
116
  chunks = []
117
117
 
118
- result = Buildbox::Command.run(FIXTURES_PATH.join('sleep_script')) do |command, chunk|
118
+ result = Buildbox::Command.run(FIXTURES_PATH.join('sleep_script')) do |chunk|
119
119
  chunks << chunk
120
120
  end
121
121
 
122
- chunks.should == ["test 0\r\n", "test 1\r\n", "test 2\r\n"]
122
+ chunks.map(&:chomp).reject(&:empty?).should == ["test 0", "test 1", "test 2"]
123
123
  end
124
124
  end
125
125
  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.3.3
4
+ version: 0.3.4
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-16 00:00:00.000000000 Z
11
+ date: 2013-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday