@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
543 lines (541 loc) • 22.6 kB
JavaScript
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