From 838983fa2a56a60c2e26b9177117fd7d75c6cd22 Mon Sep 17 00:00:00 2001 From: "Haonan Tang (from Dev Box)" Date: Mon, 8 Jun 2026 15:58:30 +0800 Subject: [PATCH 1/7] winget update prewarm --- src/cascadia/TerminalApp/FreOverlay.cpp | 141 ++++++++++++++++++++++++ src/cascadia/TerminalApp/FreOverlay.h | 15 +++ 2 files changed, 156 insertions(+) diff --git a/src/cascadia/TerminalApp/FreOverlay.cpp b/src/cascadia/TerminalApp/FreOverlay.cpp index 68f650833..328cde24c 100644 --- a/src/cascadia/TerminalApp/FreOverlay.cpp +++ b/src/cascadia/TerminalApp/FreOverlay.cpp @@ -13,6 +13,7 @@ #include "AgentPaneLog.h" #include +#include using namespace winrt::Windows::Foundation; using namespace winrt::Windows::UI::Xaml; @@ -22,6 +23,13 @@ namespace Automation = winrt::Windows::UI::Xaml::Automation; namespace winrt::TerminalApp::implementation { + // ── Static prewarm state (single-flight per process) ──────────── + // See FreOverlay.h for the design contract. Definitions live here + // because C++ requires out-of-line definitions for non-inline static + // class members. + std::mutex FreOverlay::s_prewarmMutex; + winrt::Windows::Foundation::IAsyncAction FreOverlay::s_prewarmAction{ nullptr }; + FreOverlay::FreOverlay() { InitializeComponent(); @@ -315,6 +323,16 @@ namespace winrt::TerminalApp::implementation // "ProgressRing". Automation::AutomationProperties::SetName( SavingProgressRing(), RS_(L"FreOverlay_SettingUp")); + + // ── Pre-warm winget source cache ─────────────────────────────── + // While the user reads the Welcome + Settings pages (typically + // 5-30s), pre-warm winget's source manifest cache so the on-Save + // install skips the slow refresh step. Best-effort, no error UI. + // Save will await any in-flight prewarm before its own winget call + // to keep the two operations serialised. + _MaybeStartPrewarm( + /*copilotMissing*/ !_IsAgentInstalled(L"copilot"), + /*nodeMissing*/ !_IsNodeInstalled()); } // ── Agent selection changed ───────────────────────────────────────── @@ -411,6 +429,98 @@ namespace winrt::TerminalApp::implementation }); } + // ── WinGet source pre-warm ────────────────────────────────────────── + // + // Kick off `winget source update --name winget` in the background as + // soon as the FRE overlay is shown, so that the on-Save `winget install` + // sees a warm source manifest cache and skips the 3-20s refresh step. + // Gated on whether the install would actually run (Copilot or Node + // missing) AND winget being available. Single-flight per process — + // re-entrant Initialize() calls and multi-window FRE coalesce onto + // one running prewarm. The Save handler awaits s_prewarmAction before + // its own winget call (see _SaveAndInstallAsync), guaranteeing the + // two winget operations never run concurrently. + + void FreOverlay::_MaybeStartPrewarm(bool copilotMissing, bool nodeMissing) + { + // Gate: nothing to pre-warm if no winget install step will run. + if (!copilotMissing && !nodeMissing) + { + return; + } + if (!_IsWingetInstalled()) + { + return; + } + + // Single-flight: first caller wins; later callers find the + // existing IAsyncAction in the slot and bail out. + std::lock_guard lock{ s_prewarmMutex }; + if (s_prewarmAction) + { + return; + } + // _RunPrewarmAsync starts on the calling thread, hops to background + // at its first co_await, and returns the IAsyncAction handle here + // for storage and later co_await by Save. + s_prewarmAction = _RunPrewarmAsync(); + } + + winrt::Windows::Foundation::IAsyncAction FreOverlay::_RunPrewarmAsync() + { + // Hop to background — must never block the UI thread. + co_await winrt::resume_background(); + + try + { + _agentPaneLog("[FRE] Pre-warm: winget source update --name winget"); + + STARTUPINFOW si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + PROCESS_INFORMATION pi{}; + + // CreateProcessW requires a *writable* cmdline buffer (it may + // mutate the string in-place when parsing). `--disable-interactivity` + // prevents any prompt (e.g. source first-run agreement) from + // hanging the hidden child process forever. + wchar_t cmdline[] = L"winget source update --name winget --disable-interactivity"; + if (!CreateProcessW(nullptr, cmdline, nullptr, nullptr, FALSE, + CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) + { + _agentPaneLog("[FRE] Pre-warm: CreateProcess failed err=" + + std::to_string(GetLastError())); + co_return; + } + + // Wait up to 120s. Corporate proxies / cold caches can push + // honest cases past 30s, so we err on the side of patience. + // We deliberately do NOT TerminateProcess on timeout: killing + // winget mid-write can corrupt its source DB. The Save handler + // awaits this whole coroutine, which only completes after + // WaitForSingleObject returns, so even a slow prewarm cannot + // collide with the eventual install. + const DWORD wait = WaitForSingleObject(pi.hProcess, 120000); + DWORD exitCode = 0; + GetExitCodeProcess(pi.hProcess, &exitCode); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + _agentPaneLog(wait == WAIT_TIMEOUT + ? "[FRE] Pre-warm: still running after 120s (proceeding)" + : "[FRE] Pre-warm: completed exit=" + std::to_string(exitCode)); + } + catch (...) + { + // Pre-warm is strictly best-effort; never let an exception + // escape into the IAsyncAction promise. Save's co_await on + // s_prewarmAction also has its own try/catch as belt-and- + // suspenders, but this is the primary guard. + LOG_CAUGHT_EXCEPTION(); + } + } + // ── WinGet install helper ─────────────────────────────────────────── IAsyncOperation FreOverlay::_WingetInstallAsync(winrt::hstring packageId) @@ -718,6 +828,37 @@ namespace winrt::TerminalApp::implementation _ShowProblem(FreProblemKind::WingetMissing); co_return; } + + // ── Await any in-flight pre-warm before kicking off install ── + // The Initialize() handler may have started a `winget source + // update` in the background. Winget's intra-process + // coordination across concurrent operations is not a + // guaranteed contract — we serialise here to avoid two + // winget instances stepping on each other. Snapshot the + // action under the mutex (Initialize may still be racing to + // assign it), then co_await OUTSIDE the lock (holding a + // std::mutex across a suspension point is undefined behaviour). + winrt::Windows::Foundation::IAsyncAction pending{ nullptr }; + { + std::lock_guard lock{ s_prewarmMutex }; + pending = s_prewarmAction; + } + if (pending && + pending.Status() != winrt::Windows::Foundation::AsyncStatus::Completed) + { + _agentPaneLog("[FRE] Save: waiting for pre-warm to finish"); + try + { + co_await pending; + } + catch (...) + { + // Pre-warm failure is non-fatal; install will just + // pay the source-refresh cost itself. + LOG_CAUGHT_EXCEPTION(); + } + _agentPaneLog("[FRE] Save: pre-warm done, proceeding with install"); + } } if (needsCopilot) diff --git a/src/cascadia/TerminalApp/FreOverlay.h b/src/cascadia/TerminalApp/FreOverlay.h index 337d11aa9..398b871ff 100644 --- a/src/cascadia/TerminalApp/FreOverlay.h +++ b/src/cascadia/TerminalApp/FreOverlay.h @@ -85,6 +85,21 @@ namespace winrt::TerminalApp::implementation static bool _IsNodeInstalled(); static bool _IsWingetInstalled(); + // ── Winget source pre-warm coordination ───────────────────── + // While the FRE overlay is on screen (Welcome + Settings pages), + // pre-warm winget's source manifest cache in the background so + // the on-Save `winget install` skips the 3-20s source refresh. + // Single-flight per process — re-entrant Initialize() calls and + // multi-window FRE coalesce onto one running prewarm. The Save + // handler awaits s_prewarmAction before its own winget call to + // guarantee the two winget operations never run concurrently + // (winget's intra-process locking is not a guaranteed contract). + static std::mutex s_prewarmMutex; + static winrt::Windows::Foundation::IAsyncAction s_prewarmAction; + + static void _MaybeStartPrewarm(bool copilotMissing, bool nodeMissing); + static winrt::Windows::Foundation::IAsyncAction _RunPrewarmAsync(); + // Run a winget install synchronously on a background thread. // Returns true on success. static winrt::Windows::Foundation::IAsyncOperation _WingetInstallAsync(winrt::hstring packageId); From 6a78cfdd2e2b9425bc86e7c7adb1a23540a9732f Mon Sep 17 00:00:00 2001 From: "Haonan Tang (from Dev Box)" Date: Wed, 10 Jun 2026 14:16:50 +0800 Subject: [PATCH 2/7] winget install via COM API --- src/cascadia/TerminalApp/FreOverlay.cpp | 300 +++++++++++++++++------- 1 file changed, 210 insertions(+), 90 deletions(-) diff --git a/src/cascadia/TerminalApp/FreOverlay.cpp b/src/cascadia/TerminalApp/FreOverlay.cpp index 328cde24c..dc4929ba4 100644 --- a/src/cascadia/TerminalApp/FreOverlay.cpp +++ b/src/cascadia/TerminalApp/FreOverlay.cpp @@ -11,6 +11,7 @@ #include "../inc/ShellIntegration.h" #include "../inc/RtlHelper.h" #include "AgentPaneLog.h" +#include "WindowsPackageManagerFactory.h" #include #include @@ -522,115 +523,234 @@ namespace winrt::TerminalApp::implementation } // ── WinGet install helper ─────────────────────────────────────────── - - IAsyncOperation FreOverlay::_WingetInstallAsync(winrt::hstring packageId) + // + // Installs a package via the WinGet COM/WinRT API + // (`Microsoft.Management.Deployment.PackageManager`) instead of + // shelling out to `winget.exe`. + // + // Why this matters: + // + // The CLI path doesn't work for us. winget.exe, when launched from + // a packaged GUI parent (which IT is), runs through an App Execution + // Alias activation that breaks stdio inheritance — child writes + // nothing to whatever pipe/file we redirect to. We verified across + // 6 spawn variants (NUL stdin, pipe stdin, GetStdHandle stdin, + // combined vs split pipes, plus `cmd.exe /c "winget … > tempfile 2>&1"`) + // that the captured output is consistently 0 bytes for real failures. + // This is a Microsoft design limitation, not a winget bug: + // packaged-from-packaged stdio inheritance is documented as unsupported + // (see https://github.com/microsoft/winget-cli/issues/504). + // + // The COM API bypasses the alias activation entirely — it calls + // AppInstaller's out-of-proc COM server directly via CoCreateInstance + // (CLSCTX_LOCAL_SERVER, no child process spawn). We get back a + // structured InstallResult with InstallResultStatus, ExtendedErrorCode + // (the same HRESULT that the CLI would have printed), and + // InstallerErrorCode — far better diagnostics than the CLI ever gave + // us, and reliably available from packaged context. + + namespace { - // Copy packageId before switching threads (coroutine parameter safety) - auto id = std::wstring{ packageId }; + using namespace winrt::Microsoft::Management::Deployment; - co_await winrt::resume_background(); + // Enum-to-string helpers — log values are human-readable instead + // of bare ints, so anyone reading the log can grep the winmd / + // PackageManager.idl directly without an enum reference table. - auto cmdline = fmt::format( - L"winget install --id {} --exact --silent " - L"--source winget " - L"--accept-source-agreements --accept-package-agreements " - L"--disable-interactivity", - id); - - // Create a pipe to capture winget's combined stdout+stderr for - // diagnostic logging. The pipe is inheritable so the child - // process writes directly to it. - SECURITY_ATTRIBUTES sa{}; - sa.nLength = sizeof(sa); - sa.bInheritHandle = TRUE; - HANDLE hReadPipe = nullptr, hWritePipe = nullptr; - const bool hasPipe = CreatePipe(&hReadPipe, &hWritePipe, &sa, 0); - if (hasPipe) + constexpr const char* ConnectStatusName(ConnectResultStatus s) noexcept { - // Prevent the read end from being inherited by the child. - SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); + switch (s) + { + case ConnectResultStatus::Ok: return "Ok"; + case ConnectResultStatus::CatalogError: return "CatalogError"; + case ConnectResultStatus::SourceAgreementsNotAccepted: return "SourceAgreementsNotAccepted"; + default: return "Unknown"; + } } - STARTUPINFOW si{}; - si.cb = sizeof(si); - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; - if (hasPipe) - { - si.dwFlags |= STARTF_USESTDHANDLES; - si.hStdOutput = hWritePipe; - si.hStdError = hWritePipe; - si.hStdInput = nullptr; - } - PROCESS_INFORMATION pi{}; - - auto success = CreateProcessW( - nullptr, - cmdline.data(), - nullptr, nullptr, hasPipe ? TRUE : FALSE, - CREATE_NO_WINDOW, - nullptr, nullptr, &si, &pi); - - // Close the write end in the parent so ReadFile sees EOF - // when the child exits. - if (hWritePipe) + constexpr const char* FindStatusName(FindPackagesResultStatus s) noexcept { - CloseHandle(hWritePipe); - hWritePipe = nullptr; + switch (s) + { + case FindPackagesResultStatus::Ok: return "Ok"; + case FindPackagesResultStatus::BlockedByPolicy: return "BlockedByPolicy"; + case FindPackagesResultStatus::CatalogError: return "CatalogError"; + case FindPackagesResultStatus::InvalidOptions: return "InvalidOptions"; + case FindPackagesResultStatus::InternalError: return "InternalError"; + default: return "Unknown"; + } } - if (!success) + constexpr const char* InstallStatusName(InstallResultStatus s) noexcept { - _agentPaneLog("[FRE] winget CreateProcess failed: GetLastError=" + std::to_string(GetLastError())); - if (hReadPipe) CloseHandle(hReadPipe); - co_return false; + switch (s) + { + case InstallResultStatus::Ok: return "Ok"; + case InstallResultStatus::BlockedByPolicy: return "BlockedByPolicy"; + case InstallResultStatus::CatalogError: return "CatalogError"; + case InstallResultStatus::InternalError: return "InternalError"; + case InstallResultStatus::InvalidOptions: return "InvalidOptions"; + case InstallResultStatus::DownloadError: return "DownloadError"; + case InstallResultStatus::InstallError: return "InstallError"; + case InstallResultStatus::ManifestError: return "ManifestError"; + case InstallResultStatus::NoApplicableInstallers: return "NoApplicableInstallers"; + case InstallResultStatus::NoApplicableUpgrade: return "NoApplicableUpgrade"; + case InstallResultStatus::PackageAgreementsNotAccepted: return "PackageAgreementsNotAccepted"; + default: return "Unknown"; + } } + } + + IAsyncOperation FreOverlay::_WingetInstallAsync(winrt::hstring packageId) + { + using namespace winrt::Microsoft::Management::Deployment; + + // Copy packageId before switching threads (coroutine parameter safety) + auto id = winrt::hstring{ packageId }; - // Wait for the child process first, then drain any remaining - // pipe output. This avoids the synchronous ReadFile blocking - // indefinitely if winget spawns child processes that inherit - // the pipe handle and outlive winget itself. - WaitForSingleObject(pi.hProcess, 300000); // 5 min timeout - - // Drain pipe output (non-blocking — child has exited, so the - // write end is closed and ReadFile will see EOF promptly). - // Keep only the last ~500 bytes to cap memory usage. - static constexpr size_t kMaxOutput = 500; - std::string output; - if (hasPipe && hReadPipe) + co_await winrt::resume_background(); + + try { - char buf[512]; - DWORD bytesRead = 0; - while (ReadFile(hReadPipe, buf, sizeof(buf) - 1, &bytesRead, nullptr) && bytesRead > 0) + // ── 1. Activate the out-of-proc PackageManager COM server ── + const PackageManager pm = WindowsPackageManagerFactory::CreatePackageManager(); + + // ── 2. Connect to the winget catalog ── + // Mirror the pattern used by `TerminalPage._FindPackageAsync`: + // up to 3 attempts to absorb transient connection flakes. + // Set AcceptSourceAgreements(true) — equivalent to the CLI's + // --accept-source-agreements; without this, first-time winget + // users (no prior agreement acceptance recorded) would hit + // SourceAgreementsNotAccepted and be unable to install. + auto catalogRef = pm.GetPredefinedPackageCatalog(PredefinedPackageCatalog::OpenWindowsCatalog); + catalogRef.AcceptSourceAgreements(true); + + ConnectResult connectResult{ nullptr }; + for (int attempt = 0; attempt < 3; ++attempt) { - buf[bytesRead] = '\0'; - output += buf; - // Keep only the tail - if (output.size() > kMaxOutput * 2) - output = output.substr(output.size() - kMaxOutput); + connectResult = catalogRef.Connect(); + if (connectResult.Status() == ConnectResultStatus::Ok) + { + break; + } + } + if (connectResult.Status() != ConnectResultStatus::Ok) + { + _agentPaneLog(fmt::format( + "[FRE] winget catalog connect failed: {} (status={})", + ConnectStatusName(connectResult.Status()), + static_cast(connectResult.Status()))); + co_return false; + } + + // ── 3. Find the package by exact ID ── + auto filter = WindowsPackageManagerFactory::CreatePackageMatchFilter(); + filter.Field(PackageMatchField::Id); + filter.Option(PackageFieldMatchOption::Equals); + filter.Value(id); + + auto findOpts = WindowsPackageManagerFactory::CreateFindPackagesOptions(); + findOpts.Filters().Append(filter); + findOpts.ResultLimit(1); + + const auto findResult = co_await connectResult.PackageCatalog().FindPackagesAsync(findOpts); + + if (findResult.Status() != FindPackagesResultStatus::Ok) + { + _agentPaneLog(fmt::format( + "[FRE] winget FindPackages failed: {} (status={})", + FindStatusName(findResult.Status()), + static_cast(findResult.Status()))); + co_return false; + } + if (findResult.Matches().Size() == 0) + { + _agentPaneLog("[FRE] winget package not found: " + winrt::to_string(id)); + co_return false; + } + + const CatalogPackage package = findResult.Matches().GetAt(0).CatalogPackage(); + + // ── 4. Configure install options and kick off install ── + auto installOpts = WindowsPackageManagerFactory::CreateInstallOptions(); + installOpts.AcceptPackageAgreements(true); + installOpts.PackageInstallMode(PackageInstallMode::Silent); + installOpts.PackageInstallScope(PackageInstallScope::Any); + + const auto installOp = pm.InstallPackageAsync(package, installOpts); + + // ── 5. Bounded wait for install to complete ── + // The COM API has no built-in timeout. Without one, a stuck + // broker / unreachable installer would freeze the FRE Save + // flow indefinitely. We allow up to 20 min (observed cold + // installs are ~5-6 min, so 20 min covers the P99 with a + // ~3x safety margin); at the 5 min mark we log a heads-up + // so anyone tailing the log can tell "still running" apart + // from "deadlocked". + constexpr DWORD kInstallSoftWarnMs = 5 * 60 * 1000; // 5 min + constexpr DWORD kInstallHardCapMs = 20 * 60 * 1000; // 20 min + const auto startTick = GetTickCount64(); + bool warnedSoft = false; + while (installOp.Status() == winrt::Windows::Foundation::AsyncStatus::Started) + { + const auto elapsed = GetTickCount64() - startTick; + if (!warnedSoft && elapsed > kInstallSoftWarnMs) + { + _agentPaneLog("[FRE] winget install: still running after 5 min, will hard-cancel at 20 min"); + warnedSoft = true; + } + if (elapsed > kInstallHardCapMs) + { + _agentPaneLog("[FRE] winget install: hard timeout after 20 min, cancelling"); + installOp.Cancel(); + co_return false; + } + co_await winrt::resume_after(std::chrono::milliseconds(500)); + } + const auto installResult = installOp.GetResults(); + + const auto status = installResult.Status(); + const auto exHr = installResult.ExtendedErrorCode(); + const auto installerErr = installResult.InstallerErrorCode(); + + if (status != InstallResultStatus::Ok) + { + _agentPaneLog(fmt::format( + "[FRE] winget install failed: {} (status={}) hr=0x{:08X} installerErr={}", + InstallStatusName(status), + static_cast(status), + static_cast(exHr), + installerErr)); + co_return false; } - CloseHandle(hReadPipe); - hReadPipe = nullptr; - } - DWORD exitCode = 1; - GetExitCodeProcess(pi.hProcess, &exitCode); - CloseHandle(pi.hProcess); - CloseHandle(pi.hThread); + // Surface RebootRequired so the caller / log readers can see + // it. GitHub.Copilot never sets this; some MSI-style packages + // (e.g. Node.js LTS) theoretically might. We still return + // true because the install itself succeeded — the caller's + // post-install steps (PATH refresh, hook install) may or may + // not work fully until reboot, but that's the caller's call + // and we shouldn't fail an otherwise-successful install. + if (installResult.RebootRequired()) + { + _agentPaneLog("[FRE] winget install: ok (reboot required)"); + } - // Log the result — truncate output to avoid unbounded log growth. - if (exitCode != 0) + co_return true; + } + catch (const winrt::hresult_error& e) { - // Trim trailing whitespace - while (!output.empty() && (output.back() == '\n' || output.back() == '\r' || output.back() == ' ')) - output.pop_back(); - // Cap at 500 chars - if (output.size() > 500) - output = output.substr(output.size() - 500); - _agentPaneLog("[FRE] winget exit=" + std::to_string(exitCode) + " output: " + output); + _agentPaneLog(fmt::format( + "[FRE] winget exception: hr=0x{:08X} msg={}", + static_cast(e.code().value), + winrt::to_string(e.message()))); + co_return false; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + co_return false; } - - co_return exitCode == 0; } From 8c330a2b42ee2d5a05ab99aeb8ddd06c9dd615ed Mon Sep 17 00:00:00 2001 From: "Haonan Tang (from Dev Box)" Date: Wed, 10 Jun 2026 14:42:58 +0800 Subject: [PATCH 3/7] resolve comments --- src/cascadia/TerminalApp/FreOverlay.cpp | 27 +++++++++++++++++++++---- src/cascadia/TerminalApp/FreOverlay.h | 4 ++-- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/cascadia/TerminalApp/FreOverlay.cpp b/src/cascadia/TerminalApp/FreOverlay.cpp index 6980d7f73..08cc6a1d6 100644 --- a/src/cascadia/TerminalApp/FreOverlay.cpp +++ b/src/cascadia/TerminalApp/FreOverlay.cpp @@ -438,10 +438,17 @@ namespace winrt::TerminalApp::implementation // sees a warm source manifest cache and skips the 3-20s refresh step. // Gated on whether the install would actually run (Copilot or Node // missing) AND winget being available. Single-flight per process — - // re-entrant Initialize() calls and multi-window FRE coalesce onto + // reentrant Initialize() calls and multi-window FRE coalesce onto // one running prewarm. The Save handler awaits s_prewarmAction before - // its own winget call (see _SaveAndInstallAsync), guaranteeing the - // two winget operations never run concurrently. + // its own winget call (see _SaveAndInstallAsync); in practice the + // two winget operations never run concurrently. Exception: if + // _RunPrewarmAsync hits its 120s timeout, it returns while the + // underlying `winget source update` may still be running in the + // background. We accept this tradeoff because killing winget + // mid-write risks corrupting its source DB, and 120s timeouts are + // very rare in practice. A future migration of the prewarm to the + // COM `RefreshPackageCatalogAsync` API would eliminate this race + // entirely. void FreOverlay::_MaybeStartPrewarm(bool copilotMissing, bool nodeMissing) { @@ -941,6 +948,18 @@ namespace winrt::TerminalApp::implementation // available before kicking off the install — otherwise the user // gets a generic "install failed" error that wrongly points at // the package's docs instead of the winget setup docs. + // + // Note: `_IsWingetInstalled()` checks for `winget.exe` on PATH, + // which is the CLI's App Execution Alias. `_WingetInstallAsync` + // itself now uses the WinGet COM API and does not strictly + // require the alias to be on PATH (only AppInstaller / the COM + // server). In practice the alias and the COM server are + // installed/uninstalled together with AppInstaller, so this + // check still correctly distinguishes "winget environment + // present" from "winget environment absent" in 99%+ of cases. + // The edge case (alias disabled while AppInstaller present) is + // rare enough that incorrectly showing WingetMissing is + // acceptable; the user docs still apply. if (needsCopilot || needsNode) { if (!_IsWingetInstalled()) @@ -952,7 +971,7 @@ namespace winrt::TerminalApp::implementation // ── Await any in-flight pre-warm before kicking off install ── // The Initialize() handler may have started a `winget source - // update` in the background. Winget's intra-process + // update` in the background. WinGet's intra-process // coordination across concurrent operations is not a // guaranteed contract — we serialise here to avoid two // winget instances stepping on each other. Snapshot the diff --git a/src/cascadia/TerminalApp/FreOverlay.h b/src/cascadia/TerminalApp/FreOverlay.h index 398b871ff..295116763 100644 --- a/src/cascadia/TerminalApp/FreOverlay.h +++ b/src/cascadia/TerminalApp/FreOverlay.h @@ -85,11 +85,11 @@ namespace winrt::TerminalApp::implementation static bool _IsNodeInstalled(); static bool _IsWingetInstalled(); - // ── Winget source pre-warm coordination ───────────────────── + // ── WinGet source pre-warm coordination ───────────────────── // While the FRE overlay is on screen (Welcome + Settings pages), // pre-warm winget's source manifest cache in the background so // the on-Save `winget install` skips the 3-20s source refresh. - // Single-flight per process — re-entrant Initialize() calls and + // Single-flight per process — reentrant Initialize() calls and // multi-window FRE coalesce onto one running prewarm. The Save // handler awaits s_prewarmAction before its own winget call to // guarantee the two winget operations never run concurrently From 94412154609183d40d788b135bca7da66272e340 Mon Sep 17 00:00:00 2001 From: "Haonan Tang (from Dev Box)" Date: Wed, 10 Jun 2026 14:55:53 +0800 Subject: [PATCH 4/7] resolve comment --- src/cascadia/TerminalApp/FreOverlay.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cascadia/TerminalApp/FreOverlay.h b/src/cascadia/TerminalApp/FreOverlay.h index 295116763..6d06ab847 100644 --- a/src/cascadia/TerminalApp/FreOverlay.h +++ b/src/cascadia/TerminalApp/FreOverlay.h @@ -6,6 +6,8 @@ #include "FreAgentEntry.g.h" #include "FreOverlay.g.h" +#include + namespace winrt::TerminalApp::implementation { struct FreAgentEntry : FreAgentEntryT From be91f9dabe5dc9fa36aca80b0f266a65e00a37dd Mon Sep 17 00:00:00 2001 From: "Haonan Tang (from Dev Box)" Date: Thu, 11 Jun 2026 15:18:31 +0800 Subject: [PATCH 5/7] Surface structured winget install failures in FRE Map InstallResult/ExtendedErrorCode/InstallerErrorCode + winget-CLI APPINSTALLER_CLI_ERROR_* HRESULTs into 7 distinct failure kinds (Network / BlockedByPolicy / PackageNotFound / NoCompatibleInstaller / InstallerFailed / Timeout / Generic), each with its own localized message instead of the previous one-size-fits-all 'Failed to install ... Check your network and try again.' Also fixes a latent UB in _SaveAndInstallAsync (Dispatcher() after co_await could deref dangling this if the overlay was destroyed mid-install) by capturing the dispatcher up front. All 88 non-en-US locale resources updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cascadia/TerminalApp/FreOverlay.cpp | 397 ++++++++++++++++-- src/cascadia/TerminalApp/FreOverlay.h | 99 ++++- .../Resources/af-ZA/Resources.resw | 48 ++- .../Resources/am-ET/Resources.resw | 48 ++- .../Resources/ar-SA/Resources.resw | 48 ++- .../Resources/as-IN/Resources.resw | 69 ++- .../Resources/az-Latn-AZ/Resources.resw | 48 ++- .../Resources/bg-BG/Resources.resw | 47 ++- .../Resources/bn-IN/Resources.resw | 69 ++- .../Resources/bs-Latn-BA/Resources.resw | 47 ++- .../Resources/ca-ES/Resources.resw | 46 +- .../Resources/ca-Es-VALENCIA/Resources.resw | 46 +- .../Resources/cs-CZ/Resources.resw | 47 ++- .../Resources/cy-GB/Resources.resw | 46 +- .../Resources/da-DK/Resources.resw | 46 +- .../Resources/de-DE/Resources.resw | 46 +- .../Resources/el-GR/Resources.resw | 47 ++- .../Resources/en-GB/Resources.resw | 46 +- .../Resources/en-US/Resources.resw | 48 ++- .../Resources/es-ES/Resources.resw | 46 +- .../Resources/es-MX/Resources.resw | 46 +- .../Resources/et-EE/Resources.resw | 47 ++- .../Resources/eu-ES/Resources.resw | 46 +- .../Resources/fa-IR/Resources.resw | 48 ++- .../Resources/fi-FI/Resources.resw | 46 +- .../Resources/fil-PH/Resources.resw | 46 +- .../Resources/fr-CA/Resources.resw | 46 +- .../Resources/fr-FR/Resources.resw | 46 +- .../Resources/ga-IE/Resources.resw | 46 +- .../Resources/gd-gb/Resources.resw | 48 ++- .../Resources/gl-ES/Resources.resw | 46 +- .../Resources/gu-IN/Resources.resw | 69 ++- .../Resources/he-IL/Resources.resw | 48 ++- .../Resources/hi-IN/Resources.resw | 69 ++- .../Resources/hr-HR/Resources.resw | 47 ++- .../Resources/hu-HU/Resources.resw | 47 ++- .../Resources/hy-AM/Resources.resw | 48 ++- .../Resources/id-ID/Resources.resw | 46 +- .../Resources/is-IS/Resources.resw | 46 +- .../Resources/it-IT/Resources.resw | 46 +- .../Resources/ja-JP/Resources.resw | 46 +- .../Resources/ka-GE/Resources.resw | 48 ++- .../Resources/kk-KZ/Resources.resw | 48 ++- .../Resources/km-KH/Resources.resw | 46 +- .../Resources/kn-IN/Resources.resw | 69 ++- .../Resources/ko-KR/Resources.resw | 46 +- .../Resources/kok-IN/Resources.resw | 69 ++- .../Resources/lb-LU/Resources.resw | 46 +- .../Resources/lo-LA/Resources.resw | 46 +- .../Resources/lt-LT/Resources.resw | 47 ++- .../Resources/lv-LV/Resources.resw | 47 ++- .../Resources/mi-NZ/Resources.resw | 46 +- .../Resources/mk-MK/Resources.resw | 47 ++- .../Resources/ml-IN/Resources.resw | 69 ++- .../Resources/mr-IN/Resources.resw | 69 ++- .../Resources/ms-MY/Resources.resw | 46 +- .../Resources/mt-MT/Resources.resw | 48 ++- .../Resources/nb-NO/Resources.resw | 46 +- .../Resources/ne-NP/Resources.resw | 69 ++- .../Resources/nl-NL/Resources.resw | 46 +- .../Resources/nn-NO/Resources.resw | 46 +- .../Resources/or-IN/Resources.resw | 69 ++- .../Resources/pa-IN/Resources.resw | 69 ++- .../Resources/pl-PL/Resources.resw | 47 ++- .../Resources/pt-BR/Resources.resw | 46 +- .../Resources/pt-PT/Resources.resw | 46 +- .../Resources/qps-ploc/Resources.resw | 46 +- .../Resources/qps-ploca/Resources.resw | 46 +- .../Resources/qps-plocm/Resources.resw | 46 +- .../Resources/quz-PE/Resources.resw | 48 ++- .../Resources/ro-RO/Resources.resw | 46 +- .../Resources/ru-RU/Resources.resw | 47 ++- .../Resources/sk-SK/Resources.resw | 47 ++- .../Resources/sl-SI/Resources.resw | 47 ++- .../Resources/sq-AL/Resources.resw | 47 ++- .../Resources/sr-Cyrl-BA/Resources.resw | 47 ++- .../Resources/sr-Cyrl-RS/Resources.resw | 47 ++- .../Resources/sr-Latn-RS/Resources.resw | 47 ++- .../Resources/sv-SE/Resources.resw | 46 +- .../Resources/ta-IN/Resources.resw | 69 ++- .../Resources/te-IN/Resources.resw | 69 ++- .../Resources/th-TH/Resources.resw | 46 +- .../Resources/tr-TR/Resources.resw | 47 ++- .../Resources/tt-RU/Resources.resw | 48 ++- .../Resources/ug-CN/Resources.resw | 48 ++- .../Resources/uk-UA/Resources.resw | 47 ++- .../Resources/ur-PK/Resources.resw | 69 ++- .../Resources/uz-Latn-UZ/Resources.resw | 48 ++- .../Resources/vi-VN/Resources.resw | 46 +- .../Resources/zh-CN/Resources.resw | 46 +- .../Resources/zh-TW/Resources.resw | 46 +- 91 files changed, 4141 insertions(+), 823 deletions(-) diff --git a/src/cascadia/TerminalApp/FreOverlay.cpp b/src/cascadia/TerminalApp/FreOverlay.cpp index 08cc6a1d6..9bd02d778 100644 --- a/src/cascadia/TerminalApp/FreOverlay.cpp +++ b/src/cascadia/TerminalApp/FreOverlay.cpp @@ -609,13 +609,38 @@ namespace winrt::TerminalApp::implementation } } - IAsyncOperation FreOverlay::_WingetInstallAsync(winrt::hstring packageId) + IAsyncOperation FreOverlay::_WingetInstallAsync(winrt::hstring packageId) { using namespace winrt::Microsoft::Management::Deployment; + using Kind = FreWingetFailureKind; + auto encode = [](Kind k) noexcept { return static_cast(k); }; + + // Capture a weak reference so writes to _lastWinget* are safe even + // if the overlay is destroyed mid-await (e.g. user dismissed the + // window during a long install). + auto weak = get_weak(); + + // Helper: persist hr + installer error code to instance state, if + // the overlay is still alive. Read by _SaveAndInstallAsync right + // after our co_return to drive _ShowWingetProblem. + auto stash = [&weak](int32_t hr, uint32_t installerErr) { + if (auto self = weak.get()) + { + self->_lastWingetHr = hr; + self->_lastWingetInstallerErrorCode = installerErr; + } + }; // Copy packageId before switching threads (coroutine parameter safety) auto id = winrt::hstring{ packageId }; + // Local diagnostic state. We write to the instance fields only via + // `stash(...)` immediately before each co_return, so the caller + // always sees consistent (kind, hr, installerErr) tuples and a + // stale value never leaks across calls. + int32_t hr = 0; + uint32_t installerErr = 0; + co_await winrt::resume_background(); try @@ -648,7 +673,17 @@ namespace winrt::TerminalApp::implementation "[FRE] winget catalog connect failed: {} (status={})", ConnectStatusName(connectResult.Status()), static_cast(connectResult.Status()))); - co_return false; + // CatalogError during connect almost always means we + // couldn't reach the catalog server (DNS / TLS / proxy / + // firewall). The 1.8 contract doesn't expose + // ConnectResult.ExtendedErrorCode so we can't whitelist + // here — treat the catalog-error case as Network and + // anything else (SourceAgreementsNotAccepted, future + // statuses) as Generic. + stash(hr, installerErr); + co_return encode(connectResult.Status() == ConnectResultStatus::CatalogError + ? Kind::Network + : Kind::Generic); } // ── 3. Find the package by exact ID ── @@ -669,12 +704,16 @@ namespace winrt::TerminalApp::implementation "[FRE] winget FindPackages failed: {} (status={})", FindStatusName(findResult.Status()), static_cast(findResult.Status()))); - co_return false; + stash(hr, installerErr); + co_return encode(findResult.Status() == FindPackagesResultStatus::BlockedByPolicy + ? Kind::BlockedByPolicy + : Kind::Generic); } if (findResult.Matches().Size() == 0) { _agentPaneLog("[FRE] winget package not found: " + winrt::to_string(id)); - co_return false; + stash(hr, installerErr); + co_return encode(Kind::PackageNotFound); } const CatalogPackage package = findResult.Matches().GetAt(0).CatalogPackage(); @@ -709,9 +748,14 @@ namespace winrt::TerminalApp::implementation } if (elapsed > kInstallHardCapMs) { + // Cancel is best-effort — if the installer's already + // running, it may keep going in the background. We + // surface that nuance in the user-facing Timeout + // message (see FreOverlay_InstallError_Timeout). _agentPaneLog("[FRE] winget install: hard timeout after 20 min, cancelling"); installOp.Cancel(); - co_return false; + stash(hr, installerErr); + co_return encode(Kind::Timeout); } co_await winrt::resume_after(std::chrono::milliseconds(500)); } @@ -719,7 +763,9 @@ namespace winrt::TerminalApp::implementation const auto status = installResult.Status(); const auto exHr = installResult.ExtendedErrorCode(); - const auto installerErr = installResult.InstallerErrorCode(); + const auto rawInstallerErr = installResult.InstallerErrorCode(); + hr = static_cast(exHr); + installerErr = rawInstallerErr; if (status != InstallResultStatus::Ok) { @@ -728,14 +774,53 @@ namespace winrt::TerminalApp::implementation InstallStatusName(status), static_cast(status), static_cast(exHr), - installerErr)); - co_return false; + rawInstallerErr)); + + Kind kind = Kind::Generic; + switch (status) + { + case InstallResultStatus::BlockedByPolicy: + kind = Kind::BlockedByPolicy; + break; + case InstallResultStatus::NoApplicableInstallers: + kind = Kind::NoCompatibleInstaller; + break; + case InstallResultStatus::DownloadError: + // Network whitelist gates the user-facing "check your + // VPN" message — anything else (hash/cert/disk/AV) + // falls back to Generic so we don't send the user + // chasing the wrong problem. + kind = _IsNetworkLikeHResult(hr) + ? Kind::Network + : Kind::Generic; + break; + case InstallResultStatus::InstallError: + kind = Kind::InstallerFailed; + break; + // CatalogError / InternalError / ManifestError / + // InvalidOptions / NoApplicableUpgrade / + // PackageAgreementsNotAccepted / unknown future values + // → Generic. CatalogError is a special case: it often + // has a winget-specific or network HRESULT attached + // (e.g. APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY if the + // catalog is GP-blocked, or a WinINet DNS code if the + // source server is unreachable), so route through the + // full classifier instead of just the network check. + case InstallResultStatus::CatalogError: + kind = _ClassifyWingetHResult(hr); + break; + default: + kind = Kind::Generic; + break; + } + stash(hr, installerErr); + co_return encode(kind); } // Surface RebootRequired so the caller / log readers can see // it. GitHub.Copilot never sets this; some MSI-style packages // (e.g. Node.js LTS) theoretically might. We still return - // true because the install itself succeeded — the caller's + // Success because the install itself succeeded — the caller's // post-install steps (PATH refresh, hook install) may or may // not work fully until reboot, but that's the caller's call // and we shouldn't fail an otherwise-successful install. @@ -744,23 +829,138 @@ namespace winrt::TerminalApp::implementation _agentPaneLog("[FRE] winget install: ok (reboot required)"); } - co_return true; + stash(0, 0); // success: clear any stale diagnostic state + co_return encode(Kind::Success); } catch (const winrt::hresult_error& e) { + hr = static_cast(e.code().value); _agentPaneLog(fmt::format( "[FRE] winget exception: hr=0x{:08X} msg={}", - static_cast(e.code().value), + static_cast(hr), winrt::to_string(e.message()))); - co_return false; + stash(hr, installerErr); + // _ClassifyWingetHResult recognizes APPINSTALLER_CLI_ERROR_* + // codes (e.g. 0x8A15003A == BLOCKED_BY_POLICY), so a GP + // block surfaces as Kind::BlockedByPolicy with the + // actionable "contact IT admin" message instead of the + // generic "(error code 0x8A15003A)" fallback. + co_return encode(_ClassifyWingetHResult(hr)); } catch (...) { LOG_CAUGHT_EXCEPTION(); - co_return false; + stash(hr, installerErr); + co_return encode(Kind::Generic); + } + } + + // Conservative network-class HRESULT whitelist. We list the specific + // WinINet / WinHTTP / Winsock codes that genuinely indicate a network + // problem (DNS failure, connection refused/timed out, TLS handshake + // failure, etc.). We deliberately do NOT include: + // * HTTP-status HRESULTs (0x80190xxx) — HTTP 404 / 403 / 5xx aren't + // "check your VPN" situations, they mean the request reached the + // server. + // * RPC_E_* — those are COM/service activation failures, not network. + // * Whole facility ranges — too easy to misclassify edge cases. + // + // Names in trailing comments are the macros from winhttp.h / wininet.h + // / winsock2.h, kept here so we don't need to pull those headers in. + bool FreOverlay::_IsNetworkLikeHResult(int32_t hr) noexcept + { + switch (static_cast(hr)) + { + // FACILITY_INTERNET (12xxx range) — WinINet & WinHTTP share these + case 0x80072EE2: // ERROR_INTERNET_TIMEOUT / ERROR_WINHTTP_TIMEOUT (12002) + case 0x80072EE7: // ERROR_INTERNET_NAME_NOT_RESOLVED (12007) + case 0x80072EFD: // ERROR_INTERNET_CANNOT_CONNECT (12029) + case 0x80072EFE: // ERROR_INTERNET_CONNECTION_ABORTED (12030) + case 0x80072EFF: // ERROR_INTERNET_CONNECTION_RESET (12031) + case 0x80072F8F: // ERROR_INTERNET_SECURITY_CHANNEL_ERROR (TLS) (12175) + // FACILITY_WIN32 (Winsock 100xx, mapped via HRESULT_FROM_WIN32) + case 0x80072742: // WSAENETDOWN (10050) + case 0x80072743: // WSAENETUNREACH (10051) + case 0x80072744: // WSAENETRESET (10052) + case 0x80072745: // WSAECONNABORTED (10053) + case 0x80072746: // WSAECONNRESET (10054) + case 0x8007274C: // WSAETIMEDOUT (10060) + case 0x8007274D: // WSAECONNREFUSED (10061) + case 0x80072751: // WSAEHOSTUNREACH (10065) + case 0x80072AF9: // WSAHOST_NOT_FOUND (11001) + case 0x80072AFC: // WSANO_DATA (11004) + return true; + default: + return false; } } + // Map a raw HRESULT to the most-specific FreWingetFailureKind we can + // infer. Used in two places: + // * the catch block of _WingetInstallAsync, where winget COM throws + // APPINSTALLER_CLI_ERROR_* codes directly (this is how policy + // blocks surface — winget throws 0x8A15003A *before* it ever + // returns an InstallResult); + // * the CatalogError install-status branch, where the structured + // Status is generic but the ExtendedErrorCode tells us why. + // + // The match order matters. APPINSTALLER_CLI_ERROR_* codes are + // checked first because their meaning is unambiguous; the network + // whitelist comes last as a transport-level fallback. + // + // Code names come from + // https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerSharedLib/Public/AppInstallerErrors.h + // and are kept here as comments so we don't need to take a header + // dependency on the winget-cli repo. + FreOverlay::FreWingetFailureKind FreOverlay::_ClassifyWingetHResult(int32_t hr) noexcept + { + using Kind = FreWingetFailureKind; + switch (static_cast(hr)) + { + // BlockedByPolicy family — group policy disabled winget or a + // specific source/feature. Triggered by setting + // HKLM\SOFTWARE\Policies\Microsoft\Windows\AppInstaller\EnableAppInstaller + // (and friends) to 0. + case 0x8A15003A: // APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY + case 0x8A15001B: // APPINSTALLER_CLI_ERROR_MSSTORE_BLOCKED_BY_POLICY + case 0x8A15001C: // APPINSTALLER_CLI_ERROR_MSSTORE_APP_BLOCKED_BY_POLICY + case 0x8A15001D: // APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED + case 0x8A15010F: // APPINSTALLER_CLI_ERROR_INSTALL_BLOCKED_BY_POLICY (install-phase variant of 0x8A15003A) + return Kind::BlockedByPolicy; + + // Network / download failure codes that winget itself attaches + // (separate from the generic WinINet/Winsock whitelist below). + // INSTALL_NO_NETWORK is winget self-diagnosing "no network"; + // DOWNLOAD_FAILED is the install-phase wrapper around any + // transport error during package download. + case 0x8A150008: // APPINSTALLER_CLI_ERROR_DOWNLOAD_FAILED + case 0x8A150107: // APPINSTALLER_CLI_ERROR_INSTALL_NO_NETWORK + return Kind::Network; + + // Manifest was found but no installer entry matches this + // machine's OS / architecture / scope. Usually surfaces as + // InstallResultStatus::NoApplicableInstallers, but cover the + // exception form for older winget versions / unusual flows. + case 0x8A150010: // APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER + return Kind::NoCompatibleInstaller; + + // No manifest with the requested package ID exists in any + // configured source. Usually surfaces as + // findResult.Matches().Size() == 0, but defensive coverage for + // the exception form. + case 0x8A150014: // APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND + return Kind::PackageNotFound; + } + + // No winget-specific match — fall back to the transport-level + // network whitelist (DNS / connect / TLS), then Generic. + if (_IsNetworkLikeHResult(hr)) + { + return Kind::Network; + } + return Kind::Generic; + } + // ── Hooks install helper ──────────────────────────────────────────── @@ -802,14 +1002,6 @@ namespace winrt::TerminalApp::implementation ErrorText().Text(RS_(L"FreOverlay_InstallErrorWingetMissing")); url += L"#1-winget-windows-package-manager"; break; - case FreProblemKind::CopilotInstall: - ErrorText().Text(RS_(L"FreOverlay_InstallErrorCopilot")); - url += L"#31-github-copilot-cli"; - break; - case FreProblemKind::NodeInstall: - ErrorText().Text(RS_(L"FreOverlay_InstallErrorNode")); - url += L"#2-nodejs-lts--shared-prerequisite"; - break; case FreProblemKind::ShellIntegrationExecutionPolicy: ErrorText().Text(RS_(L"FreOverlay_InstallErrorShellIntegrationExecutionPolicy")); url += L"#4-powershell-shell-integration"; @@ -846,6 +1038,116 @@ namespace winrt::TerminalApp::implementation break; } + _FinalizeProblemDisplay(url); + } + + // Render a winget install failure with package + failure-kind specific + // text. The mapping from FreWingetFailureKind → resource template is the + // user-facing half of the COM API rewrite: structured InstallResultStatus + // values become actionable, distinct messages instead of the previous + // one-size-fits-all "check your network and try again". The help-link + // URL is keyed on the package so it still deep-links to the right + // manual-setup section. + void FreOverlay::_ShowWingetProblem(FreWingetPackage package, + FreWingetFailureKind kind, + int32_t hr, + uint32_t installerErrorCode) + { + static constexpr std::wstring_view baseUrl{ L"https://aka.ms/intelligent-terminal-dependency" }; + std::wstring url{ baseUrl }; + + // Per-package: URL anchor + display name. The display name is a + // localized resource (Copilot's name doesn't translate, but + // "Node.js (LTS)" might be punctuated differently in some + // locales — keep it loc-controlled either way). + winrt::hstring packageName; + switch (package) + { + case FreWingetPackage::Copilot: + url += L"#31-github-copilot-cli"; + packageName = RS_(L"FreOverlay_PackageDisplayName_Copilot"); + break; + case FreWingetPackage::Node: + url += L"#2-nodejs-lts--shared-prerequisite"; + packageName = RS_(L"FreOverlay_PackageDisplayName_Node"); + break; + } + + // Pre-format the numeric codes in C++ instead of relying on + // resource-side format specs (`{1:08X}` is not portable across + // every resource consumer, and pre-formatting also guarantees + // ASCII hex digits regardless of the user's locale digit shape). + const winrt::hstring hrStr{ fmt::format(L"0x{:08X}", static_cast(hr)) }; + const winrt::hstring installerStr{ std::to_wstring(installerErrorCode) }; + + // RS_fmt requires literal keys (extracted at build time), so the + // template selection is a switch rather than a key-lookup. + std::wstring text; + switch (kind) + { + case FreWingetFailureKind::Network: + text = RS_fmt(L"FreOverlay_InstallError_Network", packageName); + break; + case FreWingetFailureKind::BlockedByPolicy: + text = RS_fmt(L"FreOverlay_InstallError_BlockedByPolicy", packageName); + break; + case FreWingetFailureKind::PackageNotFound: + text = RS_fmt(L"FreOverlay_InstallError_PackageNotFound", packageName); + break; + case FreWingetFailureKind::NoCompatibleInstaller: + text = RS_fmt(L"FreOverlay_InstallError_NoCompatibleInstaller", packageName); + break; + case FreWingetFailureKind::InstallerFailed: + // If the installer reported a specific exit code, surface it. + // Otherwise (winget said InstallError but InstallerErrorCode + // is 0 — installer crashed without an exit code, winget + // didn't capture one, etc.), claiming "reported error + // (code 0)" would mislead the user. Fall back to the + // Generic template with the HRESULT, or GenericNoCode if + // we also lack an HRESULT. + if (installerErrorCode != 0) + { + text = RS_fmt(L"FreOverlay_InstallError_InstallerFailed", packageName, installerStr); + } + else if (hr != 0) + { + text = RS_fmt(L"FreOverlay_InstallError_Generic", packageName, hrStr); + } + else + { + text = RS_fmt(L"FreOverlay_InstallError_GenericNoCode", packageName); + } + break; + case FreWingetFailureKind::Timeout: + text = RS_fmt(L"FreOverlay_InstallError_Timeout", packageName); + break; + case FreWingetFailureKind::Success: + // Caller shouldn't invoke us on Success — fall through to + // the no-code Generic template so a bug here surfaces a + // readable message instead of "error code 0x00000000". + case FreWingetFailureKind::Generic: + default: + // When hr == 0 we have no actionable error code to show + // (e.g. catalog connect / package search failed before any + // installer ran). Use the no-code template so users don't + // see the misleading "(error code 0x00000000)". + if (hr == 0) + { + text = RS_fmt(L"FreOverlay_InstallError_GenericNoCode", packageName); + } + else + { + text = RS_fmt(L"FreOverlay_InstallError_Generic", packageName, hrStr); + } + break; + } + ErrorText().Text(winrt::hstring{ text }); + + _FinalizeProblemDisplay(url); + } + + void FreOverlay::_FinalizeProblemDisplay(const std::wstring& url) + { ErrorHelpRun().Text(RS_(L"FreOverlay_ErrorHelpLink")); ErrorHelpLink().NavigateUri(Uri{ winrt::hstring{ url } }); ErrorPanel().Visibility(Visibility::Visible); @@ -898,6 +1200,15 @@ namespace winrt::TerminalApp::implementation IAsyncAction FreOverlay::_SaveAndInstallAsync() { auto weak = get_weak(); + // Capture the dispatcher while we're definitely on the UI thread. + // After any subsequent `co_await` that resumes on a background + // thread (e.g. _WingetInstallAsync, _InstallHooksAsync), calling + // `Dispatcher()` directly would implicitly dereference `this` — + // which is UB if the FRE overlay was destroyed mid-await (user + // closed the tab / window / quit the app during a long winget + // install). The captured value is a ref-counted CoreDispatcher, + // independent of `this`'s lifetime. + const auto dispatcher = Dispatcher(); // 1. Read selections on the UI thread winrt::hstring agentId; @@ -1004,37 +1315,51 @@ namespace winrt::TerminalApp::implementation if (needsCopilot) { _agentPaneLog("[FRE] Installing GitHub.Copilot via winget"); - bool ok = co_await _WingetInstallAsync(L"GitHub.Copilot"); + const auto kindInt = co_await _WingetInstallAsync(L"GitHub.Copilot"); // Helper internally does co_await winrt::resume_background(), // so the continuation may resume on a thread-pool thread. // Hop back to the UI thread before any XAML access (the - // _ShowProblem call below touches ErrorText / ErrorPanel / - // toggles); without this, RPC_E_WRONG_THREAD is thrown and + // _ShowWingetProblem call below touches ErrorText / ErrorPanel + // / toggles); without this, RPC_E_WRONG_THREAD is thrown and // silently swallowed by IAsyncAction, leaving the // SavingOverlay stuck. - co_await winrt::resume_foreground(Dispatcher()); + co_await winrt::resume_foreground(dispatcher); auto self = weak.get(); if (!self) co_return; - _agentPaneLog("[FRE] Copilot install: " + std::string(ok ? "ok" : "FAILED")); - if (!ok) + const auto kind = static_cast(kindInt); + _agentPaneLog("[FRE] Copilot install: " + + std::string(kind == FreWingetFailureKind::Success ? "ok" : "FAILED")); + if (kind != FreWingetFailureKind::Success) { - _ShowProblem(FreProblemKind::CopilotInstall); + // _lastWingetHr / _lastWingetInstallerErrorCode were + // populated by _WingetInstallAsync on this same instance; + // safe to read here because the Copilot install awaited + // above is the only writer in this sequential chain. + _ShowWingetProblem(FreWingetPackage::Copilot, + kind, + _lastWingetHr, + _lastWingetInstallerErrorCode); co_return; } } if (needsNode) { _agentPaneLog("[FRE] Installing Node.js via winget"); - bool ok = co_await _WingetInstallAsync(L"OpenJS.NodeJS.LTS"); + const auto kindInt = co_await _WingetInstallAsync(L"OpenJS.NodeJS.LTS"); // See note above for the Copilot install — same threading // concern applies here. - co_await winrt::resume_foreground(Dispatcher()); + co_await winrt::resume_foreground(dispatcher); auto self = weak.get(); if (!self) co_return; - _agentPaneLog("[FRE] Node.js install: " + std::string(ok ? "ok" : "FAILED")); - if (!ok) + const auto kind = static_cast(kindInt); + _agentPaneLog("[FRE] Node.js install: " + + std::string(kind == FreWingetFailureKind::Success ? "ok" : "FAILED")); + if (kind != FreWingetFailureKind::Success) { - _ShowProblem(FreProblemKind::NodeInstall); + _ShowWingetProblem(FreWingetPackage::Node, + kind, + _lastWingetHr, + _lastWingetInstallerErrorCode); co_return; } } @@ -1096,7 +1421,7 @@ namespace winrt::TerminalApp::implementation // throws RPC_E_WRONG_THREAD, which IAsyncAction swallows — // the SavingOverlay would then be stuck with no error // surfaced. - co_await winrt::resume_foreground(Dispatcher()); + co_await winrt::resume_foreground(dispatcher); self = weak.get(); if (!self) co_return; @@ -1189,7 +1514,7 @@ namespace winrt::TerminalApp::implementation { _agentPaneLog("[FRE] Showing problem: " + std::string(shellIntegFailed ? "ShellIntegration" : "Hooks")); - co_await winrt::resume_foreground(Dispatcher()); + co_await winrt::resume_foreground(dispatcher); auto self = weak.get(); if (!self) co_return; @@ -1200,7 +1525,7 @@ namespace winrt::TerminalApp::implementation } // 6. Resume UI thread before touching controls / raising events - co_await winrt::resume_foreground(Dispatcher()); + co_await winrt::resume_foreground(dispatcher); { auto self = weak.get(); if (!self) co_return; diff --git a/src/cascadia/TerminalApp/FreOverlay.h b/src/cascadia/TerminalApp/FreOverlay.h index 6d06ab847..b3cbf9437 100644 --- a/src/cascadia/TerminalApp/FreOverlay.h +++ b/src/cascadia/TerminalApp/FreOverlay.h @@ -57,14 +57,45 @@ namespace winrt::TerminalApp::implementation // Things that can block FRE completion, in priority order (lower value // = higher priority). Only the highest-priority problem is surfaced in // the bottom-left error area at a time (see _ShowProblem). + // + // WinGet install failures are not in this enum because they carry + // richer structured state (package + failure kind + HRESULT + installer + // exit code); those go through _ShowWingetProblem instead, which uses + // FreWingetPackage + FreWingetFailureKind below. enum class FreProblemKind { WingetMissing = 0, // hard prerequisite — winget itself unavailable - CopilotInstall = 1, // hard prerequisite — winget GitHub.Copilot - NodeInstall = 2, // hard prerequisite — winget OpenJS.NodeJS.LTS - ShellIntegrationExecutionPolicy = 3, // optional feature — error detection blocked by PowerShell execution policy - ShellIntegration = 4, // optional feature — error detection (generic install failure) - Hooks = 5, // optional feature — session management + ShellIntegrationExecutionPolicy = 1, // optional feature — error detection blocked by PowerShell execution policy + ShellIntegration = 2, // optional feature — error detection (generic install failure) + Hooks = 3, // optional feature — session management + }; + + // Which winget-installed prerequisite a failure refers to. Used by + // _ShowWingetProblem to pick the right package display name and + // manual-fix URL anchor. + enum class FreWingetPackage + { + Copilot = 0, // GitHub.Copilot + Node = 1, // OpenJS.NodeJS.LTS + }; + + // Categorization of why a winget install failed, derived from the COM + // API's structured status + HRESULT in _WingetInstallAsync. Each kind + // maps to a localized user-facing message that tells the user what + // happened and what to do next (retry, contact IT, install manually). + // The Success sentinel lets _WingetInstallAsync encode success/failure + // in a single IAsyncOperation return value (WinRT projection + // can't carry a richer struct without an IDL type). + enum class FreWingetFailureKind : int32_t + { + Success = -1, // install completed OK + Network = 0, // connect / download failed with a network-like HRESULT + BlockedByPolicy = 1, // winget GP / org policy blocked the install + PackageNotFound = 2, // catalog has no manifest with this ID + NoCompatibleInstaller = 3, // manifest exists but no installer matches this OS/arch + InstallerFailed = 4, // installer ran but reported an error (e.g. MSI 1603) + Timeout = 5, // we hit our own 20-min hard timeout + Generic = 6, // everything else (catalog corruption, internal error, unknown HRESULT, …) }; // Show a single problem: set the error message + manual-fix link, then @@ -72,6 +103,22 @@ namespace winrt::TerminalApp::implementation // any) and re-enable the Save button. Does not raise Completed. void _ShowProblem(FreProblemKind kind); + // Show a winget install failure with package-aware, failure-kind-aware + // text. Picks the localized template by `kind`, substitutes the + // package display name and (for InstallerFailed / Generic) a + // pre-formatted error code string. Re-enables Save like _ShowProblem. + void _ShowWingetProblem(FreWingetPackage package, + FreWingetFailureKind kind, + int32_t hr, + uint32_t installerErrorCode); + + // Shared tail end of _ShowProblem / _ShowWingetProblem after the + // caller has set ErrorText and computed the help URL: applies the + // URL to the help link, makes the panel visible, refreshes the + // agent dropdown, fires the Narrator notification, re-enables + // editing, and parks focus on the help link. + void _FinalizeProblemDisplay(const std::wstring& url); + // Apply the detection→suggestion master-detail dependency: detection // off turns the suggestion toggle off and disables it; detection on // re-enables it (preserving the stored value). @@ -102,9 +149,45 @@ namespace winrt::TerminalApp::implementation static void _MaybeStartPrewarm(bool copilotMissing, bool nodeMissing); static winrt::Windows::Foundation::IAsyncAction _RunPrewarmAsync(); - // Run a winget install synchronously on a background thread. - // Returns true on success. - static winrt::Windows::Foundation::IAsyncOperation _WingetInstallAsync(winrt::hstring packageId); + // Run a winget install asynchronously on a background thread. + // Returns FreWingetFailureKind cast to int32_t — Success (-1) on + // success, or one of the failure kinds otherwise. On failure, the + // associated HRESULT and installer exit code (if any) are stored in + // the _lastWinget* instance fields below for the caller to read. + // + // Per-instance state, not static: each FreOverlay window has its + // own _lastWinget* slot, so two FRE windows installing concurrently + // (multi-window scenario) can't clobber each other's diagnostics. + // Within one instance, the caller (_SaveAndInstallAsync) awaits + // Copilot before kicking off Node, so no intra-instance race either. + winrt::Windows::Foundation::IAsyncOperation _WingetInstallAsync(winrt::hstring packageId); + + // Diagnostic state from the last _WingetInstallAsync call — read by + // the caller right after `co_await` to pass into _ShowWingetProblem. + // Both fields are reset to 0 by _WingetInstallAsync on each entry. + int32_t _lastWingetHr{ 0 }; + uint32_t _lastWingetInstallerErrorCode{ 0 }; + + // Decide whether an HRESULT looks like a network-class failure + // (WinINet / WinHTTP / Winsock). Conservative whitelist of specific + // codes rather than facility-range scans, to avoid misclassifying + // HTTP-status HRESULTs (HTTP 404 is 0x80190194 — not a "check your + // VPN" situation) or RPC failures as network issues. + static bool _IsNetworkLikeHResult(int32_t hr) noexcept; + + // Classify a raw HRESULT (from a winget COM exception or from + // InstallResult.ExtendedErrorCode) into the most-specific + // FreWingetFailureKind we can infer. Recognizes the winget-CLI's + // APPINSTALLER_CLI_ERROR_* family for policy blocks, missing + // packages, no-applicable-installer, and falls back to + // _IsNetworkLikeHResult, then Generic. + // + // Without this layer, winget COM exceptions like + // APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY (0x8A15003A — thrown + // when group policy disables winget) would map to a generic + // "(error code 0x8A15003A)" message instead of the actionable + // "blocked by policy — contact your IT admin" message. + static FreWingetFailureKind _ClassifyWingetHResult(int32_t hr) noexcept; // Run wta.exe hooks install on a background thread. // Returns true on success. diff --git a/src/cascadia/TerminalApp/Resources/af-ZA/Resources.resw b/src/cascadia/TerminalApp/Resources/af-ZA/Resources.resw index af4209da8..46ab5ca15 100644 --- a/src/cascadia/TerminalApp/Resources/af-ZA/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/af-ZA/Resources.resw @@ -1,4 +1,4 @@ - + text/microsoft-resx 2.0 @@ -107,17 +107,49 @@ Word opgestel... - - ⚠ Kon nie GitHub Copilot installeer nie. Kontroleer jou netwerkverbinding en probeer weer. - {Locked="GitHub Copilot"} + + ⚠ Installering van {0} is deur 'n Windows-pakketbestuurderbeleid geblokkeer. As jy op 'n bestuurde toestel is, kontak jou IT-administrateur. + FRE setup error. {0} is the package display name. + + + ⚠ Kon nie {0} installeer nie (foutkode {1}). Sien die logboek vir besonderhede, of installeer {0} handmatig. + FRE setup error fallback. {0} is the package display name (appears twice). {1} is a pre-formatted HRESULT string. + + + ⚠ Kon nie {0} installeer nie. Sien die logboek vir besonderhede, of installeer {0} handmatig. + FRE setup error fallback used when no actionable error code is available. {0} is the package display name (appears twice). + + + ⚠ Die {0}-installeerder het ’n fout gerapporteer (kode {1}). Gaan die logboek na vir besonderhede, of installeer {0} handmatig. + FRE setup error. {0} is the package display name (appears twice). {1} is decimal error code. + + + ⚠ Kon nie die Windows-pakketbestuurder bereik terwyl {0} geïnstalleer is nie. Kontroleer jou internetverbinding (VPN, instaanbediener of brandmuur blokkeer dit dalk) en probeer weer. + {Locked="VPN"} FRE setup error. {0} is the package display name. + + + ⚠ Geen versoenbare installeerder vir {0} is op hierdie stelsel beskikbaar nie (OS-weergawe of argitektuur word dalk nie ondersteun nie). Installeer {0} handmatig. + FRE setup error. {0} is the package display name (appears twice). + + + ⚠ {0} is nie in die Windows-pakketbestuurderkatalogus gevind nie. Probeer winget-bronne verfris, of installeer {0} handmatig. + {Locked="winget"} FRE setup error. {0} is the package display name (appears twice). + + + ⚠ Installering van {0} het langer as 20 minute geneem. Intelligent Terminal het opgehou wag, maar die installeerder loop dalk nog in die agtergrond. Gaan Task Manager na, of probeer later weer. + {Locked="Intelligent Terminal","Task Manager"} FRE setup error. {0} is the package display name. ⚠ Windows-pakketbestuurder (winget) is nie geïnstalleer nie of is nie beskikbaar nie. Installeer dit eers en probeer dan weer. - {Locked="winget","PATH"} Error shown in the FRE setup overlay when a prerequisite install was attempted but the winget command is not on PATH. + {Locked="winget"} Error shown in the FRE setup overlay when a prerequisite install was attempted but winget itself is not available on this system. + + + GitHub Copilot + {Locked="GitHub Copilot"} Product display name substituted into the FreOverlay_InstallError_* templates when reporting a winget install failure for the Copilot CLI prerequisite. - - ⚠ Kon nie Node.js installeer nie. Kontroleer jou netwerkverbinding en probeer weer. - {Locked="Node.js"} + + Node.js (LTS) + {Locked="Node.js","LTS"} Product display name substituted into the FreOverlay_InstallError_* templates when reporting a winget install failure for the Node.js prerequisite. Aan diff --git a/src/cascadia/TerminalApp/Resources/am-ET/Resources.resw b/src/cascadia/TerminalApp/Resources/am-ET/Resources.resw index b038b13d5..ff7f2fe7b 100644 --- a/src/cascadia/TerminalApp/Resources/am-ET/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/am-ET/Resources.resw @@ -1,4 +1,4 @@ - + text/microsoft-resx 2.0 @@ -107,17 +107,49 @@ በማያቄር ላይ... - - ⚠ GitHub Copilot መጠቀም አልተሳካም። የመረብዎን ይፈትሩ እና እንደገና ይሞክሩ። - {Locked="GitHub Copilot"} + + ⚠ የ{0} ጭነት በWindows Package Manager ፖሊሲ ታግዷል። በሚተዳደር መሣሪያ ላይ ከሆኑ፣ የIT አስተዳዳሪዎን ያግኙ። + FRE setup error. {0} is the package display name. + + + ⚠ {0}ን መጫን አልተቻለም (የስህተት ኮድ {1})። ለዝርዝሮች ሎጉን ይመልከቱ፣ ወይም {0}ን በእጅ ይጫኑ። + FRE setup error fallback. {0} is the package display name (appears twice). {1} is a pre-formatted HRESULT string. + + + ⚠ {0}ን መጫን አልተቻለም። ለዝርዝሮች ሎጉን ይመልከቱ፣ ወይም {0}ን በእጅ ይጫኑ። + FRE setup error fallback used when no actionable error code is available. {0} is the package display name (appears twice). + + + ⚠ የ{0} ጫኚ ስህተት አሳውቋል (ኮድ {1})። ለዝርዝሮች ሎጉን ይመልከቱ፣ ወይም {0}ን በእጅ ይጫኑ። + FRE setup error. {0} is the package display name (appears twice). {1} is decimal error code. + + + ⚠ {0}ን በመጫን ላይ ሳለ Windows Package Managerን መድረስ አልተቻለም። የኢንተርኔት ግንኙነትዎን ይፈትሹ (VPN፣ ፕሮክሲ ወይም ፋየርዎል ሊከለክለው ይችላል) እና እንደገና ይሞክሩ። + {Locked="VPN"} FRE setup error. {0} is the package display name. + + + ⚠ በዚህ ስርዓት ላይ ለ{0} ተኳኋኝ ጫኚ አይገኝም (የOS ስሪት ወይም አርክቴክቸር ላይደገፍ ይችላል)። {0}ን በእጅ ይጫኑ። + FRE setup error. {0} is the package display name (appears twice). + + + ⚠ {0} በWindows Package Manager ካታሎግ ውስጥ አልተገኘም። የwinget ምንጮችን ለማደስ ይሞክሩ፣ ወይም {0}ን በእጅ ይጫኑ። + {Locked="winget"} FRE setup error. {0} is the package display name (appears twice). + + + ⚠ {0}ን መጫን ከ20 ደቂቃ በላይ ወሰደ። Intelligent Terminal መጠበቁን አቁሟል፣ ግን ጫኚው አሁንም ከበስተጀርባ እየሰራ ሊሆን ይችላል። Task Managerን ይፈትሹ፣ ወይም በኋላ እንደገና ይሞክሩ። + {Locked="Intelligent Terminal","Task Manager"} FRE setup error. {0} is the package display name. ⚠ Windows Package Manager (winget) አልተጫነም ወይም አይገኝም። መጀመሪያ ይጫኑት፣ ከዚያ እንደገና ይሞክሩ። - {Locked="winget","PATH"} Error shown in the FRE setup overlay when a prerequisite install was attempted but the winget command is not on PATH. + {Locked="winget"} Error shown in the FRE setup overlay when a prerequisite install was attempted but winget itself is not available on this system. + + + GitHub Copilot + {Locked="GitHub Copilot"} Product display name substituted into the FreOverlay_InstallError_* templates when reporting a winget install failure for the Copilot CLI prerequisite. - - ⚠ Node.js መጠቀም አልተሳካም። የመረብዎን ይፈትሩ እና እንደገና ይሞክሩ። - {Locked="Node.js"} + + Node.js (LTS) + {Locked="Node.js","LTS"} Product display name substituted into the FreOverlay_InstallError_* templates when reporting a winget install failure for the Node.js prerequisite. እንቅ diff --git a/src/cascadia/TerminalApp/Resources/ar-SA/Resources.resw b/src/cascadia/TerminalApp/Resources/ar-SA/Resources.resw index ee5c7b3de..7d9635d20 100644 --- a/src/cascadia/TerminalApp/Resources/ar-SA/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/ar-SA/Resources.resw @@ -1,4 +1,4 @@ - +