ObjectVirus allows very simple implementation of object centric debugging. We can infect given object by virus which will halt any message send.
Let's create new GhostBehaviour for this:
GHGhostBehaviour subclass: #GHObjectCallHalt
instanceVariableNames: ''
classVariableNames: ''
package: 'Ghost-ObjectCallHalt'
GHGhostBehaviour>>send: aMessage to: anObject
| method breakpoint |
anObject recoverFromVirus.
self halt.
^aMessage sendTo: anObject
First we disable interception behaviour to not halt any following messages (to get halt once logic). And then we halt message processing. It will open debugger on any message send. And to access target method "step into" button should be pressed few times in the debugger.
To activate such behaviour on objects we can create suitable method:
Object>>haltOnNextCall
| virus |
virus := GHObjectVirus behaviour: GHObjectCallHalt new.
virus infect: self
And now we can play with this:
point := 2@3.
point haltOnNextCall.
0@0 distanceTo: point
It will open debugger:
Of course we don't want to step into target method manually. We want to see target method just in the debugger. To achieve this we can set up breakpoint on target method and run it instead of halt message processing:
GHGhostBehaviour >>send: aMessage to: anObject
| method breakpoint |
anObject recoverFromVirus.
method := anObject class lookupSelector: aMessage selector.
breakpoint := Breakpoint new
node: method ast;
once.
breakpoint link condition: [ :receiver | receiver == anObject ] arguments: #(#object).
breakpoint install.
^aMessage sendTo: anObject
And with this change we will see what we want:
Debugger shows Point>>x method because #distanceTo: asks #x and #y of given point to compute distance:
Last thing which is not nice is that we see all message interception logic on the debugger stack view. Fortunately Pharo stack model allows context manipulation and we can really remove unneeded stack elements from execution context:
GHGhostBehaviour >>send: aMessage to: anObject
| method breakpoint |
anObject recoverFromVirus.
method := anObject class lookupSelector: aMessage selector.
breakpoint := Breakpoint new
node: method ast;
once.
breakpoint link condition: [ :receiver | receiver == anObject ] arguments: #(#object).
breakpoint install.
self activateTargetMethodOf: aMessage for: anObject
GHGhostBehaviour >>activateTargetMethodOf: aMessage for: anObject
| targetContext sender objectClass |
sender := thisContext.
[ sender selector == #cannotInterpret: ] whileFalse: [ sender := sender sender ].
thisContext terminateTo: sender sender.
"We should not use direct message to object because we propaply already install breakpoint to it"
objectClass := GHMetaMessages extractClassOf: anObject.
targetContext := thisContext sender
activateMethod: (objectClass lookupSelector: aMessage selector)
withArgs: aMessage arguments
receiver: anObject
class: objectClass.
targetContext jump
It covers our tracks of "halt-virus" infection:
At the end we can extend Pharo debugger with new button "Run to next self send" which will resume process execution and halt on next call to current receiver:
ResumeDebugAction subclass: #GHRunToNextSelfCallDebugAction
instanceVariableNames: ''
classVariableNames: ''
package: 'Ghost-ObjectCallHalt'
GHRunToNextSelfCallDebugAction >>defaultLabel
^ 'Run to next self call'
GHRunToNextSelfCallDebugAction >>id
^ #runToNextObjectCall
GHRunToNextSelfCallDebugAction class >>gtStackDebuggingActionFor: aDebugger
<gtStackDebuggingAction>
^ (self forDebugger: aDebugger)
icon: GLMUIThemeExtraIcons glamorousPlay
GHRunToNextSelfCallDebugAction >>executeAction
self currentContext receiver haltOnNextCall.
^super executeAction.
And with these debugger will be looked like:
And if you press new button debugger will be opened on #y method of current point:
Code is hosted in Ghost repository. You can use it in your development process:
Metacello new
baseline: 'Ghost';
repository: 'github://dionisiydk/Ghost';
load: 'ObjectCallHalt'
Let's create new GhostBehaviour for this:
GHGhostBehaviour subclass: #GHObjectCallHalt
instanceVariableNames: ''
classVariableNames: ''
package: 'Ghost-ObjectCallHalt'
GHGhostBehaviour>>send: aMessage to: anObject
| method breakpoint |
anObject recoverFromVirus.
self halt.
^aMessage sendTo: anObject
First we disable interception behaviour to not halt any following messages (to get halt once logic). And then we halt message processing. It will open debugger on any message send. And to access target method "step into" button should be pressed few times in the debugger.
To activate such behaviour on objects we can create suitable method:
Object>>haltOnNextCall
| virus |
virus := GHObjectVirus behaviour: GHObjectCallHalt new.
virus infect: self
And now we can play with this:
point := 2@3.
point haltOnNextCall.
0@0 distanceTo: point
It will open debugger:
Of course we don't want to step into target method manually. We want to see target method just in the debugger. To achieve this we can set up breakpoint on target method and run it instead of halt message processing:
GHGhostBehaviour >>send: aMessage to: anObject
| method breakpoint |
anObject recoverFromVirus.
method := anObject class lookupSelector: aMessage selector.
breakpoint := Breakpoint new
node: method ast;
once.
breakpoint link condition: [ :receiver | receiver == anObject ] arguments: #(#object).
breakpoint install.
^aMessage sendTo: anObject
And with this change we will see what we want:
Debugger shows Point>>x method because #distanceTo: asks #x and #y of given point to compute distance:
Last thing which is not nice is that we see all message interception logic on the debugger stack view. Fortunately Pharo stack model allows context manipulation and we can really remove unneeded stack elements from execution context:
GHGhostBehaviour >>send: aMessage to: anObject
| method breakpoint |
anObject recoverFromVirus.
method := anObject class lookupSelector: aMessage selector.
breakpoint := Breakpoint new
node: method ast;
once.
breakpoint link condition: [ :receiver | receiver == anObject ] arguments: #(#object).
breakpoint install.
self activateTargetMethodOf: aMessage for: anObject
GHGhostBehaviour >>activateTargetMethodOf: aMessage for: anObject
| targetContext sender objectClass |
sender := thisContext.
[ sender selector == #cannotInterpret: ] whileFalse: [ sender := sender sender ].
thisContext terminateTo: sender sender.
"We should not use direct message to object because we propaply already install breakpoint to it"
objectClass := GHMetaMessages extractClassOf: anObject.
targetContext := thisContext sender
activateMethod: (objectClass lookupSelector: aMessage selector)
withArgs: aMessage arguments
receiver: anObject
class: objectClass.
targetContext jump
It covers our tracks of "halt-virus" infection:
At the end we can extend Pharo debugger with new button "Run to next self send" which will resume process execution and halt on next call to current receiver:
ResumeDebugAction subclass: #GHRunToNextSelfCallDebugAction
instanceVariableNames: ''
classVariableNames: ''
package: 'Ghost-ObjectCallHalt'
GHRunToNextSelfCallDebugAction >>defaultLabel
^ 'Run to next self call'
GHRunToNextSelfCallDebugAction >>id
^ #runToNextObjectCall
GHRunToNextSelfCallDebugAction class >>gtStackDebuggingActionFor: aDebugger
<gtStackDebuggingAction>
^ (self forDebugger: aDebugger)
icon: GLMUIThemeExtraIcons glamorousPlay
GHRunToNextSelfCallDebugAction >>executeAction
self currentContext receiver haltOnNextCall.
^super executeAction.
And with these debugger will be looked like:
And if you press new button debugger will be opened on #y method of current point:
Code is hosted in Ghost repository. You can use it in your development process:
Metacello new
baseline: 'Ghost';
repository: 'github://dionisiydk/Ghost';
load: 'ObjectCallHalt'