Repository navigation
fix: hide the Windows console for the tool-update service - #14344
Conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Entire-Checkpoint: 01M4S20NQP34BZPMSK8VW7DRBX
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to No actionable issue remains in the reviewed change. Windows 11 Scheduled Task console behavior still needs validation. Pre-merge checks ·
|
|
<!-- entire-trail-link-start --> https://entire.io/gh/jdx/mise/trails/640 <!-- entire-trail-link-end --> ### 🚀 Features - **(bootstrap)** add apt repositories and PPAs with [bootstrap.apt.sources] by @jdx in [#14306](#14306) - **(bootstrap)** embed apt source signing keys inline in .sources files by @jdx in [#14348](#14348) - **(bootstrap)** render templates in [bootstrap.repos] checkout paths by @jdx in [#14359](#14359) - **(bootstrap)** declare macOS login items by @jdx in [#14354](#14354) - **(bootstrap)** layer dnf packages with rpm-ostree on Fedora Atomic hosts by @jdx in [#14357](#14357) - **(bootstrap)** support run0 and doas for privilege elevation by @jdx in [#14351](#14351) - **(dotfiles)** show tracked entries that no variant selects in status by @jdx in [#14266](#14266) - **(dotfiles)** group mise dot status into sections by @jdx in [#14361](#14361) - **(github)** add oauth_copy_code to copy the device code to the clipboard by @jdx in [#14269](#14269) - **(github)** keep the native OAuth refresh token in a password manager or keychain by @jdx in [#14360](#14360) - **(http)** add libc() and avx2() template functions by @jdx in [#14298](#14298) - **(pypi)** read upload times from PEP 691 simple indexes by @jdx in [#14352](#14352) - **(rust)** download rustup-init from RUSTUP_UPDATE_ROOT by @jdx in [#14356](#14356) ### 🐛 Bug Fixes - **(aqua)** warn once for packages with no repo to list versions from by @jdx in [#14336](#14336) - **(backend)** keep decoded download file names inside the download directory by @JamBalaya56562 in [#14274](#14274) - **(bootstrap)** support dnf package groups by @jdx in [#14347](#14347) - **(bootstrap)** don't print a stdin-only helper as a manual command by @jdx in [#14350](#14350) - **(bootstrap)** explain why dnf packages cannot install on ostree hosts by @jdx in [#14353](#14353) - **(brew)** install bottle etc files and run post_install_steps when pouring by @jdx in [#14272](#14272) - **(brew)** refuse to chown a prefix owned by another user by @jdx in [#14345](#14345) - **(brew)** preserve protected post-install copy destinations by @jdx in [#14311](#14311) - **(brew-cask)** support generate completions in evaluated casks by @pmmmwh in [#14255](#14255) - **(docker)** install libatomic1 in the Debian image so Node.js 26 runs by @jdx in [#14303](#14303) - **(dotfiles)** keep existing ciphertext when an encrypted file is unchanged by @jdx in [#14300](#14300) - **(dotfiles)** stop a newly selected profile from deleting its shared file by @jdx in [#14275](#14275) - **(dotfiles)** restore config entries when adopting under a symlinked home by @jdx in [#14315](#14315) - **(dotfiles)** show per-file diffs for directory copy entries in dot diff by @jdx in [#14333](#14333) - **(dotfiles)** apply mise's own config before planning the other entries by @jdx in [#14339](#14339) - **(go)** read an exact go line in go.mod as the toolchain version by @jdx in [#14308](#14308) - **(http)** retry artifact downloads started by mise exec and shims by @jdx in [#14342](#14342) - **(npm)** resolve unlocked latest to the npm version of a graph-locked install by @jdx in [#14307](#14307) - **(npm)** give unlocked requests their own install under the identity layout by @jdx in [#14309](#14309) - **(oci)** ignore host apt hooks when installing apt bootstrap packages by @jdx in [#14296](#14296) - **(packslip)** let a workflow list carry the signer pin across a workflow move by @jdx in [#14338](#14338) - **(packslip)** keep a renamed project's lockfile signer under packslip 1.7 by @jdx in [#14362](#14362) - **(prune)** keep a uv-locked pipx tool's installs for every Python by @jdx in [#14335](#14335) - **(prune)** keep versions of task-scoped tools by @jdx in [#14334](#14334) - **(schema)** accept [bootstrap.secrets] by @JamBalaya56562 in [#14322](#14322) - **(schema)** accept [bootstrap.groups] and [bootstrap.users] by @JamBalaya56562 in [#14340](#14340) - **(schema)** accept table forms for vars._.file and vars._.source by @JamBalaya56562 in [#14383](#14383) - **(shim)** keep other projects' command wrappers on reshim by @jdx in [#14355](#14355) - **(task)** bump gix so remote tasks work in git hooks by @any-victor in [#14316](#14316) - hide the Windows console for the tool-update service by @jdx in [#14344](#14344) ### 🚜 Refactor - replace serde_yaml with serde-saphyr by @jdx in [#14328](#14328) ### 📚 Documentation - **(bootstrap)** explain when to use --from versus --adopt by @jdx in [#14301](#14301) - **(bootstrap)** recommend fish = "auto" for fish shell activation by @jdx in [#14304](#14304) - **(dotfiles)** explain when to use sync, pull, and apply by @jdx in [#14343](#14343) - **(shims)** explain how to check whether a tool is installed by @jdx in [#14337](#14337) - switch body text to DM Sans and tighten the releases page by @jdx in [#14302](#14302) - self-host DM Sans and JetBrains Mono so they load on mise.jdx.dev by @jdx in [#14310](#14310) ### 🧪 Testing - **(bootstrap)** don't depend on bc being absent in the apt sources test by @jdx in [#14371](#14371) ### 📦️ Dependency Updates - install Homebrew uses_from_macos dependencies on Linux by @itskyf in [#14288](#14288) - bump packslip to 1.7.0 by @jdx in [#14363](#14363) - lock file maintenance by @renovate[bot] in [#14320](#14320) - update github actions by @renovate[bot] in [#14380](#14380) - update dependency python to v3.14.8 by @renovate[bot] in [#14381](#14381) - update rust crates by @renovate[bot] in [#14382](#14382) - update docker images by @renovate[bot] in [#14379](#14379) ### 📦 Registry - install opencode 2 from npm by @jdx in [#14295](#14295) ### Chore - **(ci)** switch to jactionlint v2 and drop zizmor by @jdx in [#14256](#14256) - fix CI for the aqua registry update and the unsafe-libyaml advisory by @jdx in [#14326](#14326) ### Ci - double the unit-macos job timeout by @jdx in [#14346](#14346) ### Security - **(config)** require trust for path: tool versions by @jdx in [#14358](#14358) ### New Contributors - @pmmmwh made their first contribution in [#14255](#14255) - @itskyf made their first contribution in [#14288](#14288) ## 📦 Aqua Registry Updates ### New Packages (3) - [`Alice39s/xmtr`](https://github.com/Alice39s/xmtr) - [`bufbuild/protoschema-plugins/protoc-gen-jsonschema`](https://github.com/bufbuild/protoschema-plugins) - [`takumin/qemu-bin`](https://github.com/takumin/qemu-bin) ### Updated Packages (3) - [`bootandy/dust`](https://github.com/bootandy/dust) - [`connectrpc/connect-go/protoc-gen-connect-go`](https://github.com/connectrpc/connect-go) - [`sharkdp/hyperfine`](https://github.com/sharkdp/hyperfine)
…5265) mise's built-in Windows background services and environment-service launcher should not depend on keeping a Windows Terminal window open. On Windows with `AllocConsoleWithOptions`, this change releases their private Terminal console after startup and keeps the service running in an inheritable windowless console. Shared interactive shells are left alone. Fixes #14313 on the supported API path; older-Windows limitations are described below. ## Reproduction and root cause With Windows Terminal as the default terminal, declare the updater in your global mise config and run `mise bootstrap services apply`: ```toml [bootstrap.services.mise-tool-update] builtin = "tool-update" ``` Task Scheduler starts the persistent `mise __tool-update --watch` process in its own console. Before this change, its Terminal window remains open, and closing it terminates the watcher. For a pseudoconsole, [`GetConsoleWindow`](https://learn.microsoft.com/en-us/windows/console/getconsolewindow) returns a message-only HWND, **not the visible Terminal window**. `ShowWindow(..., SW_HIDE)` therefore hides the wrong window. - #14344 (`1882c24b1`) correctly classified `__tool-update --watch` as unattended, but retained that existing hiding mechanism. - The related #14995 report, moved to [discussion #14997](#14997), concerned prompt-started update passes. #15038 (`6b5e5a870`) adds `CREATE_NO_WINDOW` for those child passes. It does not retroactively change the console already owned by the persistent watcher or environment-service launcher. ## Fix Keep the existing unattended-startup hook and sole-console-client guard. For a private pseudoconsole, dynamically resolve [`AllocConsoleWithOptions`](https://learn.microsoft.com/en-us/windows/console/allocconsolewithoptions), detach from the old console, and allocate an inheritable windowless replacement **in the same process**. Ordinary console children can inherit it rather than opening another window. Preserve file/pipe redirection and console code pages, replace invalidated console standard handles with NUL, and perform console replacement before registering signal listeners. Propagate allocation/restoration failures instead of continuing without a replacement console. Task Scheduler still tracks the original PID; service job supervision and full-width exit status are unchanged. ## Compatibility and design tradeoff - The replacement API requires Windows 11 24H2 / Windows Server 2025 or later. Dynamic lookup preserves startup compatibility; unavailable-API systems retain the old hiding behavior, which can still leave Windows Terminal visible. Classic Console Host continues using `ShowWindow`. - **This is not a zero-startup-window fix.** A window can appear before mise reaches the existing hook. In earlier debug-build stress testing, desktop window-state sampling observed one scheduled start visible for about 27 seconds under compilation load at the default low service priority. That is not an optimized-release measurement or a duration bound. - Terminal's own keep-open policy can retain a disconnected tab. The desktop regression requires automatic tab closing and application-title changes; it does not claim policy-independent UI disappearance. This intentionally changes the existing launch/supervision architecture as little as possible. Eliminating startup visibility would instead require changing the initial launch, for example a GUI-subsystem launcher that creates mise with `CREATE_NO_WINDOW`. **If maintainers prefer that broader rewrite, please feel free to reject this PR in its favor.** I hope the diagnosis and reproducible regression here can help a replacement PR meet that stricter goal. No new user flags, settings, or migration steps are required. The services guide documents startup visibility, retained tabs, and the older-Windows fallback. ## CI prerequisite The base lockfile also contained `pulldown-cmark 0.13.5`, which fails `cargo deny` for [RUSTSEC-2026-0433](https://rustsec.org/advisories/RUSTSEC-2026-0433). A separate commit updates only that locked package and its checksum to `0.13.6`; no manifest requirements or other dependencies change. ## Validation - Strict workspace/all-features/all-targets Clippy and default-feature Windows Clippy passed without lint exclusions; canonical `mise run lint-fix` and scoped formatting/lint checks passed. - Two Rust console unit tests and seven repository Windows Pester cases passed, with no skips locally. The desktop regression starts real mise suspended in an already visible private console, releases it after the helper detaches, and checks real desktop windows while the watcher remains alive. The release baseline 2026.10.24 failed the persistent-visibility assertion; the patched debug build passes. - Seven additional isolated lifecycle smoke cases passed: Ctrl-C/Ctrl-Break, UTF-8 code pages, redirected JSON/stderr, scheduled update passes and descendants, and service stop/restart/full-width exit status. Hostile logging settings and post-Pester environment restoration were also checked. No test tasks or watchers remained. - Unavailable-export observer gating was exercised with a genuine failed lookup; this is **not an older-Windows production-fallback run**. Older Windows, Windows Server, classic Console Host, alternate architectures, third-party hosts, and `closeOnExit: "never"` were not runtime-tested locally. - Documentation build (1,535 tests, 423 pages, links), rendered-page inspection, and generated-index drift checks passed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **New Features** * On supported Windows versions, unattended services can replace a private Windows Terminal console with a windowless console. Ordinary console children inherit the replacement instead of opening additional windows; an initial startup window can still appear. * Shared interactive consoles remain visible, and Classic Console Host windows continue to be hidden directly. * **Bug Fixes** * Console allocation and handle/code-page restoration failures are propagated instead of continuing without a replacement console. * **Documentation** * Added details about Windows version support and cases where a terminal window or disconnected tab may remain visible. <!-- end of auto-generated comment: release notes by coderabbit.ai --> *AI-assisted — Tool: oh-my-pi (omp); model: openai-codex/gpt-6.1-sol; version: 18.15.0.*
https://entire.io/gh/jdx/mise/trails/668
On Windows, the built-in
tool-updateservice opened a terminal window that stayed on screen, and closing it stopped the updater. The Scheduled Task runsmise.exe __tool-update --watch, which was never classified as unattended, so it kept the console Windows hands a task's console program.tool-update --watchis now treated like the dotfiles watcher, so its console is hidden right after parsing.mise tool-update <tool>still keeps its console so errors can be shown.I only verified the classification on Linux (extended
only_a_starting_watcher_runs_unattended); I haven't confirmed the window disappears on Windows 11. Discussion #13426 reports the history watcher still showing a window on build 26200.9457, so that may need separate work.Fixes #14313
AI-assisted — Tool: Claude Code; model: anthropic/claude-sonnet-5-5; version: 2.1.295.
🤖 Generated with Claude Code
Note
Low Risk
Small CLI classification change on Windows console behavior; interactive tool-update and non-watch paths are explicitly excluded by tests.
Overview
On Windows, the
mise __tool-update --watchbootstrap/Scheduled Task path is now classified as unattended, so the CLI hides its console right after parse (same policy as the dotfiles watcher and bootstrap service launcher). That stops the updater window from staying on screen and from being closed in a way that kills the service.ToolUpdate::is_watch()exposes whether--watchis set, andCommands::runs_unattendeddelegates to it. Interactive__tool-update <tool>invocations are unchanged and still keep a console for errors. Tests lock in that only--watchis unattended.Reviewed by Cursor Bugbot for commit 6487ecb. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit