@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
506 lines (504 loc) • 20.2 kB
JavaScript
import { from, of, Subject, zip } from 'rxjs';
import { filter, map, mergeMap, take, tap } from 'rxjs/operators';
import { LogLevel } from '../../diagnostics/log-level';
import { Logging } from '../../diagnostics/logging';
import { RpcExtensionBrokerResponseKey } from '../../rpc/extension-broker/rpc-extension-broker-model';
import { RpcExtensionBrokerModuleSubjectServer } from '../../rpc/extension-broker/rpc-extension-broker-module-request-subject-server';
import { RpcExtensionBrokerRequestClient } from '../../rpc/extension-broker/rpc-extension-broker-request-client';
export class ExtensionBrokerQuery {
rpc;
constructor(rpc) {
this.rpc = rpc;
}
watcher;
resultEmitter;
/**
* Handles rpc response messages from the extension host.
* @param data the result of the extension host request
*/
onRpcResponse(data) {
this.watcher.next(data);
return Promise.resolve();
}
/**
* Calls a method on a extension instance and returns its result.
* @param instanceId The instance id of the extension to call
* @param method The method to call
* @param version The version of the method to call
* @param args The arguments to pass to the method
* @returns an observable for the result of the call (may be null if method call is void)
*/
call(instanceId, method, version, ...args) {
const payload = {
requestType: 'call',
instanceId: instanceId,
version: version,
method: method,
arguments: args,
callType: 'worker'
};
return this.request(payload).pipe(map((response) => response.return));
}
/**
* Destroys a extension instance
* @param instanceId The instance id of the extension to destroy
* @returns an observable for when the extension is destroyed
*/
destroy(instanceId) {
const payload = {
requestType: 'destroy',
instanceId: instanceId
};
return this.request(payload);
}
/**
* Moves a extension instance
* @param instanceId The instance id of the extension to destroy
* @param rect The rectangular position to move the extension to (relative to the iframe)
* @param zIndex the z index to move to, default is 1
* @returns an observable for when the extension is destroyed
*/
move(instanceId, rect, zIndex = 1) {
const payload = {
requestType: 'move',
instanceId: instanceId,
rect: rect,
zIndex: zIndex
};
return this.request(payload);
}
}
/**
* Extension broker listener class
* Register a listener without needed full extension broker service
*/
export class ExtensionBrokerListener {
static extensionBrokerModuleSubjectServer;
static requestReceivedSubject;
static get requestReceived() {
if (!ExtensionBrokerListener.requestReceivedSubject) {
ExtensionBrokerListener.requestReceivedSubject = new Subject();
}
return ExtensionBrokerListener.requestReceivedSubject.asObservable();
}
static initialize(rpc) {
// set up extension broker request channel
if (!ExtensionBrokerListener.extensionBrokerModuleSubjectServer) {
ExtensionBrokerListener.extensionBrokerModuleSubjectServer = new RpcExtensionBrokerModuleSubjectServer(rpc);
ExtensionBrokerListener.extensionBrokerModuleSubjectServer.subject
.subscribe(request => ExtensionBrokerListener.handleRpcRequest(request));
}
}
static handleRpcRequest(request) {
Logging.log({
level: LogLevel.Debug,
message: `Extension broker request received: ${request}`,
source: 'ExtensionBroker',
params: request
});
request.deferred.resolve();
if (ExtensionBrokerListener.requestReceivedSubject) {
ExtensionBrokerListener.requestReceivedSubject.next(request.data);
}
}
}
/**
* Extension Broker Module Side Service.
* Manages requests designated for other extensions through the shell using RPC
*/
export class ExtensionBroker extends ExtensionBrokerQuery {
/**
* The source name to use for logging
*/
get logSourceName() {
return 'ExtensionBroker';
}
/**
* Initializes a new instance of the Extension Broker class
* @param rpc The rpc to use to communicate with the shell
*/
constructor(rpc) {
super(rpc);
// set up extension broker response channel
this.watcher = new Subject();
this.rpc.register(RpcExtensionBrokerResponseKey.command, this.onRpcResponse.bind(this));
// set up extension broker request channel
ExtensionBrokerListener.initialize(rpc);
// hook up emit event pipeline
ExtensionBrokerListener.requestReceived.pipe(filter(request => request.payload && request.payload.requestType === 'emit')).subscribe(request => this.onEmitReceived(request));
this.resultEmitter = new Subject();
}
/**
* Occurs when an emit request is received from shell
*/
onEmitReceived(request) {
Logging.log({
level: LogLevel.Debug,
message: `Emit request received`,
source: this.logSourceName,
params: request
});
const payload = request.payload;
this.resultEmitter.next(payload);
}
/**
* Gets the entry point ids for all the extensions that fulfill an extension targets contract
* In some cases, as specified by the extension target, the shell may ask the user to select a preferred
* extension that fulfills the target contract. Returning only the 1 entry point id.
* @param extensionTargetId The id of the extension target
* @return An observable for the entry point ids of the extensions that fulfill an extension targets contract.
*/
getTargetExtensions(extensionTargetId) {
const payload = {
requestType: 'fulfill',
extensionTargetId: extensionTargetId
};
return this.request(payload).pipe(map((response) => {
return response.fulfillment;
}));
}
/**
* Creates a SnapIn instance given its entry point id.
* This instance is tied to the calling extension and will close when destroy is called or the calling extension is destroyed.
* @param entryPointId The id of the worker to create
*/
createSnapIn(entryPointId) {
const payload = {
requestType: 'create',
entryPointId: entryPointId
};
return this.request(payload).pipe(map((response) => {
const instance = {
instanceId: response.instanceId,
call: (method, version, ...args) => this.call(response.instanceId, method, version, ...args),
destroy: () => this.destroy(response.instanceId).pipe(map(MsftSme.noop)),
move: (rect, zIndex) => this.move(response.instanceId, rect, zIndex).pipe(map(MsftSme.noop)),
listen: (eventType) => this.listen(eventType, response.instanceId)
};
return instance;
}));
}
/**
* Finds a worker given either its entryPointId or instanceId
* @param searchOptions The search options for finding a worker instance
*/
findWorker(searchOptions) {
// if search will always result in no worker, just return no worker.
if (MsftSme.isNullOrUndefined(searchOptions)
|| (MsftSme.isNullOrUndefined(searchOptions.entryPointId)
&& MsftSme.isNullOrUndefined(searchOptions.instanceId))) {
return of(null);
}
const payload = {
requestType: 'find',
searchOptions: searchOptions
};
return this.request(payload).pipe(map((response) => {
if (!response.found) {
return null;
}
const instance = {
instanceId: response.instanceId,
extenderDefinition: response.extenderDefinition,
contract: response.contract,
call: (method, version, ...args) => this.call(response.instanceId, method, version, ...args),
destroy: () => this.destroy(response.instanceId).pipe(map(MsftSme.noop)),
listen: (eventType) => this.listen(eventType, response.instanceId)
};
return instance;
}));
}
/**
* Finds extensions by search condition.
* @param searchOptions The search options for finding manifest entry points that meet the condition.
*/
findEntriesByCondition(searchOptions) {
// set of search options must meet.
if (MsftSme.isNullOrUndefined(searchOptions)
|| !MsftSme.isNullOrUndefined(searchOptions.entryPointId)
|| !MsftSme.isNullOrUndefined(searchOptions.instanceId)
|| searchOptions.createIfNotFound
|| (searchOptions.extensionTypes == null || searchOptions.extensionTypes.length === 0)
|| MsftSme.isNullOrUndefined(searchOptions.condition)) {
throw new Error('Argument error for \"searchOptions\" on findEntriesByCondition() function');
}
const payload = {
requestType: 'find',
searchOptions: searchOptions
};
return this.request(payload).pipe(map((response) => {
const conditionValidated = searchOptions.condition.validationRequired;
let entryPoints = [];
if (response.found) {
entryPoints = response.entryPoints;
}
return { entryPoints, conditionValidated };
}));
}
/**
* return all registered entry points by type
* @param types entry point types to return
*/
getExtensionEntryPointsByType(types) {
if (MsftSme.isNullOrUndefined(types)) {
return of(null);
}
const payload = {
requestType: 'find',
searchOptions: { extensionTypes: types }
};
return this.request(payload).pipe(map((response) => {
if (!response.found) {
return null;
}
return response.entryPoints;
}));
}
/**
* Creates a worker instance given its entry point id.
* This instance is tied to the calling extension and will close when destroy is called or the calling extension is destroyed.
* @param entryPointId The id of the worker to create
*/
createWorker(entryPointId, extensionTarget) {
const payload = {
requestType: 'create',
entryPointId: entryPointId,
extensionTarget: extensionTarget
};
return this.request(payload).pipe(map((response) => {
const instance = {
instanceId: response.instanceId,
extenderDefinition: response.extenderDefinition,
contract: response.contract,
call: (method, version, ...args) => this.call(response.instanceId, method, version, ...args),
destroy: () => this.destroy(response.instanceId).pipe(map(MsftSme.noop)),
listen: (eventType) => this.listen(eventType, response.instanceId)
};
return instance;
}));
}
/**
* subscribe to results emission for specific action
*/
listen(eventType, instanceId) {
// send request to shell to add to database
// return result emitter filtered by type and instance
const payload = {
requestType: 'listen',
instanceId,
eventType
};
return this.request(payload).pipe(mergeMap(() => this.resultEmitter.asObservable()), filter(result => result.instanceId === instanceId && result.eventType === eventType));
}
/**
* Runs a worker instance and returns the result of one method. After which the worker is destroyed.
* @param entryPointId The id of the worker to create
* @param method The method to call
* @param version The version of the method to call
* @param args The arguments to pass to the method
* @returns an observable for the result of the worker.
*/
runWorker(entryPointId, method, version, ...args) {
const payload = {
requestType: 'run',
entryPointId: entryPointId,
version: version,
method: method,
arguments: args
};
return this.request(payload).pipe(map((response) => {
return response.return;
}));
}
/**
* Runs a worker instance with no association to the calling extension.
* The worker will run its own workflow until it is finished
* @param entryPointId The id of the worker to create
* @param method The method to call
* @param version The version of the method to call
* @param args The arguments to pass to the method
* @returns an observable fro the creation of the worker.
*/
startWorker(entryPointId, method, version, ...args) {
const payload = {
requestType: 'run',
expectReturn: false,
entryPointId: entryPointId,
version: version,
method: method,
arguments: args
};
return this.request(payload).pipe(map(MsftSme.noop));
}
/**
* Runs a dialog instance and returns its result. After which the dialog is destroyed.
* @param entryPointId The id of the dialog to create
* @param version The version of the dialog to call
* @param args The arguments to pass to the dialog
* @returns an observable for the result of the dialog.
*/
createDialog(entryPointId) {
const payload = {
requestType: 'create',
entryPointId: entryPointId
};
return this.request(payload).pipe(map((response) => {
const instance = {
instanceId: response.instanceId,
extenderDefinition: response.extenderDefinition,
contract: response.contract,
show: (version, ...args) => this.show(response.instanceId, version, ...args),
hide: () => this.destroy(response.instanceId).pipe(map(MsftSme.noop)),
listen: (eventType) => this.listen(eventType, response.instanceId)
};
return instance;
}));
}
/**
* Runs a dialog instance and returns its result. After which the dialog is destroyed.
* @param entryPointId The id of the dialog to create
* @param version The version of the dialog to call
* @param args The arguments to pass to the dialog
* @returns an observable for the result of the dialog.
*/
showDialog(entryPointId, version, ...args) {
const payload = {
requestType: 'run',
entryPointId: entryPointId,
version: version,
method: 'show',
arguments: args
};
return this.request(payload).pipe(map((response) => {
return response.return;
}));
}
/**
* Runs a dialog instance and returns its result. After which the dialog is destroyed.
* @param instanceId The id of the extension to show
* @param version The version of the extension tp show
* @param args The arguments to pass to the extension
* @returns an observable for the result of the dialog.
*/
show(instanceId, version, ...args) {
const payload = {
requestType: 'call',
instanceId: instanceId,
version: version,
method: 'show',
arguments: args,
callType: 'component'
};
return this.request(payload).pipe(map((response) => {
return response.return;
}));
}
/**
* Emit a result to listeners subscribed to the given eventType and instanceId
* @param instanceId The instance Id of the component emitting the result
* @param eventType the type of event being emitted, can be any string like "output", "validation", "changes"
* so listeners can subscribe to specific event streams. listeners must be aware of chosen eventType
* @param result the result to be sent to event listeners
*/
emitResult(instanceId, eventType, result) {
const payload = {
requestType: 'emit',
instanceId: instanceId,
eventType: eventType,
data: result
};
return this.request(payload);
}
/**
* Calls a method on a service extension
* @param entryPointId The id of the service to call
* @param method The method to call
* @param version The version of the method to call
* @param args The arguments to pass to the method
* @returns an observable for the result of the service method.
*/
callService(entryPointId, method, version, ...args) {
const payload = {
requestType: 'call',
instanceId: entryPointId,
version: version,
method: method,
arguments: args,
callType: 'service'
};
return this.request(payload).pipe(map((response) => {
return response.return;
}));
}
destroyRequested(entryPointId, version, args) {
const payload = {
requestType: 'call',
instanceId: entryPointId,
version: version,
method: 'destroyRequested',
arguments: args,
callType: 'component'
};
return this.request(payload).pipe(map((response) => {
return response.return;
}));
}
/**
* Makes an extension broker request via the RPC
* @param requestType The @see RpcExtensionBrokerRequestType.
* @param payload The payload for this message. Depends on requestType.
* @returns An observable for the response message from the shell
*/
request(payload) {
const request = {
requestId: MsftSme.newGuid(),
payload: payload
};
Logging.log({
level: LogLevel.Debug,
message: `Sending request to Extension Broker Service: ${request.requestId}`,
source: 'ExtensionBroker',
params: {
request: request
}
});
return zip(
// wait for our request promise to be fulfilled
from(RpcExtensionBrokerRequestClient.extensionBrokerRequest(this.rpc, request)).pipe(tap(() => {
Logging.log({
level: LogLevel.Debug,
message: `Request verified received from Extension Broker Shell Service: ${request.requestId}`,
source: 'ExtensionBroker',
params: {
request: request
}
});
})),
// wait for our response to come in via RPC, this may happen anytime after the request is sent,
// but not necessarily after its promise is resolved
this.watcher.pipe(filter((result) => {
const isMatch = result.requestId === request.requestId;
Logging.log({
level: LogLevel.Debug,
message: `Checking potential response from Extension Broker Shell Service: ${result.requestId} === ${request.requestId} => ${isMatch}`,
source: 'ExtensionBroker',
params: {
request: request,
potentialResponse: result
}
});
return isMatch;
}))).pipe(take(1), map(([_, result]) => {
if (!MsftSme.isNullOrUndefined(result)) {
if (!MsftSme.isNullOrUndefined(result.payload)) {
return result.payload;
}
else if (!MsftSme.isNullOrUndefined(result.error)) {
throw result.error;
}
}
throw new Error('Unexpected Response from Extension Broker RPC Request');
}));
}
}
//# sourceMappingURL=extension-broker.js.map