UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

543 lines (541 loc) 22.6 kB
import { NativeDeferred } from '../data/native-q'; import { Net } from '../data/net'; import { LogLevel } from '../diagnostics/log-level'; import { Logging } from '../diagnostics/logging'; import { EnvironmentModule } from '../manifest/environment-modules'; import { RpcBase, RpcMessagePacketType, RpcOutboundCommands, RpcType } from './rpc-base'; import { RpcOutbound } from './rpc-outbound'; import { RpcSeekKey, RpcSeekMode } from './seek/rpc-seek-model'; /** * RpcChannel class. * - Both Shell and Module creates one instance to present itself. */ export class RpcChannel extends RpcBase { // Current Rpc mode. rpcMode; // Signature of the gateway running instance. signature; // RpcShell/RpcModule collection. rpcCollection = new Map(); sequence = 0; deferredQueue = new Map(); global = window; inboundHandlers; listenerFunction; webpackInvalid = false; /** * Initiates a new instance of the RpcChannel class. * * @param name the public name of itself. * @param origin the origin url of itself. * @param signature the signature of the gateway running instance. */ constructor(name, origin, signature) { super(null, name, origin, RpcType.Channel); this.signature = signature; if (MsftSme.isShell()) { this.rpcMode = 0 /* RpcMode.Shell */; this.depth = 0; } else { this.rpcMode = 1 /* RpcMode.Module */; this.depth = null; } } /** * Sets the rpc inbound handlers to use when creating for seek command. */ set rpcInboundHandlers(handlers) { this.inboundHandlers = handlers; } /** * Register Inbound/Outbound. * * @param rpcObject the RpcInbound/RpcOutbound class instance. * @param type the type of rpc object. */ registerRpc(rpcObject, type) { if (rpcObject.type !== type) { const message = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTypeNoMatch.message; throw new Error(message.format('registerRpc')); } this.addToCollection(rpcObject); } /** * Unregister module with subName * * @param name the name of module. * @param subName the subName. * @return RpcBase the rpc object. */ unregisterRpc(name, subName, type) { // unregister it by both origin and name. const rpcObject = this.getFromCollection(name, subName, true); if (rpcObject.type !== type) { const message = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTypeNoMatch.message; throw new Error(message.format('unregisterRpc')); } if (rpcObject) { this.removeFromCollection(rpcObject); return rpcObject; } else { const message = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcNotFoundModule.message; throw new Error(message.format(name, subName)); } } /** * Get Rpc object by module with subName for Inbound. * * @param name the name of module. * @param subName the subName. * @param type the type of rpc object. * @param exact the matching type forced. * @return RpcBase the rpc object. */ getRpc(name, subName, type, nullOk = false) { const rpcObject = this.getFromCollection(name, subName, true); if (rpcObject && rpcObject.type !== type) { if (nullOk) { return null; } const message = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTypeNoMatch.message; throw new Error(message.format('getRpc')); } // return null if it cannot find. return rpcObject; } /** * Get all Rpc objects for the specified type. */ getAllRpc(type) { const results = []; this.rpcCollection.forEach((subCollection) => { subCollection.forEach((rpc) => { if (rpc.type === type) { results.push(rpc); } }); }); return results; } /** * Get RpcInbound/RpcOutbound object for module name and module sub name. * If it doesn't configure subName yet, it returns it so the channel set it up. * * @param name the module name. * @param subName the sub name of the iframe object. * @return RpcBase the matched Rpc object. */ getFromCollection(name, subName, exact) { const subCollection = this.rpcCollection.get(name); if (subCollection == null) { return null; } return subCollection.find(value => (!exact && value.subName == null) || value.subName === subName); } removeFromCollection(rpcObject) { const subCollection = this.rpcCollection.get(rpcObject.name); if (subCollection == null) { return null; } const results = MsftSme.remove(subCollection, rpcObject); if (subCollection.length === 0) { // remove the entry if it's empty. this.rpcCollection.delete(rpcObject.name); } if (results && results.length === 1) { return results[0]; } return null; } addToCollection(rpcObject) { let subCollection = this.rpcCollection.get(rpcObject.name); if (subCollection == null) { subCollection = [rpcObject]; this.rpcCollection.set(rpcObject.name, subCollection); } else { subCollection.push(rpcObject); } } /** * Start the message listener. */ start() { this.listenerFunction = (ev) => this.listener(ev); this.global.addEventListener('message', this.listenerFunction); } /** * Stop the message listener. */ stop() { this.global.removeEventListener('message', this.listenerFunction); } /** * Post the message with retry delay. * * @param target the RpcToModule or RpcToShell object. * @param message the message packet. * @param count the retry count. * @param delay the interval milliseconds. * @return Promise<T> the promise object. */ retryPost(target, message, count, delay) { if (target == null || target.window == null) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTargetWindowNotConfigured.message; throw new Error(message2); } const deferred = new NativeDeferred(); const lastSequence = this.sequence; this.deferredQueue[this.sequence] = deferred; message.srcName = this.name; message.srcSubName = this.subName; message.srcDepth = this.depth; message.destName = target.name; message.destSubName = target.subName; message.signature = this.signature; message.sequence = this.sequence; message.type = RpcMessagePacketType.Request; // post this.sequence++; const header = `Retry ${RpcMessagePacketType[message.type]} "${message.command}" to ${message.destName}!${message.destSubName}`; this.debugLogRpcMessage(message, header); target.window.postMessage(message, target.origin); const timer = setInterval(() => { if (deferred.isPending) { if (--count < 0) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcExpiredRetry.message; clearInterval(timer); deferred.reject(message2.format(message.command)); if (this.deferredQueue[lastSequence]) { delete this.deferredQueue[lastSequence]; } return; } target.window.postMessage(message, target.origin); return; } clearInterval(timer); }, delay); return deferred.promise; } /** * Post the request message. * * @param target the RpcToModule or RpcToShell object. * @param message the message packet. * @param timeout the timeout. (10 seconds at default) * @return Promise<TResult> the promise object. */ post(target, message, timeout) { let ignoreTimeout = false; if (timeout === -1) { ignoreTimeout = true; timeout = null; } timeout = timeout || 10 * 1000; // 10 seconds if (target == null || target.window == null) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTargetWindowNotConfigured.message; throw new Error(message2); } const deferred = new NativeDeferred(); const lastSequence = this.sequence; this.deferredQueue[this.sequence] = deferred; message.srcName = this.name; message.srcSubName = this.subName; message.srcDepth = this.depth; message.destName = target.name; message.destSubName = target.subName; message.signature = this.signature; message.sequence = this.sequence; message.type = RpcMessagePacketType.Request; // post this.sequence++; const header = `${RpcMessagePacketType[message.type]} "${message.command}" to ${message.destName}!${message.destSubName}`; this.debugLogRpcMessage(message, header); target.window.postMessage(message, target.origin); setTimeout(() => { if (deferred.isPending) { if (ignoreTimeout) { deferred.resolve(); } else { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcExpired.message; deferred.reject(message2.format(this.name, this.subName, target.name, target.subName, message.command, message.type)); } } if (this.deferredQueue[lastSequence]) { delete this.deferredQueue[lastSequence]; } }, timeout); return deferred.promise; } /** * Validate the target window if exist by sending null packet. * * @param target the target Rpc object. * @return boolean if false, it remove the target from the list. */ validate(target) { try { target.window.postMessage({ validate: 'validate' }, target.origin); return true; } catch (error) { this.removeFromCollection(target); return false; } } /** * Log the debug message. * @param message the message object. * @param header the header string (used for the log group header). */ debugLog(message, header) { Logging.log({ source: 'rpc', message: message, level: LogLevel.Debug, consoleGroupHeader: header }); } /** * Process and log and rpc message. * @param message the rpc message packet * @param header the header string (used for the log group header). */ debugLogRpcMessage(message, header) { const logMessage = { ...message }; if (message.command === RpcOutboundCommands[RpcOutboundCommands.Init]) { // Why is this hidden? logMessage.data = '(hidden...)'; } this.debugLog(logMessage, header); } /** * The listen handler. * * @param messageEvent the Rpc message event. */ listener(messageEvent) { // ignore any shell hosting messages (don't handle them at all) if (MsftSme.isShell()) { const type = MsftSme.getValue(messageEvent, 'data.type'); if (typeof type === 'string' && type.startsWith('msft-sme-shell-host')) { return; } } // We are operating as an iframe, any message we get, we should format to the parent to handle if (MsftSme.isExtension() && window && window.parent && messageEvent.source === window.self && messageEvent.data && (messageEvent.data.type === 'webpackInvalid' || messageEvent.data.type === 'webpackOk')) { // The rpc channel is an iframe, we need to send a message to the parent for the parent to reload after code change // window.parent references the parent window, postMessage sends a message to the parent // the '*' refers to sending the message to any origin, this can introduce potential security // concerns so we only send messages of type 'webpackInvalid' and 'webpackOk' window.parent.postMessage(messageEvent.data, '*'); } if ( // Extension windows will only trust messages from there parent window (MsftSme.isExtension() && messageEvent.source !== window.parent) // Shell window only trusts extension origins on rpc || (MsftSme.isShell() && !EnvironmentModule.isExtensionOrigin(messageEvent.origin))) { // if we don't trust the origin, then just log a message this.debugLog('RPC listener received message from untrusted sender: {0}'.format(messageEvent)); return; } if (messageEvent.data && messageEvent.data.type) { // Watch for webpack reloads coming from the iframe if (messageEvent.data.type === 'webpackInvalid') { // Webpack is invalid, 1st message this.webpackInvalid = true; } else if (this.webpackInvalid && messageEvent.data.type === 'webpackOk') { // Webpack is okay, 2nd message, we can reload this.webpackInvalid = false; location.reload(); } } // if the message if malformed, ignore it. if (!messageEvent.data || !messageEvent.data.command) { // ignore null event. this.debugLog('RPC listener received malformed message from sender: {0}'.format(messageEvent)); return; } const message = messageEvent.data; const header = `${RpcMessagePacketType[message.type]} "${message.command}" from ${message.srcName}!${message.srcSubName}`; this.debugLogRpcMessage(message, header); if (message.signature !== this.signature) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcSignatureError.message; throw new Error(message2); } // accept shell seek query if (message.destName !== this.name) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcUnexpectedDestination.message; throw new Error(message2.format(message.destName)); } let target = this.getFromCollection(message.srcName, message.srcSubName, false); if (!target) { // unknown request was received. if (message.type === RpcMessagePacketType.Request && message.command === RpcOutboundCommands[RpcOutboundCommands.Ping]) { target = this.getFromCollection('*', '*', true); if (target) { // keep remote window object to respond. // current channel is child, and target is parent. // target could be shell or a parent module. // remove the rpcInbound object once and re-register back again with new name. this.removeFromCollection(target); target.name = message.srcName; target.subName = message.srcSubName; target.window = messageEvent.source; target.origin = messageEvent.origin; target.depth = message.srcDepth; this.subName = message.destSubName; this.depth = message.srcDepth + 1; this.registerRpc(target, RpcType.Inbound); } } } // Seek to create or delete RpcInbound on the shell to access a child call. if (message.command === RpcSeekKey.command && this.name === EnvironmentModule.nameOfShell && message.type === RpcMessagePacketType.Request) { const seekData = message.data; if (seekData.mode === RpcSeekMode.Create) { if (target) { // update window object. target.subName = message.srcSubName; target.window = messageEvent.source; target.depth = message.srcDepth; } else { target = new RpcOutbound(this, message.srcName, messageEvent.origin); target.subName = message.srcSubName; target.window = messageEvent.source; target.depth = message.srcDepth; target.registerAll(this.inboundHandlers); this.registerRpc(target, RpcType.Outbound); } } else if (seekData.mode === RpcSeekMode.Delete && target) { this.removeFromCollection(target); } } if (!target) { // ignore older/unknown response packet. current channel no longer watching it for response, but treat new request as an error. if (message.type === RpcMessagePacketType.Request) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcUnexpectedEvent.message; throw new Error(message2.format(message.srcName, message.srcSubName)); } return; } let deferred; switch (message.type) { case RpcMessagePacketType.Request: // post: processing response/error. target.handle(message.command, message.version, message.srcName, message.srcSubName, message.data).then((data) => { message.data = data; return this.response(target, message); }, error => { let logMessage = ''; let logStack = ''; if (typeof error === 'string') { message.data = error; logMessage = error; } else { message.data = {}; if (error && error.xhr) { const netError = Net.getErrorMessage(error); message.data.message = netError; logMessage = netError; } else if (error.message) { message.data.message = error.message; logMessage = error.message; } if (error.stack) { message.data.stack = error.stack; logStack = error.stack; } } Logging.log({ source: 'RpcChannel', level: LogLevel.Error, message: logMessage, stack: logStack }); // telemetry with predefined view/action name. Logging.trace({ view: 'sme-generic-error', instance: 'rpc-channel', action: 'exceptionLog', data: { stack: '' } }); return this.error(target, message); }); break; case RpcMessagePacketType.Response: // response: received result with success. deferred = this.deferredQueue[message.sequence]; if (!deferred) { if (message.command === RpcOutboundCommands[RpcOutboundCommands.Ping]) { // ping can be sent multiple times and deferred could be settled already. break; } const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcUnexpectedSequence.message; throw new Error(message2); } delete this.deferredQueue[message.sequence]; deferred.resolve(message.data); break; case RpcMessagePacketType.Error: // error: received result with error. deferred = this.deferredQueue[message.sequence]; if (!deferred) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcUnexpectedErrorSequence.message; throw new Error(message2); } delete this.deferredQueue[message.sequence]; deferred.reject(message.data); break; } } /** * Sending response message. * * @param target the RpcToModule or RpcToShell object. * @param message the Rpc message packet. */ response(target, message) { if (target == null || target.window == null) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTargetWindowNotConfigured.message; throw new Error(message2); } message.srcName = this.name; message.srcSubName = this.subName; message.srcDepth = this.depth; message.destName = target.name; message.destSubName = target.subName; message.signature = this.signature; message.type = RpcMessagePacketType.Response; // response target.window.postMessage(message, target.origin); } /** * Sending error message. * * @param target the RpcToModule or RpcToShell object. * @param message the Rpc message packet. */ error(target, message) { if (target == null || target.window == null) { const message2 = MsftSme.getStrings().MsftSmeShell.Core.Error.RpcTargetWindowNotConfigured.message; throw new Error(message2); } message.srcName = this.name; message.srcSubName = this.subName; message.srcDepth = this.depth; message.destName = target.name; message.destSubName = target.subName; message.signature = this.signature; message.type = RpcMessagePacketType.Error; // error target.window.postMessage(message, target.origin); } } //# sourceMappingURL=rpc-channel.js.map