Skip to content
Merged
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
2 changes: 1 addition & 1 deletion extension/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ ASPIRE_EXTENSION_E2E_SHARD=edge-cases ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/tes
ASPIRE_EXTENSION_E2E_ENABLE_AZURE_FUNCTIONS=true ASPIRE_EXTENSION_E2E_DOTNET_RUNTIME_VSIX=/path/to/vscode-dotnet-runtime.vsix ASPIRE_EXTENSION_E2E_CSHARP_VSIX=/path/to/vscode-csharp.vsix ASPIRE_EXTENSION_E2E_AZURE_RESOURCE_GROUPS_VSIX=/path/to/vscode-azureresourcegroups.vsix ASPIRE_EXTENSION_E2E_AZURE_FUNCTIONS_VSIX=/path/to/vscode-azurefunctions.vsix ASPIRE_EXTENSION_E2E_SHARD=azure-functions ASPIRE_EXTENSION_E2E_SPEC=out/test-e2e/test-e2e/azureFunctions.e2e.test.js ASPIRE_EXTENSION_E2E_CLI_PATH=/path/to/aspire corepack yarn test:e2e
```

The Azure Functions shard also requires Azure Functions Core Tools v4 (`func`) on `PATH`. It installs the real .NET Install Tool, C#, Azure Resource Groups, and Azure Functions extensions into the isolated VS Code instance, generates a dedicated HTTPS certificate, and activates the Azure Functions extension so it registers its `func` task definition and listeners. Aspire then creates and runs a registered `func: host start` task for the generated .NET isolated Functions resource; the shard probes its HTTPS endpoint and verifies that stopping the Aspire resource ends the same VS Code task. CI runs this shard on Linux with pinned, checksum-verified copies of Core Tools 4.12.1, .NET Install Tool 3.1.0, C# 2.148.23 for Linux x64, Azure Resource Groups 0.12.7, and Azure Functions 1.22.0.
The E2E fixtures target .NET 10, matching the SDK pinned by the repository's `global.json`. The Azure Functions shard additionally requires Azure Functions Core Tools v4 (`func`) on `PATH`. It installs the real .NET Install Tool, C#, Azure Resource Groups, and Azure Functions extensions into the isolated VS Code instance, generates a dedicated HTTPS certificate with shell-sensitive arguments, and activates the Azure Functions extension so it registers its `func` task definition and listeners. Aspire then creates and runs a registered `func: host start` task for the generated .NET isolated Functions resource; the shard probes its HTTPS endpoint and verifies that stopping the Aspire resource ends the same VS Code task. CI runs this shard on Linux with pinned, checksum-verified copies of Core Tools 4.12.1, .NET Install Tool 3.1.0, C# 2.148.23 for Linux x64, Azure Resource Groups 0.12.7, and Azure Functions 1.22.0.

`ASPIRE_EXTENSION_E2E_SPEC` accepts either one compiled spec path or a glob, so local runs can target one spec or a small subset without editing the test runner:

Expand Down
3 changes: 3 additions & 0 deletions extension/loc/xlf/aspire-vscode.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions extension/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"aspire-vscode.strings.azureFunctionsUnsupportedTaskShell": "The configured VS Code task shell is not supported for Azure Functions launch arguments. Configure terminal.integrated.automationProfile to use PowerShell, Command Prompt, bash, zsh, fish, or WSL.",
"aspire-vscode.strings.azureFunctionsCmdPercentArgument": "Azure Functions launch arguments containing \"%\" are not supported by cmd.exe. Configure terminal.integrated.automationProfile.windows to use PowerShell and try again.",
"aspire-vscode.strings.azureFunctionsCmdDelayedExpansion": "Azure Functions launch arguments containing \"!\" are not supported by cmd.exe because delayed environment variable expansion may change them. Configure terminal.integrated.automationProfile.windows to use PowerShell and try again.",
"aspire-vscode.strings.azureFunctionsInvalidProcessId": "Azure Functions returned an invalid worker process ID: {0}.",
"aspire-vscode.strings.azureFunctionsTaskExitedBeforeStartup": "Azure Functions task exited with code {0} before startup completed.",
"aspire-vscode.strings.azureFunctionsHostStartupTimedOut": "Timed out after {0} seconds waiting for the Azure Functions host to start on port {1}.",
"aspire-vscode.strings.azureFunctionsWorkerStartupTimedOut": "Timed out after {0} seconds waiting for the Azure Functions worker process to start.",
Expand Down
21 changes: 13 additions & 8 deletions extension/scripts/run-e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -1189,8 +1189,13 @@ function resolveRequiredVsixPath(environmentVariable) { const configuredPath =
}

function validateAzureFunctionsCoreTools() {
const executable = process.platform === 'win32' ? 'func.cmd' : 'func';
const result = spawnSync(executable, ['--version'], {
// Node cannot launch .cmd files directly on Windows, so invoke the trusted, constant
// Core Tools command through ComSpec instead.
// https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
const displayName = isWindows ? 'func.cmd' : 'func';
const executable = isWindows ? (process.env.ComSpec || 'cmd.exe') : displayName;
const args = isWindows ? ['/d', '/s', '/c', 'func.cmd --version'] : ['--version'];
const result = spawnSync(executable, args, {
cwd: extensionRoot,
env: getAspireCliEnvironment(),
shell: false,
Expand All @@ -1199,7 +1204,7 @@ function validateAzureFunctionsCoreTools() {
});

if (result.error) {
throw new Error(`Unable to execute Azure Functions Core Tools (${executable}): ${result.error.message}`);
throw new Error(`Unable to execute Azure Functions Core Tools (${displayName}): ${result.error.message}`);
}

if (result.status !== 0) {
Expand Down Expand Up @@ -1357,7 +1362,7 @@ function writeAppHostProject(projectName, resolvedAppHostSdkVersion, includeAzur

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down Expand Up @@ -1461,12 +1466,12 @@ function writeAzureFunctionsProject(projectName) {
const projectDirectory = path.join(workspaceRoot, projectName);
const propertiesDirectory = path.join(projectDirectory, 'Properties');
const certificatePath = path.join(projectDirectory, 'https-e2e.pfx');
const certificatePassword = 'AspireE2E';
const certificatePassword = String.raw`Aspire E2E p@ss'\word`;
fs.mkdirSync(propertiesDirectory, { recursive: true });
fs.writeFileSync(path.join(projectDirectory, `${projectName}.csproj`), `<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down Expand Up @@ -1520,7 +1525,7 @@ public sealed class HttpsFunction
profiles: {
[projectName]: {
commandName: 'Project',
commandLineArgs: `--useHttps --cert ${certificatePath} --password ${certificatePassword}`,
commandLineArgs: `--useHttps --cert "${certificatePath}" --password "${certificatePassword}"`,
launchBrowser: false,
},
},
Expand All @@ -1537,7 +1542,7 @@ function writeWorkerProject(projectName) {
fs.writeFileSync(path.join(projectDirectory, `${projectName}.csproj`), `<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
40 changes: 31 additions & 9 deletions extension/src/debugger/languages/azureFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
azureFunctionsCmdDelayedExpansion,
azureFunctionsCmdPercentArgument,
azureFunctionsHostStartupTimedOut,
azureFunctionsInvalidProcessId,
azureFunctionsTaskExitedBeforeStartup,
azureFunctionsUnsupportedTaskShell,
azureFunctionsWorkerStartupTimedOut,
Expand All @@ -28,6 +29,9 @@ const REQUEST_TIMEOUT_MS = 1_000;
const TASK_SHUTDOWN_TIMEOUT_MS = 30_000;
const TEMP_DIRECTORY_CLEANUP_TIMEOUT_MS = 30_000;
const TEMP_DIRECTORY_CLEANUP_RETRY_DELAY_MS = 100;
// Node validates process IDs by signed 32-bit coercion before dispatching to the OS.
// Keep debugger attach within that same supported range.
const MAX_WORKER_PROCESS_ID = 0x7fffffff;
const TEMP_DIRECTORY_CLEANUP_MAX_ATTEMPTS =
TEMP_DIRECTORY_CLEANUP_TIMEOUT_MS / TEMP_DIRECTORY_CLEANUP_RETRY_DELAY_MS;

Expand Down Expand Up @@ -492,17 +496,31 @@ function readWorkerProcessId(discovery: WorkerProcessIdDiscovery): number | unde
continue;
}

