Skip to content

Comments

Ensure uv.exe exits when uvw.exe or uvx.exe is killed#17500

Merged
zanieb merged 5 commits intoastral-sh:mainfrom
zaniebot:claude/investigate-issue-17492-znH4F
Jan 29, 2026
Merged

Ensure uv.exe exits when uvw.exe or uvx.exe is killed#17500
zanieb merged 5 commits intoastral-sh:mainfrom
zaniebot:claude/investigate-issue-17492-znH4F

Conversation

@zanieb
Copy link
Member

@zanieb zanieb commented Jan 15, 2026

This matches the approach used by the trampoline in bounce.rs so we combine the implementations in a new uv-windows crate.

Closes #17492
Closes #11817

@zanieb zanieb added the bug Something isn't working label Jan 15, 2026
@zaniebot zaniebot force-pushed the claude/investigate-issue-17492-znH4F branch 7 times, most recently from c89872b to f1ebfb9 Compare January 15, 2026 23:50
@zanieb zanieb changed the title Ensure uv.exe exits when uvw.exe is killed Ensure uv.exe exits when uvw.exe or uvx.exe is killed Jan 15, 2026
@zanieb
Copy link
Member Author

zanieb commented Jan 15, 2026

Terrifying code. Will need to review this closely.

@zaniebot zaniebot force-pushed the claude/investigate-issue-17492-znH4F branch 3 times, most recently from 934469f to e37fe6d Compare January 16, 2026 13:27
@zanieb zanieb force-pushed the claude/investigate-issue-17492-znH4F branch from aa418af to c29d707 Compare January 16, 2026 22:40
@samypr100
Copy link
Collaborator

samypr100 commented Jan 17, 2026

This relates to #11817 (can also close)

@zanieb
Copy link
Member Author

zanieb commented Jan 17, 2026

Ah thanks I had forgotten about that discussion.

@zanieb zanieb force-pushed the claude/investigate-issue-17492-znH4F branch 4 times, most recently from 08c1d52 to 5d4c5f6 Compare January 18, 2026 21:32
On Windows, use Job Objects to ensure the child `uv.exe` process is
terminated when wrapper processes (`uvw.exe`, `uvx.exe`) are killed.
This is important for tools like Task Scheduler that terminate the
wrapper process.

The fix uses the `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` flag which
instructs Windows to terminate all processes associated with the job
when the job object handle is closed (which happens when the wrapper
is terminated).

This change:
- Creates a shared `windows_spawn` module with the Job Object logic
- Updates both `uvw.exe` and `uvx.exe` to use this shared module
- Matches the approach used by the trampoline in `bounce.rs`

Closes astral-sh#17492
@zanieb zanieb force-pushed the claude/investigate-issue-17492-znH4F branch 2 times, most recently from 683933f to f920c85 Compare January 28, 2026 04:01
@zanieb zanieb added the test:extended Enable extended tests for a pull request label Jan 28, 2026
@zanieb zanieb force-pushed the claude/investigate-issue-17492-znH4F branch from f920c85 to 073f7b0 Compare January 28, 2026 04:21
@zanieb zanieb marked this pull request as ready for review January 28, 2026 14:08
@konstin konstin requested a review from EliteTK January 28, 2026 14:33
Copy link
Contributor

@EliteTK EliteTK left a comment

Choose a reason for hiding this comment

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

Looks good, but note the handle leak in spawn_child. That's the only real issue, the rest can wait.

Comment on lines +140 to +141
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we're only writing to the basic information field why are we querying the extended information?

Looks like this is a carry-over from the previous code... Might be a problem for another PR.

/// to receive and handle the control signal, not the wrapper. Returning `TRUE`
/// from the handler tells Windows we've handled the signal (by ignoring it).
///
/// See: `distlib/PC/launcher.c::control_key_handler`
Copy link
Contributor

Choose a reason for hiding this comment

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

The actual logic in the referenced code is somewhat more sophisticated.

Specifically it handles CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, and CTRL_SHUTDOWN_EVENT specially by disabling JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. The reason here is, as I understand it, ignoring these events doesn't stop windows from killing your process, and these events are also going to go to the child anyway. Disabling KILL_ON_JOB_CLOSE gives the child a chance to more gracefully handle these.

Copy link
Contributor

Choose a reason for hiding this comment

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

Although, again, this looks like it was taken from the previous code, so maybe not relevant here.

Comment on lines 63 to 64
// Explicitly drop child to close the process handle before exiting.
drop(child);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we do this we might want to drop the job first too.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's unclear to me if we need to do either drops manually? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't believe we need to drop child manually at all.

