jmesnil / jmx4r

a JMX library for JRuby

This URL has Read+Write access

jmesnil (author)
Thu Oct 22 05:36:35 -0700 2009
commit  092aed8e0cefb886464ac4ae5aa4f41b3de4547f
tree    c925f9e3d2e28c8ad7be9f7d653ac4eda129f833
parent  d6cc276484dd57d486bafa07552cdaf497a43d7d
jmx4r / lib / jmx4r.rb
100644 293 lines (266 sloc) 10.818 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# Copyright 2007 Jeff Mesnil (http://jmesnil.net)
require 'java'
 
class String
  # Transform a CamelCase String to a snake_case String.
  #--
  # Code has been taken from ActiveRecord
  def snake_case
    self.to_s.gsub(/::/, '/').
    gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
    gsub(/([a-z\d])([A-Z])/,'\1_\2').
    tr("-", "_").
    downcase
  end
end
 
module JMX
  require 'dynamic_mbean'
  require 'open_data_helper'
  require 'objectname_helper'
  require 'jdk_helper'
  require 'jruby'
 
  class MBeanServerConnectionProxy
    attr_reader :connector
 
    # Adds a connector attribute to Java's native MBeanServerConnection class.
    #
    # The connector attribute can be used to manage the connection (e.g, to close it).
    # Why this isn't included in the native MBeanServerConnection class is beyond me.
    #
    # connector:: JMXConnector instance as returned by JMXConnectorFactory.connect.
    def initialize(connector)
      @connector = connector
      @connection = connector.getMBeanServerConnection
    end
 
    # Close the connection (an unfortunate omission from the MBeanServerConnection class, imho)
    def close
      @connector.close
    end
 
    # Forward all other method messages to the underlying MBeanServerConnection instance.
    def method_missing(method, *args, &block)
      @connection.send method, *args, &block
    end
  end
 
  class MBean
    include_class 'java.util.HashMap'
    include_class 'javax.naming.Context'
    include_class 'javax.management.Attribute'
    include_class 'javax.management.ObjectName'
    include_class 'javax.management.remote.JMXConnector'
    include_class 'javax.management.remote.JMXConnectorFactory'
    include_class 'javax.management.remote.JMXServiceURL'
    JThread = java.lang.Thread
 
    attr_reader :object_name, :operations, :attributes, :connection
 
    def metaclass; class << self; self; end; end
    def meta_def name, &blk
      metaclass.instance_eval do
          define_method name, &blk
      end
    end
 
    # Creates a new MBean.
    #
    # object_name:: a string corresponding to a valid ObjectName
    # connection:: a connection to a MBean server. If none is passed,
    # use the global connection created by
    # MBean.establish_connection
    def initialize(object_name, connection=nil)
      @connection = connection || @@connection
      @object_name = object_name
      info = @connection.getMBeanInfo @object_name
      @attributes = Hash.new
      info.attributes.each do | mbean_attr |
        @attributes[mbean_attr.name.snake_case] = mbean_attr.name
      end
      @operations = Hash.new
      info.operations.each do |mbean_op|
        param_types = mbean_op.signature.map {|param| param.type}
        @operations[mbean_op.name.snake_case] = [mbean_op.name, param_types]
      end
    end
 
    def method_missing(method, *args, &block) #:nodoc:
      method_in_snake_case = method.to_s.snake_case # this way Java/JRuby styles are compatible
 
      if @operations.keys.include?(method_in_snake_case)
        op_name, param_types = @operations[method_in_snake_case]
        @connection.invoke @object_name,
                           op_name,
                           args.to_java(:Object),
                           param_types.to_java(:String)
      else
        super
      end
    end
 
    @@connection = nil
 
    # establish a connection to a remote MBean server which will
    # be used by all subsequent MBeans.
    #
    # See MBean.create_connection for a list of the keys that are
    # accepted in arguments.
    #
    # Examples
    #
    # JMX::MBean.establish_connection :port => "node23", :port => 1090
    # JMX::MBean.establish_connection :port => "node23", :username => "jeff", :password => "secret"
    # JMX::MBean.establish_connection :command => /jconsole/i
    def self.establish_connection(args={})
      @@connection ||= create_connection args
    end
 
    def self.remove_connection(args={})
      if @@connection
        @@connection.close rescue nil
      end
      @@connection = nil
    end
 
    def self.connection(args={})
      if args.has_key? :host or args.has_key? :port
        return create_connection(args)
      else
        @@connection ||= MBean.establish_connection(args)
      end
    end
 
    # Create a connection to a remote MBean server.
    #
    # The args accepts the following keys:
    #
    # [:host] the host of the MBean server (defaults to "localhost")
    #
    # [:port] the port of the MBean server (defaults to 3000)
    #
    # [:url] the url of the MBean server.
    # No default.
    # if the url is specified, the host & port parameters are
    # not taken into account
    #
    # [:command] the pattern matches the command line of the local
    # JVM process including the MBean server.
    # (command lines are listed on the connection dialog
    # in JConsole).
    # No default.
    # this feature needs a JDK (>=5) installed on the local
    # system.
    # if the command is specified, the host & port or the url
    # parameters are not taken into account
    #
    # [:username] the name of the user (if the MBean server requires authentication).
    # No default
    #
    # [:password] the password of the user (if the MBean server requires authentication).
    # No default
    #
    # [:credentials] custom credentials (if the MBean server requires authentication).
    # No default. It has precedence over :username and :password (i.e. if
    # :credentials is specified, :username & :password are ignored)
    #
    # [:provider_package] use to fill the JMXConnectorFactory::PROTOCOL_PROVIDER_PACKAGES.
    # No default
    #
    def self.create_connection(args={})
      host= args[:host] || "localhost"
      port = args[:port] || 3000
      username = args[:username]
      password = args[:password]
      credentials = args[:credentials]
      provider_package = args[:provider_package]
      
      if args[:command]
        url = JDKHelper.find_local_url(args[:command]) or
          raise "no locally attacheable VMs"
      else
        # host & port are not taken into account if url is set (see issue #7)
        standard_url = "service:jmx:rmi:///jndi/rmi://#{host}:#{port}/jmxrmi"
        url = args[:url] || standard_url
      end
      
      unless credentials
        if !username.nil? and username.length > 0
          user_password_credentials = [username, password]
          credentials = user_password_credentials.to_java(:String)
        end
      end
      
      env = HashMap.new
      env.put(JMXConnector::CREDENTIALS, credentials) if credentials
      # only fill the Context and JMXConnectorFactory properties if provider_package is set
      if provider_package
        env.put(Context::SECURITY_PRINCIPAL, username) if username
        env.put(Context::SECURITY_CREDENTIALS, password) if password
        env.put(JMXConnectorFactory::PROTOCOL_PROVIDER_PACKAGES, provider_package)
      end
 
      # the context class loader is set to JRuby's classloader when
      # creating the JMX Connection so that classes loaded using
      # JRuby "require" (and not from its classpath) can also be
      # accessed (see issue #6)
      begin
        context_class_loader = JThread.current_thread.context_class_loader
        JThread.current_thread.context_class_loader = JRuby.runtime.getJRubyClassLoader
        
        connector = JMXConnectorFactory::connect JMXServiceURL.new(url), env
        MBeanServerConnectionProxy.new connector
      ensure
        # ... and we reset the previous context class loader
        JThread.current_thread.context_class_loader = context_class_loader
      end
    end
 
    # Returns an array of MBeans corresponding to all the MBeans
    # registered for the ObjectName passed in parameter (which may be
    # a pattern).
    #
    # The args accepts the same keys than #create_connection and an
    # additional one:
    #
    # [:connection] a MBean server connection (as returned by #create_connection)
    # No default. It has precedence over :host and :port (i.e if
    # :connection is specified, :host and :port are ignored)
    #
    def self.find_all_by_name(name, args={})
      object_name = ObjectName.new(name)
      connection = args[:connection] || MBean.connection(args)
      object_names = connection.queryNames(object_name, nil)
      object_names.map { |on| create_mbean on, connection }
    end
 
    # Same as #find_all_by_name but the ObjectName passed in parameter
    # can not be a pattern.
    # Only one single MBean is returned.
    def self.find_by_name(name, args={})
      connection = args[:connection] || MBean.connection(args)
      create_mbean ObjectName.new(name), connection
    end
 
    def self.create_mbean(object_name, connection)
      info = connection.getMBeanInfo object_name
      mbean = MBean.new object_name, connection
      # define attribute accessor methods for the mbean
      info.attributes.each do |mbean_attr|
        mbean.meta_def mbean_attr.name.snake_case do
          connection.getAttribute object_name, mbean_attr.name
        end
        if mbean_attr.isWritable
          mbean.meta_def "#{mbean_attr.name.snake_case}=" do |value|
            attribute = Attribute.new mbean_attr.name, value
            connection.setAttribute object_name, attribute
          end
        end
      end
      mbean
    end
 
    def self.pretty_print (object_name, args={})
      connection = args[:connection] || MBean.connection(args)
      info = connection.getMBeanInfo ObjectName.new(object_name)
      puts "object_name: #{object_name}"
      puts "class: #{info.class_name}"
      puts "description: #{info.description}"
      puts "operations:"
      info.operations.each do | op |
        puts " #{op.name}"
        op.signature.each do | param |
          puts " #{param.name} (#{param.type} #{param.description})"
        end
        puts " ----"
        puts " description: #{op.description}"
        puts " return_type: #{op.return_type}"
        puts " impact: #{op.impact}"
      end
      puts "attributes:"
      info.attributes.each do | attr |
        puts " #{attr.name}"
        puts " description: #{attr.description}"
        puts " type: #{attr.type}"
        puts " readable: #{attr.readable}"
        puts " writable: #{attr.writable}"
        puts " is: #{attr.is}"
      end
    end
  end
end