1 module ddbus.router; 2 3 import ddbus.thin; 4 import ddbus.c_lib; 5 import ddbus.util; 6 import std..string; 7 import std.typecons; 8 import core.memory; 9 import std.array; 10 import std.algorithm; 11 import std.format; 12 13 struct MessagePattern { 14 ObjectPath path; 15 InterfaceName iface; 16 string method; 17 bool signal; 18 19 this(Message msg) { 20 path = msg.path(); 21 iface = msg.iface(); 22 method = msg.member(); 23 signal = (msg.type() == MessageType.Signal); 24 } 25 26 deprecated("Use the constructor taking a ObjectPath and InterfaceName instead") 27 this(string path, string iface, string method, bool signal = false) { 28 this(ObjectPath(path), interfaceName(iface), method, signal); 29 } 30 31 this(ObjectPath path, InterfaceName iface, string method, bool signal = false) { 32 this.path = path; 33 this.iface = iface; 34 this.method = method; 35 this.signal = signal; 36 } 37 38 size_t toHash() const @safe nothrow { 39 size_t hash = 0; 40 auto stringHash = &(typeid(path).getHash); 41 hash += stringHash(&path); 42 hash += stringHash(&iface); 43 hash += stringHash(&method); 44 hash += (signal ? 1 : 0); 45 return hash; 46 } 47 48 bool opEquals(ref const typeof(this) s) const @safe pure nothrow { 49 return (path == s.path) && (iface == s.iface) && (method == s.method) && (signal == s.signal); 50 } 51 } 52 53 unittest { 54 import dunit.toolkit; 55 56 auto msg = Message(busName("org.example.test"), ObjectPath("/test"), interfaceName("org.example.testing"), "testMethod"); 57 auto patt = new MessagePattern(msg); 58 patt.assertEqual(patt); 59 patt.signal.assertFalse(); 60 patt.path.assertEqual("/test"); 61 } 62 63 struct MessageHandler { 64 alias HandlerFunc = void delegate(Message call, Connection conn); 65 HandlerFunc func; 66 string[] argSig; 67 string[] retSig; 68 } 69 70 class MessageRouter { 71 MessageHandler[MessagePattern] callTable; 72 73 bool handle(Message msg, Connection conn) { 74 MessageType type = msg.type(); 75 if (type != MessageType.Call && type != MessageType.Signal) { 76 return false; 77 } 78 79 auto pattern = MessagePattern(msg); 80 // import std.stdio; debug writeln("Handling ", pattern); 81 82 if (pattern.iface == "org.freedesktop.DBus.Introspectable" 83 && pattern.method == "Introspect" && !pattern.signal) { 84 handleIntrospect(pattern.path, msg, conn); 85 return true; 86 } 87 88 MessageHandler* handler = (pattern in callTable); 89 if (handler is null) { 90 return false; 91 } 92 93 // Check for matching argument types 94 version (DDBusNoChecking) { 95 96 } else { 97 if (!equal(join(handler.argSig), msg.signature())) { 98 return false; 99 } 100 } 101 102 handler.func(msg, conn); 103 return true; 104 } 105 106 void setHandler(Ret, Args...)(MessagePattern patt, Ret delegate(Args) handler) { 107 void handlerWrapper(Message call, Connection conn) { 108 Tuple!Args args = call.readTuple!(Tuple!Args)(); 109 auto retMsg = call.createReturn(); 110 111 static if (!is(Ret == void)) { 112 Ret ret = handler(args.expand); 113 static if (is(Ret == Tuple!T, T...)) { 114 retMsg.build!T(ret.expand); 115 } else { 116 retMsg.build(ret); 117 } 118 } else { 119 handler(args.expand); 120 } 121 122 if (!patt.signal) { 123 conn.send(retMsg); 124 } 125 } 126 127 static string[] args = typeSigArr!Args; 128 129 static if (is(Ret == void)) { 130 static string[] ret = []; 131 } else { 132 static string[] ret = typeSigReturn!Ret; 133 } 134 135 // dfmt off 136 MessageHandler handleStruct = { 137 func: &handlerWrapper, 138 argSig: args, 139 retSig: ret 140 }; 141 // dfmt on 142 143 callTable[patt] = handleStruct; 144 } 145 146 static string introspectHeader = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> 147 <node name="%s">`; 148 149 deprecated("Use introspectXML(ObjectPath path) instead") 150 string introspectXML(string path) { 151 return introspectXML(ObjectPath(path)); 152 } 153 154 string introspectXML(ObjectPath path) { 155 // dfmt off 156 auto methods = callTable 157 .byKey() 158 .filter!(a => (a.path == path) && !a.signal) 159 .array 160 .sort!((a, b) => a.iface < b.iface)(); 161 // dfmt on 162 163 auto ifaces = methods.groupBy(); 164 auto app = appender!string; 165 formattedWrite(app, introspectHeader, path); 166 foreach (iface; ifaces) { 167 formattedWrite(app, `<interface name="%s">`, cast(string)iface.front.iface); 168 169 foreach (methodPatt; iface.array()) { 170 formattedWrite(app, `<method name="%s">`, methodPatt.method); 171 auto handler = callTable[methodPatt]; 172 173 foreach (arg; handler.argSig) { 174 formattedWrite(app, `<arg type="%s" direction="in"/>`, arg); 175 } 176 177 foreach (arg; handler.retSig) { 178 formattedWrite(app, `<arg type="%s" direction="out"/>`, arg); 179 } 180 181 app.put("</method>"); 182 } 183 184 app.put("</interface>"); 185 } 186 187 auto childPath = path; 188 189 auto children = callTable.byKey().filter!(a => a.path.startsWith(childPath) 190 && a.path != childPath && !a.signal)().map!((s) => s.path.chompPrefix(childPath)) 191 .map!((s) => s.value[1 .. $].findSplit("/")[0]) 192 .array().sort().uniq(); 193 194 foreach (child; children) { 195 formattedWrite(app, `<node name="%s"/>`, child); 196 } 197 198 app.put("</node>"); 199 return app.data; 200 } 201 202 deprecated("Use the method taking an ObjectPath instead") 203 void handleIntrospect(string path, Message call, Connection conn) { 204 handleIntrospect(ObjectPath(path), call, conn); 205 } 206 207 void handleIntrospect(ObjectPath path, Message call, Connection conn) { 208 auto retMsg = call.createReturn(); 209 retMsg.build(introspectXML(path)); 210 conn.sendBlocking(retMsg); 211 } 212 } 213 214 extern (C) private DBusHandlerResult filterFunc(DBusConnection* dConn, 215 DBusMessage* dMsg, void* routerP) { 216 MessageRouter router = cast(MessageRouter) routerP; 217 dbus_message_ref(dMsg); 218 Message msg = Message(dMsg); 219 dbus_connection_ref(dConn); 220 Connection conn = Connection(dConn); 221 bool handled = router.handle(msg, conn); 222 223 if (handled) { 224 return DBusHandlerResult.DBUS_HANDLER_RESULT_HANDLED; 225 } else { 226 return DBusHandlerResult.DBUS_HANDLER_RESULT_NOT_YET_HANDLED; 227 } 228 } 229 230 extern (C) private void unrootUserData(void* userdata) { 231 GC.removeRoot(userdata); 232 } 233 234 void registerRouter(Connection conn, MessageRouter router) { 235 void* routerP = cast(void*) router; 236 GC.addRoot(routerP); 237 dbus_connection_add_filter(conn.conn, &filterFunc, routerP, &unrootUserData); 238 } 239 240 unittest { 241 import dunit.toolkit; 242 243 import std.typecons : BitFlags; 244 import std.variant : Algebraic; 245 246 auto router = new MessageRouter(); 247 // set up test messages 248 MessagePattern patt = MessagePattern(ObjectPath("/root"), interfaceName("ca.thume.test"), "test"); 249 router.setHandler!(int, int)(patt, (int p) { return 6; }); 250 patt = MessagePattern(ObjectPath("/root"), interfaceName("ca.thume.tester"), "lolwut"); 251 router.setHandler!(void, int, string)(patt, (int p, string p2) { }); 252 patt = MessagePattern(ObjectPath("/root/wat"), interfaceName("ca.thume.tester"), "lolwut"); 253 router.setHandler!(int, int)(patt, (int p) { return 6; }); 254 patt = MessagePattern(ObjectPath("/root/bar"), interfaceName("ca.thume.tester"), "lolwut"); 255 router.setHandler!(Variant!DBusAny, int)(patt, (int p) { 256 return variant(DBusAny(p)); 257 }); 258 patt = MessagePattern(ObjectPath("/root/foo"), interfaceName("ca.thume.tester"), "lolwut"); 259 router.setHandler!(Tuple!(string, string, int), int, 260 Variant!DBusAny)(patt, (int p, Variant!DBusAny any) { 261 Tuple!(string, string, int) ret; 262 ret[0] = "a"; 263 ret[1] = "b"; 264 ret[2] = p; 265 return ret; 266 }); 267 patt = MessagePattern(ObjectPath("/troll"), interfaceName("ca.thume.tester"), "wow"); 268 router.setHandler!(void)(patt, { return; }); 269 270 patt = MessagePattern(ObjectPath("/root/fancy"), interfaceName("ca.thume.tester"), "crazyTest"); 271 enum F : ushort { 272 a = 1, 273 b = 8, 274 c = 16 275 } 276 277 struct S { 278 ubyte b; 279 ulong ul; 280 F f; 281 } 282 283 router.setHandler!(int)(patt, (Algebraic!(ushort, BitFlags!F, S) v) { 284 if (v.type is typeid(ushort) || v.type is typeid(BitFlags!F)) { 285 return v.coerce!int; 286 } else if (v.type is typeid(S)) { 287 auto s = v.get!S; 288 final switch (s.f) { 289 case F.a: 290 return s.b; 291 case F.b: 292 return cast(int) s.ul; 293 case F.c: 294 return cast(int) s.ul + s.b; 295 } 296 } 297 298 assert(false); 299 }); 300 301 static assert(!__traits(compiles, { 302 patt = MessagePattern("/root/bar", "ca.thume.tester", "lolwut"); 303 router.setHandler!(void, DBusAny)(patt, (DBusAny wrongUsage) { return; }); 304 })); 305 306 // TODO: these tests rely on nondeterministic hash map ordering 307 static string introspectResult = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> 308 <node name="/root"><interface name="ca.thume.test"><method name="test"><arg type="i" direction="in"/><arg type="i" direction="out"/></method></interface><interface name="ca.thume.tester"><method name="lolwut"><arg type="i" direction="in"/><arg type="s" direction="in"/></method></interface><node name="bar"/><node name="fancy"/><node name="foo"/><node name="wat"/></node>`; 309 router.introspectXML(ObjectPath("/root")).assertEqual(introspectResult); 310 static string introspectResult2 = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> 311 <node name="/root/foo"><interface name="ca.thume.tester"><method name="lolwut"><arg type="i" direction="in"/><arg type="v" direction="in"/><arg type="s" direction="out"/><arg type="s" direction="out"/><arg type="i" direction="out"/></method></interface></node>`; 312 router.introspectXML(ObjectPath("/root/foo")).assertEqual(introspectResult2); 313 static string introspectResult3 = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> 314 <node name="/root/fancy"><interface name="ca.thume.tester"><method name="crazyTest"><arg type="v" direction="in"/><arg type="i" direction="out"/></method></interface></node>`; 315 router.introspectXML(ObjectPath("/root/fancy")).assertEqual(introspectResult3); 316 router.introspectXML(ObjectPath("/")) 317 .assertEndsWith(`<node name="/"><node name="root"/><node name="troll"/></node>`); 318 }