heroku-commander 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +7 -0
- data/README.md +9 -1
- data/examples/heroku-run.rb +1 -1
- data/lib/config/locales/en.yml +4 -0
- data/lib/heroku/commander.rb +2 -1
- data/lib/heroku/commander/errors.rb +1 -0
- data/lib/heroku/commander/errors/invalid_option_error.rb +13 -0
- data/lib/heroku/commander/version.rb +1 -1
- data/lib/heroku/runner.rb +62 -19
- data/spec/heroku/commander_spec.rb +9 -0
- data/spec/heroku/runner_spec.rb +83 -1
- metadata +4 -3
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
|
-
* **
|
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
|
|
data/examples/heroku-run.rb
CHANGED
@@ -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 = []
|
data/lib/config/locales/en.yml
CHANGED
@@ -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."
|
data/lib/heroku/commander.rb
CHANGED
@@ -34,7 +34,8 @@ module Heroku
|
|
34
34
|
|
35
35
|
# Run a process synchronously
|
36
36
|
def run(command, options = {}, &block)
|
37
|
-
|
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'
|
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
|
-
[
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
data/spec/heroku/runner_spec.rb
CHANGED
@@ -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.
|
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-
|
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:
|
94
|
+
hash: 3343082937396916843
|
94
95
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
96
|
none: false
|
96
97
|
requirements:
|