Skip to content

fix: resolve sudo workload install hang on macOS and visibility mismatch on Linux#521

Open
agneszitte wants to merge 26 commits into
mainfrom
dev/agzi/issue-515-workload-context-followup
Open

fix: resolve sudo workload install hang on macOS and visibility mismatch on Linux#521
agneszitte wants to merge 26 commits into
mainfrom
dev/agzi/issue-515-workload-context-followup

Conversation

@agneszitte
Copy link
Copy Markdown
Member

@agneszitte agneszitte commented Mar 29, 2026

Summary

Fixes the macOS sudo PTY hang (#526) and the Linux workload visibility/permission mismatch (#515) by introducing a robust user-first workload install strategy with safe sudo fallback.

Fixes #515
Fixes #526


Problem

macOS (#526)

On macOS, sudo uses an exec_pty relay that hangs indefinitely when launched via Process.Start because there is no real TTY attached. This made uno-check --fix unusable on macOS when workload install required elevated privileges.

Linux (#515)

When workloads are installed as root (e.g. via CI or manual sudo dotnet workload install), a subsequent user-level uno-check cannot see or repair them because dotnet workload list only shows workloads for the current user context. This caused false "not installed" reports and permission-denied failures during fix attempts.


Solution

macOS sudo fix

  • Use sudo -S -p "" (read password from stdin, suppress prompt) instead of interactive TTY-attached sudo
  • Add ReadPasswordFromConsole() for masked password input from the user
  • Add FlushAndCloseInput() on ShellProcessRunner to signal EOF after piping the password, preventing indefinite stdin waits

Linux workload visibility fix

  • User-first install: Always attempt dotnet workload install as the current user first
  • Proactive writability check: IsSdkPathWritable() detects permission issues before they cause cryptic failures
  • Safe sudo fallback: If user-install fails with permission errors:
    1. Try sudo -n (non-interactive, no password) — works in CI and pre-authenticated sessions
    2. If that fails and a TTY is available, fall back to interactive sudo -S with piped stdin
  • Workload listing probe: GetInstalledWorkloads also runs sudo -n dotnet workload list to discover root-owned workloads

Pattern matching

  • ShouldRetryWithSudo() detects permission-denied patterns:
    • "permission denied", "access to the path", "EACCES", "are required to perform this operation", "inadequate permissions", "elevated privileges"

Testing

Automated (CI)

  • 14 new unit test cases passing:
    • 12 ShouldRetryWithSudo theory cases (8 simple patterns + 4 verbose output scenarios)
    • 2 IsSdkPathWritable facts (writable dir + non-existent dir)
  • All 62 CI check runs passing (Windows, Linux, macOS integration validation)
  • Release build: zero warnings/errors

Manual Testing (Linux)

Performed on Ubuntu 24.04 (WSL), .NET SDK 9.0.313. Two user contexts: root (writable SDK at /root/.dotnet) and testdev (non-writable SDK at /opt/dotnet, NOPASSWD sudo).

# SDK Path Verbose User Install Method Result
1 /root/.dotnet (writable) No root Direct user-level install Pass
2 /root/.dotnet (writable) Yes (-v) root Direct user-level install Pass
3 /opt/dotnet (non-writable) No testdev sudo -n dotnet workload install Pass
4 /opt/dotnet (non-writable) Yes (-v) testdev sudo -n dotnet workload install Pass

Key observations:

  • Writable path: installs directly without sudo (user-first approach works)
  • Non-writable path: IsSdkPathWritable detects the restriction, skips user-first attempt, goes directly to sudo -n
  • With -v: shows SDK path '/opt/dotnet' is not writable by the current user. Using elevated privileges.
  • Without -v: only heartbeat progress dots during install (no diagnostic noise)
  • All 4 tests confirmed workload packs downloaded and dotnet workload list shows them installed after fix

Manual Testing (macOS)

Performed on a mac mini (macOS, user user, admin group, sudo requires password).

# SDK Path Permissions Command Scenario Result
1 ~/dotnet-writable user:staff (writable) uno-check --fix --target wasm User-first install, no sudo needed Pass (exit 0)
2 ~/dotnet-writable user:staff (writable) uno-check --fix --target wasm -v Same as 1, verbose output confirms user-first path Pass (exit 0)
3 ~/dotnet-nonwritable root:wheel (non-writable) uno-check --fix --target wasm Non-writable SDK — password prompt — sudo -S install Pass (exit 0)
4 ~/dotnet-nonwritable root:wheel (non-writable) uno-check --fix --target macos Same as 3, different workload target Pass (exit 0)

Verification: After tests 3 & 4, confirmed workload packs installed as root:wheel in ~/dotnet-nonwritable/metadata/workloads/ (7 WebAssembly packs).

.NET SDK: 9.0.313 (both test SDKs)

CI Test Coverage Notes

  • Covered in CI: User-first install path (SDK paths are writable on GitHub Actions runners), ShouldRetryWithSudo pattern matching, IsSdkPathWritable logic
  • Not testable in CI (by design): Interactive sudo -S with password piping — requires Console.ReadKey which is unavailable when stdin is redirected. Code gracefully handles this via Console.IsInputRedirected check.
  • Covered by manual testing only: sudo -n fallback on non-writable SDK paths (CI runners have writable paths), interactive sudo -S with piped password on macOS

Commits

Core implementation

  1. fix: use user-first workload install with sudo fallback on Linux — core logic
  2. fix: add 'inadequate permissions' and 'elevated privileges' to sudo retry patterns — broader detection
  3. fix: use sudo -n in CI/non-interactive mode for workload install fallback — non-interactive first
  4. fix: proactive SDK writability check and TTY-safe sudo fallbackIsSdkPathWritable + safe TTY check
  5. fix: use sudo -S piped stdin to avoid macOS PTY hang — macOS-specific fix

Review follow-ups

  1. fix: harden sudo helpers per review feedback — guard clauses, error handling, code cleanup
  2. fix: harden FlushAndCloseInput and fix doc comment per review — defensive pattern on StandardInput access
  3. fix: correct RetryWithSudo doc comment to describe sudo -S piping — doc accuracy
  4. fix: check cancellation before blocking password prompt and remove test manifests — CancellationToken check + cleanup
  5. docs: expand ReadPasswordFromConsole XML doc to cover all null-return cases — doc completeness

Copilot AI review requested due to automatic review settings March 29, 2026 19:25
@agneszitte agneszitte marked this pull request as draft March 29, 2026 19:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses an infinite uno-check --fix loop on Linux when .NET workloads are installed under an elevated context (root-owned locations) but checked under the current user context, leading to repeated “missing workload” detections.

Changes:

  • Add a sudo -n (no-prompt) wrapper and use it to probe root-owned workloads when the user workload list is empty.
  • Change workload install/repair to run as the current user first, retrying with sudo only when output suggests a permission issue.
  • Add unit tests for the new sudo-retry detection helper (ShouldRetryWithSudo).

Reviewed changes

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

File Description
UnoCheck/Util.cs Adds a no-prompt sudo wrapper path (sudo -n) for non-interactive probing.
UnoCheck/DotNet/DotNetWorkloadManager.cs Implements user-first install/repair with sudo fallback and adds sudo probing for workload listing when empty.
UnoCheck.Tests/DotNetWorkloadFeedbackTests.cs Adds theory coverage for ShouldRetryWithSudo string matching behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 3 out of 3 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

On apt-installed .NET (e.g. Ubuntu), workloads installed via sudo are
not visible to the normal user and vice versa, causing an infinite
fix loop.

- Try workload install/repair as current user first
- Probe sudo -n (non-interactive) to detect if sudo is available
- Fall back to sudo only when exit code or stderr suggests permission
  denial (ShouldRetryWithSudo)
- GetInstalledWorkloads also tries sudo -n fallback when user-level
  list returns empty but root may own the metadata

Fixes #515
…etry patterns

The .NET SDK uses 'Inadequate permissions. Run the command with elevated
privileges.' on Linux/macOS CI, which was not matched by ShouldRetryWithSudo.
This caused the user-first install to fail without falling back to sudo.
…back

- Extract RetryWithSudo helper to centralize sudo fallback logic
- Use sudo -n (non-interactive) when Util.NonInteractive or Util.CI
  is set to prevent hanging on password prompt in CI runners
- Add WrapShellCommandWithSudoNoPrompt overload with CancellationToken
- Add IsSdkPathWritable proactive check before user-level install attempts
  to skip doomed installs on root-owned SDK paths (addresses download
  failures that were not caught by ShouldRetryWithSudo patterns)
- RetryWithSudo now always tries sudo -n first (cached creds/NOPASSWD),
  then falls back to TTY-attached interactive sudo in non-CI mode
- Add WrapShellCommandWithSudoInteractive (RedirectOutput=false) so
  sudo can prompt for a password without hanging
- Add verbose-output pattern tests for ShouldRetryWithSudo
- Add IsSdkPathWritable unit tests (writable dir, non-existent dir)
- Replace TTY-attached sudo with sudo -S -p '' to prevent the
  macOS exec_pty relay from hanging when started via .NET Process.Start
- Add ReadPasswordFromConsole() for masked password input
- Add FlushAndCloseInput() to signal EOF after piping password
@agneszitte agneszitte force-pushed the dev/agzi/issue-515-workload-context-followup branch from a37de9e to 200c149 Compare April 29, 2026 21:45
@agneszitte agneszitte changed the title fix: use user-first workload install with sudo fallback on Linux fix: resolve sudo workload install hang on macOS and visibility mismatch on Linux Apr 29, 2026
@agneszitte agneszitte requested a review from Copilot April 29, 2026 23:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/Util.cs
Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/Process/ShellProcessRunner.cs
- Use unique GUID-based probe filename in IsSdkPathWritable to avoid
  collisions when multiple instances run concurrently
- Escape single quotes in shell command args passed to sudo to prevent
  shell injection
- Return actionable stderr message when ReadPasswordFromConsole returns
  null instead of a silent empty result
- Guard FlushAndCloseInput against InvalidOperationException when
  standard input is not redirected
@agneszitte agneszitte requested a review from Copilot April 30, 2026 01:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/Process/ShellProcessRunner.cs Outdated
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
@agneszitte agneszitte marked this pull request as draft May 1, 2026 06:09
@ajpinedam ajpinedam marked this pull request as ready for review May 2, 2026 04:38
Copilot AI review requested due to automatic review settings May 2, 2026 04:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/Process/ShellProcessRunner.cs
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/Util.cs Outdated
Comment thread specs/sudo-elevation.md Outdated
Copilot AI review requested due to automatic review settings May 6, 2026 02:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 7 out of 7 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UnoCheck/Checkups/DotNetWorkloadsCheckup.cs Outdated
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs
Comment thread UnoCheck/DotNet/DotNetWorkloadManager.cs Outdated
Comment thread UnoCheck/Util.cs Outdated
Comment thread UnoCheck/Util.cs
Comment thread UnoCheck/Checkups/DotNetWorkloadsCheckup.cs Outdated
Comment thread UnoCheck/Util.cs Outdated
@ajpinedam ajpinedam force-pushed the dev/agzi/issue-515-workload-context-followup branch from 4b396be to 9e2afd0 Compare May 6, 2026 04:09
ajpinedam added 6 commits May 6, 2026 00:15
… repair can't expire the cached ticket and fall back to a hidden prompt
…ble workload-list output stops masquerading as 'no workloads installed'
…oInteractive so a missing sudo binary returns an actionable failure instead of throwing
…Async so a missing sudo binary returns false instead of bypassing the friendly handshake-failed flow
…ard the wrong-password fix when sudo is denied or missing
…nsole so 'uno-check --fix > log.txt' doesn't send the prompt to the log and leave the user typing blind
@ajpinedam
Copy link
Copy Markdown
Contributor

@khaskoul @jhanvi03 @agneszitte This branch/fix is ready to be re-tested.

I went through the comments for the previous tests and I fixed the issues mentioned. I also tested it locally on macOS and Linux (did not test on Windows so please pay attention to that one) and it worked as expected, I was able to see the Sudo happening accordingly to the specs and the requirements.

Comment thread UnoCheck.Tests/DotNetWorkloadFeedbackTests.cs
… so ShouldRetryWithSudo and BuildCliFailureMessage substring matchers fire on non-en-US hosts; sudo paths invoke through env to outflank env_reset
Copilot AI review requested due to automatic review settings May 13, 2026 04:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 7 out of 7 changed files in this pull request and generated 3 comments.

Comment on lines +81 to +98
/// Builds an args list for invoking <paramref name="dotnetExe"/> through <c>env</c>, with
/// <see cref="WorkloadCliEnv"/> re-set inside the elevated child. Required for sudo paths
/// because sudo's default <c>env_reset</c> strips any var we set on the parent process.
/// <paramref name="dotnetExe"/> is shell-double-quoted so a path containing spaces
/// (e.g., <c>/Users/My Name/.dotnet/dotnet</c>) survives both the <c>sh -c '…'</c>
/// interpolation used by <see cref="Util.WrapShellCommandWithSudoNoPrompt"/> and the
/// .NET argv tokenizer used by <see cref="Util.WrapShellCommandWithSudoInteractive"/>.
/// </summary>
static string[] BuildEnglishCliSudoArgs(string dotnetExe, string[] args)
{
var result = new List<string>(args.Length + WorkloadCliEnv.Count + 1);
foreach (var kv in WorkloadCliEnv)
result.Add($"{kv.Key}={kv.Value}");
result.Add(Util.ShellDoubleQuote(dotnetExe));
result.AddRange(args);
return result.ToArray();
}

Comment thread UnoCheck/Util.cs
Comment on lines +307 to +308
"This can happen when standard input is redirected or the terminal is non-interactive. " +
"Please rerun this command in an interactive terminal or configure passwordless sudo."
Comment thread specs/sudo-elevation.md
@@ -0,0 +1,154 @@
# Sudo elevation for `dotnet workload install`

Status: accepted, implemented in `dev/agzi/issue-515-workload-context-followup`
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.

uno.check fails to install workloads when sudo is needed [uno-check][Linux] .NET SDK workloads do not download / install on Linux

4 participants