heroku-commander 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ 0.3.0 (04/15/2013)
2
+ ==================
3
+ * Fixed an infinite loop in the tail restart method used by `Heroku::Commander.run` with `detached: true` - [@macreery](https://github.com/macreery).
4
+ * The `Heroku::Commander.run` with `detached: true` will now restart the tail process when aborted without having received a process exit message - [@dblock](https://github.com/dblock).
5
+ * Added a `tail_retries` that defines the maximum number of tail restarts, default is 3 - [@dblock](https://github.com/dblock).
6
+ * Added a `size` option to `Heroku::Runner` and `Heroku::Commander.run`, supporting `2X` dynos - [@dblock](https://github.com/dblock).
7
+
1
8
  0.2.0 (02/14/2013)
2
9
  ==================
3
10
 
data/README.md CHANGED
@@ -47,6 +47,12 @@ commander = Heroku::Commander.new({ :app => "heroku-commander" })
47
47
  commander.run "uname -a" # => [ "Linux 2.6.32-348-ec2 #54-Ubuntu SMP x86_64 GNU" ]
48
48
  ```
49
49
 
50
+ You can specify the dyno size with `size`.
51
+
52
+ ``` ruby
53
+ commander.run "uname -a", { size: "2X" }
54
+ ```
55
+
50
56
  Heroku Detached Run
51
57
  -------------------
52
58
 
@@ -67,7 +73,9 @@ end
67
73
 
68
74
  You can pass the following options along with `:detached`:
69
75
 
70
- * **tail_timeout**: number of seconds to wait before terminating `heroku logs --tail`, expecting more output (default to 5).
76
+ * **size**: dyno size, eg. `2X` for double-dynos.
77
+ * **tail_timeout**: number of seconds to wait before terminating `heroku logs --tail`, expecting more output (defaults to 5).
78
+ * **tail_retries**: number of times to restart the tail process on error (defaults to 3).
71
79
 
72
80
  For more information about Heroku one-off dynos see [this documentation](https://devcenter.heroku.com/articles/one-off-dynos).
73
81
 
@@ -7,7 +7,7 @@ logger = Logger.new($stdout)
7
7
  logger.level = Logger::DEBUG
8
8
  commander = Heroku::Commander.new({ :logger => logger })
9
9
 
10
- uname = commander.run "uname -a"
10
+ uname = commander.run "uname -a", { :size => "2X" }
11
11
  logger.info "Heroku dyno is a #{uname.join('\n')}."
12
12
 
13
13
  files = []
@@ -33,3 +33,7 @@ en:
33
33
  message: "The process `%{pid}` does not exist."
34
34
  summary: "The output of heroku ps did not list the requested process."
35
35
  resolution: "Verify that the process is running."
36
+ invalid_option:
37
+ message: "Invalid option `%{name}`."
38
+ summary: "The value of the option is invalid: `%{value}`. Must be %{range}."
39
+ resolution: "Refer to the documentation for acceptable values."
@@ -34,7 +34,8 @@ module Heroku
34
34
 
35
35
  # Run a process synchronously
36
36
  def run(command, options = {}, &block)
37
- runner = Heroku::Runner.new({ :app => app, :logger => logger, :command => command })
37
+ size = options.delete(:size) if options
38
+ runner = Heroku::Runner.new({ :app => app, :logger => logger, :command => command, size: size })
38
39
  runner.run!(options, &block)
39
40
  end
40
41
 
@@ -6,3 +6,4 @@ require 'heroku/commander/errors/missing_pid_error'
6
6
  require 'heroku/commander/errors/unexpected_output_error'
7
7
  require 'heroku/commander/errors/already_running_error'
8
8
  require 'heroku/commander/errors/no_such_process_error'
9
+ require 'heroku/commander/errors/invalid_option_error'
@@ -0,0 +1,13 @@
1
+ module Heroku
2
+ class Commander
3
+ module Errors
4
+ class InvalidOptionError < Heroku::Commander::Errors::Base
5
+
6
+ def initialize(opts = {})
7
+ super(compose_message("invalid_option", opts))
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  module Heroku
2
2
  class Commander
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
data/lib/heroku/runner.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  module Heroku
2
2
  class Runner
3
3
 
4
- attr_accessor :app, :logger, :command
4
+ attr_accessor :app, :logger, :command, :size
5
5
  attr_reader :pid, :running, :tail
6
6
 
7
7
  def initialize(options = {})
8
8
  @app = options[:app]
9
9
  @logger = options[:logger]
10
10
  @command = options[:command]
11
+ @size = options[:size]
11
12
  raise Heroku::Commander::Errors::MissingCommandError unless @command
12
13
  end
13
14
 
@@ -58,7 +59,12 @@ module Heroku
58
59
  end
59
60
 
60
61
  def cmdline(options = {})
61
- [ "heroku", options[:detached] ? "run:detached" : "run", "\"(#{command} 2>&1 ; echo rc=\\$?)\"", @app ? "--app #{@app}" : nil ].compact.join(" ")
62
+ [
63
+ "heroku", options[:detached] ? "run:detached" : "run",
64
+ @size ? "--size #{@size}" : nil,
65
+ "\"(#{command} 2>&1 ; echo rc=\\$?)\"",
66
+ @app ? "--app #{@app}" : nil
67
+ ].compact.join(" ")
62
68
  end
63
69
 
64
70
  def check_exit_status!(lines)
@@ -84,24 +90,61 @@ module Heroku
84
90
  lines = []
85
91
  tail_cmdline = [ "heroku", "logs", "-p #{@pid}", "--tail", @app ? "--app #{@app}" : nil ].compact.join(" ")
86
92
  previous_line = nil # delay by 1 to avoid rc=status lines
87
- Heroku::Executor.run tail_cmdline, { :logger => logger } do |line|
88
- line ||= ""
89
- # remove any ANSI output
90
- line = line.gsub /\e\[(\d+)m/, ''
91
- # lines are returned as [date/time] app/heroku[pid]: output
92
- if (line_after_prefix = line.split("[#{@pid}]:")[-1])
93
- line = line_after_prefix.strip
94
- end
95
- if line.match(/Starting process with command/) || line.match(/State changed from \w+ to up/)
96
- # ignore
97
- elsif line.match(/State changed from \w+ to complete/) || line.match(/Process exited with status \d+/)
98
- terminate_executor!(options[:tail_timeout] || 5)
99
- else
100
- if block_given?
101
- yield previous_line if previous_line
102
- previous_line = line
93
+ process_completed = false
94
+ # tail retries
95
+ tail_retries_left = (options[:tail_retries] || 3).to_i
96
+ if tail_retries_left < 0
97
+ raise Heroku::Commander::Errors::InvalidOptionError.new({
98
+ :name => "tail_retries",
99
+ :value => options[:tail_retries],
100
+ :range => "greater or equal to 0"
101
+ })
102
+ end
103
+ # tail timeout
104
+ tail_timeout = (options[:tail_timeout] || 5).to_i
105
+ if tail_timeout < 0
106
+ raise Heroku::Commander::Errors::InvalidOptionError.new({
107
+ :name => "tail_timeout",
108
+ :value => options[:tail_timeout],
109
+ :range => "greater or equal to 0"
110
+ })
111
+ end
112
+ # tail
113
+ process_completed = false
114
+ while ! process_completed
115
+ tail_retries_left -= 1
116
+ begin
117
+ Heroku::Executor.run tail_cmdline, { :logger => logger } do |line|
118
+ line ||= ""
119
+ # remove any ANSI output
120
+ line = line.gsub /\e\[(\d+)m/, ''
121
+ # lines are returned as [date/time] app/heroku[pid]: output
122
+ if (line_after_prefix = line.split("[#{@pid}]:")[-1])
123
+ line = line_after_prefix.strip
124
+ end
125
+ if line.match(/Starting process with command/) || line.match(/State changed from \w+ to up/)
126
+ # ignore
127
+ elsif line.match(/State changed from \w+ to complete/) || line.match(/Process exited with status \d+/)
128
+ process_completed = true
129
+ terminate_executor!(options[:tail_timeout] || 5)
130
+ else
131
+ if block_given?
132
+ yield previous_line if previous_line
133
+ previous_line = line
134
+ end
135
+ lines << line
136
+ end
137
+ end
138
+ rescue
139
+ @running = false
140
+ raise if tail_retries_left <= 0
141
+ ensure
142
+ if tail_retries_left <= 0
143
+ @running = false
144
+ raise
145
+ elsif !process_completed
146
+ logger.debug "Restarting #{tail_cmdline}, #{tail_retries_left} #{tail_retries_left == 1 ? 'retry' : 'retries'} left." if logger
103
147
  end
104
- lines << line
105
148
  end
106
149
  end
107
150
  lines
@@ -76,6 +76,15 @@ describe Heroku::Commander do
76
76
  Heroku::Runner.any_instance.should_receive(:terminate_executor!).with(42).twice
77
77
  subject.run("ls -1", { :detached => true, :tail_timeout => 42 }).should == [ "bin", "app" ]
78
78
  end
79
+ it "passes size option" do
80
+ Heroku::Executor.stub(:run).with("heroku run --size 2X \"(ls -1 2>&1 ; echo rc=\\$?)\"", { :logger => nil }).
81
+ and_yield("Running `...` attached to terminal... up, run.1234").
82
+ and_yield("app").
83
+ and_yield("bin").
84
+ and_yield("rc=0").
85
+ and_return([ "Running `...` attached to terminal... up, run.1234", "app", "bin", "rc=0" ])
86
+ subject.run("ls -1", { size: "2X" }).should == [ "app", "bin" ]
87
+ end
79
88
  end
80
89
  context "processes" do
81
90
  context "without processes" do
@@ -103,6 +103,88 @@ describe Heroku::Runner do
103
103
  subject.should_not be_running
104
104
  end
105
105
  end
106
+ context "tail_retries" do
107
+ before :each do
108
+ Heroku::Executor.stub(:run).with(subject.send(:cmdline, { :detached => true }), { :logger => nil }).
109
+ and_yield("Running `ls -1` detached... up, run.8748").
110
+ and_yield("Use `heroku logs -p run.8748` to view the output.").
111
+ and_yield("rc=0").
112
+ and_return([ "Running `ls -1` detached... up, run.8748", "Use `heroku logs -p run.8748` to view the output.", "rc=0" ])
113
+ # first iteration
114
+ Heroku::Executor.should_receive(:run).with("heroku logs -p run.8748 --tail", { :logger => nil }).
115
+ and_yield("2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`").
116
+ and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: bin").
117
+ and_return([
118
+ "2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`",
119
+ "2013-01-31T01:39:31+00:00 app[run.8748]: bin",
120
+ ])
121
+ end
122
+ context "with a successful second iteration" do
123
+ before :each do
124
+ # second iteration
125
+ Heroku::Executor.should_receive(:run).with("heroku logs -p run.8748 --tail", { :logger => nil }).
126
+ and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: app").
127
+ and_yield(nil).
128
+ and_yield("2013-01-31T00:56:13+00:00 app[run.8748]: rc=0").
129
+ and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0").
130
+ and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete").
131
+ and_return([
132
+ "2013-01-31T01:39:31+00:00 app[run.8748]: app",
133
+ "2013-01-31T00:56:13+00:00 app[run.8748]: rc=0",
134
+ "2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0",
135
+ "2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete"
136
+ ])
137
+ Heroku::Runner.any_instance.should_receive(:terminate_executor!).twice
138
+ end
139
+ it "restarts tailer" do
140
+ lines = []
141
+ subject.run!({ :detached => true }).each do |line|
142
+ lines << line
143
+ end
144
+ lines.should == [ "bin", "app", "" ]
145
+ subject.pid.should == "run.8748"
146
+ subject.should_not be_running
147
+ end
148
+ end
149
+ context "with a caught exception on second iteration" do
150
+ before :each do
151
+ # second iteration that raises a caught exception
152
+ Heroku::Executor.should_receive(:run).exactly(2).times.with("heroku logs -p run.8748 --tail", { :logger => nil }).
153
+ and_return(nil)
154
+ end
155
+ it "raises exception" do
156
+ lines = []
157
+ expect {
158
+ subject.run!({ :detached => true }).each do |line|
159
+ lines << line
160
+ end
161
+ }.to raise_error RuntimeError
162
+ subject.should_not be_running
163
+ end
164
+ end
165
+ end
166
+ context "options" do
167
+ before :each do
168
+ Heroku::Executor.stub(:run).
169
+ and_yield("Running `ls -1` detached... up, run.8748").
170
+ and_return([])
171
+ end
172
+ it "raises an error for an invalid tail_timeout option" do
173
+ expect {
174
+ subject.run!({ :detached => true, :tail_timeout => -1 })
175
+ }.to raise_error Heroku::Commander::Errors::InvalidOptionError, /Invalid option `tail_timeout`./
176
+ end
177
+ it "raises an error for an invalid tail_retries option" do
178
+ expect {
179
+ subject.run!({ :detached => true, :tail_retries => -1 })
180
+ }.to raise_error Heroku::Commander::Errors::InvalidOptionError, /Invalid option `tail_retries`./
181
+ end
182
+ end
183
+ end
184
+ context "with size" do
185
+ subject do
186
+ Heroku::Runner.new({ :command => "ls -1", size: "2X" })
187
+ end
188
+ its(:cmdline) { should eq "heroku run --size 2X \"(ls -1 2>&1 ; echo rc=\\$?)\"" }
106
189
  end
107
190
  end
108
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku-commander
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-02-14 00:00:00.000000000 Z
13
+ date: 2013-04-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n
@@ -57,6 +57,7 @@ files:
57
57
  - lib/heroku/commander/errors/base.rb
58
58
  - lib/heroku/commander/errors/client_eio_error.rb
59
59
  - lib/heroku/commander/errors/command_error.rb
60
+ - lib/heroku/commander/errors/invalid_option_error.rb
60
61
  - lib/heroku/commander/errors/missing_command_error.rb
61
62
  - lib/heroku/commander/errors/missing_pid_error.rb
62
63
  - lib/heroku/commander/errors/no_such_process_error.rb
@@ -90,7 +91,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
91
  version: '0'
91
92
  segments:
92
93
  - 0
93
- hash: -3503047870647109606
94
+ hash: 3343082937396916843
94
95
  required_rubygems_version: !ruby/object:Gem::Requirement
95
96
  none: false
96
97
  requirements: