Skip to content

MSC improvements: per-row stages, removal UX, failure surfacing (WIP)#57

Merged
rodchristiansen merged 15 commits into
mainfrom
fix/msc-ui-thread-marshal
Jun 20, 2026
Merged

MSC improvements: per-row stages, removal UX, failure surfacing (WIP)#57
rodchristiansen merged 15 commits into
mainfrom
fix/msc-ui-thread-marshal

Conversation

@rodchristiansen

Copy link
Copy Markdown
Contributor

Managed Software Center improvements (WIP — kept open for iteration)

Ongoing work on the MSC WinUI 3 GUI and the managedsoftwareupdate engine. This PR is a draft and will keep getting commits as we iterate; do not merge yet.

Engine

  • Surface install/uninstall failures with a parsed exit-code reason (common MSI codes glossed), streamed on the itemStatus pipe and written to problem_items in InstallInfo.yaml (durable, user-reportable).
  • Emit an early pending stage per targeted --item so GUI rows show progress from t=0 instead of flashing at the end.
  • Treat msiexec 1605/1614 (product already absent) as idempotent removal success.

Managed Software Center

  • Per-row live stages: pending → downloading → downloaded → installing → installed/removed/failed, streamed over the status pipe.
  • Removal UX: My Items shows Remove for installed items, Cancel for pending; progress banner is verb-aware (Removing vs Installing).
  • Inline problems: each problem's error renders on its own row; only orphan problems (no matching row) stay in the standalone Problem Items section.
  • Self-serve responsiveness: cancelling a pending request clears it from the Updates tab immediately (RequestsChanged event); Updates surfaces self-serve install-requested items.
  • Log viewer: hides DEBUG lines by default with a Show debug toggle; adds a Copy button.
  • Adds tests/gui-harness to drive the install/remove path headlessly.

Status

Built signed and deployed locally (MSU+MSC 2026.06.14). Iterating — next up includes install-side silent-flag inference for stub installers and a repo-wide uninstall-coverage audit (tracked separately in Azure Boards).

…ation start

InstallInfoService (FileSystemWatcher), TriggerService (flag-file poll loop),
ProgressServer/ProgressPipeClient (socket read loops) all raised events on
threadpool threads. Viewmodel handlers set XAML-bound ObservableProperties,
so PropertyChanged hit active bindings cross-thread: COMException 0x8001010E
(RPC_E_WRONG_THREAD) inside async void handlers, killing the process. Seen
in the field as MSC crashing the moment a self-serve install finished and
InstallInfo.yaml changed.

New UiDispatcher captures the App DispatcherQueue at startup; sources raise
through Post(), which runs inline when no queue is set (unit tests).

ShellViewModel now navigates to Updates whenever an operation starts, so a
self-serve click surfaces the pending item immediately. RequestNavigate
resets NavigateToPage to null first; the generated setter suppresses
PropertyChanged for equal values, which silently dropped repeat navigations.

Program.cs logs AppDomain.UnhandledException to msc_crash.log — background
crashes previously bypassed App.UnhandledException and left no trace.
… at InstallInfo write time

Self-serve promotion flipped a manifest optional item's action to install,
so WriteInstallInfo never emitted its optional_installs record and the item
vanished from the software list (and from Featured) the moment a user
clicked Install. PromotedFromOptional now marks promoted/flipped items and
WriteInstallInfo always writes their optional record, with status tracking
the pending action (will-be-installed / will-be-removed) and reverting to
installed / not-installed when the action completes.

The toInstall/toUpdate/toUninstall lists are computed before the install
phase, so the post-session InstallInfo write described items installed
minutes earlier as still pending (installed: true + status:
will-be-installed for VLC). All branches now re-run CheckStatus at write
time and drop pending records whose action already completed.
IconImage is a plain property without change notification; assigning it
after the ObservableCollections were bound left already-rendered rows
iconless. Load icons into the source lists first, then assign.
Log viewer extracted to a shared LogViewerControl (tail, filter,
autoscroll). View Log now opens a flyout anchored to the button by
default; its pop-out button (and Ctrl+L) opens the standalone LogWindow.

