jmx4r 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -29,6 +29,11 @@ jmx4r helps to manage Java applications using JMX in a simple and powerful way:
29
29
  # trigger a Garbage Collection
30
30
  memory.gc
31
31
 
32
+ # For local processes not publishing jmxrmi ports, instead:
33
+
34
+ # connect to the local JConsole process
35
+ JMX::MBean.establish_connection :command => /jconsole/i
36
+
32
37
  == Help
33
38
 
34
39
  * Wiki[http://jmesnil.net/wiki/Jmx4r]
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require "rubygems"
6
6
 
7
7
  dir = File.dirname(__FILE__)
8
8
  lib = File.join(dir, "lib", "jmx4r.rb")
9
- version = "0.0.8"
9
+ version = "0.1.0"
10
10
 
11
11
  task :default => [:test]
12
12
 
data/lib/dynamic_mbean.rb CHANGED
@@ -3,10 +3,10 @@
3
3
  # taken from the 'jmx' gems in jruby-extras
4
4
 
5
5
  module JMX
6
- import javax.management.MBeanParameterInfo
7
- import javax.management.MBeanOperationInfo
8
- import javax.management.MBeanAttributeInfo
9
- import javax.management.MBeanInfo
6
+ java_import javax.management.MBeanParameterInfo
7
+ java_import javax.management.MBeanOperationInfo
8
+ java_import javax.management.MBeanAttributeInfo
9
+ java_import javax.management.MBeanInfo
10
10
 
11
11
  # Module that is used to bridge java to ruby and ruby to java types.
12
12
  module JavaTypeAware
@@ -99,32 +99,36 @@ module JMX
99
99
  # like: user_name= and username. So in your ruby code you can treat the attributes
100
100
  # as "regular" ruby accessors
101
101
  class DynamicMBean
102
- import javax.management.MBeanOperationInfo
103
- import javax.management.MBeanAttributeInfo
104
- import javax.management.DynamicMBean
105
- import javax.management.MBeanInfo
102
+ java_import javax.management.MBeanOperationInfo
103
+ java_import javax.management.MBeanAttributeInfo
104
+ java_import javax.management.DynamicMBean
105
+ java_import javax.management.MBeanInfo
106
106
  include JMX::JavaTypeAware
107
107
 
108
108
  #NOTE this will not be needed when JRuby-3164 is fixed.
109
109
  def self.inherited(cls)
110
110
  cls.send(:include, DynamicMBean)
111
111
  end
112
+
113
+ def self.mbean_attributes
114
+ @mbean_attributes ||= {}
115
+ end
112
116
 
113
117
  # TODO: preserve any original method_added?
114
118
  # TODO: Error handling here when it all goes wrong?
115
119
  def self.method_added(name) #:nodoc:
116
- return if Thread.current[:op].nil?
117
- Thread.current[:op].name = name
118
- operations << Thread.current[:op].to_jmx
119
- Thread.current[:op] = nil
120
+ return if self.mbean_attributes[:op].nil?
121
+ self.mbean_attributes[:op].name = name
122
+ operations << self.mbean_attributes[:op].to_jmx
123
+ self.mbean_attributes[:op] = nil
120
124
  end
121
125
 
122
126
  def self.attributes #:nodoc:
123
- Thread.current[:attrs] ||= []
127
+ self.mbean_attributes[:attrs] ||= []
124
128
  end
125
129
 
126
130
  def self.operations #:nodoc:
127
- Thread.current[:ops] ||= []
131
+ self.mbean_attributes[:ops] ||= []
128
132
  end
129
133
 
130
134
  # the <tt>rw_attribute</tt> method is used to declare a JMX read write attribute.
@@ -207,7 +211,7 @@ module JMX
207
211
  #++
208
212
  def self.operation(description=nil)
209
213
  # Wait to error check until method_added so we can know method name
210
- Thread.current[:op] = JMX::Operation.new description
214
+ self.mbean_attributes[:op] = JMX::Operation.new description
211
215
  end
212
216
 
213
217
  # Used to declare a parameter (you can declare more than one in succession) that
@@ -220,7 +224,7 @@ module JMX
220
224
  # ...
221
225
  # end
222
226
  def self.parameter(type, name=nil, description=nil)
223
- Thread.current[:op].parameters << JMX::Parameter.new(type, name, description)
227
+ self.mbean_attributes[:op].parameters << JMX::Parameter.new(type, name, description)
224
228
  end
225
229
 
226
230
  # Used to declare the return type of the operation
@@ -231,7 +235,7 @@ module JMX
231
235
  # ...
232
236
  # end
233
237
  def self.returns(type)
234
- Thread.current[:op].return_type = type
238
+ self.mbean_attributes[:op].return_type = type
235
239
  end
236
240
 
237
241
  def initialize(description="")
@@ -272,4 +276,3 @@ module JMX
272
276
  end
273
277
 
274
278
  end
275
-
data/lib/jconsole.rb CHANGED
@@ -12,7 +12,8 @@ module JConsole
12
12
  # By default, no authentication is required to connect to it.
13
13
  #
14
14
  # The args hash accepts 3 keys:
15
- # [:port] the port which will be listens to JMX connections
15
+ # [:port] the port which will be listens to JMX connections.
16
+ # if the port is 0, jmxrmi port is not published
16
17
  # [:pwd_file] the path to the file containing the authentication credentials
17
18
  # [:access_file] the path to the file containing the authorization credentials
18
19
  #
@@ -29,13 +30,19 @@ module JConsole
29
30
  cmd =<<-EOCMD.split("\n").join(" ")
30
31
  jconsole
31
32
  -J-Dcom.sun.management.jmxremote
32
- -J-Dcom.sun.management.jmxremote.port=#{port}
33
- -J-Dcom.sun.management.jmxremote.ssl=false
34
- -J-Dcom.sun.management.jmxremote.authenticate=#{!pwd_file.nil?}
35
- EOCMD
36
- if pwd_file and access_file
37
- cmd << " -J-Dcom.sun.management.jmxremote.password.file=#{pwd_file}"
38
- cmd << " -J-Dcom.sun.management.jmxremote.access.file=#{access_file}"
33
+ EOCMD
34
+
35
+ if port != 0
36
+ cmd << <<-EOCMD.split("\n").join(" ")
37
+ -J-Dcom.sun.management.jmxremote.port=#{port}
38
+ -J-Dcom.sun.management.jmxremote.ssl=false
39
+ -J-Dcom.sun.management.jmxremote.authenticate=#{!pwd_file.nil?}
40
+ EOCMD
41
+
42
+ if pwd_file and access_file
43
+ cmd << " -J-Dcom.sun.management.jmxremote.password.file=#{pwd_file}"
44
+ cmd << " -J-Dcom.sun.management.jmxremote.access.file=#{access_file}"
45
+ end
39
46
  end
40
47
  Thread.start { system cmd }
41
48
  sleep 3
@@ -46,7 +53,11 @@ EOCMD
46
53
  # By default, it will kill the process corresponding to an instance JConsole with
47
54
  # a port on 3000. Another port can be specified in parameter.
48
55
  def JConsole.stop(port=3000)
49
- jconsole_pid = `ps a -w -o pid,command | grep -w jconsole | grep port=#{port} | grep -v grep | grep -v ruby | cut -c -5`
56
+ ps = "ps a -w -o pid,command | grep -w jconsole"
57
+ ps << " | grep port=#{port}" if port != 0
58
+ ps << " | grep -v grep | grep -v ruby | cut -c -5"
59
+
60
+ jconsole_pid = `#{ps}`
50
61
  `kill #{jconsole_pid}` if jconsole_pid != ""
51
62
  sleep 1
52
63
  end
data/lib/jdk/jdk4.rb ADDED
@@ -0,0 +1,16 @@
1
+
2
+ module JMX
3
+ module JDKHelper
4
+ module JDK4
5
+
6
+ class << self
7
+ def method_missing(method, *args, &block)
8
+ raise "JDK (>= 5.0) implementation is not available - \
9
+ maybe only JREs or older JDKs are installed properly."
10
+ end
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+
data/lib/jdk/jdk5.rb ADDED
@@ -0,0 +1,34 @@
1
+
2
+ module JMX
3
+ module JDKHelper
4
+ module JDK5
5
+ include_class 'sun.jvmstat.monitor.HostIdentifier'
6
+ include_class 'sun.jvmstat.monitor.MonitoredHost'
7
+ include_class 'sun.jvmstat.monitor.MonitoredVmUtil'
8
+ include_class 'sun.jvmstat.monitor.VmIdentifier'
9
+ include_class 'sun.management.ConnectorAddressLink'
10
+
11
+ class << self
12
+
13
+ def find_local_url(command_pattern)
14
+ host_id = HostIdentifier.new(nil)
15
+ host = MonitoredHost.get_monitored_host(host_id)
16
+
17
+ host.active_vms.each do |vmid_int|
18
+ vmid = VmIdentifier.new(vmid_int.to_s)
19
+ vm = host.get_monitored_vm(vmid)
20
+ command = MonitoredVmUtil.command_line(vm)
21
+ if command_pattern === command
22
+ return ConnectorAddressLink.import_from(vmid_int)
23
+ end
24
+ end
25
+
26
+ nil
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+
data/lib/jdk/jdk6.rb ADDED
@@ -0,0 +1,69 @@
1
+
2
+ module JMX
3
+ module JDKHelper
4
+ module JDK6
5
+ include_class 'com.sun.tools.attach.VirtualMachine'
6
+
7
+ class << self
8
+ def find_local_url(command_pattern)
9
+ target_vmd = VirtualMachine.list.find do |vmd|
10
+ command_pattern === vmd.display_name
11
+ end
12
+
13
+ if target_vmd
14
+ local_connector_address(target_vmd)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def local_connector_address(vm_descriptor)
21
+ vm = VirtualMachine.attach(vm_descriptor)
22
+
23
+ address = nil
24
+ agent_loaded = false
25
+
26
+ lambda {
27
+ address = vm.get_agent_properties.get(
28
+ "com.sun.management.jmxremote.localConnectorAddress")
29
+
30
+ unless address || agent_loaded
31
+ load_management_agent(vm)
32
+ agent_loaded = true
33
+ redo
34
+ end
35
+ }.call
36
+
37
+ vm.detach
38
+
39
+ address
40
+ end
41
+
42
+ def load_management_agent(vm)
43
+ home =
44
+ vm.get_system_properties.get_property 'java.home'
45
+
46
+ try_load_management_agent(vm, [home, 'jre', 'lib']) or
47
+ try_load_management_agent(vm, [home, 'lib']) or
48
+ raise "management agent not found"
49
+ end
50
+
51
+ def try_load_management_agent(vm, path)
52
+ sep = vm.get_system_properties.get_property 'file.separator'
53
+
54
+ path = path.dup
55
+ path << 'management-agent.jar'
56
+
57
+ file = Java::java.io.File.new(path.join(sep))
58
+ if file.exists
59
+ vm.load_agent(file.get_canonical_path,
60
+ "com.sun.management.jmxremote")
61
+ true
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
69
+
data/lib/jdk_helper.rb ADDED
@@ -0,0 +1,69 @@
1
+
2
+ module JMX
3
+ module JDKHelper
4
+ include_class 'java.lang.System'
5
+
6
+ class << self
7
+
8
+ def method_missing(method, *args, &block)
9
+ init unless @jdk
10
+ @jdk.send method, *args, &block
11
+ end
12
+
13
+ private
14
+
15
+ def init
16
+ @jdk =
17
+ case
18
+ when has_java_class?("com.sun.tools.attach.VirtualMachine")
19
+ require "jdk/jdk6"
20
+ JDK6
21
+ when has_java_class?('sun.jvmstat.monitor.MonitoredHost')
22
+ require "jdk/jdk5"
23
+ JDK5
24
+ else
25
+ require "jdk/jdk4"
26
+ JDK4
27
+ end
28
+ end
29
+
30
+ def has_java_class?(name)
31
+ begin
32
+ include_class name
33
+ true
34
+ rescue
35
+ retry if load_tools_jar
36
+ false
37
+ end
38
+ end
39
+
40
+ def load_tools_jar
41
+ unless @tools_loaded
42
+ home = System.get_property 'java.home'
43
+ paths = [
44
+ [home, '..', 'lib'],
45
+ [home, 'lib'],
46
+ ]
47
+ try_load_jar('tools.jar', paths)
48
+ @tools_loaded = true
49
+ true
50
+ end
51
+ end
52
+
53
+ def try_load_jar(jar_file, paths)
54
+ sep = System.get_property 'file.separator'
55
+ paths = paths.dup
56
+ begin
57
+ path = paths.shift
58
+ require path.join(sep) + sep + jar_file
59
+ true
60
+ rescue LoadError
61
+ retry unless paths.empty?
62
+ false
63
+ end
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+
data/lib/jmx4r.rb CHANGED
@@ -18,6 +18,7 @@ module JMX
18
18
  require 'dynamic_mbean'
19
19
  require 'open_data_helper'
20
20
  require 'objectname_helper'
21
+ require 'jdk_helper'
21
22
  require 'jruby'
22
23
 
23
24
  class MBeanServerConnectionProxy
@@ -57,6 +58,13 @@ module JMX
57
58
 
58
59
  attr_reader :object_name, :operations, :attributes, :connection
59
60
 
61
+ def metaclass; class << self; self; end; end
62
+ def meta_def name, &blk
63
+ metaclass.instance_eval do
64
+ define_method name, &blk
65
+ end
66
+ end
67
+
60
68
  # Creates a new MBean.
61
69
  #
62
70
  # object_name:: a string corresponding to a valid ObjectName
@@ -70,19 +78,6 @@ module JMX
70
78
  @attributes = Hash.new
71
79
  info.attributes.each do | mbean_attr |
72
80
  @attributes[mbean_attr.name.snake_case] = mbean_attr.name
73
- self.class.instance_eval do
74
- define_method mbean_attr.name.snake_case do
75
- @connection.getAttribute @object_name, "#{mbean_attr.name}"
76
- end
77
- end
78
- if mbean_attr.isWritable
79
- self.class.instance_eval do
80
- define_method "#{mbean_attr.name.snake_case}=" do |value|
81
- attr = Attribute.new mbean_attr.name, value
82
- @connection.setAttribute @object_name, attr
83
- end
84
- end
85
- end
86
81
  end
87
82
  @operations = Hash.new
88
83
  info.operations.each do |mbean_op|
@@ -92,12 +87,14 @@ module JMX
92
87
  end
93
88
 
94
89
  def method_missing(method, *args, &block) #:nodoc:
95
- if @operations.keys.include?(method.to_s)
96
- op_name, param_types = @operations[method.to_s]
90
+ method_in_snake_case = method.to_s.snake_case # this way Java/JRuby styles are compatible
91
+
92
+ if @operations.keys.include?(method_in_snake_case)
93
+ op_name, param_types = @operations[method_in_snake_case]
97
94
  @connection.invoke @object_name,
98
- op_name,
99
- args.to_java(:Object),
100
- param_types.to_java(:String)
95
+ op_name,
96
+ args.to_java(:Object),
97
+ param_types.to_java(:String)
101
98
  else
102
99
  super
103
100
  end
@@ -115,6 +112,7 @@ module JMX
115
112
  #
116
113
  # JMX::MBean.establish_connection :port => "node23", :port => 1090
117
114
  # JMX::MBean.establish_connection :port => "node23", :username => "jeff", :password => "secret"
115
+ # JMX::MBean.establish_connection :command => /jconsole/i
118
116
  def self.establish_connection(args={})
119
117
  @@connection ||= create_connection args
120
118
  end
@@ -147,6 +145,16 @@ module JMX
147
145
  # if the url is specified, the host & port parameters are
148
146
  # not taken into account
149
147
  #
148
+ # [:command] the pattern matches the command line of the local
149
+ # JVM process including the MBean server.
150
+ # (command lines are listed on the connection dialog
151
+ # in JConsole).
152
+ # No default.
153
+ # this feature needs a JDK (>=5) installed on the local
154
+ # system.
155
+ # if the command is specified, the host & port or the url
156
+ # parameters are not taken into account
157
+ #
150
158
  # [:username] the name of the user (if the MBean server requires authentication).
151
159
  # No default
152
160
  #
@@ -168,9 +176,14 @@ module JMX
168
176
  credentials = args[:credentials]
169
177
  provider_package = args[:provider_package]
170
178
 
171
- # host & port are not taken into account if url is set (see issue #7)
172
- standard_url = "service:jmx:rmi:///jndi/rmi://#{host}:#{port}/jmxrmi"
173
- url = args[:url] || standard_url
179
+ if args[:command]
180
+ url = JDKHelper.find_local_url(args[:command]) or
181
+ raise "no locally attacheable VMs"
182
+ else
183
+ # host & port are not taken into account if url is set (see issue #7)
184
+ standard_url = "service:jmx:rmi:///jndi/rmi://#{host}:#{port}/jmxrmi"
185
+ url = args[:url] || standard_url
186
+ end
174
187
 
175
188
  unless credentials
176
189
  if !username.nil? and username.length > 0
@@ -219,7 +232,7 @@ module JMX
219
232
  object_name = ObjectName.new(name)
220
233
  connection = args[:connection] || MBean.connection(args)
221
234
  object_names = connection.queryNames(object_name, nil)
222
- object_names.map { |on| MBean.new(on, connection) }
235
+ object_names.map { |on| create_mbean on, connection }
223
236
  end
224
237
 
225
238
  # Same as #find_all_by_name but the ObjectName passed in parameter
@@ -227,7 +240,25 @@ module JMX
227
240
  # Only one single MBean is returned.
228
241
  def self.find_by_name(name, args={})
229
242
  connection = args[:connection] || MBean.connection(args)
230
- MBean.new ObjectName.new(name), connection
243
+ create_mbean ObjectName.new(name), connection
244
+ end
245
+
246
+ def self.create_mbean(object_name, connection)
247
+ info = connection.getMBeanInfo object_name
248
+ mbean = MBean.new object_name, connection
249
+ # define attribute accessor methods for the mbean
250
+ info.attributes.each do |mbean_attr|
251
+ mbean.meta_def mbean_attr.name.snake_case do
252
+ connection.getAttribute object_name, mbean_attr.name
253
+ end
254
+ if mbean_attr.isWritable
255
+ mbean.meta_def "#{mbean_attr.name.snake_case}=" do |value|
256
+ attribute = Attribute.new mbean_attr.name, value
257
+ connection.setAttribute object_name, attribute
258
+ end
259
+ end
260
+ end
261
+ mbean
231
262
  end
232
263
 
233
264
  def self.pretty_print (object_name, args={})
@@ -34,4 +34,11 @@ class TestAttribute < Test::Unit::TestCase
34
34
  def test_non_writable_attribute
35
35
  assert_raise(NoMethodError) { @memory.object_pending_finalization_count = -1 }
36
36
  end
37
+
38
+ def test_non_overlapping_attributes
39
+ assert_raise(NoMethodError) { @memory.logger_names }
40
+ logging = JMX::MBean.find_by_name "java.util.logging:type=Logging", :connection => ManagementFactory.platform_mbean_server
41
+ assert_raise(NoMethodError) { logging.verbose }
42
+ assert_raise(NoMethodError) { @memory.logger_names }
43
+ end
37
44
  end
@@ -77,4 +77,15 @@ class TestConnection < Test::Unit::TestCase
77
77
  end
78
78
  end
79
79
 
80
+ def test_establish_connection_local
81
+ begin
82
+ JConsole::start :port => 0
83
+ connection = JMX::MBean.establish_connection \
84
+ :command => /jconsole/i
85
+ assert(connection.getMBeanCount > 0)
86
+ ensure
87
+ JConsole::stop 0
88
+ end
89
+ end
90
+
80
91
  end
@@ -76,4 +76,53 @@ class TestDynamicMBean < Test::Unit::TestCase
76
76
  mbean = JMX::MBean.find_by_name "jmx4r:name=OperationInvocationMBean", :connection => mbeanServer
77
77
  assert_equal("oof", mbean.reverse("foo"))
78
78
  end
79
+
80
+ class Foo < JMX::DynamicMBean
81
+ rw_attribute :foo_attr, :string
82
+
83
+ operation
84
+ parameter :string
85
+ returns :string
86
+ def foo(arg)
87
+ "foo #{arg}"
88
+ end
89
+ end
90
+
91
+ class Bar < JMX::DynamicMBean
92
+ rw_attribute :bar_attr, :string
93
+
94
+ operation
95
+ parameter :string
96
+ returns :string
97
+ def bar(arg)
98
+ "bar #{arg}"
99
+ end
100
+ end
101
+
102
+ def test_separate_dynamic_beans_have_separate_operations_and_attributes
103
+ mbean_server = ManagementFactory.platform_mbean_server
104
+ mbean_server.register_mbean Foo.new, ObjectName.new("jmx4r:name=foo")
105
+ mbean_server.register_mbean Bar.new, ObjectName.new("jmx4r:name=bar")
106
+
107
+ foo_mbean = JMX::MBean.find_by_name "jmx4r:name=foo", :connection => mbean_server
108
+ assert_equal "foo test", foo_mbean.foo("test")
109
+ assert_raise(NoMethodError){
110
+ foo_mbean.bar("test")
111
+ }
112
+ foo_mbean.foo_attr = "test"
113
+ assert_equal "test", foo_mbean.foo_attr
114
+ assert_raise(NoMethodError){
115
+ foo_mbean.bar_attr = "test"
116
+ }
117
+ bar_mbean = JMX::MBean.find_by_name "jmx4r:name=bar", :connection => mbean_server
118
+ assert_equal "bar test", bar_mbean.bar("test")
119
+ assert_raise(NoMethodError) {
120
+ bar_mbean.foo("test")
121
+ }
122
+ bar_mbean.bar_attr = "test"
123
+ assert_equal "test", bar_mbean.bar_attr
124
+ assert_raise(NoMethodError){
125
+ bar_mbean.foo_attr = "test"
126
+ }
127
+ end
79
128
  end
@@ -0,0 +1,30 @@
1
+ # Copyright 2007 Jeff Mesnil (http://jmesnil.net)
2
+
3
+ require "test/unit"
4
+
5
+ require "jmx4r"
6
+ require "jconsole"
7
+
8
+ class TestMethods < Test::Unit::TestCase
9
+ java_import java.lang.management.ManagementFactory
10
+
11
+ def setup
12
+ @logging = JMX::MBean.find_by_name "java.util.logging:type=Logging", :connection => ManagementFactory.platform_mbean_server
13
+ end
14
+
15
+ def teardown
16
+ JMX::MBean.remove_connection
17
+ end
18
+
19
+ def test_invoke_operation
20
+ @logging.set_logger_level "global", "FINEST"
21
+ assert_equal "FINEST", @logging.get_logger_level("global")
22
+ end
23
+
24
+ # make sure we can also use Java name convention
25
+ def test_invoke_CamelCaseOperation
26
+ @logging.setLoggerLevel "global", "FINE"
27
+ assert_equal "FINE", @logging.getLoggerLevel("global")
28
+ end
29
+
30
+ end
data/test/ts_all.rb CHANGED
@@ -6,6 +6,7 @@ require "tc_connection"
6
6
  require "tc_auth"
7
7
  require "tc_multiple_connections"
8
8
  require "tc_attributes"
9
+ require "tc_methods"
9
10
  require "tc_composite_data"
10
11
  require "tc_dynamic_mbean"
11
12
 
metadata CHANGED
@@ -42,14 +42,19 @@ files:
42
42
  - examples/runtime_sysprops.rb
43
43
  - lib/dynamic_mbean.rb
44
44
  - lib/jconsole.rb
45
+ - lib/jdk_helper.rb
45
46
  - lib/jmx4r.rb
46
47
  - lib/objectname_helper.rb
47
48
  - lib/open_data_helper.rb
49
+ - lib/jdk/jdk4.rb
50
+ - lib/jdk/jdk5.rb
51
+ - lib/jdk/jdk6.rb
48
52
  - test/tc_attributes.rb
49
53
  - test/tc_auth.rb
50
54
  - test/tc_composite_data.rb
51
55
  - test/tc_connection.rb
52
56
  - test/tc_dynamic_mbean.rb
57
+ - test/tc_methods.rb
53
58
  - test/tc_multiple_connections.rb
54
59
  - test/ts_all.rb
55
60
  - Rakefile
@@ -68,12 +73,12 @@ requirements: []
68
73
 
69
74
  authors:
70
75
  - Jeff Mesnil
71
- date: 2009-06-14 22:00:00 +00:00
76
+ date: 2009-10-21 22:00:00 +00:00
72
77
  platform: ruby
73
78
  test_files:
74
79
  - test/ts_all.rb
75
80
  version: !ruby/object:Gem::Version
76
- version: 0.0.8
81
+ version: 0.1.0
77
82
  require_paths:
78
83
  - lib
79
84
  dependencies: []