import { HttpOperationResponse } from '@azure/ms-rest-js' ; import * as unixPsTree from 'ps-tree' ; import * as vscode from 'vscode' ; import { AzExtRequestPrepareOptions , IActionContext , sendRequestWithTimeout , UserCancelledError } from 'vscode-azureextensionui' ; import { hostStartTaskName } from '../constants' ; import { IPreDebugValidateResult , preDebugValidate } from '../debug/validatePreDebug' ; import { ext } from '../extensionVariables' ; import { getFuncPortFromTaskOrProject , IRunningFuncTask , isFuncHostTask , runningFuncTaskMap , stopFuncTaskIfRunning } from '../funcCoreTools/funcHostTask' ; import { localize } from '../localize' ; import { delay } from '../utils/delay' ; import { requestUtils } from '../utils/requestUtils' ; import { taskUtils } from '../utils/taskUtils' ; import { getWindowsProcessTree , IProcessInfo , IWindowsProcessTree , ProcessDataFlag } from '../utils/windowsProcessTree' ; import { getWorkspaceSetting } from '../vsCodeConfig/settings' ; const funcTaskReadyEmitter = new vscode . EventEmitter < vscode . WorkspaceFolder > ( ) ; export const onDotnetFuncTaskReady = funcTaskReadyEmitter . event ; export async function pickFuncProcess ( context , debugConfig ) { const result = await preDebugValidate ( context , debugConfig ) ; if ( ! result . shouldContinue ) { throw new UserCancelledError ( 'preDebugValidate' ) ; } await waitForPrevFuncTaskToStop ( result . workspace ) ; const preLaunchTaskName = debugConfig . preLaunchTask ; const tasks = await vscode . tasks . fetchTasks ( ) ; const funcTask = tasks . find ( t => { return t . scope === result . workspace && ( preLaunchTaskName ? t . name === preLaunchTaskName : isFuncHostTask ( t ) ) ; } ) ; if ( ! funcTask ) { throw new Error ( localize ( 'noFuncTask' , 'Failed to find "{0}" task.' , preLaunchTaskName || hostStartTaskName ) ) ; } const taskInfo = await startFuncTask ( context , result . workspace , funcTask ) ; return await pickChildProcess ( taskInfo ) ; } async function waitForPrevFuncTaskToStop ( workspaceFolder ) { stopFuncTaskIfRunning ( workspaceFolder ) ; const timeoutInSeconds = 30 ; const maxTime = Date . now ( ) + timeoutInSeconds * 1000 ; while ( Date . now ( ) < maxTime ) { if ( ! runningFuncTaskMap . has ( workspaceFolder ) ) { return ; } await delay ( 1000 ) ; } throw new Error ( localize ( 'failedToFindFuncHost' , 'Failed to stop previous running Functions host within "{0}" seconds. Make sure the task has stopped before you debug again.' , timeoutInSeconds ) ) ; } async function startFuncTask ( context , workspaceFolder , funcTask ) { const settingKey = 'pickProcessTimeout' ; const settingValue = getWorkspaceSetting < number > ( settingKey ) ; const timeoutInSeconds = Number ( settingValue ) ; if ( isNaN ( timeoutInSeconds ) ) { throw new Error ( localize ( 'invalidSettingValue' , 'The setting "{0}" must be a number, but instead found "{1}".' , settingKey , settingValue ) ) ; } context . telemetry . properties . timeoutInSeconds = timeoutInSeconds . toString ( ) ; let taskError ; const errorListener = vscode . tasks . onDidEndTaskProcess ( ( e ) => { if ( e . execution . task . scope === workspaceFolder && e . exitCode !== 0 ) { context . errorHandling . suppressReportIssue = true ; taskError = new Error ( localize ( 'taskFailed' , 'Error exists after running preLaunchTask "{0}". View task output for more information.' , e . execution . task . name , e . exitCode ) ) ; errorListener . dispose ( ) ; } } ) ; try { await taskUtils . executeIfNotActive ( funcTask ) ; const intervalMs = 500 ; const funcPort = await getFuncPortFromTaskOrProject ( context , funcTask , workspaceFolder ) ; let statusRequestTimeout = intervalMs ; const maxTime = Date . now ( ) + timeoutInSeconds * 1000 ; while ( Date . now ( ) < maxTime ) { if ( taskError !== undefined ) { throw taskError ; } const taskInfo = runningFuncTaskMap . get ( workspaceFolder ) ; if ( taskInfo ) { for ( const scheme of [ 'http' , 'https' ] ) { const statusRequest = { url : `${ scheme }://localhost:${ funcPort }/admin/host/status` , method : 'GET' } ; if ( scheme === 'https' ) { statusRequest . rejectUnauthorized = false ; } try { const response = await sendRequestWithTimeout ( context , statusRequest , statusRequestTimeout , undefined ) ; if ( response . parsedBody . state . toLowerCase ( ) === 'running' ) { funcTaskReadyEmitter . fire ( workspaceFolder ) ; return taskInfo ; } } catch ( error ) { if ( requestUtils . isTimeoutError ( error ) ) { statusRequestTimeout *= 2 ; context . telemetry . measurements . maxStatusTimeout = statusRequestTimeout ; } else { } } } } await delay ( intervalMs ) ; } throw new Error ( localize ( 'failedToFindFuncHost' , 'Failed to detect running Functions host within "{0}" seconds. You may want to adjust the "{1}" setting.' , timeoutInSeconds , `${ ext . prefix }.${ settingKey }` ) ) ; } finally { errorListener . dispose ( ) ; } } type OSAgnosticProcess = { command : string | undefined ; pid : number | string ; } ; /**
 * Picks the child process that we want to use. Scenarios to keep in mind:
 * 1. On Windows, the rootPid is almost always the parent PowerShell process
 * 2. On Unix, the rootPid may be a wrapper around the main func exe if installed with npm
 * 3. Starting with the .NET 5 worker, Windows sometimes has an inner process we _don't_ want like 'conhost.exe'
 * The only processes we should want to attach to are the "func" process itself or a "dotnet" process running a dll, so we will pick the innermost one of those
 */ async function pickChildProcess ( taskInfo ) { if ( ! isRunning ( taskInfo . processId ) && vscode . window . activeTerminal ) { const terminalPid = await vscode . window . activeTerminal . processId ; if ( terminalPid ) { taskInfo . processId = terminalPid ; } } const children = process . platform === 'win32' ? await getWindowsChildren ( taskInfo . processId ) : await getUnixChildren ( taskInfo . processId ) ; const child = children . reverse ( ) . find ( c => /(dotnet|func)(\.exe|)$/i . test ( c . command || '' ) ) ; return child ? child . pid . toString ( ) : String ( taskInfo . processId ) ; } type ActualUnixPS = unixPsTree . PS & { COMM ? : string ; } ; async function getUnixChildren ( pid ) { const processes = await new Promise ( ( resolve , reject ) : void => { unixPsTree ( pid , ( error , result ) => { if ( error ) { reject ( error ) ; } else { resolve ( result ) ; } } ) ; } ) ; return processes . map ( c => { return { command : c . COMMAND || c . COMM , pid : c . PID } ; } ) ; } async function getWindowsChildren ( pid ) { const windowsProcessTree = getWindowsProcessTree ( ) ; const processes = await new Promise ( ( resolve ) : void => { windowsProcessTree . getProcessList ( pid , resolve , ProcessDataFlag . None ) ; } ) ; return ( processes || [ ] ) . map ( c => { return { command : c . name , pid : c . pid } ; } ) ; } function isRunning ( pid ) { try { process . kill ( pid , 0 ) ; return true ; } catch { return false ; } }