Skip to content

Stop pip updates from corrupting the Python environment - #5489

Merged
Gabriel Dufresne (GabrielDuf) merged 3 commits into
mainfrom
fix/pip-concurrent-update-corruption
Oct 9, 2026
Merged

Gabriel Dufresne (GabrielDuf) merged 3 commits into
mainfrom
fix/pip-concurrent-update-corruption

Conversation

@GabrielDuf

@GabrielDuf Gabriel Dufresne (GabrielDuf) commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the two UniGetUI behaviours behind the broken dist-info and duplicated packages in #5488. Both were reproduced against real pip 26.2.1, the reporter's version, using a pair of local test packages (a client that depends on a core).

1. Concurrent pip processes

UniGetUI could run several pip operations at once against the same site-packages. Updating a package and one of its dependencies at the same time races on the shared dependency.

  • Without the fix: 1 run in 3 broke the environment with [WinError 2] and left the client on its old version.
  • With the fix: 9 runs in 9 finished clean (both packages updated, a single copy, pip check passes).

Managers can now declare SerializesOperations. Pip does, so its local operations run one at a time and show "Waiting for another Pip operation to finish..." while they wait. An operation that is waiting does not count toward the parallel-operation limit, so other managers' operations are not held up behind a backlog of Pip ones. Cancelling while waiting ends the operation as cancelled, with no stack trace.

2. The automatic --user retry after a locked file

When a package file is in use, pip fails partway through the uninstall with [WinError 32] and does not roll back. It leaves a ~-prefixed stash folder plus a half-removed copy, then suggests --user. UniGetUI followed that suggestion and retried in the user folder, installing a second copy next to the half-removed one. That is the duplicate state in the issue.

  • The --user retry is no longer taken for updates, or for any operation that fails with a file-in-use error. A first install refused with "access denied" ([WinError 5]) still retries in the user folder, because there is no existing copy to duplicate.
  • A file-in-use failure now tells the user: "A file of {package} is in use by another program. Close any program that may be using it, then try again". Retrying the normal update after the lock is gone repaired the package in place in testing.
  • On the exact pip output captured from the locked-file run, the old code chose the retry and the new code reports a failure.

Not covered

  • Already-damaged environments are not repaired. The reporter's install needs manual cleanup.
  • No per-file integrity check. The reporter asked for one; this PR does not add it.
  • Broker operations are not gated. Operations routed through the Devolutions Agent broker don't use the new lock.
  • I don't know what locked the reporter's files. This fix doesn't depend on which program it was.

Notes for reviewers

  • The file-in-use explanation lives in the shared PackageOperation and matches on Python's [WinError 32] text, since the Operations project doesn't reference the Pip project.
  • The gate tests start a real ping process to keep an operation busy. They ignore its exit code: on ARM64, ping occasionally crashes with 0xC000001D when test processes run side by side, and these tests only check ordering.
  • Both dotnet format checks (whitespace and style) pass.
  • UniGetUI.PackageEngine.Tests pass in full on the Windows target. On net10.0, the only failure is an unrelated NuGetV3ManagerTests HTTP-listener flake.

fix #5488

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.

🟡 Changes recommended

The externally-managed-environment fallback can still switch updates to the user installation scheme.

1 open finding
What changed in this PR

Prevents Pip environment corruption by serializing local operations and restricting unsafe user-scope retries.

Changes:

  • Adds manager-level operation serialization with cancellation and queue handling.
  • Improves Pip retry behavior and file-lock errors.
  • Adds regression and concurrency tests.
File Description
PipManagerTests.cs Tests Pip retry and serialization behavior.
PackageOperationsTests.cs Tests gating, cancellation, queue slots, and errors.
PackageOperations.cs Implements manager gates and file-lock messaging.
AbstractOperation.cs Excludes manager-gate waiters from slot accounting.
Pip.cs Enables serialized Pip operations.
PipPkgOperationHelper.cs Restricts user-scope retries.
ManagerCapabilities.cs Adds serialization capability.
lang_en.json Adds new user-facing messages.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

🔵 Needs a closer look

A cancellation race can still log a stack trace, and the new waiting status is not announced to screen readers.

0 open findings

1 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Announce waiting state transitions to screen readers

src/​UniGetUI.PackageEngine.Operations/​PackageOperations.cs:363

This new wait state is only emitted as a progress log line. OperationViewModel copies it to LiveLine, but the operation card exposes that value only as AutomationProperties.HelpText (MainWindow.axaml:403-405), not a live region, so screen-reader users are not notified that execution has changed from running to waiting. Announce this transition once via AccessibilityAnnouncementService with Polite, consistent with operation progress/success announcements in AvaloniaOperationRegistry.cs:219-237.

Medium severity Avoid logging stack traces for raced operation cancellation

src/​UniGetUI.PackageEngine.Operations/​PackageOperations.cs:386

Cancellation can race with acquiring the gate: WaitAsync may complete successfully just as the token is canceled, and base.PerformOperation() then throws at its pre-start cancellation check. _runOperation converts that exception to Canceled but also logs its stack trace, so the promised stack-trace-free cancellation is not guaranteed. Handle cancellation around the process call while still releasing the acquired gate.

🧠 Review effort: Balanced

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "100a0820-c357-11f1-808e-ad75064c4f52",
	"headSha": "6daf9b6e965d3a0de60625233dd0586aa02bf6eb",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

@GabrielDuf
Gabriel Dufresne (GabrielDuf) merged commit c4ee2f1 into main Oct 9, 2026
7 checks passed
@GabrielDuf
Gabriel Dufresne (GabrielDuf) deleted the fix/pip-concurrent-update-corruption branch October 9, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Pip updates can create invalid dist-info / missing RECORD through scope changes and concurrent dependency writes

2 participants