From acf3cd100fffdb63e7bb11205c2ca7083a5fd23f Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:10:33 +0200 Subject: [PATCH 1/5] Add native call failure policy --- src/SightAdapt/NativeCall.cs | 148 +++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/SightAdapt/NativeCall.cs diff --git a/src/SightAdapt/NativeCall.cs b/src/SightAdapt/NativeCall.cs new file mode 100644 index 00000000..4952b7a7 --- /dev/null +++ b/src/SightAdapt/NativeCall.cs @@ -0,0 +1,148 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace SightAdapt; + +internal static class NativeCall +{ + public static nint RequireHandle( + nint handle, + string operation) + { + return RequireHandle( + handle, + operation, + Marshal.GetLastWin32Error); + } + + internal static nint RequireHandle( + nint handle, + string operation, + Func getLastError) + { + ValidateArguments(operation, getLastError); + return handle != nint.Zero + ? handle + : throw CreateException( + operation, + getLastError()); + } + + public static void RequireSuccess( + bool succeeded, + string operation) + { + RequireSuccess( + succeeded, + operation, + Marshal.GetLastWin32Error); + } + + internal static void RequireSuccess( + bool succeeded, + string operation, + Func getLastError) + { + ValidateArguments(operation, getLastError); + if (!succeeded) + { + throw CreateException( + operation, + getLastError()); + } + } + + public static bool TryTransient( + bool succeeded, + string operation) + { + return TryTransient( + succeeded, + operation, + Marshal.GetLastWin32Error, + ReportFailure); + } + + internal static bool TryTransient( + bool succeeded, + string operation, + Func getLastError, + Action reportFailure) + { + ValidateArguments(operation, getLastError); + ArgumentNullException.ThrowIfNull(reportFailure); + + if (succeeded) + { + return true; + } + + reportFailure(FormatFailure( + operation, + getLastError())); + return false; + } + + public static void BestEffort( + bool succeeded, + string operation) + { + BestEffort( + succeeded, + operation, + Marshal.GetLastWin32Error, + ReportFailure); + } + + internal static void BestEffort( + bool succeeded, + string operation, + Func getLastError, + Action reportFailure) + { + ValidateArguments(operation, getLastError); + ArgumentNullException.ThrowIfNull(reportFailure); + + if (!succeeded) + { + reportFailure(FormatFailure( + operation, + getLastError())); + } + } + + internal static string FormatFailure( + string operation, + int errorCode) + { + ArgumentException.ThrowIfNullOrWhiteSpace(operation); + var description = + new Win32Exception(errorCode).Message; + return $"{operation} failed with Win32 error " + + $"{errorCode}: {description}"; + } + + private static Win32Exception CreateException( + string operation, + int errorCode) + { + return new Win32Exception( + errorCode, + FormatFailure(operation, errorCode)); + } + + private static void ValidateArguments( + string operation, + Func getLastError) + { + ArgumentException.ThrowIfNullOrWhiteSpace(operation); + ArgumentNullException.ThrowIfNull(getLastError); + } + + private static void ReportFailure( + string message) + { + Debug.WriteLine($"SightAdapt native call: {message}"); + } +} From 5e6e9e5cbe1b706b9222e9d8bfa350f6c40dfa5d Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:11:09 +0200 Subject: [PATCH 2/5] Apply explicit native overlay failure policies --- src/SightAdapt/MagnifierOverlay.cs | 180 ++++++++++++++++++----------- 1 file changed, 110 insertions(+), 70 deletions(-) diff --git a/src/SightAdapt/MagnifierOverlay.cs b/src/SightAdapt/MagnifierOverlay.cs index b07e2fda..94c3cbc0 100644 --- a/src/SightAdapt/MagnifierOverlay.cs +++ b/src/SightAdapt/MagnifierOverlay.cs @@ -1,4 +1,3 @@ -using System.ComponentModel; using System.Drawing; namespace SightAdapt; @@ -86,48 +85,48 @@ protected override void OnShown(EventArgs eventArgs) { base.OnShown(eventArgs); - NativeMethods.SetLayeredWindowAttributes( - Handle, - 0, - 255, - NativeMethods.LwaAlpha); - - _magnifierWindow = NativeMethods.CreateWindowEx( - 0, - NativeMethods.WcMagnifier, - "SightAdapt Magnifier", - NativeMethods.WsChild | NativeMethods.WsVisible, - 0, - 0, - Math.Max(ClientSize.Width, 1), - Math.Max(ClientSize.Height, 1), - Handle, - nint.Zero, - nint.Zero, - nint.Zero); - - if (_magnifierWindow == nint.Zero) - { - throw new Win32Exception( - System.Runtime.InteropServices.Marshal.GetLastWin32Error(), - "Could not create the Windows magnifier control."); - } + NativeCall.RequireSuccess( + NativeMethods.SetLayeredWindowAttributes( + Handle, + 0, + 255, + NativeMethods.LwaAlpha), + "Set layered overlay opacity"); + + _magnifierWindow = NativeCall.RequireHandle( + NativeMethods.CreateWindowEx( + 0, + NativeMethods.WcMagnifier, + "SightAdapt Magnifier", + NativeMethods.WsChild | + NativeMethods.WsVisible, + 0, + 0, + Math.Max(ClientSize.Width, 1), + Math.Max(ClientSize.Height, 1), + Handle, + nint.Zero, + nint.Zero, + nint.Zero), + "Create Windows magnifier control"); var transform = MagTransform.Identity; - if (!NativeMethods.MagSetWindowTransform(_magnifierWindow, ref transform)) - { - throw new Win32Exception( - "Could not initialize the magnifier transform."); - } + NativeCall.RequireSuccess( + NativeMethods.MagSetWindowTransform( + _magnifierWindow, + ref transform), + "Initialize magnifier transform"); ApplyColorEffectToMagnifier(); var excludedWindows = new[] { Handle }; - NativeMethods.MagSetWindowFilterList( - _magnifierWindow, - NativeMethods.MwFilterModeExclude, - excludedWindows.Length, - excludedWindows); + NativeCall.RequireSuccess( + NativeMethods.MagSetWindowFilterList( + _magnifierWindow, + NativeMethods.MwFilterModeExclude, + excludedWindows.Length, + excludedWindows), + "Exclude overlay window from magnifier source"); _initialized = true; _updateTimer.Start(); @@ -144,7 +143,10 @@ protected override void Dispose(bool disposing) if (_magnifierWindow != nint.Zero) { - NativeMethods.DestroyWindow(_magnifierWindow); + NativeCall.BestEffort( + NativeMethods.DestroyWindow( + _magnifierWindow), + "Destroy magnifier control"); _magnifierWindow = nint.Zero; } @@ -165,11 +167,11 @@ protected override void WndProc(ref Message message) private void ApplyColorEffectToMagnifier() { var colorEffect = _colorEffect; - if (!NativeMethods.MagSetColorEffect(_magnifierWindow, ref colorEffect)) - { - throw new Win32Exception( - $"Could not apply the '{_transformId}' visual transform."); - } + NativeCall.RequireSuccess( + NativeMethods.MagSetColorEffect( + _magnifierWindow, + ref colorEffect), + $"Apply '{_transformId}' visual transform"); } private void UpdateOverlay() @@ -179,7 +181,8 @@ private void UpdateOverlay() return; } - var targetExists = NativeMethods.IsWindow(TargetHandle); + var targetExists = + NativeMethods.IsWindow(TargetHandle); var targetAvailable = targetExists && NativeMethods.IsWindowVisible(TargetHandle) && !NativeMethods.IsIconic(TargetHandle) && @@ -187,7 +190,8 @@ private void UpdateOverlay() if (!targetAvailable) { - if (_hasRenderedFrame && IsWithinTransitionGrace()) + if (_hasRenderedFrame && + IsWithinTransitionGrace()) { return; } @@ -198,7 +202,7 @@ private void UpdateOverlay() } else { - NativeMethods.ShowWindow(Handle, NativeMethods.SwHide); + HideOverlay(); } return; @@ -211,38 +215,70 @@ private void UpdateOverlay() _overlayScope, out var geometry)) { - NativeMethods.ShowWindow(Handle, NativeMethods.SwHide); + HideOverlay(); return; } var destination = geometry.Destination; - NativeMethods.SetWindowPos( - Handle, - NativeMethods.HwndTopMost, - destination.Left, - destination.Top, - destination.Width, - destination.Height, - NativeMethods.SwpNoActivate | NativeMethods.SwpShowWindow); - - NativeMethods.SetWindowPos( - _magnifierWindow, - nint.Zero, - 0, - 0, - destination.Width, - destination.Height, - NativeMethods.SwpNoActivate | NativeMethods.SwpNoZOrder); + if (!NativeCall.TryTransient( + NativeMethods.SetWindowPos( + Handle, + NativeMethods.HwndTopMost, + destination.Left, + destination.Top, + destination.Width, + destination.Height, + NativeMethods.SwpNoActivate | + NativeMethods.SwpShowWindow), + "Position overlay window")) + { + HideOverlay(); + return; + } + + if (!NativeCall.TryTransient( + NativeMethods.SetWindowPos( + _magnifierWindow, + nint.Zero, + 0, + 0, + destination.Width, + destination.Height, + NativeMethods.SwpNoActivate | + NativeMethods.SwpNoZOrder), + "Resize magnifier control")) + { + HideOverlay(); + return; + } var source = geometry.Source; - if (!NativeMethods.MagSetWindowSource(_magnifierWindow, source)) + if (!NativeCall.TryTransient( + NativeMethods.MagSetWindowSource( + _magnifierWindow, + source), + "Set magnifier source rectangle")) { - NativeMethods.ShowWindow(Handle, NativeMethods.SwHide); + HideOverlay(); return; } _hasRenderedFrame = true; - NativeMethods.InvalidateRect(_magnifierWindow, nint.Zero, true); + + // A repaint request is intentionally best effort. InvalidateRect's + // return value does not provide a useful extended error contract. + _ = NativeMethods.InvalidateRect( + _magnifierWindow, + nint.Zero, + true); + } + + private void HideOverlay() + { + // ShowWindow returns the previous visibility state, not success. + _ = NativeMethods.ShowWindow( + Handle, + NativeMethods.SwHide); } private bool IsWithinTransitionGrace() @@ -265,8 +301,11 @@ private void ResetTransitionGrace() private bool IsTargetForeground() { - var foreground = NativeMethods.GetForegroundWindow(); - foreground = NativeMethods.GetAncestor(foreground, NativeMethods.GaRoot); + var foreground = + NativeMethods.GetForegroundWindow(); + foreground = NativeMethods.GetAncestor( + foreground, + NativeMethods.GaRoot); return foreground == TargetHandle; } @@ -279,7 +318,8 @@ private static nint ValidateTarget(nint targetHandle) nameof(targetHandle)); } - private static string NormalizeTransformId(string transformId) + private static string NormalizeTransformId( + string transformId) { return !string.IsNullOrWhiteSpace(transformId) ? transformId.Trim() From 9815cb3c9057b9b685e5e71bc148c6857b80cb53 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:11:31 +0200 Subject: [PATCH 3/5] Test native call failure classifications --- tests/SightAdapt.Tests/NativeCallTests.cs | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/SightAdapt.Tests/NativeCallTests.cs diff --git a/tests/SightAdapt.Tests/NativeCallTests.cs b/tests/SightAdapt.Tests/NativeCallTests.cs new file mode 100644 index 00000000..148bd6fa --- /dev/null +++ b/tests/SightAdapt.Tests/NativeCallTests.cs @@ -0,0 +1,119 @@ +using System.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace SightAdapt.Tests; + +[TestClass] +public sealed class NativeCallTests +{ + [TestMethod] + public void CriticalFailureIncludesOperationAndNativeError() + { + var exception = + Assert.ThrowsException(() => + NativeCall.RequireSuccess( + succeeded: false, + "Apply magnifier filter", + () => 5)); + + Assert.AreEqual(5, exception.NativeErrorCode); + StringAssert.Contains( + exception.Message, + "Apply magnifier filter"); + StringAssert.Contains( + exception.Message, + "Win32 error 5"); + } + + [TestMethod] + public void RequiredHandleRejectsZeroHandle() + { + var exception = + Assert.ThrowsException(() => + NativeCall.RequireHandle( + nint.Zero, + "Create magnifier control", + () => 87)); + + Assert.AreEqual(87, exception.NativeErrorCode); + StringAssert.Contains( + exception.Message, + "Create magnifier control"); + } + + [TestMethod] + public void TransientFailureIsReportedWithoutThrowing() + { + var messages = new List(); + + var succeeded = NativeCall.TryTransient( + succeeded: false, + "Position overlay", + () => 1400, + messages.Add); + + Assert.IsFalse(succeeded); + Assert.AreEqual(1, messages.Count); + StringAssert.Contains( + messages[0], + "Position overlay"); + StringAssert.Contains( + messages[0], + "Win32 error 1400"); + } + + [TestMethod] + public void BestEffortFailureIsReported() + { + var messages = new List(); + + NativeCall.BestEffort( + succeeded: false, + "Destroy magnifier control", + () => 6, + messages.Add); + + Assert.AreEqual(1, messages.Count); + StringAssert.Contains( + messages[0], + "Destroy magnifier control"); + } + + [TestMethod] + public void SuccessfulCallsDoNotReadOrReportAnError() + { + var errorReads = 0; + var messages = new List(); + + NativeCall.RequireSuccess( + succeeded: true, + "Initialize magnifier", + () => + { + errorReads++; + return 1; + }); + var transient = NativeCall.TryTransient( + succeeded: true, + "Position overlay", + () => + { + errorReads++; + return 1; + }, + messages.Add); + NativeCall.BestEffort( + succeeded: true, + "Destroy magnifier control", + () => + { + errorReads++; + return 1; + }, + messages.Add); + + Assert.IsTrue(transient); + Assert.AreEqual(0, errorReads); + Assert.AreEqual(0, messages.Count); + } +} From 1de8d3a25acf80e1bf020eca03691be6573936be Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:11:46 +0200 Subject: [PATCH 4/5] Advance version to 0.5.0.33 --- src/SightAdapt/SightAdapt.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SightAdapt/SightAdapt.csproj b/src/SightAdapt/SightAdapt.csproj index 5a9e204a..61c91af9 100644 --- a/src/SightAdapt/SightAdapt.csproj +++ b/src/SightAdapt/SightAdapt.csproj @@ -16,10 +16,10 @@ Copyright © $(Company) MIT https://github.com/KeyffMS/SightAdapt - 0.5.0.32 + 0.5.0.33 0.5.0.0 - 0.5.0.32 - 0.5.0.32 + 0.5.0.33 + 0.5.0.33 x64 x64 10.0.19041.0 From cc2937fc7466b7315791a06c0124230cf4cd30d4 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:12:18 +0200 Subject: [PATCH 5/5] Document native call failure policy --- docs/ARCHITECTURE.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c9047c3f..1f236fc2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -66,6 +66,7 @@ A failed mutation or failed write does not replace committed settings and does n | Bounded process identity cache | `ApplicationIdentityCache` | | Overlay geometry | `OverlayBoundsResolver` | | Overlay resource lifetime and retargeting | `OverlayController` | +| Native call failure classification and diagnostics | `NativeCall` | | Native target, rendering, geometry refresh, and transition grace | `MagnifierOverlay` | | Notification-area presentation | `TrayPresenter` | | Application-table presentation and edit mechanics | `ApplicationProfilesGrid` | @@ -111,6 +112,16 @@ A rendered frame may remain visible for at most 125 ms during target transition. The current backend uses the same rectangle for the magnifier source and overlay destination. +## Native call failure policy + +`NativeCall` classifies fallible Win32 and Magnification API operations explicitly: + +- **critical** initialization and effect calls throw a `Win32Exception` containing the operation name and native error code; +- **transient** geometry, positioning, and source-update failures are diagnosed, hide the overlay, and allow a later timer tick to recover; +- **best effort** cleanup failures are diagnosed without replacing the primary application failure. + +`ShowWindow` and `InvalidateRect` are handled explicitly at their call sites because their Boolean return values do not represent a standard extended-error success contract. + ## Configuration grid boundary `ApplicationProfilesGrid` owns columns, rows, selectors, status painting, selection, empty state, stable executable-path keys, separate typed change events, row updates, and failed-cell restoration. It does not know about persistence or dialogs. @@ -130,7 +141,7 @@ The current backend uses the same rectangle for the magnifier source and overlay ## Architecture test strategy -Architecture checks are behavior-first. Transaction publication, defensive settings snapshots, failed persistence, expected and unexpected transaction failures, emergency ordering, runtime state transitions, transform catalog consistency, overlay-scope recovery, grid commits, menu roles, preview caching, and profile-manager refresh behavior are exercised through executable tests. +Architecture checks are behavior-first. Transaction publication, defensive settings snapshots, failed persistence, expected and unexpected transaction failures, emergency ordering, runtime state transitions, native call classification, transform catalog consistency, overlay-scope recovery, grid commits, menu roles, preview caching, and profile-manager refresh behavior are exercised through executable tests. Source inspection is retained only for exhaustive negative rules that cannot be proven by a finite runtime scenario: