Skip to content

Commit

Permalink
Show error dialog for missing runtime/framework (dotnet/core-setup#8509)
Browse files Browse the repository at this point in the history
Commit migrated from dotnet/core-setup@346b717
  • Loading branch information
elinor-fung authored Oct 18, 2019
1 parent 4d82434 commit c17bb03
Show file tree
Hide file tree
Showing 11 changed files with 372 additions and 100 deletions.
10 changes: 9 additions & 1 deletion src/installer/corehost/cli/apphost/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ set(HEADERS
./bundle/dir_utils.h
)

if(WIN32)
list(APPEND SOURCES
apphost.windows.cpp)

list(APPEND HEADERS
apphost.windows.h)
endif()

include(../exe.cmake)

add_definitions(-DFEATURE_APPHOST=1)
Expand All @@ -56,5 +64,5 @@ endif()

# Specify non-default Windows libs to be used for Arm/Arm64 builds
if (WIN32 AND (CLI_CMAKE_PLATFORM_ARCH_ARM OR CLI_CMAKE_PLATFORM_ARCH_ARM64))
target_link_libraries(apphost Advapi32.lib)
target_link_libraries(apphost Advapi32.lib shell32.lib)
endif()
137 changes: 137 additions & 0 deletions src/installer/corehost/cli/apphost/apphost.windows.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#include "apphost.windows.h"
#include "error_codes.h"
#include "pal.h"
#include "trace.h"
#include "utils.h"

namespace
{
pal::string_t g_buffered_errors;

void buffering_trace_writer(const pal::char_t* message)
{
// Add to buffer for later use.
g_buffered_errors.append(message).append(_X("\n"));
// Also write to stderr immediately
pal::err_fputs(message);
}

// Determines if the current module (apphost executable) is marked as a Windows GUI application
bool is_gui_application()
{
HMODULE module = ::GetModuleHandleW(nullptr);
assert(module != nullptr);

// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
BYTE *bytes = reinterpret_cast<BYTE *>(module);
UINT32 pe_header_offset = reinterpret_cast<IMAGE_DOS_HEADER *>(bytes)->e_lfanew;
UINT16 subsystem = reinterpret_cast<IMAGE_NT_HEADERS *>(bytes + pe_header_offset)->OptionalHeader.Subsystem;

return subsystem == IMAGE_SUBSYSTEM_WINDOWS_GUI;
}

void write_errors_to_event_log(const pal::char_t *executable_path, const pal::char_t *executable_name)
{
// Report errors to the Windows Event Log.
auto eventSource = ::RegisterEventSourceW(nullptr, _X(".NET Runtime"));
const DWORD traceErrorID = 1023; // Matches CoreCLR ERT_UnmanagedFailFast
pal::string_t message;
message.append(_X("Description: A .NET Core application failed.\n"));
message.append(_X("Application: ")).append(executable_name).append(_X("\n"));
message.append(_X("Path: ")).append(executable_path).append(_X("\n"));
message.append(_X("Message: ")).append(g_buffered_errors).append(_X("\n"));

LPCWSTR messages[] = {message.c_str()};
::ReportEventW(eventSource, EVENTLOG_ERROR_TYPE, 0, traceErrorID, nullptr, 1, 0, messages, nullptr);
::DeregisterEventSource(eventSource);
}

void show_error_dialog(const pal::char_t *executable_name, int error_code)
{
// Show message dialog for UI apps with actionable errors
if (error_code != StatusCode::CoreHostLibMissingFailure // missing hostfxr
&& error_code != StatusCode::FrameworkMissingFailure) // missing framework
return;

pal::string_t gui_errors_disabled;
if (pal::getenv(_X("DOTNET_DISABLE_GUI_ERRORS"), &gui_errors_disabled) && pal::xtoi(gui_errors_disabled.c_str()) == 1)
return;

pal::string_t dialogMsg = _X("To run this application, you must install .NET Core.\n\n");
pal::string_t url;
if (error_code == StatusCode::CoreHostLibMissingFailure)
{
url = get_download_url();
}
else if (error_code == StatusCode::FrameworkMissingFailure)
{
// We don't have a great way of passing out different kinds of detailed error info across components, so
// just match the expected error string. See fx_resolver.messages.cpp.
pal::string_t line;
pal::stringstream_t ss(g_buffered_errors);
while (std::getline(ss, line, _X('\n'))){
const pal::string_t prefix = _X("The framework '");
const pal::string_t suffix = _X("' was not found.");
const pal::string_t url_prefix = _X(" - ") DOTNET_CORE_APPLAUNCH_URL _X("?");
if (starts_with(line, prefix, true) && ends_with(line, suffix, true))
{
dialogMsg.append(line);
dialogMsg.append(_X("\n\n"));
}
else if (starts_with(line, url_prefix, true))
{
size_t offset = url_prefix.length() - pal::strlen(DOTNET_CORE_APPLAUNCH_URL) - 1;
url = line.substr(offset, line.length() - offset);
break;
}
}
}

dialogMsg.append(_X("Would you like to download it now?"));

assert(url.length() > 0);
url.append(_X("&apphost_version="));
url.append(_STRINGIFY(COMMON_HOST_PKG_VER));

trace::verbose(_X("Showing error dialog for application: '%s' - error code: 0x%x - url: '%s'"), executable_name, error_code, url.c_str());
if (::MessageBoxW(nullptr, dialogMsg.c_str(), executable_name, MB_ICONERROR | MB_YESNO) == IDYES)
{
// Open the URL in default browser
::ShellExecuteW(
nullptr,
_X("open"),
url.c_str(),
nullptr,
nullptr,
SW_SHOWNORMAL);
}
}
}

void apphost::buffer_errors()
{
trace::verbose(_X("Redirecting errors to custom writer."));
trace::set_error_writer(buffering_trace_writer);
}

void apphost::write_buffered_errors(int error_code)
{
if (g_buffered_errors.empty())
return;

pal::string_t executable_path;
pal::string_t executable_name;
if (pal::get_own_executable_path(&executable_path))
{
executable_name = get_filename(executable_path);
}

write_errors_to_event_log(executable_path.c_str(), executable_name.c_str());

if (is_gui_application())
show_error_dialog(executable_name.c_str(), error_code);
}
14 changes: 14 additions & 0 deletions src/installer/corehost/cli/apphost/apphost.windows.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#ifndef __APPHOST_WINDOWS_H__
#define __APPHOST_WINDOWS_H__

namespace apphost
{
void buffer_errors();
void write_buffered_errors(int error_code);
}

#endif // __APPHOST_WINDOWS_H__
13 changes: 2 additions & 11 deletions src/installer/corehost/cli/deps_format.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,8 @@ void deps_json_t::reconcile_libraries_with_targets(
// Returns the RID determined (computed or fallback) for the platform the host is running on.
pal::string_t deps_json_t::get_current_rid(const rid_fallback_graph_t& rid_fallback_graph)
{

pal::string_t currentRid;
if (!pal::getenv(_X("DOTNET_RUNTIME_ID"), &currentRid))
{
currentRid = pal::get_current_os_rid_platform();
if (!currentRid.empty())
{
currentRid = currentRid + pal::string_t(_X("-")) + get_arch();
}
}

pal::string_t currentRid = get_current_runtime_id(false /*use_fallback*/);

trace::info(_X("HostRID is %s"), currentRid.empty()? _X("not available"): currentRid.c_str());

// If the current RID is not present in the RID fallback graph, then the platform
Expand Down
9 changes: 5 additions & 4 deletions src/installer/corehost/cli/fxr/fx_resolver.messages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ void fx_resolver_t::display_missing_framework_error(
// Display the error message about missing FX.
if (fx_version.length())
{
trace::error(_X("The specified framework '%s', version '%s' was not found."), fx_name.c_str(), fx_version.c_str());
trace::error(_X("The framework '%s', version '%s' was not found."), fx_name.c_str(), fx_version.c_str());
}
else
{
trace::error(_X("The specified framework '%s' was not found."), fx_name.c_str());
trace::error(_X("The framework '%s' was not found."), fx_name.c_str());
}

if (framework_infos.size())
Expand All @@ -133,11 +133,12 @@ void fx_resolver_t::display_missing_framework_error(
trace::error(_X(" - No frameworks were found."));
}

pal::string_t url = get_download_url(fx_name.c_str(), fx_version.c_str());
trace::error(_X(""));
trace::error(_X("You can resolve the problem by installing the specified framework and/or SDK."));
trace::error(_X(""));
trace::error(_X("The .NET Core frameworks can be found at:"));
trace::error(_X(" - %s"), DOTNET_CORE_DOWNLOAD_URL);
trace::error(_X("The specified framework can be found at:"));
trace::error(_X(" - %s"), url.c_str());
}

void fx_resolver_t::display_incompatible_loaded_framework_error(
Expand Down
3 changes: 3 additions & 0 deletions src/installer/corehost/cli/fxr_resolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ bool fxr_resolver::try_get_path(const pal::string_t& root_path, pal::string_t* o
default_install_location.c_str(),
dotnet_root_env_var_name.c_str(),
self_registered_message.c_str());
trace::error(_X(""));
trace::error(_X("The .NET Core runtime can be found at:"));
trace::error(_X(" - %s"), get_download_url().c_str());
return false;
}

Expand Down
46 changes: 46 additions & 0 deletions src/installer/corehost/common/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ const pal::char_t* get_arch()
#endif
}

pal::string_t get_current_runtime_id(bool use_fallback)
{
pal::string_t rid;
if (pal::getenv(_X("DOTNET_RUNTIME_ID"), &rid))
return rid;

rid = pal::get_current_os_rid_platform();
if (rid.empty() && use_fallback)
rid = pal::get_current_os_fallback_rid();

if (!rid.empty())
{
rid.append(_X("-"));
rid.append(get_arch());
}

return rid;
}

bool get_env_shared_store_dirs(std::vector<pal::string_t>* dirs, const pal::string_t& arch, const pal::string_t& tfm)
{
pal::string_t path;
Expand Down Expand Up @@ -383,6 +402,33 @@ pal::string_t get_dotnet_root_from_fxr_path(const pal::string_t &fxr_path)
return get_directory(get_directory(fxr_root));
}

pal::string_t get_download_url(const pal::char_t *framework_name, const pal::char_t *framework_version)
{
pal::string_t url = DOTNET_CORE_APPLAUNCH_URL _X("?");
if (framework_name != nullptr && pal::strlen(framework_name) > 0)
{
url.append(_X("framework="));
url.append(framework_name);
if (framework_version != nullptr && pal::strlen(framework_version) > 0)
{
url.append(_X("&framework_version="));
url.append(framework_version);
}
}
else
{
url.append(_X("missing_runtime=true"));
}

url.append(_X("&arch="));
url.append(get_arch());
pal::string_t rid = get_current_runtime_id(true /*use_fallback*/);
url.append(_X("&rid="));
url.append(rid);

return url;
}

#define TEST_ONLY_MARKER "d38cc827-e34f-4453-9df4-1e796e9f1d07"

// Retrieves environment variable which is only used for testing.
Expand Down
7 changes: 6 additions & 1 deletion src/installer/corehost/common/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
#else
#define DOTNET_CORE_INSTALL_PREREQUISITES_URL _X("https://go.microsoft.com/fwlink/?linkid=2063370")
#endif
#define DOTNET_CORE_DOWNLOAD_RUNTIME_URL _X("https://aka.ms/dotnet-download-runtime")
#define DOTNET_CORE_DOWNLOAD_URL _X("https://aka.ms/dotnet-download")
#define DOTNET_CORE_APPLAUNCH_URL _X("https://aka.ms/dotnet-core-applaunch")

#define RUNTIME_STORE_DIRECTORY_NAME _X("store")

Expand All @@ -35,6 +35,7 @@ void remove_trailing_dir_seperator(pal::string_t* dir);
void replace_char(pal::string_t* path, pal::char_t match, pal::char_t repl);
pal::string_t get_replaced_char(const pal::string_t& path, pal::char_t match, pal::char_t repl);
const pal::char_t* get_arch();
pal::string_t get_current_runtime_id(bool use_fallback);
bool get_env_shared_store_dirs(std::vector<pal::string_t>* dirs, const pal::string_t& arch, const pal::string_t& tfm);
bool get_global_shared_store_dirs(std::vector<pal::string_t>* dirs, const pal::string_t& arch, const pal::string_t& tfm);
bool multilevel_lookup_enabled();
Expand All @@ -47,6 +48,10 @@ pal::string_t get_deps_from_app_binary(const pal::string_t& app_base, const pal:
void get_runtime_config_paths(const pal::string_t& path, const pal::string_t& name, pal::string_t* cfg, pal::string_t* dev_cfg);
pal::string_t get_dotnet_root_from_fxr_path(const pal::string_t &fxr_path);

// Get a download URL for a specific framework and version
// If no framework is specified, a download URL for the runtime is returned
pal::string_t get_download_url(const pal::char_t *framework_name = nullptr, const pal::char_t *framework_version = nullptr);

// Retrieves environment variable which is only used for testing.
// This will return the value of the variable only if the product binary is stamped
// with test-only marker.
Expand Down
43 changes: 6 additions & 37 deletions src/installer/corehost/corehost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
#include "cli/apphost/bundle/marker.h"
#include "cli/apphost/bundle/runner.h"

#if defined(_WIN32)
#include "cli/apphost/apphost.windows.h"
#endif

#define CURHOST_TYPE _X("apphost")
#define CUREXE_PKG_VER COMMON_HOST_PKG_VER
#define CURHOST_EXE
Expand Down Expand Up @@ -249,19 +253,6 @@ int exe_start(const int argc, const pal::char_t* argv[])
return rc;
}

#if defined(_WIN32) && defined(FEATURE_APPHOST)
pal::string_t g_buffered_errors;

void buffering_trace_writer(const pal::char_t* message)
{
// Add to buffer for later use.
g_buffered_errors.append(message).append(_X("\n"));
// Also write to stderr immediately
pal::err_fputs(message);
}

#endif

#if defined(_WIN32)
int __cdecl wmain(const int argc, const pal::char_t* argv[])
#else
Expand All @@ -281,9 +272,8 @@ int main(const int argc, const pal::char_t* argv[])
}

#if defined(_WIN32) && defined(FEATURE_APPHOST)
trace::verbose(_X("Redirecting errors to custom writer."));
// Buffer errors to use them later.
trace::set_error_writer(buffering_trace_writer);
apphost::buffer_errors();
#endif

int exit_code = exe_start(argc, argv);
Expand All @@ -293,28 +283,7 @@ int main(const int argc, const pal::char_t* argv[])

#if defined(_WIN32) && defined(FEATURE_APPHOST)
// No need to unregister the error writer since we're exiting anyway.
if (!g_buffered_errors.empty())
{
// If there are errors buffered, write them to the Windows Event Log.
pal::string_t executable_path;
pal::string_t executable_name;
if (pal::get_own_executable_path(&executable_path))
{
executable_name = get_filename(executable_path);
}

auto eventSource = ::RegisterEventSourceW(nullptr, _X(".NET Runtime"));
const DWORD traceErrorID = 1023; // Matches CoreCLR ERT_UnmanagedFailFast
pal::string_t message;
message.append(_X("Description: A .NET Core application failed.\n"));
message.append(_X("Application: ")).append(executable_name).append(_X("\n"));
message.append(_X("Path: ")).append(executable_path).append(_X("\n"));
message.append(_X("Message: ")).append(g_buffered_errors).append(_X("\n"));

LPCWSTR messages[] = {message.c_str()};
::ReportEventW(eventSource, EVENTLOG_ERROR_TYPE, 0, traceErrorID, nullptr, 1, 0, messages, nullptr);
::DeregisterEventSource(eventSource);
}
apphost::write_buffered_errors(exit_code);
#endif

return exit_code;
Expand Down
Loading

0 comments on commit c17bb03

Please sign in to comment.