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

Handle Windows AV/EDR file locks during script installations #9543

Conversation

Coruscant11
Copy link
Contributor

@Coruscant11 Coruscant11 commented Nov 30, 2024

Fixes #9531

Context

While working with uv, I encountered issues with a python dependency, httpx unable to be installed because of a os error 5 permission denied.
The error occur when we try to persist a .exe file from a temporary folder into a persistent one.
I only reproduce the issue in an enterprise Windows Jenkins Runner. In my virtual machines, I don't have any issues. So I think this is most probably coming from the system configuration. This windows runner contains an AV/EDR. And the fact that the file locked occured only once for an executable make me think that it's most probably the cause.

While doing some research and speaking with some colleagues (hi @vmeurisse), it seems that the issue is a very recurrent one on Windows.
In the Javascript ecosystem, there is this package, created by the @isaacs, npm inventor: https://www.npmjs.com/package/graceful-fs, used inside npm, allowing its package installations to be more resilient to filesystem errors:

The improvements are meant to normalize behavior across different platforms and environments, and to make filesystem access more resilient to errors.
One of its core feature is this one:
On Windows, it retries renaming a file for up to one second if EACCESS or EPERM error occurs, likely because antivirus software has locked the directory.

So I tried to implement the same algorithm on uv, and it fixed my issue! I can finally install httpx.

Then, as I mentionned in this issue, I saw that you already implemented exactly the same algorithm in an asynchronous function for renames 😄

pub async fn rename_with_retry(

Summary of changes

  • I added a similar function for persist (was not easy to have the benediction of the borrow checker 😄)
  • I added a sync variant of rename_with_retry
  • I edited install_script to use the function including retries on Windows

Let me know if I should change anything 🙂

Thanks!!

Test Plan

This pull-request should be totally iso-functional, so I think it should be covered by existing tests in case of regression.
All tests are still passing on my side.
Also, of course validated that my windows machines (windows 10 & windows 11) containing AV/EDR software are now able to install httpx.exe script.
However, if you think any additional test is needed, feel free to tell me!

@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from 796532b to b865526 Compare November 30, 2024 15:45
@Coruscant11 Coruscant11 changed the title Handle Windows AV/EDR file locks during script installations [DRAFT] Handle Windows AV/EDR file locks during script installations Nov 30, 2024
@Coruscant11 Coruscant11 marked this pull request as draft November 30, 2024 15:49
@Coruscant11 Coruscant11 changed the title [DRAFT] Handle Windows AV/EDR file locks during script installations Handle Windows AV/EDR file locks during script installations Nov 30, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from b865526 to 1fd66e6 Compare November 30, 2024 16:31
@Coruscant11
Copy link
Contributor Author

Coruscant11 commented Nov 30, 2024

(Currently developping on a Windows virtual machine, have to admit I lost a lot of productivity 😆That's why I will stick to a draft for now and abuse of the GitHub CI, not enough cpu on my side unfortunately)

@Coruscant11
Copy link
Contributor Author

Coruscant11 commented Nov 30, 2024

Hi @charliermarsh, I wanted to reuse an existing function, but which is already async, instead of writing a duplicate sync one. Is it a good idea? The install wheel crate is synchronous.
I am currently facing some few tests not passing because of conflicted runtimes. Unfortunately I lack knowledge in async rust...

The goal is to re-use rename_with_retry.
Explained here: #9531 (comment)
I can also implement a sync version,

Am I currently on the good way by trying to run it with a tokio runtime?
Or do you think we should convert the uv-install-wheel to async before?

Thanks!

@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch 3 times, most recently from 756faa1 to f705dd6 Compare November 30, 2024 19:46
@Coruscant11 Coruscant11 marked this pull request as ready for review November 30, 2024 19:55
@Coruscant11
Copy link
Contributor Author

Coruscant11 commented Nov 30, 2024

So for now, I made a sync version of rename_with_retry.
My first goal is to let you validate the functional behaviour solving the issue, I'm open to any comments🙂
Confirmed it solved the issue under one Windows 10 and one Windows 11 machines both under the coverage of EDR security softwares.

Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from f705dd6 to 38ea29e Compare December 1, 2024 01:02
)
})?;

rename_with_retry_sync(&target, &script_absolute)?;
Copy link
Member

Choose a reason for hiding this comment

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

My only concern is that this is now using rename rather than persist. And on Windows, it looks like persist is a bit different, in tempfile?

