console-mux 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/console-mux +8 -0
- data/lib/console/mux.rb +64 -0
- data/lib/console/mux/buffer_outputter.rb +55 -0
- data/lib/console/mux/bundle_exec.sh +35 -0
- data/lib/console/mux/color_formatter.rb +110 -0
- data/lib/console/mux/command.rb +134 -0
- data/lib/console/mux/command_set.rb +235 -0
- data/lib/console/mux/console.rb +288 -0
- data/lib/console/mux/console_outputter.rb +60 -0
- data/lib/console/mux/env_with_merged.rb +45 -0
- data/lib/console/mux/events.rb +41 -0
- data/lib/console/mux/object_compose.rb +32 -0
- data/lib/console/mux/process.rb +206 -0
- data/lib/console/mux/pty_handler.rb +46 -0
- data/lib/console/mux/rolling_array.rb +41 -0
- data/lib/console/mux/run_with.rb +74 -0
- data/lib/console/mux/shell.rb +85 -0
- data/lib/console/mux/util.rb +56 -0
- metadata +150 -0
data/bin/console-mux
ADDED
data/lib/console/mux.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (C) 2012 Common Ground Publishing
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
# this software and associated documentation files (the "Software"), to deal in
|
7
|
+
# the Software without restriction, including without limitation the rights to
|
8
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
# subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
#
|
22
|
+
|
23
|
+
require 'optparse'
|
24
|
+
|
25
|
+
require 'console/mux/console'
|
26
|
+
|
27
|
+
module Console
|
28
|
+
module Mux
|
29
|
+
include Log4r
|
30
|
+
|
31
|
+
BUNDLE_EXEC_SH = File.expand_path('bundle_exec.sh',
|
32
|
+
File.join(__FILE__, '..', 'mux'))
|
33
|
+
|
34
|
+
# @param [Array] args commandline arguments
|
35
|
+
def self.run_argv(args = ARGV)
|
36
|
+
options = {
|
37
|
+
:init_file => nil
|
38
|
+
}
|
39
|
+
|
40
|
+
OptionParser.new do |opts|
|
41
|
+
opts.banner = "Usage: console-mux <init_file>"
|
42
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
43
|
+
puts opts
|
44
|
+
exit
|
45
|
+
end
|
46
|
+
opts.on('-f', '--init=FILE', 'Load this init file') do |file|
|
47
|
+
options[:init_file] = file
|
48
|
+
end
|
49
|
+
end.parse!(args)
|
50
|
+
|
51
|
+
begin
|
52
|
+
run(options)
|
53
|
+
rescue Errno::ENOENT => e
|
54
|
+
$stderr.puts e.message
|
55
|
+
$stderr.puts e.backtrace.join("\n ")
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.run(options)
|
61
|
+
Console.new(options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2012 Common Ground Publishing
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require "log4r/outputter/outputter"
|
22
|
+
require "log4r/staticlogger"
|
23
|
+
|
24
|
+
require 'console/mux/rolling_array'
|
25
|
+
|
26
|
+
module Console
|
27
|
+
|
28
|
+
module Mux
|
29
|
+
|
30
|
+
# A Log4r outputtter that captures a rolling buffer of logged
|
31
|
+
# messages.
|
32
|
+
class BufferOutputter < Log4r::Outputter
|
33
|
+
|
34
|
+
def initialize(_name, size, hash={})
|
35
|
+
@buffer = RollingArray.new(size)
|
36
|
+
super(_name, hash)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Iterate through each captured log line.
|
40
|
+
def each(*args, &block)
|
41
|
+
@buffer.each(*args, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# write to buffer
|
47
|
+
def write(data)
|
48
|
+
@buffer << data
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
#
|
3
|
+
# bundle_exec.sh - emulate 'bundle exec' but with a shell script
|
4
|
+
# rather than a ruby script. Modifies the environment just like
|
5
|
+
# bundle exec would.
|
6
|
+
#
|
7
|
+
# Running "bundle exec ruby" starts a ruby in which then execs the
|
8
|
+
# command, 'ruby'. In JRuby 1.6, the command is started in a child
|
9
|
+
# process. When using a Foreman-like tool that has the ability to
|
10
|
+
# kill and/or restart such a command, only the parent will be killed
|
11
|
+
# while the child ruby continues on. This bundle_exec.sh wokrs
|
12
|
+
# similarly to bundle exec, but the exec is done in the shell rather
|
13
|
+
# than in JRuby to avoid the difficult-to-kill child process.
|
14
|
+
#
|
15
|
+
# For bourne-compatible shells.
|
16
|
+
#
|
17
|
+
# Author: Patrick Mahoney <pat@polycrystal.org>
|
18
|
+
|
19
|
+
# Run 'bundle exec' in a subshell; echo a string that when eval'ed
|
20
|
+
# will duplicate the 'bundle exec' environment.
|
21
|
+
env=$(bundle exec sh -c \
|
22
|
+
'echo "export BUNDLE_BIN_PATH=\"$BUNDLE_BIN_PATH\"; \
|
23
|
+
export PATH=\"$PATH\"; \
|
24
|
+
export BUNDLE_GEMFILE=\"$BUNDLE_GEMFILE\"; \
|
25
|
+
export RUBYOPT=\"$RUBYOPT\"";')
|
26
|
+
|
27
|
+
ret="$?"
|
28
|
+
if [ "$ret" = 0 ]; then
|
29
|
+
# Bring bundle exec environment into the current shell.
|
30
|
+
eval $env
|
31
|
+
exec "$@"
|
32
|
+
else
|
33
|
+
echo "$env"
|
34
|
+
exit "$ret"
|
35
|
+
fi
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2012 Common Ground Publishing
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'log4r'
|
22
|
+
|
23
|
+
class String
|
24
|
+
# DJB hash algorithm 2. The default Ruby string hash is not stable
|
25
|
+
# across Ruby invocations (for security reasons).
|
26
|
+
def hash_djb2
|
27
|
+
hash = 5381
|
28
|
+
each_codepoint do |c|
|
29
|
+
hash = ((hash << 5) + hash) + c # equiv. hash * 33 + c?
|
30
|
+
end
|
31
|
+
hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module Console
|
36
|
+
|
37
|
+
module Mux
|
38
|
+
|
39
|
+
class ColorFormatter < Log4r::BasicFormatter
|
40
|
+
BASIC_COLORS = {
|
41
|
+
:black => 0,
|
42
|
+
:red => 1,
|
43
|
+
:green => 2,
|
44
|
+
:yellow => 3,
|
45
|
+
:blue => 4,
|
46
|
+
:magenta => 5,
|
47
|
+
:cyan => 6,
|
48
|
+
:grey => 7,
|
49
|
+
}
|
50
|
+
|
51
|
+
# On 256 color term, many colors beyond 127 seem to be
|
52
|
+
# unreadably light. Of course, if your terminal has a dark
|
53
|
+
# background that may be preferable.
|
54
|
+
N_COLORS = [127, `tput colors`.to_i].min
|
55
|
+
|
56
|
+
COLOR_STRINGS = []
|
57
|
+
N_COLORS.times do |i|
|
58
|
+
COLOR_STRINGS[i] = `tput setaf #{i}`
|
59
|
+
end
|
60
|
+
|
61
|
+
RESET_COLORS = `tput sgr0` # last string is reset
|
62
|
+
|
63
|
+
MAX_WIDTH = 35
|
64
|
+
MIN_WIDTH = 8
|
65
|
+
|
66
|
+
attr_accessor :label_width
|
67
|
+
|
68
|
+
def initialize(hash={})
|
69
|
+
super(hash)
|
70
|
+
@label_width = MIN_WIDTH
|
71
|
+
@colors = Hash.new
|
72
|
+
end
|
73
|
+
|
74
|
+
def format(event)
|
75
|
+
self.label_width = [[self.label_width, event.name.size].max, MAX_WIDTH].min
|
76
|
+
|
77
|
+
color, name = case event.name
|
78
|
+
when 'process'
|
79
|
+
[:red, '-' * label_width]
|
80
|
+
else
|
81
|
+
[color_for(event.name), event.name]
|
82
|
+
end
|
83
|
+
|
84
|
+
"%s%-#{label_width}s|%s %s" %
|
85
|
+
[set_color_str(color), name, unset_color_str, event.data]
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def color_for(name)
|
91
|
+
@colors[name] ||= name.hash_djb2 % N_COLORS
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_color_str(color)
|
95
|
+
c = if color.kind_of? Symbol
|
96
|
+
BASIC_COLORS[color]
|
97
|
+
else
|
98
|
+
color
|
99
|
+
end
|
100
|
+
COLOR_STRINGS[c]
|
101
|
+
end
|
102
|
+
|
103
|
+
def unset_color_str
|
104
|
+
RESET_COLORS
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2012 Common Ground Publishing
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'console/mux/util'
|
22
|
+
require 'console/mux/process'
|
23
|
+
require 'console/mux/run_with'
|
24
|
+
require 'pathname'
|
25
|
+
|
26
|
+
module Console
|
27
|
+
module Mux
|
28
|
+
class Command
|
29
|
+
include Util
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def option_reader(*syms)
|
33
|
+
syms.each do |sym|
|
34
|
+
define_method(sym) do
|
35
|
+
self[sym]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def option_writer(*syms)
|
41
|
+
syms.each do |sym|
|
42
|
+
define_method("#{sym}=") do |val|
|
43
|
+
self[sym] = val
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def option_accessor(*syms)
|
49
|
+
option_reader(*syms)
|
50
|
+
option_writer(*syms)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :opts, :commandline
|
55
|
+
|
56
|
+
option_accessor :name, :command, :env, :run_with, :chdir
|
57
|
+
|
58
|
+
# @param opts execution options arbitrary keys and values, but some are special
|
59
|
+
#
|
60
|
+
# @option opts [Boolean] :noop if true, the command will not be
|
61
|
+
# run (debugging convenience)
|
62
|
+
#
|
63
|
+
# @option opts [String] :command the command to run, e.g. +ls -l+
|
64
|
+
#
|
65
|
+
# @option opts [String] :name the name of the command; if
|
66
|
+
# missing, a name will be auto-generated from the command
|
67
|
+
#
|
68
|
+
# @option opts [String] :chdir change to this dir before running the command
|
69
|
+
#
|
70
|
+
# @option opts [String] :run_with An array of filters. The filters are
|
71
|
+
# applied to the command in reverse order. A string filter is
|
72
|
+
# simply prepended to the command. A symbol is called as a method
|
73
|
+
# on RunWith that accepts `(command, opts)` args and returns
|
74
|
+
# `[new_command, new_opts]`.
|
75
|
+
def initialize(opts)
|
76
|
+
@opts = opts.dup
|
77
|
+
|
78
|
+
self.env ||= {}
|
79
|
+
self.run_with ||= []
|
80
|
+
|
81
|
+
# name need not be unique here. When added to a CommandSet,
|
82
|
+
# within that set it may be assigned a unique name based off
|
83
|
+
# this name.
|
84
|
+
self.name ||= auto_name(command)
|
85
|
+
|
86
|
+
@commandline = expand
|
87
|
+
end
|
88
|
+
|
89
|
+
def [](key)
|
90
|
+
value = opts[key]
|
91
|
+
if value.respond_to? :call
|
92
|
+
value.call(self)
|
93
|
+
else
|
94
|
+
value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set an option on this command. The +value+ may be a Proc
|
99
|
+
# object taking a single argument (or anything that responds to
|
100
|
+
# +:call+), in which case the value on get (+#[]) will be the
|
101
|
+
# result of that Proc called with +self+.
|
102
|
+
def []=(key, value)
|
103
|
+
opts[key] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
def dir
|
107
|
+
dir = self[:chdir] || '.'
|
108
|
+
if self[:base_dir] && !Pathname.new(dir).absolute?
|
109
|
+
File.join(self[:base_dir], dir)
|
110
|
+
else
|
111
|
+
dir
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_s
|
116
|
+
commandline.sub(/^#{::Console::Mux::BUNDLE_EXEC_SH}/, '*bundle_exec.sh')
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Apply any +:run_with+ options to generate the expanded
|
122
|
+
# commandline that should be executed.
|
123
|
+
def expand
|
124
|
+
run_with.reverse.reduce(command) do |commandline, filter|
|
125
|
+
if RunWith.respond_to?(filter)
|
126
|
+
RunWith.send(filter, self, commandline)
|
127
|
+
else
|
128
|
+
"#{filter} #{commandline}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2012 Common Ground Publishing
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
require 'console/mux/events'
|
22
|
+
|
23
|
+
module Console
|
24
|
+
module Mux
|
25
|
+
class CommandSet
|
26
|
+
include Console::Mux::Events
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Yield to the block only after all processes are stopped.
|
30
|
+
# Does not check that all processes are actually running; just
|
31
|
+
# waits for an +:exit+ event from each.
|
32
|
+
#
|
33
|
+
# @param [Array<Process>] processes
|
34
|
+
def join(processes)
|
35
|
+
pending_exit = processes.size
|
36
|
+
processes.each do |proc|
|
37
|
+
proc.on(:exit) do
|
38
|
+
pending_exit -= 1
|
39
|
+
if pending_exit == 0 && block_given?
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :max_width
|
48
|
+
|
49
|
+
def initialize
|
50
|
+
@max_width = 0
|
51
|
+
@commands = Array.new
|
52
|
+
@commands_by_name = Hash.new
|
53
|
+
|
54
|
+
@processes = Hash.new
|
55
|
+
@stopped_at = Hash.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# Add a command (see #add) and start it (see #start).
|
59
|
+
def add_and_start(command)
|
60
|
+
name = add(command)
|
61
|
+
start(name)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add a command to this CommandSet.
|
65
|
+
#
|
66
|
+
# @param [Command] command
|
67
|
+
#
|
68
|
+
# @return [String] the command name, which may be different from
|
69
|
+
# +command.name+ if required to be unique within this CommandSet
|
70
|
+
def add(command)
|
71
|
+
name = unique_name(command.name)
|
72
|
+
|
73
|
+
@commands << command
|
74
|
+
@commands_by_name[name] = command
|
75
|
+
@max_width = [@max_width, name.size].max
|
76
|
+
|
77
|
+
name
|
78
|
+
end
|
79
|
+
|
80
|
+
def remove(name)
|
81
|
+
return unless @commands_by_name[name]
|
82
|
+
|
83
|
+
stop(name) rescue nil
|
84
|
+
@stopped_at.delete(name)
|
85
|
+
@commands.delete(@commands_by_name.delete(name))
|
86
|
+
end
|
87
|
+
|
88
|
+
# TODO: make this work?
|
89
|
+
# def merge(command)
|
90
|
+
# if i = @commands.first_index(command)
|
91
|
+
# @commands[i] = command
|
92
|
+
# @commands_by_name[command.name] = command
|
93
|
+
# else
|
94
|
+
# add(command)
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
|
98
|
+
def include?(command)
|
99
|
+
@commands.include?(command)
|
100
|
+
end
|
101
|
+
|
102
|
+
def stopped?
|
103
|
+
@processes.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
def each(&block)
|
107
|
+
@commands.each(&block)
|
108
|
+
end
|
109
|
+
|
110
|
+
def [](name)
|
111
|
+
@commands_by_name[name]
|
112
|
+
end
|
113
|
+
|
114
|
+
def start(name)
|
115
|
+
if proc = @processes[name]
|
116
|
+
raise "already have process for #{name}: #{proc.pid}"
|
117
|
+
end
|
118
|
+
|
119
|
+
@stopped_at.delete(name)
|
120
|
+
proc = Process.start(@commands_by_name[name], name)
|
121
|
+
proc.on(:exit) do
|
122
|
+
@processes.delete(name)
|
123
|
+
@stopped_at[name] = Time.now
|
124
|
+
fire(:stopped) if @processes.empty?
|
125
|
+
end
|
126
|
+
@processes[name] = proc
|
127
|
+
proc
|
128
|
+
end
|
129
|
+
|
130
|
+
def stop(name)
|
131
|
+
if proc = @processes.delete(name)
|
132
|
+
proc.stop
|
133
|
+
else
|
134
|
+
raise "no process for #{name}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def stop_all
|
139
|
+
@processes.each do |name, proc|
|
140
|
+
proc.stop
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# When all currently-running processes stop, yield to the block
|
145
|
+
def join(&block)
|
146
|
+
self.class.join(@processes.values, &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Stop and then start the process +name+.
|
150
|
+
def restart(name)
|
151
|
+
if proc = @processes[name]
|
152
|
+
proc.on(:exit) do
|
153
|
+
start(name)
|
154
|
+
end
|
155
|
+
proc.stop
|
156
|
+
elsif @commands_by_name[name]
|
157
|
+
start(name)
|
158
|
+
else
|
159
|
+
raise "no process for #{name}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def pretty_seconds(secs)
|
164
|
+
if secs < 60
|
165
|
+
"#{secs} s"
|
166
|
+
elsif secs < (60 * 60)
|
167
|
+
"#{secs/60} min"
|
168
|
+
elsif secs < (60 * 60 * 24)
|
169
|
+
"#{secs/60/60} hr"
|
170
|
+
else
|
171
|
+
"%0.1f days" % (secs/60.0/60.0/24.0)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def pretty_bytes(bytes)
|
176
|
+
if bytes < 1024
|
177
|
+
"#{bytes}B"
|
178
|
+
elsif bytes < 1024 * 1024
|
179
|
+
"#{bytes/1024} kiB"
|
180
|
+
elsif bytes < 1024* 1024 * 1024
|
181
|
+
"#{bytes/1024/1024} MiB"
|
182
|
+
else
|
183
|
+
"%0.2d GiB" % (bytes.to_f/(1024**3))
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def status
|
188
|
+
fmt = "%-#{max_width}s %8s %12s %8s %10s\n"
|
189
|
+
|
190
|
+
rows = []
|
191
|
+
rows << fmt % ['NAME', 'PID', 'RSS', 'TIME', 'CPUTIME']
|
192
|
+
@commands_by_name.each do |name, c|
|
193
|
+
pid, rss, time, cputime =
|
194
|
+
if proc = @processes[name]
|
195
|
+
[proc.pid.to_s,
|
196
|
+
pretty_bytes(proc.rss),
|
197
|
+
proc.etime,
|
198
|
+
proc.cputime]
|
199
|
+
elsif @stopped_at[name]
|
200
|
+
downtime = Time.now.to_i - @stopped_at[name].to_i
|
201
|
+
['-', " down #{pretty_seconds(downtime)}", nil, nil]
|
202
|
+
else
|
203
|
+
['-', " (pending)", nil, nil]
|
204
|
+
end
|
205
|
+
|
206
|
+
rows << (fmt % [name, pid, rss, time, cputime])
|
207
|
+
end
|
208
|
+
|
209
|
+
rows
|
210
|
+
end
|
211
|
+
|
212
|
+
def count
|
213
|
+
"#{@commands.size} commands with #{@processes.size} running"
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
# Generate a unique command name based on `name`. If there is
|
219
|
+
# already a command `name`, a digit will be appended, starting
|
220
|
+
# at 2, and increasing until an unused name is found.
|
221
|
+
def unique_name(name)
|
222
|
+
if @commands_by_name.include?(name)
|
223
|
+
count = 2
|
224
|
+
name2 = nil
|
225
|
+
while (name2 = name + count.to_s) && @commands_by_name.include?(name2)
|
226
|
+
count += 1
|
227
|
+
end
|
228
|
+
name2
|
229
|
+
else
|
230
|
+
name
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|