diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 802ba7a53..2d1465fb0 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -18,7 +18,15 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// instance for all subsequent application operations. /// /// A new instance. - public static IApplication Create () { return new ApplicationImpl (); } + /// + /// Thrown if the legacy static Application model has already been used in this process. + /// + public static IApplication Create () + { + ApplicationImpl.MarkInstanceBasedModelUsed (); + + return new ApplicationImpl (); + } /// [RequiresUnreferencedCode ("AOT")] @@ -65,5 +73,12 @@ public static partial class Application // Lifecycle (Init/Shutdown) // guaranteeing that the state of this singleton is deterministic when Init // starts running and after Shutdown returns. [Obsolete ("The legacy static Application object is going away.")] - internal static void ResetState (bool ignoreDisposed = false) => ApplicationImpl.Instance?.ResetState (ignoreDisposed); + internal static void ResetState (bool ignoreDisposed = false) + { + // Reset the model usage tracking first to allow access to Instance if needed + ApplicationImpl.ResetModelUsageTracking (); + + // Now safe to access Instance for cleanup + ApplicationImpl.Instance?.ResetState (ignoreDisposed); + } } diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index ed1e98741..18db0fa6d 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -273,6 +273,10 @@ public partial class ApplicationImpl // gui.cs does no longer process any callbacks. See #1084 for more details: // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); + + // === 12. Reset application model usage tracking === + // Reset the model usage tracking to allow the process to use either model after shutdown + ResetModelUsageTracking (); } /// diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 2dc54cbda..9277a266d 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -21,6 +21,11 @@ public partial class ApplicationImpl : IApplication #region Singleton + /// + /// Tracks which application model has been used in this process. + /// + private static ApplicationModelUsage _modelUsage = ApplicationModelUsage.None; + /// /// Configures the singleton instance of to use the specified backend implementation. /// @@ -33,10 +38,72 @@ public partial class ApplicationImpl : IApplication /// /// Gets the currently configured backend implementation of gateway methods. /// - public static IApplication Instance => _instance ??= new ApplicationImpl (); + public static IApplication Instance + { + get + { + // If an instance already exists, return it without fence checking + // This allows for cleanup/reset operations + if (_instance is { }) + { + return _instance; + } + + // Only check the fence when creating a new instance + if (_modelUsage == ApplicationModelUsage.InstanceBased) + { + throw new InvalidOperationException ( + "Cannot use legacy static Application model (Application.Init/ApplicationImpl.Instance) after using modern instance-based model (Application.Create). " + + "Use only one model per process."); + } + + _modelUsage = ApplicationModelUsage.LegacyStatic; + + return _instance = new ApplicationImpl (); + } + } + + /// + /// INTERNAL: Marks that the instance-based model has been used. Called by Application.Create(). + /// + internal static void MarkInstanceBasedModelUsed () + { + if (_modelUsage == ApplicationModelUsage.LegacyStatic) + { + throw new InvalidOperationException ( + "Cannot use modern instance-based model (Application.Create) after using legacy static Application model (Application.Init/ApplicationImpl.Instance). " + + "Use only one model per process."); + } + + _modelUsage = ApplicationModelUsage.InstanceBased; + } + + /// + /// INTERNAL: Resets the model usage tracking. Only for testing purposes. + /// + internal static void ResetModelUsageTracking () + { + _modelUsage = ApplicationModelUsage.None; + _instance = null; + } #endregion Singleton + /// + /// Defines the different application usage models. + /// + private enum ApplicationModelUsage + { + /// No model has been used yet. + None, + + /// Legacy static model (Application.Init/ApplicationImpl.Instance). + LegacyStatic, + + /// Modern instance-based model (Application.Create). + InstanceBased + } + private string? _driverName; #region Input diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationModelFencingTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationModelFencingTests.cs new file mode 100644 index 000000000..b9728c151 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/ApplicationModelFencingTests.cs @@ -0,0 +1,152 @@ +namespace UnitTests.ApplicationTests; + +/// +/// Tests to ensure that mixing legacy static Application and modern instance-based models +/// throws appropriate exceptions. +/// +[Collection ("Global Test Setup")] +public class ApplicationModelFencingTests +{ + public ApplicationModelFencingTests () + { + // Reset the model usage tracking before each test + ApplicationImpl.ResetModelUsageTracking (); + } + + [Fact] + public void Create_ThenInstanceAccess_ThrowsInvalidOperationException () + { + // Create a modern instance-based application + IApplication app = Application.Create (); + + // Attempting to access the legacy static instance should throw + InvalidOperationException ex = Assert.Throws (() => + { + IApplication _ = ApplicationImpl.Instance; + }); + + Assert.Contains ("Cannot use legacy static Application model", ex.Message); + Assert.Contains ("after using modern instance-based model", ex.Message); + + // Clean up + app.Shutdown (); + } + + [Fact] + public void InstanceAccess_ThenCreate_ThrowsInvalidOperationException () + { + // Access the legacy static instance + IApplication staticInstance = ApplicationImpl.Instance; + + // Attempting to create a modern instance-based application should throw + InvalidOperationException ex = Assert.Throws (() => + { + IApplication _ = Application.Create (); + }); + + Assert.Contains ("Cannot use modern instance-based model", ex.Message); + Assert.Contains ("after using legacy static Application model", ex.Message); + + // Clean up + staticInstance.Shutdown (); + } + + [Fact] + public void Init_ThenCreate_ThrowsInvalidOperationException () + { + // Initialize using legacy static API + IApplication staticInstance = ApplicationImpl.Instance; + staticInstance.Init ("fake"); + + // Attempting to create a modern instance-based application should throw + InvalidOperationException ex = Assert.Throws (() => + { + IApplication _ = Application.Create (); + }); + + Assert.Contains ("Cannot use modern instance-based model", ex.Message); + Assert.Contains ("after using legacy static Application model", ex.Message); + + // Clean up + staticInstance.Shutdown (); + } + + [Fact] + public void Create_ThenInit_ThrowsInvalidOperationException () + { + // Create a modern instance-based application + IApplication app = Application.Create (); + app.Init ("fake"); + + // Attempting to access the legacy static instance should throw + // (Init calls ApplicationImpl.Instance internally) + InvalidOperationException ex = Assert.Throws (() => + { + IApplication _ = ApplicationImpl.Instance; + }); + + Assert.Contains ("Cannot use legacy static Application model", ex.Message); + Assert.Contains ("after using modern instance-based model", ex.Message); + + // Clean up + app.Shutdown (); + } + + [Fact] + public void MultipleCreate_Calls_DoNotThrow () + { + // Multiple calls to Create should not throw + IApplication app1 = Application.Create (); + IApplication app2 = Application.Create (); + IApplication app3 = Application.Create (); + + Assert.NotNull (app1); + Assert.NotNull (app2); + Assert.NotNull (app3); + + // Clean up + app1.Shutdown (); + app2.Shutdown (); + app3.Shutdown (); + } + + [Fact] + public void MultipleInstanceAccess_DoesNotThrow () + { + // Multiple accesses to Instance should not throw (it's a singleton) + IApplication instance1 = ApplicationImpl.Instance; + IApplication instance2 = ApplicationImpl.Instance; + IApplication instance3 = ApplicationImpl.Instance; + + Assert.NotNull (instance1); + Assert.Same (instance1, instance2); + Assert.Same (instance2, instance3); + + // Clean up + instance1.Shutdown (); + } + + [Fact] + public void ResetModelUsageTracking_AllowsSwitchingModels () + { + // Use modern model + IApplication app1 = Application.Create (); + app1.Shutdown (); + + // Reset the tracking + ApplicationImpl.ResetModelUsageTracking (); + + // Should now be able to use legacy model + IApplication staticInstance = ApplicationImpl.Instance; + Assert.NotNull (staticInstance); + staticInstance.Shutdown (); + + // Reset again + ApplicationImpl.ResetModelUsageTracking (); + + // Should be able to use modern model again + IApplication app2 = Application.Create (); + Assert.NotNull (app2); + app2.Shutdown (); + } +}