LogWindow centered itself with hardcoded 2250x1500 math against a 900x600
window, so on shorter displays Y went negative and it opened above the
visible area. Both windows now center using actual window size offset by
the work area origin and clamped to non-negative coordinates; MainWindow
also caps its initial size to the work area.
…tive

Each MSC click >10s apart spawned its own managedsoftwareupdate process;
the second lost the instance lock and exited code 1, so queued self-serve
installs silently never ran. CimianWatcher now claims a run slot before
consuming a flag file: while a triggered process is alive, new/updated
flag files stay on disk (MSC keeps merging additional --item requests
into them) and are consumed on the first poll after the run exits.

MSC TriggerService no longer counts wait time against its 30s timeout
while a managedsoftwareupdate process is running — the unconsumed flag is
the queued request, not a dead service (it used to delete the queue and
throw).

engine: pending records for promoted self-serve items survive targeted
(--item) writes — a queued item outside the run's filter is still pending
and keeps its managed_installs record, so the Updates list no longer goes
blank after a partial run.

msc: standing self-serve subscriptions no longer render as eternal
'Install pending' on installed items (Software, item detail, My Items);
removal requests only show pending while the item is installed. My Items
also loads icons before binding.
It was registered transient: MainWindow held one instance, but every page
navigation constructed a fresh one via App.GetService — created AFTER the
operation-started event fired, so a newly navigated Updates page saw
IsInstalling=false and showed stale content while an install ran silently.
Each throwaway instance also left its service event subscriptions behind.
Shell state (progress overlay, badges, navigation requests) is app-wide;
register it once.
…cel, categories grid

Engine streams itemStatus messages over the status connection (new 'item'
field): pending -> downloading -> downloaded -> installing -> installed,
plus removing/removed/failed. The status TCP stream is now duplex — the
reporter runs a command read loop and the GUI's stop command cancels the
run gracefully before the next item (previously SendCommandAsync was a
silent no-op).

Updates page: the full-page takeover overlay is replaced by a compact
progress banner; the item list stays visible underneath with a live stage
indicator per row (spinner while downloading/installing/removing, glyph +
color for terminal states). View Log stays accessible during a run and
the banner has a Cancel button that clears a queued flag file and stops a
running session. Items the engine reports that aren't in the list yet get
synthetic rows so a fresh self-serve click shows progress immediately.
UpdatesViewModel is a singleton so navigation keeps mid-run stage state.
InstallableItem implements INotifyPropertyChanged for LiveStage only.

Categories page: rows become fixed-width wrapping grid cards.
…ly per-item pending stage; MSI uninstall idempotency

Streams a failure reason (parsed exit code, common MSI codes glossed) on the itemStatus pipe and writes problem_items into InstallInfo.yaml. Emits an early pending stage per targeted item so GUI rows show progress from t=0. Treats msiexec 1605/1614 (already-absent product) as idempotent removal success.
…ebug filter

Updates tab shows self-serve install-requested items and renders each problem's error inline on its row (orphans only in the standalone section). My Items shows Remove for installed items, Cancel for pending. Progress banner verb-aware (Removing vs Installing). Self-serve manifest raises RequestsChanged so a cancel clears Updates immediately. Log viewer hides DEBUG by default with a Show debug toggle, plus a Copy button. Adds tests/gui-harness for headless drive of the install/remove path.
When an item reaches installed/removed, its row lingers on the Updates page with the green check instead of vanishing the instant InstallInfo is rewritten. Excluded from the pending count and Install Now targets; cleared on explicit re-check or when navigating away from the page.
Engine:
- configurable --status-port (default 19847); MSC uses 19848 so the
  login-window CimianStatus listener can no longer collide with MSC
- emit terminal itemStatus for an already-satisfied --item so the GUI
  confirms instead of sitting empty

MSC:
- single "--item A B C" arg form; repeated --item flags crashed the
  engine's sequence option ("defined multiple times", exit 1) and
  stranded multi-item self-serve batches
- surface self-serve removals live (symmetric to installs) so Remove
  shows under Pending Removals immediately with real name/version/icon
- enrich synthetic live-progress rows from InstallInfo; reclassify a
  removal mis-filed under installs via the generic pending stage
