From 6c9bee7391bcc71ebd631ee2ba07e6178c3308fb Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sat, 16 May 2026 08:48:06 -0400 Subject: [PATCH] fix(wpf): catch participant-left race in ToggleIsoAsync, toast instead of crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator path: click Enable on a participant -> AsyncRelayCommand fires ToggleIsoAsync -> IsoController.EnableIsoAsync(id) -> tracker lookup -> throws InvalidOperationException 'Participant not currently visible on the network' when the participant has departed between the click and the engine resolving the id. Previously this exception escaped AsyncRelayCommand.Execute via the unawaited Task in ICommand.Execute, hit System.Threading.Tasks.Task.ThrowAsync, and ended up in Dispatcher.UnhandledException — which the App.CrashHandlers path treats as a fatal and fires the crash dialog. Fatal in the log captured during this morning's session at 08:08:27. Wrap the EnableIsoAsync / DisableIsoAsync calls in try/catch: - InvalidOperationException -> toast 'X just left the meeting'; leave IsEnabled at its current value (engine state of record) - Exception -> toast 'Couldn't toggle ISO for X: '; same rationale - finally clause still flips IsProcessing back so the spinner clears No new tests — the race is hard to trigger deterministically without introducing a mocking seam on the controller. The behavior change is small and the surface is the only call site for EnableIso/DisableIso from the participant row. --- .../ViewModels/ParticipantViewModel.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs b/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs index 6a9a03d..712f89a 100644 --- a/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs +++ b/src/TeamsISO.App/ViewModels/ParticipantViewModel.cs @@ -501,6 +501,27 @@ public sealed class ParticipantViewModel : ObservableObject IsEnabled = true; } } + catch (InvalidOperationException) + { + // Race window: participant left the meeting between when the operator + // clicked Enable/Disable and when the engine resolved the ID. The + // controller throws InvalidOperationException with a "not currently + // visible on the network" message in this case. Surface it as a soft + // warning toast rather than letting it escape into the dispatcher's + // unhandled-exception channel (which fires a fatal crash dialog). + // + // Leave IsEnabled at its current value — the engine refused the state + // change, so the VM should reflect the actual engine state. + _toast?.Warn($"{DisplayName} just left the meeting"); + } + catch (Exception ex) + { + // Defensive catch-all for any other engine-side failure (port bind + // race, pipeline factory throw, etc.). Same reasoning as above — + // an exception from an operator click should never tear down the + // dispatcher. + _toast?.Warn($"Couldn't toggle ISO for {DisplayName}: {ex.Message}"); + } finally { IsProcessing = false;