But I did think about this a bit further and I think there's another subtle problem. Job might get dropped immediately after we assign the process as we never use it again.

I think dropping job here explicitly might guarantee it lives long enough... There are also some other options.

But, I think really what we should do here is something like:

struct SupervisedChild<'a> {
    job: HANDLE,
    child: HANDLE,
    _child_lifetime: PhantomData<&'a ()>,
}

impl<'a> SupervisedChild<'a> {
    fn new<T>(child: HANDLE, _child_reference: &'a T) -> Self {
        let job = unsafe { CreateJobObjectW(None, None) }.map_err(|e| JobError::Create(e.code().0))?;
        // Configure limits
        // Assign the child
        // Maybe even set up the control handler given both code paths do it?
        Ok(Self { job, child, PhantomData::default() })
    }

    fn wait(self) -> Result<whatever> {
        WaitForSingleObject ...
    }
}

The PhantomData would ensure that the child outlives the JobSupervisedChild. And moving the wait here would ensure that we don't drop the SupervisedChild too early.

For the trampoline case we might just want to have a tiny wrapper around HANDLE that drops it at the right time and lets you access the HANDLE, then we can use a reference to the wrapper for the lifetime handling.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could even go a step further and have the ChildHandle wrapper be in the same place as SupervisedChild, give it a From implementation for the std case, and then you can just give SupervisedChild the ChildHandle reference directly, let it keep it, and avoid the PhantomData.

One last alternative (probably too much boilerplate for two code paths): we could make SupervisedChild generic over the type, have our ChildHandle wrapper for the trampoline case, have it implement our own AsRawHandle trait, and then implement it for Child in std contexts...

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

spawn.rs is std only right now.

Copy link
Member Author

Choose a reason for hiding this comment

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

We may want to fix this in the trampolines too but maybe I'll just do that in a separate change? It seems like it'd be a pre-existing problem?

I guess in the worst-case we just vendor the std impl?

Copy link
Contributor

Choose a reason for hiding this comment

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

The existing code (in the trampolines) didn't have this problem because it didn't have a Drop impl on the Job handle so it couldn't automatically get cleaned up too early.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, I see. Will look at that briefly then.

Copy link
Contributor

Choose a reason for hiding this comment

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

@zanieb zanieb force-pushed the claude/investigate-issue-17492-znH4F branch from 628b493 to 15be283 Compare January 28, 2026 17:49
@zanieb zanieb merged commit 1b4407f into astral-sh:main Jan 29, 2026
167 checks passed
tmeijn pushed a commit to tmeijn/dotfiles that referenced this pull request Feb 2, 2026
This MR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [astral-sh/uv](https://github.com/astral-sh/uv) | patch | `0.9.27` → `0.9.28` |

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.9.28`](https://github.com/astral-sh/uv/blob/HEAD/CHANGELOG.md#0928)

[Compare Source](astral-sh/uv@0.9.27...0.9.28)

Released on 2026-01-29.

##### Python

- Update CPython to use [OpenSSL 3.5.5](https://github.com/openssl/openssl/releases/tag/openssl-3.5.5) which includes fixes for high severity CVEs ([python-build-standalone#960](astral-sh/python-build-standalone#960))

##### Enhancements

- Add support for Pyodide interpreter on Windows ([#&#8203;17658](astral-sh/uv#17658))
- Warn if multiple indexes include `default = true` ([#&#8203;17713](astral-sh/uv#17713))
- Skip uploads when validation reports 'Already uploaded' ([#&#8203;17412](astral-sh/uv#17412))

##### Configuration

- Add a reflink alias for the "clone" link mode ([#&#8203;17724](astral-sh/uv#17724))

##### Bug fixes

- Ensure `uv.exe` exits when `uvw.exe` or `uvx.exe` is killed ([#&#8203;17500](astral-sh/uv#17500))

</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:eyJjcmVhdGVkSW5WZXIiOiI0Mi45NC42IiwidXBkYXRlZEluVmVyIjoiNDIuOTQuNiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiUmVub3ZhdGUgQm90IiwiYXV0b21hdGlvbjpib3QtYXV0aG9yZWQiLCJkZXBlbmRlbmN5LXR5cGU6OnBhdGNoIl19-->
zanieb added a commit that referenced this pull request Feb 4, 2026
This matches the approach used by the trampoline in `bounce.rs` so we
combine the implementations in a new `uv-windows` crate.

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

Labels

bug Something isn't working test:extended Enable extended tests for a pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Terminating uvw does not terminate uv [Windows] Processes spawned by uv are not killed when uv is killed

3 participants