console-mux 2.0.2
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/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
@@ -0,0 +1,288 @@
|
|
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 'eventmachine'
|
22
|
+
require 'log4r'
|
23
|
+
require 'ripl/readline/em'
|
24
|
+
|
25
|
+
require 'console/mux/buffer_outputter'
|
26
|
+
require 'console/mux/command'
|
27
|
+
require 'console/mux/command_set'
|
28
|
+
require 'console/mux/console_outputter'
|
29
|
+
require 'console/mux/color_formatter'
|
30
|
+
require 'console/mux/pty_handler'
|
31
|
+
require 'console/mux/shell'
|
32
|
+
|
33
|
+
|
34
|
+
module Console
|
35
|
+
module Mux
|
36
|
+
class Console
|
37
|
+
include Log4r
|
38
|
+
|
39
|
+
MARK_PERIOD = 60 # seconds
|
40
|
+
|
41
|
+
BUFFER_LINES = 10000
|
42
|
+
|
43
|
+
attr_reader :commands, :default_options, :formatter, :buffer, :logger
|
44
|
+
|
45
|
+
def initialize(options={})
|
46
|
+
@commands = CommandSet.new
|
47
|
+
@default_options = Hash.new
|
48
|
+
@base_dir = '.'
|
49
|
+
|
50
|
+
@formatter = ColorFormatter.new
|
51
|
+
|
52
|
+
@logger = Logger.new('process')
|
53
|
+
logger.add ConsoleOutputter.new('console',
|
54
|
+
self,
|
55
|
+
:formatter => formatter)
|
56
|
+
|
57
|
+
@buffer = BufferOutputter.new('buffer',
|
58
|
+
BUFFER_LINES,
|
59
|
+
:formatter => formatter)
|
60
|
+
logger.add @buffer
|
61
|
+
|
62
|
+
EventMachine.run do
|
63
|
+
logger.info { 'Initializing' }
|
64
|
+
|
65
|
+
EventMachine.add_periodic_timer(MARK_PERIOD) do
|
66
|
+
now = Time.now.strftime('%Y-%m-%d %H:%M')
|
67
|
+
logger.info { "#{now} have #{commands.count}" }
|
68
|
+
end
|
69
|
+
|
70
|
+
EventMachine.next_tick do
|
71
|
+
load(options[:init_file]) if options[:init_file]
|
72
|
+
end
|
73
|
+
|
74
|
+
@shell = Shell.new(self)
|
75
|
+
Ripl.start :binding => @shell.instance_eval { binding }
|
76
|
+
EventMachine.watch_stdin(Ripl::Readline::EmInput,
|
77
|
+
:on_exit => proc { shutdown })
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def load(file)
|
82
|
+
old_base = @base_dir
|
83
|
+
@base_dir = File.expand_path(File.dirname(file))
|
84
|
+
begin
|
85
|
+
@shell.instance_eval(File.read(file), file)
|
86
|
+
@last_file = file
|
87
|
+
ensure
|
88
|
+
@base_dir = old_base
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def run_commands()
|
93
|
+
next_command = commands.command_q.shift
|
94
|
+
if (!next_command.nil?)
|
95
|
+
commands.startCommand( next_command )
|
96
|
+
|
97
|
+
# if next_command.opts[:blocking]
|
98
|
+
# EventMachine.next_tick do
|
99
|
+
# check_command(next_command)
|
100
|
+
# end
|
101
|
+
# else
|
102
|
+
# end
|
103
|
+
|
104
|
+
if (!(commands.command_q.empty?))
|
105
|
+
EventMachine.next_tick do
|
106
|
+
run_commands()
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# EM::Iterator requires the beta version of eventmachine
|
112
|
+
# EM::Iterator.new(commands.command_q).each do |command,iter|
|
113
|
+
# commands.start( commands.command_q.shift )
|
114
|
+
# iter.next
|
115
|
+
# end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
# Using set_ rather than '=' style accessor so config file needn't
|
120
|
+
# use self.default_options =.
|
121
|
+
def set_default_options(opts)
|
122
|
+
@default_options = opts
|
123
|
+
end
|
124
|
+
|
125
|
+
# Run a single command, a sequence of commands, or a sequence of
|
126
|
+
# single and parallel commands with default shell argument
|
127
|
+
# expansion.
|
128
|
+
#
|
129
|
+
# Each options hash is merged with the default options and then
|
130
|
+
# passed to +Command.new+.
|
131
|
+
#
|
132
|
+
# If multiple command options are given, run them sequentially
|
133
|
+
# one-at-a-time. Only the final command will remain in the
|
134
|
+
# active command list; the others will be removed as they
|
135
|
+
# complete and exit.
|
136
|
+
#
|
137
|
+
# If an array of command options is given, they will be run in
|
138
|
+
# parallel, even if part of sequential sequence. Thus you can
|
139
|
+
# specify +run(c1, c2, [c3,c4,c5], c6)+ which will run commands
|
140
|
+
# 1 and 2 sequentially, then 3, 4 and 5 in parallel, then
|
141
|
+
# finally command 6 only after all previous commands complete.
|
142
|
+
#
|
143
|
+
# In the following example, the first +ls+ is run, and when it
|
144
|
+
# exits the next two +ls+ instances are spawned in parallel.
|
145
|
+
# When those two exit, the final +ls+ is run.
|
146
|
+
#
|
147
|
+
# run({:command => 'ls'},
|
148
|
+
# [{:command => 'ls'}, {:command => 'ls'}],
|
149
|
+
# {:command => 'ls'})
|
150
|
+
#
|
151
|
+
# @param [Hash] *opts one or more option hashes passed to +Command.new+.
|
152
|
+
def run(*optses)
|
153
|
+
names = optses.map do |opts|
|
154
|
+
if opts.kind_of? Array
|
155
|
+
opts.map { |o| make_command_and_add(o) }
|
156
|
+
else
|
157
|
+
make_command_and_add(opts)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
seq_names(names.compact)
|
162
|
+
end
|
163
|
+
|
164
|
+
def make_command(opts)
|
165
|
+
opts = @default_options.merge(opts)
|
166
|
+
return if opts[:noop]
|
167
|
+
|
168
|
+
opts[:base_dir] = @base_dir
|
169
|
+
|
170
|
+
begin
|
171
|
+
Command.new(opts)
|
172
|
+
rescue => e
|
173
|
+
# optimistically assume console-mux errors are uninteresting
|
174
|
+
first_relevant = e.backtrace.find { |line| !(line =~ %r{lib/console/mux}) }
|
175
|
+
# logger.error e.backtrace.join("\n\t")
|
176
|
+
logger.error { "#{opts[:command]}: #{e.message} at #{first_relevant}" }
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
end
|
180
|
+
private :make_command
|
181
|
+
|
182
|
+
# @return [String, nil] the name of the command or nil if
|
183
|
+
# command was noop or if there was an error
|
184
|
+
def make_command_and_add(opts)
|
185
|
+
if c = make_command(opts)
|
186
|
+
commands.add(c)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
private :make_command_and_add
|
190
|
+
|
191
|
+
def seq_names(names)
|
192
|
+
return unless names.size > 0
|
193
|
+
|
194
|
+
name_or_ary = names.shift
|
195
|
+
EventMachine.next_tick do
|
196
|
+
# name may be a single name or array of names, so we
|
197
|
+
# normalize everything to an array
|
198
|
+
to_start = [name_or_ary].flatten
|
199
|
+
procs = to_start.map { |n| start(n) }
|
200
|
+
|
201
|
+
CommandSet.join(procs) do
|
202
|
+
if names.size > 0
|
203
|
+
to_start.each { |n| commands.remove(n) }
|
204
|
+
seq_names(names)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
private :seq_names
|
210
|
+
|
211
|
+
def status
|
212
|
+
puts commands.status
|
213
|
+
end
|
214
|
+
|
215
|
+
# Stop all commands in the command set, then destroy the command
|
216
|
+
# set, starting with an empty one.
|
217
|
+
def reset(&block)
|
218
|
+
old_commands = commands
|
219
|
+
@commands = CommandSet.new
|
220
|
+
old_commands.join(&block)
|
221
|
+
end
|
222
|
+
|
223
|
+
def stop(name)
|
224
|
+
logger.info { "Stopping #{name}" }
|
225
|
+
commands.stop(name)
|
226
|
+
end
|
227
|
+
|
228
|
+
def start(name)
|
229
|
+
logger.info { "Starting #{name}" }
|
230
|
+
commands.start(name)
|
231
|
+
end
|
232
|
+
|
233
|
+
def restart(name)
|
234
|
+
logger.info { "Restarting #{name}" }
|
235
|
+
commands.restart(name)
|
236
|
+
end
|
237
|
+
|
238
|
+
def shutdown
|
239
|
+
if commands.stopped?
|
240
|
+
EventMachine.stop_event_loop
|
241
|
+
else
|
242
|
+
timer = EventMachine.add_timer(30) do
|
243
|
+
logger.error { "could not halt all processes; giving up :(" }
|
244
|
+
EventMachine.stop_event_loop
|
245
|
+
end
|
246
|
+
|
247
|
+
commands.on(:stopped) do
|
248
|
+
EventMachine.cancel_timer(timer)
|
249
|
+
EventMachine.stop_event_loop
|
250
|
+
end
|
251
|
+
|
252
|
+
commands.stop_all
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def lastlog(arg=//)
|
257
|
+
regex = case arg
|
258
|
+
when String
|
259
|
+
if arg.eql?(arg.downcase)
|
260
|
+
/#{arg}/i
|
261
|
+
else
|
262
|
+
/#{arg}/
|
263
|
+
end
|
264
|
+
when Regexp
|
265
|
+
arg
|
266
|
+
else
|
267
|
+
raise ArgumentError, 'need string or regexp'
|
268
|
+
end
|
269
|
+
|
270
|
+
@buffer.each do |msg|
|
271
|
+
puts msg if msg =~ regex
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def puts(message = '')
|
276
|
+
# See http://stackoverflow.com/questions/1512028/gnu-readline-how-do-clear-the-input-line
|
277
|
+
print "\b \b" * Readline.line_buffer.size
|
278
|
+
print "\r"
|
279
|
+
begin
|
280
|
+
$stdout.puts message
|
281
|
+
$stdout.flush
|
282
|
+
ensure
|
283
|
+
::Readline.forced_update_display
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
+
module Console
|
25
|
+
|
26
|
+
module Mux
|
27
|
+
|
28
|
+
##
|
29
|
+
# IO Outputter invokes print then flush on the wrapped IO
|
30
|
+
# object. If the IO stream dies, IOOutputter sets itself to OFF
|
31
|
+
# and the system continues on its merry way.
|
32
|
+
#
|
33
|
+
# To find out why an IO stream died, create a logger named 'log4r'
|
34
|
+
# and look at the output.
|
35
|
+
|
36
|
+
class ConsoleOutputter < Log4r::Outputter
|
37
|
+
|
38
|
+
def initialize(_name, console, hash={})
|
39
|
+
@console = console
|
40
|
+
super(_name, hash)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# perform the write (copied from Log4r::IOOutputter but uses console.puts)
|
46
|
+
def write(data)
|
47
|
+
begin
|
48
|
+
@console.puts data
|
49
|
+
rescue => e # recover from this instead of crash
|
50
|
+
Log4r::Logger.log_internal {"IOError in Outputter '#{@name}'!"}
|
51
|
+
Log4r::Logger.log_internal {e}
|
52
|
+
raise e
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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
|
+
class << ENV
|
22
|
+
# Merge the values from `hash` into the global ENV; yield the block;
|
23
|
+
# restore the original values of ENV.
|
24
|
+
#
|
25
|
+
# @param [Hash] hash
|
26
|
+
def with_merged(hash)
|
27
|
+
ENV.restore do
|
28
|
+
ENV.update(hash)
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Not threadsafe. Save the current ENV, yield to the block, then restore the original ENV.
|
34
|
+
def restore
|
35
|
+
# save original values
|
36
|
+
orig = ENV.to_hash
|
37
|
+
|
38
|
+
begin
|
39
|
+
yield
|
40
|
+
ensure
|
41
|
+
ENV.clear
|
42
|
+
ENV.update(orig)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,41 @@
|
|
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
|
+
module Console
|
22
|
+
module Mux
|
23
|
+
module Events
|
24
|
+
def event_handlers
|
25
|
+
@event_handlers ||= Hash.new { |h,k| h[k] = Array.new }
|
26
|
+
end
|
27
|
+
|
28
|
+
def on(event, &handler)
|
29
|
+
event_handlers[event] << handler
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def fire(event, *args)
|
35
|
+
event_handlers[event].each do |handler|
|
36
|
+
handler.call(event, self, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
+
class Object
|
22
|
+
# For each symbol in `syms`, call the corresponding method with
|
23
|
+
# `*args`, then call subsequent methods with the return value of the
|
24
|
+
# previous call.
|
25
|
+
#
|
26
|
+
# @return [Object] the value of the last method called
|
27
|
+
def compose(syms, *args)
|
28
|
+
syms.reduce(args) do |args, sym|
|
29
|
+
method(sym).call(*args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|