virtualbox 0.4.1 → 0.4.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/Readme.md +9 -9
- data/VERSION +1 -1
- data/docs/GettingStarted.md +11 -11
- data/docs/WhatsNew.md +1 -1
- data/lib/virtualbox.rb +10 -1
- data/lib/virtualbox/abstract_model.rb +47 -29
- data/lib/virtualbox/abstract_model/attributable.rb +16 -16
- data/lib/virtualbox/abstract_model/dirty.rb +10 -10
- data/lib/virtualbox/abstract_model/relatable.rb +22 -22
- data/lib/virtualbox/abstract_model/validatable.rb +4 -4
- data/lib/virtualbox/attached_device.rb +23 -23
- data/lib/virtualbox/command.rb +9 -9
- data/lib/virtualbox/dvd.rb +7 -7
- data/lib/virtualbox/ext/subclass_listing.rb +1 -1
- data/lib/virtualbox/extra_data.rb +17 -17
- data/lib/virtualbox/forwarded_port.rb +23 -23
- data/lib/virtualbox/hard_drive.rb +27 -27
- data/lib/virtualbox/image.rb +25 -18
- data/lib/virtualbox/nic.rb +22 -22
- data/lib/virtualbox/proxies/collection.rb +4 -4
- data/lib/virtualbox/shared_folder.rb +25 -25
- data/lib/virtualbox/storage_controller.rb +16 -16
- data/lib/virtualbox/vm.rb +95 -42
- data/test/virtualbox/abstract_model/attributable_test.rb +28 -28
- data/test/virtualbox/abstract_model/dirty_test.rb +13 -13
- data/test/virtualbox/abstract_model/relatable_test.rb +36 -36
- data/test/virtualbox/abstract_model/validatable_test.rb +22 -22
- data/test/virtualbox/abstract_model_test.rb +46 -36
- data/test/virtualbox/attached_device_test.rb +47 -47
- data/test/virtualbox/command_test.rb +12 -12
- data/test/virtualbox/dvd_test.rb +15 -19
- data/test/virtualbox/ext/subclass_listing_test.rb +2 -2
- data/test/virtualbox/extra_data_test.rb +37 -37
- data/test/virtualbox/forwarded_port_test.rb +31 -31
- data/test/virtualbox/hard_drive_test.rb +40 -48
- data/test/virtualbox/image_test.rb +36 -33
- data/test/virtualbox/nic_test.rb +22 -22
- data/test/virtualbox/proxies/collection_test.rb +6 -6
- data/test/virtualbox/shared_folder_test.rb +36 -36
- data/test/virtualbox/storage_controller_test.rb +14 -14
- data/test/virtualbox/vm_test.rb +121 -70
- data/test/virtualbox_test.rb +19 -0
- data/virtualbox.gemspec +5 -3
- metadata +4 -2
@@ -1,7 +1,7 @@
|
|
1
1
|
module VirtualBox
|
2
2
|
class AbstractModel
|
3
3
|
# Tracks "dirtiness" of values for a class. Its not tied to AbstractModel
|
4
|
-
# in any way other than the namespace.
|
4
|
+
# in any way other than the namespace.
|
5
5
|
#
|
6
6
|
# # Checking if a Value was Changed
|
7
7
|
#
|
@@ -12,7 +12,7 @@ module VirtualBox
|
|
12
12
|
# # Previous Value
|
13
13
|
#
|
14
14
|
# Can also view the previous value of an attribute:
|
15
|
-
#
|
15
|
+
#
|
16
16
|
# obj.foo # => "foo" initially
|
17
17
|
# obj.foo = "bar"
|
18
18
|
# obj.foo_was # => "foo"
|
@@ -56,14 +56,14 @@ module VirtualBox
|
|
56
56
|
# on an attribute.
|
57
57
|
#
|
58
58
|
# # Ignoring Dirtiness Tracking
|
59
|
-
#
|
59
|
+
#
|
60
60
|
# Sometimes, for features such as mass assignment, dirtiness tracking
|
61
61
|
# should be disabled. This can be done with the `ignore_dirty` method.
|
62
62
|
#
|
63
63
|
# ignore_dirty do |obj|
|
64
64
|
# obj.name = "Foo"
|
65
65
|
# end
|
66
|
-
#
|
66
|
+
#
|
67
67
|
# obj.changed? # => false
|
68
68
|
#
|
69
69
|
# # Clearing Dirty State
|
@@ -107,7 +107,7 @@ module VirtualBox
|
|
107
107
|
end
|
108
108
|
end
|
109
109
|
end
|
110
|
-
|
110
|
+
|
111
111
|
# Clears dirty state for a field.
|
112
112
|
#
|
113
113
|
# @param [Symbol] key The field to clear dirty state.
|
@@ -119,8 +119,8 @@ module VirtualBox
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
-
# Ignores any dirty changes during the duration of the block.
|
123
|
-
# Guarantees the dirty state will be the same before and after
|
122
|
+
# Ignores any dirty changes during the duration of the block.
|
123
|
+
# Guarantees the dirty state will be the same before and after
|
124
124
|
# the method call, but not within the block itself.
|
125
125
|
def ignore_dirty(&block)
|
126
126
|
current_changes = @changed_attributes.dup rescue nil
|
@@ -149,13 +149,13 @@ module VirtualBox
|
|
149
149
|
def changes
|
150
150
|
@changed_attributes ||= {}
|
151
151
|
end
|
152
|
-
|
152
|
+
|
153
153
|
# Method missing is used to implement the "magic" methods of
|
154
154
|
# `field_changed`, `field_change`, and `field_was`.
|
155
155
|
def method_missing(meth, *args)
|
156
156
|
meth_string = meth.to_s
|
157
|
-
|
158
|
-
if meth_string =~ /^(.+?)_changed\?$/
|
157
|
+
|
158
|
+
if meth_string =~ /^(.+?)_changed\?$/
|
159
159
|
changed?($1.to_sym)
|
160
160
|
elsif meth_string =~ /^(.+?)_change$/
|
161
161
|
changes[$1.to_sym]
|
@@ -2,10 +2,10 @@ module VirtualBox
|
|
2
2
|
class AbstractModel
|
3
3
|
# Provides simple relationship features to any class. These relationships
|
4
4
|
# can be anything, since this module makes no assumptions and doesn't
|
5
|
-
# differentiate between "has many" or "belongs to" or any of that.
|
5
|
+
# differentiate between "has many" or "belongs to" or any of that.
|
6
6
|
#
|
7
7
|
# The way it works is simple:
|
8
|
-
#
|
8
|
+
#
|
9
9
|
# 1. Relationships are defined with a relationship name and a
|
10
10
|
# class of the relationship objects.
|
11
11
|
# 2. When {#populate_relationships} is called, `populate_relationship` is
|
@@ -50,7 +50,7 @@ module VirtualBox
|
|
50
50
|
#
|
51
51
|
# If `Foo` has the `set_relationship` method, then it will be called by
|
52
52
|
# `foos=`. It is expected to return the new value for the relationship. To
|
53
|
-
# facilitate this need, the `set_relationship` method is given three
|
53
|
+
# facilitate this need, the `set_relationship` method is given three
|
54
54
|
# parameters: caller, old value, and new value. An example implementation,
|
55
55
|
# albeit a silly one, is below:
|
56
56
|
#
|
@@ -66,7 +66,7 @@ module VirtualBox
|
|
66
66
|
# instance.foos = "bar"
|
67
67
|
# instance.foos # => "Changed to: bar"
|
68
68
|
#
|
69
|
-
# If the relationship class _does not implement_ the `set_relationship`
|
69
|
+
# If the relationship class _does not implement_ the `set_relationship`
|
70
70
|
# method, then a {Exceptions::NonSettableRelationshipException} will be raised if
|
71
71
|
# a user attempts to set that relationship.
|
72
72
|
#
|
@@ -82,10 +82,10 @@ module VirtualBox
|
|
82
82
|
def self.included(base)
|
83
83
|
base.extend ClassMethods
|
84
84
|
end
|
85
|
-
|
85
|
+
|
86
86
|
module ClassMethods
|
87
87
|
# Define a relationship. The name and class must be specified. This
|
88
|
-
# class will be used to call the `populate_relationship,
|
88
|
+
# class will be used to call the `populate_relationship,
|
89
89
|
# `save_relationship`, etc. methods.
|
90
90
|
#
|
91
91
|
# @param [Symbol] name Relationship name. This will also be used for
|
@@ -95,23 +95,23 @@ module VirtualBox
|
|
95
95
|
# {AbstractModel#destroy} will propagate through to relationships.
|
96
96
|
def relationship(name, klass, options = {})
|
97
97
|
name = name.to_sym
|
98
|
-
|
98
|
+
|
99
99
|
relationships << [name, { :klass => klass }.merge(options)]
|
100
|
-
|
100
|
+
|
101
101
|
# Define the method to read the relationship
|
102
102
|
define_method(name) { relationship_data[name] }
|
103
|
-
|
103
|
+
|
104
104
|
# Define the method to set the relationship
|
105
105
|
define_method("#{name}=") { |*args| set_relationship(name, *args) }
|
106
106
|
end
|
107
|
-
|
107
|
+
|
108
108
|
# Returns a hash of all the relationships.
|
109
109
|
#
|
110
110
|
# @return [Hash]
|
111
111
|
def relationships_hash
|
112
112
|
Hash[*relationships.flatten]
|
113
113
|
end
|
114
|
-
|
114
|
+
|
115
115
|
# Returns an array of the relationships in order of being added.
|
116
116
|
#
|
117
117
|
# @return [Array]
|
@@ -125,19 +125,19 @@ module VirtualBox
|
|
125
125
|
def has_relationship?(name)
|
126
126
|
!!relationships.detect { |r| r[0] == name }
|
127
127
|
end
|
128
|
-
|
128
|
+
|
129
129
|
# Used to propagate relationships to subclasses. This method makes sure that
|
130
130
|
# subclasses of a class with {Relatable} included will inherit the
|
131
131
|
# relationships as well, which would be the expected behaviour.
|
132
132
|
def inherited(subclass)
|
133
133
|
super rescue NoMethodError
|
134
|
-
|
134
|
+
|
135
135
|
relationships.each do |name, options|
|
136
136
|
subclass.relationship(name, nil, options)
|
137
137
|
end
|
138
138
|
end
|
139
139
|
end
|
140
|
-
|
140
|
+
|
141
141
|
# Saves the model, calls save_relationship on all relations. It is up to
|
142
142
|
# the relation to determine whether anything changed, etc. Simply
|
143
143
|
# calls `save_relationship` on each relationshp class passing in the
|
@@ -154,7 +154,7 @@ module VirtualBox
|
|
154
154
|
options[:klass].save_relationship(self, relationship_data[name], *args)
|
155
155
|
end
|
156
156
|
end
|
157
|
-
|
157
|
+
|
158
158
|
# The equivalent to {Attributable#populate_attributes}, but with
|
159
159
|
# relationships.
|
160
160
|
def populate_relationships(data)
|
@@ -163,7 +163,7 @@ module VirtualBox
|
|
163
163
|
relationship_data[name] = options[:klass].populate_relationship(self, data)
|
164
164
|
end
|
165
165
|
end
|
166
|
-
|
166
|
+
|
167
167
|
# Calls `destroy_relationship` on each of the relationships. Any
|
168
168
|
# arbitrary args may be added and they will be forarded to the
|
169
169
|
# relationship's `destroy_relationship` method.
|
@@ -172,7 +172,7 @@ module VirtualBox
|
|
172
172
|
destroy_relationship(name, *args)
|
173
173
|
end
|
174
174
|
end
|
175
|
-
|
175
|
+
|
176
176
|
# Destroys only a single relationship. Any arbitrary args
|
177
177
|
# may be added to the end and they will be pushed through to
|
178
178
|
# the class's `destroy_relationship` method.
|
@@ -183,7 +183,7 @@ module VirtualBox
|
|
183
183
|
return unless options && options[:klass].respond_to?(:destroy_relationship)
|
184
184
|
options[:klass].destroy_relationship(self, relationship_data[name], *args)
|
185
185
|
end
|
186
|
-
|
186
|
+
|
187
187
|
# Hash to data associated with relationships. You should instead
|
188
188
|
# use the accessors created by Relatable.
|
189
189
|
#
|
@@ -191,18 +191,18 @@ module VirtualBox
|
|
191
191
|
def relationship_data
|
192
192
|
@relationship_data ||= {}
|
193
193
|
end
|
194
|
-
|
194
|
+
|
195
195
|
# Returns boolean denoting if a relationship exists.
|
196
196
|
#
|
197
197
|
# @return [Boolean]
|
198
198
|
def has_relationship?(key)
|
199
199
|
self.class.has_relationship?(key.to_sym)
|
200
200
|
end
|
201
|
-
|
201
|
+
|
202
202
|
# Sets a relationship to the given value. This is not guaranteed to
|
203
203
|
# do anything, since "set_relationship" will be called on the class
|
204
204
|
# that the relationship is associated with and its expected to return
|
205
|
-
# the resulting relationship to set.
|
205
|
+
# the resulting relationship to set.
|
206
206
|
#
|
207
207
|
# If the relationship class doesn't respond to the set_relationship
|
208
208
|
# method, then an exception {Exceptions::NonSettableRelationshipException} will
|
@@ -216,7 +216,7 @@ module VirtualBox
|
|
216
216
|
key = key.to_sym
|
217
217
|
relationship = self.class.relationships_hash[key]
|
218
218
|
return unless relationship
|
219
|
-
|
219
|
+
|
220
220
|
raise Exceptions::NonSettableRelationshipException.new unless relationship[:klass].respond_to?(:set_relationship)
|
221
221
|
relationship_data[key] = relationship[:klass].set_relationship(self, relationship_data[key], value)
|
222
222
|
end
|
@@ -6,12 +6,12 @@ module VirtualBox
|
|
6
6
|
def errors
|
7
7
|
@errors ||= {}
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def add_error(field, error)
|
11
11
|
errors[field] ||= []
|
12
12
|
errors[field].push(error)
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def clear_errors
|
16
16
|
@errors = {}
|
17
17
|
end
|
@@ -20,12 +20,12 @@ module VirtualBox
|
|
20
20
|
validate
|
21
21
|
errors.empty?
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
# Subclasses should override this method.
|
25
25
|
def validate
|
26
26
|
true
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def validates_presence_of(field)
|
30
30
|
if field.is_a?(Array)
|
31
31
|
field.map { |v| validates_presence_of(v) }.all? { |v| v == true }
|
@@ -13,7 +13,7 @@ module VirtualBox
|
|
13
13
|
# storage_controller.devices << ad
|
14
14
|
# ad.save
|
15
15
|
#
|
16
|
-
# The only quirk is that the attached device **must** be attached to a
|
16
|
+
# The only quirk is that the attached device **must** be attached to a
|
17
17
|
# storage controller. The above assumes that `storage_controller` exists,
|
18
18
|
# which adds the device.
|
19
19
|
#
|
@@ -25,7 +25,7 @@ module VirtualBox
|
|
25
25
|
# ad = VirtualBox::AttachedDevice.new
|
26
26
|
# ad.port = 0
|
27
27
|
# ad.image = VirtualBox::DVD.empty_drive
|
28
|
-
#
|
28
|
+
#
|
29
29
|
# # Now attaching to existing VM
|
30
30
|
# vm = VirtualBox::VM.find("FooVM")
|
31
31
|
# vm.storage_controllers[0].devices << ad
|
@@ -40,7 +40,7 @@ module VirtualBox
|
|
40
40
|
#
|
41
41
|
# Properties of the model are exposed using standard ruby instance
|
42
42
|
# methods which are generated on the fly. Because of this, they are not listed
|
43
|
-
# below as available instance methods.
|
43
|
+
# below as available instance methods.
|
44
44
|
#
|
45
45
|
# These attributes can be accessed and modified via standard ruby-style
|
46
46
|
# `instance.attribute` and `instance.attribute=` methods. The attributes are
|
@@ -73,7 +73,7 @@ module VirtualBox
|
|
73
73
|
attribute :uuid, :readonly => true
|
74
74
|
attribute :port
|
75
75
|
relationship :image, Image
|
76
|
-
|
76
|
+
|
77
77
|
class <<self
|
78
78
|
# Populate relationship with another model.
|
79
79
|
#
|
@@ -82,7 +82,7 @@ module VirtualBox
|
|
82
82
|
# @return [Array<AttachedDevice>]
|
83
83
|
def populate_relationship(caller, data)
|
84
84
|
relation = Proxies::Collection.new(caller)
|
85
|
-
|
85
|
+
|
86
86
|
counter = 0
|
87
87
|
loop do
|
88
88
|
break unless data["#{caller.name}-#{counter}-0".downcase.to_sym]
|
@@ -90,17 +90,17 @@ module VirtualBox
|
|
90
90
|
relation.push(nic)
|
91
91
|
counter += 1
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
relation
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
# Destroy attached devices associated with another model.
|
98
98
|
#
|
99
99
|
# **This method typically won't be used except internally.**
|
100
100
|
def destroy_relationship(caller, data, *args)
|
101
101
|
data.each { |v| v.destroy(*args) }
|
102
102
|
end
|
103
|
-
|
103
|
+
|
104
104
|
# Saves the relationship. This simply calls {#save} on every
|
105
105
|
# member of the relationship.
|
106
106
|
#
|
@@ -112,7 +112,7 @@ module VirtualBox
|
|
112
112
|
end
|
113
113
|
end
|
114
114
|
end
|
115
|
-
|
115
|
+
|
116
116
|
# @overload initialize(data={})
|
117
117
|
# Creates a new AttachedDevice which is a new record. This
|
118
118
|
# should be attached to a storage controller and saved.
|
@@ -127,7 +127,7 @@ module VirtualBox
|
|
127
127
|
# to extract the relationship data.
|
128
128
|
def initialize(*args)
|
129
129
|
super()
|
130
|
-
|
130
|
+
|
131
131
|
if args.length == 3
|
132
132
|
populate_from_data(*args)
|
133
133
|
elsif args.length == 1
|
@@ -139,29 +139,29 @@ module VirtualBox
|
|
139
139
|
raise NoMethodError.new
|
140
140
|
end
|
141
141
|
end
|
142
|
-
|
142
|
+
|
143
143
|
# Validates an attached device.
|
144
144
|
def validate
|
145
145
|
super
|
146
|
-
|
146
|
+
|
147
147
|
validates_presence_of :parent
|
148
148
|
validates_presence_of :image
|
149
149
|
validates_presence_of :port
|
150
150
|
end
|
151
|
-
|
151
|
+
|
152
152
|
# Saves or creates an attached device.
|
153
153
|
#
|
154
154
|
# @param [Boolean] raise_errors If true, {Exceptions::CommandFailedException}
|
155
155
|
# will be raised if the command failed.
|
156
156
|
# @return [Boolean] True if command was successful, false otherwise.
|
157
|
-
def save(raise_errors=false)
|
157
|
+
def save(raise_errors=false)
|
158
158
|
return true unless changed?
|
159
|
-
|
159
|
+
|
160
160
|
if !valid?
|
161
161
|
raise Exceptions::ValidationFailedException.new(errors) if raise_errors
|
162
162
|
return false
|
163
163
|
end
|
164
|
-
|
164
|
+
|
165
165
|
# If the port changed, we have to destroy the old one, then create
|
166
166
|
# a new one
|
167
167
|
destroy({:port => port_was}, raise_errors) if port_changed? && !port_was.nil?
|
@@ -169,13 +169,13 @@ module VirtualBox
|
|
169
169
|
Command.vboxmanage("storageattach #{Command.shell_escape(parent.parent.name)} --storagectl #{Command.shell_escape(parent.name)} --port #{port} --device 0 --type #{image.image_type} --medium #{medium}")
|
170
170
|
existing_record!
|
171
171
|
clear_dirty!
|
172
|
-
|
172
|
+
|
173
173
|
true
|
174
174
|
rescue Exceptions::CommandFailedException
|
175
175
|
raise if raise_errors
|
176
176
|
false
|
177
177
|
end
|
178
|
-
|
178
|
+
|
179
179
|
# Medium of the attached image. This attribute will be dependent
|
180
180
|
# on the attached image and will return one of the following values:
|
181
181
|
#
|
@@ -194,7 +194,7 @@ module VirtualBox
|
|
194
194
|
image.uuid
|
195
195
|
end
|
196
196
|
end
|
197
|
-
|
197
|
+
|
198
198
|
# Destroys the attached device. By default, this only removes any
|
199
199
|
# media inserted within the device, but does not destroy it. This
|
200
200
|
# option can be specified, however, through the `destroy_image`
|
@@ -209,21 +209,21 @@ module VirtualBox
|
|
209
209
|
# parent = storagecontroller
|
210
210
|
# parent.parent = vm
|
211
211
|
destroy_port = options[:port] || port
|
212
|
-
Command.vboxmanage("storageattach #{Command.shell_escape(parent.parent.name)} --storagectl #{Command.shell_escape(parent.name)} --port #{destroy_port} --device 0 --medium none")
|
212
|
+
Command.vboxmanage("storageattach #{Command.shell_escape(parent.parent.name)} --storagectl #{Command.shell_escape(parent.name)} --port #{destroy_port} --device 0 --medium none")
|
213
213
|
image.destroy(raise_errors) if options[:destroy_image] && image
|
214
214
|
rescue Exceptions::CommandFailedException
|
215
215
|
raise if raise_errors
|
216
216
|
false
|
217
217
|
end
|
218
|
-
|
218
|
+
|
219
219
|
# Relationship callback when added to a collection. This is automatically
|
220
220
|
# called by any relationship collection when this object is added.
|
221
221
|
def added_to_relationship(parent)
|
222
222
|
write_attribute(:parent, parent)
|
223
223
|
end
|
224
|
-
|
224
|
+
|
225
225
|
protected
|
226
|
-
|
226
|
+
|
227
227
|
# Populates the model based on data from a parsed vminfo. This
|
228
228
|
# method is used to create a model which already exists and is
|
229
229
|
# part of a relationship.
|
data/lib/virtualbox/command.rb
CHANGED
@@ -12,7 +12,7 @@ module VirtualBox
|
|
12
12
|
#
|
13
13
|
class Command
|
14
14
|
@@vboxmanage = "VBoxManage"
|
15
|
-
|
15
|
+
|
16
16
|
class <<self
|
17
17
|
# Returns true if the last run command was a success. Obviously this
|
18
18
|
# will introduce all sorts of thread-safe problems. Those will have to
|
@@ -20,36 +20,36 @@ module VirtualBox
|
|
20
20
|
def success?
|
21
21
|
$?.to_i == 0
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
# Sets the path to VBoxManage, which is required for this gem to
|
25
25
|
# work.
|
26
26
|
def vboxmanage=(path)
|
27
27
|
@@vboxmanage = path
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
# Runs a VBoxManage command and returns the output.
|
31
31
|
def vboxmanage(command)
|
32
|
-
result = execute("#{@@vboxmanage} #{command}")
|
32
|
+
result = execute("#{@@vboxmanage} -q #{command}")
|
33
33
|
raise Exceptions::CommandFailedException.new(result) if !Command.success?
|
34
34
|
result
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
# Runs a command and returns a boolean result showing
|
38
|
-
# if the command ran successfully or not based on the
|
38
|
+
# if the command ran successfully or not based on the
|
39
39
|
# exit code.
|
40
40
|
def test(command)
|
41
41
|
execute(command)
|
42
42
|
success?
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
# Runs a command and returns the STDOUT result. The reason this is
|
46
|
-
# a method at the moment is because in the future we may want to
|
46
|
+
# a method at the moment is because in the future we may want to
|
47
47
|
# change the way commands are run (replace the backticks), plus it
|
48
48
|
# makes testing easier.
|
49
49
|
def execute(command)
|
50
50
|
`#{command}`
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
# Shell escapes a string. This is almost a direct copy/paste from
|
54
54
|
# the ruby mailing list. I'm not sure how well it works but so far
|
55
55
|
# it hasn't failed!
|