Skip to content

Report cancelled SDK uninstalls accurately#2731

Open
GaTTGeng wants to merge 9 commits into
dotnet:mainfrom
GaTTGeng:fix/2698-surface-uninstall-error
Open

Report cancelled SDK uninstalls accurately#2731
GaTTGeng wants to merge 9 commits into
dotnet:mainfrom
GaTTGeng:fix/2698-surface-uninstall-error

Conversation

@GaTTGeng

Copy link
Copy Markdown

Summary

  • preserve the failure reason returned by the global SDK uninstaller
  • recognize the @vscode/sudo-prompt UAC dismissal message as user cancellation
  • report a dismissed elevation prompt as a cancelled uninstall instead of suggesting that another install may be in progress
  • add regression coverage for the library failure propagation and the final language-model-facing message

Root cause

When the Windows UAC prompt was dismissed, @vscode/sudo-prompt returned
User did not grant permission.. The global uninstall worker discarded that
status and replaced it with an arbitrary error code and a fixed
Another install may be in progress message.

User impact

Copilot and other callers now receive an accurate cancellation message with
guidance to retry and accept the elevation prompt. Other uninstall failures
continue to use the existing generic guidance.

Validation

  • npm test in vscode-dotnet-runtime-library: 255 passing, 7 pending
  • npm run test:lm-tools in vscode-dotnet-runtime-extension: 68 passing

Fixes #2698

GaTTGeng added 4 commits June 19, 2026 09:35
When a Windows user dismisses the UAC dialog during global SDK uninstall,
@vscode/sudo-prompt surfaces "User did not grant permission." That string
matched none of the existing tokens in isUserCancellationMessage, so the
LLM tool path classified the cancellation as a generic failure and showed
the wrong retry guidance. Broaden the regex to recognize it, and export
the helper so the extension command path can reuse it (dotnet#2698).
uninstallGlobal previously discarded the installer's status string and
returned the arbitrary code 117778 with a fixed "Another install may be
in progress" message. When the user dismisses the UAC dialog the status
already carries "User did not grant permission.", so propagate it into
the DotnetUninstallFailed event and the return value instead. Add a unit
test that stubs both global installers to assert the surfaced text (dotnet#2698).
The wrapper at the extension command path used to claim every non-zero
uninstallGlobal result was "may have been cancelled, blocked by another
install in progress, or require manual removal", and embedded the raw
result inside "(code ...)". Now that uninstallGlobal forwards the
elevation provider's reason string, detect the cancellation case via
isUserCancellationMessage and emit a clear "admin/elevation prompt was
dismissed" message; for non-numeric reasons drop the "code" wrapping
since the result already reads as text (dotnet#2698).
Move uninstall failure formatting into a testable helper and verify that dismissing the elevation prompt is reported as a cancellation rather than an unrelated installer conflict.

Fix dotnet#2698
@GaTTGeng GaTTGeng changed the title [codex] Report cancelled SDK uninstalls accurately Report cancelled SDK uninstalls accurately Jun 19, 2026
@GaTTGeng

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@GaTTGeng GaTTGeng marked this pull request as ready for review June 19, 2026 01:55
Copilot AI review requested due to automatic review settings June 19, 2026 01:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts Outdated
Comment thread vscode-dotnet-runtime-extension/src/LanguageModelTools.ts Outdated

@nagilson nagilson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for taking this on! You did a great job investigating. My feedback is mostly nitpicking and enhancements but happy to move this forward afterward and don't see any overall issues.

if (result !== '0' && result !== '')
{
throw new Error(`Uninstall of .NET ${commandContext.version} did not succeed (code ${result}). The uninstaller may have been cancelled, blocked by another install in progress, or require manual removal.`);
throw new Error(buildUninstallFailureMessage(commandContext.version, result));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I like the use of buildUninstallFailureMessage here. I think outside of registering the language model tools, I'd rather avoid importing any implementation from that file here because it will increase the risk of circular dependencies.

I suggest we move buildUninstallFailureMessage into a separate file now, such as ErrorMessageUtilities.ts like how there is a VersionUtilities.ts file.

I would do similarly for isUserCancellationMessage or ideally not export it at all. That method doesn't consider exit code 126 and I'd rather it not be used elsewhere as it may be confused with the check in parseVSCodeSudoExecError which is more robust.


export function buildUninstallFailureMessage(version: string, result: string): string
{
const detail = /^-?\d+$/.test(result) ? `code ${result}` : result;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If at all possible, I'd rather use a native or nodejs method to compare if the string is a whole integer as '^-?\d+$/' is harder to read imo.

// When command execution is non-terminal, the status may contain a useful error from the elevation
// provider (for example, "User did not grant permission.") instead of only a numeric exit code.
const failureReason = uninstallResult.trim();
const failureDetails = failureReason ? ` ${failureReason}` : '';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to create failureDetails.

const originalUninstallSDK = WinMacGlobalInstaller.prototype.uninstallSDK;
const originalDisableMutex = process.env.VSCODE_DOTNET_RUNTIME_DISABLE_MUTEX;

process.env.VSCODE_DOTNET_RUNTIME_DISABLE_MUTEX = 'true';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be necessary, although it could be argued that it may increase test consistency, I'd err towards simplicity here.

} as GlobalInstallerResolver;
const originalLinuxGetExpectedGlobalSDKPath = LinuxGlobalInstaller.prototype.getExpectedGlobalSDKPath;
const originalLinuxUninstallSDK = LinuxGlobalInstaller.prototype.uninstallSDK;
const originalGetExpectedGlobalSDKPath = WinMacGlobalInstaller.prototype.getExpectedGlobalSDKPath;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of this method is nearly identical between the LinuxGlobalInstaller and WinMac one. I wonder if this prototype modification can be parameterized and deduplicated so that the test doesn't need to do the same thing for each type?

@GaTTGeng

Copy link
Copy Markdown
Author

Thank you so much for the thorough review, @nagilson ! This is my first contribution to an open-source project, and receiving such detailed and constructive feedback genuinely means a lot to me. I'll work through each of your suggestions.

- Fix uninstallGlobal to emit DotnetUninstallSkipped and return '0'
  when other dependents remain instead of falling through to the
  failure path (Copilot)
- Move buildUninstallFailureMessage into ErrorMessageUtilities.ts to
  avoid circular dependency between extension.ts and LanguageModelTools
  (nagilson)
- Keep isUserCancellationMessage private in both modules (nagilson)
- Trim result before numeric detection and use Number.isInteger instead
  of regex for readability (Copilot + nagilson)
- Remove redundant failureDetails variable (nagilson)
- Drop unnecessary VSCODE_DOTNET_RUNTIME_DISABLE_MUTEX env toggle in
  test (nagilson)
- Parameterize prototype patching for Linux/WinMac installers to reduce
  duplication in regression test (nagilson)
@GaTTGeng

Copy link
Copy Markdown
Author

Hi @nagilson, are there any areas where I need to improve? If so, please let me know. Thank u!

Comment thread vscode-dotnet-runtime-extension/src/ErrorMessageUtilities.ts Outdated
Comment thread vscode-dotnet-runtime-extension/src/ErrorMessageUtilities.ts Outdated

@nagilson nagilson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your kind words, you've done a great job for your first open-source PR especially! I appreciate the effort you put into applying the fixes. I have a remaining nitpick and once that is resolved this looks good to go.

function isUserCancellationMessage(message: string): boolean
{
return /cancel|user rejected|user denied|password request/i.test(message);
return /cancel|user rejected|user denied|password request|did not grant permission/i.test(message);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be able to import the error utilities function, so we can delete this function instead.


// Note: it's ok not to check live dependents here (though we could) since this will require UAC and extensions do not depend on us to auto-manage admin installs
if (force || await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).installHasNoRegisteredDependentsBesidesId(install, context.installDirectoryProvider, false, context.acquisitionContext.requestingExtensionId ?? ''))
if (!force && !(await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).installHasNoRegisteredDependentsBesidesId(install, context.installDirectoryProvider, false, context.acquisitionContext.requestingExtensionId ?? '')))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice little optimization here by changing the tautology.

Co-authored-by: Noah Gilson <OTAKUPENGUINOP@GMAIL.COM>
Copilot AI review requested due to automatic review settings June 23, 2026 01:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Co-authored-by: Noah Gilson <OTAKUPENGUINOP@GMAIL.COM>
Copilot AI review requested due to automatic review settings June 23, 2026 01:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings June 23, 2026 06:35
@GaTTGeng GaTTGeng force-pushed the fix/2698-surface-uninstall-error branch from c6cbdae to afe9e6f Compare June 23, 2026 06:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@GaTTGeng

Copy link
Copy Markdown
Author

Hi @nagilson, all review feedback has been addressed. Also force-pushed to fix an earlier commit that accidentally normalized line endings and made the diff hard to read. The current diff should now be clean. Happy to iterate further if anything still needs attention.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread vscode-dotnet-runtime-extension/src/ErrorMessageUtilities.ts
Comment thread vscode-dotnet-runtime-extension/src/ErrorMessageUtilities.ts

@nagilson nagilson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks excellent! Thank you for so diligently following up with my feedback.

Most likely the actual release including this fix will be in July in 3.2.0

@nagilson nagilson enabled auto-merge June 23, 2026 23:50
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 23, 2026 23:53
@nagilson nagilson requested a review from a team June 23, 2026 23:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment on lines +7 to +10
* Heuristically detects whether an error/installer message indicates the user cancelled or declined an
* elevation/credential prompt. Kept private to this module: the more robust check in CommandExecutor's
* parseVSCodeSudoExecError is preferred everywhere else, and this heuristic does not consider exit code 126.
* "did not grant permission" comes from @vscode/sudo-prompt when the UAC dialog is dismissed on Windows.
const detailSuffix = detailText ? ` (${detailText})` : '';
return isUserCancellationMessage(normalized)
? `Uninstall of .NET ${version} was cancelled. A permission/elevation prompt was dismissed or rejected. Retry and accept the prompt to continue.${detailSuffix}`
: `Uninstall of .NET ${version} did not succeed${detailSuffix}. The uninstaller may be blocked by another install in progress or require manual removal.`;
Comment on lines +651 to +655
// When command execution is non-terminal, the status may contain a useful error from the elevation
// provider (for example, "User did not grant permission.") instead of only a numeric exit code.
const failureReason = uninstallResult.trim();
context.eventStream.post(new DotnetUninstallFailed(`Failed to uninstall .NET ${install.installId}.${failureReason ? ` ${failureReason}` : ''}`));
return failureReason || '1';
@GaTTGeng

Copy link
Copy Markdown
Author

Thank you for the kind words, and for the thorough review — it made the fix much better than what I started with. It's been a great experience contributing to this project!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve “Another install may be in progress” text - this shows when cancelling the uninstallation by dismissing the UAC/admin prompt.

3 participants