/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { TYPES: { NETWORK_EVENT_STACKTRACE }, } = require("resource://devtools/server/actors/resources/index.js"); loader.lazyRequireGetter( this, "ChannelEventSinkFactory", "resource://devtools/server/actors/network-monitor/channel-event-sink.js", true ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { NetworkUtils: "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs", }); class NetworkEventStackTracesWatcher { /** * Start watching for all network event's stack traces related to a given Target actor. * * @param TargetActor targetActor * The target actor from which we should observe the strack traces * @param Object options * Dictionary object with following attributes: * - onAvailable: mandatory * This will be called for each resource. */ async watch(targetActor, { onAvailable }) { this.stacktraces = new Map(); this.onStackTraceAvailable = onAvailable; this.targetActor = targetActor; Services.obs.addObserver(this, "http-on-opening-request"); Services.obs.addObserver(this, "document-on-opening-request"); Services.obs.addObserver(this, "network-monitor-alternate-stack"); ChannelEventSinkFactory.getService().registerCollector(this); } /** * Allows clearing of network stacktrace resources */ clear() { this.stacktraces.clear(); } /** * Stop watching for network event's strack traces related to a given Target Actor. * * @param TargetActor targetActor * The target actor from which we should stop observing the strack traces */ destroy(targetActor) { this.clear(); Services.obs.removeObserver(this, "http-on-opening-request"); Services.obs.removeObserver(this, "document-on-opening-request"); Services.obs.removeObserver(this, "network-monitor-alternate-stack"); ChannelEventSinkFactory.getService().unregisterCollector(this); } onChannelRedirect(oldChannel, newChannel, flags) { // We can be called with any nsIChannel, but are interested only in HTTP channels try { oldChannel.QueryInterface(Ci.nsIHttpChannel); newChannel.QueryInterface(Ci.nsIHttpChannel); } catch (ex) { return; } const oldId = oldChannel.channelId; const stacktrace = this.stacktraces.get(oldId); if (stacktrace) { this._setStackTrace(newChannel.channelId, stacktrace); } } observe(subject, topic, data) { let channel, id; try { // We need to QI nsIHttpChannel in order to load the interface's // methods / attributes for later code that could assume we are dealing // with a nsIHttpChannel. channel = subject.QueryInterface(Ci.nsIHttpChannel); id = channel.channelId; } catch (e1) { try { channel = subject.QueryInterface(Ci.nsIIdentChannel); id = channel.channelId; } catch (e2) { // WebSocketChannels do not have IDs, so use the serial. When a WebSocket is // opened in a content process, a channel is created locally but the HTTP // channel for the connection lives entirely in the parent process. When // the server code running in the parent sees that HTTP channel, it will // look for the creation stack using the websocket's serial. try { channel = subject.QueryInterface(Ci.nsIWebSocketChannel); id = channel.serial; } catch (e3) { // Try if the channel is a nsIWorkerChannelInfo which is the substitute // of the channel in the parent process. try { channel = subject.QueryInterface(Ci.nsIWorkerChannelInfo); id = channel.channelId; } catch (e4) { // Channels which don't implement the above interfaces can appear here, // such as nsIFileChannel. Ignore these channels. return; } } } } if ( !lazy.NetworkUtils.matchRequest(channel, { targetActor: this.targetActor, }) ) { return; } if (this.stacktraces.has(id)) { // We can get up to two stack traces for the same channel: one each from // the two observer topics we are listening to. Use the first stack trace // which is specified, and ignore any later one. return; } const stacktrace = []; switch (topic) { case "http-on-opening-request": case "document-on-opening-request": { // The channel is being opened on the main thread, associate the current // stack with it. // // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be // passed around through message managers etc. let frame = Components.stack; if (frame?.caller) { frame = frame.caller; while (frame) { stacktrace.push({ filename: frame.filename, lineNumber: frame.lineNumber, columnNumber: frame.columnNumber, functionName: frame.name, asyncCause: frame.asyncCause, }); frame = frame.caller || frame.asyncCaller; } } break; } case "network-monitor-alternate-stack": { // An alternate stack trace is being specified for this channel. // The topic data is the JSON for the saved frame stack we should use, // so convert this into the expected format. // // This topic is used in the following cases: // // - The HTTP channel is opened asynchronously or on a different thread // from the code which triggered its creation, in which case the stack // from Components.stack will be empty. The alternate stack will be // for the point we want to associate with the channel. // // - The channel is not a nsIHttpChannel, and we will receive no // opening request notification for it. let frame = JSON.parse(data); while (frame) { stacktrace.push({ filename: frame.source, lineNumber: frame.line, columnNumber: frame.column, functionName: frame.functionDisplayName, asyncCause: frame.asyncCause, }); frame = frame.parent || frame.asyncParent; } break; } default: throw new Error("Unexpected observe() topic"); } this._setStackTrace(id, stacktrace); } _setStackTrace(resourceId, stacktrace) { this.stacktraces.set(resourceId, stacktrace); this.onStackTraceAvailable([ { resourceType: NETWORK_EVENT_STACKTRACE, resourceId, stacktraceAvailable: stacktrace && !!stacktrace.length, lastFrame: stacktrace && stacktrace.length ? stacktrace[0] : undefined, }, ]); } getStackTrace(id) { let stacktrace = []; if (this.stacktraces.has(id)) { stacktrace = this.stacktraces.get(id); } return stacktrace; } } module.exports = NetworkEventStackTracesWatcher;