UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

506 lines (504 loc) 20.2 kB
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