Skip to content

Commit 3bb2663

Browse files
committed
spike out interactive mode
1 parent 3a26271 commit 3bb2663

File tree

5 files changed

+120
-28
lines changed

5 files changed

+120
-28
lines changed

lib/foreman/buffer.rb

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
ANSI_TOKEN = /\e\[(?:\??\d{1,4}(?:;\d{0,4})*)?[A-Za-z]/
2+
NEWLINE_TOKEN = /\n/
3+
TOKENIZER = Regexp.new("(#{ANSI_TOKEN}|#{NEWLINE_TOKEN})")
4+
5+
class Buffer
6+
@buffer = ''
7+
8+
def initialize(initial = '')
9+
@buffer = initial
10+
end
11+
12+
def each_token
13+
remainder = ''
14+
@buffer.split(TOKENIZER).each do |token|
15+
if token.include?("\e") && !token.match(ANSI_TOKEN)
16+
remainder << token
17+
else
18+
yield token unless token.empty?
19+
end
20+
end
21+
@buffer = remainder
22+
end
23+
24+
def gets
25+
return nil unless @buffer.include?("\n")
26+
27+
line, @buffer = @buffer.split("\n", 2)
28+
line
29+
end
30+
31+
def write(data)
32+
@buffer << data
33+
end
34+
end

lib/foreman/cli.rb

+7-6
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ class Foreman::CLI < Foreman::Thor
1919

2020
desc "start [PROCESS]", "Start the application (or a specific PROCESS)"
2121

22-
method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled"
23-
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
24-
method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"', :desc => 'Specify what processes will run and how many. Default: "all=1"'
25-
method_option :port, :type => :numeric, :aliases => "-p"
26-
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."
27-
method_option :timestamp, :type => :boolean, :default => true, :desc => "Include timestamp in output"
22+
method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled"
23+
method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env"
24+
method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"', :desc => 'Specify what processes will run and how many. Default: "all=1"'
25+
method_option :interactive, :type => :string, :aliases => "-i", :desc => "Run a process interactively"
26+
method_option :port, :type => :numeric, :aliases => "-p"
27+
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."
28+
method_option :timestamp, :type => :boolean, :default => false, :desc => "Include timestamp in output"
2829

2930
class << self
3031
# Hackery. Take the run method away from Thor so that we can redefine it.

lib/foreman/engine.rb

+60-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "foreman"
2+
require "foreman/buffer"
23
require "foreman/env"
34
require "foreman/process"
45
require "foreman/procfile"
@@ -30,6 +31,7 @@ def initialize(options={})
3031
@options[:formation] ||= "all=1"
3132
@options[:timeout] ||= 5
3233

34+
@buffers = {}
3335
@env = {}
3436
@mutex = Mutex.new
3537
@names = {}
@@ -148,6 +150,7 @@ def handle_signal_forward(signal)
148150
def register(name, command, options={})
149151
options[:env] ||= env
150152
options[:cwd] ||= File.dirname(command.split(" ").first)
153+
options[:interactive] ||= @options[:interactive] == name
151154
process = Foreman::Process.new(command, options)
152155
@names[process] = name
153156
@processes << process
@@ -320,6 +323,10 @@ def name_for_index(process, index)
320323
[ @names[process], index.to_s ].compact.join(".")
321324
end
322325

326+
def process_for(reader)
327+
@running[@readers.invert[reader]].first
328+
end
329+
323330
def parse_formation(formation)
324331
pairs = formation.to_s.gsub(/\s/, "").split(",")
325332

@@ -350,28 +357,26 @@ def termination_message_for(status)
350357
end
351358
end
352359

353-
def flush_reader(reader)
354-
until reader.eof?
355-
data = reader.gets
356-
output_with_mutex name_for(@readers.key(reader)), data
357-
end
358-
end
359-
360360
## Engine ###########################################################
361361

362362
def spawn_processes
363363
@processes.each do |process|
364364
1.upto(formation[@names[process]]) do |n|
365365
reader, writer = create_pipe
366366
begin
367-
pid = process.run(:output => writer, :env => {
368-
"PORT" => port_for(process, n).to_s,
369-
"PS" => name_for_index(process, n)
370-
})
367+
pid = process.run(
368+
input: process.interactive? ? $stdin : :close,
369+
output: writer,
370+
env: {
371+
'PORT' => port_for(process, n).to_s,
372+
'PS' => name_for_index(process, n)
373+
}
374+
)
371375
writer.puts "started with pid #{pid}"
372376
rescue Errno::ENOENT
373377
writer.puts "unknown command: #{process.command}"
374378
end
379+
@buffers[reader] = Buffer.new
375380
@running[pid] = [process, n]
376381
@readers[pid] = reader
377382
end
@@ -395,11 +400,52 @@ def handle_io(readers)
395400
next if reader == @selfpipe[:reader]
396401

397402
if reader.eof?
398-
@readers.delete_if { |key, value| value == reader }
403+
@buffers.delete(reader)
404+
@readers.delete_if { |_key, value| value == reader }
405+
elsif process_for(reader).interactive?
406+
handle_io_interactive reader
399407
else
400-
data = reader.gets
401-
output_with_mutex name_for(@readers.invert[reader]), data
408+
handle_io_noninteractive reader
409+
end
410+
end
411+
end
412+
413+
def handle_io_interactive(reader)
414+
done = false
415+
name = name_for(@readers.invert[reader])
416+
417+
output_partial prefix(name)
418+
419+
loop do
420+
@buffers[reader].write(reader.read_nonblock(10))
421+
422+
@buffers[reader].each_token do |token|
423+
case token
424+
when /^\e\[(\d+)G$/
425+
output_partial "\e[#{::Regexp.last_match(1).to_i + prefix(name).gsub(ANSI_TOKEN, "").length}G"
426+
when ANSI_TOKEN
427+
output_partial token
428+
when "\n"
429+
output_partial token
430+
output_partial prefix(name)
431+
else
432+
output_partial token
433+
end
434+
done = (token == "\n")
402435
end
436+
rescue IO::WaitReadable
437+
retry if IO.select([reader], [], [], 1)
438+
return if done
439+
rescue EOFError
440+
end
441+
ensure
442+
output_partial "\n"
443+
end
444+
445+
def handle_io_noninteractive(reader)
446+
@buffers[reader].write(reader.read_nonblock(10))
447+
while line = @buffers[reader].gets
448+
output_with_mutex name_for(@readers.invert[reader]), line
403449
end
404450
end
405451

lib/foreman/engine/cli.rb

+16-8
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,30 @@ def startup
5555

5656
def output(name, data)
5757
data.to_s.lines.map(&:chomp).each do |message|
58-
output = ""
59-
output += $stdout.color(@colors[name.split(".").first].to_sym)
60-
output += "#{Time.now.strftime("%H:%M:%S")} " if options[:timestamp]
61-
output += "#{pad_process_name(name)} | "
62-
output += $stdout.color(:reset)
63-
output += message
64-
$stdout.puts output
58+
$stdout.write prefix(name)
59+
$stdout.puts message
6560
$stdout.flush
6661
end
6762
rescue Errno::EPIPE
6863
terminate_gracefully
6964
end
7065

71-
def shutdown
66+
def output_partial(data)
67+
$stdout.write data
68+
$stdout.flush
7269
end
7370

71+
def prefix(name)
72+
output = ''
73+
output += $stdout.color(@colors[name.split('.').first].to_sym)
74+
output += "#{Time.now.strftime('%H:%M:%S')} " if options[:timestamp]
75+
output += "#{pad_process_name(name)} | "
76+
output += $stdout.color(:reset)
77+
output
78+
end
79+
80+
def shutdown; end
81+
7482
private
7583

7684
def name_padding

lib/foreman/process.rb

+3
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,7 @@ def cwd
7777
File.expand_path(@options[:cwd] || ".")
7878
end
7979

80+
def interactive?
81+
@options[:interactive]
82+
end
8083
end

0 commit comments

Comments
 (0)