'From etoys3.0 of 7 March 2008 [latest update: #2020] on 19 June 2008 at 9:52:49 pm'! "Change Set: DBus-Objects-bf-2 Date: 19 June 2008 Author: Bert Freudenberg Name: DBus-Objects-bf.2 Author: bf Time: 19 June 2008, 9:30:07 pm UUID: 70c0b43f-2b78-4eed-92fd-34d7237562d3 Ancestors: DBus-Objects-bf.1 first real version: - add mainloop - add reply handlers and matches - export DBusObjects as service on the DBus - signal DBusError when receiving error"! DBusConnection subclass: #DBus instanceVariableNames: 'exported imported process accessLock replyHandlers matchHandlers ' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'! !DBus commentStamp: 'bf 6/11/2008 17:14' prior: 0! I am a DBus connection. I retrieve messages in a background loop. Messages can be sent through me synchronously (that is, they block until a result comes in). I can export objects on the DBus to provide services for other applications. FIXME: startup should only happen when I am used first, not at image startup time (DBus sessionBus getObject: 'org.gnome.ScreenSaver' path: '/') SetActive: true! Error subclass: #DBusError instanceVariableNames: '' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'! Object subclass: #DBusHandler instanceVariableNames: 'onSuccess onError' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'! Object subclass: #DBusMatch instanceVariableNames: 'matches argMatches' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'! Object subclass: #DBusMethod instanceVariableNames: 'interface selector inSignature outSignature member ' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'! !DBusObject commentStamp: 'bf 5/1/2008 20:25' prior: 0! I am an object exposed on the DBus. Methods that are exported on the DBus are marked in the source using dbusMethod: and a string giving the DBus interface, selector, and signatures ('interface.methodoutSignature'). Most of my methods are prefixed with 'dbus' to minimize selector space polution for subclasses. ! !DBus methodsFor: 'exporting' stamp: 'bf 6/19/2008 14:12'! exportedAt: aPathString ^exported ifNotNil: [exported dbusChildAtPath: (aPathString findTokens: '/')].! ! !DBus methodsFor: 'importing' stamp: 'bf 4/30/2008 18:11'! get: aProxyClass ^aProxyClass new setConnection: self busName: aProxyClass dbusName objectPath: aProxyClass dbusPath! ! !DBus methodsFor: 'mainloop' stamp: 'bf 6/19/2008 14:13'! dispatchMessage: msg | object | object := self exportedAt: msg path. object ifNil: [ ^msg isMethodCall ifTrue: [ self sendMessage: (DBusMessageError newFor: msg name: DBusMessageError dbusErrorUnknownObject withMessage: 'Unknown object ', msg path)]]. object dbusHandle: msg from: self! ! !DBus methodsFor: 'mainloop' stamp: 'bf 6/19/2008 14:46'! handleMessage: msg self logDebug: self asString, ' received ', msg asString. [accessLock critical: [ matchHandlers keysAndValuesDo: [:match :handler | (match matches: msg) ifTrue: [handler valueWithArguments: {msg}]]. msg isReplyOrError ifTrue: [(replyHandlers removeKey: msg replySerial ifAbsent: []) ifNotNilDo: [:handler | handler handleReplyOrError: msg]] ifFalse: [self dispatchMessage: msg]] ] forkNamed: self printString, ' handler ', msg serial asString! ! !DBus methodsFor: 'mainloop' stamp: 'bf 6/19/2008 13:55'! mainloop [true] whileTrue: [self processMessages. readSemaphore wait] ! ! !DBus methodsFor: 'mainloop' stamp: 'bf 6/19/2008 13:55'! processMessages [self dataRemains] whileTrue: [self popMessage ifNotNilDo: [:msg | self handleMessage: msg]]. ! ! !DBus methodsFor: 'mainloop' stamp: 'bf 6/19/2008 13:53'! restartMainloop process ifNotNil: [process terminate]. process := [self mainloop] forkAt: Processor userInterruptPriority named: self printString, ' mainloop'! ! !DBus methodsFor: 'logging' stamp: 'bf 5/1/2008 16:43'! logDebug: aString WorldState addDeferredUIMessage: [Transcript cr; show: aString].! ! !DBus methodsFor: 'logging' stamp: 'bf 5/1/2008 16:39'! logWarning: aString WorldState addDeferredUIMessage: [Transcript cr; show: aString].! ! !DBus methodsFor: 'matching' stamp: 'bf 6/19/2008 14:52'! onMatch: aDBusMatch do: aBlockOrMessageSend accessLock critical: [ self primAddMatch: aDBusMatch matchString squeakToUtf8. matchHandlers at: aDBusMatch put: aBlockOrMessageSend]! ! !DBus methodsFor: 'matching' stamp: 'bf 6/19/2008 14:53'! removeMatch: aDBusMatch accessLock critical: [ self primRemoveMatch: aDBusMatch matchString squeakToUtf8. matchHandlers removeKey: aDBusMatch ifAbsent: []]! ! !DBus methodsFor: 'sending' stamp: 'bf 5/26/2008 17:42'! sendDBusMessage: aMessage "send aMessage synchronously, wait until a response comes back. Answer reply message, or raise an error if send failed or timed out" ^self sendDBusMessage: aMessage timeout: -1! ! !DBus methodsFor: 'sending' stamp: 'bf 6/19/2008 12:14'! sendDBusMessage: aMessage timeout: seconds "send aMessage synchronously, wait until a response comes back. Answer reply message, or raise an error if send failed or timed out" | sema reply | sema := Semaphore new. self sendDBusMessage: aMessage timeout: seconds onSuccess: [:msg | reply := msg. sema signal] onError: [: msg | reply := msg. sema signal]. sema wait. reply isError ifTrue: [^DBusError signal: reply message]. ^reply! ! !DBus methodsFor: 'sending' stamp: 'bf 6/19/2008 14:11'! sendDBusMessage: aMessage timeout: seconds onSuccess: successHandler onError: errorHandler "send aMessage asynchronously, evaluating successHandler or errorHandler when a response or error comes in, or the timeout is over." | replyCode | accessLock critical: [ replyCode := self sendMessage: aMessage timeout: seconds. replyHandlers at: replyCode put: (DBusHandler onSuccess: successHandler onError: errorHandler) ]. ! ! !DBus methodsFor: 'private' stamp: 'bf 6/12/2008 16:25'! sendMessage: msg timeout: seconds "Low-level send. Use sendDBusMessage:timeout: instead!!" self logDebug: self asString, ' sent ', msg asString. ^super sendMessage: msg timeout: seconds! ! !DBus methodsFor: 'initialize-release' stamp: 'bf 6/19/2008 14:28'! startUp exported := nil. replyHandlers := Dictionary new. matchHandlers := Dictionary new. accessLock := Semaphore forMutualExclusion. self restartMainloop. super startUp. ! ! !DBusArgument methodsFor: '*dbus-objects' stamp: 'bf 5/1/2008 20:26'! fromDBusArgument: sender "XXX refactor into subclasses" self isBasicType ifTrue: [ type = DBusArgument objectPath ifTrue: [^sender dbusObjectForPath: value]. ^value]. ^type caseOf: { [DBusArgument struct] -> [value collect: [:ea | ea fromDBusArgument: sender]]. [DBusArgument dictEntry] -> [(value key fromDBusArgument: sender) -> (value value fromDBusArgument: sender)]. [DBusArgument array] -> [| arr | signature = 'ay' ifTrue: [^value]. arr := value collect: [:ea | ea fromDBusArgument: sender]. self containedSignature first =${ ifTrue: [arr as: Dictionary] ifFalse: [arr]]. [DBusArgument variant] -> [value fromDBusArgument: sender] } ! ! !DBusHandler methodsFor: 'processing' stamp: 'bf 6/18/2008 16:20'! handleReplyOrError: dbusReplyOrError (dbusReplyOrError isError ifTrue: [onError] ifFalse: [onSuccess]) valueWithArguments: {dbusReplyOrError} "#valueWithArguments: works for both Blocks and MessageSends"! ! !DBusHandler methodsFor: 'accessing' stamp: 'bf 6/10/2008 16:22'! onError "Answer the value of onError" ^ onError! ! !DBusHandler methodsFor: 'accessing' stamp: 'bf 6/18/2008 16:19'! onError: aBlockOrMessageSend onError := aBlockOrMessageSend! ! !DBusHandler methodsFor: 'accessing' stamp: 'bf 6/10/2008 16:22'! onSuccess "Answer the value of onSuccess" ^ onSuccess! ! !DBusHandler methodsFor: 'accessing' stamp: 'bf 6/18/2008 16:20'! onSuccess: aBlockOrMessageSend onSuccess := aBlockOrMessageSend! ! !DBusHandler class methodsFor: 'instance creation' stamp: 'bf 6/10/2008 17:34'! onSuccess: successHandler onError: errorHandler ^self new onSuccess: successHandler; onError: errorHandler; yourself! ! !DBusMatch methodsFor: 'private' stamp: 'bf 6/19/2008 14:25'! argMatches ^argMatches! ! !DBusMatch methodsFor: 'private' stamp: 'bf 6/19/2008 14:25'! matches ^matches! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 19:46'! arg: index is: anObject argMatches at: index put: anObject asString! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 18:39'! destination: aString matches at: #destination put: aString! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 18:38'! interface: aString matches at: #interface put: aString! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 18:38'! member: aString matches at: #member put: aString! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 18:39'! path: aString matches at: #path put: aString! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 18:37'! sender: aString matches at: #sender put: aString! ! !DBusMatch methodsFor: 'setting' stamp: 'bf 6/18/2008 19:53'! type: aString "One of 'signal', 'method_call', 'method_return', 'error'. Defaults to signal" matches at: #type put: aString! ! !DBusMatch methodsFor: 'setting-args' stamp: 'bf 6/18/2008 19:48'! firstArg: anObject self arg: 0 is: anObject! ! !DBusMatch methodsFor: 'setting-args' stamp: 'bf 6/18/2008 19:48'! secondArg: anObject self arg: 1 is: anObject! ! !DBusMatch methodsFor: 'setting-args' stamp: 'bf 6/18/2008 19:48'! thirdArg: anObject self arg: 2 is: anObject! ! !DBusMatch methodsFor: 'comparing' stamp: 'bf 6/19/2008 14:23'! hash ^matches hash bitXor: argMatches hash! ! !DBusMatch methodsFor: 'comparing' stamp: 'bf 6/19/2008 15:04'! = other (other isKindOf: self class) ifFalse: [^false]. ^other matches = matches and: [other argMatches = argMatches]! ! !DBusMatch methodsFor: 'initialize' stamp: 'bf 6/18/2008 22:06'! initialize matches := Dictionary new. argMatches := Dictionary new. self typeSignal! ! !DBusMatch methodsFor: 'matching' stamp: 'bf 6/18/2008 22:36'! matches: aDBusMessage matches keysAndValuesDo: [:selector :value | (aDBusMessage perform: selector) = value ifFalse: [^false]]. argMatches keysAndValuesDo: [:index :value | (aDBusMessage arguments at: index+1 ifAbsent: [^false]) fromDBusArgument asString = value ifFalse: [^false]]. ^true! ! !DBusMatch methodsFor: 'printing' stamp: 'bf 6/18/2008 22:23'! matchString ^String streamContents: [:stream | self printMatchStringOn: stream] ! ! !DBusMatch methodsFor: 'printing' stamp: 'bf 6/18/2008 22:16'! printMatchStringOn: aStream | first | first := true. matches keysAndValuesDo: [:selector :string | first ifTrue: [first := false] ifFalse: [aStream nextPut: $,]. aStream nextPutAll: selector; nextPut: $=; print: string]. argMatches keysAndValuesDo: [:index :string | first ifTrue: [first := false] ifFalse: [aStream nextPut: $,]. aStream nextPutAll: 'arg'; print: index; nextPut: $=; print: string]. ! ! !DBusMatch methodsFor: 'printing' stamp: 'bf 6/18/2008 22:24'! printOn: aStream aStream nextPutAll: self class name. aStream nextPut: $(. self printMatchStringOn: aStream. aStream nextPut: $) ! ! !DBusMatch methodsFor: 'setting-types' stamp: 'bf 6/19/2008 01:45'! typeError self type: DBusMessageError type! ! !DBusMatch methodsFor: 'setting-types' stamp: 'bf 6/19/2008 01:46'! typeMethodCall self type: DBusMessageMethodCall type! ! !DBusMatch methodsFor: 'setting-types' stamp: 'bf 6/19/2008 01:46'! typeMethodReturn self type: DBusMessageMethodReply type! ! !DBusMatch methodsFor: 'setting-types' stamp: 'bf 6/19/2008 01:48'! typeSignal self type: DBusMessageSignal type! ! !DBusMethod methodsFor: 'accessing' stamp: 'bf 6/19/2008 01:16'! fullSelector ^interface, '.', member! ! !DBusMethod methodsFor: 'accessing' stamp: 'bf 6/19/2008 01:16'! member ^member! ! !DBusMethod methodsFor: 'initialize-release' stamp: 'bf 6/19/2008 01:17'! initializeFromSpecString: aString | p | interface := aString copyUpToLast: $.. member := aString copyFrom: interface size+2 to: (p := aString indexOf: $<)-1. inSignature := aString copyFrom: p+1 to: (p := aString indexOf: $>)-1. outSignature := aString copyFrom: p+1 to: aString size.! ! !DBusMethod methodsFor: 'initialize-release' stamp: 'bf 6/19/2008 01:17'! setInterface: anInterface member: aMember in: anInSignature out: anOutSignature interface := anInterface. member := aMember. inSignature := anInSignature. outSignature := anOutSignature. ! ! !DBusMethod methodsFor: 'printing' stamp: 'bf 6/19/2008 01:18'! printOn: aStream aStream nextPutAll: interface; nextPut: $.; nextPutAll: member; nextPut: $<; nextPutAll: inSignature; nextPut: $>; nextPutAll: outSignature! ! !DBusMethod methodsFor: 'printing' stamp: 'bf 6/19/2008 01:19'! printXMLOn: xml xml startTag: 'method'; attribute: 'name' value: member; endTag. #('in' 'out') with: {inSignature. outSignature} do: [:direction :signature | DBusArgument signaturesIn: signature do: [:type | xml startTag: 'arg'; attribute: 'direction' value: direction; attribute: 'type' value: type asString; endEmptyTag: 'arg']]. xml endTag: 'method'.! ! !DBusMethod class methodsFor: 'instance creation' stamp: 'bf 6/19/2008 01:18'! interface: anInterface member: aMember in: anInSignature out: anOutSignature ^self new setInterface: anInterface member: aMember in: anInSignature out: anOutSignature! ! !DBusObject methodsFor: 'accessing' stamp: 'bf 6/11/2008 17:30'! dbusChildAtPath: aPathArray ^aPathArray size = 0 ifTrue: [self] ifFalse: [(self dbusChildren at: aPathArray first ifAbsent: [^nil]) dbusChildAtPath: aPathArray allButFirst]! ! !DBusObject methodsFor: 'accessing' stamp: 'bf 6/11/2008 17:29'! dbusChildren ^dbusChildren! ! !DBusObject methodsFor: 'accessing' stamp: 'bf 6/18/2008 16:37'! dbusCoerceTo: type ^self dbusPath! ! !DBusObject methodsFor: 'accessing' stamp: 'bf 6/17/2008 13:01'! dbusMethodSpecsAndSelectorsDo: aBlock "Evaluate aBlock with all method specs and corresponding selectors that are to be exposed on the DBus. By default, this gathers only methods marked as #dbusMethod:." | cls | cls := self class. [cls selectorsAndMethodsDo: [:sel :meth | (meth numLiterals >= 2 and: [(meth literalAt: 1) == #dbusMethod:]) ifTrue: [aBlock value: (meth literalAt: 2) value: sel]]. cls == DBusObject] whileFalse: [cls := cls superclass]. ! ! !DBusObject methodsFor: 'accessing' stamp: 'bf 6/19/2008 01:16'! dbusMethods "Answer a Dictionary mapping DBus selectors to DBusMethod instances" | methods method | methods := Dictionary new. self dbusMethodSpecsAndSelectorsDo: [:spec :selector | method := DBusMethod fromSpecString: spec. methods at: method member put: method]. ^methods ! ! !DBusObject methodsFor: 'accessing' stamp: 'bf 6/19/2008 14:13'! dbusObjectForPath: aString ^(dbusConnection exportedAt: aString) ifNil: [aString]! ! !DBusObject methodsFor: 'handling' stamp: 'bf 6/19/2008 01:15'! dbusHandle: aMessage from: aDBus "aMessage for me was received on the D-Bus. Go through my dbusMethods and dispatch to the right one. Then collect the return values, pack them as DBusArguments and send a reply.". self dbusMethodSpecsAndSelectorsDo: [:spec :sel | (aMessage interface ifNil: [((spec copyUpTo: $<) copyAfterLast: $.) = aMessage member] ifNotNil: [(spec copyUpTo: $<) = aMessage fullSelector]) ifTrue: [^self dbusHandle: aMessage from: aDBus spec: spec selector: sel]]. "no method found" aMessage isMethodCall ifTrue: [ aDBus sendMessage: (DBusMessageError newFor: aMessage name: DBusMessageError dbusErrorUnknownMethod withMessage: 'Unknown method ', aMessage fullSelector, '()')]. ! ! !DBusObject methodsFor: 'handling' stamp: 'bf 6/17/2008 13:57'! dbusHandle: aMessage from: aDBus spec: aSpecString selector: aSelector ^self dbusHandle: aMessage from: aDBus spec: aSpecString selector: aSelector in: self! ! !DBusObject methodsFor: 'handling' stamp: 'bf 6/17/2008 14:06'! dbusHandle: aMessage from: aDBus spec: aSpecString selector: aSelector in: anObject | result | [ result := anObject perform: aSelector withArguments: (aMessage arguments collect: [:arg | arg fromDBusArgument: self]). aMessage reply ifTrue: [self dbusSendReply: result for: aMessage from: aDBus spec: aSpecString] ] on: Error do: [:error | aDBus sendMessage: (DBusMessageError newFor: aMessage name: 'org.squeak.error.', error class name withMessage: error messageText). error pass ]! ! !DBusObject methodsFor: 'handling' stamp: 'bf 6/17/2008 16:02'! dbusSendReply: anObject for: aMessage from: aDBus spec: aSpecString | reply signatures | reply := DBusMessage newReplyFor: aMessage. signatures := DBusArgument splitSignature: (aSpecString copyAfterLast: $>). signatures size = 1 ifTrue: [reply addArgument: (anObject asDBusArgumentSignature: signatures first)]. signatures size > 1 ifTrue: [reply with: signatures do: [:val :sig | reply addArgument: (val asDBusArgumentSignature: sig)]]. aDBus sendMessage: reply! ! !DBusObject methodsFor: 'dbus methods' stamp: 'bf 6/16/2008 19:18'! introspect self dbusMethod: 'org.freedesktop.DBus.Introspectable.Introspect<>s'. ^String streamContents: [:stream | | xml | stream nextPutAll: ''. xml := XMLWriter on: stream. xml startTag: 'node'; endTag. self dbusChildren do: [:child | xml startTag: 'node'; attribute: 'name' value: child dbusName; endEmptyTag: 'node']. self dbusInterfaces do: [:interface | interface = 'org.freedesktop.DBus.Introspectable' ifFalse: [ xml startTag: 'interface'; attribute: 'name' value: interface; endTag. self dbusInterface: interface methodsDo: [:method | method printXMLOn: xml]. xml endTag: 'interface' ]]. xml endTag: 'node']! ! !DBusProxy methodsFor: 'accessing' stamp: 'bf 6/18/2008 16:38'! dbusCoerceTo: type ^ self dbusPath! ! !DBusProxy methodsFor: 'accessing' stamp: 'bf 6/19/2008 01:18'! dbusMethods "Answer a Dictionary mapping DBus selectors to DBusMethod instances" | introspected types | introspected := Dictionary new. (XMLDOMParser parseDocumentFrom: self introspect readStream) elements first tagsNamed: #interface do: [:interface | interface tagsNamed: #method do: [:method | types := {'in' -> String new writeStream. 'out' -> String new writeStream} as: Dictionary. method tagsNamed: #arg do: [:arg | (types at: (arg attributeAt: 'direction')) nextPutAll: (arg attributeAt: 'type')]. introspected at: (method attributeAt: 'name') put: (DBusMethod interface: (interface attributeAt: 'name') member: (method attributeAt: 'name') in: (types at: 'in') contents out: (types at: 'out') contents)]]. ^introspected ! ! !DBusProxy methodsFor: 'accessing' stamp: 'bf 5/1/2008 20:27'! dbusObjectForPath: aPath ^DBusProxy connection: self dbusConnection busName: self dbusName objectPath: aPath! ! !DBusProxy methodsFor: 'calling' stamp: 'bf 6/19/2008 21:12'! dbusPerformAsync: memberString interface: interfaceString ^self dbusPerformAsync: memberString interface: interfaceString withArguments: #() ! ! !DBusProxy methodsFor: 'calling' stamp: 'bf 6/19/2008 21:14'! dbusPerformAsync: memberString interface: interfaceString withArguments: argumentArray | msg | msg := DBusMessageMethodCall destination: self dbusName path: self dbusPath interface: interfaceString member: memberString reply: false. argumentArray do: [:each | msg addArgument: each]. self dbusConnection sendMessage: msg. ^nil! ! !DBusProxy methodsFor: 'calling' stamp: 'bf 6/19/2008 01:12'! dbusPerform: memberString interface: interfaceString withArguments: argumentArray | msg reply | msg := DBusMessageMethodCall destination: self dbusName path: self dbusPath interface: interfaceString member: memberString. argumentArray do: [:each | msg addArgument: each]. reply := self dbusConnection sendDBusMessage: msg. reply arguments size = 0 ifTrue: [^nil]. reply arguments size = 1 ifTrue: [^reply arguments first fromDBusArgument: self]. ^reply arguments collect: [:each | each fromDBusArgument: self]! ! !DBusProxy methodsFor: 'printing' stamp: 'bf 6/19/2008 12:23'! printOn: aStream aStream print: self class; nextPut: $(; print: self dbusName; space; print: self dbusPath; nextPut: $)! ! DBusProxy removeSelector: #asDBusArgument! DBusProxy removeSelector: #dbusProxyForPath:! DBusObject removeSelector: #asDBusArgument! DBusMethod class removeSelector: #interface:selector:in:out:! DBusMethod removeSelector: #selector! DBusMethod removeSelector: #setInterface:selector:in:out:! Object subclass: #DBusMethod instanceVariableNames: 'interface member inSignature outSignature' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'! DBusConnection subclass: #DBus instanceVariableNames: 'exported process replyHandlers matchHandlers accessLock' classVariableNames: '' poolDictionaries: '' category: 'DBus-Objects'!