rbus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/CHANGELOG.txt +9 -0
  2. data/COPYING.txt +341 -0
  3. data/HACKING.txt +104 -0
  4. data/Manifest.txt +58 -0
  5. data/README.txt +17 -0
  6. data/Rakefile +97 -0
  7. data/TUTORIAL.txt +303 -0
  8. data/bin/rbus-send +165 -0
  9. data/examples/async_rb_and_notify.rb +56 -0
  10. data/examples/async_rb_loop.rb +52 -0
  11. data/examples/glib_async_rb_loop.rb +57 -0
  12. data/examples/glib_rhythmbox.rb +54 -0
  13. data/examples/hal_device_info.rb +54 -0
  14. data/examples/listnames.rb +37 -0
  15. data/examples/network_manager_get_properties.rb +50 -0
  16. data/examples/notification_bubble.rb +46 -0
  17. data/examples/rhythmbox_print_playing_uri.rb +41 -0
  18. data/examples/rhythmbox_signal_print_playing.rb +54 -0
  19. data/examples/rhythmbox_start_service.rb +39 -0
  20. data/examples/rhythmbox_toggle_playing.rb +46 -0
  21. data/lib/rbus.rb +25 -0
  22. data/lib/rbus/auth/auth.rb +53 -0
  23. data/lib/rbus/auth/dbus_cookie_sha1.rb +66 -0
  24. data/lib/rbus/auth/dummy.rb +37 -0
  25. data/lib/rbus/auth/external.rb +34 -0
  26. data/lib/rbus/auth/state_machine.rb +168 -0
  27. data/lib/rbus/bus/bus.rb +101 -0
  28. data/lib/rbus/bus/proxy.rb +137 -0
  29. data/lib/rbus/bus/transport.rb +125 -0
  30. data/lib/rbus/default.rb +29 -0
  31. data/lib/rbus/etc/exception.rb +44 -0
  32. data/lib/rbus/etc/log.rb +100 -0
  33. data/lib/rbus/etc/types.rb +56 -0
  34. data/lib/rbus/etc/version.rb +34 -0
  35. data/lib/rbus/glib.rb +29 -0
  36. data/lib/rbus/mainloop/glib.rb +77 -0
  37. data/lib/rbus/mainloop/mainloop.rb +84 -0
  38. data/lib/rbus/mainloop/observers.rb +149 -0
  39. data/lib/rbus/mainloop/thread.rb +78 -0
  40. data/lib/rbus/message/constants.rb +51 -0
  41. data/lib/rbus/message/marshal.rb +139 -0
  42. data/lib/rbus/message/message.rb +110 -0
  43. data/lib/rbus/message/reader.rb +108 -0
  44. data/lib/rbus/message/serial_generator.rb +48 -0
  45. data/lib/rbus/message/unmarshal.rb +171 -0
  46. data/lib/rbus/message/writer.rb +69 -0
  47. data/setup.rb +1608 -0
  48. data/spec/auth_spec.rb +123 -0
  49. data/spec/bus_spec.rb +178 -0
  50. data/spec/helpers/bus_mocks.rb +64 -0
  51. data/spec/helpers/spec_helper.rb +24 -0
  52. data/spec/mainloop_spec.rb +74 -0
  53. data/spec/marshal_spec.rb +91 -0
  54. data/spec/message_spec.rb +61 -0
  55. data/spec/observers_spec.rb +28 -0
  56. data/spec/proxy_spec.rb +120 -0
  57. data/spec/transport_spec.rb +187 -0
  58. data/spec/unmarshal_spec.rb +186 -0
  59. metadata +118 -0
