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