diff --git a/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs b/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs index d964b8ea..21fa75ba 100644 --- a/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs +++ b/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs @@ -27,12 +27,23 @@ protected override void Cleanup() { OnGameModeExit(); - if (_serviceProvider is IDisposable disposable) + if (_serviceProvider is not IDisposable disposable) { - // TODO: This cleanup is called so late - we can't unsubscribe event handlers anymore, but the disposables in registered systems will try to unsubscribe them. This may cause a System.ExecutionEngineException on shutdown. - disposable.Dispose(); - _serviceProvider = null; + return; } + + // Cleanup runs after open.mp has already started tearing down other components + // pointers in IComponentList / ICore are unsafe to deref by now, so we flag the + // environment as shutting down and let SafeEventHandlerRegistration etc. skip + // their native unsubscribe calls. Without this, disposing systems crashes with + // an AV in IComponentList::QueryComponent. + if (_serviceProvider.GetService() is { } environment) + { + environment.IsShuttingDown = true; + } + + disposable.Dispose(); + _serviceProvider = null; } private void UnhandledExceptionHandler(string context, Exception exception) diff --git a/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs b/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs index c1a8469d..c0abf923 100644 --- a/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs +++ b/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs @@ -13,4 +13,14 @@ namespace SampSharp.Entities; /// The assembly which was configured to launch in open.mp. Used to discover game mode classes and other application types. /// The interface for the open.mp server. Provides access to core server functionality and extensions. /// The of open.mp. Manages all game components (players, vehicles, objects, etc.) accessible on the server. -public record SampSharpEnvironment(Assembly EntryAssembly, ICore Core, IComponentList Components); \ No newline at end of file +public record SampSharpEnvironment(Assembly EntryAssembly, ICore Core, IComponentList Components) +{ + /// + /// Indicates that the host is in the process of shutting down. Set during EcsHost.Cleanup + /// before the DI scope is disposed. While true, code paths that rely on native open.mp objects + /// (e.g. component lookups via P/Invoke) must skip those calls — by this point open.mp may have + /// already torn down components, and re-entering native code can crash with an access violation + /// (we have observed this in SafeEventHandlerRegistration.Dispose). + /// + public bool IsShuttingDown { get; internal set; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs b/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs index 7d5bca68..25d4c5e0 100644 --- a/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs +++ b/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs @@ -17,6 +17,16 @@ public void Dispose() _disposed = true; + // During host shutdown the native IComponentList/ICore pointers may already be + // invalidated by open.mp. Calling QueryComponent here would crash with an AV in + // the P/Invoke trampoline before HasValue could even return false. Just free the + // marshalled handler and skip the unsubscribe — the native side is going away. + if (environment.IsShuttingDown) + { + TEventHandler.Marshaller.Marshal(handler).Free(); + return; + } + var component = environment.Components.QueryComponent(); if (!component.HasValue) @@ -51,6 +61,13 @@ public void Dispose() _disposed = true; + // See note in the component-typed overload above — same reasoning for ICore. + if (environment.IsShuttingDown) + { + TEventHandler.Marshaller.Marshal(handler).Free(); + return; + } + if (!environment.Core.HasValue) { TEventHandler.Marshaller.Marshal(handler).Free(); diff --git a/src/sampsharp-component/managed-host.cpp b/src/sampsharp-component/managed-host.cpp index 60790feb..d0843bf5 100644 --- a/src/sampsharp-component/managed-host.cpp +++ b/src/sampsharp-component/managed-host.cpp @@ -214,6 +214,10 @@ const char * ManagedHost::get_error(int code) const case 0x800080a6: return "Hosting API does not support the requested scenario"; case 0x800080a7: return "Support for a requested feature is disabled"; case ERROR_MISSING_EXPORT: return "Missing export fuction in host library"; - default: return "Unkown error"; + default: { + static thread_local char buf[64]; + snprintf(buf, sizeof(buf), "Unknown HRESULT 0x%08X", static_cast(code)); + return buf; + } } } \ No newline at end of file