@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
350 lines (348 loc) • 16.2 kB
JavaScript
import { of, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { Net } from '../data/net';
import { LogLevel } from '../diagnostics/log-level';
import { Logging } from '../diagnostics/logging';
import { EnvironmentModule } from '../manifest/environment-modules';
import { ConnectionUtility } from './connection';
/**
* The live connection status type.
*/
export var LiveConnectionStatusType;
(function (LiveConnectionStatusType) {
/**
* Online status.
*/
LiveConnectionStatusType[LiveConnectionStatusType["Online"] = 0] = "Online";
/**
* Warning status.
*/
LiveConnectionStatusType[LiveConnectionStatusType["Warning"] = 1] = "Warning";
/**
* Unauthorized status.
*/
LiveConnectionStatusType[LiveConnectionStatusType["Unauthorized"] = 2] = "Unauthorized";
/**
* Error status.
*/
LiveConnectionStatusType[LiveConnectionStatusType["Error"] = 3] = "Error";
/**
* Fatal status.
*/
LiveConnectionStatusType[LiveConnectionStatusType["Fatal"] = 4] = "Fatal";
/**
* Unknown status (used for loading status).
*/
LiveConnectionStatusType[LiveConnectionStatusType["Unknown"] = 5] = "Unknown";
/**
* Forbidden status.
*/
LiveConnectionStatusType[LiveConnectionStatusType["Forbidden"] = 6] = "Forbidden";
})(LiveConnectionStatusType || (LiveConnectionStatusType = {}));
/**
* ConnectionStream class that enables to get all connections once and listen to the change.
*
* TODO:
* 1. Support live connection status for a single connection in such a way that one could subscribe to it from ActiveConnection
* with that observable always being for the active connection.
* 2. Support updating all connection status on an interval. (using existing expiration field in cache)
* 3. Support preserving status across sessions during the interval time.
* currently we are using session storage, this may also require credentials to be preserved across sessions.
*/
export class ConnectionStream {
rpc;
connectionManager;
powershellConnection;
gatewayConnection;
nodeConnection;
authorizationManager;
extensionBroker;
statusLabelMap = {};
connectionStrings = MsftSme.getStrings().MsftSmeShell.Core.Connection;
cacheLiveConnection;
/**
* Initializes a new instance of the ConnectionStream class.
* @param connectionManager the connection manager object.
* @param powershellConnection the powerShell connection object.
* @param gatewayConnection the gateway connection object.
*/
constructor(rpc, connectionManager, powershellConnection, gatewayConnection, nodeConnection, authorizationManager, extensionBroker) {
this.rpc = rpc;
this.connectionManager = connectionManager;
this.powershellConnection = powershellConnection;
this.gatewayConnection = gatewayConnection;
this.nodeConnection = nodeConnection;
this.authorizationManager = authorizationManager;
this.extensionBroker = extensionBroker;
this.statusLabelMap[LiveConnectionStatusType.Error] = this.connectionStrings.ErrorState.label;
this.statusLabelMap[LiveConnectionStatusType.Fatal] = this.connectionStrings.FatalState.label;
this.statusLabelMap[LiveConnectionStatusType.Online] = this.connectionStrings.OnlineState.label;
this.statusLabelMap[LiveConnectionStatusType.Unauthorized] = this.connectionStrings.NeedsAuthorizationState.label;
this.statusLabelMap[LiveConnectionStatusType.Unknown] = this.connectionStrings.UnknownState.label;
this.statusLabelMap[LiveConnectionStatusType.Warning] = this.connectionStrings.WarningState.label;
}
/**
* Wraps a connection in a live connection object by retrieving its current status
* @param connection the connection object.
*/
getLiveConnection(connection) {
// get the connection types status provider
const typeInfo = ConnectionUtility.getConnectionTypeInfo(connection);
if (typeInfo && typeInfo.provider && typeInfo.provider.connectionStatusProvider) {
const now = Date.now();
const statusProvider = typeInfo.provider.connectionStatusProvider;
if (statusProvider.skipStatusCheck) {
return of({
connection: connection,
loading: false,
status: {
type: LiveConnectionStatusType.Online
},
isPowerShell: false,
powerShellEndpoint: null
});
}
if (this.cacheLiveConnection
&& this.cacheLiveConnection.status.type === LiveConnectionStatusType.Online
&& this.cacheLiveConnection.connection
&& this.cacheLiveConnection.connection.name === connection.name
&& this.cacheLiveConnection.connection.type === connection.type
&& now - this.cacheLiveConnection.lastUpdated < 2000) {
return of(this.cacheLiveConnection);
}
else {
return this.getConnectionStatus(statusProvider, connection, connection.name)
.pipe(map(data => {
this.cacheLiveConnection = data;
return data;
}));
}
}
// this should not happen, it means we have a malformed manifest or the user has uninstalled the relevant connection manager extension.
const logMessage = this.connectionStrings.NoStatusProvider.message.format(connection.type);
// log warning about this condition
Logging.log({
source: 'ConnectionStream',
level: LogLevel.Warning,
message: logMessage
});
// we dont need to fail, we can set this items status to unknown and continue
const statusLabel = this.connectionStrings.NoStatusProvider.label;
return of({
connection: connection,
loading: false,
status: {
label: statusLabel,
type: LiveConnectionStatusType.Unknown,
details: logMessage
},
isPowerShell: false,
powerShellEndpoint: null
});
}
/**
* Get connection status and aliases from statusProvider, retry alaises when connection nodeName NotFound
* @param statusProvider status provider from typeInfo manifest
* @param connection original connection
* @param nodeName retry nodeName, can be connection.name or alias
*/
getConnectionStatus(statusProvider, connection, nodeName) {
// collect the status data from the status provider
let statusAwaiter;
if (statusProvider.powerShell) {
statusAwaiter = this.getConnectionStatusFromPowershell(nodeName, statusProvider.powerShell);
}
else if (statusProvider.relativeGatewayUrl) {
statusAwaiter = this.getConnectionStatusFromGatewayUrl(nodeName, statusProvider.relativeGatewayUrl);
}
else if (statusProvider.service) {
statusAwaiter = this.getConnectionStatusFromService(nodeName, statusProvider.service);
}
else if (statusProvider.worker) {
statusAwaiter = this.getConnectionStatusFromWorker(nodeName, statusProvider.worker);
}
return statusAwaiter
.pipe(map(statusData => {
// create connection object to return
const liveConnection = {
connection: connection,
loading: false,
lastUpdated: Date.now(),
isPowerShell: !!statusProvider.powerShell,
powerShellEndpoint: statusProvider.powerShell && this.authorizationManager.getJeaEndpoint(connection.name)
};
// extract the status field from the data
if (statusData.status) {
liveConnection.status = statusData.status;
delete statusData.status;
}
else {
// if there is no status field, assume everything is good since we got this far.
liveConnection.status = {
type: LiveConnectionStatusType.Online
};
}
// load localized values for label and details
if (statusProvider.displayValueMap) {
const label = statusProvider.displayValueMap[liveConnection.status.label];
const details = statusProvider.displayValueMap[liveConnection.status.details];
liveConnection.status.label = label || liveConnection.status.label;
liveConnection.status.details = details || liveConnection.status.details;
}
// extract the aliases field from the data
if (statusData.aliases) {
this.connectionManager.saveAliasesData(statusData.aliases, connection, nodeName);
delete statusData.aliases;
}
// any other properties returned by status call live in property bag.
liveConnection.properties = statusData;
// fill properties of connection with status data.
connection.properties = connection.properties || {};
Object.assign(connection.properties, statusData);
// return the new live connection
return liveConnection;
}), catchError((error) => {
const liveConnection = {
connection: connection,
loading: false,
lastUpdated: Date.now(),
isPowerShell: !!statusProvider.powerShell,
powerShellEndpoint: statusProvider.powerShell && this.authorizationManager.getJeaEndpoint(connection.name)
};
if (Net.isUnauthorized(error)) {
liveConnection.status = {
type: LiveConnectionStatusType.Unauthorized
};
}
else if (Net.isForbidden(error)) {
liveConnection.status = {
type: LiveConnectionStatusType.Forbidden
};
}
else if (error.status === 404 /* HttpStatusCode.NotFound */ || error.status === 400 /* HttpStatusCode.BadRequest */) {
// failed connect to target node
const alias = this.connectionManager.getActiveAlias(connection.name);
// found alias, retry
if (alias) {
return this.getConnectionStatus(statusProvider, connection, alias);
}
else { // no alias, return error
// For Badrequest(400), try to extract the PS error code.
if (error.status === 400 /* HttpStatusCode.BadRequest */) {
liveConnection.status = {
type: LiveConnectionStatusType.Fatal,
details: Net.getErrorMessage(error)
};
}
else { // else just show the default connection failure error.
const label = this.connectionStrings.NoConnection.label;
const message = this.connectionStrings.NoConnection.message;
liveConnection.status = {
label: label,
type: LiveConnectionStatusType.Unknown,
details: message
};
}
}
}
else {
liveConnection.status = {
type: LiveConnectionStatusType.Fatal,
details: Net.getErrorMessage(error)
};
}
return of(liveConnection);
}), tap(liveConnection => {
if (!liveConnection.status.label) {
liveConnection.status.label = this.statusLabelMap[liveConnection.status.type]
|| this.statusLabelMap[LiveConnectionStatusType.Unknown];
}
}));
}
/**
* Retrieves a connections status from a powershell status provider
* @param nodeName the node name.
* @param options The powershell options.
*/
getConnectionStatusFromPowershell(nodeName, options) {
if (!nodeName) {
// log warning about this condition
const message = this.connectionStrings.ErrorNodeName.message;
Logging.log({
source: 'ConnectionStream',
level: LogLevel.Error,
message: message.format(nodeName)
});
return throwError(() => new Error(message));
}
const psSession = this.powershellConnection.createAutomaticSession(nodeName, { noAuth: true });
return this.powershellConnection.run(psSession, options.command ? options : options.script)
.pipe(map(response => {
// we expect the script to return an object as the first result.
// The object should have some property called 'status' but we will check this later.
return response.results[0];
}));
}
/**
* Retrieves a connections status from a gatewayUrl status provider
* @param nodeName the node name.
* @param relativeUrl the relative url from the relativeGatewayUrl provider.
*/
getConnectionStatusFromGatewayUrl(nodeName, relativeUrl) {
if (!relativeUrl) {
// log warning about this condition
const message = this.connectionStrings.ErrorGatewayUrl.message;
Logging.logError('ConnectionStream', message);
return throwError(() => new Error(message));
}
relativeUrl = relativeUrl.replace('$connectionName', nodeName);
// scan GWv2 API and use nodeConnection API so it can include authorization information.
// /Services/LinuxBase/ssh/nodes/$connectionName/connectionstatus
const segments = MsftSme.trimStart(relativeUrl, '/').split('/');
if (segments.length > 4 && segments[0].localeCompareIgnoreCase('services') === 0) {
segments.shift();
const serviceName = segments.shift();
const controllerName = segments.shift();
segments.shift();
segments.shift();
const remained = segments.join('/');
return this.nodeConnection.get({ serviceName, controllerName, nodeName }, remained, { noAuth: true });
}
return this.gatewayConnection.get(relativeUrl);
}
/**
* Retrieves a connections status from a extension service method
*/
getConnectionStatusFromService(nodeName, methodId) {
if (!methodId) {
// log warning about this condition
const message = this.connectionStrings.ErrorService.message;
Logging.log({
source: 'ConnectionStream',
level: LogLevel.Error,
message: message
});
return throwError(() => new Error(message));
}
const entryPointId = EnvironmentModule.createEntrypointId(methodId.module, methodId.name);
return this.extensionBroker.callService(entryPointId, methodId.method, methodId.version, nodeName);
}
/**
* Retrieves a connections status from a extension worker method
*/
getConnectionStatusFromWorker(nodeName, methodId) {
if (!methodId) {
// log warning about this condition
const message = this.connectionStrings.ErrorWorker.message;
Logging.log({
source: 'ConnectionStream',
level: LogLevel.Error,
message: message
});
return throwError(() => new Error(message));
}
const entryPointId = EnvironmentModule.createEntrypointId(methodId.module, methodId.name);
return this.extensionBroker.runWorker(entryPointId, methodId.method, methodId.version, nodeName);
}
}
//# sourceMappingURL=connection-stream.js.map