pub fn persist(old_path: &Path, new_path: &Path, overwrite: bool) -> io::Result<()> {
    unsafe {
        let old_path_w = to_utf16(old_path);
        let new_path_w = to_utf16(new_path);

        // Don't succeed if this fails. We don't want to claim to have successfully persisted a file
        // still marked as temporary because this file won't have the same consistency guarantees.
        if SetFileAttributesW(old_path_w.as_ptr(), FILE_ATTRIBUTE_NORMAL) == 0 {
            return Err(io::Error::last_os_error());
        }

        let mut flags = 0;

        if overwrite {
            flags |= MOVEFILE_REPLACE_EXISTING;
        }

        if MoveFileExW(old_path_w.as_ptr(), new_path_w.as_ptr(), flags) == 0 {
            let e = io::Error::last_os_error();
            // If this fails, the temporary file is now un-hidden and no longer marked temporary
            // (slightly less efficient) but it will still work.
            let _ = SetFileAttributesW(old_path_w.as_ptr(), FILE_ATTRIBUTE_TEMPORARY);
            Err(e)
        } else {
            Ok(())
        }
    }
}

Copy link
Contributor Author

@Coruscant11 Coruscant11 Dec 1, 2024

Choose a reason for hiding this comment

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

You're right! I will try to see if I can still use the persist function.
Just currently fighting with the borrow-checker, as persist takes self and re-returns it in case of PersistError,

error[E0507]: cannot move out of `*from`, as `from` is a captured variable in an `FnMut` closure
   --> crates\uv-fs\src\lib.rs:277:42
    |
262 |     from: &NamedTempFile,
    |     ---- captured outer variable