@@ -0,0 +1,58 @@
1
+ CHANGELOG.txt
2
+ COPYING.txt
3
+ HACKING.txt
4
+ Manifest.txt
5
+ README.txt
6
+ Rakefile
7
+ TUTORIAL.txt
8
+ bin/rbus-send
9
+ examples/async_rb_and_notify.rb
10
+ examples/async_rb_loop.rb
11
+ examples/glib_async_rb_loop.rb
12
+ examples/glib_rhythmbox.rb
13
+ examples/hal_device_info.rb
14
+ examples/listnames.rb
15
+ examples/network_manager_get_properties.rb
16
+ examples/notification_bubble.rb
17
+ examples/rhythmbox_print_playing_uri.rb
18
+ examples/rhythmbox_signal_print_playing.rb
19
+ examples/rhythmbox_start_service.rb
20
+ examples/rhythmbox_toggle_playing.rb
21
+ lib/rbus.rb
22
+ lib/rbus/auth/auth.rb
23
+ lib/rbus/auth/dbus_cookie_sha1.rb
24
+ lib/rbus/auth/dummy.rb
25
+ lib/rbus/auth/external.rb
26
+ lib/rbus/auth/state_machine.rb
27
+ lib/rbus/bus/bus.rb
28
+ lib/rbus/bus/proxy.rb
29
+ lib/rbus/bus/transport.rb
30
+ lib/rbus/default.rb
31
+ lib/rbus/etc/exception.rb
32
+ lib/rbus/etc/log.rb
33
+ lib/rbus/etc/types.rb
34
+ lib/rbus/etc/version.rb
35
+ lib/rbus/glib.rb
36
+ lib/rbus/mainloop/glib.rb
37
+ lib/rbus/mainloop/mainloop.rb
38
+ lib/rbus/mainloop/observers.rb
39
+ lib/rbus/mainloop/thread.rb
40
+ lib/rbus/message/constants.rb
41
+ lib/rbus/message/marshal.rb
42
+ lib/rbus/message/message.rb
43
+ lib/rbus/message/reader.rb
44
+ lib/rbus/message/serial_generator.rb
45
+ lib/rbus/message/unmarshal.rb
46
+ lib/rbus/message/writer.rb
47
+ setup.rb
48
+ spec/auth_spec.rb
49
+ spec/bus_spec.rb
50
+ spec/helpers/bus_mocks.rb
51
+ spec/helpers/spec_helper.rb
52
+ spec/mainloop_spec.rb
53
+ spec/marshal_spec.rb
54
+ spec/message_spec.rb
55
+ spec/observers_spec.rb
56
+ spec/proxy_spec.rb
57
+ spec/transport_spec.rb
58
+ spec/unmarshal_spec.rb
@@ -0,0 +1,17 @@
1
+ = README for R-Bus
2
+
3
+ R-Bus is a native implementation of the D-Bus protocol, with these goals in mind:
4
+
5
+ * Ruby + standard library is the only dependencies
6
+ * A Rubyish API, approach and way of doing things
7
+ * Complete client functionality
8
+ * A comprehensive test suite.
9
+
10
+ It's possible that server functionality will be added later, but for now the
11
+ goal is to talk to existing D-Bus servers.
12
+
13
+ == See also
14
+
15
+ * {TUTORIAL}[link:files/TUTORIAL_txt.html].
16
+ * {HACKING}[link:files/HACKING_txt.html] for some info on how to help the
17
+ development.
@@ -0,0 +1,97 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ include FileUtils
12
+ require File.join(File.dirname(__FILE__), 'lib', 'rbus', 'etc', 'version')
13
+
14
+ AUTHOR = "Kristoffer Lundén" # can also be an array of Authors
15
+ EMAIL = "kristoffer.lunden@gmail.com"
16
+ DESCRIPTION = "A native implementation of the D-Bus protocol."
17
+ GEM_NAME = "rbus" # what ppl will type to install your gem
18
+ RUBYFORGE_PROJECT = "rbus" # The unix name for your project
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+
21
+
22
+ NAME = "rbus"
23
+ REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
24
+ VERS = ENV['VERSION'] || (RBus::VERSION::STRING + (REV ? ".#{REV}" : ""))
25
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config']
26
+ RDOC_OPTS = ['--quiet', '--title', "R-Bus documentation",
27
+ "--opname", "index.html",
28
+ "--line-numbers",
29
+ "--main", "README.txt",
30
+ "--charset", "utf-8",
31
+ "--inline-source"]
32
+
33
+ class Hoe
34
+ def extra_deps
35
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
36
+ end
37
+ end
38
+
39
+ # Generate all the Rake tasks
40
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
41
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
42
+ p.author = AUTHOR
43
+ p.description = DESCRIPTION
44
+ p.email = EMAIL
45
+ p.summary = DESCRIPTION
46
+ p.url = HOMEPATH
47
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
48
+ p.test_globs = ["test/**/*_test.rb"]
49
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
50
+ p.spec_extras = {
51
+ :rdoc_options => RDOC_OPTS,
52
+ :extra_rdoc_files => ['README.txt','TUTORIAL.txt','HACKING.txt','COPYING.txt','CHANGELOG.txt','bin/rbus-send']
53
+ }
54
+ # == Optional
55
+ #p.changes - A description of the release's latest changes.
56
+ #p.extra_deps - An array of rubygem dependencies.
57
+ #p.spec_extras - A hash of extra values to set in the gemspec.
58
+ end
59
+
60
+ desc "Find all TODO's"
61
+ task :todo do
62
+ FileList['**/*.rb'].egrep(/TODO/)
63
+ end
64
+
65
+ desc "Insert GPL into all source files"
66
+ task :GPL do
67
+ gpl = File.readlines('GPL_header.txt')
68
+ FileList['**/*.rb'].each do |filename|
69
+ File.open(filename, 'r+') do |file|
70
+ lines = file.readlines
71
+ # Skip shebang line
72
+ i = (lines[0].index('#!') == 0) ? 1 : 0
73
+ # Already have header?
74
+ next if lines[i].index('#--') == 0
75
+
76
+ puts "Liberating #{filename}"
77
+
78
+ file.pos = 0
79
+ file.print lines.insert(i, gpl).flatten
80
+ file.truncate(file.pos)
81
+ end
82
+ end
83
+ end
84
+
85
+ require 'spec/rake/spectask'
86
+
87
+ desc "Run all specs"
88
+ Spec::Rake::SpecTask.new('spec') do |t|
89
+ t.spec_files = FileList['spec/**/*.rb']
90
+ end
91
+
92
+ desc "Run all specs with RCov"
93
+ Spec::Rake::SpecTask.new('rcov') do |t|
94
+ t.spec_files = FileList['spec/**/*.rb']
95
+ t.rcov_dir = 'spec/coverage'
96
+ t.rcov = true
97
+ end
@@ -0,0 +1,303 @@
1
+ = TUTORIAL
2
+
3
+ Kristoffer Lundén <kristoffer.lunden@gmail.com>
4
+
5
+ <em>This is a work in progress. Please also see the examples for more info
6
+ on how to use this library.</em>
7
+
8
+ == Definitions
9
+
10
+ [D-Bus] is the protocol, see http://dbus.freedesktop.org
11
+ [R-Bus] is a native implementation of the D-Bus protocol, written in Ruby.
12
+
13
+ == Connecting to a Bus
14
+
15
+ To communicate via D-Bus, you need to connect to a bus daemon, that will forward
16
+ the messages between connected application. You do this in R-Bus by creating an
17
+ instance of RBus::Bus, which represents this connection.
18
+
19
+ On most systems, you will have a Session Bus, that is per login, and a System
20
+ Bus, that is global for the machine. For special needs and testing, you may also
21
+ want to connect to a non-standard bus.
22
+
23
+ The Session and System Bus objects are Singleton objects, meaning that you
24
+ always get the same object and connection even if you get it several times.
25
+ Apart from this, you can communicate with as many buses as you wish in the same
26
+ application.
27
+
28
+ There are three convenience methods
29
+ available for making these connections:
30
+
31
+ === Connecting to the Session Bus
32
+
33
+ require 'rbus'
34
+ bus = RBus.session_bus
35
+
36
+ This connects to the Session Bus, or returns the already active connection.
37
+
38
+ === Connecting to the System Bus
39
+
40
+ require 'rbus'
41
+ bus = RBus.system_bus
42
+
43
+ This connects to the System Bus, or returns the already active connection.
44
+
45
+ === Connecting to a specified address
46
+
47
+ require 'rbus'
48
+ tcp_bus = RBus.get_bus('tcp:host=192.168.0.1,port=6666')
49
+ unix_bus = RBus.get_bus('unix:path=/tmp/dbus-test')
50
+ abstract_bus = RBus.get_bus('unix:abstract=/tmp/dbus-lafi7lJhll,guid=a190e4459a37da0c62586ac4de5fa400')
51
+
52
+ This connects to the given address, which is given in the same form as
53
+ reported by the Bus Daemon.
54
+
55
+ == Remote objects
56
+
57
+ D-Bus applications can export objects that other applications can call methods
58
+ on, and receive signals from. A remote object has a <em>well-known name</em>, which
59
+ represents the exporting application, and a <em>object path</em>, which represents the
60
+ object itself.
61
+
62
+ Examples of well-known names are <tt>org.freedesktop.NetworkManager</tt> and
63
+ <tt>org.gnome.Rhythmbox</tt>. Examples of object paths are
64
+ <tt>/org/freedesktop/NetworkManager/Devices/eth0</tt> and
65
+ <tt>/org/gnome/Rhythmbox/Player</tt>.
66
+
67
+ Later in this document we will see how to export
68
+ our own objects, but first we will look at interacting with other objects.
69
+
70
+ == Proxy objects
71
+
72
+ To communicate with the remote object, we need a local representation of it, a
73
+ Proxy object, that we can call methods on and receive signals from. We can
74
+ obtain a Proxy from the Bus via the method +get_object+, which takes a
75
+ well-known name and an object path as describe above.
76
+
77
+ # get the Player object from Rhythmbox:
78
+ rb_player = session_bus.get_object('org.gnome.Rhythmbox', '/org/gnome/Rhythmbox/Player')
79
+
80
+ == Method calls
81
+
82
+ To make method calls on the remote object, we simply call the exact same
83
+ method on the Proxy object. Any method not defined in the Proxy class will be
84
+ forwarded to the other side of the wire. To avoid any collisions with built-in
85
+ methods, the Proxy class is almost completely emptied. Also, to avoid almost any
86
+ possible clash, the few methods that operate on Proxy objects end in '!', such
87
+ as <tt>method!</tt> and <tt>interface!</tt>. It may not be not 100% idiomatic Ruby, so better
88
+ suggestions are welcome. :)
89
+
90
+ Some examples of method calls:
91
+
92
+ # get the uri of the playing song
93
+ uri = rb_player.getPlayingUri()
94
+
95
+ # get information on a HAL device
96
+ info = device.GetProperty('info.product')
97
+
98
+ # pop up a notification bubble
99
+ notify.Notify('R-Bus',0,'info','R-Bus Notification',
100
+ 'A test example to see that everything works.',[],{},-1)
101
+
102
+ == Signatures, introspection and data types
103
+
104
+ D-Bus
105
+ {defines a number of standard data types}[http://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-signatures]
106
+ that can be used to send and receive data between applications.
107
+ R-Bus is able to use Rubys standard types for the most part, as long as there is
108
+ a signature available for the conversion. When a new Proxy object is created,
109
+ R-Bus will ask the remote application for a list of methods and signatures via
110
+ the standard methos +Introspect+. If the application provides such a list,
111
+ everything should just work. :)
112
+
113
+ If the introspection fails, the Proxy object may not know how to call the
114
+ method. In this case you can add the method manually with +method!+:
115
+
116
+ obj.method!(:MethodName, 'as')
117
+
118
+ This would define a method +MethodName+ that takes an Array of Strings.
119
+
120
+ == Interfaces
121
+
122
+ D-Bus uses interfaces to provide namespaces for methods. An interface groups
123
+ methods and signals under a common name, which is a dot-separated name starting
124
+ with a reversed domain name. They are in many cases
125
+ optional, only if an object have multiple methods with the same name, an
126
+ interface is actually needed.
127
+
128
+ You can use the <tt>interface!</tt> method to set an interface on an object.
129
+
130
+ Some examples:
131
+
132
+ rb_player.interface!('org.gnome.Rhythmbox.Player')
133
+ puts rb_player.getPlayingUri()
134
+
135
+ eth0.interface!('org.freedesktop.NetworkManager.Devices')
136
+ eth0.getProperties()
137
+
138
+ <tt>interface!</tt> also returns the object itself, so commands can be chained:
139
+
140
+ rb_player.interface!('org.gnome.Rhythmbox.Player').getPlayingUri()
141
+
142
+ If you want to set an interface temporarily, you can use a block, which will
143
+ get a copy of the object with a temporary interface:
144
+
145
+ rb_player.interface!('org.gnome.Rhythmbox.SomethingElse')
146
+ rb_player.interface!('org.gnome.Rhythmbox.Player') do |p|
147
+ p.getPlayingUri()
148
+ end
149
+ # ...here the interface is +SomethingElse+ again.
150
+
151
+ == Asynchronous method calls
152
+
153
+ To make an asynchronous call, simply attach a block to the method call instead
154
+ of waiting for a reply.
155
+
156
+ # from async_rb_loop.rb / glib_async_rb_loop.rb
157
+ 10.times do |i|
158
+ rb.getPlayingUri() do |uri|
159
+ sleep(rand(2))
160
+ puts "#{i}: #{uri}"
161
+ end
162
+ end
163
+
164
+ With the default version of R-Bus, that uses threads, the above code will return
165
+ a list of uris in random order.
166
+
167
+ With the GLib version, the mainloop needs to be started (see
168
+ Event loop) and the calls will be returned in the order they were queued.
169
+ You can use the threaded version with GLib/Gtk+ applications if you
170
+ need the extra flexibility though, however, mostly you don't. :)
171
+
172
+ == Receiving signals
173
+
174
+ Remote objects can notify listeners of events via signals. To listen to a
175
+ signal on the remote object, call the <tt>connect!</tt> method with a block to
176
+ process the signal:
177
+
178
+ # from rhythmbox_signal_print_playing.rb:
179
+ # get notifications from Rhythmbox when the song changes
180
+ rb_player.connect!(:playingUriChanged) { |uri|
181
+ properties = rb_shell.getSongProperties(uri)
182
+ duration = Time.at(properties['duration'].to_i).strftime("%M:%S")
183
+ artist = properties['artist']
184
+ title = properties['title']
185
+ puts "Now playing: #{artist} - #{title} (#{duration})"
186
+ printf "File-size: %.2f MB\n", properties['file-size'].to_f/1024/1024
187
+ }
188
+
189
+ RBus.mainloop
190
+
191
+ The block will receive whatever data the signal sends.
192
+
193
+ <tt>connect!</tt> takes the name of the signal, and an optional Hash of
194
+ extra match rules. See
195
+ {Match rules}[http://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules]
196
+ in the D-Bus specification for a list of rules.
197
+
198
+ obj.connect!(:SignalName, {:arg0 => 'Foo', :arg1 => 'Bar'}) do |*args|
199
+ # ...
200
+ end
201
+
202
+ To receive signals, you need a mainloop. See the next section for a few more
203
+ details.
204
+
205
+ == Event loop
206
+
207
+ To be able to receive signals and answers to asynchronous calls, an event loop
208
+ needs to be running and listening for them. In the dfault version, using
209
+ <tt>require 'rbus'</tt>, such a loop is always started automatically and in
210
+ most cases nothing more neds to be done. However, a word of caution: as it uses
211
+ threads you need to be wary of the same kind of concurrency issues as with
212
+ any Ruby threads.
213
+
214
+ With the GLib loop, used with <tt>require 'rbus/glib'</tt>, you need to start
215
+ the loop. You can either use <tt>RBus.mainloop</tt> or <tt>Gtk.main</tt>. The
216
+ former simply calls the latter, but you can use it for compatibility with
217
+ other R-Bus mainloops.
218
+
219
+ You can use <tt>RBus.mainloop</tt> in the threaded version as well to just sit
220
+ idle and wait for signals and method replies.
221
+
222
+ == Exporting objects
223
+
224
+ <em>This section has yet to be written, and the functionality is
225
+ currently missing.</em>
226
+
227
+ == Starting Services
228
+
229
+ require 'rbus'
230
+ session_bus = RBus.session_bus()
231
+
232
+ # Start Rhythmbox
233
+ session_bus.StartServiceByName('org.gnome.Rhythmbox', 0)
234
+
235
+ == Extra utilities
236
+
237
+ === rbus-send
238
+
239
+ R-Bus comes with its own variant of <em>dbus-send</em>, called
240
+ <em>{rbus-send}[link:files/bin/rbus-send.html]</em>, which should be pretty much compatible, except for these
241
+ known issues:
242
+
243
+ * There is no <tt>--reply-timeout</tt>. Trivial to fix in seconds, though
244
+ milli-seconds may not be.
245
+ * Currently dumps the any server answer in YAML[http://www.yaml.org/] instead of
246
+ the dbus-send format.
247
+
248
+ The invokation looks like this:
249
+
250
+ rbus-send <destination object path> <message name> [options] [arguments ...]
251
+ --dest=NAME Specify the name of the connection to receive the message.
252
+ --print-reply Block for a reply to the message sent, and print any reply received.
253
+ --system Send to the system message bus.
254
+ --session Send to the session message bus. (This is the default.)
255
+ --type=TYPE Specify "method_call" or "signal" (defaults to "signal").
256
+
257
+ The object path and the name of the message to send must always be specified.
258
+ Any following non-options are message arguments. These are given as
259
+ type-specified values and may include containers (arrays, dicts, and variants)
260
+ as follows:
261
+
262
+ <arguments> ::= <item> | <container> [ <item> | <container>...]
263
+ <item> ::= <type>:<value>
264
+ <container> ::= <array> | <dict> | <variant>
265
+ <array> ::= array:<type>:<value>[,<value>...]
266
+ <dict> ::= dict:<type>:<type>:<key>,<value>[,<key>,<value>...]
267
+ <variant> ::= variant:<type>:<value>
268
+ <type> ::= string | int16 | uint16 | int32 | uint32 | int64 | uint64 | double | byte | boolean | objpath
269
+
270
+ Some sample queries:
271
+
272
+ # List all HAL properties:
273
+ $ rbus-send --system --print-reply --dest=org.freedesktop.Hal \
274
+ /org/freedesktop/Hal/devices/computer \
275
+ org.freedesktop.Hal.Device.GetAllProperties
276
+
277
+ # Print URI of current song in Rhythmbox:
278
+ $ rbus-send --dest='org.gnome.Rhythmbox' --print-reply \
279
+ /org/gnome/Rhythmbox/Player org.gnome.Rhythmbox.Player.getPlayingUri
280
+
281
+ # Toggle play/pause in Rhytmbox:
282
+ $ rbus-send --dest='org.gnome.Rhythmbox' /org/gnome/Rhythmbox/Player \
283
+ org.gnome.Rhythmbox.Player.playPause boolean:true
284
+
285
+ # Find services possible to start on the Bus:
286
+ $ rbus-send --print-reply --dest=org.freedesktop.DBus \
287
+ /org/freedesktop/DBus org.freedesktop.DBus.ListActivatableNames
288
+
289
+ # Start such a service (Rhythmbox in this case):
290
+ $ rbus-send --type=method_call --dest=org.freedesktop.DBus \
291
+ /org/freedesktop/DBus org.freedesktop.DBus.StartServiceByName \
292
+ string:'org.gnome.Rhythmbox' uint32:0
293
+
294
+ # Pop up a notification bubble:
295
+ $ rbus-send --dest=org.freedesktop.Notifications \
296
+ /org/freedesktop/Notifications org.freedesktop.Notifications.Notify \
297
+ string:'R-Bus' uint32:0 string:'info' string:'R-Bus Notification' \
298
+ string:'A test example to see that everything works.' \
299
+ array:string: dict:string:variant: int32:-1
300
+
301
+ If you have not installed the library, you can use
302
+ <tt>ruby -Ilib bin/rbus-send [invocation]</tt> from the root of the project
303
+ to try it out.