Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Source/WebCore/automation/AutomationInstrumentation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ void AutomationInstrumentation::clearClient()
automationClient().clear();
}

bool AutomationInstrumentation::hasClient()
{
return !!automationClient();
}

void AutomationInstrumentation::addMessageToConsole(const std::unique_ptr<ConsoleMessage>& message)
{
if (!automationClient()) [[likely]]
Expand All @@ -74,6 +79,28 @@ void AutomationInstrumentation::addMessageToConsole(const std::unique_ptr<Consol
});
}

void AutomationInstrumentation::scriptRealmCreated(FrameIdentifier frameID, const String& origin)
{
if (!automationClient()) [[likely]]
return;

WTF::ensureOnMainThread([frameID, origin = origin.isolatedCopy()] {
if (RefPtr client = automationClient().get())
client->scriptRealmCreated(frameID, origin);
});
}

void AutomationInstrumentation::scriptRealmDestroyed(FrameIdentifier frameID)
{
if (!automationClient()) [[likely]]
return;

WTF::ensureOnMainThread([frameID] {
if (RefPtr client = automationClient().get())
client->scriptRealmDestroyed(frameID);
});
}

} // namespace WebCore

#endif
6 changes: 6 additions & 0 deletions Source/WebCore/automation/AutomationInstrumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#if ENABLE(WEBDRIVER_BIDI)

#include "FrameIdentifier.h"
#include <JavaScriptCore/ConsoleMessage.h>
#include <JavaScriptCore/ConsoleTypes.h>
#include <wtf/AbstractRefCountedAndCanMakeWeakPtr.h>
Expand All @@ -53,15 +54,20 @@ class WEBCORE_EXPORT AutomationInstrumentationClient : public AbstractRefCounted
virtual ~AutomationInstrumentationClient() = default;

virtual void addMessageToConsole(const JSC::MessageSource&, const JSC::MessageLevel&, const String&, const JSC::MessageType&, const WallTime&) = 0;
virtual void scriptRealmCreated(FrameIdentifier, const String& origin) = 0;
virtual void scriptRealmDestroyed(FrameIdentifier) = 0;
};


class WEBCORE_EXPORT AutomationInstrumentation {
public:
static void setClient(const AutomationInstrumentationClient&);
static void clearClient();
static bool hasClient();

static void addMessageToConsole(const std::unique_ptr<Inspector::ConsoleMessage>&);
static void scriptRealmCreated(FrameIdentifier, const String& origin);
static void scriptRealmDestroyed(FrameIdentifier);
};

} // namespace WebCore
Expand Down
53 changes: 51 additions & 2 deletions Source/WebCore/bindings/js/WindowProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,48 @@

#include "CommonVM.h"
#include "DOMWrapperWorld.h"
#include "DocumentLoader.h"
#include "DocumentPage.h"
#include "FrameConsoleClient.h"
#include "FrameLoader.h"
#include "GarbageCollectionController.h"
#include "JSDOMWindowBase.h"
#include "JSWindowProxy.h"
#include "LocalFrame.h"
#include "LocalFrameInlines.h"
#include "Page.h"
#include "PageGroup.h"
#include "RemoteFrame.h"
#include "ScriptController.h"
#include "SecurityOrigin.h"
#include "runtime_root.h"
#include <JavaScriptCore/JSLock.h>
#include <JavaScriptCore/StrongInlines.h>
#include <JavaScriptCore/WeakGCMapInlines.h>
#include <wtf/MemoryPressureHandler.h>
#include <wtf/TZoneMallocInlines.h>

#if ENABLE(WEBDRIVER_BIDI)
#include "AutomationInstrumentation.h"
#endif

