| //------------------------------------------------------------------------------------------------------- |
| // Copyright (C) Microsoft. All rights reserved. |
| // Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. |
| //------------------------------------------------------------------------------------------------------- |
| #include "RuntimeDebugPch.h" |
| |
| #ifdef ENABLE_MUTATION_BREAKPOINT |
| Js::MutationBreakpoint::MutationBreakpoint(ScriptContext *scriptContext, DynamicObject *obj, const PropertyRecord *pr, MutationType type, Js::PropertyId parentPropertyId) |
| : isValid(true) |
| , didCauseBreak(false) |
| , mFlag(MutationTypeNone) |
| , obj(nullptr) |
| , properties(nullptr) |
| , mutationBreakpointDelegate(nullptr) |
| , breakMutationType(MutationTypeNone) |
| , propertyRecord(nullptr) |
| , newValue(nullptr) |
| , parentPropertyId(Constants::NoProperty) |
| { |
| // create weak reference to object |
| this->obj = scriptContext->GetRecycler()->CreateWeakReferenceHandle(obj); |
| |
| // initialize property mutation list |
| this->properties = RecyclerNew(scriptContext->GetRecycler(), PropertyMutationList, scriptContext->GetRecycler()); |
| |
| // Save the property id of parent object |
| this->parentPropertyId = parentPropertyId; |
| |
| // set breakpoint |
| this->SetBreak(type, pr); |
| } |
| |
| Js::MutationBreakpoint::~MutationBreakpoint() |
| {} |
| |
| bool Js::MutationBreakpoint::HandleSetProperty(Js::ScriptContext *scriptContext, RecyclableObject *object, Js::PropertyId propertyId, Var newValue) |
| { |
| Assert(scriptContext); |
| Assert(object); |
| ScriptContext *objectContext = object->GetScriptContext(); |
| if (IsFeatureEnabled(scriptContext) |
| && objectContext->HasMutationBreakpoints()) |
| { |
| MutationBreakpoint *bp = nullptr; |
| DynamicObject *dynObj = DynamicObject::FromVar(object); |
| |
| if (dynObj->GetInternalProperty(object, InternalPropertyIds::MutationBp, reinterpret_cast<Var*>(&bp), nullptr, objectContext) |
| && bp) |
| { |
| if (!bp->IsValid()) |
| { |
| bp->Reset(); |
| } |
| else |
| { |
| MutationType mutationType = MutationTypeUpdate; |
| if (!object->HasProperty(propertyId)) |
| { |
| mutationType = MutationTypeAdd; |
| } |
| if (bp->ShouldBreak(mutationType, propertyId)) |
| { |
| const PropertyRecord *pr = scriptContext->GetPropertyName(propertyId); |
| bp->newValue = newValue; |
| bp->Break(scriptContext, mutationType, pr); |
| bp->newValue = nullptr; |
| return true; |
| } |
| else |
| { |
| // Mutation breakpoint exists; do not update cache |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| void Js::MutationBreakpoint::HandleDeleteProperty(ScriptContext *scriptContext, Var instance, PropertyId propertyId) |
| { |
| Assert(scriptContext); |
| Assert(instance); |
| if (MutationBreakpoint::CanSet(instance)) |
| { |
| DynamicObject *obj = DynamicObject::FromVar(instance); |
| if (obj->GetScriptContext()->HasMutationBreakpoints()) |
| { |
| MutationBreakpoint *bp = nullptr; |
| if (obj->GetInternalProperty(obj, InternalPropertyIds::MutationBp, reinterpret_cast<Var *>(&bp), nullptr, scriptContext) |
| && bp) |
| { |
| if (!bp->IsValid()) |
| { |
| bp->Reset(); |
| } |
| else if (bp->ShouldBreak(MutationTypeDelete, propertyId)) |
| { |
| const PropertyRecord *pr = scriptContext->GetPropertyName(propertyId); |
| bp->Break(scriptContext, MutationTypeDelete, pr); |
| } |
| } |
| } |
| } |
| } |
| |
| void Js::MutationBreakpoint::HandleDeleteProperty(ScriptContext *scriptContext, Var instance, Js::JavascriptString *propertyNameString) |
| { |
| PropertyRecord const *propertyRecord = nullptr; |
| DynamicObject *obj = DynamicObject::FromVar(instance); |
| |
| if (JavascriptOperators::ShouldTryDeleteProperty(obj, propertyNameString, &propertyRecord)) |
| { |
| Assert(propertyRecord); |
| HandleDeleteProperty(scriptContext, instance, propertyRecord->GetPropertyId()); |
| } |
| } |
| |
| bool Js::MutationBreakpoint::DeleteProperty(PropertyRecord *pr) |
| { |
| Assert(pr != nullptr); |
| |
| for (int i = 0; i < this->properties->Count(); i++) |
| { |
| PropertyMutation pm = this->properties->Item(i); |
| if (pm.pr == pr) |
| { |
| this->properties->RemoveAt(i); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| const Js::Var Js::MutationBreakpoint::GetMutationObjectVar() const |
| { |
| Var retVar = nullptr; |
| Assert(this->didCauseBreak); |
| if (this->obj != nullptr) |
| { |
| DynamicObject *dynObj = this->obj->Get(); |
| if (dynObj != nullptr) |
| { |
| retVar = static_cast<Js::Var>(dynObj); |
| } |
| } |
| return retVar; |
| } |
| |
| const Js::Var Js::MutationBreakpoint::GetBreakNewValueVar() const |
| { |
| |
| Assert(this->didCauseBreak); |
| return this->newValue; |
| } |
| |
| bool Js::MutationBreakpoint::IsFeatureEnabled(ScriptContext *scriptContext) |
| { |
| Assert(scriptContext != nullptr); |
| return scriptContext->IsScriptContextInDebugMode() && !PHASE_OFF1(Js::ObjectMutationBreakpointPhase); |
| } |
| |
| bool Js::MutationBreakpoint::CanSet(Var object) |
| { |
| if (!object) |
| { |
| return false; |
| } |
| |
| TypeId id = JavascriptOperators::GetTypeId(object); |
| return JavascriptOperators::IsObjectType(id) && !JavascriptOperators::IsSpecialObjectType(id); |
| } |
| |
| Js::MutationBreakpoint * Js::MutationBreakpoint::New(ScriptContext *scriptContext, DynamicObject *obj, const PropertyRecord *pr, MutationType type, Js::PropertyId propertyId) |
| { |
| return RecyclerNewFinalized(scriptContext->GetRecycler(), MutationBreakpoint, scriptContext, obj, pr, type, propertyId); |
| } |
| |
| Js::MutationBreakpointDelegate * Js::MutationBreakpoint::GetDelegate() |
| { |
| // Create a new breakpoint object if needed |
| if (!mutationBreakpointDelegate) |
| { |
| mutationBreakpointDelegate = Js::MutationBreakpointDelegate::New(this); |
| // NOTE: no need to add ref here, as a new MutationBreakpointDelegate is initialized with 1 ref count |
| } |
| mutationBreakpointDelegate->AddRef(); |
| return mutationBreakpointDelegate; |
| } |
| |
| bool Js::MutationBreakpoint::IsValid() const |
| { |
| return isValid; |
| } |
| |
| void Js::MutationBreakpoint::Invalidate() |
| { |
| AssertMsg(isValid, "Breakpoint already invalid"); |
| isValid = false; |
| } |
| |
| // Return true if breakpoint should break on object with a specific mutation type |
| bool Js::MutationBreakpoint::ShouldBreak(MutationType type) |
| { |
| return ShouldBreak(type, Constants::NoProperty); |
| } |
| |
| // Return true if breakpoint should break on object, or a property pid, with |
| // a specific mutation type |
| bool Js::MutationBreakpoint::ShouldBreak(MutationType type, PropertyId pid) |
| { |
| Assert(isValid); |
| if (mFlag == MutationTypeNone && pid == Constants::NoProperty) |
| { |
| return false; |
| } |
| else if (type != MutationTypeNone && (type & mFlag) == type) |
| { |
| // Break on object |
| return true; |
| } |
| |
| // search properties vector |
| for (int i = 0; i < properties->Count(); i++) |
| { |
| PropertyMutation pm = properties->Item(i); |
| |
| if (pm.pr->GetPropertyId() == pid) |
| { |
| if (pm.mFlag == MutationTypeNone) |
| { |
| return false; |
| } |
| else if (type != MutationTypeNone && (pm.mFlag & type) == type) |
| { |
| return true; |
| } |
| break; |
| } |
| } |
| return false; |
| } |
| |
| bool Js::MutationBreakpoint::Reset() |
| { |
| // Invalidate breakpoint |
| if (isValid) |
| { |
| this->Invalidate(); |
| } |
| |
| // Release existing delegate object |
| if (mutationBreakpointDelegate) |
| { |
| mutationBreakpointDelegate->Release(); |
| mutationBreakpointDelegate = nullptr; |
| } |
| |
| // Clear all property records |
| if (properties) |
| { |
| properties->ClearAndZero(); |
| } |
| |
| // Get object and remove strong ref |
| DynamicObject *obj = this->obj->Get(); |
| return obj && obj->SetInternalProperty(InternalPropertyIds::MutationBp, nullptr, PropertyOperation_SpecialValue, NULL); |
| } |
| |
| void Js::MutationBreakpoint::SetBreak(MutationType type, const PropertyRecord *pr) |
| { |
| // Break on entire object if pid is NoProperty |
| if (!pr) |
| { |
| if (type == MutationTypeNone) |
| { |
| mFlag = MutationTypeNone; |
| } |
| else |
| { |
| mFlag = static_cast<MutationType>(mFlag | type); |
| } |
| return; |
| } |
| |
| // Check if property is already added |
| for (int i = 0; i < properties->Count(); i++) |
| { |
| PropertyMutation& pm = properties->Item(i); |
| if (pm.pr == pr) |
| { |
| // added to existing property mutation struct |
| if (type == MutationTypeNone) |
| { |
| pm.mFlag = MutationTypeNone; |
| } |
| else |
| { |
| pm.mFlag = static_cast<MutationType>(pm.mFlag | type); |
| } |
| return; |
| } |
| } |
| // if not in list, add new property mutation |
| PropertyMutation pm = { |
| pr, |
| type |
| }; |
| properties->Add(pm); |
| } |
| |
| void Js::MutationBreakpoint::Break(ScriptContext *scriptContext, MutationType mutationType, const PropertyRecord *pr) |
| { |
| this->didCauseBreak = true; |
| this->breakMutationType = mutationType; |
| this->propertyRecord = pr; |
| |
| InterpreterHaltState haltState(STOP_MUTATIONBREAKPOINT, /*executingFunction*/nullptr, this); |
| scriptContext->GetDebugContext()->GetProbeContainer()->DispatchMutationBreakpoint(&haltState); |
| |
| this->didCauseBreak = false; |
| } |
| |
| bool Js::MutationBreakpoint::GetDidCauseBreak() const |
| { |
| return this->didCauseBreak; |
| } |
| |
| const char16 * Js::MutationBreakpoint::GetBreakPropertyName() const |
| { |
| Assert(this->didCauseBreak); |
| Assert(this->propertyRecord); |
| return this->propertyRecord->GetBuffer(); |
| } |
| |
| const Js::PropertyId Js::MutationBreakpoint::GetParentPropertyId() const |
| { |
| Assert(this->didCauseBreak); |
| return this->parentPropertyId; |
| } |
| |
| const char16 * Js::MutationBreakpoint::GetParentPropertyName() const |
| { |
| Assert(this->didCauseBreak); |
| const PropertyRecord *pr = nullptr; |
| if ((this->parentPropertyId != Constants::NoProperty) && (this->obj != nullptr)) |
| { |
| DynamicObject *dynObj = this->obj->Get(); |
| if (dynObj != nullptr) |
| { |
| pr = dynObj->GetScriptContext()->GetPropertyName(this->parentPropertyId); |
| return pr->GetBuffer(); |
| } |
| } |
| return _u(""); |
| } |
| |
| MutationType Js::MutationBreakpoint::GetBreakMutationType() const |
| { |
| Assert(this->didCauseBreak); |
| return this->breakMutationType; |
| } |
| |
| const Js::PropertyId Js::MutationBreakpoint::GetBreakPropertyId() const |
| { |
| Assert(this->didCauseBreak); |
| Assert(this->propertyRecord); |
| return this->propertyRecord->GetPropertyId(); |
| } |
| |
| const char16 * Js::MutationBreakpoint::GetBreakMutationTypeName(MutationType mutationType) |
| { |
| switch (mutationType) |
| { |
| case MutationTypeUpdate: return _u("Changing"); |
| case MutationTypeDelete: return _u("Deleting"); |
| case MutationTypeAdd: return _u("Adding"); |
| default: AssertMsg(false, "Unhandled break reason mutation type. Did we add a new mutation type?"); return _u(""); |
| } |
| } |
| |
| const char16 * Js::MutationBreakpoint::GetMutationTypeForConditionalEval(MutationType mutationType) |
| { |
| switch (mutationType) |
| { |
| case MutationTypeUpdate: return _u("update"); |
| case MutationTypeDelete: return _u("delete"); |
| case MutationTypeAdd: return _u("add"); |
| default: AssertMsg(false, "Unhandled mutation type in conditional object mutation breakpoint."); return _u(""); |
| } |
| } |
| |
| // setOnObject - Is true if we are watching the parent object |
| // parentPropertyId - Property ID of object on which Mutation is set |
| // propertyId - Property ID of property. If setOnObject is false we are watching a specific property (propertyId) |
| |
| Js::MutationBreakpointDelegate * Js::MutationBreakpoint::Set(ScriptContext *scriptContext, Var obj, BOOL setOnObject, MutationType type, PropertyId parentPropertyId, PropertyId propertyId) |
| { |
| Assert(obj); |
| Assert(scriptContext); |
| |
| if (!CanSet(obj)) |
| { |
| return nullptr; |
| } |
| DynamicObject *dynObj = static_cast<DynamicObject*>(obj); |
| |
| const PropertyRecord *pr = nullptr; |
| |
| if (!setOnObject && (propertyId != Constants::NoProperty)) |
| { |
| pr = scriptContext->GetPropertyName(propertyId); |
| Assert(pr); |
| } |
| |
| MutationBreakpoint *bp = nullptr; |
| |
| // Breakpoint exists; update it. |
| if (dynObj->GetInternalProperty(dynObj, InternalPropertyIds::MutationBp, reinterpret_cast<Var*>(&bp), nullptr, scriptContext) |
| && bp) |
| { |
| // Valid bp; update it. |
| if (bp->IsValid()) |
| { |
| Assert(bp->mutationBreakpointDelegate); // Delegate must already exist |
| // set breakpoint |
| bp->SetBreak(type, pr); |
| |
| return bp->GetDelegate(); |
| } |
| // bp invalidated by haven't got cleaned up yet, so reset it right here |
| // and then create new bp |
| else |
| { |
| bp->Reset(); |
| } |
| } |
| |
| // Create new breakpoint |
| bp = MutationBreakpoint::New(scriptContext, dynObj, const_cast<PropertyRecord *>(pr), type, parentPropertyId); |
| |
| // Adding reference to dynamic object |
| dynObj->SetInternalProperty(InternalPropertyIds::MutationBp, bp, PropertyOperation_SpecialValue, nullptr); |
| |
| // Track in object's own script context |
| dynObj->GetScriptContext()->InsertMutationBreakpoint(bp); |
| |
| return bp->GetDelegate(); |
| } |
| |
| void Js::MutationBreakpoint::Finalize(bool isShutdown) {} |
| void Js::MutationBreakpoint::Dispose(bool isShutdown) |
| { |
| // TODO (t-shchan): If removed due to detaching debugger, do not fire event |
| // TODO (t-shchan): Fire debugger event for breakpoint removal |
| if (mutationBreakpointDelegate) |
| { |
| mutationBreakpointDelegate->Release(); |
| } |
| } |
| void Js::MutationBreakpoint::Mark(Recycler * recycler) {} |
| |
| |
| /* |
| MutationBreakpointDelegate definition |
| */ |
| |
| Js::MutationBreakpointDelegate::MutationBreakpointDelegate(Js::MutationBreakpoint *bp) |
| : m_refCount(1), m_breakpoint(bp), m_didCauseBreak(false), m_propertyRecord(nullptr), m_type(MutationTypeNone) |
| { |
| Assert(bp != nullptr); |
| } |
| |
| Js::MutationBreakpointDelegate * Js::MutationBreakpointDelegate::New(Js::MutationBreakpoint *bp) |
| { |
| return HeapNew(Js::MutationBreakpointDelegate, bp); |
| } |
| |
| /* |
| IMutationBreakpoint interface definition |
| */ |
| |
| STDMETHODIMP_(ULONG) Js::MutationBreakpointDelegate::AddRef() |
| { |
| return (ULONG)InterlockedIncrement(&m_refCount); |
| } |
| |
| STDMETHODIMP_(ULONG) Js::MutationBreakpointDelegate::Release() |
| { |
| ULONG refCount = (ULONG)InterlockedDecrement(&m_refCount); |
| |
| if (0 == refCount) |
| { |
| HeapDelete(this); |
| } |
| |
| return refCount; |
| } |
| |
| STDMETHODIMP Js::MutationBreakpointDelegate::QueryInterface(REFIID iid, void ** ppv) |
| { |
| if (!ppv) |
| { |
| return E_INVALIDARG; |
| } |
| |
| if (__uuidof(IUnknown) == iid || __uuidof(IMutationBreakpoint) == iid) |
| { |
| *ppv = static_cast<IUnknown*>(static_cast<IMutationBreakpoint*>(this)); |
| } |
| else |
| { |
| *ppv = NULL; |
| return E_NOINTERFACE; |
| } |
| |
| AddRef(); |
| return S_OK; |
| } |
| |
| STDMETHODIMP Js::MutationBreakpointDelegate::Delete(void) |
| { |
| this->m_breakpoint->Invalidate(); |
| return S_OK; |
| } |
| |
| STDMETHODIMP Js::MutationBreakpointDelegate::DidCauseBreak( |
| /* [out] */ __RPC__out BOOL *didCauseBreak) |
| { |
| if (!didCauseBreak) |
| { |
| return E_INVALIDARG; |
| } |
| *didCauseBreak = this->m_breakpoint->GetDidCauseBreak(); |
| return S_OK; |
| } |
| #endif |