heroku-commander 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +9 -0
- data/README.md +16 -0
- data/examples/executor-run.rb +16 -0
- data/examples/heroku-processes.rb +12 -0
- data/lib/config/locales/en.yml +10 -1
- data/lib/heroku-commander.rb +1 -1
- data/lib/heroku/commander.rb +17 -0
- data/lib/heroku/commander/errors.rb +2 -0
- data/lib/heroku/commander/errors/command_error.rb +22 -9
- data/lib/heroku/commander/errors/missing_pid_error.rb +13 -0
- data/lib/heroku/commander/errors/no_such_process_error.rb +13 -0
- data/lib/heroku/commander/version.rb +1 -1
- data/lib/heroku/executor.rb +50 -20
- data/lib/heroku/process.rb +25 -0
- data/lib/heroku/runner.rb +20 -19
- data/spec/heroku/commander_spec.rb +42 -2
- data/spec/heroku/executor_spec.rb +11 -2
- data/spec/heroku/process_spec.rb +24 -0
- data/spec/heroku/runner_spec.rb +20 -4
- metadata +10 -4
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
0.2.0 (02/14/2013)
|
2
|
+
==================
|
3
|
+
|
4
|
+
* When the process exit status cannot be determined the error message says "The command failed without returning an exit status." - [@dblock](https://github.com/dblock).
|
5
|
+
* Added the process ID into the `Heroku::Commander::Errors::CommandError` problem description - [@dblock](https://github.com/dblock).
|
6
|
+
* Fix: Heroku `run` or `run:detached` output does not always combine "attached to terminal" and "up" status, which causes the runner to incorrectly parse the Heroku PID - [@dblock](https://github.com/dblock).
|
7
|
+
* Added `Heroku::Commander.processes` that returns or yields an array of `Heroku::Process` by running `heroku ps` - [@dblock](https://github.com/dblock).
|
8
|
+
* Added a `tail_timeout` option to `Heroku::Commander.run` with `detached: true` that suspends tail process termination for a chance to receive additional `heroku logs --tail` output - [@dblock](https://github.com/dblock).
|
9
|
+
|
1
10
|
0.1.0 (01/31/2013)
|
2
11
|
==================
|
3
12
|
|
data/README.md
CHANGED
@@ -25,6 +25,18 @@ commander = Heroku::Commander.new({ :app => "heroku-commander" })
|
|
25
25
|
commander.config # => a hash of all settings for the heroku-commander app
|
26
26
|
```
|
27
27
|
|
28
|
+
Heroku Processes
|
29
|
+
----------------
|
30
|
+
|
31
|
+
Returns or yields an array of processes by running `heroku ps`.
|
32
|
+
|
33
|
+
``` ruby
|
34
|
+
commander = Heroku::Commander.new({ :app => "heroku-commander" })
|
35
|
+
commander.processes do |process|
|
36
|
+
# try process.pid and process.status
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
28
40
|
Heroku Run
|
29
41
|
----------
|
30
42
|
|
@@ -53,6 +65,10 @@ commander.run("ls -R", { :detached => true }) do |line|
|
|
53
65
|
end
|
54
66
|
```
|
55
67
|
|
68
|
+
You can pass the following options along with `:detached`:
|
69
|
+
|
70
|
+
* **tail_timeout**: number of seconds to wait before terminating `heroku logs --tail`, expecting more output (default to 5).
|
71
|
+
|
56
72
|
For more information about Heroku one-off dynos see [this documentation](https://devcenter.heroku.com/articles/one-off-dynos).
|
57
73
|
|
58
74
|
More Examples
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'heroku-commander'
|
5
|
+
|
6
|
+
logger = Logger.new($stdout)
|
7
|
+
logger.level = Logger::INFO
|
8
|
+
|
9
|
+
uname = Heroku::Executor.run "uname -a", { :logger => logger }
|
10
|
+
logger.info "Local system is #{uname.join('\n')}."
|
11
|
+
|
12
|
+
files = []
|
13
|
+
Heroku::Executor.run "ls -1", { :logger => logger } do |line|
|
14
|
+
files << line
|
15
|
+
end
|
16
|
+
logger.info "Local file system has #{files.count} file(s): #{files.join(', ')}"
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'heroku-commander'
|
5
|
+
|
6
|
+
logger = Logger.new($stdout)
|
7
|
+
logger.level = Logger::DEBUG
|
8
|
+
commander = Heroku::Commander.new({ :logger => logger })
|
9
|
+
|
10
|
+
commander.processes.each do |process|
|
11
|
+
logger.info "Process: #{process.pid} (#{process.status})"
|
12
|
+
end
|
data/lib/config/locales/en.yml
CHANGED
@@ -8,7 +8,7 @@ en:
|
|
8
8
|
summary: "The heroku client stopped sending output before exiting."
|
9
9
|
resolution: "This is unexpected and represents a Heroku server error. Try again."
|
10
10
|
command_error:
|
11
|
-
message: "The command `%{cmd}
|
11
|
+
message: "The command `%{cmd}`%{pid} failed%{status_message}."
|
12
12
|
summary: "%{message}%{lines}"
|
13
13
|
resolution: "Examine the command line and refer to the command documentation."
|
14
14
|
unexpected_output:
|
@@ -20,7 +20,16 @@ en:
|
|
20
20
|
summary: "The instance of Heroku::Runner is missing a command argument."
|
21
21
|
resolution: "Specify a command, for example:\n
|
22
22
|
\_\_Heroku::Runner.new({ :command => 'ls -1' })"
|
23
|
+
missing_pid_error:
|
24
|
+
message: "Missing pid."
|
25
|
+
summary: "The instance of Heroku::Status is missing a pid argument."
|
26
|
+
resolution: "Specify a pid, for example:\n
|
27
|
+
\_\_Heroku::Status.new({ :pid => 'run.1234' })"
|
23
28
|
already_running_error:
|
24
29
|
message: "The process is already running with pid %{pid}."
|
25
30
|
summary: "You can only run! or detach! an instance of Heroku::Runner once."
|
26
31
|
resolution: "This is a bug in your code."
|
32
|
+
no_such_process:
|
33
|
+
message: "The process `%{pid}` does not exist."
|
34
|
+
summary: "The output of heroku ps did not list the requested process."
|
35
|
+
resolution: "Verify that the process is running."
|
data/lib/heroku-commander.rb
CHANGED
data/lib/heroku/commander.rb
CHANGED
@@ -15,6 +15,23 @@ module Heroku
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
def processes(&block)
|
19
|
+
processes = []
|
20
|
+
cmdline = [ "heroku", "ps", @app ? "--app #{@app}" : nil ].compact.join(" ")
|
21
|
+
Heroku::Executor.run cmdline, { :logger => logger } do |line|
|
22
|
+
next if ! line || line[0] == '=' || line.length == 0
|
23
|
+
if (match = line.match /(\w+\.\d+): (\w+)/)
|
24
|
+
process = Heroku::Process.new({ :logger => logger, :pid => match[1], :status => match[2], :app => app })
|
25
|
+
processes << process
|
26
|
+
logger.info "Process: #{process.pid} (#{process.status}) from '#{line}'" if logger
|
27
|
+
yield process if block_given?
|
28
|
+
else
|
29
|
+
logger.warn "Unexpected line from heroku ps: #{line}" if logger
|
30
|
+
end
|
31
|
+
end
|
32
|
+
processes
|
33
|
+
end
|
34
|
+
|
18
35
|
# Run a process synchronously
|
19
36
|
def run(command, options = {}, &block)
|
20
37
|
runner = Heroku::Runner.new({ :app => app, :logger => logger, :command => command })
|
@@ -2,5 +2,7 @@ require 'heroku/commander/errors/base'
|
|
2
2
|
require 'heroku/commander/errors/client_eio_error'
|
3
3
|
require 'heroku/commander/errors/command_error'
|
4
4
|
require 'heroku/commander/errors/missing_command_error'
|
5
|
+
require 'heroku/commander/errors/missing_pid_error'
|
5
6
|
require 'heroku/commander/errors/unexpected_output_error'
|
6
7
|
require 'heroku/commander/errors/already_running_error'
|
8
|
+
require 'heroku/commander/errors/no_such_process_error'
|
@@ -7,27 +7,40 @@ module Heroku
|
|
7
7
|
|
8
8
|
def initialize(opts = {})
|
9
9
|
@inner_exception = opts[:inner_exception]
|
10
|
-
|
10
|
+
opts = opts.dup
|
11
|
+
prepare_lines(opts)
|
12
|
+
prepare_status_message(opts)
|
13
|
+
prepare_pid(opts)
|
14
|
+
super(compose_message("command_error", opts))
|
11
15
|
end
|
12
16
|
|
13
17
|
private
|
14
18
|
|
19
|
+
def prepare_status_message(opts)
|
20
|
+
if opts[:status] && opts[:status] != ""
|
21
|
+
opts[:status_message] = " with exit status #{opts[:status]}"
|
22
|
+
else
|
23
|
+
opts[:status_message] = " without reporting an exit status"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
15
27
|
def prepare_lines(opts)
|
16
28
|
if opts[:lines] && opts[:lines].size > 4
|
17
29
|
lines = opts[:lines][0..2]
|
18
30
|
lines.push "... skipping #{opts[:lines].size - 4} line(s) ..."
|
19
31
|
lines.concat opts[:lines][-2..-1]
|
20
|
-
|
21
|
-
result[:lines] = "\n\t" + lines.join("\n\t")
|
22
|
-
result
|
32
|
+
opts[:lines] = "\n\t" + lines.join("\n\t")
|
23
33
|
elsif opts[:lines]
|
24
|
-
|
25
|
-
result[:lines] = "\n\t" + result[:lines].join("\n\t")
|
26
|
-
result
|
27
|
-
else
|
28
|
-
opts
|
34
|
+
opts[:lines] = "\n\t" + opts[:lines].join("\n\t")
|
29
35
|
end
|
30
36
|
end
|
37
|
+
|
38
|
+
def prepare_pid(opts)
|
39
|
+
if opts[:pid]
|
40
|
+
opts[:pid] = " (pid: #{opts[:pid]})"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
31
44
|
end
|
32
45
|
end
|
33
46
|
end
|
data/lib/heroku/executor.rb
CHANGED
@@ -2,6 +2,12 @@ module Heroku
|
|
2
2
|
class Executor
|
3
3
|
|
4
4
|
class Terminate < StandardError
|
5
|
+
|
6
|
+
attr_accessor :timeout
|
7
|
+
|
8
|
+
def initialize(timeout = 0)
|
9
|
+
@timeout = timeout
|
10
|
+
end
|
5
11
|
end
|
6
12
|
|
7
13
|
class << self
|
@@ -9,47 +15,41 @@ module Heroku
|
|
9
15
|
# Executes a command and yields output line-by-line.
|
10
16
|
def run(cmd, options = {}, &block)
|
11
17
|
lines = []
|
18
|
+
running_pid = nil
|
12
19
|
logger = options[:logger]
|
13
20
|
logger.debug "Running: #{cmd}" if logger
|
14
21
|
PTY.spawn(cmd) do |r, w, pid|
|
22
|
+
running_pid = pid
|
15
23
|
logger.debug "Started: #{pid}" if logger
|
16
24
|
terminated = false
|
17
25
|
begin
|
18
26
|
r.sync = true
|
19
|
-
r
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
yield line
|
24
|
-
end
|
25
|
-
lines << line
|
26
|
-
end
|
27
|
-
rescue Heroku::Executor::Terminate
|
28
|
-
logger.debug "Terminating #{pid}." if logger
|
29
|
-
Process.kill("TERM", pid)
|
27
|
+
read_from(r, pid, options, lines, &block)
|
28
|
+
rescue Heroku::Executor::Terminate => e
|
29
|
+
logger.debug "Waiting: #{e.timeout} second(s) to terminate #{pid}" if logger
|
30
|
+
terminate_process! pid, e.timeout
|
30
31
|
terminated = true
|
31
32
|
rescue Errno::EIO, IOError => e
|
32
|
-
|
33
|
+
# ignore
|
33
34
|
rescue PTY::ChildExited => e
|
34
35
|
logger.debug "Terminated: #{pid}" if logger
|
35
|
-
|
36
|
+
terminated = true
|
36
37
|
raise e
|
37
38
|
ensure
|
38
39
|
unless terminated
|
40
|
+
# wait for process
|
39
41
|
logger.debug "Waiting: #{pid}" if logger
|
40
|
-
Process.wait(pid)
|
42
|
+
::Process.wait(pid)
|
41
43
|
end
|
42
44
|
end
|
43
45
|
end
|
44
|
-
check_exit_status! cmd, $?.exitstatus, lines
|
46
|
+
check_exit_status! cmd, running_pid, $?.exitstatus, lines
|
45
47
|
lines
|
46
48
|
rescue Errno::ECHILD => e
|
47
|
-
|
48
|
-
check_exit_status! cmd, $?.exitstatus, lines
|
49
|
+
check_exit_status! cmd, running_pid, $?.exitstatus, lines
|
49
50
|
lines
|
50
51
|
rescue PTY::ChildExited => e
|
51
|
-
|
52
|
-
check_exit_status! cmd, $!.status.exitstatus, lines
|
52
|
+
check_exit_status! cmd, running_pid, $!.status.exitstatus, lines
|
53
53
|
lines
|
54
54
|
rescue Heroku::Commander::Errors::Base => e
|
55
55
|
logger.debug "Error: #{e.problem}" if logger
|
@@ -58,6 +58,7 @@ module Heroku
|
|
58
58
|
logger.debug "#{e.class}: #{e.respond_to?(:problem) ? e.problem : e.message}" if logger
|
59
59
|
raise Heroku::Commander::Errors::CommandError.new({
|
60
60
|
:cmd => cmd,
|
61
|
+
:pid => running_pid,
|
61
62
|
:status => $?.exitstatus,
|
62
63
|
:message => e.message,
|
63
64
|
:inner_exception => e,
|
@@ -67,16 +68,45 @@ module Heroku
|
|
67
68
|
|
68
69
|
private
|
69
70
|
|
70
|
-
def
|
71
|
+
def read_from(r, pid, options, lines, &block)
|
72
|
+
logger = options[:logger]
|
73
|
+
while ! r.eof do
|
74
|
+
line = r.readline
|
75
|
+
line.strip! if line
|
76
|
+
logger.debug "#{pid}: #{line}" if logger
|
77
|
+
if block_given?
|
78
|
+
yield line
|
79
|
+
end
|
80
|
+
lines << line
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_exit_status!(cmd, pid, status, lines = nil)
|
71
85
|
return if ! status || status == 0
|
72
86
|
raise Heroku::Commander::Errors::CommandError.new({
|
73
87
|
:cmd => cmd,
|
88
|
+
:pid => pid,
|
74
89
|
:status => status,
|
75
90
|
:message => "The command #{cmd} failed with exit status #{status}.",
|
76
91
|
:lines => lines
|
77
92
|
})
|
78
93
|
end
|
79
94
|
|
95
|
+
def terminate_process!(pid, timeout)
|
96
|
+
if timeout
|
97
|
+
# Delay terminating of the process, usually to let the output flush.
|
98
|
+
Thread.new(pid, timeout) do |pid, timeout|
|
99
|
+
begin
|
100
|
+
sleep(timeout) if timeout
|
101
|
+
::Process.kill("TERM", pid)
|
102
|
+
rescue
|
103
|
+
end
|
104
|
+
end
|
105
|
+
else
|
106
|
+
::Process.kill("TERM", pid)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
80
110
|
end
|
81
111
|
end
|
82
112
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Heroku
|
2
|
+
class Process
|
3
|
+
|
4
|
+
attr_accessor :app, :logger, :pid, :status
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@app = options[:app]
|
8
|
+
@logger = options[:logger]
|
9
|
+
@pid = options[:pid]
|
10
|
+
@status = options[:status]
|
11
|
+
raise Heroku::Commander::Errors::MissingPidError unless @pid
|
12
|
+
end
|
13
|
+
|
14
|
+
def refresh_status!
|
15
|
+
refreshed = false
|
16
|
+
Heroku::Commander.new({ :logger => logger, :app => app }).processes.each do |process|
|
17
|
+
next unless ! refreshed && process.pid == pid
|
18
|
+
@status = process.status
|
19
|
+
refreshed = true
|
20
|
+
end
|
21
|
+
refreshed
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/heroku/runner.rb
CHANGED
@@ -17,38 +17,40 @@ module Heroku
|
|
17
17
|
|
18
18
|
def run!(options = {}, &block)
|
19
19
|
if options && options[:detached]
|
20
|
-
run_detached! &block
|
20
|
+
run_detached! options, &block
|
21
21
|
else
|
22
|
-
run_attached! &block
|
22
|
+
run_attached! options, &block
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
26
|
protected
|
27
27
|
|
28
|
-
def run_attached!(&block)
|
28
|
+
def run_attached!(options = {}, &block)
|
29
29
|
@pid = nil
|
30
30
|
previous_line = nil # delay by 1 to avoid rc=status line
|
31
|
+
lines_until_pid = 0
|
31
32
|
lines = Heroku::Executor.run cmdline, { :logger => logger } do |line|
|
32
33
|
if ! @pid
|
33
34
|
check_pid(line)
|
35
|
+
lines_until_pid += 1
|
34
36
|
elsif block_given?
|
35
37
|
yield previous_line if previous_line
|
36
38
|
previous_line = line
|
37
39
|
end
|
38
40
|
end
|
39
|
-
lines.shift # remove Running `...` attached to terminal... up, run.xyz
|
41
|
+
lines.shift(lines_until_pid) # remove Running `...` attached to terminal... up, run.xyz
|
40
42
|
check_exit_status! lines
|
41
43
|
lines
|
42
44
|
end
|
43
45
|
|
44
|
-
def run_detached!(&block)
|
46
|
+
def run_detached!(options = {}, &block)
|
45
47
|
raise Heroku::Commander::Errors::AlreadyRunningError.new({ :pid => @pid }) if running?
|
46
48
|
@running = true
|
47
49
|
@pid = nil
|
48
50
|
@tail = nil
|
49
51
|
lines = Heroku::Executor.run cmdline({ :detached => true }), { :logger => logger } do |line|
|
50
52
|
check_pid(line) unless @pid
|
51
|
-
@tail ||= tail!(&block) if @pid
|
53
|
+
@tail ||= tail!(options, &block) if @pid
|
52
54
|
end
|
53
55
|
check_exit_status! @tail || lines
|
54
56
|
@running = false
|
@@ -64,37 +66,36 @@ module Heroku
|
|
64
66
|
lines.pop if status
|
65
67
|
raise Heroku::Commander::Errors::CommandError.new({
|
66
68
|
:cmd => @command,
|
69
|
+
:pid => @pid,
|
67
70
|
:status => status,
|
68
|
-
:message => "The command #{@command} failed
|
71
|
+
:message => "The command #{@command} failed.",
|
69
72
|
:lines => lines
|
70
73
|
}) unless status && status == "0"
|
71
74
|
end
|
72
75
|
|
73
76
|
def check_pid(line)
|
74
|
-
if (match = line.match /
|
77
|
+
if (match = line.match /up, (run.\d+)$/)
|
75
78
|
@pid = match[1]
|
76
79
|
logger.debug "Heroku pid #{@pid} up." if logger
|
77
|
-
elsif (match = line.match /detached... up, (run.\d+)$/)
|
78
|
-
@pid = match[1]
|
79
|
-
logger.debug "Heroku detached pid #{@pid} up." if logger
|
80
|
-
else
|
81
|
-
@pid = ''
|
82
80
|
end
|
83
81
|
end
|
84
82
|
|
85
|
-
def tail!(&block)
|
83
|
+
def tail!(options = {}, &block)
|
86
84
|
lines = []
|
87
85
|
tail_cmdline = [ "heroku", "logs", "-p #{@pid}", "--tail", @app ? "--app #{@app}" : nil ].compact.join(" ")
|
88
|
-
previous_line = nil # delay by 1 to avoid rc=status
|
86
|
+
previous_line = nil # delay by 1 to avoid rc=status lines
|
89
87
|
Heroku::Executor.run tail_cmdline, { :logger => logger } do |line|
|
88
|
+
line ||= ""
|
90
89
|
# remove any ANSI output
|
91
90
|
line = line.gsub /\e\[(\d+)m/, ''
|
92
91
|
# lines are returned as [date/time] app/heroku[pid]: output
|
93
|
-
|
92
|
+
if (line_after_prefix = line.split("[#{@pid}]:")[-1])
|
93
|
+
line = line_after_prefix.strip
|
94
|
+
end
|
94
95
|
if line.match(/Starting process with command/) || line.match(/State changed from \w+ to up/)
|
95
96
|
# ignore
|
96
97
|
elsif line.match(/State changed from \w+ to complete/) || line.match(/Process exited with status \d+/)
|
97
|
-
terminate_executor!
|
98
|
+
terminate_executor!(options[:tail_timeout] || 5)
|
98
99
|
else
|
99
100
|
if block_given?
|
100
101
|
yield previous_line if previous_line
|
@@ -106,8 +107,8 @@ module Heroku
|
|
106
107
|
lines
|
107
108
|
end
|
108
109
|
|
109
|
-
def terminate_executor!
|
110
|
-
raise Heroku::Executor::Terminate
|
110
|
+
def terminate_executor!(timeout = 10)
|
111
|
+
raise Heroku::Executor::Terminate.new(timeout)
|
111
112
|
end
|
112
113
|
|
113
114
|
end
|
@@ -73,8 +73,48 @@ describe Heroku::Commander do
|
|
73
73
|
"2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0",
|
74
74
|
"2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete"
|
75
75
|
])
|
76
|
-
Heroku::Runner.any_instance.should_receive(:terminate_executor!).twice
|
77
|
-
subject.run("ls -1", { :detached => true }).should == [ "bin", "app" ]
|
76
|
+
Heroku::Runner.any_instance.should_receive(:terminate_executor!).with(42).twice
|
77
|
+
subject.run("ls -1", { :detached => true, :tail_timeout => 42 }).should == [ "bin", "app" ]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
context "processes" do
|
81
|
+
context "without processes" do
|
82
|
+
before :each do
|
83
|
+
Heroku::Executor.stub(:run).with("heroku ps", { :logger => nil })
|
84
|
+
end
|
85
|
+
its(:processes) { should be_empty }
|
86
|
+
end
|
87
|
+
context "a web process" do
|
88
|
+
before :each do
|
89
|
+
Heroku::Executor.stub(:run).with("heroku ps", { :logger => nil }).
|
90
|
+
and_yield("=== web: `bundle exec ruby config.ru`").
|
91
|
+
and_yield("web.1: idle 2013/02/04 13:23:40 (~ 4h ago)")
|
92
|
+
end
|
93
|
+
its(:processes) { should_not be_empty }
|
94
|
+
it "has correct pid and status" do
|
95
|
+
processes = subject.processes
|
96
|
+
processes.count.should == 1
|
97
|
+
process = processes.first
|
98
|
+
process.pid.should eq "web.1"
|
99
|
+
process.status.should eq "idle"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
context "a combination of one-off and web processes" do
|
103
|
+
before :each do
|
104
|
+
Heroku::Executor.stub(:run).with("heroku ps", { :logger => nil }).
|
105
|
+
and_yield("=== run: one-off processes").
|
106
|
+
and_yield("run.9174: up 2013/02/04 17:37:37 (~ 9m ago): `bundle exec rails console`").
|
107
|
+
and_yield(nil).
|
108
|
+
and_yield("").
|
109
|
+
and_yield("=== web: `bundle exec ruby config.ru`").
|
110
|
+
and_yield("web.1: up 2013/02/04 11:14:53 (~ 6h ago)").
|
111
|
+
and_yield("web.2: up 2013/02/04 11:14:17 (~ 6h ago)")
|
112
|
+
end
|
113
|
+
it "has correct pid and status" do
|
114
|
+
processes = subject.processes
|
115
|
+
processes.count.should == 3
|
116
|
+
processes.map(&:pid).should == [ "run.9174", "web.1", "web.2" ]
|
117
|
+
end
|
78
118
|
end
|
79
119
|
end
|
80
120
|
end
|
@@ -7,7 +7,15 @@ describe Heroku::Executor do
|
|
7
7
|
end
|
8
8
|
context "command does not exist" do
|
9
9
|
subject { lambda { Heroku::Executor.run "executor_spec.rb" } }
|
10
|
-
it { should raise_error Heroku::Commander::Errors::CommandError, /The command `executor_spec.rb`
|
10
|
+
it { should raise_error Heroku::Commander::Errors::CommandError, /The command `executor_spec.rb`(.*)failed with exit status \d+./ }
|
11
|
+
end
|
12
|
+
context "command does not return exit status" do
|
13
|
+
before do
|
14
|
+
PTY.stub(:spawn).and_raise("foobar!")
|
15
|
+
Process::Status.any_instance.stub(:exitstatus).and_return(nil)
|
16
|
+
end
|
17
|
+
subject { lambda { Heroku::Executor.run "executor_spec.rb" } }
|
18
|
+
it { should raise_error Heroku::Commander::Errors::CommandError, /The command `executor_spec.rb` failed without reporting an exit status./ }
|
11
19
|
end
|
12
20
|
context "command exists" do
|
13
21
|
subject { lambda { Heroku::Executor.run "ls -1" } }
|
@@ -25,7 +33,8 @@ describe Heroku::Executor do
|
|
25
33
|
it "doesn't yield nil lines" do
|
26
34
|
r = double(IO)
|
27
35
|
r.stub(:sync=)
|
28
|
-
r.stub(:
|
36
|
+
r.stub(:eof).and_return(false, false, false, true)
|
37
|
+
r.stub(:readline).and_return("line1", nil, "rc=0")
|
29
38
|
Process.stub(:wait)
|
30
39
|
PTY.stub(:spawn).and_yield(r, nil, 42)
|
31
40
|
Heroku::Executor.run("foobar").should == [ "line1", nil, "rc=0" ]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Heroku::Process do
|
4
|
+
context "without pid" do
|
5
|
+
subject { lambda { Heroku::Process.new } }
|
6
|
+
it { should raise_error Heroku::Commander::Errors::MissingPidError, /Missing pid./ }
|
7
|
+
end
|
8
|
+
context "with pid" do
|
9
|
+
subject do
|
10
|
+
Heroku::Process.new({ :pid => "run.1234", :status => "idle" })
|
11
|
+
end
|
12
|
+
its(:pid) { should eq "run.1234" }
|
13
|
+
its(:status) { should eq "idle" }
|
14
|
+
context "refresh_status!" do
|
15
|
+
before do
|
16
|
+
Heroku::Executor.stub(:run).with("heroku ps", { :logger => nil }).
|
17
|
+
and_yield("run.1234: up for ~ 1m")
|
18
|
+
subject.refresh_status!
|
19
|
+
end
|
20
|
+
its(:status) { should eq "up" }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
data/spec/heroku/runner_spec.rb
CHANGED
@@ -17,14 +17,29 @@ describe Heroku::Runner do
|
|
17
17
|
its(:command) { should eq "ls -1" }
|
18
18
|
its(:cmdline) { should eq "heroku run \"(ls -1 2>&1 ; echo rc=\\$?)\"" }
|
19
19
|
its(:running?) { should be_false }
|
20
|
+
context "check_pid" do
|
21
|
+
it "parses up, run.1234" do
|
22
|
+
subject.send(:check_pid, "up, run.1234")
|
23
|
+
subject.pid.should == "run.1234"
|
24
|
+
end
|
25
|
+
it "parses attached to terminal ... up, run.1234" do
|
26
|
+
subject.send(:check_pid, "parses attached to terminal ... up, run.1234")
|
27
|
+
subject.pid.should == "run.1234"
|
28
|
+
end
|
29
|
+
it "parses detached ... up, run.1234" do
|
30
|
+
subject.send(:check_pid, "detached ... up, run.1234")
|
31
|
+
subject.pid.should == "run.1234"
|
32
|
+
end
|
33
|
+
end
|
20
34
|
context "run!" do
|
21
35
|
before :each do
|
22
36
|
Heroku::Executor.stub(:run).with(subject.send(:cmdline), { :logger => nil }).
|
23
|
-
and_yield("Running `...`
|
37
|
+
and_yield("Running `...`").
|
38
|
+
and_yield("attached to terminal... up, run.9783").
|
24
39
|
and_yield("app").
|
25
40
|
and_yield("bin").
|
26
41
|
and_yield("rc= 0").
|
27
|
-
and_return([ "Running `...` attached to terminal... up, run.9783", "app", "bin", "rc=0" ])
|
42
|
+
and_return([ "Running `...`", "attached to terminal... up, run.9783", "app", "bin", "rc=0" ])
|
28
43
|
end
|
29
44
|
it "runs the command w/o a block" do
|
30
45
|
subject.run!.should == [ "app", "bin" ]
|
@@ -59,6 +74,7 @@ describe Heroku::Runner do
|
|
59
74
|
and_yield("2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`").
|
60
75
|
and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: bin").
|
61
76
|
and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: app").
|
77
|
+
and_yield(nil).
|
62
78
|
and_yield("2013-01-31T00:56:13+00:00 app[run.8748]: rc=0").
|
63
79
|
and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0").
|
64
80
|
and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete").
|
@@ -73,7 +89,7 @@ describe Heroku::Runner do
|
|
73
89
|
Heroku::Runner.any_instance.should_receive(:terminate_executor!).twice
|
74
90
|
end
|
75
91
|
it "runs the command w/o a block" do
|
76
|
-
subject.run!({ :detached => true }).should == [ "bin", "app" ]
|
92
|
+
subject.run!({ :detached => true }).should == [ "bin", "app", "" ]
|
77
93
|
subject.pid.should == "run.8748"
|
78
94
|
subject.should_not be_running
|
79
95
|
end
|
@@ -82,7 +98,7 @@ describe Heroku::Runner do
|
|
82
98
|
subject.run!({ :detached => true }).each do |line|
|
83
99
|
lines << line
|
84
100
|
end
|
85
|
-
lines.should == [ "bin", "app" ]
|
101
|
+
lines.should == [ "bin", "app", "" ]
|
86
102
|
subject.pid.should == "run.8748"
|
87
103
|
subject.should_not be_running
|
88
104
|
end
|
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.2.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-
|
13
|
+
date: 2013-02-14 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: i18n
|
@@ -43,7 +43,9 @@ files:
|
|
43
43
|
- README.md
|
44
44
|
- Rakefile
|
45
45
|
- assets/heroku-commander.png
|
46
|
+
- examples/executor-run.rb
|
46
47
|
- examples/heroku-config.rb
|
48
|
+
- examples/heroku-processes.rb
|
47
49
|
- examples/heroku-run-detached.rb
|
48
50
|
- examples/heroku-run.rb
|
49
51
|
- heroku-commander.gemspec
|
@@ -56,16 +58,20 @@ files:
|
|
56
58
|
- lib/heroku/commander/errors/client_eio_error.rb
|
57
59
|
- lib/heroku/commander/errors/command_error.rb
|
58
60
|
- lib/heroku/commander/errors/missing_command_error.rb
|
61
|
+
- lib/heroku/commander/errors/missing_pid_error.rb
|
62
|
+
- lib/heroku/commander/errors/no_such_process_error.rb
|
59
63
|
- lib/heroku/commander/errors/unexpected_output_error.rb
|
60
64
|
- lib/heroku/commander/version.rb
|
61
65
|
- lib/heroku/config.rb
|
62
66
|
- lib/heroku/executor.rb
|
67
|
+
- lib/heroku/process.rb
|
63
68
|
- lib/heroku/pty.rb
|
64
69
|
- lib/heroku/runner.rb
|
65
70
|
- lib/heroku_commander.rb
|
66
71
|
- spec/heroku/commander_spec.rb
|
67
72
|
- spec/heroku/config_spec.rb
|
68
73
|
- spec/heroku/executor_spec.rb
|
74
|
+
- spec/heroku/process_spec.rb
|
69
75
|
- spec/heroku/runner_spec.rb
|
70
76
|
- spec/heroku/version_spec.rb
|
71
77
|
- spec/spec_helper.rb
|
@@ -84,7 +90,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
90
|
version: '0'
|
85
91
|
segments:
|
86
92
|
- 0
|
87
|
-
hash:
|
93
|
+
hash: -3503047870647109606
|
88
94
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
95
|
none: false
|
90
96
|
requirements:
|
@@ -93,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
99
|
version: 1.3.6
|
94
100
|
requirements: []
|
95
101
|
rubyforge_project:
|
96
|
-
rubygems_version: 1.8.
|
102
|
+
rubygems_version: 1.8.25
|
97
103
|
signing_key:
|
98
104
|
specification_version: 3
|
99
105
|
summary: Control Heroku from Ruby via its `heroku` shell command.
|