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