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