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.
@@ -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