diff --git a/lib/foreman/buffer.rb b/lib/foreman/buffer.rb new file mode 100644 index 00000000..b34f498d --- /dev/null +++ b/lib/foreman/buffer.rb @@ -0,0 +1,34 @@ +ANSI_TOKEN = /\e\[(?:\??\d{1,4}(?:;\d{0,4})*)?[A-Za-z]/ +NEWLINE_TOKEN = /\n/ +TOKENIZER = Regexp.new("(#{ANSI_TOKEN}|#{NEWLINE_TOKEN})") + +class Buffer + @buffer = '' + + def initialize(initial = '') + @buffer = initial + end + + def each_token + remainder = '' + @buffer.split(TOKENIZER).each do |token| + if token.include?("\e") && !token.match(ANSI_TOKEN) + remainder << token + else + yield token unless token.empty? + end + end + @buffer = remainder + end + + def gets + return nil unless @buffer.include?("\n") + + line, @buffer = @buffer.split("\n", 2) + line + end + + def write(data) + @buffer << data + end +end diff --git a/lib/foreman/cli.rb b/lib/foreman/cli.rb index f0affefb..91bda63c 100644 --- a/lib/foreman/cli.rb +++ b/lib/foreman/cli.rb @@ -19,12 +19,13 @@ class Foreman::CLI < Foreman::Thor desc "start [PROCESS]", "Start the application (or a specific PROCESS)" - method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled" - 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"', :desc => 'Specify what processes will run and how many. Default: "all=1"' - method_option :port, :type => :numeric, :aliases => "-p" - method_option :timeout, :type => :numeric, :aliases => "-t", :desc => "Specify the amount of time (in seconds) processes have to shutdown gracefully before receiving a SIGKILL, defaults to 5." - method_option :timestamp, :type => :boolean, :default => true, :desc => "Include timestamp in output" + method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled" + 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"', :desc => 'Specify what processes will run and how many. Default: "all=1"' + method_option :interactive, :type => :string, :aliases => "-i", :desc => "Run a process interactively" + method_option :port, :type => :numeric, :aliases => "-p" + method_option :timeout, :type => :numeric, :aliases => "-t", :desc => "Specify the amount of time (in seconds) processes have to shutdown gracefully before receiving a SIGKILL, defaults to 5." + method_option :timestamp, :type => :boolean, :default => false, :desc => "Include timestamp in output" class << self # Hackery. Take the run method away from Thor so that we can redefine it. diff --git a/lib/foreman/engine.rb b/lib/foreman/engine.rb index a1316593..db47da83 100644 --- a/lib/foreman/engine.rb +++ b/lib/foreman/engine.rb @@ -1,4 +1,5 @@ require "foreman" +require "foreman/buffer" require "foreman/env" require "foreman/process" require "foreman/procfile" @@ -30,6 +31,7 @@ def initialize(options={}) @options[:formation] ||= "all=1" @options[:timeout] ||= 5 + @buffers = {} @env = {} @mutex = Mutex.new @names = {} @@ -148,6 +150,7 @@ def handle_signal_forward(signal) def register(name, command, options={}) options[:env] ||= env options[:cwd] ||= File.dirname(command.split(" ").first) + options[:interactive] ||= @options[:interactive] == name process = Foreman::Process.new(command, options) @names[process] = name @processes << process @@ -320,6 +323,10 @@ def name_for_index(process, index) [ @names[process], index.to_s ].compact.join(".") end + def process_for(reader) + @running[@readers.invert[reader]].first + end + def parse_formation(formation) pairs = formation.to_s.gsub(/\s/, "").split(",") @@ -350,13 +357,6 @@ def termination_message_for(status) 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 @@ -364,14 +364,19 @@ def spawn_processes 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, - "PS" => name_for_index(process, n) - }) + pid = process.run( + input: process.interactive? ? $stdin : :close, + output: writer, + env: { + 'PORT' => port_for(process, n).to_s, + 'PS' => name_for_index(process, n) + } + ) writer.puts "started with pid #{pid}" rescue Errno::ENOENT writer.puts "unknown command: #{process.command}" end + @buffers[reader] = Buffer.new @running[pid] = [process, n] @readers[pid] = reader end @@ -395,11 +400,52 @@ def handle_io(readers) next if reader == @selfpipe[:reader] if reader.eof? - @readers.delete_if { |key, value| value == reader } + @buffers.delete(reader) + @readers.delete_if { |_key, value| value == reader } + elsif process_for(reader).interactive? + handle_io_interactive reader else - data = reader.gets - output_with_mutex name_for(@readers.invert[reader]), data + handle_io_noninteractive reader + end + end + end + + def handle_io_interactive(reader) + done = false + name = name_for(@readers.invert[reader]) + + output_partial prefix(name) + + loop do + @buffers[reader].write(reader.read_nonblock(10)) + + @buffers[reader].each_token do |token| + case token + when /^\e\[(\d+)G$/ + output_partial "\e[#{::Regexp.last_match(1).to_i + prefix(name).gsub(ANSI_TOKEN, "").length}G" + when ANSI_TOKEN + output_partial token + when "\n" + output_partial token + output_partial prefix(name) + else + output_partial token + end + done = (token == "\n") end + rescue IO::WaitReadable + retry if IO.select([reader], [], [], 1) + return if done + rescue EOFError + end + ensure + output_partial "\n" + end + + def handle_io_noninteractive(reader) + @buffers[reader].write(reader.read_nonblock(10)) + while line = @buffers[reader].gets + output_with_mutex name_for(@readers.invert[reader]), line end end diff --git a/lib/foreman/engine/cli.rb b/lib/foreman/engine/cli.rb index 1cbc66f4..8a9476ce 100644 --- a/lib/foreman/engine/cli.rb +++ b/lib/foreman/engine/cli.rb @@ -55,19 +55,28 @@ def startup def output(name, data) data.to_s.lines.map(&:chomp).each do |message| - output = "" - output += $stdout.color(@colors[name.split(".").first].to_sym) - output += "#{Time.now.strftime("%H:%M:%S")} " if options[:timestamp] - output += "#{pad_process_name(name)} | " - output += $stdout.color(:reset) - output += message - $stdout.puts output + $stdout.write prefix(name) + $stdout.puts message $stdout.flush end rescue Errno::EPIPE terminate_gracefully end + def output_partial(data) + $stdout.write data + $stdout.flush + end + + def prefix(name) + output = '' + output += $stdout.color(@colors[name.split('.').first].to_sym) + output += "#{Time.now.strftime('%H:%M:%S')} " if options[:timestamp] + output += "#{pad_process_name(name)} | " + output += $stdout.color(:reset) + output + end + def shutdown end diff --git a/lib/foreman/process.rb b/lib/foreman/process.rb index ee3de948..f5d23d91 100644 --- a/lib/foreman/process.rb +++ b/lib/foreman/process.rb @@ -77,4 +77,7 @@ def cwd File.expand_path(@options[:cwd] || ".") end + def interactive? + @options[:interactive] + end end