dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

How to pass APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS using hostfxr #39587

Closed cheverdyukv closed 1 day ago

cheverdyukv commented 4 years ago

I was using https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting#create-a-host-using-mscoreeh to host .NET runtime. But after discussion in #39167 I was told to use hostfxr to host .NET runtime.

But as far as I can see there is no way to pass APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS and our application needs for 2 reasons:

  1. We use some 3rd party components that create threads and raises unhandled exception.
  2. Our application handles all unhandled exceptions anyway using appropriate callbacks, send report to us etc. We just don't need runtime to terminate process at that moment.

This is last issue that prevents us to use hostfxr

jkotas commented 3 years ago

API proposal issue: https://github.com/dotnet/runtime/issues/42275

julianxhokaxhiu commented 1 year ago

Is there any progress being made? I'm currently working on a Mod Manager for FF7 on PC ( https://github.com/tsunamods-codes/7th-Heaven ) and I've been impacted exactly by this. By hooking two APIs ( CloseHandle and DuplicateHandle ) using the Hostxfr, which do throw unhandled exceptions if the handle is invalid, makes the game crash. Having the possibility to ignore unhandled exceptions or handle them instead of force closing the process would be HIGHLY appreciated.

Thank you in advance.

vladimir-cheverdyuk-altium commented 1 year ago

You can use GetCLRRuntimeHost to initialize runtime second time and disable unhanded exceptions. I didn't full battle test it, but our huge app starts fine with it. I also test it with unhanded exception and it suppresses them.

It just must be at appropriate time.

julianxhokaxhiu commented 1 year ago

@vladimir-cheverdyuk-altium I'd be very interested to test this. Do you have a sample code I can use as a basis? Thanks!

vladimir-cheverdyuk-altium commented 1 year ago

I have it but it in Delphi (Pascal like language). If you want I can paste a few functions here.

julianxhokaxhiu commented 1 year ago

Yep, fine by me. I can relate, thanks :)

vladimir-cheverdyuk-altium commented 1 year ago
// hostfxr does not have ability to suppress unhandled exception. See https://github.com/dotnet/runtime/issues/39587
// As result I had to use obsolete way to do it.
// This function suppose to do nothing except setting up APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS

// Unfortunatelly we cannot use this way to initialize .NET runtime because this API do not initialize AssemblyLoadContext
// For more info check https://github.com/dotnet/runtime/issues/39167
Procedure DisableUnhandledExceptions;
Const
    cDefaultAppDomainId = 1; // There is only one domain in .NET Core
Type
    TGetCLRRuntimeHostFunc = Function(Const riid : TGUID) : IUnknown; Safecall;
Var
    ClrDirectory          : String;
    hCoreClr              : THandle;
    GetCLRRuntimeHostFunc : TGetCLRRuntimeHostFunc;
    Host                  : ICLRRuntimeHost2;
    DomainId              : DWORD;
    PropCount             : SIZE_T;
    PropKeys              : Array Of pchar_t;
    PropValues            : Array Of pchar_t;
    ErrorCode             : Int32;
Begin
    ClrDirectory := ExtractFilePath(GetRuntimeProperty('FX_DEPS_FILE'));

    hCoreClr := LoadLibraryEx(PChar(ClrDirectory + 'coreclr.dll'), 0, 0);
    If hCoreClr = 0 Then
        RaiseLastOSError(GetLastError, 'Failed to load CoreCLR');

    GetCLRRuntimeHostFunc := GetProcAddress(hCoreClr, 'GetCLRRuntimeHost');
    If Not Assigned(GetCLRRuntimeHostFunc) Then
        Raise Exception.Create('Cannot find "GetCLRRuntimeHost" function');

    Host := ICLRRuntimeHost2(GetCLRRuntimeHostFunc(ICLRRuntimeHost2));
    If Host = Nil Then
        Raise Exception.Create('Failed to create ICLRRuntimeHost2');

    // Host already initialized, so there is no need to do it again
    // Host.SetStartupFlags(STARTUP_FLAGS.STARTUP_CONCURRENT_GC Or STARTUP_FLAGS.STARTUP_SINGLE_APPDOMAIN Or STARTUP_FLAGS.STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN);
    Host.Start();

    PropCount := 0;
    // Get number of properties
    ErrorCode := gv_hostfxr_get_runtime_properties_fn(gv_HostfxrHandle, PropCount, Nil, Nil);
    If (ErrorCode <> StatusCode.Success) And (ErrorCode <> Int32(StatusCode.HostApiBufferTooSmall)) Then
        Raise Exception.Create('Failed to call hostfxr_get_runtime_properties');

    // Allocate arrays for names and values
    SetLength(PropKeys, PropCount);
    SetLength(PropValues, PropCount);

    // Retrieve names and values
    If gv_hostfxr_get_runtime_properties_fn(gv_HostfxrHandle, PropCount, @PropKeys[0], @PropValues[0]) <> StatusCode.Success  Then
        Raise Exception.Create('Failed to call hostfxr_get_runtime_properties');

    // There is only one domain possible to .NET Core, so we can create it here
    // We pass all properties from hostfxr because otherwise they will be empty
    DomainId := Host.CreateAppDomainWithManager(
        'Default Domain',
        APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS Or
        APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP Or
        APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT Or
        APPDOMAIN_SECURITY_FLAGS.APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS,
        Nil,
        Nil,
        PropCount,
        @PropKeys[0],
        @PropValues[0]);

    // Just o check that it didn't fail
    If DomainId <> cDefaultAppDomainId Then
        // Only one domain can be created in .NET. Retriving default domain is not thread safe and as a result we will use constant
        Raise Exception.Create('Invalid domain id');