namespace WebCore {

using namespace JSC;

#if ENABLE(WEBDRIVER_BIDI)
static String resolveOriginForRealm(LocalFrame& localFrame)
{
if (auto* document = localFrame.document())
return document->securityOrigin().toString();

if (auto* loader = localFrame.loader().activeDocumentLoader(); loader && !loader->url().isEmpty())
return SecurityOrigin::create(loader->url())->toString();

return "null"_s;
}
#endif

static void collectGarbageAfterWindowProxyDestruction()
{
// Make sure to GC Extra Soon(tm) during memory pressure conditions
Expand Down Expand Up @@ -107,6 +128,15 @@ void WindowProxy::destroyJSWindowProxy(DOMWrapperWorld& world)
{
ASSERT(m_jsWindowProxies.contains(&world));
m_jsWindowProxies.remove(&world);

#if ENABLE(WEBDRIVER_BIDI)
// Notify about realm destruction for automation
// Only notify for normal world (main page execution), not user/internal worlds
if (world.isNormal()) {
if (RefPtr localFrame = dynamicDowncast<LocalFrame>(*m_frame))
AutomationInstrumentation::scriptRealmDestroyed(localFrame->frameID());
}
#endif
world.didDestroyWindowProxy(this);
}

Expand All @@ -122,6 +152,16 @@ JSWindowProxy& WindowProxy::createJSWindowProxy(DOMWrapperWorld& world)
Strong<JSWindowProxy> jsWindowProxy(vm, &JSWindowProxy::create(vm, *m_frame->protectedWindow().get(), world));
m_jsWindowProxies.add(&world, jsWindowProxy);
world.didCreateWindowProxy(this);

#if ENABLE(WEBDRIVER_BIDI)
// Notify about realm creation for automation.
// Only notify for normal world (main page execution), not user/internal worlds.
if (world.isNormal()) {
if (RefPtr localFrame = dynamicDowncast<LocalFrame>(*m_frame))
AutomationInstrumentation::scriptRealmCreated(localFrame->frameID(), resolveOriginForRealm(*localFrame));
}
#endif

return *jsWindowProxy.get();
}

Expand Down Expand Up @@ -175,12 +215,11 @@ void WindowProxy::clearJSWindowProxiesNotMatchingDOMWindow(DOMWindow* newDOMWind
void WindowProxy::setDOMWindow(DOMWindow* newDOMWindow)
{
ASSERT(newDOMWindow);
ASSERT(m_frame);

if (m_jsWindowProxies.isEmpty())
return;

ASSERT(m_frame);

JSLockHolder lock(commonVM());

for (auto& windowProxy : jsWindowProxiesAsVector()) {
Expand All @@ -189,6 +228,16 @@ void WindowProxy::setDOMWindow(DOMWindow* newDOMWindow)

windowProxy->setWindow(*newDOMWindow);

#if ENABLE(WEBDRIVER_BIDI)
// Navigations reuse the JSWindowProxy with a new DOMWindow, which means a new realm.
if (windowProxy->world().isNormal()) {
if (RefPtr localFrame = dynamicDowncast<LocalFrame>(m_frame.get())) {
AutomationInstrumentation::scriptRealmDestroyed(localFrame->frameID());
AutomationInstrumentation::scriptRealmCreated(localFrame->frameID(), resolveOriginForRealm(*localFrame));
}
}
#endif

if (RefPtr localFrame = dynamicDowncast<LocalFrame>(m_frame.get())) {
CheckedRef scriptController = localFrame->script();

Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/loader/FrameLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ void FrameLoader::clear(RefPtr<Document>&& newDocument, bool clearWindowProperti

if (!neededClear)
return;

// Do this after detaching the document so that the unload event works.
if (clearWindowProperties) {
InspectorInstrumentation::frameWindowDiscarded(frame, document->protectedWindow().get());
Expand Down
158 changes: 104 additions & 54 deletions Source/WebKit/UIProcess/Automation/BidiScriptAgent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,66 +255,29 @@ RefPtr<Inspector::Protocol::BidiScript::RealmInfo> BidiScriptAgent::createRealmI

String BidiScriptAgent::generateRealmIdForFrame(const FrameInfoData& frameInfo)
{
String currentURL = frameInfo.request.url().string();
std::optional<String> currentDocumentID = frameInfo.documentID ? std::optional<String>(frameInfo.documentID->toString()) : std::nullopt;

if (auto it = m_frameRealmCache.find(frameInfo.frameID); it != m_frameRealmCache.end()) {
const auto& cachedEntry = it->value;

if (cachedEntry.url == currentURL && cachedEntry.documentID == currentDocumentID)
return cachedEntry.realmId;

// FIXME: This is a workaround until realm.created/realm.destroyed events are implemented.
// https://bugs.webkit.org/show_bug.cgi?id=304062
// If only the documentID changed but URL is the same, reuse the cached realm ID to keep
// realm IDs stable between getRealms() and evaluate()/callFunction() calls on the same document.
// Once realm lifecycle events are implemented, they will handle cache updates properly.
if (cachedEntry.url == currentURL && currentURL != "about:blank"_s) {
m_frameRealmCache.set(frameInfo.frameID, FrameRealmCacheEntry { currentURL, currentDocumentID, cachedEntry.realmId });
return cachedEntry.realmId;
}

// Special case: Transitioning to/from about:blank is typically not a navigation,
// it's either the initial page load or a new test/session starting.
// Don't treat this as a state change that increments the counter.
bool transitioningToOrFromBlank = (cachedEntry.url == "about:blank"_s) != (currentURL == "about:blank"_s);

if (transitioningToOrFromBlank) {
m_frameRealmCache.remove(frameInfo.frameID);
m_frameRealmCounters.remove(frameInfo.frameID);
}
}

// Generate a new realm ID - the state has changed or this is a new frame
// Get the browsing context handle for this frame
auto contextHandle = contextHandleForFrame(frameInfo);

String newRealmId;

if (!contextHandle) {
// Fallback to frame-based ID if we can't get context handle
newRealmId = makeString("realm-frame-"_s, String::number(frameInfo.frameID.toUInt64()));
} else {
// Use the contextHandle directly - it's already unique for both main frames and iframes
// For the first load of a context, use just the context handle
// For subsequent navigations/reloads, append a counter to make it unique
auto counterIt = m_frameRealmCounters.find(frameInfo.frameID);
if (counterIt == m_frameRealmCounters.end()) {
// First realm for this frame - no counter suffix
newRealmId = makeString("realm-"_s, *contextHandle);
// Start counter at 1 so the NEXT navigation will use "-1" suffix
m_frameRealmCounters.set(frameInfo.frameID, 1);
} else {
// Subsequent realm (reload/navigation) - use and increment counter
uint64_t counter = counterIt->value;
newRealmId = makeString("realm-"_s, *contextHandle, "-"_s, String::number(counter));
counterIt->value = counter + 1;
}
return makeString("realm-frame-"_s, String::number(frameInfo.frameID.toUInt64()));
}

// Update the cache with the new realm ID
m_frameRealmCache.set(frameInfo.frameID, FrameRealmCacheEntry { currentURL, currentDocumentID, newRealmId });
// Use the shared m_realmNavigationCounters to ensure consistency with notifyRealmCreated/Destroyed
auto counterIt = m_realmNavigationCounters.find(*contextHandle);
if (counterIt == m_realmNavigationCounters.end()) {
// No realm has been created yet for this context - return the base realm ID
// Note: This should not normally happen since notifyRealmCreated should be called
// before getRealms due to the sendWithAsyncReply barrier
return makeString("realm-"_s, *contextHandle);
}

// Generate realm ID based on the current counter value
uint64_t counter = counterIt->value;
String realmId = counter <= 1
? makeString("realm-"_s, *contextHandle)
: makeString("realm-"_s, *contextHandle, "-"_s, String::number(counter - 1));

return newRealmId;
return realmId;
}

String BidiScriptAgent::generateRealmIdForBrowsingContext(const String& browsingContext)
Expand Down Expand Up @@ -443,6 +406,93 @@ void BidiScriptAgent::collectExecutionReadyFrameRealms(const FrameTreeNodeData&
}
}

void BidiScriptAgent::notifyRealmCreated(const String& browsingContext, const String& origin)
{
RefPtr session = m_session.get();
if (!session)
return;

// Generate a realm ID consistent with generateRealmIdForFrame() semantics:
// first realm -> realm-{context}, subsequent -> realm-{context}-{counter}
auto counterIt = m_realmNavigationCounters.find(browsingContext);
String realmId;
if (counterIt == m_realmNavigationCounters.end()) {
realmId = makeString("realm-"_s, browsingContext);
m_realmNavigationCounters.set(browsingContext, 1);
} else {
uint64_t counter = counterIt->value;
realmId = makeString("realm-"_s, browsingContext, "-"_s, String::number(counter));
counterIt->value = counter + 1;
}

// Store realm info for getRealms queries
RealmInfo info;
info.realmId = realmId;
info.origin = origin;
info.type = "window"_s;
info.context = browsingContext;
m_activeRealms.set(realmId, WTF::move(info));

// FIXME: Only emit events to subscribers based on context-specific subscriptions.
// https://bugs.webkit.org/show_bug.cgi?id=282981
// Build script.realmCreated event per W3C BiDi spec
auto event = JSON::Object::create();
event->setString("method"_s, "script.realmCreated"_s);
event->setString("type"_s, "event"_s);

auto params = JSON::Object::create();
params->setString("realm"_s, realmId);
params->setString("origin"_s, origin);
params->setString("type"_s, "window"_s);
params->setString("context"_s, browsingContext);
event->setObject("params"_s, WTF::move(params));

// Send event immediately
session->sendBidiMessage(event->toJSONString());
}

void BidiScriptAgent::notifyRealmDestroyed(const String& browsingContext)
{
RefPtr session = m_session.get();
if (!session)
return;

auto counterIt = m_realmNavigationCounters.find(browsingContext);
if (counterIt == m_realmNavigationCounters.end())
return;

uint64_t counter = counterIt->value;
String realmId = counter == 1
? makeString("realm-"_s, browsingContext)
: makeString("realm-"_s, browsingContext, "-"_s, String::number(counter - 1));

auto activeIt = m_activeRealms.find(realmId);
if (activeIt != m_activeRealms.end())
m_activeRealms.remove(activeIt);

// FIXME: Only emit events to subscribers based on context-specific subscriptions.
// https://bugs.webkit.org/show_bug.cgi?id=282981
// Build script.realmDestroyed event per W3C BiDi spec
auto event = JSON::Object::create();
event->setString("method"_s, "script.realmDestroyed"_s);
event->setString("type"_s, "event"_s);

auto params = JSON::Object::create();
params->setString("realm"_s, realmId);
event->setObject("params"_s, WTF::move(params));

session->sendBidiMessage(event->toJSONString());
}

bool BidiScriptAgent::hasRealmForContext(const String& browsingContext) const
{
for (const auto& entry : m_activeRealms) {
if (entry.value.context == browsingContext)
return true;
}
return false;
}

} // namespace WebKit

#endif // ENABLE(WEBDRIVER_BIDI)
Loading