- per-row progress for --item runs vs global banner for broad runs
- TriggerService waits for the engine process to exit and clears the
  running state, so a run that never signals completion can't leave
  the GUI stuck on Stop
- drop the modal install/removal error dialog; failures show inline on
  the row and under Problem Items

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This draft PR iterates on the Managed Software Center (WinUI 3) + managedsoftwareupdate engine contract to provide per-item live lifecycle stages, improved removal UX, and more durable/visible failure reporting (including parsed exit-code summaries), plus a new headless GUI harness for faster end-to-end flow testing.

Changes:

  • Engine: adds per-item streamed stages (itemStatus), stop-command support over the status connection, idempotent removal handling, and durable failure surfacing via problem_items.
  • GUI: renders per-row live stages, refines install/remove/cancel UX (incl. “Remove” in My Items), improves log viewing via a reusable LogViewerControl, and hardens cross-thread event delivery via UiDispatcher.
  • Tests/harness: updates argument-building expectations (single --item flag), adds uninstallability + silent-switch inference tests, and introduces tests/gui-harness for headless contract-driven scenarios.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/Shared/BootstrapArgsBuilderTests.cs Updates tests for single --item flag with multiple values + quoting behavior.
tests/Managedsoftwareupdate/UpdateModelsTests.cs Adds uninstallability tests for exe installers and opt-out behavior.
tests/Managedsoftwareupdate/InstallerServiceTests.cs Adds tests for uninstall silent-switch inference from registry uninstaller exe name.
tests/gui-harness/README.md Documents the new headless GUI/engine contract test harness.
tests/gui-harness/MscHarness.ps1 Implements the headless harness for install/remove/cancel/scenario flows.
shared/core/Services/SelfServiceManifestService.cs Adds RequestsChanged event to support immediate GUI refresh on manifest mutations.
shared/core/Services/BootstrapArgsBuilder.cs Switches to single --item flag, improves extraction for new/legacy forms.
gui/ManagedSoftwareCenter/Views/UpdatesPage.xaml.cs Refactors run-state UI logic; adds log flyout lifecycle wiring; dismisses completed rows on unload.
gui/ManagedSoftwareCenter/Views/UpdatesPage.xaml Adds per-row stage UI, new header controls, and embedded progress banner + log flyout viewer.
gui/ManagedSoftwareCenter/Views/MyItemsPage.xaml.cs Adds Remove click handler to trigger uninstall from My Items.
gui/ManagedSoftwareCenter/Views/MyItemsPage.xaml Adds Remove vs Cancel button logic and uses user-facing status text.
gui/ManagedSoftwareCenter/Views/LogWindow.xaml.cs Simplifies window into a thin host around LogViewerControl; fixes centering math.
gui/ManagedSoftwareCenter/Views/LogWindow.xaml Hosts reusable LogViewerControl instead of inlined log viewer UI.
gui/ManagedSoftwareCenter/Views/CategoriesPage.xaml Switches categories display to wrapping GridView cards.
gui/ManagedSoftwareCenter/ViewModels/UpdatesViewModel.cs Adds per-item stage routing, request-change refresh, self-serve request surfacing, and “linger completed rows” logic.
gui/ManagedSoftwareCenter/ViewModels/SoftwareViewModel.cs Adjusts status derivation for standing requests; fixes targeted removal trigger call.
gui/ManagedSoftwareCenter/ViewModels/ShellViewModel.cs Introduces ShowGlobalBanner, improves cancellation path, and ensures navigation requests fire even when unchanged.
gui/ManagedSoftwareCenter/ViewModels/MyItemsViewModel.cs Adds Remove-from-MyItems command, better status labels, icon-load ordering, and installed-state handling.
gui/ManagedSoftwareCenter/ViewModels/ItemDetailViewModel.cs Refines “pending request” evaluation for standing installs vs actual pending state; fixes removal trigger call.
gui/ManagedSoftwareCenter/Services/UiDispatcher.cs Adds shared UI-thread dispatcher helper for safe cross-thread event delivery.
gui/ManagedSoftwareCenter/Services/TriggerService.cs Adds --status-port injection, targeted multi-item triggering, improved batching, and a process-exit safety net.
gui/ManagedSoftwareCenter/Services/ProgressServer.cs Moves MSC to port 19848, adds itemStatus parsing and duplex stop command support, marshals events to UI thread.
gui/ManagedSoftwareCenter/Services/ProgressPipeClient.cs Marshals pipe events to UI thread via UiDispatcher.
gui/ManagedSoftwareCenter/Services/ITriggerService.cs Extends trigger interface for item-scoped operations and multi-item targeted runs.
gui/ManagedSoftwareCenter/Services/InstallInfoService.cs Marshals file watcher callbacks to UI thread.
gui/ManagedSoftwareCenter/Program.cs Logs background unhandled exceptions for better post-mortem signal.
gui/ManagedSoftwareCenter/Models/ProgressMessage.cs Adds ItemStatus message type for per-row live stages.
gui/ManagedSoftwareCenter/Models/InstallInfo.cs Adds LiveStage and LiveStageDetail with change notification.
gui/ManagedSoftwareCenter/MainWindow.xaml.cs Caps window size to work area and fixes centering on smaller displays.
gui/ManagedSoftwareCenter/Converters/Converters.cs Adds converters for stage rendering and “no stage” visibility.
gui/ManagedSoftwareCenter/Controls/LogViewerControl.xaml.cs Adds reusable log viewer with filtering, debug toggle, copy, pop-out, and tailing support.
gui/ManagedSoftwareCenter/Controls/LogViewerControl.xaml Defines reusable log viewer UI (toolbar + log + status).
gui/ManagedSoftwareCenter/App.xaml.cs Initializes UiDispatcher; makes Shell/Updates viewmodels singletons to preserve live state.
cli/managedsoftwareupdate/Services/UpdateEngine.cs Adds status-port support, itemStatus emission, stop handling, durable failure surfacing, and targeted-run optimizations.
cli/managedsoftwareupdate/Services/StatusReporter.cs Adds configurable port, duplex command reader, and itemStatus messages.
cli/managedsoftwareupdate/Services/ManifestService.cs Marks optional items promoted by self-serve intent.
cli/managedsoftwareupdate/Services/InstallerService.cs Adds uninstall-via-registry fallback, silent-switch inference, and idempotent MSI uninstall handling.
cli/managedsoftwareupdate/Program.cs Adds --status-port option and wires it into engine run.
cli/managedsoftwareupdate/Models/UpdateModels.cs Extends uninstallability rules for exe installers; adds promoted-from-optional metadata.
cli/cimiwatcher/Services/FileWatcherService.cs Serializes flag consumption while an update is running to avoid losing queued requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gui/ManagedSoftwareCenter/Services/ITriggerService.cs
Comment thread cli/managedsoftwareupdate/Services/InstallerService.cs Outdated
Comment thread tests/gui-harness/MscHarness.ps1 Outdated
Comment thread gui/ManagedSoftwareCenter/ViewModels/UpdatesViewModel.cs
Comment thread gui/ManagedSoftwareCenter/Converters/Converters.cs Outdated
…shal

# Conflicts:
#	cli/managedsoftwareupdate/Models/UpdateModels.cs
#	cli/managedsoftwareupdate/Services/ManifestService.cs
- ITriggerService: correct --item doc to single-flag form (--item A B)
- InstallerService: dispose registry base keys (handle leak on uninstall)
- UpdatesViewModel: guard async-void progress handler with try/catch
- Converters: cache ItemStageConverter brushes (avoid per-call alloc)
- MscHarness: emit single --item flag so multi-item runs don't exit 1
PowerShell renders Write-Error to stderr with a prefix (and Windows
PowerShell echoes the command, doubling the marker on the line) plus ANSI
color codes under pwsh. The start-anchored regex never matched a marker
emitted via stderr. Strip ANSI and walk to the last marker occurrence on
the line so stderr-emitted warnings are captured like stdout ones.
@rodchristiansen
rodchristiansen marked this pull request as ready for review June 20, 2026 00:16
@rodchristiansen
rodchristiansen merged commit 9ce1d77 into main Jun 20, 2026
1 check passed
@rodchristiansen
rodchristiansen deleted the fix/msc-ui-thread-marshal branch June 20, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants