Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SampSharpEnvironment>() is { } environment)
{
environment.IsShuttingDown = true;
}

disposable.Dispose();
_serviceProvider = null;
}

private void UnhandledExceptionHandler(string context, Exception exception)
Expand Down
12 changes: 11 additions & 1 deletion src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ namespace SampSharp.Entities;
/// <param name="EntryAssembly">The assembly which was configured to launch in open.mp. Used to discover game mode classes and other application types.</param>
/// <param name="Core">The <see cref="ICore" /> interface for the open.mp server. Provides access to core server functionality and extensions.</param>
/// <param name="Components">The <see cref="IComponentList" /> of open.mp. Manages all game components (players, vehicles, objects, etc.) accessible on the server.</param>
public record SampSharpEnvironment(Assembly EntryAssembly, ICore Core, IComponentList Components);
public record SampSharpEnvironment(Assembly EntryAssembly, ICore Core, IComponentList Components)
{
/// <summary>
/// Indicates that the host is in the process of shutting down. Set during <c>EcsHost.Cleanup</c>
/// 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 <c>SafeEventHandlerRegistration.Dispose</c>).
/// </summary>
public bool IsShuttingDown { get; internal set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<TComponent>();

if (!component.HasValue)
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion src/sampsharp-component/managed-host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned int>(code));
return buf;
}
}
}