Skip to content
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

Use Uri.fspath as key for FuncRunningMap/Port #4309

Open
wants to merge 2 commits into
base: nat/startFuncApi
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 79 additions & 62 deletions src/commands/pickFuncProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,54 @@ import * as vscode from 'vscode';
import { hostStartTaskName } from '../constants';
import { preDebugValidate, type IPreDebugValidateResult } from '../debug/validatePreDebug';
import { ext } from '../extensionVariables';
import { getFuncPortFromTaskOrProject, isFuncHostTask, runningFuncTaskMap, stopFuncTaskIfRunning, type IRunningFuncTask } from '../funcCoreTools/funcHostTask';
import { AzureFunctionTaskDefinition, 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';

const funcTaskReadyEmitter = new vscode.EventEmitter<vscode.WorkspaceFolder>();
const funcTaskReadyEmitter = new vscode.EventEmitter<string>();
export const onDotnetFuncTaskReady = funcTaskReadyEmitter.event;

export async function startFuncProcessFromApi(
workspaceFolder: vscode.WorkspaceFolder,
buildPath: string,
args?: string[]
args: string[],
env: { [key: string]: string }
): Promise<{ processId: string; success: boolean; error: string }> {
const result = {
processId: '',
success: false,
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(workspaceFolder);
const funcTask = new vscode.Task({ type: 'func' },
workspaceFolder,
await waitForPrevFuncTaskToStop(azFuncTaskDefinition.functionsApp);
const funcTask = new vscode.Task(azFuncTaskDefinition,
vscode.TaskScope.Global,
hostStartTaskName, 'func',
new vscode.ShellExecution(funcHostStartCmd, {
cwd: buildPath,
env: env
}));

const taskInfo = await startFuncTask(context, workspaceFolder, funcTask);
const taskInfo = await startFuncTask(context, funcTask);
result.processId = await pickChildProcess(taskInfo);
result.success = true;
} catch (err) {
Expand All @@ -65,7 +75,7 @@ export async function pickFuncProcess(context: IActionContext, debugConfig: vsco
throw new UserCancelledError('preDebugValidate');
}

await waitForPrevFuncTaskToStop(result.workspace);
await waitForPrevFuncTaskToStop(result.workspace.uri.fsPath);

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const preLaunchTaskName: string | undefined = debugConfig.preLaunchTask;
Expand All @@ -78,25 +88,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, result.workspace, funcTask);
const taskInfo = await startFuncTask(context, funcTask);
return await pickChildProcess(taskInfo);
}

async function waitForPrevFuncTaskToStop(workspaceFolder: vscode.WorkspaceFolder): Promise<void> {
stopFuncTaskIfRunning(workspaceFolder);
async function waitForPrevFuncTaskToStop(functionApp: string): Promise<void> {
stopFuncTaskIfRunning(functionApp);

const timeoutInSeconds: number = 30;
const maxTime: number = Date.now() + timeoutInSeconds * 1000;
while (Date.now() < maxTime) {
if (!runningFuncTaskMap.has(workspaceFolder)) {
if (!runningFuncTaskMap.has(functionApp)) {
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, workspaceFolder: vscode.WorkspaceFolder, funcTask: vscode.Task): Promise<IRunningFuncTask> {
async function startFuncTask(context: IActionContext, funcTask: vscode.Task): Promise<IRunningFuncTask> {
const settingKey: string = 'pickProcessTimeout';
const settingValue: number | undefined = getWorkspaceSetting<number>(settingKey);
const timeoutInSeconds: number = Number(settingValue);
Expand All @@ -105,64 +115,71 @@ async function startFuncTask(context: IActionContext, workspaceFolder: vscode.Wo
}
context.telemetry.properties.timeoutInSeconds = timeoutInSeconds.toString();

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();
}
});

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;
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();
}
});

const taskInfo: IRunningFuncTask | undefined = runningFuncTaskMap.get(workspaceFolder);
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 workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(funcTask.definition.functionsApp))

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 {
// 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 = 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;
}
} 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

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
}
}
}
}

await delay(intervalMs);
}

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();
}

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();
}
else {
throw new Error(localize('failedToFindFuncTask', 'Failed to detect AzFunctions Task'));
}
}

Expand Down
70 changes: 53 additions & 17 deletions src/funcCoreTools/funcHostTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,36 @@ import { getLocalSettingsJson } from '../funcConfig/local.settings';
import { getWorkspaceSetting } from '../vsCodeConfig/settings';

export interface IRunningFuncTask {
taskExecution: vscode.TaskExecution;
processId: number;
}

export const runningFuncTaskMap: Map<vscode.WorkspaceFolder | vscode.TaskScope, IRunningFuncTask> = new Map<vscode.WorkspaceFolder | vscode.TaskScope, IRunningFuncTask>();
export class AzureFunctionTaskDefinition implements vscode.TaskDefinition {
type: string;
// This is either vscode.WorkspaceFolder.uri.fsPath or a vscode.Uri.file().fsPath
functionsApp: string

const funcTaskStartedEmitter = new vscode.EventEmitter<vscode.WorkspaceFolder | vscode.TaskScope | undefined>();
static is(taskDefinition: vscode.TaskDefinition): taskDefinition is AzureFunctionTaskDefinition {
return taskDefinition.type.startsWith('func') && "functionsApp" in taskDefinition;
}
}

interface DotnetDebugDebugConfiguration extends vscode.DebugConfiguration {
launchServiceData: { [key: string]: string }
}

namespace DotnetDebugDebugConfiguration {
export function is(debugConfiguration: vscode.DebugConfiguration): debugConfiguration is DotnetDebugDebugConfiguration {
return debugConfiguration.type === 'coreclr' && 'launchServiceData' in debugConfiguration
}
}

