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,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