diff options
Diffstat (limited to 'devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs')
-rw-r--r-- | devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs | 710 |
1 files changed, 0 insertions, 710 deletions
diff --git a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs b/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs deleted file mode 100644 index acb5e97110..0000000000 --- a/devtools/server/connectors/js-window-actor/DevToolsFrameChild.sys.mjs +++ /dev/null @@ -1,710 +0,0 @@ -/* 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/. */ - -import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; -import * as Loader from "resource://devtools/shared/loader/Loader.sys.mjs"; - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - isWindowGlobalPartOfContext: - "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", - releaseDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", - TargetActorRegistry: - "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", - useDistinctSystemPrincipalLoader: - "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", - WindowGlobalLogger: - "resource://devtools/server/connectors/js-window-actor/WindowGlobalLogger.sys.mjs", -}); - -const isEveryFrameTargetEnabled = Services.prefs.getBoolPref( - "devtools.every-frame-target.enabled", - false -); - -// Name of the attribute into which we save data in `sharedData` object. -const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher"; - -// If true, log info about WindowGlobal's being created. -const DEBUG = false; -function logWindowGlobal(windowGlobal, message) { - if (!DEBUG) { - return; - } - lazy.WindowGlobalLogger.logWindowGlobal(windowGlobal, message); -} - -export class DevToolsFrameChild extends JSWindowActorChild { - constructor() { - super(); - - // The map is indexed by the Watcher Actor ID. - // The values are objects containing the following properties: - // - connection: the DevToolsServerConnection itself - // - actor: the WindowGlobalTargetActor instance - this._connections = new Map(); - - EventEmitter.decorate(this); - - // Set the following preference on the constructor, so that we can easily - // toggle these preferences on and off from tests and have the new value being picked up. - - // bfcache-in-parent changes significantly how navigation behaves. - // We may start reusing previously existing WindowGlobal and so reuse - // previous set of JSWindowActor pairs (i.e. DevToolsFrameParent/DevToolsFrameChild). - // When enabled, regular navigations may also change and spawn new BrowsingContexts. - // If the page we navigate from supports being stored in bfcache, - // the navigation will use a new BrowsingContext. And so force spawning - // a new top-level target. - ChromeUtils.defineLazyGetter( - this, - "isBfcacheInParentEnabled", - () => - Services.appinfo.sessionHistoryInParent && - Services.prefs.getBoolPref("fission.bfcacheInParent", false) - ); - } - - /** - * Try to instantiate new target actors for the current WindowGlobal, and that, - * for all the currently registered Watcher actors. - * - * Read the `sharedData` to get metadata about all registered watcher actors. - * If these watcher actors are interested in the current WindowGlobal, - * instantiate a new dedicated target actor for each of the watchers. - * - * @param Object options - * @param Boolean options.isBFCache - * True, if the request to instantiate a new target comes from a bfcache navigation. - * i.e. when we receive a pageshow event with persisted=true. - * This will be true regardless of bfcacheInParent being enabled or disabled. - * @param Boolean options.ignoreIfExisting - * By default to false. If true is passed, we avoid instantiating a target actor - * if one already exists for this windowGlobal. - */ - instantiate({ isBFCache = false, ignoreIfExisting = false } = {}) { - const { sharedData } = Services.cpmm; - const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!sessionDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets" - ); - } - - // Create one Target actor for each prefix/client which listen to frames - for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { - const { connectionPrefix, sessionContext } = sessionData; - // For bfcache navigations, we only create new targets when bfcacheInParent is enabled, - // as this would be the only case where new DocShells will be created. This requires us to spawn a - // new WindowGlobalTargetActor as one such actor is bound to a unique DocShell. - const forceAcceptTopLevelTarget = - isBFCache && this.isBfcacheInParentEnabled; - if ( - sessionData.targets?.includes("frame") && - lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, { - forceAcceptTopLevelTarget, - }) - ) { - // If this was triggered because of a navigation, we want to retrieve the existing - // target we were debugging so we can destroy it before creating the new target. - // This is important because we had cases where the destruction of an old target - // was unsetting a flag on the **new** target document, breaking the toolbox (See Bug 1721398). - - // We're checking for an existing target given a watcherActorID + browserId + browsingContext - // Note that a target switch might create a new browsing context, so we wouldn't - // retrieve the existing target here. We are okay with this as: - // - this shouldn't happen much - // - in such case we weren't seeing the issue of Bug 1721398 (the old target can't access the new document) - const existingTarget = this._findTargetActor({ - watcherActorID, - sessionContext, - browsingContextId: this.manager.browsingContext.id, - }); - - // See comment in handleEvent(DOMDocElementInserted) to know why we try to - // create targets if none already exists - if (existingTarget && ignoreIfExisting) { - continue; - } - - // Bail if there is already an existing WindowGlobalTargetActor which wasn't - // created from a JSWIndowActor. - // This means we are reloading or navigating (same-process) a Target - // which has not been created using the Watcher, but from the client (most likely - // the initial target of a local-tab toolbox). - // However, we force overriding the first message manager based target in case of - // BFCache navigations. - if ( - existingTarget && - !existingTarget.createdFromJsWindowActor && - !isBFCache - ) { - continue; - } - - // If we decide to instantiate a new target and there was one before, - // first destroy the previous one. - // Otherwise its destroy sequence will be executed *after* the new one - // is being initialized and may easily revert changes made against platform API. - // (typically toggle platform boolean attributes back to default…) - if (existingTarget) { - existingTarget.destroy({ isTargetSwitching: true }); - } - - this._createTargetActor({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - isDocumentCreation: true, - }); - } - } - } - - /** - * Instantiate a new WindowGlobalTarget for the given connection. - * - * @param Object options - * @param String options.watcherActorID - * The ID of the WatcherActor who requested to observe and create these target actors. - * @param String options.parentConnectionPrefix - * The prefix of the DevToolsServerConnection of the Watcher Actor. - * This is used to compute a unique ID for the target actor. - * @param Object options.sessionData - * All data managed by the Watcher Actor and WatcherRegistry.sys.mjs, containing - * target types, resources types to be listened as well as breakpoints and any - * other data meant to be shared across processes and threads. - * @param Boolean options.isDocumentCreation - * Set to true if the function is called from `instantiate`, i.e. when we're - * handling a new document being created. - * @param Boolean options.fromInstantiateAlreadyAvailable - * Set to true if the function is called from handling `DevToolsFrameParent:instantiate-already-available` - * query. - */ - _createTargetActor({ - watcherActorID, - parentConnectionPrefix, - sessionData, - isDocumentCreation, - fromInstantiateAlreadyAvailable, - }) { - if (this._connections.get(watcherActorID)) { - // If this function is called as a result of a `DevToolsFrameParent:instantiate-already-available` - // message, we might have a legitimate race condition: - // In frame-helper, we want to create the initial targets for a given browser element. - // It might happen that the `DevToolsFrameParent:instantiate-already-available` is - // aborted if the page navigates (and the document is destroyed) while the query is still pending. - // In such case, frame-helper will try to send a new message. In the meantime, - // the DevToolsFrameChild `DOMWindowCreated` handler may already have been called and - // the new target already created. - // We don't want to throw in such case, as our end-goal, having a target for the document, - // is achieved. - if (fromInstantiateAlreadyAvailable) { - return; - } - throw new Error( - "DevToolsFrameChild _createTargetActor was called more than once" + - ` for the same Watcher (Actor ID: "${watcherActorID}")` - ); - } - - // Compute a unique prefix, just for this WindowGlobal, - // which will be used to create a JSWindowActorTransport pair between content and parent processes. - // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`, - // but here, we can't have access to any DevTools connection as we are really early in the content process startup - // XXX: WindowGlobal's innerWindowId should be unique across processes, I think. So that should be safe? - // (this.manager == WindowGlobalChild interface) - const forwardingPrefix = - parentConnectionPrefix + "windowGlobal" + this.manager.innerWindowId; - - logWindowGlobal( - this.manager, - "Instantiate WindowGlobalTarget with prefix: " + forwardingPrefix - ); - - const { connection, targetActor } = this._createConnectionAndActor( - forwardingPrefix, - sessionData - ); - const form = targetActor.form(); - // Ensure unregistering and destroying the related DevToolsServerConnection+Transport - // on both content and parent process JSWindowActors. - targetActor.once("destroyed", options => { - // This will destroy the content process one - this._destroyTargetActor(watcherActorID, options); - // And this will destroy the parent process one - try { - this.sendAsyncMessage("DevToolsFrameChild:destroy", { - actors: [ - { - watcherActorID, - form, - }, - ], - options, - }); - } catch (e) { - // Ignore exception when the JSWindowActorChild has already been destroyed. - // We often try to emit this message while the WindowGlobal is in the process of being - // destroyed. We eagerly destroy the target actor during reloads, - // just before the WindowGlobal starts destroying, but sendAsyncMessage - // doesn't have time to complete and throws. - if ( - !e.message.includes("JSWindowActorChild cannot send at the moment") - ) { - throw e; - } - } - }); - this._connections.set(watcherActorID, { - connection, - actor: targetActor, - }); - - // Immediately queue a message for the parent process, - // in order to ensure that the JSWindowActorTransport is instantiated - // before any packet is sent from the content process. - // As the order of messages is guaranteed to be delivered in the order they - // were queued, we don't have to wait for anything around this sendAsyncMessage call. - // In theory, the WindowGlobalTargetActor may emit events in its constructor. - // If it does, such RDP packets may be lost. - // The important point here is to send this message before processing the sessionData, - // which will start the Watcher and start emitting resources on the target actor. - this.sendAsyncMessage("DevToolsFrameChild:connectFromContent", { - watcherActorID, - forwardingPrefix, - actor: targetActor.form(), - }); - - // Pass initialization data to the target actor - for (const type in sessionData) { - // `sessionData` will also contain `browserId` as well as entries with empty arrays, - // which shouldn't be processed. - const entries = sessionData[type]; - if (!Array.isArray(entries) || !entries.length) { - continue; - } - targetActor.addOrSetSessionDataEntry( - type, - entries, - isDocumentCreation, - "set" - ); - } - } - - /** - * @param {string} watcherActorID - * @param {object} options - * @param {boolean} options.isModeSwitching - * true when this is called as the result of a change to the devtools.browsertoolbox.scope pref - */ - _destroyTargetActor(watcherActorID, options) { - const connectionInfo = this._connections.get(watcherActorID); - // This connection has already been cleaned? - if (!connectionInfo) { - throw new Error( - `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}` - ); - } - connectionInfo.connection.close(options); - this._connections.delete(watcherActorID); - if (this._connections.size == 0) { - this.didDestroy(options); - } - } - - _createConnectionAndActor(forwardingPrefix, sessionData) { - this.useCustomLoader = this.document.nodePrincipal.isSystemPrincipal; - - if (!this.loader) { - // When debugging chrome pages, use a new dedicated loader, using a distinct chrome compartment. - this.loader = this.useCustomLoader - ? lazy.useDistinctSystemPrincipalLoader(this) - : Loader; - } - const { DevToolsServer } = this.loader.require( - "resource://devtools/server/devtools-server.js" - ); - - const { WindowGlobalTargetActor } = this.loader.require( - "resource://devtools/server/actors/targets/window-global.js" - ); - - DevToolsServer.init(); - - // We want a special server without any root actor and only target-scoped actors. - // We are going to spawn a WindowGlobalTargetActor instance in the next few lines, - // it is going to act like a root actor without being one. - DevToolsServer.registerActors({ target: true }); - - const connection = DevToolsServer.connectToParentWindowActor( - this, - forwardingPrefix - ); - - // In the case of the browser toolbox, tab's BrowsingContext don't have - // any parent BC and shouldn't be considered as top-level. - // This is why we check for browserId's. - const browsingContext = this.manager.browsingContext; - const isTopLevelTarget = - !browsingContext.parent && - browsingContext.browserId == sessionData.sessionContext.browserId; - - // Create the actual target actor. - const targetActor = new WindowGlobalTargetActor(connection, { - docShell: this.docShell, - // Targets created from the server side, via Watcher actor and DevToolsFrame JSWindow - // actor pair are following WindowGlobal lifecycle. i.e. will be destroyed on any - // type of navigation/reload. - followWindowGlobalLifeCycle: true, - isTopLevelTarget, - ignoreSubFrames: isEveryFrameTargetEnabled, - sessionContext: sessionData.sessionContext, - }); - // There is no root actor in content processes and so - // the target actor can't be managed by it, but we do have to manage - // the actor to have it working and be registered in the DevToolsServerConnection. - // We make it manage itself and become a top level actor. - targetActor.manage(targetActor); - targetActor.createdFromJsWindowActor = true; - - return { connection, targetActor }; - } - - /** - * Supported Queries - */ - - sendPacket(packet, prefix) { - this.sendAsyncMessage("DevToolsFrameChild:packet", { packet, prefix }); - } - - /** - * JsWindowActor API - */ - - async sendQuery(msg, args) { - try { - const res = await super.sendQuery(msg, args); - return res; - } catch (e) { - console.error("Failed to sendQuery in DevToolsFrameChild", msg); - console.error(e.toString()); - throw e; - } - } - - receiveMessage(message) { - // Assert that the message is intended for this window global, - // except for "packet" messages which use a dedicated routing - if ( - message.name != "DevToolsFrameParent:packet" && - message.data.sessionContext.type == "browser-element" - ) { - const { browserId } = message.data.sessionContext; - // Re-check here, just to ensure that both parent and content processes agree - // on what should or should not be watched. - if ( - this.manager.browsingContext.browserId != browserId && - !lazy.isWindowGlobalPartOfContext( - this.manager, - message.data.sessionContext, - { - forceAcceptTopLevelTarget: true, - } - ) - ) { - throw new Error( - "Mismatch between DevToolsFrameParent and DevToolsFrameChild " + - (this.manager.browsingContext.browserId == browserId - ? "window global shouldn't be notified (isWindowGlobalPartOfContext mismatch)" - : `expected browsing context with browserId ${browserId}, but got ${this.manager.browsingContext.browserId}`) - ); - } - } - switch (message.name) { - case "DevToolsFrameParent:instantiate-already-available": { - const { watcherActorID, connectionPrefix, sessionData } = message.data; - - return this._createTargetActor({ - watcherActorID, - parentConnectionPrefix: connectionPrefix, - sessionData, - fromInstantiateAlreadyAvailable: true, - }); - } - case "DevToolsFrameParent:destroy": { - const { watcherActorID, options } = message.data; - return this._destroyTargetActor(watcherActorID, options); - } - case "DevToolsFrameParent:addOrSetSessionDataEntry": { - const { watcherActorID, sessionContext, type, entries, updateType } = - message.data; - return this._addOrSetSessionDataEntry( - watcherActorID, - sessionContext, - type, - entries, - updateType - ); - } - case "DevToolsFrameParent:removeSessionDataEntry": { - const { watcherActorID, sessionContext, type, entries } = message.data; - return this._removeSessionDataEntry( - watcherActorID, - sessionContext, - type, - entries - ); - } - case "DevToolsFrameParent:packet": - return this.emit("packet-received", message); - default: - throw new Error( - "Unsupported message in DevToolsFrameParent: " + message.name - ); - } - } - - /** - * Return an existing target given a WatcherActor id, a browserId and an optional - * browsing context id. - * /!\ Note that we may have multiple targets for a given (watcherActorId, browserId) couple, - * for example if we have 2 remote iframes sharing the same origin, which is why you - * might want to pass a specific browsing context id to filter the list down. - * - * @param {Object} options - * @param {Object} options.watcherActorID - * @param {Object} options.sessionContext - * @param {Object} options.browsingContextId: Optional browsing context id to narrow the - * search to a specific browsing context. - * - * @returns {WindowGlobalTargetActor|null} - */ - _findTargetActor({ watcherActorID, sessionContext, browsingContextId }) { - // First let's check if a target was created for this watcher actor in this specific - // DevToolsFrameChild instance. - const connectionInfo = this._connections.get(watcherActorID); - const targetActor = connectionInfo ? connectionInfo.actor : null; - if (targetActor) { - return targetActor; - } - - // If we couldn't find such target, we want to see if a target was created for this - // (watcherActorId,browserId, {browsingContextId}) in another DevToolsFrameChild instance. - // This might be the case if we're navigating to a new page with server side target - // enabled and we want to retrieve the target of the page we're navigating from. - if ( - lazy.isWindowGlobalPartOfContext(this.manager, sessionContext, { - forceAcceptTopLevelTarget: true, - }) - ) { - // Ensure retrieving the one target actor related to this connection. - // This allows to distinguish actors created for various toolboxes. - // For ex, regular toolbox versus browser console versus browser toolbox - const connectionPrefix = watcherActorID.replace(/watcher\d+$/, ""); - const targetActors = lazy.TargetActorRegistry.getTargetActors( - sessionContext, - connectionPrefix - ); - - if (!browsingContextId) { - return targetActors[0] || null; - } - return targetActors.find( - actor => actor.browsingContextID === browsingContextId - ); - } - return null; - } - - _addOrSetSessionDataEntry( - watcherActorID, - sessionContext, - type, - entries, - updateType - ) { - // /!\ We may have an issue here as there could be multiple targets for a given - // (watcherActorID,browserId) pair. - // This should be clarified as part of Bug 1725623. - const targetActor = this._findTargetActor({ - watcherActorID, - sessionContext, - }); - - if (!targetActor) { - throw new Error( - `No target actor for this Watcher Actor ID:"${watcherActorID}" / BrowserId:${sessionContext.browserId}` - ); - } - return targetActor.addOrSetSessionDataEntry( - type, - entries, - false, - updateType - ); - } - - _removeSessionDataEntry(watcherActorID, sessionContext, type, entries) { - // /!\ We may have an issue here as there could be multiple targets for a given - // (watcherActorID,browserId) pair. - // This should be clarified as part of Bug 1725623. - const targetActor = this._findTargetActor({ - watcherActorID, - sessionContext, - }); - // By the time we are calling this, the target may already have been destroyed. - if (!targetActor) { - return null; - } - return targetActor.removeSessionDataEntry(type, entries); - } - - handleEvent({ type, persisted, target }) { - // Ignore any event that may fire for children WindowGlobals/documents - if (target != this.document) { - return; - } - - // DOMWindowCreated is registered from FrameWatcher via `ActorManagerParent.addJSWindowActors` - // as a DOM event to be listened to and so is fired by JS Window Actor code platform code. - if (type == "DOMWindowCreated") { - this.instantiate(); - return; - } - // We might have ignored the DOMWindowCreated event because it was the initial about:blank document. - // But when loading same-process iframes, we reuse the WindowGlobal of the about:bank document - // to load the actual URL loaded in the iframe. This means we won't have a new DOMWindowCreated - // for the actual document. There is a DOMDocElementInserted fired just after, that we can catch - // to create a target for same-process iframes. - // This means that we still do not create any target for the initial documents. - // It is complex to instantiate targets for initial documents because: - // - it would mean spawning two targets for the same WindowGlobal and sharing the same innerWindowId - // - or have WindowGlobalTargets to handle more than one document (it would mean reusing will-navigate/window-ready events - // both on client and server) - if (type == "DOMDocElementInserted") { - this.instantiate({ ignoreIfExisting: true }); - return; - } - - // If persisted=true, this is a BFCache navigation. - // - // With Fission enabled and bfcacheInParent, BFCache navigations will spawn a new DocShell - // in the same process: - // * the previous page won't be destroyed, its JSWindowActor will stay alive (`didDestroy` won't be called) - // and a 'pagehide' with persisted=true will be emitted on it. - // * the new page page won't emit any DOMWindowCreated, but instead a pageshow with persisted=true - // will be emitted. - - if (type === "pageshow" && persisted) { - // Notify all bfcache navigations, even the one for which we don't create a new target - // as that's being useful for parent process storage resource watchers. - this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pageshow"); - - // Here we are going to re-instantiate a target that got destroyed before while processing a pagehide event. - // We force instantiating a new top level target, within `instantiate()` even if server targets are disabled. - // But we only do that if bfcacheInParent is enabled. Otherwise for same-process, same-docshell bfcache navigation, - // we don't want to spawn new targets. - this.instantiate({ - isBFCache: true, - }); - return; - } - - if (type === "pagehide" && persisted) { - // Notify all bfcache navigations, even the one for which we don't create a new target - // as that's being useful for parent process storage resource watchers. - this.sendAsyncMessage("DevToolsFrameChild:bf-cache-navigation-pagehide"); - - // We might navigate away for the first top level target, - // which isn't using JSWindowActor (it still uses messages manager and is created by the client, via TabDescriptor.getTarget). - // We have to unregister it from the TargetActorRegistry, otherwise, - // if we navigate back to it, the next DOMWindowCreated won't create a new target for it. - const { sharedData } = Services.cpmm; - const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME); - if (!sessionDataByWatcherActor) { - throw new Error( - "Request to instantiate the target(s) for the BrowsingContext, but `sharedData` is empty about watched targets" - ); - } - - const actors = []; - // A flag to know if the following for loop ended up destroying all the actors. - // It may not be the case if one Watcher isn't having server target switching enabled. - let allActorsAreDestroyed = true; - for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) { - const { sessionContext } = sessionData; - - // /!\ We may have an issue here as there could be multiple targets for a given - // (watcherActorID,browserId) pair. - // This should be clarified as part of Bug 1725623. - const existingTarget = this._findTargetActor({ - watcherActorID, - sessionContext, - }); - - if (!existingTarget) { - continue; - } - - // Use `originalWindow` as `window` can be set when a document was selected from - // the iframe picker in the toolbox toolbar. - if (existingTarget.originalWindow.document != target) { - throw new Error("Existing target actor is for a distinct document"); - } - // Do not do anything if both bfcache in parent and server targets are disabled - // As history navigations will be handled within the same DocShell and by the - // same WindowGlobalTargetActor. The actor will listen to pageshow/pagehide by itself. - // We should not destroy any target. - if ( - !this.isBfcacheInParentEnabled && - !sessionContext.isServerTargetSwitchingEnabled - ) { - allActorsAreDestroyed = false; - continue; - } - - actors.push({ - watcherActorID, - form: existingTarget.form(), - }); - existingTarget.destroy(); - } - - if (actors.length) { - // The most important is to unregister the actor from TargetActorRegistry, - // so that it is no longer present in the list when new DOMWindowCreated fires. - // This will also help notify the client that the target has been destroyed. - // And if we navigate back to this target, the client will receive the same target actor ID, - // so that it is really important to destroy it correctly on both server and client. - this.sendAsyncMessage("DevToolsFrameChild:destroy", { actors }); - } - - if (allActorsAreDestroyed) { - // Completely clear this JSWindow Actor. - // Do this after having called _findTargetActor, - // as it would clear the registered target actors. - this.didDestroy(); - } - } - } - - didDestroy(options) { - logWindowGlobal(this.manager, "Destroy WindowGlobalTarget"); - for (const { connection } of this._connections.values()) { - connection.close(options); - } - this._connections.clear(); - - if (this.loader) { - if (this.useCustomLoader) { - lazy.releaseDistinctSystemPrincipalLoader(this); - } - this.loader = null; - } - } -} |