From 51a704939ef5f1cc01557929b7d0b0180630e620 Mon Sep 17 00:00:00 2001 From: David Dollar Date: Sun, 10 Jun 2012 22:58:09 -0400 Subject: [PATCH] massive refactoring for programmatic control and stability --- .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 2 + data/example/Procfile | 7 +- data/example/spawnee | 14 + data/example/spawner | 7 + data/export/bluepill/master.pill.erb | 20 +- data/export/launchd/launchd.plist.erb | 6 +- data/export/runit/log/run.erb | 7 + data/export/runit/log_run.erb | 7 - data/export/runit/run.erb | 4 +- data/export/supervisord/app.conf.erb | 24 +- data/export/upstart/master.conf.erb | 4 +- data/export/upstart/process.conf.erb | 6 +- lib/foreman/cli.rb | 77 ++-- lib/foreman/color.rb | 40 -- lib/foreman/engine.rb | 380 ++++++++++-------- lib/foreman/engine/cli.rb | 98 +++++ lib/foreman/env.rb | 27 ++ lib/foreman/export.rb | 1 - lib/foreman/export/base.rb | 93 +++-- lib/foreman/export/bluepill.rb | 20 +- lib/foreman/export/inittab.rb | 19 +- lib/foreman/export/launchd.rb | 20 +- lib/foreman/export/runit.rb | 53 +-- lib/foreman/export/supervisord.rb | 16 +- lib/foreman/export/upstart.rb | 36 +- lib/foreman/process.rb | 123 +++--- lib/foreman/procfile.rb | 100 +++-- lib/foreman/procfile_entry.rb | 26 -- lib/foreman/tmux_engine.rb | 37 -- lib/foreman/utils.rb | 18 - spec/foreman/cli_spec.rb | 196 ++------- spec/foreman/color_spec.rb | 31 -- spec/foreman/engine_spec.rb | 126 +++--- spec/foreman/export/base_spec.rb | 11 +- spec/foreman/export/bluepill_spec.rb | 13 +- spec/foreman/export/inittab_spec.rb | 14 +- spec/foreman/export/launchd_spec.rb | 11 +- spec/foreman/export/runit_spec.rb | 29 +- spec/foreman/export/supervisord_spec.rb | 63 +-- spec/foreman/export/upstart_spec.rb | 41 +- spec/foreman/process_spec.rb | 151 ++----- spec/foreman/procfile_entry_spec.rb | 13 - spec/foreman/procfile_spec.rb | 42 +- spec/foreman/tmux_engine_spec.rb | 75 ---- spec/resources/.env | 1 + spec/resources/Procfile | 4 + spec/resources/bin/echo | 2 + spec/resources/bin/env | 2 + spec/resources/bin/test | 2 + .../export/bluepill/app-concurrency.pill | 8 +- spec/resources/export/bluepill/app.pill | 8 +- .../log/run} | 0 .../{app-alpha-1-run => app-alpha-1/run} | 0 .../log/run} | 0 .../{app-alpha-2-run => app-alpha-2/run} | 0 .../log/run} | 0 .../{app-bravo-1-run => app-bravo-1/run} | 0 .../export/supervisord/app-alpha-1.conf | 24 ++ .../export/supervisord/app-alpha-2.conf | 8 +- .../supervisord/app-env-with-comma.conf | 24 -- .../resources/export/supervisord/app-env.conf | 21 - spec/resources/export/supervisord/app.conf | 24 -- spec/spec_helper.rb | 63 ++- 65 files changed, 984 insertions(+), 1317 deletions(-) create mode 100755 data/example/spawnee create mode 100755 data/example/spawner create mode 100644 data/export/runit/log/run.erb delete mode 100644 data/export/runit/log_run.erb delete mode 100644 lib/foreman/color.rb create mode 100644 lib/foreman/engine/cli.rb create mode 100644 lib/foreman/env.rb delete mode 100644 lib/foreman/procfile_entry.rb delete mode 100644 lib/foreman/tmux_engine.rb delete mode 100644 lib/foreman/utils.rb delete mode 100644 spec/foreman/color_spec.rb delete mode 100644 spec/foreman/procfile_entry_spec.rb delete mode 100644 spec/foreman/tmux_engine_spec.rb create mode 100644 spec/resources/.env create mode 100644 spec/resources/Procfile create mode 100755 spec/resources/bin/echo create mode 100755 spec/resources/bin/env create mode 100755 spec/resources/bin/test rename spec/resources/export/runit/{app-alpha-1-log-run => app-alpha-1/log/run} (100%) rename spec/resources/export/runit/{app-alpha-1-run => app-alpha-1/run} (100%) rename spec/resources/export/runit/{app-alpha-2-log-run => app-alpha-2/log/run} (100%) rename spec/resources/export/runit/{app-alpha-2-run => app-alpha-2/run} (100%) rename spec/resources/export/runit/{app-bravo-1-log-run => app-bravo-1/log/run} (100%) rename spec/resources/export/runit/{app-bravo-1-run => app-bravo-1/run} (100%) create mode 100644 spec/resources/export/supervisord/app-alpha-1.conf delete mode 100644 spec/resources/export/supervisord/app-env-with-comma.conf delete mode 100644 spec/resources/export/supervisord/app-env.conf delete mode 100644 spec/resources/export/supervisord/app.conf diff --git a/.gitignore b/.gitignore index 9f0c521..fa78454 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /.bundle /.rbenv-version +/.yardoc /coverage /example/log/* /man/*.html diff --git a/Gemfile b/Gemfile index b417d1a..3470ea2 100644 --- a/Gemfile +++ b/Gemfile @@ -19,4 +19,5 @@ group :development do gem 'rspec', '~> 2.0' gem "simplecov", :require => false gem 'timecop' + gem 'yard' end diff --git a/Gemfile.lock b/Gemfile.lock index 8410339..691ffa6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,6 +43,7 @@ GEM timecop (0.3.5) win32console (1.3.0-x86-mingw32) xml-simple (1.0.15) + yard (0.8.2) PLATFORMS java @@ -61,3 +62,4 @@ DEPENDENCIES simplecov timecop win32console (~> 1.3.0) + yard diff --git a/data/example/Procfile b/data/example/Procfile index 6dd4b4e..219cd90 100644 --- a/data/example/Procfile +++ b/data/example/Procfile @@ -1,3 +1,4 @@ -ticker: ruby ./ticker $PORT -error: ruby ./error -utf8: ruby ./utf8 +ticker: ruby ./ticker $PORT +error: ruby ./error +utf8: ruby ./utf8 +spawner: ./spawner diff --git a/data/example/spawnee b/data/example/spawnee new file mode 100755 index 0000000..b245cee --- /dev/null +++ b/data/example/spawnee @@ -0,0 +1,14 @@ +#!/bin/sh + +NAME="$1" + +sigterm() { + echo "$NAME: got sigterm" +} + +#trap sigterm SIGTERM + +while true; do + echo "$NAME: ping" + sleep 1 +done diff --git a/data/example/spawner b/data/example/spawner new file mode 100755 index 0000000..8c1a6fc --- /dev/null +++ b/data/example/spawner @@ -0,0 +1,7 @@ +#!/bin/sh + +./spawnee A & +./spawnee B & +./spawnee C & + +wait diff --git a/data/export/bluepill/master.pill.erb b/data/export/bluepill/master.pill.erb index b6a0e89..ed7271c 100644 --- a/data/export/bluepill/master.pill.erb +++ b/data/export/bluepill/master.pill.erb @@ -3,25 +3,25 @@ Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/ app.uid = "<%= user %>" app.gid = "<%= user %>" -<% engine.procfile.entries.each do |process| %> -<% 1.upto(concurrency[process.name]) do |num| %> -<% port = engine.port_for(process, num, self.port) %> - app.process("<%= process.name %>-<%=num%>") do |process| - process.start_command = "<%= process.command.gsub("$PORT", port.to_s) %>" +<% engine.each_process do |name, process| %> +<% 1.upto(engine.formation[name]) do |num| %> + <% port = engine.port_for(process, num) %> + app.process("<%= name %>-<%= num %>") do |process| + process.start_command = "<%= process.command %>" - process.working_dir = "<%= engine.directory %>" + process.working_dir = "<%= engine.root %>" process.daemonize = true - process.environment = {"PORT" => "<%= port %>"<% engine.environment.each_pair do |var,env| %> , "<%= var.upcase %>" => "<%= env %>" <% end %>} + process.environment = <%= engine.env.merge("PORT" => port.to_s).inspect %> process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_grace_time = 45.seconds - process.stdout = process.stderr = "<%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log" + process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log" process.monitor_children do |children| - children.stop_command "kill -QUIT {{PID}}" + children.stop_command "kill {{PID}}" end - process.group = "<%= app %>-<%= process.name %>" + process.group = "<%= app %>-<%= name %>" end <% end %> <% end %> diff --git a/data/export/launchd/launchd.plist.erb b/data/export/launchd/launchd.plist.erb index 6b57fa9..faf2db6 100644 --- a/data/export/launchd/launchd.plist.erb +++ b/data/export/launchd/launchd.plist.erb @@ -3,7 +3,7 @@ Label - <%= "#{app}-#{process.name}-#{num}" %> + <%= "#{app}-#{name}-#{num}" %> ProgramArguments <%= process.command %> @@ -13,10 +13,10 @@ RunAtLoad StandardErrorPath - <%= log_root %>/<%= app %>-<%= process.name %>-<%=num%>.log + <%= log %>/<%= app %>-<%= name %>-<%=num%>.log UserName <%= user %> WorkingDirectory - <%= engine.directory %> + <%= engine.root %> diff --git a/data/export/runit/log/run.erb b/data/export/runit/log/run.erb new file mode 100644 index 0000000..fc9cf56 --- /dev/null +++ b/data/export/runit/log/run.erb @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +LOG=<%= log %>/<%= name %>-<%= num %> + +test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG" +exec chpst -u <%= user %> svlogd "$LOG" diff --git a/data/export/runit/log_run.erb b/data/export/runit/log_run.erb deleted file mode 100644 index ca143c9..0000000 --- a/data/export/runit/log_run.erb +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -set -e - -LOG=<%= log_root %>/<%= process.name %>-<%= num %> - -test -d "$LOG" || mkdir -p m2750 "$LOG" && chown <%= user %> "$LOG" -exec chpst -u <%= user %> svlogd "$LOG" \ No newline at end of file diff --git a/data/export/runit/run.erb b/data/export/runit/run.erb index a992edd..799568f 100644 --- a/data/export/runit/run.erb +++ b/data/export/runit/run.erb @@ -1,3 +1,3 @@ #!/bin/sh -cd <%= engine.directory %> -exec chpst -u <%= user %> -e <%= process_env_directory %> <%= process.command %> \ No newline at end of file +cd <%= engine.root %> +exec chpst -u <%= user %> -e <%= File.join(location, "#{process_directory}/env") %> <%= process.command %> diff --git a/data/export/supervisord/app.conf.erb b/data/export/supervisord/app.conf.erb index bb22c41..95f77bd 100644 --- a/data/export/supervisord/app.conf.erb +++ b/data/export/supervisord/app.conf.erb @@ -1,23 +1,23 @@ <% app_names = [] -engine.procfile.entries.each do |process| - next if (conc = self.concurrency[process.name]) < 1 - 1.upto(self.concurrency[process.name]) do |num| - port = engine.port_for(process, num, self.port) - name = if (conc > 1); "#{process.name}-#{num}" else process.name; end - environment = (engine.environment.keys.sort.map{ |var| %{#{var.upcase}="#{engine.environment[var]}"} } + [%{PORT="#{port}"}]) - app_name = "#{app}-#{name}" - app_names << app_name +engine.each_process do |name, process| + 1.upto(engine.formation[name]) do |num| + port = engine.port_for(process, num) + full_name = "#{app}-#{name}-#{num}" + environment = engine.env.merge("PORT" => port.to_s).map do |key, value| + "#{key}=#{shell_quote(value)}" + end + app_names << full_name %> -[program:<%= app_name %>] +[program:<%= full_name %>] command=<%= process.command %> autostart=true autorestart=true stopsignal=QUIT -stdout_logfile=<%= log_root %>/<%=process.name%>-<%=num%>-out.log -stderr_logfile=<%= log_root %>/<%=process.name%>-<%=num%>-err.log +stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log +stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log user=<%= user %> -directory=<%= engine.directory %> +directory=<%= engine.root %> environment=<%= environment.join(',') %><% end end diff --git a/data/export/upstart/master.conf.erb b/data/export/upstart/master.conf.erb index bff9962..a0b94aa 100644 --- a/data/export/upstart/master.conf.erb +++ b/data/export/upstart/master.conf.erb @@ -1,8 +1,8 @@ pre-start script bash << "EOF" - mkdir -p <%= log_root %> - chown -R <%= user %> <%= log_root %> + mkdir -p <%= log %> + chown -R <%= user %> <%= log %> EOF end script diff --git a/data/export/upstart/process.conf.erb b/data/export/upstart/process.conf.erb index b5c762c..9eb6770 100644 --- a/data/export/upstart/process.conf.erb +++ b/data/export/upstart/process.conf.erb @@ -1,5 +1,5 @@ -start on starting <%= app %>-<%= process.name %> -stop on stopping <%= app %>-<%= process.name %> +start on starting <%= app %>-<%= name %> +stop on stopping <%= app %>-<%= name %> respawn -exec su - <%= user %> -c 'cd <%= engine.directory %>; export PORT=<%= port %>;<% engine.environment.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log_root %>/<%=process.name%>-<%=num%>.log 2>&1' +exec su - <%= user %> -c 'cd <%= engine.root %>; export PORT=<%= port %>;<% engine.env.each_pair do |var,env| %> export <%= var.upcase %>=<%= shell_quote(env) %>; <% end %> <%= process.command %> >> <%= log %>/<%=name%>-<%=num%>.log 2>&1' diff --git a/lib/foreman/cli.rb b/lib/foreman/cli.rb index 1d1461d..8a62b78 100644 --- a/lib/foreman/cli.rb +++ b/lib/foreman/cli.rb @@ -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 diff --git a/lib/foreman/color.rb b/lib/foreman/color.rb deleted file mode 100644 index ff0157b..0000000 --- a/lib/foreman/color.rb +++ /dev/null @@ -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 diff --git a/lib/foreman/engine.rb b/lib/foreman/engine.rb index 416b9ff..9ab6069 100644 --- a/lib/foreman/engine.rb +++ b/lib/foreman/engine.rb @@ -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 diff --git a/lib/foreman/engine/cli.rb b/lib/foreman/engine/cli.rb new file mode 100644 index 0000000..d669aba --- /dev/null +++ b/lib/foreman/engine/cli.rb @@ -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 diff --git a/lib/foreman/env.rb b/lib/foreman/env.rb new file mode 100644 index 0000000..788c1e4 --- /dev/null +++ b/lib/foreman/env.rb @@ -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 diff --git a/lib/foreman/export.rb b/lib/foreman/export.rb index 97f7188..71eb19b 100644 --- a/lib/foreman/export.rb +++ b/lib/foreman/export.rb @@ -24,7 +24,6 @@ module Foreman::Export end - require "foreman/export/base" require "foreman/export/inittab" require "foreman/export/upstart" diff --git a/lib/foreman/export/base.rb b/lib/foreman/export/base.rb index 9580971..fca2844 100644 --- a/lib/foreman/export/base.rb +++ b/lib/foreman/export/base.rb @@ -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 diff --git a/lib/foreman/export/bluepill.rb b/lib/foreman/export/bluepill.rb index b72da2e..1372b5a 100644 --- a/lib/foreman/export/bluepill.rb +++ b/lib/foreman/export/bluepill.rb @@ -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 diff --git a/lib/foreman/export/inittab.rb b/lib/foreman/export/inittab.rb index 31b8036..28b04a8 100644 --- a/lib/foreman/export/inittab.rb +++ b/lib/foreman/export/inittab.rb @@ -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 diff --git a/lib/foreman/export/launchd.rb b/lib/foreman/export/launchd.rb index 003a075..fd73681 100644 --- a/lib/foreman/export/launchd.rb +++ b/lib/foreman/export/launchd.rb @@ -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 diff --git a/lib/foreman/export/runit.rb b/lib/foreman/export/runit.rb index b71b279..ce22d0e 100644 --- a/lib/foreman/export/runit.rb +++ b/lib/foreman/export/runit.rb @@ -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 diff --git a/lib/foreman/export/supervisord.rb b/lib/foreman/export/supervisord.rb index 11eedd6..8a3a4dd 100644 --- a/lib/foreman/export/supervisord.rb +++ b/lib/foreman/export/supervisord.rb @@ -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 diff --git a/lib/foreman/export/upstart.rb b/lib/foreman/export/upstart.rb index 84966a0..01d36ef 100644 --- a/lib/foreman/export/upstart.rb +++ b/lib/foreman/export/upstart.rb @@ -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 diff --git a/lib/foreman/process.rb b/lib/foreman/process.rb index ebd8529..d4f7f59 100644 --- a/lib/foreman/process.rb +++ b/lib/foreman/process.rb @@ -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 diff --git a/lib/foreman/procfile.rb b/lib/foreman/procfile.rb index 6d69569..47238c5 100644 --- a/lib/foreman/procfile.rb +++ b/lib/foreman/procfile.rb @@ -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 diff --git a/lib/foreman/procfile_entry.rb b/lib/foreman/procfile_entry.rb deleted file mode 100644 index 016f089..0000000 --- a/lib/foreman/procfile_entry.rb +++ /dev/null @@ -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 diff --git a/lib/foreman/tmux_engine.rb b/lib/foreman/tmux_engine.rb deleted file mode 100644 index f4f9291..0000000 --- a/lib/foreman/tmux_engine.rb +++ /dev/null @@ -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 diff --git a/lib/foreman/utils.rb b/lib/foreman/utils.rb deleted file mode 100644 index 822f4d3..0000000 --- a/lib/foreman/utils.rb +++ /dev/null @@ -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 diff --git a/spec/foreman/cli_spec.rb b/spec/foreman/cli_spec.rb index d45e747..744516e 100644 --- a/spec/foreman/cli_spec.rb +++ b/spec/foreman/cli_spec.rb @@ -3,12 +3,23 @@ require "foreman/cli" describe "Foreman::CLI", :fakefs do subject { Foreman::CLI.new } - let(:engine) { subject.send(:engine) } - let(:entries) { engine.procfile.entries.inject({}) { |h,e| h.update(e.name => e) } } + + describe ".foreman" do + before { File.open(".foreman", "w") { |f| f.puts "formation: alpha=2" } } + + it "provides default options" do + subject.send(:options)["formation"].should == "alpha=2" + end + + it "is overridden by options at the cli" do + subject = Foreman::CLI.new([], :formation => "alpha=3") + subject.send(:options)["formation"].should == "alpha=3" + end + end describe "start" do - describe "with a non-existent Procfile" do - it "prints an error" do + describe "when a Procfile doesnt exist", :fakefs do + it "displays an error" do mock_error(subject, "Procfile does not exist.") do dont_allow.instance_of(Foreman::Engine).start subject.start @@ -16,175 +27,50 @@ describe "Foreman::CLI", :fakefs do end end - describe "with a Procfile" do - before(:each) { write_procfile } - - it "runs successfully" do - dont_allow(subject).error - mock.instance_of(Foreman::Engine).start - subject.start - end - - it "can run a single process" do - dont_allow(subject).error - stub(engine).watch_for_output - stub(engine).watch_for_termination - mock(entries["alpha"]).spawn(1, is_a(IO), engine.directory, {}, 5000) { [] } - mock(entries["bravo"]).spawn(0, is_a(IO), engine.directory, {}, 5100) { [] } - subject.start("alpha") - end - end - - describe "with an alternate root" do - it "reads the Procfile from that root" do - write_procfile "/some/app/Procfile" - mock(Foreman::Procfile).new("/some/app/Procfile") - mock.instance_of(Foreman::Engine).start - foreman %{ start -d /some/app } - end - end - end - - describe "export" do - describe "options" do - it "uses .foreman" do - write_procfile - File.open(".foreman", "w") { |f| f.puts "concurrency: alpha=2" } - mock_export = mock(Foreman::Export::Upstart) - mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "concurrency" => "alpha=2" }) { mock_export } - mock_export.export - foreman %{ export upstart /upstart } - end - - it "respects --env" do - write_procfile - write_env("envfile") - mock_export = mock(Foreman::Export::Upstart) - mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "env" => "envfile" }) { mock_export } - mock_export.export - foreman %{ export upstart /upstart --env envfile } - end - end - - describe "with a non-existent Procfile" do - it "prints an error" do - mock_error(subject, "Procfile does not exist.") do - dont_allow.instance_of(Foreman::Engine).export - subject.export("testapp") - end - end - end - - describe "with a Procfile" do - before(:each) { write_procfile } - - describe "with a formatter with a generic error" do - before do - mock(Foreman::Export).formatter("errorful") { Class.new(Foreman::Export::Base) do - def export - raise Foreman::Export::Exception.new("foo") - end - end } - end - - it "prints an error" do - mock_error(subject, "foo") do - subject.export("errorful") - end + describe "with a valid Procfile" do + it "can run a single command" do + without_fakefs do + output = foreman("start env -f #{resource_path("Procfile")}") + output.should =~ /env.1/ + output.should_not =~ /test.1/ end end - describe "with a valid config" do - before(:each) { write_foreman_config("testapp") } - - it "runs successfully" do - dont_allow(subject).error - mock_export = mock(Foreman::Export::Upstart) - mock(Foreman::Export::Upstart).new("/tmp/foo", is_a(Foreman::Engine), {}) { mock_export } - mock_export.export - subject.export("upstart", "/tmp/foo") + it "can run all commands" do + without_fakefs do + output = foreman("start -f #{resource_path("Procfile")} -e #{resource_path(".env")}") + output.should =~ /echo.1 \| echoing/ + output.should =~ /env.1 \| bar/ + output.should =~ /test.1 \| testing/ end end end end describe "check" do - describe "with a valid Procfile" do - before { write_procfile } - - it "displays the jobs" do - mock(subject).puts("valid procfile detected (alpha, bravo)") - subject.check - end + it "with a valid Procfile displays the jobs" do + write_procfile + foreman("check").should == "valid procfile detected (alpha, bravo)\n" end - describe "with a blank Procfile" do - before do - FileUtils.touch("Procfile") - end - - it "displays an error" do - mock_error(subject, "no processes defined") do - subject.check - end - end + it "with a blank Procfile displays an error" do + FileUtils.touch "Procfile" + foreman("check").should == "ERROR: no processes defined\n" end - describe "without a Procfile" do - it "displays an error" do - mock_error(subject, "Procfile does not exist.") do - subject.check - end - end + it "without a Procfile displays an error" do + FileUtils.rm_f "Procfile" + foreman("check").should == "ERROR: Procfile does not exist.\n" end end describe "run" do - describe "with a valid Procfile" do - before { write_procfile } + it "can run a command" do + forked_foreman("run echo 1").should == "1\n" + end - describe "and a command" do - let(:command) { ["ls", "-l", "foo bar"] } - - before(:each) do - stub(subject).exec - end - - it "should load the environment file" do - write_env - preserving_env do - subject.run *command - ENV["FOO"].should == "bar" - end - - ENV["FOO"].should be_nil - end - - it "should exec the argument list as a shell command" do - mock(subject).exec(command.shelljoin) - subject.run *command - end - end - - describe "and a non-existent command" do - let(:command) { "iuhtngrglhulhdfg" } - - it "should print an error" do - mock_error(subject, "command not found: #{command}") do - subject.run command - end - end - end - - describe "and a non-executable command" do - let(:command) { __FILE__ } - - it "should print an error" do - mock_error(subject, "not executable: #{command}") do - subject.run command - end - end - end + it "includes the environment" do + forked_foreman("run #{resource_path("bin/env FOO")} -e #{resource_path(".env")}").should == "bar\n" end end diff --git a/spec/foreman/color_spec.rb b/spec/foreman/color_spec.rb deleted file mode 100644 index 011224a..0000000 --- a/spec/foreman/color_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require "spec_helper" -require "foreman/color" - -describe Foreman::Color do - - let(:io) { Object.new } - - it "should extend an object with colorization" do - Foreman::Color.enable(io) - io.should respond_to(:color) - end - - it "should not colorize if the object does not respond to isatty" do - mock(io).respond_to?(:isatty) { false } - Foreman::Color.enable(io) - io.color(:white).should == "" - end - - it "should not colorize if the object is not a tty" do - mock(io).isatty { false } - Foreman::Color.enable(io) - io.color(:white).should == "" - end - - it "should colorize if the object is a tty" do - mock(io).isatty { true } - Foreman::Color.enable(io) - io.color(:white).should == "\e[37m" - end - -end diff --git a/spec/foreman/engine_spec.rb b/spec/foreman/engine_spec.rb index e14344f..d759892 100644 --- a/spec/foreman/engine_spec.rb +++ b/spec/foreman/engine_spec.rb @@ -1,14 +1,25 @@ require "spec_helper" require "foreman/engine" -describe "Foreman::Engine", :fakefs do - subject { Foreman::Engine.new("Procfile", {}) } +class Foreman::Engine::Tester < Foreman::Engine + attr_reader :buffer - before do - any_instance_of(Foreman::Engine) do |engine| - stub(engine).proctitle - stub(engine).termtitle - end + def startup + @buffer = "" + end + + def output(name, data) + @buffer += "#{name}: #{data}" + end + + def shutdown + end +end + +describe "Foreman::Engine", :fakefs do + subject do + write_procfile "Procfile" + Foreman::Engine::Tester.new.load_procfile("Procfile") end describe "initialize" do @@ -16,65 +27,53 @@ describe "Foreman::Engine", :fakefs do before { write_procfile } it "reads the processes" do - subject.procfile["alpha"].command.should == "./alpha" - subject.procfile["bravo"].command.should == "./bravo" + subject.process("alpha").command.should == "./alpha" + subject.process("bravo").command.should == "./bravo" end end end describe "start" do it "forks the processes" do - write_procfile - mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./alpha", is_a(IO)) - mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./bravo", is_a(IO)) + mock(subject.process("alpha")).run(anything) + mock(subject.process("bravo")).run(anything) mock(subject).watch_for_output mock(subject).watch_for_termination subject.start end it "handles concurrency" do - write_procfile - engine = Foreman::Engine.new("Procfile",:concurrency => "alpha=2") - mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./alpha", is_a(IO)).twice - mock.instance_of(Foreman::Process).run_process(Dir.pwd, "./bravo", is_a(IO)).never - mock(engine).watch_for_output - mock(engine).watch_for_termination - engine.start + subject.options[:formation] = "alpha=2" + mock(subject.process("alpha")).run(anything).twice + mock(subject.process("bravo")).run(anything).never + mock(subject).watch_for_output + mock(subject).watch_for_termination + subject.start end end describe "directories" do it "has the directory default relative to the Procfile" do write_procfile "/some/app/Procfile" - engine = Foreman::Engine.new("/some/app/Procfile") - engine.directory.should == "/some/app" + engine = Foreman::Engine.new.load_procfile("/some/app/Procfile") + engine.root.should == "/some/app" end end describe "environment" do - before(:each) do - write_procfile - stub(Process).fork - any_instance_of(Foreman::Engine) do |engine| - stub(engine).info - stub(engine).spawn_processes - stub(engine).watch_for_termination - end - end - - it "should read if specified" do + it "should read env files" do File.open("/tmp/env", "w") { |f| f.puts("FOO=baz") } - engine = Foreman::Engine.new("Procfile", :env => "/tmp/env") - engine.environment.should == {"FOO"=>"baz"} - engine.start + subject.load_env("/tmp/env") + subject.env["FOO"].should == "baz" end it "should read more than one if specified" do File.open("/tmp/env1", "w") { |f| f.puts("FOO=bar") } File.open("/tmp/env2", "w") { |f| f.puts("BAZ=qux") } - engine = Foreman::Engine.new("Procfile", :env => "/tmp/env1,/tmp/env2") - engine.environment.should == { "FOO"=>"bar", "BAZ"=>"qux" } - engine.start + subject.load_env "/tmp/env1" + subject.load_env "/tmp/env2" + subject.env["FOO"].should == "bar" + subject.env["BAZ"].should == "qux" end it "should handle quoted values" do @@ -84,55 +83,22 @@ describe "Foreman::Engine", :fakefs do f.puts "FRED='barney'" f.puts 'OTHER="escaped\"quote"' end - engine = Foreman::Engine.new("Procfile", :env => "/tmp/env") - engine.environment.should == { "FOO" => "bar", "BAZ" => "qux", "FRED" => "barney", "OTHER" => 'escaped"quote' } + subject.load_env "/tmp/env" + subject.env["FOO"].should == "bar" + subject.env["BAZ"].should == "qux" + subject.env["FRED"].should == "barney" + subject.env["OTHER"].should == 'escaped"quote' end it "should fail if specified and doesnt exist" do - mock.instance_of(Foreman::Engine).error("No such file: /tmp/env") - engine = Foreman::Engine.new("Procfile", :env => "/tmp/env") - end - - it "should read .env if none specified" do - File.open(".env", "w") { |f| f.puts("FOO=qoo") } - engine = Foreman::Engine.new("Procfile") - engine.environment.should == {"FOO"=>"qoo"} - engine.start + lambda { subject.load_env "/tmp/env" }.should raise_error(Errno::ENOENT) end it "should set port from .env if specified" do - File.open(".env", "w") { |f| f.puts("PORT=8017") } - engine = Foreman::Engine.new("Procfile") - engine.send(:base_port).should == "8017" - engine.start - end - - it "should be loaded relative to the Procfile" do - FileUtils.mkdir_p "/some/app" - File.open("/some/app/.env", "w") { |f| f.puts("FOO=qoo") } - write_procfile "/some/app/Procfile" - engine = Foreman::Engine.new("/some/app/Procfile") - engine.environment.should == {"FOO"=>"qoo"} - engine.start + File.open("/tmp/env", "w") { |f| f.puts("PORT=9000") } + subject.load_env "/tmp/env" + subject.send(:base_port).should == 9000 end end - describe "utf8" do - before(:each) do - File.open("Procfile", "w") do |file| - file.puts "utf8: #{resource_path("bin/utf8")}" - end - end - - it "should spawn" do - stub(subject).watch_for_output - stub(subject).watch_for_termination - subject.start - Process.waitall - mock(subject).info(/started with pid \d+/, "utf8.1", anything) - mock(subject).info("\xff\x03\n", "utf8.1", anything) - subject.send(:poll_readers) - subject.send(:poll_readers) - end - end end diff --git a/spec/foreman/export/base_spec.rb b/spec/foreman/export/base_spec.rb index c0b1333..4d203ef 100644 --- a/spec/foreman/export/base_spec.rb +++ b/spec/foreman/export/base_spec.rb @@ -1,10 +1,11 @@ require "spec_helper" -require "foreman/export/base" +require "foreman/engine" +require "foreman/export" -describe "Foreman::Export::Base" do +describe "Foreman::Export::Base", :fakefs do let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } let(:location) { "/tmp/init" } - let(:engine) { Foreman::Engine.new(procfile) } + let(:engine) { Foreman::Engine.new().load_procfile(procfile) } let(:subject) { Foreman::Export::Base.new(location, engine) } it "has a say method for displaying info" do @@ -12,10 +13,6 @@ describe "Foreman::Export::Base" do subject.send(:say, "foo") end - it "export needs to be overridden" do - lambda { subject.export }.should raise_error("export method must be overridden") - end - it "raises errors as a Foreman::Export::Exception" do lambda { subject.send(:error, "foo") }.should raise_error(Foreman::Export::Exception, "foo") end diff --git a/spec/foreman/export/bluepill_spec.rb b/spec/foreman/export/bluepill_spec.rb index d6074b6..7d0f4e6 100644 --- a/spec/foreman/export/bluepill_spec.rb +++ b/spec/foreman/export/bluepill_spec.rb @@ -4,10 +4,11 @@ require "foreman/export/bluepill" require "tmpdir" describe Foreman::Export::Bluepill, :fakefs do - let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:engine) { Foreman::Engine.new(procfile) } - let(:options) { Hash.new } - let(:bluepill) { Foreman::Export::Bluepill.new("/tmp/init", engine, options) } + let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } + let(:formation) { nil } + let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } + let(:options) { Hash.new } + let(:bluepill) { Foreman::Export::Bluepill.new("/tmp/init", engine, options) } before(:each) { load_export_templates_into_fakefs("bluepill") } before(:each) { stub(bluepill).say } @@ -24,8 +25,8 @@ describe Foreman::Export::Bluepill, :fakefs do bluepill.export end - context "with concurrency" do - let(:options) { Hash[:concurrency => "alpha=2"] } + context "with a process formation" do + let(:formation) { "alpha=2" } it "exports to the filesystem with concurrency" do bluepill.export diff --git a/spec/foreman/export/inittab_spec.rb b/spec/foreman/export/inittab_spec.rb index 30c921b..2a0d0ed 100644 --- a/spec/foreman/export/inittab_spec.rb +++ b/spec/foreman/export/inittab_spec.rb @@ -4,12 +4,12 @@ require "foreman/export/inittab" require "tmpdir" describe Foreman::Export::Inittab, :fakefs do - let(:location) { "/tmp/inittab" } - let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:location) { "/tmp/inittab" } - let(:engine) { Foreman::Engine.new(procfile) } - let(:options) { Hash.new } - let(:inittab) { Foreman::Export::Inittab.new(location, engine, options) } + let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } + let(:location) { "/tmp/inittab" } + let(:formation) { nil } + let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } + let(:options) { Hash.new } + let(:inittab) { Foreman::Export::Inittab.new(location, engine, options) } before(:each) { load_export_templates_into_fakefs("inittab") } before(:each) { stub(inittab).say } @@ -29,7 +29,7 @@ describe Foreman::Export::Inittab, :fakefs do end context "with concurrency" do - let(:options) { Hash[:concurrency => "alpha=2"] } + let(:formation) { "alpha=2" } it "exports to the filesystem with concurrency" do inittab.export diff --git a/spec/foreman/export/launchd_spec.rb b/spec/foreman/export/launchd_spec.rb index 674c516..984b35c 100644 --- a/spec/foreman/export/launchd_spec.rb +++ b/spec/foreman/export/launchd_spec.rb @@ -5,20 +5,17 @@ require "tmpdir" describe Foreman::Export::Launchd, :fakefs do let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:engine) { Foreman::Engine.new(procfile) } let(:options) { Hash.new } - let(:launchd) { Foreman::Export::Launchd.new("/tmp/init", engine, options) } + let(:engine) { Foreman::Engine.new().load_procfile(procfile) } + let(:launchd) { Foreman::Export::Launchd.new("/tmp/init", engine, options) } before(:each) { load_export_templates_into_fakefs("launchd") } before(:each) { stub(launchd).say } it "exports to the filesystem" do launchd.export - - normalize_space(File.read("/tmp/init/app-alpha-1.plist")).should == normalize_space(example_export_file("launchd/launchd-a.default")) - - normalize_space(File.read("/tmp/init/app-bravo-1.plist")).should == normalize_space(example_export_file("launchd/launchd-b.default")) - + File.read("/tmp/init/app-alpha-1.plist").should == example_export_file("launchd/launchd-a.default") + File.read("/tmp/init/app-bravo-1.plist").should == example_export_file("launchd/launchd-b.default") end end diff --git a/spec/foreman/export/runit_spec.rb b/spec/foreman/export/runit_spec.rb index 6d04ae6..71d4c52 100644 --- a/spec/foreman/export/runit_spec.rb +++ b/spec/foreman/export/runit_spec.rb @@ -5,33 +5,28 @@ require "tmpdir" describe Foreman::Export::Runit, :fakefs do let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", 'bar=baz') } - let(:engine) { Foreman::Engine.new(procfile) } - let(:runit) { Foreman::Export::Runit.new('/tmp/init', engine, :concurrency => 'alpha=2,bravo=1') } + let(:engine) { Foreman::Engine.new(:formation => "alpha=2,bravo=1").load_procfile(procfile) } + let(:options) { Hash.new } + let(:runit) { Foreman::Export::Runit.new('/tmp/init', engine, options) } before(:each) { load_export_templates_into_fakefs("runit") } before(:each) { stub(runit).say } before(:each) { stub(FakeFS::FileUtils).chmod } it "exports to the filesystem" do - FileUtils.mkdir_p('/tmp/init') - + engine.env["BAR"] = "baz" runit.export - File.read("/tmp/init/app-alpha-1/run").should == example_export_file('runit/app-alpha-1-run') - File.read("/tmp/init/app-alpha-1/log/run").should == - example_export_file('runit/app-alpha-1-log-run') + File.read("/tmp/init/app-alpha-1/run").should == example_export_file('runit/app-alpha-1/run') + File.read("/tmp/init/app-alpha-1/log/run").should == example_export_file('runit/app-alpha-1/log/run') File.read("/tmp/init/app-alpha-1/env/PORT").should == "5000\n" - File.read("/tmp/init/app-alpha-1/env/BAR").should == "baz\n" - - File.read("/tmp/init/app-alpha-2/run").should == example_export_file('runit/app-alpha-2-run') - File.read("/tmp/init/app-alpha-2/log/run").should == - example_export_file('runit/app-alpha-2-log-run') + File.read("/tmp/init/app-alpha-1/env/BAR").should == "baz\n" + File.read("/tmp/init/app-alpha-2/run").should == example_export_file('runit/app-alpha-2/run') + File.read("/tmp/init/app-alpha-2/log/run").should == example_export_file('runit/app-alpha-2/log/run') File.read("/tmp/init/app-alpha-2/env/PORT").should == "5001\n" - File.read("/tmp/init/app-alpha-2/env/BAR").should == "baz\n" - - File.read("/tmp/init/app-bravo-1/run").should == example_export_file('runit/app-bravo-1-run') - File.read("/tmp/init/app-bravo-1/log/run").should == - example_export_file('runit/app-bravo-1-log-run') + File.read("/tmp/init/app-alpha-2/env/BAR").should == "baz\n" + File.read("/tmp/init/app-bravo-1/run").should == example_export_file('runit/app-bravo-1/run') + File.read("/tmp/init/app-bravo-1/log/run").should == example_export_file('runit/app-bravo-1/log/run') File.read("/tmp/init/app-bravo-1/env/PORT").should == "5100\n" end diff --git a/spec/foreman/export/supervisord_spec.rb b/spec/foreman/export/supervisord_spec.rb index 2a65991..44c1ad1 100644 --- a/spec/foreman/export/supervisord_spec.rb +++ b/spec/foreman/export/supervisord_spec.rb @@ -4,18 +4,18 @@ require "foreman/export/supervisord" require "tmpdir" describe Foreman::Export::Supervisord, :fakefs do - let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:engine) { Foreman::Engine.new(procfile) } - let(:options) { Hash.new } - let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) } + let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } + let(:formation) { nil } + let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } + let(:options) { Hash.new } + let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) } before(:each) { load_export_templates_into_fakefs("supervisord") } before(:each) { stub(supervisord).say } it "exports to the filesystem" do supervisord.export - - File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app.conf") + File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-alpha-1.conf") end it "cleans up if exporting into an existing dir" do @@ -25,7 +25,7 @@ describe Foreman::Export::Supervisord, :fakefs do end context "with concurrency" do - let(:options) { Hash[:concurrency => "alpha=2"] } + let(:formation) { "alpha=2" } it "exports to the filesystem with concurrency" do supervisord.export @@ -33,53 +33,4 @@ describe Foreman::Export::Supervisord, :fakefs do end end - context "with alternate templates" do - let(:template_root) { "/tmp/alternate" } - let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, :template => template_root) } - - before do - FileUtils.mkdir_p template_root - File.open("#{template_root}/app.conf.erb", "w") { |f| f.puts "alternate_template" } - end - - it "can export with alternate template files" do - supervisord.export - File.read("/tmp/init/app.conf").should == "alternate_template\n" - end - end - - context "with alternate templates from home dir" do - let(:default_template_root) {File.expand_path("#{ENV['HOME']}/.foreman/templates")} - - before do - ENV['_FOREMAN_SPEC_HOME'] = ENV['HOME'] - ENV['HOME'] = "/home/appuser" - FileUtils.mkdir_p default_template_root - File.open("#{default_template_root}/app.conf.erb", "w") { |f| f.puts "default_alternate_template" } - end - - after do - ENV['HOME'] = ENV.delete('_FOREMAN_SPEC_HOME') - end - - it "can export with alternate template files" do - supervisord.export - File.read("/tmp/init/app.conf").should == "default_alternate_template\n" - end - end - - context "environment export" do - it "correctly translates environment when exporting" do - File.open("/tmp/supervisord_env", "w") { |f| f.puts("QUEUE=fastqueue,slowqueue\nVERBOSE=1") } - - engine = Foreman::Engine.new(procfile,:env => "/tmp/supervisord_env") - supervisor = Foreman::Export::Supervisord.new("/tmp/init", engine, options) - stub(supervisor).say - - supervisor.export - - File.read("/tmp/init/app.conf").should == example_export_file("supervisord/app-env-with-comma.conf") - end - end - end diff --git a/spec/foreman/export/upstart_spec.rb b/spec/foreman/export/upstart_spec.rb index 4f357ef..8c41d88 100644 --- a/spec/foreman/export/upstart_spec.rb +++ b/spec/foreman/export/upstart_spec.rb @@ -4,10 +4,11 @@ require "foreman/export/upstart" require "tmpdir" describe Foreman::Export::Upstart, :fakefs do - let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } - let(:engine) { Foreman::Engine.new(procfile) } - let(:options) { Hash.new } - let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, options) } + let(:procfile) { write_procfile("/tmp/app/Procfile") } + let(:formation) { nil } + let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } + let(:options) { Hash.new } + let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, options) } before(:each) { load_export_templates_into_fakefs("upstart") } before(:each) { stub(upstart).say } @@ -34,13 +35,14 @@ describe Foreman::Export::Upstart, :fakefs do end it "quotes and escapes environment variables" do - engine.environment['KEY'] = 'd"\|d' + engine.env['KEY'] = 'd"\|d' upstart.export - File.read("/tmp/init/app-alpha-1.conf").should include('KEY="d\"\\\\|d"') + "foobarfoo".should include "bar" + File.read("/tmp/init/app-alpha-1.conf").should =~ /KEY="d\\"\\\\\\\|d/ end - context "with concurrency" do - let(:options) { Hash[:concurrency => "alpha=2"] } + context "with a formation" do + let(:formation) { "alpha=2" } it "exports to the filesystem with concurrency" do upstart.export @@ -54,38 +56,31 @@ describe Foreman::Export::Upstart, :fakefs do end context "with alternate templates" do - let(:template_root) { "/tmp/alternate" } - let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, :template => template_root) } + let(:template) { "/tmp/alternate" } + let(:options) { { :app => "app", :template => template } } before do - FileUtils.mkdir_p template_root - File.open("#{template_root}/master.conf.erb", "w") { |f| f.puts "alternate_template" } + FileUtils.mkdir_p template + File.open("#{template}/master.conf.erb", "w") { |f| f.puts "alternate_template" } end it "can export with alternate template files" do upstart.export - File.read("/tmp/init/app.conf").should == "alternate_template\n" end end context "with alternate templates from home dir" do - let(:default_template_root) {File.expand_path("#{ENV['HOME']}/.foreman/templates")} before do - ENV['_FOREMAN_SPEC_HOME'] = ENV['HOME'] - ENV['HOME'] = "/home/appuser" - FileUtils.mkdir_p default_template_root - File.open("#{default_template_root}/master.conf.erb", "w") { |f| f.puts "default_alternate_template" } - end - - after do - ENV['HOME'] = ENV.delete('_FOREMAN_SPEC_HOME') + FileUtils.mkdir_p File.expand_path("~/.foreman/templates/upstart") + File.open(File.expand_path("~/.foreman/templates/upstart/master.conf.erb"), "w") do |file| + file.puts "default_alternate_template" + end end it "can export with alternate template files" do upstart.export - File.read("/tmp/init/app.conf").should == "default_alternate_template\n" end end diff --git a/spec/foreman/process_spec.rb b/spec/foreman/process_spec.rb index 1f2d1ea..efc089d 100644 --- a/spec/foreman/process_spec.rb +++ b/spec/foreman/process_spec.rb @@ -5,141 +5,44 @@ require 'timeout' require 'tmpdir' describe Foreman::Process do - subject { described_class.new entry, number, port } - let(:number) { 1 } - let(:port) { 777 } - let(:command) { "script" } - let(:name) { "foobar" } - let(:entry) { OpenStruct.new :name => name, :command => command } + def run(process, options={}) + rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY") + process.run(options.merge(:output => wr)) + rd.gets + end - its(:entry) { entry } - its(:num) { number } - its(:port) { port } - its(:name) { "#{name}.#{port}" } - its(:pid) { nil } + describe "#run" do - describe '#run' do - let(:pipe) { :pipe } - let(:basedir) { Dir.mktmpdir } - let(:env) {{ 'foo' => 'bar' }} - let(:init_delta) { 0.1 } - - after { FileUtils.remove_entry_secure basedir } - - def run(cmd=command) - entry.command = cmd - subject.run pipe, basedir, env - subject.detach && sleep(init_delta) + it "runs the process" do + process = Foreman::Process.new(resource_path("bin/test")) + run(process).should == "testing\n" end - def run_file(executable, code) - file = File.open("#{basedir}/script", 'w') {|it| it << code } - run "#{executable} #{file.path}" - sleep 1 + it "can set environment" do + process = Foreman::Process.new(resource_path("bin/env FOO"), :env => { "FOO" => "bar" }) + run(process).should == "bar\n" end - context 'options' do - it 'should set PORT for environment' do - mock(subject).run_process(basedir, command, pipe) do - ENV['PORT'].should == port.to_s - end - run - end - - it 'should set custom variables for environment' do - mock(subject).run_process(basedir, command, pipe) do - ENV['foo'].should == 'bar' - end - run - end - - it 'should restore environment afterwards' do - mock(subject).run_process(basedir, command, pipe) - run - ENV.should_not include('PORT', 'foo') - end + it "can set per-run environment" do + process = Foreman::Process.new(resource_path("bin/env FOO")) + run(process, :env => { "FOO" => "bar "}).should == "bar\n" end - context 'process' do - around do |spec| - IO.pipe do |reader, writer| - @reader, @writer = reader, writer - spec.run - end - end + it "can handle env vars in the command" do + process = Foreman::Process.new(resource_path("bin/echo $FOO"), :env => { "FOO" => "bar" }) + run(process).should == "bar\n" + end - let(:pipe) { @writer } - let(:output) { @reader.read_nonblock 1024 } + it "can handle per-run env vars in the command" do + process = Foreman::Process.new(resource_path("bin/echo $FOO")) + run(process, :env => { "FOO" => "bar" }).should == "bar\n" + end - it 'should not block' do - expect { - Timeout.timeout(2*init_delta) { run 'sleep 2' } - }.should_not raise_exception - end - - it 'should be alive' do - run 'sleep 1' - subject.should be_alive - end - - it 'should be dead' do - run 'exit' - subject.should be_dead - end - - it 'should be killable' do - run 'sleep 1' - subject.kill 'TERM' - subject.should be_dead - end - - it 'should send different signals' do - run_file 'ruby', <<-CODE - trap "TERM", "IGNORE" - loop { sleep 1 } - CODE - subject.should be_alive - subject.kill 'TERM' - subject.should be_alive - subject.kill 'KILL' - subject.should be_dead - end - - it 'should redirect stdout' do - run 'echo hey' - output.should include('hey') - end - - it 'should redirect stderr' do - run 'echo hey >2' - output.should include('hey') - end - - it 'should handle variables' do - run 'echo $PORT' - output.should include('777') - end - - it 'should handle multi-word arguments (old test)' do - # TODO: This test used to be marked pending; it now passes, - # but is very slow. The next test is a fast replacement. - run %{ sh -c "trap '' TERM; sleep 10" } - subject.should be_alive - end - - it 'should handle multi-word arguments' do - # We have to be a little clever here since Foreman will always - # print a status message that includes the command. - run %{ sh -c 'echo abcdef | tr a-c x | tr d-f y' } - output.should include('xxxyyy') - end - - it 'should not clobber "$x"-subexpressions' do - pending 'this conflicts with the variable interpolation hack' - run %{ sh -c 'echo \$abcdef | tr \$ %' } - output.should include('%abcdef') - end + it "should output utf8 properly" do + process = Foreman::Process.new(resource_path("bin/utf8")) + run(process).should == "\xFF\x03\n" end end + end diff --git a/spec/foreman/procfile_entry_spec.rb b/spec/foreman/procfile_entry_spec.rb deleted file mode 100644 index daa55f2..0000000 --- a/spec/foreman/procfile_entry_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' -require 'foreman/procfile_entry' -require 'pathname' -require 'tmpdir' - -describe Foreman::ProcfileEntry do - subject { described_class.new('alpha', './alpha') } - - it "stringifies as a Procfile line" do - subject.to_s.should == 'alpha: ./alpha' - end - -end diff --git a/spec/foreman/procfile_spec.rb b/spec/foreman/procfile_spec.rb index a01e3a3..b447a72 100644 --- a/spec/foreman/procfile_spec.rb +++ b/spec/foreman/procfile_spec.rb @@ -3,29 +3,39 @@ require 'foreman/procfile' require 'pathname' require 'tmpdir' -describe Foreman::Procfile do - subject { described_class.new } +describe Foreman::Procfile, :fakefs do + subject { Foreman::Procfile.new } - let(:testdir) { Pathname(Dir.tmpdir) } - let(:procfile) { testdir + 'Procfile' } + it "can load from a file" do + write_procfile + subject.load "Procfile" + subject["alpha"].should == "./alpha" + subject["bravo"].should == "./bravo" + end + + it "loads a passed-in Procfile" do + write_procfile + procfile = Foreman::Procfile.new("Procfile") + procfile["alpha"].should == "./alpha" + procfile["bravo"].should == "./bravo" + end it "can have a process appended to it" do - subject << ['alpha', './alpha'] - subject['alpha'].should be_a(Foreman::ProcfileEntry) + subject["charlie"] = "./charlie" + subject["charlie"].should == "./charlie" end - it "can write itself out to a file" do - subject << ['alpha', './alpha'] - subject.write(procfile) - procfile.read.should == "alpha: ./alpha\n" + it "can write to a string" do + subject["foo"] = "./foo" + subject["bar"] = "./bar" + subject.to_s.should == "foo: ./foo\nbar: ./bar" end - it "can re-read entries from a file" do - procfile.open('w') { |io| io.puts "gamma: ./radiation", "theta: ./rate" } - subject << ['alpha', './alpha'] - subject.load(procfile) - subject.process_names.should have(2).members - subject.process_names.should include('gamma', 'theta') + it "can write to a file" do + subject["foo"] = "./foo" + subject["bar"] = "./bar" + subject.save "/tmp/proc" + File.read("/tmp/proc").should == "foo: ./foo\nbar: ./bar\n" end end diff --git a/spec/foreman/tmux_engine_spec.rb b/spec/foreman/tmux_engine_spec.rb deleted file mode 100644 index 7b4d15f..0000000 --- a/spec/foreman/tmux_engine_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -require "spec_helper" -require "foreman/tmux_engine" - -describe "Foreman::TmuxEngine", :fakefs do - subject { Foreman::TmuxEngine.new("Procfile", {}) } - let(:session) { Time.now.to_i } - - before do - any_instance_of(Foreman::TmuxEngine) do |engine| - stub(engine).proctitle - stub(engine).termtitle - end - Timecop.freeze(Time.now) - end - - after do - FileUtils.rm_f("foreman.#{session}.log") - Timecop.return - end - - describe "initialize" do - describe "without an existing Procfile" do - it "raises an error" do - lambda { subject }.should raise_error - end - end - - describe "with a Procfile" do - before { write_procfile } - - it "reads the processes" do - subject.procfile["alpha"].command.should == "./alpha" - subject.procfile["bravo"].command.should == "./bravo" - end - end - end - - if system("which tmux") - describe "start" do - before do - write_procfile - @pid = fork do - exec("tmux start-server") - end - end - - after do - Process.waitpid(@pid) - %x{tmux kill-session -t #{session} &> /dev/null} - end - - it "creates a tmux session and attaches" do - %x{tmux has-session -t #{session} &> /dev/null} - $?.exitstatus.should == 1 - - mock(Kernel).exec("tmux attach-session -t #{session}") - subject.start - - %x{tmux has-session -t #{session}} - $?.exitstatus.should == 0 - - %x{tmux list-windows -t #{session}}.split("\n").map { |window| - if window =~ /[0-9]+:\s(.+?)\s/ - $1 - end - }.should == ["alpha.1", "bravo.1", "all"] - sleep 1 - %x{tmux kill-session -t #{session}} - output = %x[cat /tmp/foreman.#{session}.log] - output.should =~ /alpha\.1.+\.\/alpha: No such file or directory/ - output.should =~ /bravo\.1.+\.\/bravo: No such file or directory/ - end - end - end -end diff --git a/spec/resources/.env b/spec/resources/.env new file mode 100644 index 0000000..c075a74 --- /dev/null +++ b/spec/resources/.env @@ -0,0 +1 @@ +FOO=bar diff --git a/spec/resources/Procfile b/spec/resources/Procfile new file mode 100644 index 0000000..f4bcb95 --- /dev/null +++ b/spec/resources/Procfile @@ -0,0 +1,4 @@ +echo: bin/echo echoing +env: bin/env FOO +test: bin/test +utf8: bin/utf8 diff --git a/spec/resources/bin/echo b/spec/resources/bin/echo new file mode 100755 index 0000000..94704ce --- /dev/null +++ b/spec/resources/bin/echo @@ -0,0 +1,2 @@ +#!/bin/sh +echo $* diff --git a/spec/resources/bin/env b/spec/resources/bin/env new file mode 100755 index 0000000..ee0f02e --- /dev/null +++ b/spec/resources/bin/env @@ -0,0 +1,2 @@ +#!/bin/sh +echo ${!1} diff --git a/spec/resources/bin/test b/spec/resources/bin/test new file mode 100755 index 0000000..1b3f303 --- /dev/null +++ b/spec/resources/bin/test @@ -0,0 +1,2 @@ +#!/bin/sh +echo "testing" diff --git a/spec/resources/export/bluepill/app-concurrency.pill b/spec/resources/export/bluepill/app-concurrency.pill index b8e19ef..fa1e2db 100644 --- a/spec/resources/export/bluepill/app-concurrency.pill +++ b/spec/resources/export/bluepill/app-concurrency.pill @@ -11,14 +11,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil process.working_dir = "/tmp/app" process.daemonize = true - process.environment = {"PORT" => "5000"} + process.environment = {"PORT"=>"5000"} process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_grace_time = 45.seconds process.stdout = process.stderr = "/var/log/app/app-alpha-1.log" process.monitor_children do |children| - children.stop_command "kill -QUIT {{PID}}" + children.stop_command "kill {{PID}}" end process.group = "app-alpha" @@ -30,14 +30,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil process.working_dir = "/tmp/app" process.daemonize = true - process.environment = {"PORT" => "5001"} + process.environment = {"PORT"=>"5001"} process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_grace_time = 45.seconds process.stdout = process.stderr = "/var/log/app/app-alpha-2.log" process.monitor_children do |children| - children.stop_command "kill -QUIT {{PID}}" + children.stop_command "kill {{PID}}" end process.group = "app-alpha" diff --git a/spec/resources/export/bluepill/app.pill b/spec/resources/export/bluepill/app.pill index 800b480..9df8c72 100644 --- a/spec/resources/export/bluepill/app.pill +++ b/spec/resources/export/bluepill/app.pill @@ -11,14 +11,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil process.working_dir = "/tmp/app" process.daemonize = true - process.environment = {"PORT" => "5000"} + process.environment = {"PORT"=>"5000"} process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_grace_time = 45.seconds process.stdout = process.stderr = "/var/log/app/app-alpha-1.log" process.monitor_children do |children| - children.stop_command "kill -QUIT {{PID}}" + children.stop_command "kill {{PID}}" end process.group = "app-alpha" @@ -29,14 +29,14 @@ Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepil process.working_dir = "/tmp/app" process.daemonize = true - process.environment = {"PORT" => "5100"} + process.environment = {"PORT"=>"5100"} process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] process.stop_grace_time = 45.seconds process.stdout = process.stderr = "/var/log/app/app-bravo-1.log" process.monitor_children do |children| - children.stop_command "kill -QUIT {{PID}}" + children.stop_command "kill {{PID}}" end process.group = "app-bravo" diff --git a/spec/resources/export/runit/app-alpha-1-log-run b/spec/resources/export/runit/app-alpha-1/log/run similarity index 100% rename from spec/resources/export/runit/app-alpha-1-log-run rename to spec/resources/export/runit/app-alpha-1/log/run diff --git a/spec/resources/export/runit/app-alpha-1-run b/spec/resources/export/runit/app-alpha-1/run similarity index 100% rename from spec/resources/export/runit/app-alpha-1-run rename to spec/resources/export/runit/app-alpha-1/run diff --git a/spec/resources/export/runit/app-alpha-2-log-run b/spec/resources/export/runit/app-alpha-2/log/run similarity index 100% rename from spec/resources/export/runit/app-alpha-2-log-run rename to spec/resources/export/runit/app-alpha-2/log/run diff --git a/spec/resources/export/runit/app-alpha-2-run b/spec/resources/export/runit/app-alpha-2/run similarity index 100% rename from spec/resources/export/runit/app-alpha-2-run rename to spec/resources/export/runit/app-alpha-2/run diff --git a/spec/resources/export/runit/app-bravo-1-log-run b/spec/resources/export/runit/app-bravo-1/log/run similarity index 100% rename from spec/resources/export/runit/app-bravo-1-log-run rename to spec/resources/export/runit/app-bravo-1/log/run diff --git a/spec/resources/export/runit/app-bravo-1-run b/spec/resources/export/runit/app-bravo-1/run similarity index 100% rename from spec/resources/export/runit/app-bravo-1-run rename to spec/resources/export/runit/app-bravo-1/run diff --git a/spec/resources/export/supervisord/app-alpha-1.conf b/spec/resources/export/supervisord/app-alpha-1.conf new file mode 100644 index 0000000..d365588 --- /dev/null +++ b/spec/resources/export/supervisord/app-alpha-1.conf @@ -0,0 +1,24 @@ + +[program:app-alpha-1] +command=./alpha +autostart=true +autorestart=true +stopsignal=QUIT +stdout_logfile=/var/log/app/alpha-1.log +stderr_logfile=/var/log/app/alpha-1.error.log +user=app +directory=/tmp/app +environment=PORT="5000" +[program:app-bravo-1] +command=./bravo +autostart=true +autorestart=true +stopsignal=QUIT +stdout_logfile=/var/log/app/bravo-1.log +stderr_logfile=/var/log/app/bravo-1.error.log +user=app +directory=/tmp/app +environment=PORT="5100" + +[group:app] +programs=app-alpha-1,app-bravo-1 diff --git a/spec/resources/export/supervisord/app-alpha-2.conf b/spec/resources/export/supervisord/app-alpha-2.conf index 972117d..2160314 100644 --- a/spec/resources/export/supervisord/app-alpha-2.conf +++ b/spec/resources/export/supervisord/app-alpha-2.conf @@ -4,8 +4,8 @@ command=./alpha autostart=true autorestart=true stopsignal=QUIT -stdout_logfile=/var/log/app/alpha-1-out.log -stderr_logfile=/var/log/app/alpha-1-err.log +stdout_logfile=/var/log/app/alpha-1.log +stderr_logfile=/var/log/app/alpha-1.error.log user=app directory=/tmp/app environment=PORT="5000" @@ -14,8 +14,8 @@ command=./alpha autostart=true autorestart=true stopsignal=QUIT -stdout_logfile=/var/log/app/alpha-2-out.log -stderr_logfile=/var/log/app/alpha-2-err.log +stdout_logfile=/var/log/app/alpha-2.log +stderr_logfile=/var/log/app/alpha-2.error.log user=app directory=/tmp/app environment=PORT="5001" diff --git a/spec/resources/export/supervisord/app-env-with-comma.conf b/spec/resources/export/supervisord/app-env-with-comma.conf deleted file mode 100644 index 6b50836..0000000 --- a/spec/resources/export/supervisord/app-env-with-comma.conf +++ /dev/null @@ -1,24 +0,0 @@ - -[program:app-alpha] -command=./alpha -autostart=true -autorestart=true -stopsignal=QUIT -stdout_logfile=/var/log/app/alpha-1-out.log -stderr_logfile=/var/log/app/alpha-1-err.log -user=app -directory=/tmp/app -environment=QUEUE="fastqueue,slowqueue",VERBOSE="1",PORT="5000" -[program:app-bravo] -command=./bravo -autostart=true -autorestart=true -stopsignal=QUIT -stdout_logfile=/var/log/app/bravo-1-out.log -stderr_logfile=/var/log/app/bravo-1-err.log -user=app -directory=/tmp/app -environment=QUEUE="fastqueue,slowqueue",VERBOSE="1",PORT="5100" - -[group:app] -programs=app-alpha,app-bravo diff --git a/spec/resources/export/supervisord/app-env.conf b/spec/resources/export/supervisord/app-env.conf deleted file mode 100644 index 23df258..0000000 --- a/spec/resources/export/supervisord/app-env.conf +++ /dev/null @@ -1,21 +0,0 @@ - -[program:app-alpha] -command=./alpha -autostart=true -autorestart=true -stopsignal=QUIT -stdout_logfile=/var/log/app/alpha-1-out.log -stderr_logfile=/var/log/app/alpha-1-err.log -user=app -directory=/tmp/app -environment=FOO="bar",PORT="5000" -[program:app-bravo] -command=./bravo -autostart=true -autorestart=true -stopsignal=QUIT -stdout_logfile=/var/log/app/bravo-1-out.log -stderr_logfile=/var/log/app/bravo-1-err.log -user=app -directory=/tmp/app -environment=FOO="bar",PORT="5100" diff --git a/spec/resources/export/supervisord/app.conf b/spec/resources/export/supervisord/app.conf deleted file mode 100644 index abf811a..0000000 --- a/spec/resources/export/supervisord/app.conf +++ /dev/null @@ -1,24 +0,0 @@ - -[program:app-alpha] -command=./alpha -autostart=true -autorestart=true -stopsignal=QUIT -stdout_logfile=/var/log/app/alpha-1-out.log -stderr_logfile=/var/log/app/alpha-1-err.log -user=app -directory=/tmp/app -environment=PORT="5000" -[program:app-bravo] -command=./bravo -autostart=true -autorestart=true -stopsignal=QUIT -stdout_logfile=/var/log/app/bravo-1-out.log -stderr_logfile=/var/log/app/bravo-1-err.log -user=app -directory=/tmp/app -environment=PORT="5100" - -[group:app] -programs=app-alpha,app-bravo diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6f2da46..f7f8538 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -24,7 +24,39 @@ def mock_error(subject, message) end def foreman(args) - Foreman::CLI.start(args.split(" ")) + capture_stdout do + begin + Foreman::CLI.start(args.split(" ")) + rescue SystemExit + end + end +end + +def forked_foreman(args) + rd, wr = IO.pipe("BINARY") + Process.spawn("bundle exec bin/foreman #{args}", :out => wr, :err => wr) + wr.close + rd.read +end + +def fork_and_capture(&blk) + rd, wr = IO.pipe("BINARY") + pid = fork do + rd.close + wr.sync = true + $stdout.reopen wr + $stderr.reopen wr + blk.call + $stdout.flush + $stdout.close + end + wr.close + Process.wait pid + buffer = "" + until rd.eof? + p [:foo] + buffer += rd.gets + end end def mock_exit(&block) @@ -56,13 +88,21 @@ def write_env(env=".env", options={"FOO"=>"bar"}) end end -def load_export_templates_into_fakefs(type) +def without_fakefs FakeFS.deactivate! - files = Dir[File.expand_path("../../data/export/#{type}/**", __FILE__)].inject({}) do |hash, file| - hash.update(file => File.read(file)) - end + ret = yield FakeFS.activate! - files.each do |filename, contents| + ret +end + +def load_export_templates_into_fakefs(type) + without_fakefs do + Dir[File.expand_path("../../data/export/#{type}/**/*", __FILE__)].inject({}) do |hash, file| + next(hash) if File.directory?(file) + hash.update(file => File.read(file)) + end + end.each do |filename, contents| + FileUtils.mkdir_p File.dirname(filename) File.open(filename, "w") do |f| f.puts contents end @@ -94,6 +134,17 @@ def normalize_space(s) s.gsub(/\n[\n\s]*/, "\n") end +def capture_stdout + old_stdout = $stdout.dup + rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY") + $stdout = wr + yield + wr.close + rd.read +ensure + $stdout = old_stdout +end + RSpec.configure do |config| config.treat_symbols_as_metadata_keys_with_true_values = true config.color_enabled = true