buildbox 0.3.3 → 0.3.4

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.
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