...
277 |         backoff::retry(backoff, || match from.persist(to) {
    |                                 --       ^^^^ ----------- `*from` moved due to this method call
    |                                 |        |
    |                                 |        move occurs because `*from` has type `NamedTempFile`, which does not implement the `Copy` trait
    |                                 captured by this `FnMut` closure
    |
note: `NamedTempFile::<F>::persist` takes ownership of the receiver `self`, which moves `*from`
   --> C:\Users\Jp\.cargo\registry\src\index.crates.io-6f17d22bba15001f\tempfile-3.14.0\src\file\mod.rs:722:36
    |
722 |     pub fn persist<P: AsRef<Path>>(self, new_path: P) -> Result<F, PersistError<F>> {

It's a good rust exercise 😄 I will ping you when I solve it

Copy link
Contributor Author

@Coruscant11 Coruscant11 Dec 1, 2024

Choose a reason for hiding this comment

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

@charliermarsh Updated! Looking forward for your opinion, the signature and the behavior of persist was not easy at the beggining to put inside a backoff::retry closure,

pub fn persist<P: AsRef<Path>>(self, new_path: P) -> Result<F, PersistError<F>>

Persist the temporary file at the target path.

If a file exists at the target path, persist will atomically replace it. If this method fails, it will return self in the resulting PersistError.

I got bullied a lot by the borrow checker, but I finally managed to do it 😄

So I found a way to get back the reference returned by the error for every retry.

Looking forward for your opinion on this!

Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from 38ea29e to 0226580 Compare December 1, 2024 16:53
Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from 0226580 to 6e9dd4d Compare December 1, 2024 16:54
Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from 6e9dd4d to f8faabb Compare December 1, 2024 16:56
Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from f8faabb to f8c9bec Compare December 1, 2024 16:58
Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from f8c9bec to ac02c95 Compare December 1, 2024 17:00
Coruscant11 added a commit to AmadeusITGroup/uv that referenced this pull request Dec 1, 2024
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch 2 times, most recently from b936cb1 to fa2eec7 Compare December 1, 2024 17:28
@Coruscant11 Coruscant11 force-pushed the install-script-retries-on-windows branch from fa2eec7 to ab0b6c9 Compare December 1, 2024 17:30
Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

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

This looks great, thank you for persisting :)

@charliermarsh charliermarsh added the windows Specific to the Windows platform label Dec 1, 2024
@charliermarsh charliermarsh merged commit 03f8808 into astral-sh:main Dec 1, 2024
28 checks passed
@Coruscant11
Copy link
Contributor Author

Coruscant11 commented Dec 1, 2024

This looks great, thank you for persisting :)

Thanks for the fast review and for making this such a welcoming project for contributors! 🙌

tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Dec 5, 2024
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [astral-sh/uv](https://github.com/astral-sh/uv) | patch | `0.5.5` -> `0.5.6` |

MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot).

**Proposed changes to behavior should be submitted there as MRs.**

---

### Release Notes

<details>
<summary>astral-sh/uv (astral-sh/uv)</summary>

### [`v0.5.6`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#056)

[Compare Source](astral-sh/uv@0.5.5...0.5.6)

##### Enhancements

-   Add `--dry-run` to `uv pip uninstall` ([#&#8203;9557](astral-sh/uv#9557))
-   Allow `--constraints` and `--overrides` in `uv tool install` ([#&#8203;9547](astral-sh/uv#9547))
-   Display removed Python executables on uninstall ([#&#8203;9459](astral-sh/uv#9459))
-   Warn when keyring has no password for `uv publish` ([#&#8203;8827](astral-sh/uv#8827))
-   Add suggested action when `.python-version` pin is incompatible with the project ([#&#8203;9590](astral-sh/uv#9590))
-   Improve error messages for mismatches in `tool.uv.sources` ([#&#8203;9482](astral-sh/uv#9482))
-   Use constraints in trace rather than irrelevant `requires-python` ([#&#8203;9529](astral-sh/uv#9529))

##### Preview features

-   Add `uv python install --default` ([#&#8203;8650](astral-sh/uv#8650))
-   Fix Python executable installation when multiple patch versions are requested ([#&#8203;9607](astral-sh/uv#9607))
-   Build backend: Revamp `include` / `exclude` ([#&#8203;9525](astral-sh/uv#9525))
-   Build backend: Add fast path  ([#&#8203;9556](astral-sh/uv#9556))
-   Build backend: Add functions to collect file list ([#&#8203;9602](astral-sh/uv#9602))
-   Build backend: Default excludes ([#&#8203;9552](astral-sh/uv#9552))
-   Build backend: Refactoring before list ([#&#8203;9558](astral-sh/uv#9558))
-   Build backend: Warn when visiting over 10k files  ([#&#8203;9523](astral-sh/uv#9523))

##### Configuration

-   Make `check-url` available in configuration files ([#&#8203;9032](astral-sh/uv#9032))

##### Performance

-   Avoid adding non-extra package with extra dependencies ([#&#8203;9540](astral-sh/uv#9540))
-   Avoid cloning `String` in marker evaluation ([#&#8203;9598](astral-sh/uv#9598))

##### Rust API

-   `uv-pep508`: Add more methods for simplifying `extra`-related expressions ([#&#8203;9469](astral-sh/uv#9469))

##### Bug fixes

-   Allow `file:` URLs to include package names ([#&#8203;9493](astral-sh/uv#9493))
-   Avoid using IDs across PubGrub states ([#&#8203;9538](astral-sh/uv#9538))
-   Consistently enforce requested-vs.-built metadata when retrieving wheels ([#&#8203;9484](astral-sh/uv#9484))
-   Do not show empty version specifier in `uv tool list` ([#&#8203;9605](astral-sh/uv#9605))
-   Include Git member information when getting metadata from cache ([#&#8203;9388](astral-sh/uv#9388))
-   Include base installation directory in uv run PATH ([#&#8203;9585](astral-sh/uv#9585))
-   Insert backslash when appending to system drive ([#&#8203;9488](astral-sh/uv#9488))
-   Normalize paths when lowering Git dependencies ([#&#8203;9595](astral-sh/uv#9595))
-   Omit origin when comparing requirements ([#&#8203;9570](astral-sh/uv#9570))
-   Override `manylinux_compatible` with `--python-platform` ([#&#8203;9526](astral-sh/uv#9526))
-   Pass extra when evaluating lockfile markers ([#&#8203;9539](astral-sh/uv#9539))
-   Propagate markers for recursive extras in resolver ([#&#8203;9509](astral-sh/uv#9509))
-   Respect path dependencies within Git dependencies ([#&#8203;9594](astral-sh/uv#9594))
-   Support recursive extras with marker in `pip compile -r pyproject.toml` ([#&#8203;9535](astral-sh/uv#9535))
-   Don't emit unpinned warning for proxy packages ([#&#8203;9497](astral-sh/uv#9497))
-   Fix `--refresh-package` flag mentioned as `--refresh-dependency` ([#&#8203;9486](astral-sh/uv#9486))
-   Handle Windows AV/EDR file locks during script installations ([#&#8203;9543](astral-sh/uv#9543))
-   Re-enable conflicting extra/group tests and fix regression from [#&#8203;9540](astral-sh/uv#9540) ([#&#8203;9582](astral-sh/uv#9582))

##### Documentation

-   Add missing word to docs for `run.md` ([#&#8203;9527](astral-sh/uv#9527))
-   Add policies reference section and license document ([#&#8203;9367](astral-sh/uv#9367))
-   Fix typo in entry point docs ([#&#8203;9491](astral-sh/uv#9491))
-   Fix up version in prior uninstall instructions ([#&#8203;9485](astral-sh/uv#9485))
-   Mention `uv pip` behavior in build system note ([#&#8203;9586](astral-sh/uv#9586))
-   Update build failures document ([#&#8203;9584](astral-sh/uv#9584))
-   Correct wording for multiple sources section ([#&#8203;9504](astral-sh/uv#9504))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this MR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box

---

This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40NDAuNyIsInVwZGF0ZWRJblZlciI6IjM3LjQ0MC43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiXX0=-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
windows Specific to the Windows platform
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Unable to install a python project depending on httpx on a corporate Jenkins Windows Runner with AV
2 participants