blob: 5700051fd408257b0ca6bc169e13820019c4f342 [file] [log] [blame]
/*
* Copyright (C) 2013-2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
// FIXME: NetworkManager lacks advanced multi-target support. (Network.loadResource invocations per-target)
WI.NetworkManager = class NetworkManager extends WI.Object
{
constructor()
{
super();
this._frameIdentifierMap = new Map;
this._mainFrame = null;
this._resourceRequestIdentifierMap = new Map;
this._orphanedResources = new Map;
this._webSocketIdentifierToURL = new Map;
this._waitingForMainFrameResourceTreePayload = true;
this._transitioningPageTarget = false;
this._sourceMapURLMap = new Map;
this._downloadingSourceMaps = new Set;
this._localResourceOverrides = new Set;
this._harImportLocalResourceMap = new Set;
this._pendingLocalResourceOverrideSaves = null;
this._saveLocalResourceOverridesDebouncer = null;
// FIXME: Provide dedicated UI to toggle Network Interception globally?
this._interceptionEnabled = true;
WI.notifications.addEventListener(WI.Notification.ExtraDomainsActivated, this._extraDomainsActivated, this);
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleFrameMainResourceDidChange, this);
if (NetworkManager.supportsLocalResourceOverrides()) {
WI.Resource.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleResourceContentDidChange, this);
WI.LocalResourceOverride.addEventListener(WI.LocalResourceOverride.Event.DisabledChanged, this._handleResourceOverrideDisabledChanged, this);
WI.Target.registerInitializationPromise((async () => {
let serializedLocalResourceOverrides = await WI.objectStores.localResourceOverrides.getAll();
this._restoringLocalResourceOverrides = true;
for (let serializedLocalResourceOverride of serializedLocalResourceOverrides) {
let localResourceOverride = WI.LocalResourceOverride.fromJSON(serializedLocalResourceOverride);
const key = null;
WI.objectStores.localResourceOverrides.associateObject(localResourceOverride, key, serializedLocalResourceOverride);
this.addLocalResourceOverride(localResourceOverride);
}
this._restoringLocalResourceOverrides = false;
})());
}
this._bootstrapScript = null;
if (NetworkManager.supportsBootstrapScript()) {
this._bootstrapScriptEnabledSetting = new WI.Setting("bootstrap-script-enabled", true);
WI.Target.registerInitializationPromise((async () => {
let bootstrapScriptSource = await WI.objectStores.general.get(NetworkManager.bootstrapScriptSourceObjectStoreKey);
if (bootstrapScriptSource !== undefined)
this.createBootstrapScript(bootstrapScriptSource);
})());
}
}
// Static
static supportsShowCertificate()
{
return InspectorFrontendHost.supportsShowCertificate
&& InspectorBackend.hasCommand("Network.getSerializedCertificate");
}
static supportsLocalResourceOverrides()
{
return InspectorBackend.hasCommand("Network.setInterceptionEnabled");
}
static supportsBootstrapScript()
{
return InspectorBackend.hasCommand("Page.setBootstrapScript");
}
static get bootstrapScriptURL()
{
return "web-inspector://bootstrap.js";
}
static get bootstrapScriptSourceObjectStoreKey()
{
return "bootstrap-script-source";
}
static synthesizeImportError(message)
{
message = WI.UIString("HAR Import Error: %s").format(message);
if (window.InspectorTest) {
console.error(message);
return;
}
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
consoleMessage.shouldRevealConsole = true;
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
}
// Target
initializeTarget(target)
{
if (target.hasDomain("Page")) {
target.PageAgent.enable();
target.PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this));
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
if (target.hasCommand("Page.setBootstrapScript") && this._bootstrapScript && this._bootstrapScriptEnabledSetting.value)
target.PageAgent.setBootstrapScript(this._bootstrapScript.content);
}
if (target.hasDomain("ServiceWorker"))
target.ServiceWorkerAgent.getInitializationInfo(this._processServiceWorkerConfiguration.bind(this));
if (target.hasDomain("Network")) {
target.NetworkAgent.enable();
// COMPATIBILITY (iOS 10.3): Network.setDisableResourceCaching did not exist.
if (target.hasCommand("Network.setResourceCachingDisabled"))
target.NetworkAgent.setResourceCachingDisabled(WI.settings.resourceCachingDisabled.value);
// COMPATIBILITY (iOS 13.0): Network.setInterceptionEnabled did not exist.
if (target.hasCommand("Network.setInterceptionEnabled")) {
if (this._interceptionEnabled)
target.NetworkAgent.setInterceptionEnabled(this._interceptionEnabled);
for (let localResourceOverride of this._localResourceOverrides) {
if (!localResourceOverride.disabled) {
target.NetworkAgent.addInterception.invoke({
url: localResourceOverride.url,
caseSensitive: localResourceOverride.isCaseSensitive,
isRegex: localResourceOverride.isRegex,
networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
});
}
}
}
}
if (target.type === WI.TargetType.Worker)
this.adoptOrphanedResourcesForTarget(target);
}
transitionPageTarget()
{
this._transitioningPageTarget = true;
this._waitingForMainFrameResourceTreePayload = true;
}
// Public
get mainFrame() { return this._mainFrame; }
get bootstrapScript() { return this._bootstrapScript; }
get frames()
{
return Array.from(this._frameIdentifierMap.values());
}
get localResourceOverrides()
{
return Array.from(this._localResourceOverrides);
}
get interceptionEnabled()
{
return this._interceptionEnabled;
}
set interceptionEnabled(enabled)
{
if (this._interceptionEnabled === enabled)
return;
this._interceptionEnabled = enabled;
for (let target of WI.targets) {
// COMPATIBILITY (iOS 13.0): Network.setInterceptionEnabled did not exist.
if (target.hasCommand("Network.setInterceptionEnabled"))
target.NetworkAgent.setInterceptionEnabled(this._interceptionEnabled);
}
}
frameForIdentifier(frameId)
{
return this._frameIdentifierMap.get(frameId) || null;
}
resourceForRequestIdentifier(requestIdentifier)
{
return this._resourceRequestIdentifierMap.get(requestIdentifier) || null;
}
downloadSourceMap(sourceMapURL, baseURL, originalSourceCode)
{
if (!WI.settings.sourceMapsEnabled.value)
return;
// The baseURL could have come from a "//# sourceURL". Attempt to get a
// reasonable absolute URL for the base by using the main resource's URL.
if (WI.networkManager.mainFrame)
baseURL = absoluteURL(baseURL, WI.networkManager.mainFrame.url);
if (sourceMapURL.startsWith("data:")) {
this._loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode);
return;
}
sourceMapURL = absoluteURL(sourceMapURL, baseURL);
if (!sourceMapURL)
return;
console.assert(originalSourceCode.url);
if (!originalSourceCode.url)
return;
// FIXME: <rdar://problem/13265694> Source Maps: Better handle when multiple resources reference the same SourceMap
if (this._sourceMapURLMap.has(sourceMapURL) || this._downloadingSourceMaps.has(sourceMapURL))
return;
let loadAndParseSourceMap = () => {
this._loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode);
};
if (!WI.networkManager.mainFrame) {
// If we don't have a main frame, then we are likely in the middle of building the resource tree.
// Delaying until the next runloop is enough in this case to then start loading the source map.
setTimeout(loadAndParseSourceMap, 0);
return;
}
loadAndParseSourceMap();
}
get bootstrapScriptEnabled()
{
console.assert(NetworkManager.supportsBootstrapScript());
console.assert(this._bootstrapScript);
return this._bootstrapScriptEnabledSetting.value;
}
set bootstrapScriptEnabled(enabled)
{
console.assert(NetworkManager.supportsBootstrapScript());
console.assert(this._bootstrapScript);
this._bootstrapScriptEnabledSetting.value = !!enabled;
let source = this._bootstrapScriptEnabledSetting.value ? this._bootstrapScript.content : undefined;
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
for (let target of WI.targets) {
if (target.hasCommand("Page.setBootstrapScript"))
target.PageAgent.setBootstrapScript(source);
}
this.dispatchEventToListeners(NetworkManager.Event.BootstrapScriptEnabledChanged, {bootstrapScript: this._bootstrapScript});
}
async createBootstrapScript(source)
{
console.assert(NetworkManager.supportsBootstrapScript());
if (this._bootstrapScript)
return;
if (!arguments.length)
source = await WI.objectStores.general.get(NetworkManager.bootstrapScriptSourceObjectStoreKey);
if (!source) {
source = `
/*
* ${WI.UIString("The Inspector Bootstrap Script is guaranteed to be the first script evaluated in any page, as well as any sub-frames.")}
* ${WI.UIString("It is evaluated immediately after the global object is created, before any other content has loaded.")}
*
* ${WI.UIString("Modifications made here will take effect on the next load of any page or sub-frame.")}
* ${WI.UIString("The contents and enabled state will be preserved across Web Inspector sessions.")}
*
* ${WI.UIString("Some examples of ways to use this script include (but are not limited to):")}
* - ${WI.UIString("overriding built-in functions to log call traces or add `debugger` statements")}
* - ${WI.UIString("ensuring that common debugging functions are available on every page via the Console")}
*/
`;
}
const target = null;
const url = null;
const sourceURL = NetworkManager.bootstrapScriptURL;
this._bootstrapScript = new WI.LocalScript(target, url, sourceURL, WI.Script.SourceType.Program, source, {injected: true, editable: true});
this._bootstrapScript.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleBootstrapScriptContentDidChange, this);
this._handleBootstrapScriptContentDidChange();
this.dispatchEventToListeners(NetworkManager.Event.BootstrapScriptCreated, {bootstrapScript: this._bootstrapScript});
}
destroyBootstrapScript()
{
console.assert(NetworkManager.supportsBootstrapScript());
if (!this._bootstrapScript)
return;
let bootstrapScript = this._bootstrapScript;
this._bootstrapScript = null;
WI.objectStores.general.delete(NetworkManager.bootstrapScriptSourceObjectStoreKey);
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
for (let target of WI.targets) {
if (target.hasCommand("Page.setBootstrapScript"))
target.PageAgent.setBootstrapScript();
}
this.dispatchEventToListeners(NetworkManager.Event.BootstrapScriptDestroyed, {bootstrapScript});
}
addLocalResourceOverride(localResourceOverride)
{
console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
console.assert(!this._localResourceOverrides.has(localResourceOverride), "Already had an existing local resource override.");
this._localResourceOverrides.add(localResourceOverride);
if (!this._restoringLocalResourceOverrides)
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
if (!localResourceOverride.disabled) {
let commandArguments = {
url: localResourceOverride.url,
caseSensitive: localResourceOverride.isCaseSensitive,
isRegex: localResourceOverride.isRegex,
networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
};
// COMPATIBILITY (iOS 13.0): Network.addInterception did not exist.
for (let target of WI.targets) {
if (target.hasCommand("Network.addInterception"))
target.NetworkAgent.addInterception.invoke(commandArguments);
}
}
this.dispatchEventToListeners(WI.NetworkManager.Event.LocalResourceOverrideAdded, {localResourceOverride});
}
removeLocalResourceOverride(localResourceOverride)
{
console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
if (!this._localResourceOverrides.delete(localResourceOverride)) {
console.assert(false, "Attempted to remove a local resource override that was not known.");
return;
}
if (this._pendingLocalResourceOverrideSaves)
this._pendingLocalResourceOverrideSaves.delete(localResourceOverride);
if (!this._restoringLocalResourceOverrides)
WI.objectStores.localResourceOverrides.deleteObject(localResourceOverride);
if (!localResourceOverride.disabled) {
let commandArguments = {
url: localResourceOverride.url,
caseSensitive: localResourceOverride.isCaseSensitive,
isRegex: localResourceOverride.isRegex,
networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
};
// COMPATIBILITY (iOS 13.0): Network.removeInterception did not exist.
for (let target of WI.targets) {
if (target.hasCommand("Network.removeInterception"))
target.NetworkAgent.removeInterception.invoke(commandArguments);
}
}
this.dispatchEventToListeners(WI.NetworkManager.Event.LocalResourceOverrideRemoved, {localResourceOverride});
}
localResourceOverrideForURL(url)
{
for (let localResourceOverride of this._localResourceOverrides) {
if (localResourceOverride.matches(url))
return localResourceOverride;
}
return null;
}
canBeOverridden(resource)
{
if (!(resource instanceof WI.Resource))
return false;
if (resource instanceof WI.SourceMapResource)
return false;
if (resource.isLocalResourceOverride)
return false;
const schemes = ["http:", "https:", "file:"];
if (!schemes.some((scheme) => resource.url.startsWith(scheme)))
return false;
let existingOverride = this.localResourceOverrideForURL(resource.url);
if (existingOverride)
return false;
switch (resource.type) {
case WI.Resource.Type.Document:
case WI.Resource.Type.StyleSheet:
case WI.Resource.Type.Script:
case WI.Resource.Type.XHR:
case WI.Resource.Type.Fetch:
case WI.Resource.Type.Image:
case WI.Resource.Type.Font:
case WI.Resource.Type.Other:
break;
case WI.Resource.Type.Ping:
case WI.Resource.Type.Beacon:
// Responses aren't really expected for Ping/Beacon.
return false;
case WI.Resource.Type.WebSocket:
// Non-HTTP traffic.
console.assert(false, "Scheme check above should have been sufficient.");
return false;
}
return true;
}
resourceForURL(url)
{
if (!this._mainFrame)
return null;
if (this._mainFrame.mainResource.url === url)
return this._mainFrame.mainResource;
return this._mainFrame.resourceForURL(url, true);
}
adoptOrphanedResourcesForTarget(target)
{
let resources = this._orphanedResources.take(target.identifier);
if (!resources)
return;
for (let resource of resources)
target.adoptResource(resource);
}
processHAR({json, error})
{
if (error) {
WI.NetworkManager.synthesizeImportError(error);
return null;
}
if (typeof json !== "object" || json === null) {
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid JSON"));
return null;
}
if (typeof json.log !== "object" || typeof json.log.version !== "string") {
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid HAR"));
return null;
}
if (json.log.version !== "1.2") {
WI.NetworkManager.synthesizeImportError(WI.UIString("unsupported HAR version"));
return null;
}
if (!Array.isArray(json.log.entries) || !Array.isArray(json.log.pages) || !json.log.pages[0] || !json.log.pages[0].startedDateTime) {
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid HAR"));
return null;
}
let mainResourceSentWalltime = WI.HARBuilder.dateFromHARDate(json.log.pages[0].startedDateTime) / 1000;
if (isNaN(mainResourceSentWalltime)) {
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid HAR"));
return null;
}
let localResources = [];
for (let entry of json.log.entries) {
let localResource = WI.LocalResource.fromHAREntry(entry, mainResourceSentWalltime);
this._harImportLocalResourceMap.add(localResource);
localResources.push(localResource);
}
return localResources;
}
// PageObserver
frameDidNavigate(framePayload)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
var frameWasLoadedInstantly = false;
var frame = this.frameForIdentifier(framePayload.id);
if (!frame) {
// If the frame wasn't known before now, then the main resource was loaded instantly (about:blank, etc.)
// Make a new resource (which will make the frame). Mark will mark it as loaded at the end too since we
// don't expect any more events about the load finishing for these frames.
let resourceOptions = {
loaderIdentifier: framePayload.loaderId,
};
let frameOptions = {
name: framePayload.name,
securityOrigin: framePayload.securityOrigin,
};
let frameResource = this._addNewResourceToFrameOrTarget(framePayload.url, framePayload.id, resourceOptions, frameOptions);
frame = frameResource.parentFrame;
frameWasLoadedInstantly = true;
console.assert(frame);
if (!frame)
return;
}
if (framePayload.loaderId === frame.provisionalLoaderIdentifier) {
// There was a provisional load in progress, commit it.
frame.commitProvisionalLoad(framePayload.securityOrigin);
} else {
let mainResource = null;
if (frame.mainResource.url !== framePayload.url || frame.loaderIdentifier !== framePayload.loaderId) {
// Navigations like back/forward do not have provisional loads, so create a new main resource here.
mainResource = new WI.Resource(framePayload.url, {
mimeType: framePayload.mimeType,
loaderIdentifier: framePayload.loaderId,
});
} else {
// The main resource is already correct, so reuse it.
mainResource = frame.mainResource;
}
frame.initialize(framePayload.name, framePayload.securityOrigin, framePayload.loaderId, mainResource);
}
var oldMainFrame = this._mainFrame;
if (framePayload.parentId) {
var parentFrame = this.frameForIdentifier(framePayload.parentId);
console.assert(parentFrame);
if (frame === this._mainFrame)
this._mainFrame = null;
if (frame.parentFrame !== parentFrame)
parentFrame.addChildFrame(frame);
} else {
if (frame.parentFrame)
frame.parentFrame.removeChildFrame(frame);
this._mainFrame = frame;
}
if (this._mainFrame !== oldMainFrame)
this._mainFrameDidChange(oldMainFrame);
if (frameWasLoadedInstantly)
frame.mainResource.markAsFinished();
}
frameDidDetach(frameId)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
var frame = this.frameForIdentifier(frameId);
if (!frame)
return;
if (frame.parentFrame)
frame.parentFrame.removeChildFrame(frame);
this._frameIdentifierMap.delete(frame.id);
var oldMainFrame = this._mainFrame;
if (frame === this._mainFrame)
this._mainFrame = null;
frame.clearExecutionContexts();
this.dispatchEventToListeners(WI.NetworkManager.Event.FrameWasRemoved, {frame});
if (this._mainFrame !== oldMainFrame)
this._mainFrameDidChange(oldMainFrame);
}
// NetworkObserver
resourceRequestWillBeSent(requestIdentifier, frameIdentifier, loaderIdentifier, request, type, redirectResponse, timestamp, walltime, initiator, targetId)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
// COMPATIBILITY (iOS 8): Timeline timestamps for legacy backends are computed
// dynamically from the first backend timestamp received. For navigations we
// need to reset that base timestamp, and an appropriate timestamp to use is
// the new main resource's will be sent timestamp. So save this value on the
// resource in case it becomes a main resource.
var originalRequestWillBeSentTimestamp = timestamp;
var elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
if (resource) {
// This is an existing request which is being redirected, update the resource.
console.assert(resource.parentFrame.id === frameIdentifier);
console.assert(resource.loaderIdentifier === loaderIdentifier);
console.assert(!targetId);
resource.updateForRedirectResponse(request, redirectResponse, elapsedTime, walltime);
return;
}
// This is a new request, make a new resource and add it to the right frame.
resource = this._addNewResourceToFrameOrTarget(request.url, frameIdentifier, {
type,
loaderIdentifier,
targetId,
requestIdentifier,
requestMethod: request.method,
requestHeaders: request.headers,
requestData: request.postData,
requestSentTimestamp: elapsedTime,
requestSentWalltime: walltime,
initiatorCallFrames: this._initiatorCallFramesFromPayload(initiator),
initiatorSourceCodeLocation: this._initiatorSourceCodeLocationFromPayload(initiator),
initiatorNode: this._initiatorNodeFromPayload(initiator),
originalRequestWillBeSentTimestamp,
});
// Associate the resource with the requestIdentifier so it can be found in future loading events.
this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
}
webSocketCreated(requestId, url)
{
this._webSocketIdentifierToURL.set(requestId, url);
}
webSocketWillSendHandshakeRequest(requestId, timestamp, walltime, request)
{
let url = this._webSocketIdentifierToURL.get(requestId);
console.assert(url);
if (!url)
return;
// COMPATIBILITY(iOS 10.3): `walltime` did not exist in 10.3 and earlier.
if (!InspectorBackend.hasEvent("Network.webSocketWillSendHandshakeRequest", "walltime")) {
request = arguments[2];
walltime = NaN;
}
// FIXME: <webkit.org/b/168475> Web Inspector: Correctly display iframe's and worker's WebSockets
let resource = new WI.WebSocketResource(url, {
loaderIdentifier: WI.networkManager.mainFrame.id,
requestIdentifier: requestId,
requestHeaders: request.headers,
timestamp,
walltime,
requestSentTimestamp: WI.timelineManager.computeElapsedTime(timestamp),
});
let frame = this.frameForIdentifier(WI.networkManager.mainFrame.id);
frame.addResource(resource);
this._resourceRequestIdentifierMap.set(requestId, resource);
}
webSocketHandshakeResponseReceived(requestId, timestamp, response)
{
let resource = this._resourceRequestIdentifierMap.get(requestId);
console.assert(resource);
if (!resource)
return;
resource.readyState = WI.WebSocketResource.ReadyState.Open;
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
// FIXME: <webkit.org/b/169166> Web Inspector: WebSockets: Implement timing information
let responseTiming = response.timing || null;
resource.updateForResponse(resource.url, resource.mimeType, resource.type, response.headers, response.status, response.statusText, elapsedTime, responseTiming);
resource.markAsFinished(elapsedTime);
}
webSocketFrameReceived(requestId, timestamp, response)
{
this._webSocketFrameReceivedOrSent(requestId, timestamp, response);
}
webSocketFrameSent(requestId, timestamp, response)
{
this._webSocketFrameReceivedOrSent(requestId, timestamp, response);
}
webSocketClosed(requestId, timestamp)
{
let resource = this._resourceRequestIdentifierMap.get(requestId);
console.assert(resource);
if (!resource)
return;
resource.readyState = WI.WebSocketResource.ReadyState.Closed;
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
resource.markAsFinished(elapsedTime);
this._webSocketIdentifierToURL.delete(requestId);
this._resourceRequestIdentifierMap.delete(requestId);
}
_webSocketFrameReceivedOrSent(requestId, timestamp, response)
{
let resource = this._resourceRequestIdentifierMap.get(requestId);
console.assert(resource);
if (!resource)
return;
// Data going from the client to the server is always masked.
let isOutgoing = !!response.mask;
let {payloadData, payloadLength, opcode} = response;
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
resource.addFrame(payloadData, payloadLength, isOutgoing, opcode, timestamp, elapsedTime);
}
markResourceRequestAsServedFromMemoryCache(requestIdentifier)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
// We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called).
// We don't want to assert in this case since we do likely have the resource, via Page.getResourceTree. The Resource
// just doesn't have a requestIdentifier for us to look it up.
if (!resource)
return;
resource.legacyMarkServedFromMemoryCache();
}
resourceRequestWasServedFromMemoryCache(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload, timestamp, initiator)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
console.assert(!this._resourceRequestIdentifierMap.has(requestIdentifier));
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
let response = cachedResourcePayload.response;
const responseSource = InspectorBackend.Enum.Network.ResponseSource.MemoryCache;
let resource = this._addNewResourceToFrameOrTarget(cachedResourcePayload.url, frameIdentifier, {
type: cachedResourcePayload.type,
loaderIdentifier,
requestIdentifier,
requestMethod: "GET",
requestSentTimestamp: elapsedTime,
initiatorCallFrames: this._initiatorCallFramesFromPayload(initiator),
initiatorSourceCodeLocation: this._initiatorSourceCodeLocationFromPayload(initiator),
initiatorNode: this._initiatorNodeFromPayload(initiator),
});
resource.updateForResponse(cachedResourcePayload.url, response.mimeType, cachedResourcePayload.type, response.headers, response.status, response.statusText, elapsedTime, response.timing, responseSource, response.security);
resource.increaseSize(cachedResourcePayload.bodySize, elapsedTime);
resource.increaseTransferSize(cachedResourcePayload.bodySize);
resource.setCachedResponseBodySize(cachedResourcePayload.bodySize);
resource.markAsFinished(elapsedTime);
console.assert(resource.cached, "This resource should be classified as cached since it was served from the MemoryCache", resource);
if (cachedResourcePayload.sourceMapURL)
this.downloadSourceMap(cachedResourcePayload.sourceMapURL, resource.url, resource);
// No need to associate the resource with the requestIdentifier, since this is the only event
// sent for memory cache resource loads.
}
resourceRequestDidReceiveResponse(requestIdentifier, frameIdentifier, loaderIdentifier, type, response, timestamp)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
var elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
// We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called).
// We don't want to assert in this case since we do likely have the resource, via Page.getResourceTree. The Resource
// just doesn't have a requestIdentifier for us to look it up, but we can try to look it up by its URL.
if (!resource) {
var frame = this.frameForIdentifier(frameIdentifier);
if (frame)
resource = frame.resourceForURL(response.url);
// If we find the resource this way we had marked it earlier as finished via Page.getResourceTree.
// Associate the resource with the requestIdentifier so it can be found in future loading events.
// and roll it back to an unfinished state, we know now it is still loading.
if (resource) {
this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
resource.revertMarkAsFinished();
}
}
// If we haven't found an existing Resource by now, then it is a resource that was loading when the inspector
// opened and we just missed the resourceRequestWillBeSent for it. So make a new resource and add it.
if (!resource) {
resource = this._addNewResourceToFrameOrTarget(response.url, frameIdentifier, {
type,
loaderIdentifier,
requestIdentifier,
requestHeaders: response.requestHeaders,
requestSentTimestamp: elapsedTime,
});
// Associate the resource with the requestIdentifier so it can be found in future loading events.
this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
}
// COMPATIBILITY (iOS 10.3): `fromDiskCache` is legacy, replaced by `source`.
if (response.fromDiskCache)
resource.legacyMarkServedFromDiskCache();
resource.updateForResponse(response.url, response.mimeType, type, response.headers, response.status, response.statusText, elapsedTime, response.timing, response.source, response.security);
}
resourceRequestDidReceiveData(requestIdentifier, dataLength, encodedDataLength, timestamp)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
var elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
// We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called).
// We don't want to assert in this case since we do likely have the resource, via Page.getResourceTree. The Resource
// just doesn't have a requestIdentifier for us to look it up.
if (!resource)
return;
resource.increaseSize(dataLength, elapsedTime);
if (encodedDataLength !== -1)
resource.increaseTransferSize(encodedDataLength);
}
resourceRequestDidFinishLoading(requestIdentifier, timestamp, sourceMapURL, metrics)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
// By now we should always have the Resource. Either it was fetched when the inspector first opened with
// Page.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse.
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
console.assert(resource);
if (!resource)
return;
if (metrics)
resource.updateWithMetrics(metrics);
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
resource.markAsFinished(elapsedTime);
if (sourceMapURL)
this.downloadSourceMap(sourceMapURL, resource.url, resource);
this._resourceRequestIdentifierMap.delete(requestIdentifier);
}
resourceRequestDidFailLoading(requestIdentifier, canceled, timestamp, errorText)
{
// Ignore this while waiting for the whole frame/resource tree.
if (this._waitingForMainFrameResourceTreePayload)
return;
// By now we should always have the Resource. Either it was fetched when the inspector first opened with
// Page.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse.
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
console.assert(resource);
if (!resource)
return;
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
resource.markAsFailed(canceled, elapsedTime, errorText);
if (resource.parentFrame && resource === resource.parentFrame.provisionalMainResource)
resource.parentFrame.clearProvisionalLoad();
this._resourceRequestIdentifierMap.delete(requestIdentifier);
}
responseIntercepted(target, requestId, response)
{
let url = WI.urlWithoutFragment(response.url);
let localResourceOverride = this.localResourceOverrideForURL(url);
if (!localResourceOverride || localResourceOverride.disabled) {
target.NetworkAgent.interceptContinue(requestId);
return;
}
let localResource = localResourceOverride.localResource;
let revision = localResource.currentRevision;
let content = revision.content;
let base64Encoded = revision.base64Encoded;
let mimeType = revision.mimeType;
let statusCode = localResource.statusCode;
let statusText = localResource.statusText;
let responseHeaders = localResource.responseHeaders;
console.assert(revision.mimeType === localResource.mimeType);
if (isNaN(statusCode))
statusCode = undefined;
if (!statusText)
statusText = undefined;
if (!responseHeaders)
responseHeaders = undefined;
target.NetworkAgent.interceptWithResponse(requestId, content, base64Encoded, mimeType, statusCode, statusText, responseHeaders);
}
// RuntimeObserver
executionContextCreated(contextPayload)
{
let frame = this.frameForIdentifier(contextPayload.frameId);
console.assert(frame);
if (!frame)
return;
let displayName = contextPayload.name || frame.mainResource.displayName;
let target = frame.mainResource.target;
let executionContext = new WI.ExecutionContext(target, contextPayload.id, displayName, contextPayload.isPageContext, frame);
frame.addExecutionContext(executionContext);
}
// Private
_addNewResourceToFrameOrTarget(url, frameIdentifier, resourceOptions = {}, frameOptions = {})
{
console.assert(!this._waitingForMainFrameResourceTreePayload);
let resource = null;
if (!frameIdentifier && resourceOptions.targetId) {
// This is a new resource for a ServiceWorker target.
console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker);
console.assert(resourceOptions.targetId === WI.mainTarget.identifier);
resource = new WI.Resource(url, resourceOptions);
resource.target.addResource(resource);
return resource;
}
let frame = this.frameForIdentifier(frameIdentifier);
if (frame) {
// This is a new request for an existing frame, which might be the main resource or a new resource.
if (resourceOptions.type === "Document" && frame.mainResource.url === url && frame.loaderIdentifier === resourceOptions.loaderIdentifier)
resource = frame.mainResource;
else if (resourceOptions.type === "Document" && frame.provisionalMainResource && frame.provisionalMainResource.url === url && frame.provisionalLoaderIdentifier === resourceOptions.loaderIdentifier)
resource = frame.provisionalMainResource;
else {
resource = new WI.Resource(url, resourceOptions);
if (resource.target === WI.pageTarget)
this._addResourceToFrame(frame, resource);
else if (resource.target)
resource.target.addResource(resource);
else
this._addOrphanedResource(resource, resourceOptions.targetId);
}
} else {
// This is a new request for a new frame, which is always the main resource.
console.assert(WI.sharedApp.debuggableType !== WI.DebuggableType.ServiceWorker);
console.assert(!resourceOptions.targetId);
resource = new WI.Resource(url, resourceOptions);
frame = new WI.Frame(frameIdentifier, frameOptions.name, frameOptions.securityOrigin, resourceOptions.loaderIdentifier, resource);
this._frameIdentifierMap.set(frame.id, frame);
// If we don't have a main frame, assume this is it. This can change later in
// frameDidNavigate when the parent frame is known.
if (!this._mainFrame) {
this._mainFrame = frame;
this._mainFrameDidChange(null);
}
this._dispatchFrameWasAddedEvent(frame);
}
console.assert(resource);
return resource;
}
_addResourceToFrame(frame, resource)
{
console.assert(!this._waitingForMainFrameResourceTreePayload);
if (this._waitingForMainFrameResourceTreePayload)
return;
console.assert(frame);
console.assert(resource);
if (resource.loaderIdentifier !== frame.loaderIdentifier && !frame.provisionalLoaderIdentifier) {
// This is the start of a provisional load which happens before frameDidNavigate is called.
// This resource will be the new mainResource if frameDidNavigate is called.
frame.startProvisionalLoad(resource);
return;
}
// This is just another resource, either for the main loader or the provisional loader.
console.assert(resource.loaderIdentifier === frame.loaderIdentifier || resource.loaderIdentifier === frame.provisionalLoaderIdentifier);
frame.addResource(resource);
}
_addResourceToTarget(target, resource)
{
console.assert(target !== WI.pageTarget);
console.assert(resource);
target.addResource(resource);
}
_initiatorCallFramesFromPayload(initiatorPayload)
{
if (!initiatorPayload)
return null;
let callFrames = initiatorPayload.stackTrace;
if (!callFrames)
return null;
return callFrames.map((payload) => WI.CallFrame.fromPayload(WI.assumingMainTarget(), payload));
}
_initiatorSourceCodeLocationFromPayload(initiatorPayload)
{
if (!initiatorPayload)
return null;
var url = null;
var lineNumber = NaN;
var columnNumber = 0;
if (initiatorPayload.stackTrace && initiatorPayload.stackTrace.length) {
var stackTracePayload = initiatorPayload.stackTrace;
for (var i = 0; i < stackTracePayload.length; ++i) {
var callFramePayload = stackTracePayload[i];
if (!callFramePayload.url || callFramePayload.url === "[native code]")
continue;
url = callFramePayload.url;
// The lineNumber is 1-based, but we expect 0-based.
lineNumber = callFramePayload.lineNumber - 1;
columnNumber = callFramePayload.columnNumber;
break;
}
} else if (initiatorPayload.url) {
url = initiatorPayload.url;
// The lineNumber is 1-based, but we expect 0-based.
lineNumber = initiatorPayload.lineNumber - 1;
}
if (!url || isNaN(lineNumber) || lineNumber < 0)
return null;
var sourceCode = WI.networkManager.resourceForURL(url);
if (!sourceCode)
sourceCode = WI.debuggerManager.scriptsForURL(url, WI.mainTarget)[0];
if (!sourceCode)
return null;
return sourceCode.createSourceCodeLocation(lineNumber, columnNumber);
}
_initiatorNodeFromPayload(initiatorPayload)
{
return WI.domManager.nodeForId(initiatorPayload.nodeId);
}
_processServiceWorkerConfiguration(error, initializationPayload)
{
console.assert(this._waitingForMainFrameResourceTreePayload);
this._waitingForMainFrameResourceTreePayload = false;
if (error) {
console.error(JSON.stringify(error));
return;
}
console.assert(initializationPayload.targetId.startsWith("serviceworker:"));
WI.mainTarget.identifier = initializationPayload.targetId;
WI.mainTarget.name = initializationPayload.url;
// Create a main resource with this content in case the content never shows up as a WI.Script.
const sourceURL = null;
const sourceType = WI.Script.SourceType.Program;
let script = new WI.LocalScript(WI.mainTarget, initializationPayload.url, sourceURL, sourceType, initializationPayload.content);
WI.mainTarget.mainResource = script;
InspectorBackend.runAfterPendingDispatches(() => {
if (WI.mainTarget.mainResource === script) {
// We've now received all the scripts, if we don't have a better main resource use this LocalScript.
WI.debuggerManager.dataForTarget(WI.mainTarget).addScript(script);
WI.debuggerManager.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptAdded, {script});
}
});
}
_processMainFrameResourceTreePayload(error, mainFramePayload)
{
console.assert(this._waitingForMainFrameResourceTreePayload);
this._waitingForMainFrameResourceTreePayload = false;
if (error) {
console.error(JSON.stringify(error));
return;
}
console.assert(mainFramePayload);
console.assert(mainFramePayload.frame);
this._resourceRequestIdentifierMap = new Map;
this._frameIdentifierMap = new Map;
var oldMainFrame = this._mainFrame;
this._mainFrame = this._addFrameTreeFromFrameResourceTreePayload(mainFramePayload, true);
if (this._mainFrame !== oldMainFrame)
this._mainFrameDidChange(oldMainFrame);
// Emulate a main resource change within this page even though we are swapping out main frames.
// This is because many managers listen only for main resource change events to perform work,
// but they don't listen for main frame changes.
if (this._transitioningPageTarget) {
this._transitioningPageTarget = false;
this._mainFrame._dispatchMainResourceDidChangeEvent(oldMainFrame.mainResource);
}
}
_createFrame(payload)
{
// If payload.url is missing or empty then this page is likely the special empty page. In that case
// we will just say it is "about:blank" so we have a URL, which is required for resources.
let mainResource = new WI.Resource(payload.url || "about:blank", {
mimeType: payload.mimeType,
loaderIdentifier: payload.loaderId,
});
var frame = new WI.Frame(payload.id, payload.name, payload.securityOrigin, payload.loaderId, mainResource);
this._frameIdentifierMap.set(frame.id, frame);
mainResource.markAsFinished();
return frame;
}
_createResource(payload, framePayload)
{
let resource = new WI.Resource(payload.url, {
mimeType: payload.mimeType,
type: payload.type,
loaderIdentifier: framePayload.loaderId,
targetId: payload.targetId,
});
if (payload.sourceMapURL)
this.downloadSourceMap(payload.sourceMapURL, resource.url, resource);
return resource;
}
_addFrameTreeFromFrameResourceTreePayload(payload, isMainFrame)
{
var frame = this._createFrame(payload.frame);
if (isMainFrame)
frame.markAsMainFrame();
for (var i = 0; payload.childFrames && i < payload.childFrames.length; ++i)
frame.addChildFrame(this._addFrameTreeFromFrameResourceTreePayload(payload.childFrames[i], false));
for (var i = 0; payload.resources && i < payload.resources.length; ++i) {
var resourcePayload = payload.resources[i];
// The main resource is included as a resource. We can skip it since we already created
// a main resource when we created the Frame. The resource payload does not include anything
// didn't already get from the frame payload.
if (resourcePayload.type === "Document" && resourcePayload.url === payload.frame.url)
continue;
var resource = this._createResource(resourcePayload, payload);
if (resource.target === WI.pageTarget)
frame.addResource(resource);
else if (resource.target)
resource.target.addResource(resource);
else
this._addOrphanedResource(resource, resourcePayload.targetId);
if (resourcePayload.failed || resourcePayload.canceled)
resource.markAsFailed(resourcePayload.canceled);
else
resource.markAsFinished();
}
this._dispatchFrameWasAddedEvent(frame);
return frame;
}
_addOrphanedResource(resource, targetId)
{
let resources = this._orphanedResources.get(targetId);
if (!resources) {
resources = [];
this._orphanedResources.set(targetId, resources);
}
resources.push(resource);
}
_dispatchFrameWasAddedEvent(frame)
{
this.dispatchEventToListeners(WI.NetworkManager.Event.FrameWasAdded, {frame});
}
_mainFrameDidChange(oldMainFrame)
{
if (oldMainFrame)
oldMainFrame.unmarkAsMainFrame();
if (this._mainFrame)
this._mainFrame.markAsMainFrame();
this.dispatchEventToListeners(WI.NetworkManager.Event.MainFrameDidChange, {oldMainFrame});
}
_loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode)
{
this._downloadingSourceMaps.add(sourceMapURL);
let sourceMapLoaded = (error, content, mimeType, statusCode) => {
if (error || statusCode >= 400) {
this._sourceMapLoadAndParseFailed(sourceMapURL);
return;
}
if (content.slice(0, 3) === ")]}") {
let firstNewlineIndex = content.indexOf("\n");
if (firstNewlineIndex === -1) {
this._sourceMapLoadAndParseFailed(sourceMapURL);
return;
}
content = content.substring(firstNewlineIndex);
}
try {
let payload = JSON.parse(content);
let baseURL = sourceMapURL.startsWith("data:") ? originalSourceCode.url : sourceMapURL;
let sourceMap = new WI.SourceMap(baseURL, payload, originalSourceCode);
this._sourceMapLoadAndParseSucceeded(sourceMapURL, sourceMap);
} catch {
this._sourceMapLoadAndParseFailed(sourceMapURL);
}
};
if (sourceMapURL.startsWith("data:")) {
let {mimeType, base64, data} = parseDataURL(sourceMapURL);
let content = base64 ? atob(data) : data;
sourceMapLoaded(null, content, mimeType, 0);
return;
}
if (!InspectorBackend.hasDomain("Network")) {
this._sourceMapLoadAndParseFailed(sourceMapURL);
return;
}
let frameIdentifier = null;
if (originalSourceCode instanceof WI.Resource && originalSourceCode.parentFrame)
frameIdentifier = originalSourceCode.parentFrame.id;
if (!frameIdentifier)
frameIdentifier = WI.networkManager.mainFrame ? WI.networkManager.mainFrame.id : "";
let target = WI.assumingMainTarget();
target.NetworkAgent.loadResource(frameIdentifier, sourceMapURL, sourceMapLoaded);
}
_sourceMapLoadAndParseFailed(sourceMapURL)
{
this._downloadingSourceMaps.delete(sourceMapURL);
}
_sourceMapLoadAndParseSucceeded(sourceMapURL, sourceMap)
{
if (!this._downloadingSourceMaps.has(sourceMapURL))
return;
this._downloadingSourceMaps.delete(sourceMapURL);
this._sourceMapURLMap.set(sourceMapURL, sourceMap);
for (let source of sourceMap.sources())
sourceMap.addResource(new WI.SourceMapResource(source, sourceMap));
// Associate the SourceMap with the originalSourceCode.
sourceMap.originalSourceCode.addSourceMap(sourceMap);
// If the originalSourceCode was not a Resource, be sure to also associate with the Resource if one exists.
// FIXME: We should try to use the right frame instead of a global lookup by URL.
if (!(sourceMap.originalSourceCode instanceof WI.Resource)) {
console.assert(sourceMap.originalSourceCode instanceof WI.Script);
let resource = sourceMap.originalSourceCode.resource;
if (resource)
resource.addSourceMap(sourceMap);
}
}
_handleResourceContentDidChange(event)
{
let resource = event.target;
if (!(resource instanceof WI.Resource))
return;
if (!resource.isLocalResourceOverride)
return;
let localResourceOverride = this.localResourceOverrideForURL(resource.url);
console.assert(localResourceOverride);
if (!localResourceOverride)
return;
if (!this._saveLocalResourceOverridesDebouncer) {
this._pendingLocalResourceOverrideSaves = new Set;
this._saveLocalResourceOverridesDebouncer = new Debouncer(() => {
for (let localResourceOverride of this._pendingLocalResourceOverrideSaves) {
console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
}
});
}
this._pendingLocalResourceOverrideSaves.add(localResourceOverride);
this._saveLocalResourceOverridesDebouncer.delayForTime(500);
}
_handleResourceOverrideDisabledChanged(event)
{
console.assert(WI.NetworkManager.supportsLocalResourceOverrides());
let localResourceOverride = event.target;
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
let commandArguments = {
url: localResourceOverride.url,
caseSensitive: localResourceOverride.isCaseSensitive,
isRegex: localResourceOverride.isRegex,
networkStage: InspectorBackend.Enum.Network.NetworkStage.Response,
};
// COMPATIBILITY (iOS 13.0): Network.addInterception / Network.removeInterception did not exist.
for (let target of WI.targets) {
if (target.hasDomain("Network")) {
if (localResourceOverride.disabled)
target.NetworkAgent.removeInterception.invoke(commandArguments);
else
target.NetworkAgent.addInterception.invoke(commandArguments);
}
}
}
_handleBootstrapScriptContentDidChange(event)
{
let source = this._bootstrapScript.content || "";
WI.objectStores.general.put(source, NetworkManager.bootstrapScriptSourceObjectStoreKey);
if (!this._bootstrapScriptEnabledSetting.value)
return;
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
for (let target of WI.targets) {
if (target.hasCommand("Page.setBootstrapScript"))
target.PageAgent.setBootstrapScript(source);
}
}
_extraDomainsActivated(event)
{
let target = WI.assumingMainTarget();
if (target.hasDomain("Page") && event.data.domains.includes("Page"))
target.PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this));
}
_handleFrameMainResourceDidChange(event)
{
if (!event.target.isMainFrame())
return;
this._sourceMapURLMap.clear();
this._downloadingSourceMaps.clear();
}
};
WI.NetworkManager.Event = {
FrameWasAdded: "network-manager-frame-was-added",
FrameWasRemoved: "network-manager-frame-was-removed",
MainFrameDidChange: "network-manager-main-frame-did-change",
BootstrapScriptCreated: "network-manager-bootstrap-script-created",
BootstrapScriptEnabledChanged: "network-manager-bootstrap-script-enabled-changed",
BootstrapScriptDestroyed: "network-manager-bootstrap-script-destroyed",
LocalResourceOverrideAdded: "network-manager-local-resource-override-added",
LocalResourceOverrideRemoved: "network-manager-local-resource-override-removed",
};