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
@@ -0,0 +1,206 @@
|
|
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
|
+
require 'pty'
|
23
|
+
|
24
|
+
require 'console/mux/env_with_merged'
|
25
|
+
require 'console/mux/pty_handler'
|
26
|
+
require 'console/mux/events'
|
27
|
+
|
28
|
+
module Console
|
29
|
+
module Mux
|
30
|
+
class Process
|
31
|
+
include Log4r
|
32
|
+
include Console::Mux::Events
|
33
|
+
|
34
|
+
attr_reader :command, :pid, :logger, :started_at, :name
|
35
|
+
|
36
|
+
class << self
|
37
|
+
alias_method :start, :new
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(command, name = nil)
|
41
|
+
@command = command
|
42
|
+
@name = name
|
43
|
+
@logger = Logger["process::#{name}"] || Logger.new("process::#{name}")
|
44
|
+
start
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop(&block)
|
48
|
+
if @handler
|
49
|
+
@handler.detach
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def running?
|
54
|
+
@handler != nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def name
|
58
|
+
command.name
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
"#{command} (#{running? ? pid : 'stopped'})"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Called by the child handler
|
66
|
+
def unbind
|
67
|
+
@stdin.close rescue nil
|
68
|
+
@stdout.close rescue nil
|
69
|
+
|
70
|
+
pid2, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
71
|
+
if pid2
|
72
|
+
on_exit(status)
|
73
|
+
else
|
74
|
+
try_kill(['INT', 'TERM', 'KILL'])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def receive_line(line)
|
79
|
+
logger.info { line }
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Integer] uptime in seconds
|
83
|
+
def uptime
|
84
|
+
Time.now.to_i - started_at.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
# Capture the output of a single field using the 'ps' command.
|
88
|
+
def ps1(field)
|
89
|
+
`ps -o #{field}= -p #{pid}`.strip
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [String] elapsed time
|
93
|
+
def etime
|
94
|
+
ps1('etime')
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Integer] resident size in bytes
|
98
|
+
def rss
|
99
|
+
ps1('rss').to_i * 1024
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [String] cputime in
|
103
|
+
def cputime
|
104
|
+
ps1('cputime')
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
# This is for comparison purposes.
|
110
|
+
def to_hash
|
111
|
+
{
|
112
|
+
:name => name,
|
113
|
+
:command => command,
|
114
|
+
:opts => opts
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# Clean some common crud from ENV before spawning a command.
|
121
|
+
def with_clean_env(env)
|
122
|
+
ENV.restore do
|
123
|
+
ENV.delete_if do |k,_|
|
124
|
+
k.start_with?('BUNDLE_') ||
|
125
|
+
k == 'RUBY' ||
|
126
|
+
k.start_with?('GEM_')
|
127
|
+
end
|
128
|
+
ENV.update(env)
|
129
|
+
yield
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def start
|
134
|
+
raise 'already started' if @handler
|
135
|
+
|
136
|
+
@started_at = Time.now
|
137
|
+
|
138
|
+
stdin, stdout, pid = Dir.chdir command.dir do
|
139
|
+
with_clean_env(command.env) do
|
140
|
+
PTY.spawn(command.commandline)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
@stdin = stdin
|
145
|
+
@stdout = stdout
|
146
|
+
@pid = pid
|
147
|
+
|
148
|
+
logger.info { "in #{File.expand_path(command.dir)}" }
|
149
|
+
logger.info { "with #{command.env.to_a.map{|k,v| "#{k}=#{v}"}.join(' ')}" }
|
150
|
+
logger.info { command }
|
151
|
+
logger.info { "started process #{pid}" }
|
152
|
+
|
153
|
+
@handler = EventMachine.attach(stdout, PTYHandler, self)
|
154
|
+
end
|
155
|
+
|
156
|
+
def try_kill(signals)
|
157
|
+
if (signals.size > 0)
|
158
|
+
signal = signals.shift
|
159
|
+
logger.info "sending #{signal} to #{pid}"
|
160
|
+
::Process.kill(signal, pid) rescue nil
|
161
|
+
|
162
|
+
check(20, 0.1) do
|
163
|
+
try_kill(signals)
|
164
|
+
end
|
165
|
+
else
|
166
|
+
on_exit(nil)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def check(tries, period, &on_fail)
|
171
|
+
if tries <= 0
|
172
|
+
on_fail.call
|
173
|
+
return
|
174
|
+
end
|
175
|
+
|
176
|
+
EventMachine.add_timer(period) do
|
177
|
+
pid2, status = ::Process.waitpid2(pid, ::Process::WNOHANG)
|
178
|
+
if !pid2
|
179
|
+
check(tries - 1, period, &on_fail)
|
180
|
+
else
|
181
|
+
on_exit(status)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def on_exit(status)
|
187
|
+
begin
|
188
|
+
if status == nil
|
189
|
+
logger.warn { "error killing process #{pid}" }
|
190
|
+
elsif status.signaled?
|
191
|
+
signo = status.termsig
|
192
|
+
signm = SignalException.new(signo).signm
|
193
|
+
logger.warn { "process #{pid} exited with uncaught signal #{signm} (#{signo})" }
|
194
|
+
elsif status.exited?
|
195
|
+
logger.info { "process #{pid} exited #{status.exitstatus}" }
|
196
|
+
else
|
197
|
+
logger.warn { "process #{pid} exited #{status.inspect}" }
|
198
|
+
end
|
199
|
+
ensure
|
200
|
+
@handler = nil
|
201
|
+
fire(:exit)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
# Handle input from a PTY that has been connected to EventMachine
|
24
|
+
module PTYHandler
|
25
|
+
include EventMachine::Protocols::LineText2
|
26
|
+
|
27
|
+
attr_reader :parent
|
28
|
+
|
29
|
+
# @param [Object] parent unbind and receive_line events are
|
30
|
+
# forwarded to this object
|
31
|
+
def initialize(parent)
|
32
|
+
super()
|
33
|
+
@parent = parent
|
34
|
+
end
|
35
|
+
|
36
|
+
def unbind
|
37
|
+
parent.unbind
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def receive_line(line)
|
42
|
+
parent.receive_line(line)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
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
|
+
# Array that limits itself to some fixed size. Newly added elements
|
24
|
+
# kick out the oldest element when at the limit.
|
25
|
+
class RollingArray < Array
|
26
|
+
attr_accessor :maxsize
|
27
|
+
|
28
|
+
# @param maxsize the maximum size of the array
|
29
|
+
def initialize(maxsize)
|
30
|
+
@maxsize = maxsize
|
31
|
+
end
|
32
|
+
|
33
|
+
# Append an element to the array. If the size of the array would
|
34
|
+
# overflow the maxsize, the array is shifted.
|
35
|
+
def <<(other)
|
36
|
+
super
|
37
|
+
shift if size > maxsize
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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
|
+
class RunWith
|
24
|
+
class << self
|
25
|
+
# A filter for running a command with RVM shell. Looks at the
|
26
|
+
# :ruby option.
|
27
|
+
#
|
28
|
+
# @param [Command] c
|
29
|
+
# @param [String] str the command string
|
30
|
+
def rvm_shell(c, str)
|
31
|
+
rvm_command = 'rvm-shell '
|
32
|
+
rvm_command += "'#{c[:ruby]}' " if c[:ruby]
|
33
|
+
rvm_command += "-c '#{str}'"
|
34
|
+
rvm_command
|
35
|
+
end
|
36
|
+
|
37
|
+
# A filter for running a command with rbenv. Sets RBENV_VERSION
|
38
|
+
# environment var will be set to the value of the :ruby
|
39
|
+
# option.
|
40
|
+
#
|
41
|
+
# @param [Command] c
|
42
|
+
# @param [String] str the command string
|
43
|
+
def rbenv(c, str)
|
44
|
+
c.env['RBENV_VERSION'] = c[:ruby]
|
45
|
+
|
46
|
+
# Strip out any rbenv version bin path. Assumes rbenv is
|
47
|
+
# installed in .rbenv. This is necessary because if we run
|
48
|
+
# console-mux via an rbenv shim, that shim first tacks on
|
49
|
+
# the bin path to the in-use ruby version prior to exec'ing
|
50
|
+
# the actual console-mux... But we don't want that path to
|
51
|
+
# follow down to sub processes spawned by console-mux.
|
52
|
+
c.env['PATH'] =
|
53
|
+
ENV['PATH'].
|
54
|
+
split(File::PATH_SEPARATOR).
|
55
|
+
reject{|p| p =~ %r{.rbenv/versions}}.
|
56
|
+
join(File::PATH_SEPARATOR)
|
57
|
+
|
58
|
+
str
|
59
|
+
end
|
60
|
+
|
61
|
+
# A filter for running a command with bundle exec. Ensures
|
62
|
+
# the BUNDLE_GEMFILE environment variable is cleared before
|
63
|
+
# running bundle exec.
|
64
|
+
def bundle_exec(c, str)
|
65
|
+
"bundle exec #{str}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def bundle_exec_sh(c, str)
|
69
|
+
"#{::Console::Mux::BUNDLE_EXEC_SH} #{str}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,85 @@
|
|
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
|
+
# The interactive console is run within an instance of Shell. All
|
24
|
+
# instance methods are thus available as user commands.
|
25
|
+
class Shell
|
26
|
+
attr_reader :console
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def delegate(obj_getter, method_syms)
|
30
|
+
method_syms.each do |sym|
|
31
|
+
send(:define_method, sym) do |*args, &block|
|
32
|
+
obj = send(obj_getter)
|
33
|
+
obj.send(sym, *args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def delegate_ok(obj_getter, method_syms)
|
39
|
+
method_syms.each do |sym|
|
40
|
+
send(:define_method, sym) do |*args, &block|
|
41
|
+
obj = send(obj_getter)
|
42
|
+
obj.send(sym, *args, &block)
|
43
|
+
:ok
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(console)
|
50
|
+
@console = console
|
51
|
+
end
|
52
|
+
|
53
|
+
def commands
|
54
|
+
console.commands
|
55
|
+
end
|
56
|
+
|
57
|
+
delegate :console, [
|
58
|
+
:default_options
|
59
|
+
]
|
60
|
+
|
61
|
+
delegate_ok :console, [
|
62
|
+
:status,
|
63
|
+
:lastlog,
|
64
|
+
:run,
|
65
|
+
:start,
|
66
|
+
:stop,
|
67
|
+
:restart,
|
68
|
+
:set_default_options,
|
69
|
+
:shutdown
|
70
|
+
]
|
71
|
+
|
72
|
+
# Yield the block with +hash+ merged into the default options.
|
73
|
+
# The options are restored.
|
74
|
+
def with_defaults(hash={})
|
75
|
+
orig_options = default_options
|
76
|
+
set_default_options(default_options.merge(hash))
|
77
|
+
begin
|
78
|
+
yield
|
79
|
+
ensure
|
80
|
+
set_default_options(orig_options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|