console-mux 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path(File.dirname(__FILE__)) + '/../lib/console/mux'
3
+
4
+ #require 'ruby-debug'
5
+ #Debugger.start
6
+
7
+ Console::Mux.run_argv
8
+
@@ -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