Skip to content

Commit c2cb7a4

Browse files
authored
Create a RunningAzureFunctions class to handle multiple function tasks in one workspace (#4320)
* Remove runtime limitations on the jsonCliTool * Fix accidental version.txt change * Use an array to hold multiple tasks by buildPath rather than making it workspace scoped * Make type unique * Fix for .NET Aspire; create fake workspace folder * Weird fix for build path * Use vscode.uri.parse to normalize path * PR feedback * Refactor function running task map
1 parent d400aac commit c2cb7a4

File tree

6 files changed

+187
-124
lines changed

6 files changed

+187
-124
lines changed

src/commands/pickFuncProcess.ts

Lines changed: 81 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ import { sendRequestWithTimeout, type AzExtRequestPrepareOptions } from '@micros
77
import { callWithTelemetryAndErrorHandling, parseError, UserCancelledError, type IActionContext } from '@microsoft/vscode-azext-utils';
88
import * as unixPsTree from 'ps-tree';
99
import * as vscode from 'vscode';
10-
import { hostStartTaskName } from '../constants';
10+
import { hostStartTaskName, ProjectLanguage } from '../constants';
1111
import { preDebugValidate, type IPreDebugValidateResult } from '../debug/validatePreDebug';
1212
import { ext } from '../extensionVariables';
13-
import { AzureFunctionTaskDefinition, getFuncPortFromTaskOrProject, isFuncHostTask, runningFuncTaskMap, stopFuncTaskIfRunning, type IRunningFuncTask } from '../funcCoreTools/funcHostTask';
13+
import { buildPathToWorkspaceFolderMap, getFuncPortFromTaskOrProject, isFuncHostTask, runningFuncTaskMap, stopFuncTaskIfRunning, type IRunningFuncTask } from '../funcCoreTools/funcHostTask';
1414
import { localize } from '../localize';
1515
import { delay } from '../utils/delay';
1616
import { requestUtils } from '../utils/requestUtils';
1717
import { taskUtils } from '../utils/taskUtils';
1818
import { getWindowsProcessTree, ProcessDataFlag, type IProcessInfo, type IWindowsProcessTree } from '../utils/windowsProcessTree';
1919
import { getWorkspaceSetting } from '../vsCodeConfig/settings';
20+
import { getCompiledProjectInfo } from '../workspace/listLocalProjects';
2021

21-
const funcTaskReadyEmitter = new vscode.EventEmitter<string>();
22+
const funcTaskReadyEmitter = new vscode.EventEmitter<vscode.WorkspaceFolder>();
2223
export const onDotnetFuncTaskReady = funcTaskReadyEmitter.event;
2324

2425
export async function startFuncProcessFromApi(
@@ -32,32 +33,38 @@ export async function startFuncProcessFromApi(
3233
error: ''
3334
};
3435

35-
const uriFile: vscode.Uri = vscode.Uri.file(buildPath)
36-
37-
const azFuncTaskDefinition: AzureFunctionTaskDefinition = {
38-
// VS Code will only run a single instance of a task `type`,
39-
// the path will be used here to make each project be unique.
40-
type: `func ${uriFile.fsPath}`,
41-
functionsApp: uriFile.fsPath
42-
}
43-
4436
let funcHostStartCmd: string = 'func host start';
4537
if (args) {
4638
funcHostStartCmd += ` ${args.join(' ')}`;
4739
}
4840

4941
await callWithTelemetryAndErrorHandling('azureFunctions.api.startFuncProcess', async (context: IActionContext) => {
5042
try {
51-
await waitForPrevFuncTaskToStop(azFuncTaskDefinition.functionsApp);
52-
const funcTask = new vscode.Task(azFuncTaskDefinition,
53-
vscode.TaskScope.Global,
54-
hostStartTaskName, 'func',
43+
let workspaceFolder: vscode.WorkspaceFolder | undefined = buildPathToWorkspaceFolderMap.get(buildPath);
44+
45+
if (workspaceFolder === undefined) {
46+
workspaceFolder = {
47+
uri: vscode.Uri.parse(buildPath),
48+
name: buildPath,
49+
index: -1
50+
}
51+
}
52+
53+
await waitForPrevFuncTaskToStop(workspaceFolder);
54+
55+
buildPathToWorkspaceFolderMap.set(buildPath, workspaceFolder);
56+
57+
const funcTask = new vscode.Task({ type: `func ${buildPath}` },
58+
workspaceFolder,
59+
hostStartTaskName,
60+
`func`,
5561
new vscode.ShellExecution(funcHostStartCmd, {
5662
cwd: buildPath,
57-
env: env
63+
env
5864
}));
5965

60-
const taskInfo = await startFuncTask(context, funcTask);
66+
// funcTask.execution?.options.cwd to get build path for later reference
67+
const taskInfo = await startFuncTask(context, workspaceFolder, buildPath, funcTask);
6168
result.processId = await pickChildProcess(taskInfo);
6269
result.success = true;
6370
} catch (err) {
@@ -75,7 +82,9 @@ export async function pickFuncProcess(context: IActionContext, debugConfig: vsco
7582
throw new UserCancelledError('preDebugValidate');
7683
}
7784

78-
await waitForPrevFuncTaskToStop(result.workspace.uri.fsPath);
85+
const projectInfo = await getCompiledProjectInfo(context, result.workspace.uri.fsPath, ProjectLanguage.CSharp);
86+
const buildPath: string = projectInfo?.compiledProjectPath || result.workspace.uri.fsPath;
87+
await waitForPrevFuncTaskToStop(result.workspace, buildPath);
7988

8089
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
8190
const preLaunchTaskName: string | undefined = debugConfig.preLaunchTask;
@@ -88,25 +97,25 @@ export async function pickFuncProcess(context: IActionContext, debugConfig: vsco
8897
throw new Error(localize('noFuncTask', 'Failed to find "{0}" task.', preLaunchTaskName || hostStartTaskName));
8998
}
9099

91-
const taskInfo = await startFuncTask(context, funcTask);
100+
const taskInfo = await startFuncTask(context, result.workspace, buildPath, funcTask);
92101
return await pickChildProcess(taskInfo);
93102
}
94103

95-
async function waitForPrevFuncTaskToStop(functionApp: string): Promise<void> {
96-
stopFuncTaskIfRunning(functionApp);
104+
async function waitForPrevFuncTaskToStop(workspaceFolder: vscode.WorkspaceFolder, buildPath?: string): Promise<void> {
105+
stopFuncTaskIfRunning(workspaceFolder, buildPath);
97106

98107
const timeoutInSeconds: number = 30;
99108
const maxTime: number = Date.now() + timeoutInSeconds * 1000;
100109
while (Date.now() < maxTime) {
101-
if (!runningFuncTaskMap.has(functionApp)) {
110+
if (!runningFuncTaskMap.has(workspaceFolder)) {
102111
return;
103112
}
104113
await delay(1000);
105114
}
106115
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));
107116
}
108117

109-
async function startFuncTask(context: IActionContext, funcTask: vscode.Task): Promise<IRunningFuncTask> {
118+
async function startFuncTask(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder, buildPath: string, funcTask: vscode.Task): Promise<IRunningFuncTask> {
110119
const settingKey: string = 'pickProcessTimeout';
111120
const settingValue: number | undefined = getWorkspaceSetting<number>(settingKey);
112121
const timeoutInSeconds: number = Number(settingValue);
@@ -115,71 +124,64 @@ async function startFuncTask(context: IActionContext, funcTask: vscode.Task): Pr
115124
}
116125
context.telemetry.properties.timeoutInSeconds = timeoutInSeconds.toString();
117126

118-
if (AzureFunctionTaskDefinition.is(funcTask.definition)) {
119-
let taskError: Error | undefined;
120-
const errorListener: vscode.Disposable = vscode.tasks.onDidEndTaskProcess((e: vscode.TaskProcessEndEvent) => {
121-
if (AzureFunctionTaskDefinition.is(e.execution.task.definition) && e.execution.task.definition.functionsApp === funcTask.definition.functionsApp && e.exitCode !== 0) {
122-
context.errorHandling.suppressReportIssue = true;
123-
// Throw if _any_ task fails, not just funcTask (since funcTask often depends on build/clean tasks)
124-
taskError = new Error(localize('taskFailed', 'Error exists after running preLaunchTask "{0}". View task output for more information.', e.execution.task.name, e.exitCode));
125-
errorListener.dispose();
126-
}
127-
});
127+
let taskError: Error | undefined;
128+
const errorListener: vscode.Disposable = vscode.tasks.onDidEndTaskProcess((e: vscode.TaskProcessEndEvent) => {
129+
if (e.execution.task.scope === workspaceFolder && e.exitCode !== 0) {
130+
context.errorHandling.suppressReportIssue = true;
131+
// Throw if _any_ task fails, not just funcTask (since funcTask often depends on build/clean tasks)
132+
taskError = new Error(localize('taskFailed', 'Error exists after running preLaunchTask "{0}". View task output for more information.', e.execution.task.name, e.exitCode));
133+
errorListener.dispose();
134+
}
135+
});
128136

129-
const workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(funcTask.definition.functionsApp))
137+
try {
138+
// 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
139+
// 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
140+
await taskUtils.executeIfNotActive(funcTask);
141+
142+
const intervalMs: number = 500;
143+
const funcPort: string = await getFuncPortFromTaskOrProject(context, funcTask, workspaceFolder);
144+
let statusRequestTimeout: number = intervalMs;
145+
const maxTime: number = Date.now() + timeoutInSeconds * 1000;
146+
while (Date.now() < maxTime) {
147+
if (taskError !== undefined) {
148+
throw taskError;
149+
}
130150

131-
try {
132-
// 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
133-
// 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
134-
await taskUtils.executeIfNotActive(funcTask);
135-
136-
const intervalMs: number = 500;
137-
const funcPort: string = await getFuncPortFromTaskOrProject(context, funcTask, workspaceFolder);
138-
let statusRequestTimeout: number = intervalMs;
139-
const maxTime: number = Date.now() + timeoutInSeconds * 1000;
140-
while (Date.now() < maxTime) {
141-
if (taskError !== undefined) {
142-
throw taskError;
143-
}
151+
const taskInfo: IRunningFuncTask | undefined = runningFuncTaskMap.get(workspaceFolder, buildPath);
152+
if (taskInfo) {
153+
for (const scheme of ['http', 'https']) {
154+
const statusRequest: AzExtRequestPrepareOptions = { url: `${scheme}://localhost:${funcPort}/admin/host/status`, method: 'GET' };
155+
if (scheme === 'https') {
156+
statusRequest.rejectUnauthorized = false;
157+
}
144158

145-
const taskInfo: IRunningFuncTask | undefined = runningFuncTaskMap.get(funcTask.definition.functionsApp);
146-
if (taskInfo) {
147-
for (const scheme of ['http', 'https']) {
148-
const statusRequest: AzExtRequestPrepareOptions = { url: `${scheme}://localhost:${funcPort}/admin/host/status`, method: 'GET' };
149-
if (scheme === 'https') {
150-
statusRequest.rejectUnauthorized = false;
159+
try {
160+
// wait for status url to indicate functions host is running
161+
const response = await sendRequestWithTimeout(context, statusRequest, statusRequestTimeout, undefined);
162+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
163+
if (response.parsedBody.state.toLowerCase() === 'running') {
164+
funcTaskReadyEmitter.fire(workspaceFolder);
165+
return taskInfo;
151166
}
152-
153-
try {
154-
// wait for status url to indicate functions host is running
155-
const response = await sendRequestWithTimeout(context, statusRequest, statusRequestTimeout, undefined);
156-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
157-
if (response.parsedBody.state.toLowerCase() === 'running') {
158-
funcTaskReadyEmitter.fire(funcTask.definition.functionsApp);
159-
return taskInfo;
160-
}
161-
} catch (error) {
162-
if (requestUtils.isTimeoutError(error)) {
163-
// 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
164-
statusRequestTimeout *= 2;
165-
context.telemetry.measurements.maxStatusTimeout = statusRequestTimeout;
166-
} else {
167-
// ignore
168-
}
167+
} catch (error) {
168+
if (requestUtils.isTimeoutError(error)) {
169+
// 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
170+
statusRequestTimeout *= 2;
171+
context.telemetry.measurements.maxStatusTimeout = statusRequestTimeout;
172+
} else {
173+
// ignore
169174
}
170175
}
171176
}
172-
173-
await delay(intervalMs);
174177
}
175178

176-
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}`));
177-
} finally {
178-
errorListener.dispose();
179+
await delay(intervalMs);
179180
}
180-
}
181-
else {
182-
throw new Error(localize('failedToFindFuncTask', 'Failed to detect AzFunctions Task'));
181+
182+
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}`));
183+
} finally {
184+
errorListener.dispose();
183185
}
184186
}
185187

0 commit comments

Comments
 (0)