End;

Procedure InitializeHostFxr(HostPath, DotNetRoot, NativeImagesPaths, TrustedPlatformAssemblies : String);
Var
    HostDirectory                            : String;
    ErrorCode                                : Integer;
    HostfxrHandle                            : THandle;
    hostfxr_initialize_for_runtime_config_fn : hostfxr_initialize_for_runtime_config_type;
    hostfxr_get_runtime_delegate_fn          : hostfxr_get_runtime_delegate_type;
    InitializePameters                       : hostfxr_initialize_parameters;
Begin
    HostDirectory := ExtractFileDir(HostPath);
    HostfxrHandle := LoadLibraryEx(PChar(HostDirectory + '\hostfxr.dll'), 0, 0);
    If HostfxrHandle = 0 Then
        RaiseLastOSError(GetLastError, 'Failed to load CoreCLR');

    hostfxr_initialize_for_runtime_config_fn := FindProcAddress(HostfxrHandle, 'hostfxr_initialize_for_runtime_config');
    hostfxr_get_runtime_delegate_fn          := FindProcAddress(HostfxrHandle, 'hostfxr_get_runtime_delegate');
    gv_hostfxr_get_runtime_property_value_fn := FindProcAddress(HostfxrHandle, 'hostfxr_get_runtime_property_value');
    gv_hostfxr_set_runtime_property_value_fn := FindProcAddress(HostfxrHandle, 'hostfxr_set_runtime_property_value');
    gv_hostfxr_get_runtime_properties_fn     := FindProcAddress(HostfxrHandle, 'hostfxr_get_runtime_properties');

    InitializePameters.size        := SizeOf(InitializePameters);
    InitializePameters.host_path   := pchar_t(HostPath);
    InitializePameters.dotnet_root := pchar_t(DotNetRoot);

    ErrorCode := hostfxr_initialize_for_runtime_config_fn(pchar_t(ChangeFileExt(HostPath, '.runtimeconfig.json')), InitializePameters, gv_HostfxrHandle);
    If ErrorCode <> StatusCode.Success Then
        Raise Exception.CreateFmt('hostfxr_initialize_for_runtime_config returned error code %x', [ErrorCode]);

    // PROBING_DIRECTORIES will be populated from additionalProbingPaths but it will resolve relative paths based on current directory instead of directory that contains
    // application's .runtimeconfig.json file. As result I created new section called additionalAssemblyPaths in .runtimeconfig.json.
    // Function ReadAdditionalAssemblyPaths will read .runtimeconfig.json file, parse it and resolve relative papths manualy.
    SetRuntimeProperty('APP_PATHS', ReadAdditionalAssemblyPaths(ChangeFileExt(HostPath, '.runtimeconfig.json')));

    SetRuntimeProperty('APP_NI_PATHS', NativeImagesPaths);

    If TrustedPlatformAssemblies <> '' Then
    Begin
        // CreateDotNetDelegate without Assembly name parameter works only with types and classes from trusted platform assemblies. This is way to add assembly to that list
        // TRUSTED_PLATFORM_ASSEMBLIES will populate automatically during call to hostfxr_initialize_for_runtime_config_fn
        // More info: https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing
        SetRuntimeProperty('TRUSTED_PLATFORM_ASSEMBLIES', GetRuntimeProperty('TRUSTED_PLATFORM_ASSEMBLIES') + ';' + TrustedPlatformAssemblies);
    End;

    // Required for AppContext.BaseDirectory property to work
    SetRuntimeProperty('APP_CONTEXT_BASE_DIRECTORY', HostDirectory);

    ErrorCode := hostfxr_get_runtime_delegate_fn(gv_HostfxrHandle, hostfxr_delegate_type.hdt_get_function_pointer, @gv_hostfxr_get_function_pointer_fn);
    If ErrorCode <> StatusCode.Success Then
        Raise Exception.CreateFmt('hostfxr_get_runtime_delegate returned error code %x', [ErrorCode]);

    ErrorCode := hostfxr_get_runtime_delegate_fn(gv_HostfxrHandle, hostfxr_delegate_type.hdt_load_assembly_and_get_function_pointer, @gv_hostfxr_load_assembly_and_get_function_pointer_fn);
    If ErrorCode <> StatusCode.Success Then
        Raise Exception.CreateFmt('hostfxr_get_runtime_delegate returned error code %x', [ErrorCode]);

    DisableUnhandledExceptions;

    // Just in case, we not suppose to finilize it because our application keeps .NET forever
