UNPKG

@microsoft/windows-admin-center-sdk

Version:

Microsoft - Windows Admin Center Shell

350 lines (348 loc) 16.2 kB
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