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 +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:
|