UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

348 lines (346 loc) 14.4 kB
import { Observable, throwError } from 'rxjs'; import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators'; import { LogLevel } from '../diagnostics/log-level'; import { Logging } from '../diagnostics/logging'; import { PowerShell } from './powershell'; import { WebsocketStreamConnectionState, WebsocketStreamDataRequestState, WebsocketStreamDataState, WebsocketStreamName, WebsocketStreamProcessor } from './websocket-stream'; /** * PowerShell Processor interface. */ class PowerShellProcessor extends WebsocketStreamProcessor { } /** * The PowerShell stream class. */ export class PowerShellStream { websocketStream; authorizationManager; static logSourceName = 'PowerShellStream'; /** * The collection of set of monitors. */ static monitorSets = []; static maxRunPerNode = 5; processors = new Map(); queues = new Map(); strings = MsftSme.getStrings().MsftSmeShell.Core.WebsocketStream.PowerShellStream; /** * Register the set of monitors. * * @param monitorSet The set of monitors. */ static registerMonitorSet(monitorSet) { const found = PowerShellStream.monitorSets.find(monitors => monitors.name === monitorSet.name); if (found) { return; } PowerShellStream.monitorSets.push(monitorSet); } /** * Unregister the set of monitors. * * @param name The name of set of monitors. * @returns boolean true if unregistered the named set. */ static unregisterMonitors(name) { const found = PowerShellStream.monitorSets.find(monitors => monitors.name === name); if (found) { PowerShellStream.monitorSets.remove(found); return true; } return false; } /** * Initializes a new instance of the PowerShellStream class. * * @param websocketStream the websocket stream object. * @param authorizationManager the authorization manager object. */ constructor(websocketStream, authorizationManager) { this.websocketStream = websocketStream; this.authorizationManager = authorizationManager; websocketStream.registerProcessor(WebsocketStreamName.PowerShellStreamName, this); } /** * PowerShell script run. * * @param nodeName the node name. * @param script the script to run. * @param options the options for this request. * @return Observable<PowerShellResult> the query observable. */ run(nodeName, commandOrScript, options) { const command = PowerShell.getPowerShellCommand(commandOrScript); return this.monitorCreateRequest(nodeName, command, PowerShell.newEndpointOptions(options)); } /** * Cancel active powershell script. * Result response comes back to the original query to end. * * @param nodeName the node name. * @param id the id of original request specified as options.queryId. */ cancel(nodeName, id, options) { const target = this.getTarget(nodeName, PowerShell.newEndpointOptions(options)); const requestState = WebsocketStreamDataRequestState.Cancel; const request = { id, target, requestState, script: null }; // remove from queue if not submitted yet. const queue = this.queues.get(target.nodeName); if (queue) { const pendingRequest = queue.pendingRequests.find(entry => entry.id === id); if (pendingRequest) { queue.pendingRequests.remove(pendingRequest); queue.outstandingCount--; const processor = this.processors.get(id); this.processors.delete(id); processor.complete(); return; } } this.websocketStream.sendNext(WebsocketStreamName.PowerShellStreamName, request); } /** * Reset data for connection cleanup. */ reset() { Logging.log({ level: LogLevel.Warning, message: this.strings.ResetError.message, source: PowerShellStream.logSourceName }); const processors = []; this.processors.forEach((value) => processors.push(value)); this.processors.clear(); processors.forEach((processor) => { processor.error(new Error(this.strings.ResetError.message)); }); } /** * Process the socket message. * * @param message the socket message. */ process(message) { if (!message) { throw new Error(this.strings.NoContentError.message); } const processor = this.processors.get(message.id); if (!processor) { Logging.log({ level: LogLevel.Warning, message: this.strings.UnexpectedReceivedError.message, source: PowerShellStream.logSourceName }); return; } switch (message.state) { case WebsocketStreamDataState.Data: this.operationNext(processor, message.response); break; case WebsocketStreamDataState.Completed: this.operationComplete(processor, message.response); this.operationEnd(message.id); break; case WebsocketStreamDataState.Error: this.operationError(processor, { xhr: message }); this.operationEnd(message.id); break; case WebsocketStreamDataState.Noop: break; } } /** * Gets the JEA powershell endpoint, if it exists * * @param nodeName The node name */ getJeaEndpoint(nodeName) { return this.authorizationManager.getJeaEndpoint(nodeName); } /** * Gets websocket stream connection state. */ getWebsocketStreamConnectionState() { return this.websocketStream.socketStateRaw; } /** * Gets WebSocket State. */ get websocketState() { return this.websocketStream.socketState; } operationNext(processor, response) { const partial = processor.options && processor.options.partial; // buffering result. if (!partial) { if (!processor.response) { processor.response = response; } else { if (response.errors) { if (!processor.response.errors) { processor.response.errors = response.errors; } else { response.errors.forEach(value => processor.response.errors.push(value)); } } if (response.progress) { if (!processor.response.progress) { processor.response.progress = response.progress; } else { response.progress.forEach(value => processor.response.progress.push(value)); } } if (response.results) { if (!processor.response.results) { processor.response.results = response.results; } else { response.results.forEach(value => processor.response.results.push(value)); } } } } else { processor.next(response); } return !partial; } operationComplete(processor, response) { if (this.operationNext(processor, response)) { processor.next(processor.response); } processor.complete(); } operationError(processor, error) { processor.error(error); } operationEnd(id) { const processor = this.processors.get(id); this.processors.delete(id); const queue = this.queues.get(processor.target.nodeName); if (--queue.outstandingCount === 0) { this.queues.delete(processor.target.nodeName); } if (queue.pendingRequests.length > 0) { // if there is queued item, then send request. const request = queue.pendingRequests.shift(); this.websocketStream.sendNext(WebsocketStreamName.PowerShellStreamName, request); } } createRequest(nodeName, command, options) { // publish object is created two ways. // 1) socket is connected so submit the request immediately with simple observable. // (if-block and this is the most of cases.) // 2) socket is not connected so wait for the socket to ready and submit request with // complex observable. Initial connect and re-connection takes this observable. // (else-block and this is a few cases.) let publish; const endpoint = this.authorizationManager.getJeaEndpoint(nodeName); const newOptions = { ...(options || {}) }; if (endpoint) { newOptions.powerShellEndpoint = endpoint; } if (this.websocketStream.socketStateRaw === WebsocketStreamConnectionState.Connected) { publish = this.createRequestSimple(nodeName, command, newOptions); } else { publish = this.websocketStream.socketState .pipe(filter(state => state === WebsocketStreamConnectionState.Connected || state === WebsocketStreamConnectionState.Failed || state === WebsocketStreamConnectionState.NotConfigured), take(1), mergeMap(state => { if (state === WebsocketStreamConnectionState.Connected) { return this.createRequestSimple(nodeName, command, newOptions); } return throwError(() => new Error(this.strings.ConnectionError.message)); })); } return publish .pipe(catchError((error) => { // retry if reset connection of socket was observed. if (error && error.message === this.strings.ResetError.message) { return this.monitorCreateRequest(nodeName, command, newOptions); } if ((!options || options.noAuth !== true) && !this.authorizationManager.signOnManager.isSignOnTokenEnabled) { if (this.authorizationManager.canHandleStreamFailure(error.xhr)) { return this.authorizationManager.handleStreamFailure(nodeName, options, error.xhr) .pipe(switchMap(updatedOptions => this.monitorCreateRequest(nodeName, command, updatedOptions))); } } if (this.authorizationManager.signOnManager.isSignOnTokenEnabled) { if (this.authorizationManager.signOnManager.canHandleStreamUnauthorizedLogin(error.xhr)) { return this.authorizationManager.signOnManager.handleStreamUnauthorizedLogin(options, error.xhr) .pipe(switchMap(updatedOptions => this.monitorCreateRequest(nodeName, command, updatedOptions))); } } return throwError(() => error); })); } createRequestSimple(nodeName, command, options) { return new Observable(observer => { const target = this.getTarget(nodeName, options); const requestState = WebsocketStreamDataRequestState.Normal; const id = this.sendRequest(observer, target, requestState, command, options); return () => { const processor = this.processors.get(id); if (processor) { processor.end = true; if (!processor.closed && !processor.closing) { this.cancel(processor.target.nodeName, id); } } }; }); } sendRequest(observer, target, requestState, command, options) { const id = (options && options.queryId) || MsftSme.getUniqueId(); const request = { ...{ id, target, requestState, options }, ...command }; const processor = new PowerShellProcessor(observer, target, options); const queue = this.queues.get(target.nodeName); this.processors.set(id, processor); // During a send request, if caller provides 'options.close' as true, // we shouldn't manage the request via a queue and on Gateway, we should create a // new, one time use Runspace, which is disposed after use, instead of using one from the pool. // As currently this is not handled on Gateway, just ignore the 'options.close' for now. /* if (options && options.close) { // disposing session. this.websocketStream.sendNext(WebsocketStreamName.PowerShellStreamName, request); return id; } */ if (++queue.outstandingCount > PowerShellStream.maxRunPerNode) { queue.pendingRequests.push(request); return id; } this.websocketStream.sendNext(WebsocketStreamName.PowerShellStreamName, request); return id; } getTarget(nodeName, options) { if (!this.queues.has(nodeName)) { const queue = { outstandingCount: 0, pendingRequests: [] }; this.queues.set(nodeName, queue); } return this.websocketStream.getTarget(this.authorizationManager, nodeName, options.powerShellEndpoint); } monitorCreateRequest(nodeName, command, options) { let monitored = (nodeName1, command1, options1) => this.createRequest(nodeName1, command1, options1); for (const monitorSet of PowerShellStream.monitorSets) { monitored = this.monitor(monitored, monitorSet); } return monitored(nodeName, command, options); } monitor(target, monitorSet) { return function (nodeName, command, options) { let context; return monitorSet.preMonitor(nodeName, command, options) .pipe(switchMap(packet => { context = packet; return target(packet.nodeName, packet.command, packet.options); }), catchError((error) => monitorSet.errorMonitor(error, context)), switchMap(response => monitorSet.successMonitor(response, context))); }; } } //# sourceMappingURL=powershell-stream.js.map