{"version":3,"sources":["../../../packages/core/data/powershell-batch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,UAAU,EAA4B,MAAM,MAAM,CAAC;AAQnE,OAAO,EAAE,eAAe,EAAmC,MAAM,oBAAoB,CAAC;AACtF,OAAO,EAAE,UAAU,EAAE,yBAAyB,EAAY,MAAM,cAAc,CAAC;AAG/E,OAAO,EAAc,iBAAiB,EAAE,iBAAiB,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAEjH;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAExC;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,EAAE,GAAG,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,GAAG,CAAC;IAEZ;;OAEG;IACH,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;CAClB;AA0FD;;GAEG;AACH,qBAAa,sBAAuB,YAAW,UAAU;IAS3B,eAAe,EAAE,eAAe;IAAE,OAAO,CAAC,QAAQ,CAAC;IAR7E;;;;;OAKG;gBACgB,eAAe,EAAE,eAAe;gBAChC,eAAe,EAAE,eAAe,EAAE,QAAQ,EAAE,yBAAyB;IAIxF;;OAEG;IACI,OAAO,IAAI,IAAI;CAKzB;AA8YD;;;;;;;;;GASG;AACH,qBAAa,eAAe;IACxB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,GAAG,CAA0C;IAE5D;;OAEG;IACH,OAAO,CAAC,OAAO,CAAyB;IAExC;;OAEG;IACH,OAAO,CAAC,KAAK,CAAoC;IAEjD;;OAEG;IACH,OAAO,CAAC,GAAG,CAAqB;IAEhC;;OAEG;IACH,OAAO,CAAC,WAAW,CAAqC;IAExD;;OAEG;IACH,OAAO,CAAC,cAAc,CAAsD;IAE5E;;OAEG;IACH,OAAO,CAAC,SAAS,CAAS;IAE1B;;;;;;;;OAQG;WACW,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,eAAe,EAAE,eAAe,GAAG,eAAe;WAC9E,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,eAAe,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,yBAAyB,GAAG,eAAe;WAChI,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,eAAe,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,yBAAyB,EACxH,cAAc,EAAE,+BAA+B,GAAG,eAAe;IAyBrE;;;;;OAKG;WACW,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe;IAIrE;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,SAAS;IAIxB;;;;;;;;OAQG;gBAEC,QAAQ,EAAE,MAAM,EAAE,EAClB,eAAe,EAAE,eAAe,EAChC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,yBAAyB,EACnC,OAAO,EAAE,+BAA+B;IAe5C;;;;;;OAMG;IACI,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,UAAU,CAAC,2BAA2B,EAAE,CAAC;IAS3H;;;;;;OAMG;IACI,GAAG,CAAC,YAAY,EAAE,iBAAiB,EAAE,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,UAAU,CAAC,2BAA2B,EAAE,CAAC;IA0BrH;;OAEG;IACI,MAAM,IAAI,UAAU,CAAC,2BAA2B,EAAE,CAAC;IAI1D;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;IAUf;;OAEG;IACH,OAAO,CAAC,OAAO;IA+Cf;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;IAgFf;;;;;OAKG;IACH,OAAO,CAAC,4BAA4B;IAWpC;;;;OAIG;IACH,OAAO,CAAC,WAAW;IASnB;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;CAkB3B","file":"powershell-batch.d.ts","sourcesContent":["import { EMPTY, Observable, Observer, of, throwError } from 'rxjs';\r\nimport { catchError, expand, map } from 'rxjs/operators';\r\nimport { LogLevel } from '../diagnostics/log-level';\r\nimport { LogRecord } from '../diagnostics/log-record';\r\nimport { Logging } from '../diagnostics/logging';\r\nimport { SmeWebTelemetry } from '../diagnostics/sme-web-telemetry';\r\nimport { TelemetryEventStates } from '../diagnostics/sme-web-telemetry-models';\r\nimport { Strings } from '../generated/strings';\r\nimport { BatchConnection, BatchRequest, BatchResponseItem } from './batch-connection';\r\nimport { Disposable, DisposableLifetimeManager, Disposer } from './disposable';\r\nimport { HttpMethod } from './http';\r\nimport { Net } from './net';\r\nimport { PowerShell, PowerShellCommand, PowerShellOptions, PowerShellSessionRequestOptions } from './powershell';\r\n\r\n/**\r\n * PowerShell batch response Item.\r\n */\r\nexport interface PowerShellBatchResponseItem {\r\n\r\n    /**\r\n     * The request Sequence number to which this response correspond to.\r\n     */\r\n    sequenceNumber: number;\r\n\r\n    /**\r\n     * The status of the specific call to a node.\r\n     */\r\n    status: number;\r\n\r\n    /**\r\n     * The node name returning the response.\r\n     */\r\n    nodeName: string;\r\n\r\n    /**\r\n     * The Json properties returned from node.\r\n     */\r\n    properties: any;\r\n\r\n    /**\r\n     * The error json obj, in case of error.\r\n     */\r\n    error?: any;\r\n\r\n    /**\r\n     * The errors json array obj, in case of aggregated errors.\r\n     */\r\n    errors?: any[];\r\n}\r\n\r\n/**\r\n * PowerShell context object interface.\r\n */\r\ninterface PowerShellBatchContext {\r\n\r\n    /**\r\n     * The shared key name for runspace.\r\n     */\r\n    key: string;\r\n\r\n    /**\r\n     * The nodes list targeted by this PowerShellBatch object.\r\n     */\r\n    nodesList: string[];\r\n\r\n    /**\r\n     * The array of referenced containers.\r\n     */\r\n    lifetimes: DisposableLifetimeManager[];\r\n\r\n    /**\r\n     * The request options for the powershell session\r\n     */\r\n    requestOptions: PowerShellSessionRequestOptions;\r\n}\r\n\r\n/**\r\n * PowerShell batch saved runspace session for a node.\r\n */\r\ninterface RunspaceSessionContext {\r\n\r\n    /**\r\n     * The runspace session Id.\r\n     */\r\n    sessionId: string;\r\n\r\n    /**\r\n     * The runspace creation time. Used to check if session is expired.\r\n     */\r\n    creationTimestamp: number;\r\n}\r\n\r\n/**\r\n * PowerShell runspace session state.\r\n */\r\nenum RunspaceSessionState {\r\n    /**\r\n     * Runspace still active.\r\n     */\r\n    Active,\r\n\r\n    /**\r\n     * Runspace already expired.\r\n     */\r\n    Expired,\r\n\r\n    /**\r\n     * No runsapce available for the give node.\r\n     * Either this is the first call, or the previous call error out\r\n     * or previous runspace was deleted due to expiry.\r\n     */\r\n    Unavailable\r\n}\r\n\r\n/**\r\n * PowerShell command queue item.\r\n */\r\ninterface PowerShellBatchCommandItem {\r\n    /**\r\n     * The nodes to execute command against.\r\n     */\r\n    nodesList: string[];\r\n    /**\r\n     * The command list to execute.\r\n     */\r\n    commandList: string[];\r\n\r\n    /**\r\n     * Options for how to handle the command (reserved)\r\n     */\r\n    options?: PowerShellOptions;\r\n\r\n    /**\r\n     * Deferred object currently waiting for command run.\r\n     */\r\n    observer: Observer<any>;\r\n}\r\n\r\n/**\r\n * The PowerShellBatchSession class.\r\n */\r\nexport class PowerShellBatchSession implements Disposable {\r\n    /**\r\n     * Initializes a new instance of the PowerShellBatchSession class.\r\n     *\r\n     * @param powerShellBatch the PowerShellBatch object.\r\n     * @param lifetime the disposable lifetime manager object.\r\n     */\r\n    public constructor(powerShellBatch: PowerShellBatch);\r\n    public constructor(powerShellBatch: PowerShellBatch, lifetime: DisposableLifetimeManager);\r\n    public constructor(public powerShellBatch: PowerShellBatch, private lifetime?: DisposableLifetimeManager) {\r\n    }\r\n\r\n    /**\r\n     * Dispose the session object.\r\n     */\r\n    public dispose(): void {\r\n        if (this.lifetime) {\r\n            this.lifetime.dispose();\r\n        }\r\n    }\r\n}\r\n\r\n/**\r\n * Class containing methods related to PowerShell runspaces creation/deletion/command using PowerShell Raw API plugin during batch run.\r\n *  - It's auto holding the runspace as long as it's used within last 3 minutes.\r\n */\r\nclass PowerShellBatchRaw implements Disposable {\r\n    // 3 minutes session holding time.\r\n    private static maxDeltaTimeInMs: number = 3 * 60 * 1000;\r\n    private nodesToSessionIdsMap: MsftSme.StringMap<RunspaceSessionContext> = {};\r\n    private timestampInMs = 0;\r\n    private markDelete = false;\r\n    private internalActive = false;\r\n    private requestedNodesList: string[];\r\n\r\n    /**\r\n     * Initializes a new instance of the PowerShellBatchRaw class.\r\n     *\r\n     * @param batchConnection The batch connection service.\r\n     * @param context The PowerShell batch session Context.\r\n     */\r\n    constructor(private batchConnection: BatchConnection, private context: PowerShellBatchContext) {\r\n\r\n    }\r\n\r\n    /**\r\n     * Gets active status of PowerShell execution.\r\n     */\r\n    public get active(): boolean {\r\n        return this.internalActive;\r\n    }\r\n\r\n    /**\r\n     * Dispose the runspace.\r\n     */\r\n    public dispose(): void {\r\n        if (!this.active) {\r\n            // only close sessions that have been created.\r\n            // If a result was cached a component may not\r\n            // execute a command and still dispose the session\r\n            // when the component is destroyed.\r\n            if (Object.keys(this.nodesToSessionIdsMap).length > 0) {\r\n                this.close().subscribe();\r\n            }\r\n        } else {\r\n            this.markDelete = true;\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Runs the given batch command, and try followup Get calls if all nodes don't complete during the initial batch call.\r\n     *\r\n     * @param nodesList The nodes list to run batch against.\r\n     * @param commandList The list of command body, corresponding to nodesList.\r\n     */\r\n    public runCommand(nodesList: string[], commandList: string[]): Observable<PowerShellBatchResponseItem[]> {\r\n        // take the timestamp only success/healthy case.\r\n        // error session would be auto-deleted after expiration time.\r\n        this.internalActive = true;\r\n        this.requestedNodesList = nodesList;\r\n\r\n        return this.command(nodesList, commandList)\r\n            .pipe(\r\n                catchError((error) => {\r\n                    this.internalActive = false;\r\n                    SmeWebTelemetry.tracePowershellBatchEvent(commandList, TelemetryEventStates.Error, error);\r\n                    return throwError(() => error);\r\n\r\n                }),\r\n                expand((response: BatchResponseItem[]) => {\r\n                    this.timestampInMs = Date.now();\r\n\r\n                    const psBatchResponse: PowerShellBatchResponseItem[] =\r\n                        this.convertBatchResponseToPowerShellBatchResponse(response);\r\n\r\n                    const incompleteNodes: string[] = this.getIncompleteNodes(psBatchResponse);\r\n                    if (incompleteNodes.length === 0) {\r\n                        this.internalActive = false;\r\n                        return EMPTY;\r\n                    }\r\n\r\n                    // create list of Get URLs for incomplete nodes.\r\n                    const incompleteNodesUrlList = this.createRelativeUrlListSingleMethod(incompleteNodes, HttpMethod.Get);\r\n\r\n                    // update requested Nodes list, so we can parse the response correctly.\r\n                    this.requestedNodesList = incompleteNodesUrlList;\r\n                    return this.batchConnection.get(incompleteNodes, incompleteNodesUrlList, <BatchRequest>this.context.requestOptions);\r\n\r\n                }),\r\n                map((response: BatchResponseItem[]) => {\r\n                    const psBatchResponse: PowerShellBatchResponseItem[] =\r\n                        this.convertBatchResponseToPowerShellBatchResponse(response);\r\n\r\n                    const errorResponses =\r\n                        psBatchResponse.filter((psResponse) => psResponse.error || psResponse.errors);\r\n\r\n                    if (errorResponses.length > 0) {\r\n                        SmeWebTelemetry.tracePowershellBatchEvent(commandList,\r\n                            TelemetryEventStates.Error,\r\n                            {response: errorResponses});\r\n                    }\r\n\r\n                    return psBatchResponse;\r\n                }));\r\n    }\r\n\r\n    /**\r\n     * Close/Delete the session / runspace map.\r\n     */\r\n    public close(): Observable<PowerShellBatchResponseItem[]> {\r\n        if (Object.keys(this.nodesToSessionIdsMap).length > 0) {\r\n\r\n            const nodeList: string[] = [];\r\n            for (const node in this.nodesToSessionIdsMap) {\r\n                if (this.nodesToSessionIdsMap.hasOwnProperty(node)) {\r\n                    nodeList.push(node);\r\n                }\r\n            }\r\n\r\n            const nodeUrls = this.createRelativeUrlListSingleMethod(nodeList, HttpMethod.Delete);\r\n\r\n            return this.batchConnection.delete(nodeList, nodeUrls, <BatchRequest>this.context.requestOptions)\r\n                .pipe(\r\n                    map((responseData: BatchResponseItem[]) => {\r\n                        this.nodesToSessionIdsMap = {};\r\n                        const psBatchResponse: PowerShellBatchResponseItem[]\r\n                            = this.convertBatchResponseToPowerShellBatchResponse(responseData);\r\n                        return psBatchResponse;\r\n                    }));\r\n        }\r\n\r\n        Logging.log({\r\n            level: LogLevel.Warning,\r\n            source: 'PowerShellBatch/close',\r\n            message: MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.PowerShellUnableSessionClose.message\r\n        });\r\n        return of(null);\r\n    }\r\n\r\n    /**\r\n     * Cancel the command.\r\n     */\r\n    public cancelCommand(): Observable<PowerShellBatchResponseItem[]> {\r\n        if (Object.keys(this.nodesToSessionIdsMap).length > 0) {\r\n\r\n            const nodeList: string[] = [];\r\n            for (const node in this.nodesToSessionIdsMap) {\r\n                if (this.nodesToSessionIdsMap.hasOwnProperty(node)) {\r\n                    nodeList.push(node);\r\n                }\r\n            }\r\n\r\n            const nodeUrls = this.createRelativeUrlListSingleMethod(nodeList, 'CANCEL');\r\n\r\n            return this.batchConnection.put(nodeList, nodeUrls)\r\n                .pipe(map((responseData: any) => {\r\n                    this.nodesToSessionIdsMap = {};\r\n                    const psBatchResponse: PowerShellBatchResponseItem[] = this.convertBatchResponseToPowerShellBatchResponse(responseData);\r\n                    return psBatchResponse;\r\n                }));\r\n        }\r\n\r\n        Logging.log({\r\n            level: LogLevel.Warning,\r\n            source: 'PowerShell',\r\n            message: MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.PowerShellUnableCancelCommand.message\r\n        });\r\n        return of(null);\r\n    }\r\n\r\n    /**\r\n     * Parse the response array for multi-part response and convert to PowerShellBatchResponse list.\r\n     *\r\n     * @param responseList The BatchResponse array received for the powershell batch call.\r\n     */\r\n    private convertBatchResponseToPowerShellBatchResponse(responseList: BatchResponseItem[]): PowerShellBatchResponseItem[] {\r\n\r\n        const powershellBatchResponse: PowerShellBatchResponseItem[] = [];\r\n\r\n        for (let itemId = 0; itemId < responseList.length; itemId++) {\r\n\r\n            const responseItem = responseList[itemId].response;\r\n            const nodeName = responseList[itemId].nodeName;\r\n            const sequenceNumber = responseList[itemId].sequenceNumber;\r\n            const jsonResponse = responseItem.response;\r\n            const status = responseItem.status;\r\n            const properties: any = Net.getItemProperties(jsonResponse);\r\n\r\n            if (status < 400) {\r\n                powershellBatchResponse.push(<PowerShellBatchResponseItem>{ sequenceNumber, status, nodeName, properties });\r\n\r\n            } else {\r\n                const responseData = responseItem.response;\r\n                if (responseData.error) {\r\n                    const error: any = responseData.error;\r\n                    powershellBatchResponse.push(<PowerShellBatchResponseItem>{ sequenceNumber, status, nodeName, properties, error });\r\n                    Logging.log(<LogRecord>{\r\n                        source: 'Batch PowerShell',\r\n                        level: LogLevel.Error,\r\n                        message: MsftSme.getStrings<Strings>().MsftSmeShell.Core.Error.BatchConnection.message\r\n                            .format(status, error.code, Net.getPowerShellErrorMessage(responseItem.response))\r\n                    });\r\n                } else if (responseData.errors) {\r\n                    const errors: any = responseData.errors;\r\n                    powershellBatchResponse.push(<PowerShellBatchResponseItem>{ sequenceNumber, status, nodeName, properties, errors });\r\n                    Logging.log(<LogRecord>{\r\n                        source: 'Batch PowerShell',\r\n                        level: LogLevel.Error,\r\n                        message: Net.getPowerShellErrorMessage(responseItem.response)\r\n                    });\r\n                }\r\n            }\r\n        }\r\n\r\n        return powershellBatchResponse;\r\n    }\r\n\r\n    /**\r\n     * Initiate command execution. It auto recycles old sessions.\r\n     *\r\n     * @param nodesList The list of nodes to run commands against\r\n     * @param commandList The command body list corresponding to nodesList.\r\n     */\r\n    private command(nodesList: string[], commandList: string[]): Observable<BatchResponseItem[]> {\r\n\r\n        const nodesSessionStateMap: MsftSme.StringMap<RunspaceSessionState> = this.getSessionsStateForNodesList(nodesList);\r\n        const methodsList: string[] = [];\r\n\r\n        for (let index = 0; index < nodesList.length; index++) {\r\n\r\n            const nodeName = nodesList[index];\r\n\r\n            if (nodesSessionStateMap[nodeName] === RunspaceSessionState.Expired) {\r\n                // Delete item from map.\r\n                delete this.nodesToSessionIdsMap[nodeName];\r\n            }\r\n\r\n            if (nodesSessionStateMap[nodeName] === RunspaceSessionState.Active) {\r\n                // Post method\r\n                methodsList.push(HttpMethod.Post);\r\n\r\n            } else {\r\n                // Put method\r\n                methodsList.push(HttpMethod.Put);\r\n            }\r\n        }\r\n\r\n        const nodeUrls = this.createRelativeUrlList(nodesList, methodsList);\r\n        return this.batchConnection.mixed(nodesList, nodeUrls, commandList, methodsList, <BatchRequest>this.context.requestOptions);\r\n    }\r\n\r\n    /**\r\n     * Check if a valid/non-expired sesison exists for each node in the list.\r\n     *\r\n     * @param nodesList The nodes list to check valid existing sesion for.\r\n     */\r\n    private getSessionsStateForNodesList(nodesList: string[]): MsftSme.StringMap<RunspaceSessionState> {\r\n\r\n        const runspaceSessionsState: MsftSme.StringMap<RunspaceSessionState> = {};\r\n\r\n        for (let index = 0; index < nodesList.length; index++) {\r\n            const savedSession: RunspaceSessionContext = this.nodesToSessionIdsMap[nodesList[index]];\r\n            if (!savedSession) {\r\n                runspaceSessionsState[nodesList[index]] = RunspaceSessionState.Unavailable;\r\n\r\n            } else if (this.isSessionEntryExpired(savedSession)) {\r\n                runspaceSessionsState[nodesList[index]] = RunspaceSessionState.Expired;\r\n\r\n            } else {\r\n                runspaceSessionsState[nodesList[index]] = RunspaceSessionState.Active;\r\n            }\r\n        }\r\n        return runspaceSessionsState;\r\n    }\r\n\r\n    /**\r\n     * Create a relative url list for PowerShell Post batch call, based on nodes and methods list.\r\n     *\r\n     * @param nodesList The list of nodes to generate urls for\r\n     * @param methodList The http method types map corresponding to nodesList.\r\n     */\r\n    private createRelativeUrlList(nodesList: string[], methodList: string[]): string[] {\r\n\r\n        const responseUrlList: string[] = [];\r\n\r\n        for (let index = 0; index < nodesList.length; index++) {\r\n\r\n            const nodeName = nodesList[index];\r\n            const method = methodList[index];\r\n\r\n            // try to get session ids for given Node from the stored map.\r\n            const savedSession: RunspaceSessionContext = this.nodesToSessionIdsMap[nodeName];\r\n            const sessionId = (savedSession && !this.isSessionEntryExpired(savedSession)) ? savedSession.sessionId : MsftSme.newGuid();\r\n\r\n            responseUrlList.push(this.createRelativeUrl(method, sessionId));\r\n        }\r\n\r\n        return responseUrlList;\r\n    }\r\n\r\n    /**\r\n     * Create a relative url list for PowerShell batch call, based on provided method.\r\n     *\r\n     * @param nodesList The list of nodes to generate urls for\r\n     * @param method The http method type\r\n     */\r\n    private createRelativeUrlListSingleMethod(nodesList: string[], method: string): string[] {\r\n\r\n        const responseUrlList: string[] = [];\r\n\r\n        for (let index = 0; index < nodesList.length; index++) {\r\n\r\n            const nodeName = nodesList[index];\r\n\r\n            // try to get session ids for given Node from the stored map.\r\n            const savedSession: RunspaceSessionContext = this.nodesToSessionIdsMap[nodeName];\r\n            const sessionId = (savedSession && !this.isSessionEntryExpired(savedSession)) ? savedSession.sessionId : MsftSme.newGuid();\r\n\r\n            responseUrlList.push(this.createRelativeUrl(method, sessionId));\r\n        }\r\n\r\n        return responseUrlList;\r\n    }\r\n\r\n    /**\r\n     * Create a relative url for the given method and session Id.\r\n     *\r\n     * @param method The Http method to use for call.\r\n     * @param sessionId The PS runspace session Id.\r\n     */\r\n    private createRelativeUrl(method: string, sessionId: string): string {\r\n\r\n        let relativeUrl = '';\r\n\r\n        if (method === HttpMethod.Delete) {\r\n            relativeUrl = Net.powerShellApiSessions.format(sessionId);\r\n\r\n        } else if (method === HttpMethod.Put) {\r\n            relativeUrl = Net.powerShellApiSessions.format(sessionId);\r\n\r\n        } else if (method === HttpMethod.Post) {\r\n            relativeUrl = Net.powerShellApiExecuteCommand.format(sessionId);\r\n\r\n        } else if (method === HttpMethod.Get) {\r\n            relativeUrl = Net.powerShellApiRetrieveOutput.format(sessionId);\r\n\r\n        } else if (method === 'CANCEL') {\r\n            relativeUrl = Net.powerShellApiCancelCommand.format(sessionId);\r\n        }\r\n\r\n        return relativeUrl;\r\n    }\r\n\r\n    /**\r\n     * Check if all indivudual nodes have returned 'completed' result and return the list of nodes which returned 'completed=false'\r\n     *\r\n     * @param responseArray The response from a PowerShell batch call.\r\n     * @return incompleteNodes The incompleteNodes array populated with nodes not completed yet.\r\n     */\r\n    private getIncompleteNodes(responseArray: PowerShellBatchResponseItem[]): string[] {\r\n\r\n        const incompleteNodes: string[] = [];\r\n        for (const responseItem of responseArray) {\r\n\r\n            const properties: any = responseItem.properties;\r\n\r\n            // skip the error cases.\r\n            if (!properties) {\r\n                continue;\r\n            }\r\n\r\n            const sessionId: string = properties.sessionId;\r\n            const creationTimestamp: number = Date.now();\r\n\r\n            if (sessionId) {\r\n                // keep the PS session GUID\r\n                this.nodesToSessionIdsMap[responseItem.nodeName] = <RunspaceSessionContext>{ sessionId, creationTimestamp };\r\n            }\r\n\r\n            if (properties.completed && properties.completed.toLowerCase() !== 'true') {\r\n                incompleteNodes.push(responseItem.nodeName);\r\n            }\r\n        }\r\n\r\n        return incompleteNodes;\r\n    }\r\n\r\n    /**\r\n     * Checks if a stored runSpace session for a specific node is expired.\r\n     *\r\n     * @param rsSessionContext runspace session context.\r\n     */\r\n    private isSessionEntryExpired(rsSessionContext: RunspaceSessionContext): boolean {\r\n        const now = Date.now();\r\n        return rsSessionContext.creationTimestamp !== 0 && (now - rsSessionContext.creationTimestamp) > PowerShellBatchRaw.maxDeltaTimeInMs;\r\n    }\r\n}\r\n\r\n/**\r\n * The PowerShellbatch class.\r\n *\r\n * - Single instance of PowerShell batch class manages a single single nodes-runspaces map, with a runspace corresponding to each node.\r\n * - It queues coming requests and process one at a time sequentially.\r\n * - If a command is slow and causing with multiple responses, it aggregates response into single Q result.\r\n * - A PowerShellBatch instance should be created through create() function, and it's statically stored/managed into _map collection.\r\n * - Once all lifetime references are gone, it deletes the runspaces map.\r\n * - To dispose the PowerShellBatch instance, it can use lifetime.dispose().\r\n */\r\nexport class PowerShellBatch {\r\n    /**\r\n     * Static collection of PowerShellbatch objects.\r\n     */\r\n    private static map: MsftSme.StringMap<PowerShellBatch> = {};\r\n\r\n    /**\r\n     * The context of PowerShellBatch object.\r\n     */\r\n    private context: PowerShellBatchContext;\r\n\r\n    /**\r\n     * The queue of PowerShell command requests.\r\n     */\r\n    private queue: PowerShellBatchCommandItem[] = [];\r\n\r\n    /**\r\n     * The reference to PowerShellRaw class object.\r\n     */\r\n    private raw: PowerShellBatchRaw;\r\n\r\n    /**\r\n     * Current data to return to caller.\r\n     */\r\n    private currentData: PowerShellBatchResponseItem[] = [];\r\n\r\n    /**\r\n     * Current data map to aggregate partial data parts from multiple data responses.\r\n     */\r\n    private currentDataMap: MsftSme.StringMap<PowerShellBatchResponseItem> = {};\r\n\r\n    /**\r\n     * Timestamp when last command started.\r\n     */\r\n    private timestamp: number;\r\n\r\n    /**\r\n     * Find or create new PowerShellbatch object.\r\n     *\r\n     * @param nodesList The nodes list targeted by this PowerShellBatch object.\r\n     * @param batchConnection The batch connection.\r\n     * @param key The shared key to queue the requests to use the single nodes-runspaces map.\r\n     * @param lifetime The lifetime container.\r\n     * @param requestOptions the options to apply to every request in this session\r\n     */\r\n    public static create(nodesList: string[], batchConnection: BatchConnection): PowerShellBatch;\r\n    public static create(nodesList: string[], batchConnection: BatchConnection, key: string, lifetime: DisposableLifetimeManager): PowerShellBatch;\r\n    public static create(nodesList: string[], batchConnection: BatchConnection, key: string, lifetime: DisposableLifetimeManager,\r\n        requestOptions: PowerShellSessionRequestOptions): PowerShellBatch;\r\n    public static create(\r\n        nodesList: string[],\r\n        batchConnection: BatchConnection,\r\n        key?: string,\r\n        lifetime?: DisposableLifetimeManager,\r\n        requestOptions?: PowerShellSessionRequestOptions): PowerShellBatch {\r\n        let ps: PowerShellBatch;\r\n\r\n        if (key && lifetime) {\r\n            ps = PowerShellBatch.map[PowerShellBatch.indexName(nodesList, key)];\r\n            if (ps) {\r\n                ps.addLifetime(lifetime);\r\n                return ps;\r\n            }\r\n        }\r\n\r\n        ps = new PowerShellBatch(nodesList, batchConnection, key, lifetime, requestOptions);\r\n        if (key && lifetime) {\r\n            PowerShellBatch.map[PowerShellBatch.indexName(nodesList, key)] = ps;\r\n        }\r\n\r\n        return ps;\r\n    }\r\n\r\n    /**\r\n     * Find existing PowerShellBatch object. Create call must be called before to create the PowerShellBatch instance.\r\n     *\r\n     * @param nodeName The node name.\r\n     * @param key The shared key to queue the requests to use the single runspace.\r\n     */\r\n    public static find(nodesList: string[], key: string): PowerShellBatch {\r\n        return PowerShellBatch.map[PowerShellBatch.indexName(nodesList, key)];\r\n    }\r\n\r\n    /**\r\n     * Create the index name in map collection.\r\n     *\r\n     * @param nodesList The nodes list targeted by this PowerShellBatch object.\r\n     * @param key The shared key to queue the requests to use the single runspace.\r\n     */\r\n    private static indexName(nodesList: string[], key: string): string {\r\n        return nodesList.join(':') + ':' + key;\r\n    }\r\n\r\n    /**\r\n     * Initializes a new instance of the PowerShellBatch class.\r\n     * (private constructor which shouldn't be called directly.)\r\n     *\r\n     * @param nodeList The nodes list targeted by this PowerShellBatch object.\r\n     * @param batchConnection The batch connection service.\r\n     * @param key The shared key to queue the requests to use the single runspace map.\r\n     * @param lifetime The lifetime container.\r\n     */\r\n    constructor(\r\n        nodeList: string[],\r\n        batchConnection: BatchConnection,\r\n        key: string,\r\n        lifetime: DisposableLifetimeManager,\r\n        options: PowerShellSessionRequestOptions) {\r\n        this.context = {\r\n            key: key,\r\n            nodesList: nodeList,\r\n            lifetimes: [],\r\n            requestOptions: PowerShell.newEndpointOptions(options)\r\n        };\r\n        this.timestamp = 0;\r\n        this.raw = new PowerShellBatchRaw(batchConnection, this.context);\r\n        if (key && lifetime) {\r\n            lifetime.registerForDispose(new Disposer(() => this.lifetimeDisposer(lifetime)));\r\n            this.context.lifetimes.push(lifetime);\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Run PowerShellBatch command.\r\n     *\r\n     * @param command The command to run against all nodes in nodesList.\r\n     * @param options The options.\r\n     * @return observable The result of PowerShell batch command.\r\n     */\r\n    public runSingleCommand(command: PowerShellCommand, options?: PowerShellOptions): Observable<PowerShellBatchResponseItem[]> {\r\n        const commandsList: PowerShellCommand[] = [];\r\n        for (let i = 0; i < this.context.nodesList.length; i++) {\r\n            commandsList.push(command);\r\n        }\r\n\r\n        return this.run(commandsList, options);\r\n    }\r\n\r\n    /**\r\n     * Run PowerShellBatch command list.\r\n     *\r\n     * @param commandsList The commands to run against given nodesList.\r\n     * @param options The options.\r\n     * @return observable The result of PowerShell batch command.\r\n     */\r\n    public run(commandsList: PowerShellCommand[], options?: PowerShellOptions): Observable<PowerShellBatchResponseItem[]> {\r\n        if (commandsList.length !== this.context.nodesList.length) {\r\n            return EMPTY;\r\n        }\r\n\r\n        const commandBodyList: string[] = [];\r\n        // wrap command in properties.\r\n        for (const command of commandsList) {\r\n            commandBodyList.push(Net.createPropertiesJSONString(command));\r\n        }\r\n\r\n        if (this.context.lifetimes.length === 0) {\r\n            // no disposer is assigned, force to close the session after every query.\r\n            const timeoutMs: number = options && options.timeoutMs;\r\n            if (options) {\r\n                options.timeoutMs = timeoutMs;\r\n                options.close = true;\r\n            } else {\r\n                options = { timeoutMs: timeoutMs, close: true };\r\n            }\r\n        }\r\n        // queue the request.\r\n        const observable = this.enqueue(this.context.nodesList, commandBodyList, options);\r\n        return observable;\r\n    }\r\n\r\n    /**\r\n     * Cancel PowerShellBatch command.\r\n     */\r\n    public cancel(): Observable<PowerShellBatchResponseItem[]> {\r\n        return this.raw.cancelCommand();\r\n    }\r\n\r\n    /**\r\n     * Enqueue a command request.\r\n     *\r\n     * @param nodesList: the node list.\r\n     * @param commandBodyList The command.\r\n     * @param options The options.\r\n     */\r\n    private enqueue(\r\n            nodesList: string[],\r\n            commandBodyList: string[],\r\n            options?: PowerShellOptions): Observable<PowerShellBatchResponseItem[]> {\r\n        return new Observable((observer: Observer<PowerShellBatchResponseItem[]>) => {\r\n            this.queue.push(<PowerShellBatchCommandItem>{ nodesList, commandList: commandBodyList, observer, options });\r\n            this.dequeue();\r\n        });\r\n    }\r\n\r\n    /**\r\n     * Dequeue a command request.\r\n     */\r\n    private dequeue(): boolean {\r\n        if (this.raw.active) {\r\n            return false;\r\n        }\r\n\r\n        const item: PowerShellBatchCommandItem = this.queue.shift();\r\n        if (item) {\r\n            this.currentData = [];\r\n            this.currentDataMap = {};\r\n            this.timestamp = Date.now();\r\n            this.raw.runCommand(item.nodesList, item.commandList).subscribe({\r\n                next: data => {\r\n\r\n                    this.collect(\r\n                        data,\r\n                        item.options && item.options.timeoutMs,\r\n                        item.options && item.options.partial ? item.observer : null);\r\n                },\r\n                error: error => {\r\n                    if (item.options && item.options.close) {\r\n                        this.raw.close().subscribe();\r\n                    }\r\n\r\n                    item.observer.error(error);\r\n                    this.timestamp = 0;\r\n                    this.dequeue();\r\n                },\r\n                complete: () => {\r\n                    if (item.options && item.options.close) {\r\n                        this.raw.close().subscribe();\r\n                    }\r\n\r\n                    if (!item.options || !item.options.partial) {\r\n                        item.observer.next(this.currentData);\r\n                    }\r\n\r\n                    item.observer.complete();\r\n                    this.timestamp = 0;\r\n                    this.dequeue();\r\n                }\r\n            });\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    /**\r\n     * Collect response results for batch call and aggregate into single object.\r\n     *\r\n     * @param properties The properties of response object.\r\n     * @param timeoutMs The timeout to cancel command.\r\n     * @param observer The observer of powershell results.\r\n     */\r\n    private collect(psResponseList: PowerShellBatchResponseItem[],\r\n        timeoutMs: number,\r\n        observer: Observer<PowerShellBatchResponseItem[]>): void {\r\n        if (timeoutMs && this.timestamp && (Date.now() - this.timestamp > timeoutMs)) {\r\n            // force to cancel the command because of unexpected longer execution.\r\n            this.raw.cancelCommand();\r\n            this.timestamp = 0;\r\n            return;\r\n        }\r\n\r\n        if (observer) {\r\n            // return partial data if observer is not null.\r\n            observer.next(psResponseList);\r\n            this.currentData = psResponseList;\r\n            return;\r\n        }\r\n\r\n        // Merge responses from calls which didn't complete in one go.\r\n        for (const item of psResponseList) {\r\n            // Check if we have saved record from previous call to add to.\r\n            if (this.currentDataMap[item.nodeName]) {\r\n\r\n                // Aggregate Results: If the newly received has results, aggregate them with saved data.\r\n                if (item.properties.results) {\r\n\r\n                    let array: any[];\r\n                    // if any previously received results, use them in aggregation.\r\n                    if (this.currentDataMap[item.nodeName].properties && this.currentDataMap[item.nodeName].properties.results) {\r\n\r\n                        if (MsftSme.getTypeOf(this.currentDataMap[item.nodeName].properties.results) === 'array') {\r\n                            array = this.currentDataMap[item.nodeName].properties.results;\r\n                        } else {\r\n                            array = [this.currentDataMap[item.nodeName].properties.results];\r\n                        }\r\n\r\n                    } else {\r\n                        array = [];\r\n                    }\r\n\r\n                    // Add results from currently received data.\r\n                    if (MsftSme.getTypeOf(item.properties.results) === 'array') {\r\n                        item.properties.results.forEach((x: any) => {\r\n                            array.push(x);\r\n                        });\r\n                    } else {\r\n                        array.push(item.properties.results);\r\n                    }\r\n\r\n                    // Update saved map with the new aggregated data\r\n                    this.currentDataMap[item.nodeName].properties.results = array;\r\n                }\r\n\r\n                // Aggregate Errors: If the newly received response has errors field, aggregate them with saved data.\r\n                if (item.errors) {\r\n\r\n                    let errorsArray: any[];\r\n                    // if any previously received errors, use them in aggregation.\r\n                    if (this.currentDataMap[item.nodeName].errors) {\r\n                        errorsArray = this.currentDataMap[item.nodeName].errors;\r\n                    } else {\r\n                        errorsArray = [];\r\n                    }\r\n\r\n                    // Add results from currently received data.\r\n                    item.errors.forEach((x: any) => {\r\n                        errorsArray.push(x);\r\n                    });\r\n\r\n                    // Update saved map with the new aggregated data\r\n                    this.currentDataMap[item.nodeName].errors = errorsArray;\r\n                }\r\n\r\n            } else {\r\n                // first response/ no saved response. Simply add to map.\r\n                this.currentDataMap[item.nodeName] = item;\r\n            }\r\n        }\r\n        this.currentData = this.convertResponseMapDataToList(this.currentDataMap);\r\n    }\r\n\r\n    /**\r\n     * Helper method to convert a map data to list\r\n     *\r\n     * @param nodeMap The map of nodenames to PowerShellBatchResponseItem. Used to track different calls in a batch.\r\n     * @return The response data for the calls in a list.\r\n     */\r\n    private convertResponseMapDataToList(nodeMap: MsftSme.StringMap<PowerShellBatchResponseItem>): PowerShellBatchResponseItem[] {\r\n        const responseList: PowerShellBatchResponseItem[] = [];\r\n\r\n        for (const key in nodeMap) {\r\n            if (nodeMap.hasOwnProperty(key)) {\r\n                responseList.push(nodeMap[key]);\r\n            }\r\n        }\r\n        return responseList;\r\n    }\r\n\r\n    /**\r\n     * Attach lifetime object to disposer when disposing.\r\n     *\r\n     * @param lifetime The lifetime object.\r\n     */\r\n    private addLifetime(lifetime: DisposableLifetimeManager): void {\r\n        const found: DisposableLifetimeManager = MsftSme.find(\r\n            this.context.lifetimes, (value: DisposableLifetimeManager) => value === lifetime);\r\n        if (!found) {\r\n            this.context.lifetimes.push(lifetime);\r\n            lifetime.registerForDispose(new Disposer(() => this.lifetimeDisposer(lifetime)));\r\n        }\r\n    }\r\n\r\n    /**\r\n     * Callback when disposing the container of view model.\r\n     * If none, reference the PowerShell object. Dispose it. (Delete runspace)\r\n     *\r\n     * @param lifetime The lifetime object.\r\n     */\r\n    private lifetimeDisposer(lifetime: DisposableLifetimeManager): void {\r\n        const found: DisposableLifetimeManager = MsftSme.find(\r\n            this.context.lifetimes, (value: DisposableLifetimeManager) => value === lifetime);\r\n        if (found) {\r\n            MsftSme.remove(this.context.lifetimes, lifetime);\r\n            if (this.context.lifetimes.length === 0) {\r\n                // cancel queue command requests.\r\n                this.queue.forEach((value: PowerShellBatchCommandItem) => {\r\n                    value.observer.next(null);\r\n                    value.observer.complete();\r\n                });\r\n\r\n                // delete from the map collection and delete the runspace/session.\r\n                delete PowerShellBatch.map[PowerShellBatch.indexName(this.context.nodesList, this.context.key)];\r\n                this.raw.dispose();\r\n            }\r\n        }\r\n    }\r\n}\r\n"]}