Permalink
Browse files

wip

  • Loading branch information...
1 parent 8b8c039 commit bbd7d0d78c27c65bbc863ace19aff6e5ca1f8945 @ddollar committed Jun 8, 2012
View
@@ -1,3 +1,8 @@
+Dir[File.join(File.expand_path("../vendor", __FILE__), "*")].each do |vendor|
+ $:.unshift File.join(vendor, "lib")
+end
+
require "anvil/heroku/client"
require "anvil/heroku/command/build"
+require "anvil/heroku/command/develop"
require "anvil/heroku/command/release"
@@ -0,0 +1,130 @@
+require "distributor/client"
+
+# manage development dynos
+#
+class Heroku::Command::Develop < Heroku::Command::Base
+
+ PROTOCOL_COMMAND_HEADER = "\000\042\000"
+ PROTOCOL_COMMAND_EXIT = 1
+
+ # develop [DIR]
+ #
+ # start a development dyno
+ #
+ # -b, --buildpack # use a custom buildpack
+ # -e, --runtime-env # use the runtime env
+ #
+ def index
+ dir = shift_argument || "."
+ validate_arguments!
+
+ app_manifest = upload_manifest("application", dir)
+ dist_manifest = upload_manifest("distributor", distributor_path)
+ p [:dm, dist_manifest]
+
+ build_options = {
+ :buildpack => prepare_buildpack(options[:buildpack]),
+ :env => options[:runtime_env] ? heroku.config_vars(app) : {}
+ }
+
+ build_url = app_manifest.build(build_options) do |chunk|
+ print process_commands(chunk)
+ end
+
+ development_app = action("Creating development app") do
+ name = "heroku-develop-#{rand(1000000000).to_s(16)}"
+ info = api.post_app({ "name" => name, "stack" => "cedar" }).body
+ status name
+ name
+ end
+
+ action("Releasing to #{development_app}") do
+ heroku.release(development_app, "Deployed initial state", :build_url => build_url)
+ end
+
+ rendezvous_url = action("Starting development dyno") do
+ run_attached(development_app, "bash")
+ end
+
+ rendezvous = action("Connecting to development dyno") do
+ begin
+ set_buffer(false)
+ $stdin.sync = $stdout.sync = true
+ Heroku::Client::Rendezvous.new(
+ :rendezvous_url => rendezvous_url,
+ :connect_timeout => 120,
+ :activity_timeout => nil,
+ :input => $stdin,
+ :output => $stdout
+ )
+ rescue Timeout::Error
+ error "\nTimeout awaiting process"
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, OpenSSL::SSL::SSLError
+ error "\nError connecting to process"
+ rescue Interrupt
+ ensure
+ set_buffer(true)
+ end
+ end
+
+ rendezvous.start
+
+ action("Deleting development app") do
+ api.delete_app(development_app)
+ end
+
+ Heroku::Command.warnings.replace([])
+ end
+
+private
+
+ def run_attached(app, command)
+ process_data = api.post_ps(app, command, { :attach => true }).body
+ process_data["rendezvous_url"]
+ end
+
+ def distributor_path
+ File.expand_path("../../../../../vendor/distributor", __FILE__)
+ end
+
+ def upload_manifest(name, dir)
+ manifest = action("Generating #{name} manifest") do
+ Heroku::Manifest.new(dir)
+ end
+
+ action("Uploading new files") do
+ count = manifest.upload
+ @status = "#{count} files needed"
+ end
+ @status = nil
+
+ manifest
+ end
+
+ def prepare_buildpack(buildpack_url)
+ return nil unless buildpack_url
+ return buildpack_url unless File.exists?(buildpack_url) && File.directory?(buildpack_url)
+ manifest = upload_manifest("buildpack", buildpack_url)
+ manifest.save
+ end
+
+ def process_commands(chunk)
+ if location = chunk.index(PROTOCOL_COMMAND_HEADER)
+ buffer = StringIO.new(chunk[location..-1])
+ header = buffer.read(3)
+ case command = buffer.read(1).ord
+ when PROTOCOL_COMMAND_EXIT then
+ code = buffer.read(1).ord
+ unless code.zero?
+ puts "ERROR: Build exited with code: #{code}"
+ exit code
+ end
+ else
+ puts "unknown[#{command}]"
+ end
+ chunk = chunk[0..location-1]
+ end
+ chunk
+ end
+
+end
@@ -0,0 +1,4 @@
+source :rubygems
+
+gem "heroku-api"
+gem "heroku"
@@ -0,0 +1,27 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ addressable (2.2.8)
+ excon (0.14.0)
+ heroku (2.26.6)
+ heroku-api (~> 0.2.4)
+ launchy (>= 0.3.2)
+ netrc (~> 0.7.2)
+ rest-client (~> 1.6.1)
+ rubyzip
+ heroku-api (0.2.4)
+ excon (~> 0.14.0)
+ launchy (2.1.0)
+ addressable (~> 2.2.6)
+ mime-types (1.18)
+ netrc (0.7.4)
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ rubyzip (0.9.8)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ heroku
+ heroku-api
@@ -0,0 +1,26 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path("../lib", __FILE__)
+
+require "distributor/client"
+require "distributor/server"
+
+if ARGV.first == "server"
+
+ server = Distributor::Server.new($stdin.dup, $stdout.dup)
+ $stdout = $stderr
+ server.start
+
+else
+
+ api = Heroku::API.new(:api_key => "01a9a253fd3caf89e1115234965682df9c7cf1cc")
+
+ client = Distributor::Client.new(IO.popen("ruby client.rb server", "w+"))
+
+ client.run("bash 2>&1") do |ch|
+ client.hookup ch, $stdin, $stdout
+ end
+
+ client.start
+
+end
@@ -0,0 +1,2 @@
+module Distributor
+end
@@ -0,0 +1,77 @@
+require "distributor"
+require "distributor/connector"
+require "distributor/multiplexer"
+require "json"
+
+class Distributor::Client
+
+ def initialize(input, output=input)
+ @connector = Distributor::Connector.new
+ @multiplexer = Distributor::Multiplexer.new(output)
+ @handlers = {}
+ @processes = []
+
+ # reserve a command channel
+ @multiplexer.reserve(0)
+
+ # feed data from the input channel into the multiplexer
+ @connector.handle(input) do |io|
+ @multiplexer.input io
+ end
+
+ # handle the command channel of the multiplexer
+ @connector.handle(@multiplexer.reader(0)) do |io|
+ data = JSON.parse(io.readpartial(4096))
+
+ case command = data["command"]
+ when "launch" then
+ ch = data["ch"]
+ @multiplexer.reserve ch
+ @handlers[data["id"]].call(ch)
+ @handlers.delete(data["id"])
+ @processes << ch
+ else
+ raise "no such command: #{command}"
+ end
+ end
+ end
+
+ def output(ch, data)
+ @multiplexer.output ch, data
+ end
+
+ def run(command, &handler)
+ id = "#{Time.now.to_f}-#{rand(10000)}"
+ @multiplexer.output 0, JSON.dump({ "id" => id, "command" => "run", "args" => command })
+ @handlers[id] = handler
+ end
+
+ def hookup(ch, input, output)
+
+ # handle data incoming on the multiplexer
+ @connector.handle(@multiplexer.reader(ch)) do |io|
+ begin
+ data = io.readpartial(4096)
+ output.write data
+ rescue EOFError
+ "channel #{ch} exited"
+ end
+ end
+
+ # handle data incoming from the input channel
+ @connector.handle(input) do |io|
+ begin
+ data = io.readpartial(4096)
+ @multiplexer.output ch, data
+ rescue EOFError
+ @processes.each { |ch| @multiplexer.close(ch) }
+ end
+ end
+
+ end
+
+ def start
+ loop { @connector.listen }
+ end
+
+end
@@ -0,0 +1,20 @@
+require "distributor"
+
+class Distributor::Connector
+
+ attr_reader :connections
+
+ def initialize
+ @connections = {}
+ end
+
+ def handle(from, &handler)
+ @connections[from] = handler
+ end
+
+ def listen
+ rs, ws = IO.select(@connections.keys)
+ rs.each { |from| self.connections[from].call(from) }
+ end
+
+end
@@ -0,0 +1,43 @@
+require "distributor"
+require "distributor/packet"
+
+class Distributor::Multiplexer
+
+ def initialize(output)
+ @output = output
+ @readers = {}
+ @writers = {}
+
+ @output.sync = true
+ end
+
+ def reserve(ch=nil)
+ ch ||= @readers.keys.length
+ raise "channel already taken: #{ch}" if @readers.has_key?(ch)
+ @readers[ch], @writers[ch] = IO.pipe
+ ch
+ end
+
+ def reader(ch)
+ @readers[ch] || raise("no such channel: #{ch}")
+ end
+
+ def writer(ch)
+ @writers[ch] || raise("no such channel: #{ch}")
+ end
+
+ def input(io)
+ ch, data = Distributor::Packet.parse(io)
+ return if ch.nil?
+ writer(ch).write data
+ end
+
+ def output(ch, data)
+ @output.write Distributor::Packet.create(ch, data)
+ end
+
+ def close(ch)
+ writer(ch).close
+ end
+
+end
@@ -0,0 +1,36 @@
+require "distributor"
+
+class Distributor::Packet
+
+ PROTOCOL_VERSION = 1
+
+ def self.create(channel, data)
+ packet = "DIST"
+ packet += pack(PROTOCOL_VERSION)
+ packet += pack(channel)
+ packet += pack(data.length)
+ packet += data
+ end
+
+ def self.parse(io)
+ header = io.read(4)
+ return if header.nil?
+ raise "invalid header" unless header == "DIST"
+ version = unpack(io.read(4))
+ channel = unpack(io.read(4))
+ length = unpack(io.read(4))
+ data = ""
+ data += io.readpartial([4096,length-data.length].min) while data.length < length
+
+ [ channel, data ]
+ end
+
+ def self.pack(num)
+ [num.to_s(16).rjust(8,"0")].pack("H8")
+ end
+
+ def self.unpack(string)
+ string.unpack("N").first
+ end
+
+end
Oops, something went wrong.

0 comments on commit bbd7d0d

Please sign in to comment.