Skip to content

Commit a81ad55

Browse files
committed
remove exit code testing from regular CI runs
1 parent 4b50564 commit a81ad55

2 files changed

Lines changed: 66 additions & 138 deletions

File tree

.github/workflows/windows-ci.yml

Lines changed: 7 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ on:
1111

1212
permissions:
1313
contents: read
14-
id-token: write # Azure OIDC: the launch/exit smoke test signs the exe first,
15-
# because an unsigned uiAccess binary refuses to launch
1614

1715
jobs:
1816
build:
@@ -123,125 +121,10 @@ jobs:
123121
throw "Release exe imports non-system runtime/dependency DLLs: $($blocked -join '; ')"
124122
}
125123
126-
# ---- launch/exit smoke test -------------------------------------------------
127-
# Replays the winget validation harness's launch-and-quit sequence and fails on any
128-
# nonzero exit code — it treats one as a failed run (winget-pkgs PR #408205: Clipp
129-
# 1.4.0.150 came back "returned exit code: -1073741189" = 0xC000027B,
130-
# STATUS_STOWED_EXCEPTION; ../WM_NIGHT died the same way in #393605).
131-
#
132-
# This MUST test the shipped bits: build_windows.ps1 stamps uiAccess=true into every
133-
# Release exe, and an unsigned uiAccess binary REFUSES TO LAUNCH ("A referral was
134-
# returned from the server"). Stripping the stamp to make it launchable would test a
135-
# binary we never ship, so CI signs first — same Trusted Signing path as the release
136-
# workflow — and launches the real thing.
137-
#
138-
# Signing needs the AZURE_* secrets, which are absent on fork PRs. Those legs skip
139-
# the smoke test rather than fail: nothing to verify without a signature.
140-
- name: Can we sign? (smoke test needs a signed binary)
141-
id: cansign
142-
env:
143-
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
144-
run: |
145-
$ok = [bool]$env:AZURE_CLIENT_ID
146-
"ok=$($ok.ToString().ToLower())" >> $env:GITHUB_OUTPUT
147-
if (-not $ok) {
148-
Write-Host "::notice::No signing credentials (fork PR?) — skipping the launch/exit smoke test."
149-
}
150-
151-
- name: Azure login (Trusted Signing)
152-
if: steps.cansign.outputs.ok == 'true'
153-
uses: azure/login@v2
154-
with:
155-
client-id: ${{ secrets.AZURE_CLIENT_ID }}
156-
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
157-
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
158-
159-
- name: Sign Windows artifacts
160-
if: steps.cansign.outputs.ok == 'true'
161-
uses: azure/trusted-signing-action@v0
162-
with:
163-
endpoint: ${{ vars.ARTIFACT_SIGNING_ENDPOINT }}
164-
trusted-signing-account-name: ${{ vars.ARTIFACT_SIGNING_ACCOUNT }}
165-
certificate-profile-name: ${{ vars.ARTIFACT_SIGNING_CERTIFICATE_PROFILE }}
166-
files-folder: ${{ github.workspace }}\build\windows-release
167-
files-folder-filter: exe,com
168-
file-digest: SHA256
169-
timestamp-rfc3161: http://timestamp.acs.microsoft.com
170-
timestamp-digest: SHA256
171-
# The action's dependency cache keys are arch-agnostic, so the amd64 and
172-
# arm64 matrix legs poison each other's TrustedSigning module cache
173-
# (Azure/trusted-signing-action#146). Fresh install costs ~1 min per leg.
174-
cache-dependencies: false
175-
176-
- name: Verify signature and uiAccess stamp
177-
if: steps.cansign.outputs.ok == 'true'
178-
run: |
179-
$exe = ".\build\windows-release\clipp.exe"
180-
$sig = Get-AuthenticodeSignature $exe
181-
if ($sig.Status -ne 'Valid') {
182-
throw "Signature on $exe is not Valid: $($sig.Status) — $($sig.StatusMessage)"
183-
}
184-
Write-Host "[+] signed by: $($sig.SignerCertificate.Subject)"
185-
# Assert we are about to launch a uiAccess binary — the whole point is that the
186-
# shipped configuration is what gets exercised.
187-
$mt = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\*\mt.exe" -ErrorAction SilentlyContinue |
188-
Sort-Object FullName -Descending | Select-Object -First 1
189-
if ($mt) {
190-
$manifest = Join-Path $env:RUNNER_TEMP 'shipped.manifest'
191-
& $mt.FullName -nologo -inputresource:"$exe;#1" -out:"$manifest" | Out-Null
192-
$xml = Get-Content -Raw $manifest
193-
if ($xml -match 'uiAccess="true"') { Write-Host '[+] uiAccess=true present (shipped configuration).' }
194-
else { Write-Host '::warning::uiAccess=true NOT present in the embedded manifest.' }
195-
}
196-
197-
- name: Enable WER local crash dumps
198-
if: steps.cansign.outputs.ok == 'true'
199-
run: |
200-
# A stowed exception is a WinRT/COM fail-fast: it bypasses unhandled-exception
201-
# filters, so Clipp's own crash handler never sees it and writes nothing. WER
202-
# LocalDumps catches it at a lower level — the only way to get a dump at all.
203-
$dumps = Join-Path $env:RUNNER_TEMP 'werdumps'
204-
New-Item -ItemType Directory -Force $dumps | Out-Null
205-
$key = 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\clipp.exe'
206-
New-Item -Path $key -Force | Out-Null
207-
Set-ItemProperty -Path $key -Name DumpFolder -Value $dumps -Type ExpandString
208-
Set-ItemProperty -Path $key -Name DumpType -Value 2 -Type DWord # full dump
209-
Set-ItemProperty -Path $key -Name DumpCount -Value 5 -Type DWord
210-
"WER_DUMP_DIR=$dumps" >> $env:GITHUB_ENV
211-
212-
# -RequireWindow: this runner has a virgin HKCU, so no group key exists and Clipp
213-
# must open its first-run XAML-Islands window — the exact path a validation VM takes,
214-
# and the one a developer box with a configured key never exercises. If the window
215-
# never appears, the island failed to come up: that is a failure, not a shrug.
216-
# Graceful path: tray-menu Exit runs main()'s full teardown and must return 0.
217-
- name: Smoke test — graceful exit (signed, uiAccess-stamped exe)
218-
if: steps.cansign.outputs.ok == 'true'
219-
run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow -CloseMode TrayExit
220-
221-
# Harness path: close the main window like Process.CloseMainWindow, then verify the
222-
# app stays resident instead of self-terminating. A 0xC000027B here IS the winget
223-
# validation failure reproduced — this is the leg that can actually catch it.
224-
- name: Smoke test — winget harness sequence
225-
if: steps.cansign.outputs.ok == 'true'
226-
run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow -CloseMode Harness
227-
228-
- name: Collect crash diagnostics
229-
if: failure() && steps.cansign.outputs.ok == 'true'
230-
run: |
231-
Write-Host '--- Application Error / WER events (last 30 min) ---'
232-
Get-WinEvent -FilterHashtable @{
233-
LogName = 'Application'; StartTime = (Get-Date).AddMinutes(-30)
234-
} -ErrorAction SilentlyContinue |
235-
Where-Object { $_.ProviderName -in 'Application Error', 'Windows Error Reporting' -and
236-
$_.Message -match 'clipp' } |
237-
ForEach-Object { $_.TimeCreated; $_.Message; '---' }
238-
Write-Host "--- Dumps in $env:WER_DUMP_DIR ---"
239-
Get-ChildItem $env:WER_DUMP_DIR -ErrorAction SilentlyContinue
240-
241-
- name: Upload crash dumps
242-
if: failure() && steps.cansign.outputs.ok == 'true'
243-
uses: actions/upload-artifact@v4
244-
with:
245-
name: werdumps-${{ matrix.arch }}
246-
path: ${{ env.WER_DUMP_DIR }}
247-
if-no-files-found: ignore
124+
# NOTE: the launch/exit smoke test (scripts/exitcode_smoke.ps1) deliberately does NOT
125+
# run here. It has to drive a SIGNED binary — an unsigned uiAccess exe refuses to
126+
# launch — and signing on every push/PR would burn Trusted Signing operations for a
127+
# check that only matters at release time. It lives in _release.yml instead, where it
128+
# runs against the INSTALLED, signed MSIX: the only configuration in which uiAccess is
129+
# actually granted (WindowsApps is a secure path), which is what winget's validation
130+
# exercises.