export const runningFuncTaskMap: Map<string, IRunningFuncTask> = new Map<string, IRunningFuncTask>();

const funcTaskStartedEmitter = new vscode.EventEmitter<string>();
export const onFuncTaskStarted = funcTaskStartedEmitter.event;

export const runningFuncPortMap = new Map<vscode.WorkspaceFolder | vscode.TaskScope | undefined, string>();
export const runningFuncPortMap = new Map<string | undefined, string>();
const defaultFuncPort: string = '7071';

export function isFuncHostTask(task: vscode.Task): boolean {
Expand All @@ -32,43 +53,58 @@ export function registerFuncHostTaskEvents(): void {
registerEvent('azureFunctions.onDidStartTask', vscode.tasks.onDidStartTaskProcess, async (context: IActionContext, e: vscode.TaskProcessStartEvent) => {
context.errorHandling.suppressDisplay = true;
context.telemetry.suppressIfSuccessful = true;
if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) {
runningFuncTaskMap.set(e.execution.task.scope, { processId: e.processId });
runningFuncPortMap.set(e.execution.task.scope, await getFuncPortFromTaskOrProject(context, e.execution.task, e.execution.task.scope));
funcTaskStartedEmitter.fire(e.execution.task.scope);
if (AzureFunctionTaskDefinition.is(e.execution.task.definition) && isFuncHostTask(e.execution.task)) {
const workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(vscode.Uri.parse(e.execution.task.definition.functionsApp))

runningFuncTaskMap.set(e.execution.task.definition.functionsApp, { taskExecution: e.execution, processId: e.processId });
runningFuncPortMap.set(e.execution.task.definition.functionsApp, await getFuncPortFromTaskOrProject(context, e.execution.task, workspaceFolder));
funcTaskStartedEmitter.fire(e.execution.task.definition.functionsApp);
}
});

registerEvent('azureFunctions.onDidEndTask', vscode.tasks.onDidEndTaskProcess, (context: IActionContext, e: vscode.TaskProcessEndEvent) => {
context.errorHandling.suppressDisplay = true;
context.telemetry.suppressIfSuccessful = true;
if (e.execution.task.scope !== undefined && isFuncHostTask(e.execution.task)) {
runningFuncTaskMap.delete(e.execution.task.scope);
if (AzureFunctionTaskDefinition.is(e.execution.task.definition) && isFuncHostTask(e.execution.task)) {
runningFuncTaskMap.delete(e.execution.task.definition.functionsApp);
}
});

registerEvent('azureFunctions.onDidTerminateDebugSession', vscode.debug.onDidTerminateDebugSession, (context: IActionContext, debugSession: vscode.DebugSession) => {
context.errorHandling.suppressDisplay = true;
context.telemetry.suppressIfSuccessful = true;

// Used to stop the task started with pickFuncProcess.ts startFuncProcessFromApi.
if (DotnetDebugDebugConfiguration.is(debugSession.configuration) && debugSession.configuration.launchServiceData.buildPath) {
const buildPathUri: vscode.Uri = vscode.Uri.file(debugSession.configuration.launchServiceData.buildPath)
stopFuncTaskIfRunning(buildPathUri.fsPath, /* terminate */ true)
}

// NOTE: Only stop the func task if this is the root debug session (aka does not have a parentSession) to fix https://github.com/microsoft/vscode-azurefunctions/issues/2925
if (getWorkspaceSetting<boolean>('stopFuncTaskPostDebug') && !debugSession.parentSession && debugSession.workspaceFolder) {
stopFuncTaskIfRunning(debugSession.workspaceFolder);
stopFuncTaskIfRunning(debugSession.workspaceFolder.uri.fsPath);
}
});
}

export function stopFuncTaskIfRunning(workspaceFolder: vscode.WorkspaceFolder): void {
const runningFuncTask: IRunningFuncTask | undefined = runningFuncTaskMap.get(workspaceFolder);
export function stopFuncTaskIfRunning(functionApp: string, terminate: boolean = false): void {
const runningFuncTask: IRunningFuncTask | undefined = runningFuncTaskMap.get(functionApp);
if (runningFuncTask !== undefined) {
// Use `process.kill` because `TaskExecution.terminate` closes the terminal pane and erases all output
// Also to hopefully fix https://github.com/microsoft/vscode-azurefunctions/issues/1401
process.kill(runningFuncTask.processId);
runningFuncTaskMap.delete(workspaceFolder);
if (terminate) {
// Tasks that are spun up by an API will execute quickly that process.kill does not terminate the func host fast enough
// that it hangs on to the port needed by a debug-restart event.
runningFuncTask.taskExecution.terminate();
}
else {
// Use `process.kill` because `TaskExecution.terminate` closes the terminal pane and erases all output
// Also to hopefully fix https://github.com/microsoft/vscode-azurefunctions/issues/1401
process.kill(runningFuncTask.processId);
}
runningFuncTaskMap.delete(functionApp);
}
}

export async function getFuncPortFromTaskOrProject(context: IActionContext, funcTask: vscode.Task | undefined, projectPathOrTaskScope: string | vscode.WorkspaceFolder | vscode.TaskScope): Promise<string> {
export async function getFuncPortFromTaskOrProject(context: IActionContext, funcTask: vscode.Task | undefined, projectPathOrTaskScope: string | vscode.WorkspaceFolder | vscode.TaskScope | undefined): Promise<string> {
try {
// First, check the task itself
if (funcTask && funcTask.execution instanceof vscode.ShellExecution) {
Expand Down
Loading