yong-ruby-dbus 0.2.1

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/lib/dbus/bus.rb ADDED
@@ -0,0 +1,690 @@
1
+ # dbus.rb - Module containing the low-level D-Bus implementation
2
+ #
3
+ # This file is part of the ruby-dbus project
4
+ # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
5
+ #
6
+ # This library is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU Lesser General Public
8
+ # License, version 2.1 as published by the Free Software Foundation.
9
+ # See the file "COPYING" for the exact licensing terms.
10
+
11
+ require 'socket'
12
+ require 'thread'
13
+ require 'singleton'
14
+
15
+ # = D-Bus main module
16
+ #
17
+ # Module containing all the D-Bus modules and classes.
18
+ module DBus
19
+ # This represents a remote service. It should not be instancied directly
20
+ # Use Bus::service()
21
+ class Service
22
+ # The service name.
23
+ attr_reader :name
24
+ # The bus the service is running on.
25
+ attr_reader :bus
26
+ # The service root (FIXME).
27
+ attr_reader :root
28
+
29
+ # Create a new service with a given _name_ on a given _bus_.
30
+ def initialize(name, bus)
31
+ @name, @bus = name, bus
32
+ @root = Node.new("/")
33
+ end
34
+
35
+ # Determine whether the serice name already exists.
36
+ def exists?
37
+ bus.proxy.ListName.member?(@name)
38
+ end
39
+
40
+ # Perform an introspection on all the objects on the service
41
+ # (starting recursively from the root).
42
+ def introspect
43
+ if block_given?
44
+ raise NotImplementedError
45
+ else
46
+ rec_introspect(@root, "/")
47
+ end
48
+ self
49
+ end
50
+
51
+ # Retrieves an object at the given _path_.
52
+ def object(path)
53
+ node = get_node(path, true)
54
+ if node.object.nil?
55
+ node.object = ProxyObject.new(@bus, @name, path)
56
+ end
57
+ node.object
58
+ end
59
+
60
+ # Export an object _obj_ (an DBus::Object subclass instance).
61
+ def export(obj)
62
+ obj.service = self
63
+ get_node(obj.path, true).object = obj
64
+ end
65
+
66
+ # Get the object node corresponding to the given _path_. if _create_ is
67
+ # true, the the nodes in the path are created if they do not already exist.
68
+ def get_node(path, create = false)
69
+ n = @root
70
+ path.sub(/^\//, "").split("/").each do |elem|
71
+ if not n[elem]
72
+ if not create
73
+ return nil
74
+ else
75
+ n[elem] = Node.new(elem)
76
+ end
77
+ end
78
+ n = n[elem]
79
+ end
80
+ if n.nil?
81
+ puts "Warning, unknown object #{path}" if $DEBUG
82
+ end
83
+ n
84
+ end
85
+
86
+ #########
87
+ private
88
+ #########
89
+
90
+ # Perform a recursive retrospection on the given current _node_
91
+ # on the given _path_.
92
+ def rec_introspect(node, path)
93
+ xml = bus.introspect_data(@name, path)
94
+ intfs, subnodes = IntrospectXMLParser.new(xml).parse
95
+ subnodes.each do |nodename|
96
+ subnode = node[nodename] = Node.new(nodename)
97
+ if path == "/"
98
+ subpath = "/" + nodename
99
+ else
100
+ subpath = path + "/" + nodename
101
+ end
102
+ rec_introspect(subnode, subpath)
103
+ end
104
+ if intfs.size > 0
105
+ node.object = ProxyObjectFactory.new(xml, @bus, @name, path).build
106
+ end
107
+ end
108
+ end
109
+
110
+ # = Object path node class
111
+ #
112
+ # Class representing a node on an object path.
113
+ class Node < Hash
114
+ # The D-Bus object contained by the node.
115
+ attr_accessor :object
116
+ # The name of the node.
117
+ attr_reader :name
118
+
119
+ # Create a new node with a given _name_.
120
+ def initialize(name)
121
+ @name = name
122
+ @object = nil
123
+ end
124
+
125
+ # Return an XML string representation of the node.
126
+ def to_xml
127
+ xml = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
128
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
129
+ <node>
130
+ '
131
+ self.each_pair do |k, v|
132
+ xml += "<node name=\"#{k}\" />"
133
+ end
134
+ if @object
135
+ @object.intfs.each_pair do |k, v|
136
+ xml += %{<interface name="#{v.name}">\n}
137
+ v.methods.each_value { |m| xml += m.to_xml }
138
+ v.signals.each_value { |m| xml += m.to_xml }
139
+ xml +="</interface>\n"
140
+ end
141
+ end
142
+ xml += '</node>'
143
+ xml
144
+ end
145
+
146
+ # Return inspect information of the node.
147
+ def inspect
148
+ # Need something here
149
+ "<DBus::Node #{sub_inspect}>"
150
+ end
151
+
152
+ # Return instance inspect information, used by Node#inspect.
153
+ def sub_inspect
154
+ s = ""
155
+ if not @object.nil?
156
+ s += "%x " % @object.object_id
157
+ end
158
+ s + "{" + keys.collect { |k| "#{k} => #{self[k].sub_inspect}" }.join(",") + "}"
159
+ end
160
+ end # class Inspect
161
+
162
+ # FIXME: rename Connection to Bus?
163
+
164
+ # D-Bus main connection class
165
+ #
166
+ # Main class that maintains a connection to a bus and can handle incoming
167
+ # and outgoing messages.
168
+ class Connection
169
+ # The unique name (by specification) of the message.
170
+ attr_reader :unique_name
171
+ # The socket that is used to connect with the bus.
172
+ attr_reader :socket
173
+
174
+ # Create a new connection to the bus for a given connect _path_. _path_
175
+ # format is described in the D-Bus specification:
176
+ # http://dbus.freedesktop.org/doc/dbus-specification.html#addresses
177
+ # and is something like:
178
+ # "transport1:key1=value1,key2=value2;transport2:key1=value1,key2=value2"
179
+ # e.g. "unix:path=/tmp/dbus-test"
180
+ #
181
+ # Current implementation of ruby-dbus supports only a single server
182
+ # address and only "unix:path=...,guid=..." and
183
+ # "unix:abstract=...,guid=..." forms
184
+ def initialize(path)
185
+ @path = path
186
+ @unique_name = nil
187
+ @buffer = ""
188
+ @method_call_replies = Hash.new
189
+ @method_call_msgs = Hash.new
190
+ @signal_matchrules = Array.new
191
+ @proxy = nil
192
+ # FIXME: can be TCP or any stream
193
+ @socket = Socket.new(Socket::Constants::PF_UNIX,
194
+ Socket::Constants::SOCK_STREAM, 0)
195
+ @object_root = Node.new("/")
196
+ end
197
+
198
+ # Connect to the bus and initialize the connection.
199
+ def connect
200
+ parse_session_string
201
+ if @transport == "unix" and @type == "abstract"
202
+ if HOST_END == LIL_END
203
+ sockaddr = "\1\0\0#{@unix_abstract}"
204
+ else
205
+ sockaddr = "\0\1\0#{@unix_abstract}"
206
+ end
207
+ elsif @transport == "unix" and @type == "path"
208
+ sockaddr = Socket.pack_sockaddr_un(@unix)
209
+ end
210
+ @socket.connect(sockaddr)
211
+ init_connection
212
+ end
213
+
214
+ # Send the buffer _buf_ to the bus using Connection#writel.
215
+ def send(buf)
216
+ @socket.write(buf)
217
+ end
218
+
219
+ # Tell a bus to register itself on the glib main loop
220
+ def glibize
221
+ require 'glib2'
222
+ # Circumvent a ruby-glib bug
223
+ @channels ||= Array.new
224
+
225
+ gio = GLib::IOChannel.new(@socket.fileno)
226
+ @channels << gio
227
+ gio.add_watch(GLib::IOChannel::IN) do |c, ch|
228
+ update_buffer
229
+ messages.each do |msg|
230
+ process(msg)
231
+ end
232
+ true
233
+ end
234
+ end
235
+
236
+ # FIXME: describe the following names, flags and constants.
237
+ # See DBus spec for definition
238
+ NAME_FLAG_ALLOW_REPLACEMENT = 0x1
239
+ NAME_FLAG_REPLACE_EXISTING = 0x2
240
+ NAME_FLAG_DO_NOT_QUEUE = 0x4
241
+
242
+ REQUEST_NAME_REPLY_PRIMARY_OWNER = 0x1
243
+ REQUEST_NAME_REPLY_IN_QUEUE = 0x2
244
+ REQUEST_NAME_REPLY_EXISTS = 0x3
245
+ REQUEST_NAME_REPLY_ALREADY_OWNER = 0x4
246
+
247
+ DBUSXMLINTRO = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
248
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
249
+ <node>
250
+ <interface name="org.freedesktop.DBus.Introspectable">
251
+ <method name="Introspect">
252
+ <arg name="data" direction="out" type="s"/>
253
+ </method>
254
+ </interface>
255
+ <interface name="org.freedesktop.DBus">
256
+ <method name="RequestName">
257
+ <arg direction="in" type="s"/>
258
+ <arg direction="in" type="u"/>
259
+ <arg direction="out" type="u"/>
260
+ </method>
261
+ <method name="ReleaseName">
262
+ <arg direction="in" type="s"/>
263
+ <arg direction="out" type="u"/>
264
+ </method>
265
+ <method name="StartServiceByName">
266
+ <arg direction="in" type="s"/>
267
+ <arg direction="in" type="u"/>
268
+ <arg direction="out" type="u"/>
269
+ </method>
270
+ <method name="Hello">
271
+ <arg direction="out" type="s"/>
272
+ </method>
273
+ <method name="NameHasOwner">
274
+ <arg direction="in" type="s"/>
275
+ <arg direction="out" type="b"/>
276
+ </method>
277
+ <method name="ListNames">
278
+ <arg direction="out" type="as"/>
279
+ </method>
280
+ <method name="ListActivatableNames">
281
+ <arg direction="out" type="as"/>
282
+ </method>
283
+ <method name="AddMatch">
284
+ <arg direction="in" type="s"/>
285
+ </method>
286
+ <method name="RemoveMatch">
287
+ <arg direction="in" type="s"/>
288
+ </method>
289
+ <method name="GetNameOwner">
290
+ <arg direction="in" type="s"/>
291
+ <arg direction="out" type="s"/>
292
+ </method>
293
+ <method name="ListQueuedOwners">
294
+ <arg direction="in" type="s"/>
295
+ <arg direction="out" type="as"/>
296
+ </method>
297
+ <method name="GetConnectionUnixUser">
298
+ <arg direction="in" type="s"/>
299
+ <arg direction="out" type="u"/>
300
+ </method>
301
+ <method name="GetConnectionUnixProcessID">
302
+ <arg direction="in" type="s"/>
303
+ <arg direction="out" type="u"/>
304
+ </method>
305
+ <method name="GetConnectionSELinuxSecurityContext">
306
+ <arg direction="in" type="s"/>
307
+ <arg direction="out" type="ay"/>
308
+ </method>
309
+ <method name="ReloadConfig">
310
+ </method>
311
+ <signal name="NameOwnerChanged">
312
+ <arg type="s"/>
313
+ <arg type="s"/>
314
+ <arg type="s"/>
315
+ </signal>
316
+ <signal name="NameLost">
317
+ <arg type="s"/>
318
+ </signal>
319
+ <signal name="NameAcquired">
320
+ <arg type="s"/>
321
+ </signal>
322
+ </interface>
323
+ </node>
324
+ '
325
+
326
+ def introspect_data(dest, path)
327
+ m = DBus::Message.new(DBus::Message::METHOD_CALL)
328
+ m.path = path
329
+ m.interface = "org.freedesktop.DBus.Introspectable"
330
+ m.destination = dest
331
+ m.member = "Introspect"
332
+ m.sender = unique_name
333
+ if not block_given?
334
+ # introspect in synchronous !
335
+ send_sync(m) do |rmsg|
336
+ if rmsg.is_a?(Error)
337
+ raise rmsg
338
+ else
339
+ return rmsg.params[0]
340
+ end
341
+ end
342
+ else
343
+ send(m.marshall)
344
+ on_return(m) do |rmsg|
345
+ if rmsg.is_a?(Error)
346
+ yield rmsg
347
+ else
348
+ yield rmsg.params[0]
349
+ end
350
+ end
351
+ end
352
+ nil
353
+ end
354
+
355
+ # Issues a call to the org.freedesktop.DBus.Introspectable.Introspect method
356
+ # _dest_ is the service and _path_ the object path you want to introspect
357
+ # If a code block is given, the introspect call in asynchronous. If not
358
+ # data is returned
359
+ #
360
+ # FIXME: link to ProxyObject data definition
361
+ # The returned object is a ProxyObject that has methods you can call to
362
+ # issue somme METHOD_CALL messages, and to setup to receive METHOD_RETURN
363
+ def introspect(dest, path)
364
+ if not block_given?
365
+ # introspect in synchronous !
366
+ data = introspect_data(dest, path)
367
+ pof = DBus::ProxyObjectFactory.new(data, self, dest, path)
368
+ return pof.build
369
+ else
370
+ introspect_data(dest, path) do |data|
371
+ yield(DBus::ProxyObjectFactory.new(data, self, dest, path).build)
372
+ end
373
+ end
374
+ end
375
+
376
+ # Exception raised when a service name is requested that is not available.
377
+ class NameRequestError < Exception
378
+ end
379
+
380
+ # Attempt to request a service _name_.
381
+ def request_service(name)
382
+ r = proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING)
383
+ raise NameRequestError if r[0] != REQUEST_NAME_REPLY_PRIMARY_OWNER
384
+ @service = Service.new(name, self)
385
+ @service
386
+ end
387
+
388
+ # Set up a ProxyObject for the bus itself, since the bus is introspectable.
389
+ # Returns the object.
390
+ def proxy
391
+ if @proxy == nil
392
+ path = "/org/freedesktop/DBus"
393
+ dest = "org.freedesktop.DBus"
394
+ pof = DBus::ProxyObjectFactory.new(DBUSXMLINTRO, self, dest, path)
395
+ @proxy = pof.build["org.freedesktop.DBus"]
396
+ end
397
+ @proxy
398
+ end
399
+
400
+ # Fill (append) the buffer from data that might be available on the
401
+ # socket.
402
+ def update_buffer
403
+ @buffer += @socket.read_nonblock(MSG_BUF_SIZE)
404
+ end
405
+
406
+ # Get one message from the bus and remove it from the buffer.
407
+ # Return the message.
408
+ def pop_message
409
+ ret = nil
410
+ begin
411
+ ret, size = Message.new.unmarshall_buffer(@buffer)
412
+ @buffer.slice!(0, size)
413
+ rescue IncompleteBufferException => e
414
+ # fall through, let ret be null
415
+ end
416
+ ret
417
+ end
418
+
419
+ # Retrieve all the messages that are currently in the buffer.
420
+ def messages
421
+ ret = Array.new
422
+ while msg = pop_message
423
+ ret << msg
424
+ end
425
+ ret
426
+ end
427
+
428
+ # The buffer size for messages.
429
+ MSG_BUF_SIZE = 4096
430
+
431
+ # Update the buffer and retrieve all messages using Connection#messages.
432
+ # Return the messages.
433
+ def poll_messages
434
+ ret = nil
435
+ r, d, d = IO.select([@socket], nil, nil, 0)
436
+ if r and r.size > 0
437
+ update_buffer
438
+ end
439
+ messages
440
+ end
441
+
442
+ # Wait for a message to arrive. Return it once it is available.
443
+ def wait_for_message
444
+ ret = pop_message
445
+ while ret == nil
446
+ r, d, d = IO.select([@socket])
447
+ if r and r[0] == @socket
448
+ update_buffer
449
+ ret = pop_message
450
+ end
451
+ end
452
+ ret
453
+ end
454
+
455
+ # Send a message _m_ on to the bus. This is done synchronously, thus
456
+ # the call will block until a reply message arrives.
457
+ def send_sync(m, &retc) # :yields: reply/return message
458
+ send(m.marshall)
459
+ @method_call_msgs[m.serial] = m
460
+ @method_call_replies[m.serial] = retc
461
+
462
+ retm = wait_for_message
463
+ process(retm)
464
+ until [DBus::Message::ERROR,
465
+ DBus::Message::METHOD_RETURN].include?(retm.message_type) and
466
+ retm.reply_serial == m.serial
467
+ retm = wait_for_message
468
+ process(retm)
469
+ end
470
+ end
471
+
472
+ # Specify a code block that has to be executed when a reply for
473
+ # message _m_ is received.
474
+ def on_return(m, &retc)
475
+ # Have a better exception here
476
+ if m.message_type != Message::METHOD_CALL
477
+ raise "on_return should only get method_calls"
478
+ end
479
+ @method_call_msgs[m.serial] = m
480
+ @method_call_replies[m.serial] = retc
481
+ end
482
+
483
+ # Asks bus to send us messages matching mr, and execute slot when
484
+ # received
485
+ def add_match(mr, &slot)
486
+ # check this is a signal.
487
+ @signal_matchrules << [mr, slot]
488
+ self.proxy.AddMatch(mr.to_s)
489
+ end
490
+
491
+ # Process a message _m_ based on its type.
492
+ # method call:: FIXME...
493
+ # method call return value:: FIXME...
494
+ # signal:: FIXME...
495
+ # error:: FIXME...
496
+ def process(m)
497
+ case m.message_type
498
+ when Message::ERROR, Message::METHOD_RETURN
499
+ raise InvalidPacketException if m.reply_serial == nil
500
+ mcs = @method_call_replies[m.reply_serial]
501
+ if not mcs
502
+ puts "no return code for #{mcs.inspect} (#{m.inspect})" if $DEBUG
503
+ else
504
+ if m.message_type == Message::ERROR
505
+ mcs.call(Error.new(m))
506
+ else
507
+ mcs.call(m)
508
+ end
509
+ @method_call_replies.delete(m.reply_serial)
510
+ @method_call_msgs.delete(m.reply_serial)
511
+ end
512
+ when DBus::Message::METHOD_CALL
513
+ if m.path == "/org/freedesktop/DBus"
514
+ puts "Got method call on /org/freedesktop/DBus" if $DEBUG
515
+ end
516
+ # handle introspectable as an exception:
517
+ if m.interface == "org.freedesktop.DBus.Introspectable" and
518
+ m.member == "Introspect"
519
+ reply = Message.new(Message::METHOD_RETURN).reply_to(m)
520
+ reply.sender = @unique_name
521
+ node = @service.get_node(m.path)
522
+ raise NotImplementedError if not node
523
+ reply.sender = @unique_name
524
+ reply.add_param(Type::STRING, @service.get_node(m.path).to_xml)
525
+ send(reply.marshall)
526
+ else
527
+ node = @service.get_node(m.path)
528
+ return if node.nil?
529
+ obj = node.object
530
+ return if obj.nil?
531
+ obj.dispatch(m) if obj
532
+ end
533
+ when DBus::Message::SIGNAL
534
+ @signal_matchrules.each do |elem|
535
+ mr, slot = elem
536
+ if mr.match(m)
537
+ slot.call(m)
538
+ return
539
+ end
540
+ end
541
+ else
542
+ puts "Unknown message type: #{m.message_type}" if $DEBUG
543
+ end
544
+ end
545
+
546
+ # Retrieves the service with the given _name_.
547
+ def service(name)
548
+ # The service might not exist at this time so we cannot really check
549
+ # anything
550
+ Service.new(name, self)
551
+ end
552
+ alias :[] :service
553
+
554
+ # Emit a signal event for the given _service_, object _obj_, interface
555
+ # _intf_ and signal _sig_ with arguments _args_.
556
+ def emit(service, obj, intf, sig, *args)
557
+ m = Message.new(DBus::Message::SIGNAL)
558
+ m.path = obj.path
559
+ m.interface = intf.name
560
+ m.member = sig.name
561
+ m.sender = service.name
562
+ i = 0
563
+ sig.params.each do |par|
564
+ m.add_param(par[1], args[i])
565
+ i += 1
566
+ end
567
+ send(m.marshall)
568
+ end
569
+
570
+ ###########################################################################
571
+ private
572
+
573
+ # Send a hello messages to the bus to let it know we are here.
574
+ def send_hello
575
+ m = Message.new(DBus::Message::METHOD_CALL)
576
+ m.path = "/org/freedesktop/DBus"
577
+ m.destination = "org.freedesktop.DBus"
578
+ m.interface = "org.freedesktop.DBus"
579
+ m.member = "Hello"
580
+ send_sync(m) do |rmsg|
581
+ @unique_name = rmsg.destination
582
+ puts "Got hello reply. Our unique_name is #{@unique_name}" if $DEBUG
583
+ end
584
+ end
585
+
586
+ # Parse the session string (socket address).
587
+ def parse_session_string
588
+ path_parsed = /^([^:]*):([^;]*)$/.match(@path)
589
+ @transport = path_parsed[1]
590
+ adr = path_parsed[2]
591
+ if @transport == "unix"
592
+ adr.split(",").each do |eqstr|
593
+ idx, val = eqstr.split("=")
594
+ case idx
595
+ when "path"
596
+ @type = idx
597
+ @unix = val
598
+ when "abstract"
599
+ @type = idx
600
+ @unix_abstract = val
601
+ when "guid"
602
+ @guid = val
603
+ end
604
+ end
605
+ end
606
+ end
607
+
608
+ # Initialize the connection to the bus.
609
+ def init_connection
610
+ @client = Client.new(@socket)
611
+ @client.authenticate
612
+ # TODO: code some real stuff here
613
+ #writel("AUTH EXTERNAL 31303030")
614
+ #s = readl
615
+ # parse OK ?
616
+ #writel("BEGIN")
617
+ end
618
+ end # class Connection
619
+
620
+ # = D-Bus session bus class
621
+ #
622
+ # The session bus is a session specific bus (mostly for desktop use).
623
+ # This is a singleton class.
624
+ class SessionBus < Connection
625
+ include Singleton
626
+
627
+ # Get the the default session bus.
628
+ def initialize
629
+ super(ENV["DBUS_SESSION_BUS_ADDRESS"])
630
+ connect
631
+ send_hello
632
+ end
633
+ end
634
+
635
+ # = D-Bus system bus class
636
+ #
637
+ # The system bus is a system-wide bus mostly used for global or
638
+ # system usages. This is a singleton class.
639
+ class SystemBus < Connection
640
+ include Singleton
641
+
642
+ # Get the default system bus.
643
+ def initialize
644
+ super(SystemSocketName)
645
+ connect
646
+ send_hello
647
+ end
648
+ end
649
+
650
+ # FIXME: we should get rid of these
651
+
652
+ def DBus.system_bus
653
+ SystemBus.instance
654
+ end
655
+
656
+ def DBus.session_bus
657
+ SessionBus.instance
658
+ end
659
+
660
+ # = Main event loop class.
661
+ #
662
+ # Class that takes care of handling message and signal events
663
+ # asynchronously. *Note:* This is a native implement and therefore does
664
+ # not integrate with a graphical widget set main loop.
665
+ class Main
666
+ # Create a new main event loop.
667
+ def initialize
668
+ @buses = Hash.new
669
+ end
670
+
671
+ # Add a _bus_ to the list of buses to watch for events.
672
+ def <<(bus)
673
+ @buses[bus.socket] = bus
674
+ end
675
+
676
+ # Run the main loop. This is a blocking call!
677
+ def run
678
+ loop do
679
+ ready, dum, dum = IO.select(@buses.keys)
680
+ ready.each do |socket|
681
+ b = @buses[socket]
682
+ b.update_buffer
683
+ while m = b.pop_message
684
+ b.process(m)
685
+ end
686
+ end
687
+ end
688
+ end
689
+ end # class Main
690
+ end # module DBus