let parsed: unknown;
try {
const event = JSON.parse(line) as { name?: unknown; workerProcessId?: unknown };
if (event.name === 'dotnet-worker-startup' &&
typeof event.workerProcessId === 'number' &&
Number.isInteger(event.workerProcessId) &&
event.workerProcessId > 0) {
workerProcessId = event.workerProcessId;
}
parsed = JSON.parse(line) as unknown;
} catch {
// The final NDJSON line may still be in flight.
continue;
}

if (typeof parsed !== 'object' || parsed === null) {
continue;
}

const event = parsed as { name?: unknown; workerProcessId?: unknown };
if (event.name !== 'dotnet-worker-startup') {
continue;
}

if (typeof event.workerProcessId !== 'number' ||
!Number.isInteger(event.workerProcessId) ||
event.workerProcessId <= 0 ||
event.workerProcessId > MAX_WORKER_PROCESS_ID) {
throw new Error(azureFunctionsInvalidProcessId(String(event.workerProcessId)));
}

workerProcessId = event.workerProcessId;
}

return workerProcessId;
Expand Down Expand Up @@ -620,7 +638,7 @@ function classifyFuncHostTaskShell(profile: TerminalProfileConfiguration | undef
}

if (identity.includes('git bash') || identity.includes('wsl') || identity.includes('cygwin') || identity.includes('msys') ||
/(?:^|[\\/\s])(ba|z|fi|k)?sh(?:\.exe)?(?:$|\s)/.test(identity)) {
/(?:^|[\\/\s])(ba|da|a|z|fi|k)?sh(?:\.exe)?(?:$|\s)/.test(identity)) {
return 'posix';
}

