UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

258 lines (256 loc) 11.1 kB
import { Observable, throwError } from 'rxjs'; import { catchError, filter, mergeMap, take } from 'rxjs/operators'; import { LogLevel } from '../diagnostics/log-level'; import { Logging } from '../diagnostics/logging'; import { WebsocketStreamConnectionState, WebsocketStreamDataRequestState, WebsocketStreamDataState, WebsocketStreamName, WebsocketStreamProcessor } from './websocket-stream'; /** * The SSH stream provider. */ export var SshProvider; (function (SshProvider) { SshProvider["BeatProvider"] = "beatProvider"; SshProvider["ShellCommandProvider"] = "shellCommandProvider"; SshProvider["FileContentProvider"] = "fileContentProvider"; SshProvider["ProcessStatProvider"] = "processStatProvider"; SshProvider["PerformanceReadingProvider"] = "performanceReadingProvider"; SshProvider["OverviewInfoProvider"] = "overviewInfoProvider"; })(SshProvider || (SshProvider = {})); /** * The SSH content type. */ export var SshContentType; (function (SshContentType) { SshContentType["Text"] = "text"; SshContentType["Error"] = "error"; SshContentType["Json"] = "json"; })(SshContentType || (SshContentType = {})); /** * The SSH stream request type. */ export var SshStreamRequestType; (function (SshStreamRequestType) { SshStreamRequestType["Start"] = "start"; SshStreamRequestType["Update"] = "update"; SshStreamRequestType["Stop"] = "stop"; })(SshStreamRequestType || (SshStreamRequestType = {})); /** * SSH processor interface. Each SSH query creates new observable. */ class SshProcessor extends WebsocketStreamProcessor { streamRequest; dataCount = 0; /** * Initializes a new instance of the SshProcessor class. * @param observer Observer to send back result to caller. * @param target Stream target object. */ constructor(observer, target, streamRequest) { super(observer, target); this.streamRequest = streamRequest; } shouldUpdateRequest() { this.dataCount++; const streamOptions = this.streamRequest.options; // Threshold is 80% of the count. Update the request with existing parameters if update is true. if (!MsftSme.isNullOrUndefined(streamOptions) && streamOptions.count > 0 && this.streamRequest.update && this.dataCount >= streamOptions.count * 0.8) { this.dataCount = 0; return true; } return false; } } /** * The SSH stream class. */ export class SshStream { websocketStream; authorizationManager; static logSourceName = 'SshStream'; static sshAgentVersion = '06132023'; processors = new Map(); strings = MsftSme.getStrings().MsftSmeShell.Core.WebsocketStream.SshStream; /** * Initializes a new instance of the SshStream 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.SshStreamName, this); } /** * Starts/sends a request to the SSH stream. * * @param nodeName the name of the node to use for this request * @param request The @see SshStreamRequestData to send. * The @see SshStreamOptions.update will determine whether the stream auto-updates. @see SshStream.updateStreamRequest can be used to manually update the stream. * If the stream request is not updated, the stream will end after all data has been emitted. * @return Observable<SshStreamData<T>> the query observable. */ sendStreamRequest(nodeName, request) { const requestState = WebsocketStreamDataRequestState.Normal; return this.createRequest(nodeName, requestState, request); } /** * Updates a previous SSH stream request. * * @param nodeName the name of the node to use for this request * @param request The @see SshStreamRequestData to send. * @return Observable<SshStreamData<T>> the query observable. */ updateStreamRequest(nodeName, request) { const target = this.websocketStream.getTarget(this.authorizationManager, nodeName); const requestState = WebsocketStreamDataRequestState.Normal; const processor = this.processors.get(request.requestId); if (!processor || processor.streamRequest.provider !== request.provider) { Logging.log({ level: LogLevel.Warning, message: this.strings.UpdateError.message, source: SshStream.logSourceName }); return; } const updateRequestData = this.fullRequest(request); const updateRequest = { target, requestState, request: updateRequestData }; this.websocketStream.sendNext(WebsocketStreamName.SshStreamName, updateRequest); } /** * Cancel active SSH query. * 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, requestId, provider, parameters) { const target = this.websocketStream.getTarget(this.authorizationManager, nodeName); const requestState = WebsocketStreamDataRequestState.Cancel; const cancelRequestData = { requestId, provider, parameters, options: { action: SshStreamRequestType.Stop, interval: 0, count: 0 } }; const cancelRequest = { target, requestState, request: this.fullRequest(cancelRequestData) }; this.websocketStream.sendNext(WebsocketStreamName.SshStreamName, cancelRequest); } /** * Reset data for connection cleanup. */ reset() { Logging.log({ level: LogLevel.Warning, message: this.strings.ResetError.message, source: SshStream.logSourceName }); const processors = []; this.processors.forEach((value, key, map) => processors.push(value)); this.processors.clear(); processors.forEach((processor, key, map) => { 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 innerMessage = JSON.parse(message.response); const processor = this.processors.get(innerMessage.requestId ?? message.id); if (!processor) { Logging.log({ level: LogLevel.Debug, message: this.strings.UnexpectedReceivedError.message, source: SshStream.logSourceName }); return; } switch (message.state) { case WebsocketStreamDataState.Data: this.operationNext(processor, innerMessage); break; case WebsocketStreamDataState.Completed: this.operationComplete(processor, innerMessage); this.operationEnd(innerMessage.requestId); break; case WebsocketStreamDataState.Error: this.operationError(processor, { xhr: message }); this.operationEnd(innerMessage.requestId); break; case WebsocketStreamDataState.Noop: break; } } operationNext(processor, response) { processor.next(response); if (processor.shouldUpdateRequest()) { processor.streamRequest.options.action = SshStreamRequestType.Update; this.updateStreamRequest(processor.target.nodeName, processor.streamRequest); } } operationComplete(processor, response) { processor.next(response); processor.complete(); } operationError(processor, error) { processor.error(error); } operationEnd(id) { this.processors.delete(id); } createRequest(nodeName, requestState, request) { // publish object is created in 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 be ready and submit the request with // complex observable. // (else-block and this is a few cases.) let publish; if (this.websocketStream.socketStateRaw === WebsocketStreamConnectionState.Connected) { publish = this.createRequestSimple(nodeName, requestState, request); } 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, requestState, request); } 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.createRequestSimple(nodeName, requestState, request); } return throwError(error); })); } createRequestSimple(nodeName, requestState, request) { return new Observable(observer => { const target = this.websocketStream.getTarget(this.authorizationManager, nodeName); const id = this.sendRequest(observer, target, requestState, request); return () => { const processor = this.processors.get(id); if (processor) { processor.end = true; if (!processor.closed && !processor.closing) { this.cancel(processor.target.nodeName, id, processor.streamRequest.provider, processor.streamRequest.parameters ?? [""]); } } }; }); } sendRequest(observer, target, requestState, request) { const fullRequest = this.fullRequest(request); const processor = new SshProcessor(observer, target, fullRequest); this.processors.set(fullRequest.requestId, processor); this.websocketStream.sendNext(WebsocketStreamName.SshStreamName, { target, requestState, request: fullRequest }); return fullRequest.requestId; } fullRequest(request) { const requestId = request.requestId ?? MsftSme.getUniqueId(); const fullRequest = { version: SshStream.sshAgentVersion, time: new Date(), requestId, parameters: [""], ...request }; return fullRequest; } } //# sourceMappingURL=ssh-stream.js.map