End;
julianxhokaxhiu commented 1 year ago

Amazing! Thanks a lot @vladimir-cheverdyuk-altium very interesting approach, I'll try to translate this into C++ native code and I'll post here the results ASAP I'll be able to test this. Cheers!

vladimir-cheverdyuk-altium commented 1 year ago

Great!

vladimir-cheverdyuk-altium commented 1 year ago

Just in case this code is assume that framework will be packaged with application and there will be .runtimeconfig.json file that application used for own needs in different than intended way. I just pasted that code to demonstrate where you need to call DisableUnhandledExceptions.

julianxhokaxhiu commented 1 year ago

Yup, crystal clear how's the approach, I never thought about "abusing it" this way, pretty smart move. It might solve also my own issues. Thanks again, appreciated!

julianxhokaxhiu commented 1 year ago

Allright for anyone interested I translated this into C++, unfortunately doesn't fix my issue but the code works as intended:

#include <stdio.h>
#include "mscoree.h" // https://github.com/dotnet/coreclr/blob/master/src/pal/prebuilt/inc/mscoree.h

hostfxr_handle context = nullptr; // here lives the hostfxr context from hostfxr_initialize_for_runtime_config()
static const wchar_t* coreCLRInstallDirectory = L"%programfiles%\\dotnet\\shared\\Microsoft.NETCore.App\\7.0.0";
static const wchar_t* coreCLRDll = L"coreclr.dll";

// -------------------------------

// Check for CoreCLR.dll in a given path and load it, if possible
HMODULE LoadCoreCLR(const wchar_t* directoryPath)
{
    wchar_t coreDllPath[MAX_PATH];
    wcscpy_s(coreDllPath, MAX_PATH, directoryPath);
    wcscat_s(coreDllPath, MAX_PATH, L"\\");
    wcscat_s(coreDllPath, MAX_PATH, coreCLRDll);

    return LoadLibraryExW(coreDllPath, NULL, 0);
}

void DisableUnhandledExceptions()
{
    wchar_t coreRoot[MAX_PATH];
    ::ExpandEnvironmentStringsW(coreCLRInstallDirectory, coreRoot, MAX_PATH);
    HMODULE coreCLRModule = LoadCoreCLR(coreRoot);

    ICLRRuntimeHost2* runtimeHost;

    FnGetCLRRuntimeHost pfnGetCLRRuntimeHost =
        (FnGetCLRRuntimeHost)::GetProcAddress(coreCLRModule, "GetCLRRuntimeHost");

    // Get the hosting interface
    HRESULT hr = pfnGetCLRRuntimeHost(IID_ICLRRuntimeHost2, (IUnknown**)&runtimeHost);

    runtimeHost->Start();

    size_t num_props = 0;
    DWORD domainId;

    hostfxr_get_runtime_properties(context, &num_props, nullptr, nullptr);

    const char_t** fxr_keys = new const char_t * [num_props];
    const char_t** fxr_values = new const char_t * [num_props];

    hostfxr_get_runtime_properties(context, &num_props, fxr_keys, fxr_values);

    // Create the AppDomain
    hr = runtimeHost->CreateAppDomainWithManager(
        L"Default Domain",
        APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS ||
        APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP ||
        APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT ||
        APPDOMAIN_IGNORE_UNHANDLED_EXCEPTIONS,
        NULL,
        NULL,
        num_props,
        fxr_keys,
        fxr_values,
        &domainId
    );
}

// Just call DisableUnhandledExceptions() after hostfxr_close();

Main differences from the above version:

Feel free to tweak at your own heart and desire. Thanks again @vladimir-cheverdyuk-altium for the snippet on your side!

vladimir-cheverdyuk-altium commented 1 year ago

Does it suppress unhanded exceptions?

jkotas commented 4 months ago

Please check https://github.com/dotnet/runtime/issues/101560 and let us know if it addresses your scenario.

vladimir-cheverdyuk-altium commented 4 months ago

Hi @jkotas. It looks like what we want to. Thank you!

elinor-fung commented 1 day ago

I'm going to close this against https://github.com/dotnet/runtime/issues/101560, as that seems to address the requested scenarios here.