src/platform/win32/PopupWindow.cpp

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -496,8 +496,15 @@ class PreviewWindow {
496496
HWND Hwnd() const { return hwnd_; }
497497

498498
void Destroy() {
499-
if (xamlSource_) {
500-
xamlSource_.Close();
499+
// Island teardown is not allowed to throw: see the note on
500+
// PopupWindow::Destroy — an hresult_error escaping here rides out
501+
// through a COM boundary and fail-fasts the process.
502+
try {
503+
if (xamlSource_) {
504+
xamlSource_.Close();
505+
xamlSource_ = nullptr;
506+
}
507+
} catch (const winrt::hresult_error&) {
501508
xamlSource_ = nullptr;
502509
}
503510
if (hwnd_ != nullptr) {
@@ -742,18 +749,35 @@ class PopupWindow {
742749
return translated == TRUE;
743750
}
744751

752+
// Teardown must be exception-PROOF, not merely exception-safe. This runs from
753+
// the tray window's WM_DESTROY — i.e. inside a window procedure dispatched by
754+
// the message loop — and a C++/WinRT hresult_error that escapes into the COM
755+
// boundary above us gets converted by RoFailFastWithErrorContext into a
756+
// STOWED EXCEPTION (0xC000027B), which no filter and no crash handler can
757+
// intercept. That is the exit code winget's install verification reported for
758+
// this build (and the same class ../WM_NIGHT hardened its own shutdown
759+
// against). Nothing here is worth dying for: every handle is about to be
760+
// abandoned by process exit anyway.
745761
void Destroy() {
746-
EndActivityNotifications();
747-
toastWindow_.Destroy();
748-
previewWindow_.Destroy();
749-
if (xamlSource_) {
750-
xamlSource_.Close();
762+
try {
763+
EndActivityNotifications();
764+
} catch (const winrt::hresult_error&) {
765+
}
766+
toastWindow_.Destroy(); // GDI only; cannot throw WinRT
767+
previewWindow_.Destroy(); // guards its own island
768+
try {
769+
if (xamlSource_) {
770+
xamlSource_.Close();
771+
xamlSource_ = nullptr;
772+
}
773+
} catch (const winrt::hresult_error&) {
751774
xamlSource_ = nullptr;
752775
}
753776
if (hwnd_ != nullptr) {
754777
DestroyWindow(hwnd_);
755778
hwnd_ = nullptr;
756779
}
780+
dispatcher_ = nullptr;
757781
}
758782

759783
private:
@@ -2667,22 +2691,43 @@ std::unique_ptr<PopupWindow> g_popupWindow;
26672691

26682692
namespace clipp {
26692693

2694+
// The three public entry points all run inside the tray window's message
2695+
// pump (hotkey, menu command, WM_DESTROY). A WinRT exception escaping any of
2696+
// them reaches a COM boundary and fail-fasts the process with a stowed
2697+
// exception (0xC000027B) — invisible to crash handlers. The popup is a
2698+
// convenience surface: losing it is always better than taking the app down,
2699+
// so every boundary swallows-and-logs instead.
26702700
void TogglePopupWindow() {
2671-
if (!g_popupWindow) {
2672-
g_popupWindow = std::make_unique<PopupWindow>();
2701+
try {
2702+
if (!g_popupWindow) {
2703+
g_popupWindow = std::make_unique<PopupWindow>();
2704+
}
2705+
g_popupWindow->Toggle();
2706+
} catch (const winrt::hresult_error& e) {
2707+
g_logger.log(__FUNCTION__, Logger::Level::Warning,
2708+
L"Popup toggle failed (0x%08X): %ls", static_cast<uint32_t>(e.code()),
2709+
e.message().c_str());
2710+
} catch (...) {
2711+
g_logger.log(__FUNCTION__, Logger::Level::Warning, L"Popup toggle failed.");
26732712
}
2674-
g_popupWindow->Toggle();
26752713
}
26762714

26772715
bool PopupPreTranslateMessage(MSG* msg) {
2678-
return g_popupWindow ? g_popupWindow->PreTranslateMessage(msg) : false;
2716+
try {
2717+
return g_popupWindow ? g_popupWindow->PreTranslateMessage(msg) : false;
2718+
} catch (...) {
2719+
return false; // an unrouted key is nothing; a dead process is something
2720+
}
26792721
}
26802722

26812723
void DestroyPopupWindow() {
2682-
if (g_popupWindow) {
2683-
g_popupWindow->Destroy();
2684-
g_popupWindow.reset();
2724+
try {
2725+
if (g_popupWindow) {
2726+
g_popupWindow->Destroy();
2727+
}
2728+
} catch (...) {
26852729
}
2730+
g_popupWindow.reset(); // released even if teardown misbehaved
26862731
}
26872732

26882733
} // namespace clipp

0 commit comments

Comments
 (0)