massive refactoring for programmatic control and stability

This commit is contained in:
David Dollar
2012-06-10 22:58:09 -04:00
parent f41cc552c7
commit 51a704939e
65 changed files with 984 additions and 1317 deletions
+46 -31
View File
@@ -1,38 +1,37 @@
require "foreman"
require "foreman/helpers"
require "foreman/engine"
require "foreman/tmux_engine"
require "foreman/engine/cli"
require "foreman/export"
require "shellwords"
require "thor"
require "yaml"
class Foreman::CLI < Thor
include Foreman::Helpers
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
class_option :tmux, :type => :boolean, :aliases => "-t", :desc => "Run in tmux session"
class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile"
class_option :app_root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory"
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
method_option :port, :type => :numeric, :aliases => "-p"
method_option :concurrency, :type => :string, :aliases => "-c", :banner => '"alpha=5,bar=3"'
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"'
method_option :port, :type => :numeric, :aliases => "-p"
class << self
# Hackery. Take the run method away from Thor so that we can redefine it.
def is_thor_reserved_word?(word, type)
return false if word == 'run'
return false if word == "run"
super
end
end
def start(process=nil)
check_procfile!
engine.options[:concurrency] = "#{process}=1" if process
load_environment!
engine.load_procfile(procfile)
engine.options[:formation] = "#{process}=1" if process
engine.start
end
@@ -48,6 +47,8 @@ class Foreman::CLI < Thor
def export(format, location=nil)
check_procfile!
load_environment!
engine.load_procfile(procfile)
formatter = Foreman::Export.formatter(format)
formatter.new(location, engine, options).export
rescue Foreman::Export::Exception => ex
@@ -58,16 +59,19 @@ class Foreman::CLI < Thor
def check
check_procfile!
error "no processes defined" unless engine.procfile.entries.length > 0
puts "valid procfile detected (#{engine.procfile.process_names.join(', ')})"
engine.load_procfile(procfile)
error "no processes defined" unless engine.processes.length > 0
puts "valid procfile detected (#{engine.process_names.join(', ')})"
end
desc "run COMMAND [ARGS...]", "Run a command using your application's environment"
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
def run(*args)
engine.apply_environment!
load_environment!
begin
exec args.shelljoin
exec engine.env, args.shelljoin
rescue Errno::EACCES
error "not executable: #{args.first}"
rescue Errno::ENOENT
@@ -75,44 +79,55 @@ class Foreman::CLI < Thor
end
end
class << self
def new_engine(procfile, options)
@engine_class ||= options[:tmux] ? Foreman::TmuxEngine : Foreman::Engine
@engine_class.new(procfile, options)
end
def engine_class=(klass)
@engine_class = klass
no_tasks do
def engine
@engine ||= begin
engine_class = Foreman::Engine::CLI
engine = engine_class.new(
:formation => options[:formation],
:port => options[:port],
:root => options[:root]
)
engine
end
end
end
private ######################################################################
def error(message)
puts "ERROR: #{message}"
exit 1
end
def check_procfile!
error("#{procfile} does not exist.") unless File.exist?(procfile)
end
def engine
@engine ||= self.class.new_engine(procfile, options)
def load_environment!
if options[:env]
options[:env].split(",").each do |file|
engine.load_env file
end
else
default_env = File.join(engine.root, ".env")
engine.load_env default_env if File.exists?(default_env)
end
end
def procfile
case
when options[:procfile] then options[:procfile]
when options[:app_root] then File.expand_path(File.join(options[:app_root], "Procfile"))
when options[:root] then File.expand_path(File.join(options[:app_root], "Procfile"))
else "Procfile"
end
end
def error(message)
puts "ERROR: #{message}"
exit 1
end
def options
original_options = super
return original_options unless File.exists?(".foreman")
defaults = YAML::load_file(".foreman") || {}
Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
end
end
-40
View File
@@ -1,40 +0,0 @@
require "foreman"
module Foreman::Color
ANSI = {
:reset => 0,
:black => 30,
:red => 31,
:green => 32,
:yellow => 33,
:blue => 34,
:magenta => 35,
:cyan => 36,
:white => 37,
:bright_black => 30,
:bright_red => 31,
:bright_green => 32,
:bright_yellow => 33,
:bright_blue => 34,
:bright_magenta => 35,
:bright_cyan => 36,
:bright_white => 37,
}
def self.enable(io)
io.extend(self)
end
def color?
return false unless self.respond_to?(:isatty)
self.isatty && ENV["TERM"]
end
def color(name)
return "" unless color?
return "" unless ansi = ANSI[name.to_sym]
"\e[#{ansi}m"
end
end
+220 -160
View File
@@ -1,8 +1,7 @@
require "foreman"
require "foreman/color"
require "foreman/env"
require "foreman/process"
require "foreman/procfile"
require "foreman/utils"
require "tempfile"
require "timeout"
require "fileutils"
@@ -10,126 +9,252 @@ require "thread"
class Foreman::Engine
attr_reader :environment
attr_reader :procfile
attr_reader :directory
attr_reader :env
attr_reader :options
attr_reader :processes
COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
intense_green intense_magenta intense_red, intense_blue )
Foreman::Color.enable($stdout)
def initialize(procfile, options={})
@procfile = Foreman::Procfile.new(procfile) if File.exists?(procfile)
@directory = options[:app_root] || File.expand_path(File.dirname(procfile))
# Create an +Engine+ for running processes
#
# @param [Hash] options
#
# @option options [String] :formation (all=1) The process formation to use
# @option options [Fixnum] :port (5000) The base port to assign to processes
# @option options [String] :root (Dir.pwd) The root directory from which to run processes
#
def initialize(options={})
@options = options.dup
@output_mutex = Mutex.new
@options[:env] ||= default_env
@environment = read_environment_files(@options[:env])
@options[:formation] ||= "all=1"
@env = {}
@mutex = Mutex.new
@names = {}
@processes = []
@running = {}
@readers = {}
end
# Start the processes registered to this +Engine+
#
def start
proctitle "ruby: foreman master"
termtitle "#{File.basename(@directory)} - foreman"
trap("TERM") { puts "SIGTERM received"; terminate_gracefully }
trap("INT") { puts "SIGINT received"; terminate_gracefully }
trap("HUP") { puts "SIGHUP received"; terminate_gracefully }
assign_colors
startup
spawn_processes
watch_for_output
watch_for_termination
sleep 0.1
watch_for_termination { terminate_gracefully }
shutdown
end
def port_for(process, num, base_port=nil)
base_port ||= 5000
offset = procfile.process_names.index(process.name) * 100
base_port.to_i + offset + num - 1
# Register a process to be run by this +Engine+
#
# @param [String] name A name for this process
# @param [String] command The command to run
# @param [Hash] options
#
# @option options [Hash] :env A custom environment for this process
#
def register(name, command, options={})
options[:env] ||= env
options[:cwd] ||= File.dirname(command.split(" ").first)
process = Foreman::Process.new(command, options)
@names[process] = name
@processes << process
end
def apply_environment!
environment.each { |k,v| ENV[k] = v }
# Clear the processes registered to this +Engine+
#
def clear
@names = {}
@processes = []
end
def self.read_environment(filename)
return {} unless File.exists?(filename)
# Register processes by reading a Procfile
#
# @param [String] filename A Procfile from which to read processes to register
#
def load_procfile(filename)
options[:root] ||= File.dirname(filename)
Foreman::Procfile.new(filename).entries do |name, command|
register name, command, :cwd => options[:root]
end
self
end
File.read(filename).split("\n").inject({}) do |hash, line|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
key, val = [$1, $2]
case val
when /\A'(.*)'\z/ then hash[key] = $1
when /\A"(.*)"\z/ then hash[key] = $1.gsub(/\\(.)/, '\1')
else hash[key] = val
end
end
hash
# Load a .env file into the +env+ for this +Engine+
#
# @param [String] filename A .env file to load into the environment
#
def load_env(filename)
Foreman::Env.new(filename).entries do |name, value|
@env[name] = value
end
end
private ######################################################################
def spawn_processes
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
procfile.entries.each do |entry|
reader, writer = (IO.method(:pipe).arity == 0 ? IO.pipe : IO.pipe("BINARY"))
entry.spawn(concurrency[entry.name], writer, @directory, @environment, port_for(entry, 1, base_port)).each do |process|
running_processes[process.pid] = process
readers[process] = reader
# Send a signal to all processesstarted by this +Engine+
#
# @param [String] signal The signal to send to each process
#
def killall(signal="SIGTERM")
@running.each do |pid, (process, index)|
system "sending #{signal} to #{name_for(pid)} at pid #{pid}"
begin
Process.kill(signal, -1 * pid)
rescue Errno::ESRCH, Errno::EPERM
end
end
end
# Get the process formation
#
# @returns [Fixnum] The formation count for the specified process
#
def formation
@formation ||= parse_formation(options[:formation])
end
# List the available process names
#
# @returns [Array] A list of process names
#
def process_names
@processes.map { |p| @names[p] }
end
# Get the +Process+ for a specifid name
#
# @param [String] name The process name
#
# @returns [Foreman::Process] The +Process+ for the specified name
#
def process(name)
@names.invert[name]
end
# Yield each +Process+ in order
#
def each_process
process_names.each do |name|
yield name, process(name)
end
end
# Get the root directory for this +Engine+
#
# @returns [String] The root directory
#
def root
File.expand_path(options[:root] || Dir.pwd)
end
# Get the port for a given process and offset
#
# @param [Foreman::Process] process A +Process+ associated with this engine
# @param [Fixnum] instance The instance of the process
#
# @returns [Fixnum] port The port to use for this instance of this process
#
def port_for(process, instance)
base_port + (@processes.index(process) * 100) + (instance - 1)
end
private
### Engine API ######################################################
def startup
raise TypeError, "must use a subclass of Foreman::Engine"
end
def output(name, data)
raise TypeError, "must use a subclass of Foreman::Engine"
end
def shutdown
raise TypeError, "must use a subclass of Foreman::Engine"
end
## Helpers ##########################################################
def base_port
options[:port] || environment["PORT"] || ENV["PORT"] || 5000
(options[:port] || env["PORT"] || ENV["PORT"] || 5000).to_i
end
def kill_all(signal="SIGTERM")
running_processes.each do |pid, process|
info "sending #{signal} to pid #{pid}"
process.kill signal
def create_pipe
IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY")
end
def name_for(pid)
process, index = @running[pid]
[ @names[process], index.to_s ].compact.join(".")
end
def parse_formation(formation)
pairs = @options[:formation].to_s.gsub(/\s/, "").split(",")
pairs.inject(Hash.new(0)) do |ax, pair|
process, amount = pair.split("=")
process == "all" ? ax.default = amount.to_i : ax[process] = amount.to_i
ax
end
end
def terminate_gracefully
return if @terminating
@terminating = true
info "sending SIGTERM to all processes"
kill_all "SIGTERM"
Timeout.timeout(5) do
while running_processes.length > 0
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process.name
def output_with_mutex(name, message)
@mutex.synchronize do
output name, message
end
end
def system(message)
output_with_mutex "system", message
end
def termination_message_for(status)
if status.exited?
"exited with code #{status.exitstatus}"
elsif status.signaled?
"terminated by SIG#{Signal.list.invert[status.termsig]}"
else
"died a mysterious death"
end
end
def flush_reader(reader)
until reader.eof?
data = reader.gets
output_with_mutex name_for(@readers.key(reader)), data
end
end
## Engine ###########################################################
def spawn_processes
@processes.each do |process|
1.upto(formation[@names[process]]) do |n|
reader, writer = create_pipe
begin
pid = process.run(:output => writer, :env => { "PORT" => port_for(process, n).to_s })
writer.puts "started with pid #{pid}"
rescue Errno::ENOENT
writer.puts "unknown command: #{process.command}"
end
@running[pid] = [process, n]
@readers[pid] = reader
end
end
rescue Timeout::Error
info "sending SIGKILL to all processes"
kill_all "SIGKILL"
end
def poll_readers
rs, ws = IO.select(readers.values, [], [], 1)
(rs || []).each do |r|
data = r.gets
next unless data
data.force_encoding("BINARY") if data.respond_to?(:force_encoding)
ps, message = data.split(",", 2)
color = colors[ps.split(".").first]
info message, ps, color
end
end
def watch_for_output
Thread.new do
require "win32console" if Foreman.windows?
begin
loop do
poll_readers
(IO.select(@readers.values).first || []).each do |reader|
data = reader.gets
output_with_mutex name_for(@readers.key(reader)), data
end
end
rescue Exception => ex
puts ex.message
@@ -140,89 +265,24 @@ private ######################################################################
def watch_for_termination
pid, status = Process.wait2
process = running_processes.delete(pid)
info "process terminated", process.name
terminate_gracefully
output_with_mutex name_for(pid), termination_message_for(status)
@running.delete(pid)
yield if block_given?
pid
rescue Errno::ECHILD
end
def info(message, name="system", color=:white)
output = ""
output += $stdout.color(color)
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
output += $stdout.color(:reset)
output += message.chomp
puts output
end
def print(message=nil)
@output_mutex.synchronize do
$stdout.print message
def terminate_gracefully
return if @terminating
@terminating = true
system "sending SIGTERM to all processes"
killall "SIGTERM"
Timeout.timeout(5) do
watch_for_termination while @running.length > 0
end
end
def puts(message=nil)
@output_mutex.synchronize do
$stdout.puts message
end
end
def longest_process_name
@longest_process_name ||= begin
longest = procfile.process_names.map { |name| name.length }.sort.last
longest = 6 if longest < 6 # system
longest
end
end
def pad_process_name(name="system")
name.to_s.ljust(longest_process_name + 3) # add 3 for process number padding
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
def running_processes
@running_processes ||= {}
end
def readers
@readers ||= {}
end
def colors
@colors ||= {}
end
def assign_colors
procfile.entries.each_with_index do |entry, idx|
colors[entry.name] = COLORS[idx % COLORS.length]
end
end
def process_by_reader(reader)
readers.invert[reader]
end
def read_environment_files(filenames)
environment = {}
(filenames || "").split(",").map(&:strip).each do |filename|
error "No such file: #{filename}" unless File.exists?(filename)
environment.merge!(Foreman::Engine.read_environment(filename))
end
environment
end
def default_env
env = File.join(directory, ".env")
File.exists?(env) ? env : ""
rescue Timeout::Error
system "sending SIGKILL to all processes"
killall "SIGKILL"
end
end
+98
View File
@@ -0,0 +1,98 @@
require "foreman/engine"
class Foreman::Engine::CLI < Foreman::Engine
module Color
ANSI = {
:reset => 0,
:black => 30,
:red => 31,
:green => 32,
:yellow => 33,
:blue => 34,
:magenta => 35,
:cyan => 36,
:white => 37,
:bright_black => 30,
:bright_red => 31,
:bright_green => 32,
:bright_yellow => 33,
:bright_blue => 34,
:bright_magenta => 35,
:bright_cyan => 36,
:bright_white => 37,
}
def self.enable(io)
io.extend(self)
end
def color?
return false unless self.respond_to?(:isatty)
self.isatty && ENV["TERM"]
end
def color(name)
return "" unless color?
return "" unless ansi = ANSI[name.to_sym]
"\e[#{ansi}m"
end
end
FOREMAN_COLORS = %w( cyan yellow green magenta red blue intense_cyan intense_yellow
intense_green intense_magenta intense_red, intense_blue )
def startup
@colors = map_colors
proctitle "foreman: master"
end
def output(name, data)
data.to_s.chomp.split("\n").each do |message|
Color.enable($stdout) unless $stdout.respond_to?(:color?)
output = ""
output += $stdout.color(@colors[name.split(".").first].to_sym)
output += "#{Time.now.strftime("%H:%M:%S")} #{pad_process_name(name)} | "
output += $stdout.color(:reset)
output += message
$stdout.puts output
end
end
def shutdown
end
private
def name_padding
@name_padding ||= begin
index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1
name_padding = @names.values.map { |n| n.length + index_padding }.sort.last
[ 6, name_padding ].max
end
end
def pad_process_name(name)
name.ljust(name_padding, " ")
end
def map_colors
colors = Hash.new("white")
@names.values.each_with_index do |name, index|
colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length]
end
colors["system"] = "intense_white"
colors
end
def proctitle(title)
$0 = title
end
def termtitle(title)
printf("\033]0;#{title}\007") unless Foreman.windows?
end
end
+27
View File
@@ -0,0 +1,27 @@
require "foreman"
class Foreman::Env
attr_reader :entries
def initialize(filename)
@entries = File.read(filename).split("\n").inject({}) do |ax, line|
if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/
key = $1
case val = $2
when /\A'(.*)'\z/ then ax[key] = $1
when /\A"(.*)"\z/ then ax[key] = $1.gsub(/\\(.)/, '\1')
else ax[key] = val
end
end
ax
end
end
def entries
@entries.each do |key, value|
yield key, value
end
end
end
-1
View File
@@ -24,7 +24,6 @@ module Foreman::Export
end
require "foreman/export/base"
require "foreman/export/inittab"
require "foreman/export/upstart"
+58 -35
View File
@@ -1,23 +1,37 @@
require "foreman/export"
require "foreman/utils"
require "shellwords"
class Foreman::Export::Base
attr_reader :location, :engine, :app, :log, :port, :user, :template, :concurrency
attr_reader :location
attr_reader :engine
attr_reader :options
attr_reader :formation
def initialize(location, engine, options={})
@location = location
@engine = engine
@app = options[:app]
@log = options[:log]
@port = options[:port]
@user = options[:user]
@template = options[:template]
@concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
@location = location
@engine = engine
@options = options.dup
@formation = engine.formation
end
def export
raise "export method must be overridden"
error("Must specify a location") unless location
FileUtils.mkdir_p(location) rescue error("Could not create: #{location}")
FileUtils.mkdir_p(log) rescue error("Could not create: #{log}")
FileUtils.chown(user, nil, log) rescue error("Could not chown #{log} to #{user}")
end
def app
options[:app] || "app"
end
def log
options[:log] || "/var/log/#{app}"
end
def user
options[:user] || app
end
private ######################################################################
@@ -29,38 +43,47 @@ private ######################################################################
def say(message)
puts "[foreman export] %s" % message
end
def clean(filename)
return unless File.exists?(filename)
say "cleaning up: #{filename}"
FileUtils.rm(filename)
end
def export_template(exporter, file, template_root)
if template_root && File.exist?(file_path = File.join(template_root, file))
File.read(file_path)
elsif File.exist?(file_path = File.expand_path(File.join("~/.foreman/templates", file)))
File.read(file_path)
else
File.read(File.expand_path("../../../../data/export/#{exporter}/#{file}", __FILE__))
end
def shell_quote(value)
'"' + Shellwords.escape(value) + '"'
end
def export_template(name)
name_without_first = name.split("/")[1..-1].join("/")
matchers = []
matchers << File.join(options[:template], name_without_first) if options[:template]
matchers << File.expand_path("~/.foreman/templates/#{name}")
matchers << File.expand_path("../../../../data/export/#{name}", __FILE__)
File.read(matchers.detect { |m| File.exists?(m) })
end
def write_template(name, target, binding)
compiled = ERB.new(export_template(name)).result(binding)
write_file target, compiled
end
def chmod(mode, file)
say "setting #{file} to mode #{mode}"
FileUtils.chmod mode, File.join(location, file)
end
def create_directory(dir)
say "creating: #{dir}"
FileUtils.mkdir_p(File.join(location, dir))
end
def write_file(filename, contents)
say "writing: #{filename}"
File.open(filename, "w") do |file|
File.open(File.join(location, filename), "w") do |file|
file.puts contents
end
end
# Quote a string to be used on the command line. Backslashes are escapde to \\ and quotes
# escaped to \"
#
# str - string to be quoted
#
# Examples
#
# shell_quote("FB|123\"\\1")
# # => "\"FB|123\"\\"\\\\1\""
#
# Returns the the escaped string surrounded by quotes
def shell_quote(str)
"\"#{str.gsub(/\\/){ '\\\\' }.gsub(/["]/){ "\\\"" }}\""
end
end
+3 -17
View File
@@ -4,23 +4,9 @@ require "foreman/export"
class Foreman::Export::Bluepill < Foreman::Export::Base
def export
error("Must specify a location") unless location
FileUtils.mkdir_p location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
Dir["#{location}/#{app}.pill"].each do |file|
say "cleaning up: #{file}"
FileUtils.rm(file)
end
master_template = export_template("bluepill", "master.pill.erb", template_root)
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}.pill", master_config
super
clean "#{location}/#{app}.pill"
write_template "bluepill/master.pill.erb", "#{app}.pill", binding
end
end
+8 -11
View File
@@ -3,21 +3,19 @@ require "foreman/export"
class Foreman::Export::Inittab < Foreman::Export::Base
def export
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
error("Must specify a location") unless location
inittab = []
inittab << "# ----- foreman #{app} processes -----"
engine.procfile.entries.inject(1) do |index, process|
1.upto(self.concurrency[process.name]) do |num|
index = 1
engine.each_process do |name, process|
1.upto(engine.formation[name]) do |num|
id = app.slice(0, 2).upcase + sprintf("%02d", index)
port = engine.port_for(process, num, self.port)
inittab << "#{id}:4:respawn:/bin/su - #{user} -c 'PORT=#{port} #{process.command} >> #{log_root}/#{process.name}-#{num}.log 2>&1'"
port = engine.port_for(process, num)
inittab << "#{id}:4:respawn:/bin/su - #{user} -c 'PORT=#{port} #{process.command} >> #{log}/#{name}-#{num}.log 2>&1'"
index += 1
end
index
end
inittab << "# ----- end foreman #{app} processes -----"
@@ -27,9 +25,8 @@ class Foreman::Export::Inittab < Foreman::Export::Base
if location == "-"
puts inittab
else
FileUtils.mkdir_p(log_root) rescue error "could not create #{log_root}"
FileUtils.chown(user, nil, log_root) rescue error "could not chown #{log_root} to #{user}"
write_file(location, inittab)
say "writing: #{location}"
File.open(location, "w") { |file| file.puts inittab }
end
end
+4 -16
View File
@@ -4,24 +4,12 @@ require "foreman/export"
class Foreman::Export::Launchd < Foreman::Export::Base
def export
error("Must specify a location") unless location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
FileUtils.mkdir_p(location)
engine.procfile.entries.each do |process|
1.upto(self.concurrency[process.name]) do |num|
master_template = export_template("launchd", "launchd.plist.erb", template_root)
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.plist", master_config
super
engine.each_process do |name, process|
1.upto(engine.formation[name]) do |num|
write_template "launchd/launchd.plist.erb", "#{app}-#{name}-#{num}.plist", binding
end
end
end
end
+14 -39
View File
@@ -2,58 +2,33 @@ require "erb"
require "foreman/export"
class Foreman::Export::Runit < Foreman::Export::Base
ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/
def export
error("Must specify a location") unless location
super
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
run_template = export_template('runit', 'run.erb', template_root)
log_run_template = export_template('runit', 'log_run.erb', template_root)
engine.procfile.entries.each do |process|
1.upto(self.concurrency[process.name]) do |num|
process_directory = "#{location}/#{app}-#{process.name}-#{num}"
process_env_directory = "#{process_directory}/env"
process_log_directory = "#{process_directory}/log"
engine.each_process do |name, process|
1.upto(engine.formation[name]) do |num|
process_directory = "#{app}-#{name}-#{num}"
create_directory process_directory
create_directory process_env_directory
create_directory process_log_directory
create_directory "#{process_directory}/env"
create_directory "#{process_directory}/log"
run = ERB.new(run_template).result(binding)
write_file "#{process_directory}/run", run
FileUtils.chmod 0755, "#{process_directory}/run"
write_template "runit/run.erb", "#{process_directory}/run", binding
chmod 0755, "#{process_directory}/run"
port = engine.port_for(process, num, self.port)
environment_variables = {'PORT' => port}.
merge(engine.environment).
merge(inline_variables(process.command))
environment_variables.each_pair do |var, env|
write_file "#{process_env_directory}/#{var.upcase}", env
port = engine.port_for(process, num)
engine.env.merge("PORT" => port.to_s).each do |key, value|
write_file "#{process_directory}/env/#{key}", value
end
log_run = ERB.new(log_run_template).result(binding)
write_file "#{process_log_directory}/run", log_run
FileUtils.chmod 0755, "#{process_log_directory}/run"
write_template "runit/log/run.erb", "#{process_directory}/log/run", binding
chmod 0755, "#{process_directory}/log/run"
end
end
end
private
def create_directory(location)
say "creating: #{location}"
FileUtils.mkdir_p(location)
end
def inline_variables(command)
variable_name_regex =
Hash[*command.scan(ENV_VARIABLE_REGEX).flatten]
end
end
+3 -13
View File
@@ -4,23 +4,13 @@ require "foreman/export"
class Foreman::Export::Supervisord < Foreman::Export::Base
def export
error("Must specify a location") unless location
FileUtils.mkdir_p location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
super
Dir["#{location}/#{app}*.conf"].each do |file|
say "cleaning up: #{file}"
FileUtils.rm(file)
clean file
end
app_template = export_template("supervisord", "app.conf.erb", template_root)
app_config = ERB.new(app_template, 0, '<').result(binding)
write_file "#{location}/#{app}.conf", app_config
write_template "supervisord/app.conf.erb", "#{app}.conf", binding
end
end
+9 -27
View File
@@ -4,40 +4,22 @@ require "foreman/export"
class Foreman::Export::Upstart < Foreman::Export::Base
def export
error("Must specify a location") unless location
FileUtils.mkdir_p location
app = self.app || File.basename(engine.directory)
user = self.user || app
log_root = self.log || "/var/log/#{app}"
template_root = self.template
super
Dir["#{location}/#{app}*.conf"].each do |file|
say "cleaning up: #{file}"
FileUtils.rm(file)
clean file
end
master_template = export_template("upstart", "master.conf.erb", template_root)
master_config = ERB.new(master_template).result(binding)
write_file "#{location}/#{app}.conf", master_config
write_template "upstart/master.conf.erb", "#{app}.conf", binding
process_template = export_template("upstart", "process.conf.erb", template_root)
engine.each_process do |name, process|
next if engine.formation[name] < 1
write_template "upstart/process_master.conf.erb", "#{app}-#{name}.conf", binding
engine.procfile.entries.each do |process|
next if (conc = self.concurrency[process.name]) < 1
process_master_template = export_template("upstart", "process_master.conf.erb", template_root)
process_master_config = ERB.new(process_master_template).result(binding)
write_file "#{location}/#{app}-#{process.name}.conf", process_master_config
1.upto(self.concurrency[process.name]) do |num|
port = engine.port_for(process, num, self.port)
process_config = ERB.new(process_template).result(binding)
write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config
1.upto(engine.formation[name]) do |num|
port = engine.port_for(process, num)
write_template "upstart/process.conf.erb", "#{app}-#{name}-#{num}.conf", binding
end
end
FileUtils.mkdir_p(log_root) rescue error "could not create #{log_root}"
FileUtils.chown(user, nil, log_root) rescue error "could not chown #{log_root} to #{user}"
end
end
+56 -67
View File
@@ -3,94 +3,83 @@ require "rubygems"
class Foreman::Process
attr_reader :entry
attr_reader :num
attr_reader :pid
attr_reader :port
attr_reader :command
attr_reader :env
def initialize(entry, num, port)
@entry = entry
@num = num
@port = port
# Create a Process
#
# @param [String] command The command to run
# @param [Hash] options
#
# @option options [String] :cwd (./) Change to this working directory before executing the process
# @option options [Hash] :env ({}) Environment variables to set for this process
#
def initialize(command, options={})
@command = command
@options = options.dup
@options[:env] ||= {}
end
def run(pipe, basedir, environment)
with_environment(environment.merge("PORT" => port.to_s)) do
run_process basedir, entry.command, pipe
# Run a +Process+
#
# @param [Hash] options
#
# @option options :env ({}) Environment variables to set for this execution
# @option options :output ($stdout) The output stream
#
# @returns [Fixnum] pid The +pid+ of the process
#
def run(options={})
env = options[:env] ? @options[:env].merge(options[:env]) : @options[:env]
output = options[:output] || $stdout
if Foreman.windows?
Dir.chdir(cwd) do
Process.spawn env, command, :out => output, :err => output, :new_pgroup => true
end
elsif Foreman.jruby?
Dir.chdir(cwd) do
require "posix/spawn"
POSIX::Spawn.spawn env, command, :out => output, :err => output, :pgroup => 0
end
else
Dir.chdir(cwd) do
Process.spawn env, command, :out => output, :err => output, :pgroup => 0
end
end
end
def name
"%s.%s" % [ entry.name, num ]
end
# Send a signal to this +Process+
#
# @param [String] signal The signal to send
#
def kill(signal)
pid && Process.kill(signal, pid)
pid && Process.kill(signal, -1 * pid)
rescue Errno::ESRCH
false
end
def detach
pid && Process.detach(pid)
end
# Test whether or not this +Process+ is still running
#
# @returns [Boolean]
#
def alive?
kill(0)
end
# Test whether or not this +Process+ has terminated
#
# @returns [Boolean]
#
def dead?
!alive?
end
private
def fork_with_io(command, basedir)
reader, writer = IO.pipe
command = replace_command_env(command)
pid = if Foreman.windows?
Dir.chdir(basedir) do
Process.spawn command, :out => writer, :err => writer
end
elsif Foreman.jruby?
require "posix/spawn"
POSIX::Spawn.spawn(Foreman.runner, "-d", basedir, command, {
:out => writer, :err => writer
})
else
fork do
writer.sync = true
$stdout.reopen writer
$stderr.reopen writer
reader.close
exec Foreman.runner, "-d", basedir, *command.shellsplit
end
end
[ reader, pid ]
def cwd
@options[:cwd] || "."
end
def run_process(basedir, command, pipe)
io, @pid = fork_with_io(command, basedir)
output pipe, "started with pid %d" % @pid
Thread.new do
until io.eof?
output pipe, io.gets
end
end
end
def output(pipe, message)
pipe.puts "%s,%s" % [ name, message ]
end
def replace_command_env(command)
command.gsub(/\$(\w+)/) { |e| ENV[e[1..-1]] }
end
def with_environment(environment)
original = ENV.to_hash
ENV.update environment
yield
ensure
ENV.replace original
end
end
+67 -33
View File
@@ -1,56 +1,90 @@
require "foreman"
require "foreman/procfile_entry"
# A valid Procfile entry is captured by this regex.
# Reads and writes Procfiles
#
# A valid Procfile entry is captured by this regex:
#
# /^([A-Za-z0-9_]+):\s*(.+)$/
#
# All other lines are ignored.
#
# /^([A-Za-z0-9_]+):\s*(.+)$/
#
# $1 = name
# $2 = command
#
class Foreman::Procfile
attr_reader :entries
# Initialize a Procfile
#
# @param [String] filename (nil) An optional filename to read from
#
def initialize(filename=nil)
@entries = []
load(filename) if filename
end
def [](name)
entries.detect { |entry| entry.name == name }
end
def process_names
entries.map(&:name)
end
def load(filename)
entries.clear
parse_procfile(filename)
end
def write(filename)
File.open(filename, 'w') do |io|
entries.each do |ent|
io.puts(ent)
end
# Yield each +Procfile+ entry in order
#
def entries(&blk)
@entries.each do |(name, command)|
yield name, command
end
end
def <<(entry)
entries << Foreman::ProcfileEntry.new(*entry)
self
# Retrieve a +Procfile+ command by name
#
# @param [String] name The name of the Procfile entry to retrieve
#
def [](name)
@entries.detect { |n,c| name == n }.last
end
# Create a +Procfile+ entry
#
# @param [String] name The name of the +Procfile+ entry to create
# @param [String] command The command of the +Procfile+ entry to create
#
def []=(name, command)
delete name
@entries << [name, command]
end
protected
# Remove a +Procfile+ entry
#
# @param [String] name The name of the +Procfile+ entry to remove
#
def delete(name)
@entries.reject! { |n,c| name == n }
end
def parse_procfile(filename)
# Load a Procfile from a file
#
# @param [String] filename The filename of the +Procfile+ to load
#
def load(filename)
@entries.replace parse(filename)
end
# Save a Procfile to a file
#
# @param [String] filename Save the +Procfile+ to this file
#
def save(filename)
File.open(filename, 'w') do |file|
file.puts self.to_s
end
end
# Get the +Procfile+ as a +String+
#
def to_s
@entries.map do |name, command|
[ name, command ].join(": ")
end.join("\n")
end
private
def parse(filename)
File.read(filename).split("\n").map do |line|
if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/
self << [ $1, $2 ]
[$1, $2]
end
end.compact
end
-26
View File
@@ -1,26 +0,0 @@
require "foreman"
class Foreman::ProcfileEntry
attr_reader :name
attr_reader :command
attr_accessor :color
def initialize(name, command)
@name = name
@command = command
end
def spawn(num, pipe, basedir, environment, base_port)
(1..num).to_a.map do |n|
process = Foreman::Process.new(self, n, base_port + (n-1))
process.run(pipe, basedir, environment)
process
end
end
def to_s
"#{name}: #{command}"
end
end
-37
View File
@@ -1,37 +0,0 @@
require "foreman"
require "foreman/engine"
class Foreman::TmuxEngine < Foreman::Engine
attr_reader :procfile
attr_reader :session
def initialize(procfile, options={})
@procfile = Foreman::Procfile.new(procfile)
@options = options.dup
@session = Time.now.to_i
end
def start
assign_colors
concurrency = Foreman::Utils.parse_concurrency(@options[:concurrency])
ENV['BUNDLE_GEMFILE'] = nil
%x{tmux new-session -d -s #{session}}
procfile.entries.each_with_index do |entry, index|
name = "#{entry.name}.#{concurrency[entry.name]}"
if index == 0
%x{tmux rename-window -t #{session}:#{index} #{name}}
else
%x{tmux new-window -t #{session}:#{index} -n #{name}}
end
%x{tmux pipe-pane -o -t #{session}:#{index} "gawk '{ printf \\"%%s\\", \\"#{$stdout.color(colors[entry.name])}\\"; print strftime(\\"%%H:%%M:%%S\\"), \\"#{pad_process_name(name)} | #{$stdout.color(:reset)}\\", \\$0; fflush(); }' >> /tmp/foreman.#{session}.log"}
%x{tmux send-keys -t #{session}:#{index} "#{entry.command}" C-m}
end
last_index = procfile.entries.length
%x{tmux new-window -t #{session}:#{last_index} -n all}
%x{tmux send-keys -t #{session}:#{last_index} "tail -f /tmp/foreman.#{session}.log" C-m}
Kernel.exec("tmux attach-session -t #{session}")
end
end
-18
View File
@@ -1,18 +0,0 @@
require "foreman"
class Foreman::Utils
def self.parse_concurrency(concurrency)
begin
pairs = concurrency.to_s.gsub(/\s/, "").split(",")
default = concurrency.nil? ? 1 : 0
pairs.inject(Hash.new(default)) do |hash, pair|
process, amount = pair.split("=")
hash.update(process => amount.to_i)
end
end
end
end