yong-ruby-dbus 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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