-
Notifications
You must be signed in to change notification settings - Fork 149
Create a RunningAzureFunctions class to handle multiple function tasks in one workspace #4320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ba133df
Remove runtime limitations on the jsonCliTool
nturinski d8bd83b
Fix accidental version.txt change
nturinski 3b3aa6d
Use an array to hold multiple tasks by buildPath rather than making i…
nturinski 605fbf0
Merge branch 'main' of https://github.com/microsoft/vscode-azurefunct…
nturinski 143b460
Make type unique
nturinski b976c55
Fix for .NET Aspire; create fake workspace folder
nturinski 55c04c5
Weird fix for build path
nturinski 9de9516
Use vscode.uri.parse to normalize path
nturinski 92c3402
PR feedback
nturinski fe185ba
Refactor function running task map
nturinski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,18 +7,19 @@ import { sendRequestWithTimeout, type AzExtRequestPrepareOptions } from '@micros | |
| import { callWithTelemetryAndErrorHandling, parseError, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils'; | ||
| import * as unixPsTree from 'ps-tree'; | ||
| import * as vscode from 'vscode'; | ||
| import { hostStartTaskName } from '../constants'; | ||
| import { hostStartTaskName, ProjectLanguage } from '../constants'; | ||
| import { preDebugValidate, type IPreDebugValidateResult } from '../debug/validatePreDebug'; | ||
| import { ext } from '../extensionVariables'; | ||
| import { AzureFunctionTaskDefinition, getFuncPortFromTaskOrProject, isFuncHostTask, runningFuncTaskMap, stopFuncTaskIfRunning, type IRunningFuncTask } from '../funcCoreTools/funcHostTask'; | ||
| import { buildPathToWorkspaceFolderMap, get, getFuncPortFromTaskOrProject, isFuncHostTask, runningFuncTaskMap, stopFuncTaskIfRunning, type IRunningFuncTask } from '../funcCoreTools/funcHostTask'; | ||
| import { localize } from '../localize'; | ||
| import { delay } from '../utils/delay'; | ||
| import { requestUtils } from '../utils/requestUtils'; | ||
| import { taskUtils } from '../utils/taskUtils'; | ||
| import { getWindowsProcessTree, ProcessDataFlag, type IProcessInfo, type IWindowsProcessTree } from '../utils/windowsProcessTree'; | ||
| import { getWorkspaceSetting } from '../vsCodeConfig/settings'; | ||
| import { getCompiledProjectInfo } from '../workspace/listLocalProjects'; | ||
|
|
||
| const funcTaskReadyEmitter = new vscode.EventEmitter<string>(); | ||
| const funcTaskReadyEmitter = new vscode.EventEmitter<vscode.WorkspaceFolder>(); | ||
| export const onDotnetFuncTaskReady = funcTaskReadyEmitter.event; | ||
|
|
||
| export async function startFuncProcessFromApi( | ||
|
|
@@ -32,32 +33,37 @@ export async function startFuncProcessFromApi( | |
| error: '' | ||
| }; | ||
|
|
||
| const uriFile: vscode.Uri = vscode.Uri.file(buildPath) | ||
|
|
||
| const azFuncTaskDefinition: AzureFunctionTaskDefinition = { | ||
| // VS Code will only run a single instance of a task `type`, | ||
| // the path will be used here to make each project be unique. | ||
| type: `func ${uriFile.fsPath}`, | ||
| functionsApp: uriFile.fsPath | ||
| } | ||
|
|
||
| let funcHostStartCmd: string = 'func host start'; | ||
| if (args) { | ||
| funcHostStartCmd += ` ${args.join(' ')}`; | ||
| } | ||
|
|
||
| await callWithTelemetryAndErrorHandling('azureFunctions.api.startFuncProcess', async (context: IActionContext) => { | ||
| try { | ||
| await waitForPrevFuncTaskToStop(azFuncTaskDefinition.functionsApp); | ||
| const funcTask = new vscode.Task(azFuncTaskDefinition, | ||
| vscode.TaskScope.Global, | ||
| hostStartTaskName, 'func', | ||
| let workspaceFolder: vscode.WorkspaceFolder | undefined = buildPathToWorkspaceFolderMap.get(buildPath); | ||
|
|
||
| if (workspaceFolder === undefined) { | ||
| workspaceFolder = { | ||
| uri: vscode.Uri.parse(buildPath), | ||
| name: buildPath, | ||
| index: -1 | ||
| } | ||
| } | ||
|
|
||
| await waitForPrevFuncTaskToStop(workspaceFolder); | ||
|
|
||
| buildPathToWorkspaceFolderMap.set(buildPath, workspaceFolder); | ||
|
|
||
| const funcTask = new vscode.Task({ type: `func ${buildPath}` }, | ||
| workspaceFolder, | ||
| hostStartTaskName, `func`, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: place on next line since its another argument |
||
| new vscode.ShellExecution(funcHostStartCmd, { | ||
| cwd: buildPath, | ||
| env: env | ||
| env | ||
| })); | ||
|
|
||
| const taskInfo = await startFuncTask(context, funcTask); | ||
| // funcTask.execution?.options.cwd to get build path for later reference | ||
| const taskInfo = await startFuncTask(context, workspaceFolder, buildPath, funcTask); | ||
| result.processId = await pickChildProcess(taskInfo); | ||
| result.success = true; | ||
| } catch (err) { | ||
|
|
@@ -75,7 +81,9 @@ export async function pickFuncProcess(context: IActionContext, debugConfig: vsco | |
| throw new UserCancelledError('preDebugValidate'); | ||
| } | ||
|
|
||
| await waitForPrevFuncTaskToStop(result.workspace.uri.fsPath); | ||
| const projectInfo = await getCompiledProjectInfo(context, result.workspace.uri.fsPath, ProjectLanguage.CSharp); | ||
| const buildPath: string = projectInfo?.compiledProjectPath || result.workspace.uri.fsPath; | ||
| await waitForPrevFuncTaskToStop(result.workspace, buildPath); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
| const preLaunchTaskName: string | undefined = debugConfig.preLaunchTask; | ||
|
|
@@ -88,25 +96,25 @@ export async function pickFuncProcess(context: IActionContext, debugConfig: vsco | |
| throw new Error(localize('noFuncTask', 'Failed to find "{0}" task.', preLaunchTaskName || hostStartTaskName)); | ||
| } | ||
|
|
||
| const taskInfo = await startFuncTask(context, funcTask); | ||
| const taskInfo = await startFuncTask(context, result.workspace, buildPath, funcTask); | ||
| return await pickChildProcess(taskInfo); | ||
| } | ||
|
|
||
| async function waitForPrevFuncTaskToStop(functionApp: string): Promise<void> { | ||
| stopFuncTaskIfRunning(functionApp); | ||
| async function waitForPrevFuncTaskToStop(workspaceFolder: vscode.WorkspaceFolder, buildPath?: string): Promise<void> { | ||
| stopFuncTaskIfRunning(workspaceFolder, buildPath); | ||
|
|
||
| const timeoutInSeconds: number = 30; | ||
| const maxTime: number = Date.now() + timeoutInSeconds * 1000; | ||
| while (Date.now() < maxTime) { | ||
| if (!runningFuncTaskMap.has(functionApp)) { | ||
| 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: IActionContext, funcTask: vscode.Task): Promise<IRunningFuncTask> { | ||
| async function startFuncTask(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, buildPath: string, funcTask: vscode.Task): Promise<IRunningFuncTask> { | ||
| const settingKey: string = 'pickProcessTimeout'; | ||
| const settingValue: number | undefined = getWorkspaceSetting<number>(settingKey); | ||
| const timeoutInSeconds: number = Number(settingValue); | ||
|
|
@@ -115,71 +123,64 @@ async function startFuncTask(context: IActionContext, funcTask: vscode.Task): Pr | |
| } | ||
| context.telemetry.properties.timeoutInSeconds = timeoutInSeconds.toString(); | ||
|
|
||
| if (AzureFunctionTaskDefinition.is(funcTask.definition)) { | ||
| let taskError: Error | undefined; | ||
| const errorListener: vscode.Disposable = vscode.tasks.onDidEndTaskProcess((e: vscode.TaskProcessEndEvent) => { | ||
| if (AzureFunctionTaskDefinition.is(e.execution.task.definition) && e.execution.task.definition.functionsApp === funcTask.definition.functionsApp && e.exitCode !== 0) { | ||
| context.errorHandling.suppressReportIssue = true; | ||
| // Throw if _any_ task fails, not just funcTask (since funcTask often depends on build/clean tasks) | ||
| 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(); | ||
| } | ||
| }); | ||
| let taskError: Error | undefined; | ||
| const errorListener: vscode.Disposable = vscode.tasks.onDidEndTaskProcess((e: vscode.TaskProcessEndEvent) => { | ||
| if (e.execution.task.scope === workspaceFolder && e.exitCode !== 0) { | ||
| context.errorHandling.suppressReportIssue = true; | ||
| // Throw if _any_ task fails, not just funcTask (since funcTask often depends on build/clean tasks) | ||
| 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(); | ||
| } | ||
| }); | ||
|
|
||
| const workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(funcTask.definition.functionsApp)) | ||
| try { | ||
| // The "IfNotActive" part helps when the user starts, stops and restarts debugging quickly in succession. We want to use the already-active task to avoid two func tasks causing a port conflict error | ||
| // The most common case we hit this is if the "clean" or "build" task is running when we get here. It's unlikely the "func host start" task is active, since we would've stopped it in `waitForPrevFuncTaskToStop` above | ||
| await taskUtils.executeIfNotActive(funcTask); | ||
|
|
||
| const intervalMs: number = 500; | ||
| const funcPort: string = await getFuncPortFromTaskOrProject(context, funcTask, workspaceFolder); | ||
| let statusRequestTimeout: number = intervalMs; | ||
| const maxTime: number = Date.now() + timeoutInSeconds * 1000; | ||
| while (Date.now() < maxTime) { | ||
| if (taskError !== undefined) { | ||
| throw taskError; | ||
| } | ||
|
|
||
| try { | ||
| // The "IfNotActive" part helps when the user starts, stops and restarts debugging quickly in succession. We want to use the already-active task to avoid two func tasks causing a port conflict error | ||
| // The most common case we hit this is if the "clean" or "build" task is running when we get here. It's unlikely the "func host start" task is active, since we would've stopped it in `waitForPrevFuncTaskToStop` above | ||
| await taskUtils.executeIfNotActive(funcTask); | ||
|
|
||
| const intervalMs: number = 500; | ||
| const funcPort: string = await getFuncPortFromTaskOrProject(context, funcTask, workspaceFolder); | ||
| let statusRequestTimeout: number = intervalMs; | ||
| const maxTime: number = Date.now() + timeoutInSeconds * 1000; | ||
| while (Date.now() < maxTime) { | ||
| if (taskError !== undefined) { | ||
| throw taskError; | ||
| } | ||
| const taskInfo: IRunningFuncTask | undefined = get(runningFuncTaskMap, workspaceFolder, buildPath); | ||
| if (taskInfo) { | ||
| for (const scheme of ['http', 'https']) { | ||
| const statusRequest: AzExtRequestPrepareOptions = { url: `${scheme}://localhost:${funcPort}/admin/host/status`, method: 'GET' }; | ||
| if (scheme === 'https') { | ||
| statusRequest.rejectUnauthorized = false; | ||
| } | ||
|
|
||
| const taskInfo: IRunningFuncTask | undefined = runningFuncTaskMap.get(funcTask.definition.functionsApp); | ||
| if (taskInfo) { | ||
| for (const scheme of ['http', 'https']) { | ||
| const statusRequest: AzExtRequestPrepareOptions = { url: `${scheme}://localhost:${funcPort}/admin/host/status`, method: 'GET' }; | ||
| if (scheme === 'https') { | ||
| statusRequest.rejectUnauthorized = false; | ||
| try { | ||
| // wait for status url to indicate functions host is running | ||
| const response = await sendRequestWithTimeout(context, statusRequest, statusRequestTimeout, undefined); | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access | ||
| if (response.parsedBody.state.toLowerCase() === 'running') { | ||
| funcTaskReadyEmitter.fire(workspaceFolder); | ||
| return taskInfo; | ||
| } | ||
|
|
||
| try { | ||
| // wait for status url to indicate functions host is running | ||
| const response = await sendRequestWithTimeout(context, statusRequest, statusRequestTimeout, undefined); | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access | ||
| if (response.parsedBody.state.toLowerCase() === 'running') { | ||
| funcTaskReadyEmitter.fire(funcTask.definition.functionsApp); | ||
| return taskInfo; | ||
| } | ||
| } catch (error) { | ||
| if (requestUtils.isTimeoutError(error)) { | ||
| // Timeout likely means localhost isn't ready yet, but we'll increase the timeout each time it fails just in case it's a slow computer that can't handle a request that fast | ||
| statusRequestTimeout *= 2; | ||
| context.telemetry.measurements.maxStatusTimeout = statusRequestTimeout; | ||
| } else { | ||
| // ignore | ||
| } | ||
| } catch (error) { | ||
| if (requestUtils.isTimeoutError(error)) { | ||
| // Timeout likely means localhost isn't ready yet, but we'll increase the timeout each time it fails just in case it's a slow computer that can't handle a request that fast | ||
| statusRequestTimeout *= 2; | ||
| context.telemetry.measurements.maxStatusTimeout = statusRequestTimeout; | ||
| } else { | ||
| // ignore | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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(); | ||
| await delay(intervalMs); | ||
| } | ||
| } | ||
| else { | ||
| throw new Error(localize('failedToFindFuncTask', 'Failed to detect AzFunctions Task')); | ||
|
|
||
| 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(); | ||
| } | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also not sure if the consequences of using index: -1 if VS Code actually looks at this 😓