From f745b16217c650501aa2e0a81bb72db1919c426a Mon Sep 17 00:00:00 2001 From: Henry Hsu Date: Mon, 5 Mar 2012 15:31:52 -0800 Subject: [PATCH] Add support for starting procfile in tmux session --- Gemfile | 1 + Gemfile.lock | 2 + bin/taskman | 8 ++++ lib/foreman/cli.rb | 15 ++++++- lib/foreman/tmux_engine.rb | 35 +++++++++++++++ man/foreman.1 | 4 ++ man/foreman.1.ronn | 4 ++ spec/foreman/tmux_engine_spec.rb | 73 ++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 1 + 9 files changed, 142 insertions(+), 1 deletion(-) create mode 100755 bin/taskman create mode 100644 lib/foreman/tmux_engine.rb create mode 100644 spec/foreman/tmux_engine_spec.rb diff --git a/Gemfile b/Gemfile index 09bd5de..b417d1a 100644 --- a/Gemfile +++ b/Gemfile @@ -18,4 +18,5 @@ group :development do gem 'rr', '~> 1.0.2' gem 'rspec', '~> 2.0' gem "simplecov", :require => false + gem 'timecop' end diff --git a/Gemfile.lock b/Gemfile.lock index fd7ffd5..8410339 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -40,6 +40,7 @@ GEM simplecov-html (~> 0.5.3) simplecov-html (0.5.3) thor (0.14.6) + timecop (0.3.5) win32console (1.3.0-x86-mingw32) xml-simple (1.0.15) @@ -58,4 +59,5 @@ DEPENDENCIES rr (~> 1.0.2) rspec (~> 2.0) simplecov + timecop win32console (~> 1.3.0) diff --git a/bin/taskman b/bin/taskman new file mode 100755 index 0000000..ae33123 --- /dev/null +++ b/bin/taskman @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby + +$:.unshift File.expand_path("../../lib", __FILE__) + +require "foreman/cli" + +Foreman::CLI.engine_class = Foreman::TmuxEngine +Foreman::CLI.start diff --git a/lib/foreman/cli.rb b/lib/foreman/cli.rb index acaa5f4..1d1461d 100644 --- a/lib/foreman/cli.rb +++ b/lib/foreman/cli.rb @@ -1,6 +1,7 @@ require "foreman" require "foreman/helpers" require "foreman/engine" +require "foreman/tmux_engine" require "foreman/export" require "shellwords" require "thor" @@ -10,6 +11,7 @@ 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" desc "start [PROCESS]", "Start the application (or a specific PROCESS)" @@ -73,6 +75,17 @@ 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 + end + end + private ###################################################################### def check_procfile! @@ -80,7 +93,7 @@ private ###################################################################### end def engine - @engine ||= Foreman::Engine.new(procfile, options) + @engine ||= self.class.new_engine(procfile, options) end def procfile diff --git a/lib/foreman/tmux_engine.rb b/lib/foreman/tmux_engine.rb new file mode 100644 index 0000000..2c203b4 --- /dev/null +++ b/lib/foreman/tmux_engine.rb @@ -0,0 +1,35 @@ +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]) + + %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/man/foreman.1 b/man/foreman.1 index 7277a1e..87f0308 100644 --- a/man/foreman.1 +++ b/man/foreman.1 @@ -46,6 +46,10 @@ Specify an alternate Procfile to load, implies \fB\-d\fR at the Procfile root\. \fB\-p\fR, \fB\-\-port\fR Specify which port to use as the base for this application\. Should be a multiple of 1000\. . +.TP +\fB\-t\fR, \fB\-\-tmux\fR +Runs the processes in a tmux session\. Creates one window for each process and an extra window containing the output of each window (requires gawk)\. +. .P \fBforeman run\fR is used to run one\-off commands using the same environment as your defined processes\. . diff --git a/man/foreman.1.ronn b/man/foreman.1.ronn index 9cac2d5..fc0e926 100644 --- a/man/foreman.1.ronn +++ b/man/foreman.1.ronn @@ -40,6 +40,10 @@ The following options control how the application is run: Specify which port to use as the base for this application. Should be a multiple of 1000. + * `-t`, `--tmux`: + Runs the processes in a tmux session. Creates one window for each process + and an extra window containing the output of each window (requires gawk). + `foreman run` is used to run one-off commands using the same environment as your defined processes. diff --git a/spec/foreman/tmux_engine_spec.rb b/spec/foreman/tmux_engine_spec.rb new file mode 100644 index 0000000..3499962 --- /dev/null +++ b/spec/foreman/tmux_engine_spec.rb @@ -0,0 +1,73 @@ +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 + + 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 49c26bc..6f2da46 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,7 @@ SimpleCov.start do end require "rspec" +require "timecop" require "fakefs/safe" require "fakefs/spec_helpers"