Skip to content

Commit

Permalink
spike out interactive mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ddollar committed Aug 2, 2024
1 parent 3a26271 commit 7bdc066
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 27 deletions.
34 changes: 34 additions & 0 deletions lib/foreman/buffer.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions lib/foreman/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 60 additions & 14 deletions lib/foreman/engine.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "foreman"
require "foreman/buffer"
require "foreman/env"
require "foreman/process"
require "foreman/procfile"
Expand Down Expand Up @@ -30,6 +31,7 @@ def initialize(options={})
@options[:formation] ||= "all=1"
@options[:timeout] ||= 5

@buffers = {}
@env = {}
@mutex = Mutex.new
@names = {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(",")

Expand Down Expand Up @@ -350,28 +357,26 @@ 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
@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,
"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
Expand All @@ -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

Expand Down
23 changes: 16 additions & 7 deletions lib/foreman/engine/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions lib/foreman/process.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,7 @@ def cwd
File.expand_path(@options[:cwd] || ".")
end

def interactive?
@options[:interactive]
end
end

0 comments on commit 7bdc066

Please sign in to comment.