pangdudu-ruby-dbus 0.1.0

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.rdoc ADDED
@@ -0,0 +1,111 @@
1
+ = Ruby D-Bus -> ffwd
2
+
3
+ Ruby D-Bus provides an implementation of the D-Bus protocol such that the
4
+ D-Bus system can be used in the Ruby programming language.
5
+
6
+
7
+ This is my personal dev-fork of mvidners upstream.
8
+
9
+ If you'd like to contrib to this fork, let me know.
10
+
11
+
12
+
13
+ Most of my work here is actually for another project: http://github.com/pangdudu/robots/tree/master
14
+
15
+ I might also start writing a less ugly version of: http://github.com/pangdudu/ruby-dbus-daemon/tree/master
16
+
17
+ If you know of a nice custom-dbus-daemon let me know. Still thinking about writing
18
+ a dbus spec compliant server/daemon implementation in (pure) Ruby, so that I can
19
+ play around with the network and authentification more easily.
20
+
21
+ Oki, will clean up a little now. :)
22
+
23
+ Peace.
24
+
25
+ == Requirements
26
+
27
+ * Ruby 1.8 (>= 1.8.6?)
28
+
29
+ == Installation
30
+
31
+ sudo gem install pangdudu-ruby-dbus --source=http://gems.github.com
32
+
33
+ == Features
34
+
35
+ Ruby D-Bus currently supports the following features:
36
+
37
+ * Connecting to local buses.
38
+ * Accessing remote services, objects and interfaces.
39
+ * Invoking methods on remote objects synchronously and asynchronously.
40
+ * Catch signals on remote objects and handle them via callbacks.
41
+ * Remote object introspection.
42
+ * Walking object trees.
43
+ * Creating services and registering them on the bus.
44
+ * Exporting objects with interfaces on a bus for remote use.
45
+ * Rubyish D-Bus object and interface syntax support that automatically
46
+ allows for introspection.
47
+ * Emitting signals on exported objects.
48
+
49
+ * Connection to a local or remote bus over TCP/IP
50
+ * Authentification mechanisms working now: External,CookieSHA1
51
+
52
+ == Usage
53
+
54
+ === Basics:
55
+
56
+ View a tutorial online on http://trac.luon.net/data/ruby-dbus/tutorial/.
57
+
58
+
59
+ === TCP/Remote stuff:
60
+
61
+ Take a look at 'config/remote.session.dbus.conf' and start 'config/start_dbus_session.sh'.
62
+
63
+ You can now try and run one of the tests e.g. 'test/simple_socket_test.rb'.
64
+
65
+
66
+ === Problems you'll have with 'CookieSHA1' when using it remotely:
67
+
68
+ Unless we [including you :)] write a funkier SASL authentification mechanism that
69
+ makes sense when using it over-wire, we're stuck with the cookie auth.
70
+
71
+ Works like this:
72
+
73
+ There is a file in '~/.dbus-keyrings' named 'org_freedesktop_general'.
74
+ It is '-rw-------' so that only you can read it (if you change the permissions,
75
+ dbus-daemon will most likely ignore the file leaving you with 'External', which is
76
+ even worse remotely). It's kind of a secret key you use to auth against yourself
77
+ later on.
78
+
79
+ If you take a look at 'dbus/auth.rb line 40+' you'll see the problem:
80
+
81
+ # Search cookie file for id
82
+ path = File.join(ENV['HOME'], '.dbus-keyrings', context)
83
+
84
+ So if you're starting 'config/start_dbus_session.sh' on one host and 'config/start_dbus_session.sh'
85
+ on another one, you'll need to make sure that the content in '~/.dbus-keyrings/org_freedesktop_general'
86
+ is the same on both machines, in order for the 'CookieSHA1' auth to work.
87
+
88
+ Not cool. I can think of hacks with nfs,smb or fuse:sshfs making it less
89
+ painful to use.
90
+
91
+ The file content also get's updated every 5 minutes (when a client fails to auth etc.).
92
+ Making copy and paste from one shell to another very frolic.
93
+
94
+ To sum it up:
95
+
96
+ Today it's acceptable to use it like this, but until next week we'll need an easy
97
+ auth mechanism that works on the wire.
98
+
99
+
100
+ more infos:
101
+ http://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms
102
+ http://lists.freedesktop.org/archives/dbus/2007-June/008067.html
103
+
104
+ Booyaa.
105
+
106
+ == License
107
+
108
+ Ruby D-Bus is free software; you can redistribute it and/or modify it
109
+ under the terms of the GNU Lesser General Public License as published by the
110
+ Free Software Foundation; either version 2.1 of the License, or (at
111
+ your option) any later version.
@@ -0,0 +1,39 @@
1
+ <!-- This configuration file controls the per-user-login-session message bus.
2
+ Add a session-local.conf and edit that rather than changing this
3
+ file directly. -->
4
+
5
+ <!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
6
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
7
+ <busconfig>
8
+ <!-- Our well-known bus type, don't change this -->
9
+ <type>session</type>
10
+
11
+ <!-- If we fork, keep the user's original umask to avoid affecting
12
+ the behavior of child processes. -->
13
+ <keep_umask/>
14
+
15
+ <!-- <allow_anonymous/> -->
16
+
17
+ <!-- Listen to everything on tcp! -->
18
+ <listen>tcp:host=0.0.0.0,port=2687,family=ipv4</listen>
19
+ <!-- Listen on socket at this file location -->
20
+ <listen>unix:path=/tmp/socket_test_session_bus_socket</listen>
21
+
22
+ <standard_session_servicedirs />
23
+
24
+ <policy context="default">
25
+ <!-- Allow everything to be sent -->
26
+ <allow send_destination="*" eavesdrop="true"/>
27
+ <!-- Allow everything to be received -->
28
+ <allow eavesdrop="true"/>
29
+ <!-- Allow anyone to own anything -->
30
+ <allow own="*"/>
31
+ </policy>
32
+
33
+ <!-- raise the service start timeout to 40 seconds as it can timeout
34
+ on the live cd on slow machines -->
35
+ <limit name="service_start_timeout">60000</limit>
36
+
37
+ <include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
38
+
39
+ </busconfig>
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ dbus-daemon --print-address --config-file=remote.session.dbus.conf
data/lib/dbus/auth.rb ADDED
@@ -0,0 +1,238 @@
1
+ # This file is part of the ruby-dbus project
2
+ # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License, version 2.1 as published by the Free Software Foundation.
7
+ # See the file "COPYING" for the exact licensing terms.
8
+
9
+ module DBus
10
+ # Exception raised when authentication fails somehow.
11
+ class AuthenticationFailed < Exception
12
+ end
13
+
14
+ # = General class for authentication.
15
+ class Authenticator
16
+ # Returns the name of the authenticator.
17
+ def name
18
+ self.class.to_s.upcase.sub(/.*::/, "")
19
+ end
20
+ end
21
+
22
+ # = External authentication class
23
+ #
24
+ # Class for 'external' type authentication.
25
+ class External < Authenticator
26
+ # Performs the authentication.
27
+ def authenticate
28
+ # Take the user id (eg integer 1000) make a string out of it "1000", take
29
+ # each character and determin hex value "1" => 0x31, "0" => 0x30. You
30
+ # obtain for "1000" => 31303030 This is what the server is expecting.
31
+ # Why? I dunno. How did I come to that conclusion? by looking at rbus
32
+ # code. I have no idea how he found that out.
33
+ return Process.uid.to_s.split(//).collect { |a| "%x" % a[0] }.join
34
+ end
35
+ end
36
+
37
+ # = Authentication class using SHA1 crypto algorithm
38
+ #
39
+ # Class for 'CookieSHA1' type authentication.
40
+ # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
41
+ class DBusCookieSHA1 < Authenticator
42
+
43
+ #the autenticate method (called in stage one of authentification)
44
+ def authenticate
45
+ require 'etc'
46
+ return "#{hex_encode(Etc.getlogin)}" #server expects it to be binary
47
+ end
48
+
49
+ #returns the modules name
50
+ def name
51
+ return 'DBUS_COOKIE_SHA1'
52
+ end
53
+
54
+ #handles the interesting crypto stuff, check the rbus-project for more info: http://rbus.rubyforge.org/
55
+ def data(hexdata)
56
+ require 'digest/sha1'
57
+ data = hex_decode(hexdata)
58
+ # name of cookie file, id of cookie in file, servers random challenge
59
+ context, id, s_challenge = data.split(' ')
60
+ # Random client challenge
61
+ c_challenge = Array.new(s_challenge.length/2).map{|obj|obj=rand(255).to_s}.join
62
+ # Search cookie file for id
63
+ path = File.join(ENV['HOME'], '.dbus-keyrings', context)
64
+ dlog "path: #{path.inspect}"
65
+ File.foreach(path) do |line|
66
+ if line.index(id) == 0
67
+ # Right line of file, read cookie
68
+ cookie = line.split(' ')[2].chomp
69
+ dlog "cookie: #{cookie.inspect}"
70
+ # Concatenate and encrypt
71
+ to_encrypt = [s_challenge, c_challenge, cookie].join(':')
72
+ sha = Digest::SHA1.hexdigest(to_encrypt)
73
+ #the almighty tcp server wants everything hex encoded
74
+ hex_response = hex_encode("#{c_challenge} #{sha}")
75
+ # Return response
76
+ response = [:AuthOk, hex_response]
77
+ return response
78
+ end
79
+ end
80
+ elog "Unable to locate cookie"
81
+ end
82
+
83
+ # encode plain to hex
84
+ def hex_encode(plain)
85
+ return nil if plain.nil?
86
+ plain.to_s.unpack('H*')[0]
87
+ end
88
+
89
+ # decode hex to plain
90
+ def hex_decode(encoded)
91
+ encoded.scan(/[[:xdigit:]]{2}/).map{|h|h.hex.chr}.join
92
+ end
93
+ end #DBusCookieSHA1 class ends here
94
+
95
+ # = Authentication client class.
96
+ #
97
+ # Class tha performs the actional authentication.
98
+ class Client
99
+ # Create a new authentication client.
100
+ def initialize(socket)
101
+ @socket = socket
102
+ @state = nil
103
+ @auth_list = [External,DBusCookieSHA1]
104
+ end
105
+
106
+ # Start the authentication process.
107
+ def authenticate
108
+ @socket.write(0.chr)
109
+ next_authenticator
110
+ @state = :Starting
111
+ while @state != :Authenticated
112
+ r = next_state
113
+ return r if not r
114
+ end
115
+ true
116
+ end
117
+
118
+ ##########
119
+ private
120
+ ##########
121
+
122
+ # Send an authentication method _meth_ with arguments _args_ to the
123
+ # server.
124
+ def send(meth, *args)
125
+ o = ([meth] + args).join(" ")
126
+ @socket.write(o + "\r\n")
127
+ end
128
+
129
+ # Try authentication using the next authenticator.
130
+ def next_authenticator
131
+ begin
132
+ raise AuthException if @auth_list.size == 0
133
+ @authenticator = @auth_list.shift.new
134
+ auth_msg = ["AUTH", @authenticator.name, @authenticator.authenticate]
135
+ dlog "auth_msg: #{auth_msg.inspect}"
136
+ send(auth_msg)
137
+ rescue AuthException
138
+ @socket.close
139
+ raise
140
+ end
141
+ end
142
+
143
+
144
+ # Read data (a buffer) from the bus until CR LF is encountered.
145
+ # Return the buffer without the CR LF characters.
146
+ def next_msg
147
+ data,crlf = "","\r\n"
148
+ left = 1024 #1024 byte, no idea if it's ever getting bigger
149
+ while left > 0
150
+ buf = @socket.read( left > 1 ? 1 : left )
151
+ break if buf.nil?
152
+ left -= buf.size
153
+ data += buf
154
+ break if data.include? crlf #crlf means line finished, the TCP socket keeps on listening, so we break
155
+ end
156
+ readline = data.chomp.split(" ")
157
+ dlog "readline: #{readline.inspect}"
158
+ return readline
159
+ end
160
+
161
+ # Try to reach the next state based on the current state.
162
+ def next_state
163
+ msg = next_msg
164
+ if @state == :Starting
165
+ dlog ":Starting msg: #{msg[0].inspect}"
166
+ case msg[0]
167
+ when "OK"
168
+ @state = :WaitingForOk
169
+ when "CONTINUE"
170
+ @state = :WaitingForData
171
+ when "REJECTED" #needed by tcp, unix-path/abstract doesn't get here
172
+ @state = :WaitingForData
173
+ end
174
+ end
175
+ dlog "state: #{@state}"
176
+ case @state
177
+ when :WaitingForData
178
+ dlog ":WaitingForData msg: #{msg[0].inspect}"
179
+ case msg[0]
180
+ when "DATA"
181
+ chall = msg[1]
182
+ resp, chall = @authenticator.data(chall)
183
+ dlog ":WaitingForData/DATA resp: #{resp.inspect}"
184
+ case resp
185
+ when :AuthContinue
186
+ send("DATA", chall)
187
+ @state = :WaitingForData
188
+ when :AuthOk
189
+ send("DATA", chall)
190
+ @state = :WaitingForOk
191
+ when :AuthError
192
+ send("ERROR")
193
+ @state = :WaitingForData
194
+ end
195
+ when "REJECTED"
196
+ next_authenticator
197
+ @state = :WaitingForData
198
+ when "ERROR"
199
+ send("CANCEL")
200
+ @state = :WaitingForReject
201
+ when "OK"
202
+ send("BEGIN")
203
+ @state = :Authenticated
204
+ else
205
+ send("ERROR")
206
+ @state = :WaitingForData
207
+ end
208
+ when :WaitingForOk
209
+ dlog ":WaitingForOk msg: #{msg[0].inspect}"
210
+ case msg[0]
211
+ when "OK"
212
+ send("BEGIN")
213
+ @state = :Authenticated
214
+ when "REJECT"
215
+ next_authenticator
216
+ @state = :WaitingForData
217
+ when "DATA", "ERROR"
218
+ send("CANCEL")
219
+ @state = :WaitingForReject
220
+ else
221
+ send("ERROR")
222
+ @state = :WaitingForOk
223
+ end
224
+ when :WaitingForReject
225
+ dlog ":WaitingForReject msg: #{msg[0].inspect}"
226
+ case msg[0]
227
+ when "REJECT"
228
+ next_authenticator
229
+ @state = :WaitingForOk
230
+ else
231
+ @socket.close
232
+ return false
233
+ end
234
+ end
235
+ return true
236
+ end # def next_state
237
+ end # class Client
238
+ end # module D-Bus
data/lib/dbus/bus.rb ADDED
@@ -0,0 +1,719 @@
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
+ require 'socket'
11
+ require 'thread'
12
+ require 'singleton'
13
+
14
+ # = D-Bus main module
15
+ #
16
+ # Module containing all the D-Bus modules and classes.
17
+ module DBus
18
+ # This represents a remote service. It should not be instancied directly
19
+ # Use Bus::service()
20
+ class Service
21
+ # The service name.
22
+ attr_reader :name
23
+ # The bus the service is running on.
24
+ attr_reader :bus
25
+ # The service root (FIXME).
26
+ attr_reader :root
27
+
28
+ # Create a new service with a given _name_ on a given _bus_.
29
+ def initialize(name, bus)
30
+ @name, @bus = name, bus
31
+ @root = Node.new("/")
32
+ end
33
+
34
+ # Determine whether the serice name already exists.
35
+ def exists?
36
+ bus.proxy.ListName.member?(@name)
37
+ end
38
+
39
+ # Perform an introspection on all the objects on the service
40
+ # (starting recursively from the root).
41
+ def introspect
42
+ if block_given?
43
+ raise NotImplementedError
44
+ else
45
+ rec_introspect(@root, "/")
46
+ end
47
+ self
48
+ end
49
+
50
+ # Retrieves an object at the given _path_.
51
+ def object(path)
52
+ node = get_node(path, true)
53
+ if node.object.nil?
54
+ node.object = ProxyObject.new(@bus, @name, path)
55
+ end
56
+ node.object
57
+ end
58
+
59
+ # Export an object _obj_ (an DBus::Object subclass instance).
60
+ def export(obj)
61
+ obj.service = self
62
+ get_node(obj.path, true).object = obj
63
+ end
64
+
65
+ # Get the object node corresponding to the given _path_. if _create_ is
66
+ # true, the the nodes in the path are created if they do not already exist.
67
+ def get_node(path, create = false)
68
+ n = @root
69
+ path.sub(/^\//, "").split("/").each do |elem|
70
+ if not n[elem]
71
+ if not create
72
+ return nil
73
+ else
74
+ n[elem] = Node.new(elem)
75
+ end
76
+ end
77
+ n = n[elem]
78
+ end
79
+ if n.nil?
80
+ wlog "Unknown object #{path.inspect}"
81
+ end
82
+ n
83
+ end
84
+
85
+ #########
86
+ private
87
+ #########
88
+
89
+ # Perform a recursive retrospection on the given current _node_
90
+ # on the given _path_.
91
+ def rec_introspect(node, path)
92
+ xml = bus.introspect_data(@name, path)
93
+ intfs, subnodes = IntrospectXMLParser.new(xml).parse
94
+ subnodes.each do |nodename|
95
+ subnode = node[nodename] = Node.new(nodename)
96
+ if path == "/"
97
+ subpath = "/" + nodename
98
+ else
99
+ subpath = path + "/" + nodename
100
+ end
101
+ rec_introspect(subnode, subpath)
102
+ end
103
+ if intfs.size > 0
104
+ node.object = ProxyObjectFactory.new(xml, @bus, @name, path).build
105
+ end
106
+ end
107
+ end
108
+
109
+ # = Object path node class
110
+ #
111
+ # Class representing a node on an object path.
112
+ class Node < Hash
113
+ # The D-Bus object contained by the node.
114
+ attr_accessor :object
115
+ # The name of the node.
116
+ attr_reader :name
117
+
118
+ # Create a new node with a given _name_.
119
+ def initialize(name)
120
+ @name = name
121
+ @object = nil
122
+ end
123
+
124
+ # Return an XML string representation of the node.
125
+ def to_xml
126
+ xml = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
127
+ <node>'
128
+
129
+ self.each_pair do |k, v|
130
+ xml += "<node name=\"#{k}\" />"
131
+ end
132
+ if @object
133
+ @object.intfs.each_pair do |k, v|
134
+ xml += %{<interface name="#{v.name}">\n}
135
+ v.methods.each_value { |m| xml += m.to_xml }
136
+ v.signals.each_value { |m| xml += m.to_xml }
137
+ xml +="</interface>\n"
138
+ end
139
+ end
140
+ xml += '</node>'
141
+ return xml
142
+ end
143
+
144
+ # Return inspect information of the node.
145
+ def inspect
146
+ # Need something here
147
+ "<DBus::Node #{sub_inspect}>"
148
+ end
149
+
150
+ # Return instance inspect information, used by Node#inspect.
151
+ def sub_inspect
152
+ s = ""
153
+ if not @object.nil?
154
+ s += "%x " % @object.object_id
155
+ end
156
+ s + "{" + keys.collect { |k| "#{k} => #{self[k].sub_inspect}" }.join(",") + "}"
157
+ end
158
+ end # class Inspect
159
+
160
+ # FIXME: rename Connection to Bus?
161
+
162
+ # D-Bus main connection class
163
+ #
164
+ # Main class that maintains a connection to a bus and can handle incoming
165
+ # and outgoing messages.
166
+ class Connection
167
+ # The unique name (by specification) of the message.
168
+ attr_reader :unique_name
169
+ # The socket that is used to connect with the bus.
170
+ attr_reader :socket
171
+
172
+ # Create a new connection to the bus for a given connect _path_. _path_
173
+ # format is described in the D-Bus specification:
174
+ # http://dbus.freedesktop.org/doc/dbus-specification.html#addresses
175
+ # and is something like:
176
+ # "transport1:key1=value1,key2=value2;transport2:key1=value1,key2=value2"
177
+ # e.g. "unix:path=/tmp/dbus-test" or "tcp:host=localhost,port=2687"
178
+ def initialize(path)
179
+ @path = path
180
+ @unique_name = nil
181
+ @buffer = ""
182
+ @method_call_replies = Hash.new
183
+ @method_call_msgs = Hash.new
184
+ @signal_matchrules = Array.new
185
+ @proxy = nil
186
+ @object_root = Node.new("/")
187
+ end
188
+
189
+ # Connect to the bus and initialize the connection.
190
+ def connect
191
+ connect_to_tcp if @path.include? "tcp:" #connect to tcp socket
192
+ connect_to_unix if @path.include? "unix:" #connect to unix socket
193
+ end
194
+
195
+ # Connect to a bus over tcp and initialize the connection.
196
+ def connect_to_tcp
197
+ #check if the path is sufficient
198
+ if @path.include? "host=" and @path.include? "port="
199
+ host,port,family = "","",""
200
+ #get the parameters
201
+ @path.split(",").each do |para|
202
+ host = para.sub("tcp:","").sub("host=","") if para.include? "host="
203
+ port = para.sub("port=","").to_i if para.include? "port="
204
+ family = para.sub("family=","") if para.include? "family="
205
+ end
206
+ dlog "host,port,family : #{host},#{port},#{family}"
207
+ begin
208
+ #initialize the tcp socket
209
+ @socket = TCPSocket.new(host,port)
210
+ init_connection
211
+ rescue
212
+ elog "Could not establish connection to: #{@path}, will now exit."
213
+ exit(0) #a little harsh
214
+ end
215
+ else
216
+ #Danger, Will Robinson: the specified "path" is not usable
217
+ elog "supplied path: #{@path}, unusable! sorry."
218
+ end
219
+ end
220
+
221
+ # Connect to an abstract unix bus and initialize the connection.
222
+ def connect_to_unix
223
+ @socket = Socket.new(Socket::Constants::PF_UNIX,Socket::Constants::SOCK_STREAM, 0)
224
+ parse_session_string
225
+ if @transport == "unix" and @type == "abstract"
226
+ if HOST_END == LIL_END
227
+ sockaddr = "\1\0\0#{@unix_abstract}"
228
+ else
229
+ sockaddr = "\0\1\0#{@unix_abstract}"
230
+ end
231
+ elsif @transport == "unix" and @type == "path"
232
+ sockaddr = Socket.pack_sockaddr_un(@unix)
233
+ end
234
+ @socket.connect(sockaddr)
235
+ init_connection
236
+ end
237
+
238
+ # Parse the session string (socket address).
239
+ def parse_session_string
240
+ path_parsed = /^([^:]*):([^;]*)$/.match(@path)
241
+ @transport = path_parsed[1]
242
+ adr = path_parsed[2]
243
+ if @transport == "unix"
244
+ adr.split(",").each do |eqstr|
245
+ idx, val = eqstr.split("=")
246
+ case idx
247
+ when "path"
248
+ @type = idx
249
+ @unix = val
250
+ when "abstract"
251
+ @type = idx
252
+ @unix_abstract = val
253
+ when "guid"
254
+ @guid = val
255
+ end
256
+ end
257
+ end
258
+ end
259
+
260
+ # Send the buffer _buf_ to the bus using Connection#writel.
261
+ def send(buf)
262
+ @socket.write(buf) unless @socket.nil?
263
+ end
264
+
265
+ # Tell a bus to register itself on the glib main loop
266
+ def glibize
267
+ require 'glib2'
268
+ # Circumvent a ruby-glib bug
269
+ @channels ||= Array.new
270
+
271
+ gio = GLib::IOChannel.new(@socket.fileno)
272
+ @channels << gio
273
+ gio.add_watch(GLib::IOChannel::IN) do |c, ch|
274
+ update_buffer
275
+ messages.each do |msg|
276
+ process(msg)
277
+ end
278
+ true
279
+ end
280
+ end
281
+
282
+ # FIXME: describe the following names, flags and constants.
283
+ # See DBus spec for definition
284
+ NAME_FLAG_ALLOW_REPLACEMENT = 0x1
285
+ NAME_FLAG_REPLACE_EXISTING = 0x2
286
+ NAME_FLAG_DO_NOT_QUEUE = 0x4
287
+
288
+ REQUEST_NAME_REPLY_PRIMARY_OWNER = 0x1
289
+ REQUEST_NAME_REPLY_IN_QUEUE = 0x2
290
+ REQUEST_NAME_REPLY_EXISTS = 0x3
291
+ REQUEST_NAME_REPLY_ALREADY_OWNER = 0x4
292
+
293
+ DBUSXMLINTRO = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
294
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
295
+ <node>
296
+ <interface name="org.freedesktop.DBus.Introspectable">
297
+ <method name="Introspect">
298
+ <arg name="data" direction="out" type="s"/>
299
+ </method>
300
+ </interface>
301
+ <interface name="org.freedesktop.DBus">
302
+ <method name="RequestName">
303
+ <arg direction="in" type="s"/>
304
+ <arg direction="in" type="u"/>
305
+ <arg direction="out" type="u"/>
306
+ </method>
307
+ <method name="ReleaseName">
308
+ <arg direction="in" type="s"/>
309
+ <arg direction="out" type="u"/>
310
+ </method>
311
+ <method name="StartServiceByName">
312
+ <arg direction="in" type="s"/>
313
+ <arg direction="in" type="u"/>
314
+ <arg direction="out" type="u"/>
315
+ </method>
316
+ <method name="Hello">
317
+ <arg direction="out" type="s"/>
318
+ </method>
319
+ <method name="NameHasOwner">
320
+ <arg direction="in" type="s"/>
321
+ <arg direction="out" type="b"/>
322
+ </method>
323
+ <method name="ListNames">
324
+ <arg direction="out" type="as"/>
325
+ </method>
326
+ <method name="ListActivatableNames">
327
+ <arg direction="out" type="as"/>
328
+ </method>
329
+ <method name="AddMatch">
330
+ <arg direction="in" type="s"/>
331
+ </method>
332
+ <method name="RemoveMatch">
333
+ <arg direction="in" type="s"/>
334
+ </method>
335
+ <method name="GetNameOwner">
336
+ <arg direction="in" type="s"/>
337
+ <arg direction="out" type="s"/>
338
+ </method>
339
+ <method name="ListQueuedOwners">
340
+ <arg direction="in" type="s"/>
341
+ <arg direction="out" type="as"/>
342
+ </method>
343
+ <method name="GetConnectionUnixUser">
344
+ <arg direction="in" type="s"/>
345
+ <arg direction="out" type="u"/>
346
+ </method>
347
+ <method name="GetConnectionUnixProcessID">
348
+ <arg direction="in" type="s"/>
349
+ <arg direction="out" type="u"/>
350
+ </method>
351
+ <method name="GetConnectionSELinuxSecurityContext">
352
+ <arg direction="in" type="s"/>
353
+ <arg direction="out" type="ay"/>
354
+ </method>
355
+ <method name="ReloadConfig">
356
+ </method>
357
+ <signal name="NameOwnerChanged">
358
+ <arg type="s"/>
359
+ <arg type="s"/>
360
+ <arg type="s"/>
361
+ </signal>
362
+ <signal name="NameLost">
363
+ <arg type="s"/>
364
+ </signal>
365
+ <signal name="NameAcquired">
366
+ <arg type="s"/>
367
+ </signal>
368
+ </interface>
369
+ </node>
370
+ '
371
+
372
+ def introspect_data(dest, path)
373
+ m = DBus::Message.new(DBus::Message::METHOD_CALL)
374
+ m.path = path
375
+ m.interface = "org.freedesktop.DBus.Introspectable"
376
+ m.destination = dest
377
+ m.member = "Introspect"
378
+ m.sender = unique_name
379
+ if not block_given?
380
+ # introspect in synchronous !
381
+ send_sync(m) do |rmsg|
382
+ if rmsg.is_a?(Error)
383
+ raise rmsg
384
+ else
385
+ return rmsg.params[0]
386
+ end
387
+ end
388
+ else
389
+ send(m.marshall)
390
+ on_return(m) do |rmsg|
391
+ if rmsg.is_a?(Error)
392
+ yield rmsg
393
+ else
394
+ yield rmsg.params[0]
395
+ end
396
+ end
397
+ end
398
+ nil
399
+ end
400
+
401
+ # Issues a call to the org.freedesktop.DBus.Introspectable.Introspect method
402
+ # _dest_ is the service and _path_ the object path you want to introspect
403
+ # If a code block is given, the introspect call in asynchronous. If not
404
+ # data is returned
405
+ #
406
+ # FIXME: link to ProxyObject data definition
407
+ # The returned object is a ProxyObject that has methods you can call to
408
+ # issue somme METHOD_CALL messages, and to setup to receive METHOD_RETURN
409
+ def introspect(dest, path)
410
+ if not block_given?
411
+ # introspect in synchronous !
412
+ data = introspect_data(dest, path)
413
+ pof = DBus::ProxyObjectFactory.new(data, self, dest, path)
414
+ return pof.build
415
+ else
416
+ introspect_data(dest, path) do |data|
417
+ yield(DBus::ProxyObjectFactory.new(data, self, dest, path).build)
418
+ end
419
+ end
420
+ end
421
+
422
+ # Exception raised when a service name is requested that is not available.
423
+ class NameRequestError < Exception
424
+ end
425
+
426
+ # Attempt to request a service _name_.
427
+ def request_service(name)
428
+ r = proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING)
429
+ raise NameRequestError if r[0] != REQUEST_NAME_REPLY_PRIMARY_OWNER
430
+ @service = Service.new(name, self)
431
+ @service
432
+ end
433
+
434
+ # Set up a ProxyObject for the bus itself, since the bus is introspectable.
435
+ # Returns the object.
436
+ def proxy
437
+ if @proxy == nil
438
+ path = "/org/freedesktop/DBus"
439
+ dest = "org.freedesktop.DBus"
440
+ pof = DBus::ProxyObjectFactory.new(DBUSXMLINTRO, self, dest, path)
441
+ @proxy = pof.build["org.freedesktop.DBus"]
442
+ end
443
+ @proxy
444
+ end
445
+
446
+ # Fill (append) the buffer from data that might be available on the
447
+ # socket.
448
+ def update_buffer
449
+ @buffer += @socket.read_nonblock(MSG_BUF_SIZE) unless @socket.nil?
450
+ end
451
+
452
+ # Get one message from the bus and remove it from the buffer.
453
+ # Return the message.
454
+ def pop_message
455
+ ret = nil
456
+ begin
457
+ ret, size = Message.new.unmarshall_buffer(@buffer)
458
+ @buffer.slice!(0, size)
459
+ rescue IncompleteBufferException => e
460
+ # fall through, let ret be null
461
+ end
462
+ ret
463
+ end
464
+
465
+ # Retrieve all the messages that are currently in the buffer.
466
+ def messages
467
+ ret = Array.new
468
+ while msg = pop_message
469
+ ret << msg
470
+ end
471
+ ret
472
+ end
473
+
474
+ # The buffer size for messages.
475
+ MSG_BUF_SIZE = 4096
476
+
477
+ # Update the buffer and retrieve all messages using Connection#messages.
478
+ # Return the messages.
479
+ def poll_messages
480
+ ret = nil
481
+ r, d, d = IO.select([@socket], nil, nil, 0)
482
+ if r and r.size > 0
483
+ update_buffer
484
+ end
485
+ messages
486
+ end
487
+
488
+ # Wait for a message to arrive. Return it once it is available.
489
+ def wait_for_message
490
+ if @socket.nil?
491
+ elog "Can't wait for messages, @socket is nil."
492
+ return
493
+ end
494
+ ret = pop_message
495
+ while ret == nil
496
+ r, d, d = IO.select([@socket])
497
+ if r and r[0] == @socket
498
+ update_buffer
499
+ ret = pop_message
500
+ end
501
+ end
502
+ ret
503
+ end
504
+
505
+ # Send a message _m_ on to the bus. This is done synchronously, thus
506
+ # the call will block until a reply message arrives.
507
+ def send_sync(m, &retc) # :yields: reply/return message
508
+ return if m.nil? #check if somethings wrong
509
+ send(m.marshall)
510
+ @method_call_msgs[m.serial] = m
511
+ @method_call_replies[m.serial] = retc
512
+
513
+ retm = wait_for_message
514
+
515
+ return if retm.nil? #check if somethings wrong
516
+
517
+ process(retm)
518
+ until [DBus::Message::ERROR,
519
+ DBus::Message::METHOD_RETURN].include?(retm.message_type) and
520
+ retm.reply_serial == m.serial
521
+ retm = wait_for_message
522
+ process(retm)
523
+ end
524
+ end
525
+
526
+ # Specify a code block that has to be executed when a reply for
527
+ # message _m_ is received.
528
+ def on_return(m, &retc)
529
+ # Have a better exception here
530
+ if m.message_type != Message::METHOD_CALL
531
+ elog "Funky exception, occured."
532
+ raise "on_return should only get method_calls"
533
+ end
534
+ @method_call_msgs[m.serial] = m
535
+ @method_call_replies[m.serial] = retc
536
+ end
537
+
538
+ # Asks bus to send us messages matching mr, and execute slot when
539
+ # received
540
+ def add_match(mr, &slot)
541
+ # check this is a signal.
542
+ @signal_matchrules << [mr, slot]
543
+ self.proxy.AddMatch(mr.to_s)
544
+ end
545
+
546
+ # Process a message _m_ based on its type.
547
+ def process(m)
548
+ return if m.nil? #check if somethings wrong
549
+ case m.message_type
550
+ when Message::ERROR, Message::METHOD_RETURN
551
+ raise InvalidPacketException if m.reply_serial == nil
552
+ mcs = @method_call_replies[m.reply_serial]
553
+ if not mcs
554
+ dlog "no return code for mcs: #{mcs.inspect} m: #{m.inspect}"
555
+ else
556
+ if m.message_type == Message::ERROR
557
+ mcs.call(Error.new(m))
558
+ else
559
+ mcs.call(m)
560
+ end
561
+ @method_call_replies.delete(m.reply_serial)
562
+ @method_call_msgs.delete(m.reply_serial)
563
+ end
564
+ when DBus::Message::METHOD_CALL
565
+ if m.path == "/org/freedesktop/DBus"
566
+ dlog "Got method call on /org/freedesktop/DBus"
567
+ end
568
+ # handle introspectable as an exception:
569
+ if m.interface == "org.freedesktop.DBus.Introspectable" and
570
+ m.member == "Introspect"
571
+ reply = Message.new(Message::METHOD_RETURN).reply_to(m)
572
+ reply.sender = @unique_name
573
+ node = @service.get_node(m.path)
574
+ raise NotImplementedError if not node
575
+ reply.sender = @unique_name
576
+ reply.add_param(Type::STRING, @service.get_node(m.path).to_xml)
577
+ send(reply.marshall)
578
+ else
579
+ node = @service.get_node(m.path)
580
+ return if node.nil?
581
+ obj = node.object
582
+ return if obj.nil?
583
+ obj.dispatch(m) if obj
584
+ end
585
+ when DBus::Message::SIGNAL
586
+ @signal_matchrules.each do |elem|
587
+ mr, slot = elem
588
+ if mr.match(m)
589
+ slot.call(m)
590
+ return
591
+ end
592
+ end
593
+ else
594
+ dlog "Unknown message type: #{m.message_type}"
595
+ end
596
+ end
597
+
598
+ # Retrieves the service with the given _name_.
599
+ def service(name)
600
+ # The service might not exist at this time so we cannot really check
601
+ # anything
602
+ Service.new(name, self)
603
+ end
604
+ alias :[] :service
605
+
606
+ # Emit a signal event for the given _service_, object _obj_, interface
607
+ # _intf_ and signal _sig_ with arguments _args_.
608
+ def emit(service, obj, intf, sig, *args)
609
+ m = Message.new(DBus::Message::SIGNAL)
610
+ m.path = obj.path
611
+ m.interface = intf.name
612
+ m.member = sig.name
613
+ m.sender = service.name
614
+ i = 0
615
+ sig.params.each do |par|
616
+ m.add_param(par[1], args[i])
617
+ i += 1
618
+ end
619
+ send(m.marshall)
620
+ end
621
+
622
+ ###########################################################################
623
+ private
624
+
625
+ # Send a hello messages to the bus to let it know we are here.
626
+ def send_hello
627
+ m = Message.new(DBus::Message::METHOD_CALL)
628
+ m.path = "/org/freedesktop/DBus"
629
+ m.destination = "org.freedesktop.DBus"
630
+ m.interface = "org.freedesktop.DBus"
631
+ m.member = "Hello"
632
+ send_sync(m) do |rmsg|
633
+ @unique_name = rmsg.destination
634
+ dlog "Got hello reply. Our unique_name is #{@unique_name}, i feel special."
635
+ end
636
+ end
637
+
638
+ # Initialize the connection to the bus.
639
+ def init_connection
640
+ @client = Client.new(@socket)
641
+ @client.authenticate
642
+ end
643
+ end # class Connection
644
+
645
+ # = D-Bus session bus class
646
+ #
647
+ # The session bus is a session specific bus (mostly for desktop use).
648
+ # This is a singleton class.
649
+ class SessionBus < Connection
650
+ include Singleton
651
+
652
+ # Get the the default session bus.
653
+ def initialize socket_name=SessionSocketName
654
+ super(socket_name)
655
+ connect
656
+ send_hello
657
+ end
658
+ end
659
+
660
+ # = D-Bus system bus class
661
+ #
662
+ # The system bus is a system-wide bus mostly used for global or
663
+ # system usages. This is a singleton class.
664
+ class SystemBus < Connection
665
+ include Singleton
666
+
667
+ # Get the default system bus.
668
+ def initialize socket_name=SystemSocketName
669
+ super(socket_name)
670
+ connect
671
+ send_hello
672
+ end
673
+ end
674
+
675
+ # FIXME: we should get rid of these singeltons
676
+
677
+ def DBus.system_bus
678
+ SystemBus.instance
679
+ end
680
+
681
+ def DBus.session_bus
682
+ SessionBus.instance
683
+ end
684
+
685
+ # = Main event loop class.
686
+ #
687
+ # Class that takes care of handling message and signal events
688
+ # asynchronously. *Note:* This is a native implement and therefore does
689
+ # not integrate with a graphical widget set main loop.
690
+ class Main
691
+ # Create a new main event loop.
692
+ def initialize
693
+ @buses = Hash.new
694
+ end
695
+
696
+ # Add a _bus_ to the list of buses to watch for events.
697
+ def <<(bus)
698
+ @buses[bus.socket] = bus
699
+ end
700
+
701
+ # Run the main loop. This is a blocking call!
702
+ def run
703
+ loop do
704
+ ready, dum, dum = IO.select(@buses.keys)
705
+ ready.each do |socket|
706
+ b = @buses[socket]
707
+ begin
708
+ b.update_buffer
709
+ rescue EOFError
710
+ return # the bus died
711
+ end
712
+ while m = b.pop_message
713
+ b.process(m)
714
+ end
715
+ end
716
+ end
717
+ end
718
+ end # class Main
719
+ end # module DBus