Expand Down Expand Up @@ -654,6 +672,11 @@ export const azureFunctionsDebuggerExtension: ResourceDebuggerExtension = {
throw new Error(invalidLaunchConfiguration(JSON.stringify(launchConfig)));
}

const rawArgs = [...(args ?? [])];
// Validate caller-provided arguments before building the project. The final
// argument list is quoted again after generated Core Tools flags are appended.
quoteFuncHostArguments(rawArgs);

const runId = debugConfiguration.runId;
const projectPath = launchConfig.project_path;
const dotNetService = new DotNetService(launchOptions.debugSession);
Expand All @@ -679,7 +702,6 @@ export const azureFunctionsDebuggerExtension: ResourceDebuggerExtension = {
registerRunCleanup(runId, () => {
void cleanupFuncRun(state);
});
const rawArgs = [...(args ?? [])];
const jsonOutputFileArgument = getJsonOutputFileArgument(rawArgs);
let workerProcessIdDiscovery: WorkerProcessIdDiscovery;
let ownedJsonOutputFileArgument: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions extension/src/loc/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const cmdShimPercentArgument = vscode.l10n.t('Arguments containing %NAME%
export const azureFunctionsUnsupportedTaskShell = vscode.l10n.t('The configured VS Code task shell is not supported for Azure Functions launch arguments. Configure terminal.integrated.automationProfile to use PowerShell, Command Prompt, bash, zsh, fish, or WSL.');
export const azureFunctionsCmdPercentArgument = vscode.l10n.t('Azure Functions launch arguments containing "%" are not supported by cmd.exe. Configure terminal.integrated.automationProfile.windows to use PowerShell and try again.');
export const azureFunctionsCmdDelayedExpansion = vscode.l10n.t('Azure Functions launch arguments containing "!" are not supported by cmd.exe because delayed environment variable expansion may change them. Configure terminal.integrated.automationProfile.windows to use PowerShell and try again.');
export const azureFunctionsInvalidProcessId = (processId: string) => vscode.l10n.t('Azure Functions returned an invalid worker process ID: {0}.', processId);
export const azureFunctionsTaskExitedBeforeStartup = (exitCode: number) => vscode.l10n.t('Azure Functions task exited with code {0} before startup completed.', exitCode);
export const azureFunctionsHostStartupTimedOut = (timeoutSeconds: number, port: number) => vscode.l10n.t('Timed out after {0} seconds waiting for the Azure Functions host to start on port {1}.', timeoutSeconds, port);
export const azureFunctionsWorkerStartupTimedOut = (timeoutSeconds: number) => vscode.l10n.t('Timed out after {0} seconds waiting for the Azure Functions worker process to start.', timeoutSeconds);
Expand Down
2 changes: 1 addition & 1 deletion extension/src/test-e2e/appHostLifecycleTools.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ async function createLinkedWorktreeAppHostFixture(): Promise<LinkedWorktreeAppHo

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion extension/src/test-e2e/helpers/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ builder.Build().Run();

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading
Loading