Skip to content

Commit

Permalink
Use TaskDialog for error dialog in Windows GUI apps (#78087)
Browse files Browse the repository at this point in the history
  • Loading branch information
elinor-fung authored Jan 18, 2023
1 parent 0e77e6b commit 7392fb7
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void GetHostFxrPath_DotNetRootParameter(bool explicitLoad, bool useAssemb
result.Should().Fail()
.And.ExitWith(1)
.And.HaveStdOutContaining($"{GetHostFxrPath} failed: 0x{Constants.ErrorCode.CoreHostLibMissingFailure.ToString("x")}")
.And.HaveStdErrContaining($"The folder [{Path.Combine(dotNetRoot, "host", "fxr")}] does not exist");
.And.HaveStdErrContaining($"[{Path.Combine(dotNetRoot, "host", "fxr")}] does not exist");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ public void AppHost_GUI_FrameworkDependent_MissingRuntimeFramework_ErrorReported
.And.HaveStdErrContaining($"Showing error dialog for application: '{Path.GetFileName(appExe)}' - error code: 0x{expectedErrorCode}")
.And.HaveStdErrContaining($"url: 'https://aka.ms/dotnet-core-applaunch?{expectedUrlQuery}")
.And.HaveStdErrContaining("&gui=true")
.And.HaveStdErrMatching($"dialog message: (?>.|\\s)*{System.Text.RegularExpressions.Regex.Escape(expectedMissingFramework)}");
.And.HaveStdErrMatching($"details: (?>.|\\s)*{System.Text.RegularExpressions.Regex.Escape(expectedMissingFramework)}");
}
}

Expand Down
185 changes: 147 additions & 38 deletions src/native/corehost/apphost/apphost.windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,19 @@ namespace
return msg;
}

pal::string_t get_runtime_not_found_message()
void open_url(const pal::char_t* url)
{
pal::string_t msg = INSTALL_NET_DESKTOP_ERROR_MESSAGE _X("\n\n");
msg.append(get_apphost_details_message());
return msg;
// Open the URL in default browser
::ShellExecuteW(
nullptr,
_X("open"),
url,
nullptr,
nullptr,
SW_SHOWNORMAL);
}

void enable_visual_styles()
bool enable_visual_styles()
{
// Create an activation context using a manifest that enables visual styles
// See https://learn.microsoft.com/windows/win32/controls/cookbook-overview
Expand All @@ -99,7 +104,7 @@ namespace
if (len == 0 || len >= MAX_PATH)
{
trace::verbose(_X("GetWindowsDirectory failed. Error code: %d"), ::GetLastError());
return;
return false;
}

pal::string_t manifest(buf);
Expand All @@ -112,17 +117,109 @@ namespace
if (context_handle == INVALID_HANDLE_VALUE)
{
trace::verbose(_X("CreateActCtxW failed using manifest '%s'. Error code: %d"), manifest.c_str(), ::GetLastError());
return;
return false;
}

ULONG_PTR cookie;
if (::ActivateActCtx(context_handle, &cookie) == FALSE)
{
trace::verbose(_X("ActivateActCtx failed. Error code: %d"), ::GetLastError());
return;
return false;
}

return;
return true;
}

void append_hyperlink(pal::string_t& str, const pal::char_t* url)
{
str.append(_X("<A HREF=\""));
str.append(url);
str.append(_X("\">"));

// & indicates an accelerator key when in hyperlink text.
// Replace & with && such that the single ampersand is shown.
for (size_t i = 0; i < pal::strlen(url); ++i)
{
str.push_back(url[i]);
if (url[i] == _X('&'))
str.push_back(_X('&'));
}

str.append(_X("</A>"));
}

bool try_show_error_with_task_dialog(
const pal::char_t *executable_name,
const pal::char_t *instruction,
const pal::char_t *details,
const pal::char_t *url)
{
HMODULE comctl32 = ::LoadLibraryExW(L"comctl32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (comctl32 == nullptr)
return false;

typedef HRESULT (WINAPI* task_dialog_indirect)(
const TASKDIALOGCONFIG* pTaskConfig,
int* pnButton,
int* pnRadioButton,
BOOL* pfVerificationFlagChecked);

task_dialog_indirect task_dialog_indirect_func = (task_dialog_indirect)::GetProcAddress(comctl32, "TaskDialogIndirect");
if (task_dialog_indirect_func == nullptr)
{
::FreeLibrary(comctl32);
return false;
}

TASKDIALOGCONFIG config{0};
config.cbSize = sizeof(TASKDIALOGCONFIG);
config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_ENABLE_HYPERLINKS | TDF_SIZE_TO_CONTENT | TDF_USE_COMMAND_LINKS;
config.dwCommonButtons = TDCBF_CLOSE_BUTTON;
config.pszWindowTitle = executable_name;
config.pszMainInstruction = instruction;

// Use the application's icon if available
HMODULE exe_module = ::GetModuleHandleW(nullptr);
assert(exe_module != nullptr);
if (::FindResourceW(exe_module, IDI_APPLICATION, RT_GROUP_ICON) != nullptr)
{
config.hInstance = exe_module;
config.pszMainIcon = IDI_APPLICATION;
}
else
{
config.pszMainIcon = TD_ERROR_ICON;
}

int download_button_id = 1000;
TASKDIALOG_BUTTON download_button { download_button_id, _X("Download it now\n") _X("You will need to run the downloaded installer") };
config.cButtons = 1;
config.pButtons = &download_button;
config.nDefaultButton = download_button_id;

pal::string_t expanded_info(details);
expanded_info.append(DOC_LINK_INTRO _X("\n"));
append_hyperlink(expanded_info, DOTNET_APP_LAUNCH_FAILED_URL);
expanded_info.append(_X("\n\nDownload link:\n"));
append_hyperlink(expanded_info, url);
config.pszExpandedInformation = expanded_info.c_str();

// Callback to handle hyperlink clicks
config.pfCallback = [](HWND hwnd, UINT uNotification, WPARAM wParam, LPARAM lParam, LONG_PTR lpRefData) -> HRESULT
{
if (uNotification == TDN_HYPERLINK_CLICKED && lParam != NULL)
open_url(reinterpret_cast<LPCWSTR>(lParam));

return S_OK;
};

int clicked_button;
bool succeeded = SUCCEEDED(task_dialog_indirect_func(&config, &clicked_button, nullptr, nullptr));
if (succeeded && clicked_button == download_button_id)
open_url(url);

::FreeLibrary(comctl32);
return succeeded;
}

void show_error_dialog(const pal::char_t* executable_name, int error_code)
Expand All @@ -131,11 +228,13 @@ namespace
if (pal::getenv(_X("DOTNET_DISABLE_GUI_ERRORS"), &gui_errors_disabled) && pal::xtoi(gui_errors_disabled.c_str()) == 1)
return;

pal::string_t dialogMsg;
const pal::char_t* instruction = nullptr;
pal::string_t details;
pal::string_t url;
if (error_code == StatusCode::CoreHostLibMissingFailure)
{
dialogMsg = get_runtime_not_found_message();
instruction = INSTALL_NET_DESKTOP_ERROR_MESSAGE;
details = get_apphost_details_message();
pal::string_t line;
pal::stringstream_t ss(g_buffered_errors);
while (std::getline(ss, line, _X('\n')))
Expand All @@ -150,7 +249,7 @@ namespace
{
// 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.
dialogMsg = pal::string_t(INSTALL_OR_UPDATE_NET_ERROR_MESSAGE _X("\n\n"));
instruction = INSTALL_OR_UPDATE_NET_ERROR_MESSAGE;
pal::string_t line;
pal::stringstream_t ss(g_buffered_errors);
bool foundCustomMessage = false;
Expand All @@ -160,18 +259,29 @@ namespace
const pal::char_t prefix_before_7_0[] = _X("The framework '");
const pal::char_t suffix_before_7_0[] = _X(" was not found.");
const pal::char_t custom_prefix[] = _X(" _ ");
if (utils::starts_with(line, prefix, true)
bool has_prefix = utils::starts_with(line, prefix, true);
if (has_prefix
|| (utils::starts_with(line, prefix_before_7_0, true) && utils::ends_with(line, suffix_before_7_0, true)))
{
dialogMsg.append(line);
dialogMsg.append(_X("\n\n"));
details.append(_X("Required: "));
if (has_prefix)
{
details.append(line.substr(utils::strlen(prefix) - 1));
}
else
{
size_t prefix_len = utils::strlen(prefix_before_7_0) - 1;
details.append(line.substr(prefix_len, line.length() - prefix_len - utils::strlen(suffix_before_7_0)));
}

details.append(_X("\n\n"));
foundCustomMessage = true;
}
else if (utils::starts_with(line, custom_prefix, true))
{
dialogMsg.erase();
dialogMsg.append(line.substr(utils::strlen(custom_prefix)));
dialogMsg.append(_X("\n\n"));
details.erase();
details.append(line.substr(utils::strlen(custom_prefix)));
details.append(_X("\n\n"));
foundCustomMessage = true;
}
else if (try_get_url_from_line(line, url))
Expand All @@ -181,7 +291,7 @@ namespace
}

if (!foundCustomMessage)
dialogMsg.append(get_apphost_details_message());
details.append(get_apphost_details_message());
}
else if (error_code == StatusCode::BundleExtractionFailure)
{
Expand All @@ -191,44 +301,43 @@ namespace
{
if (utils::starts_with(line, _X("Bundle header version compatibility check failed."), true))
{
dialogMsg = get_runtime_not_found_message();
instruction = INSTALL_NET_DESKTOP_ERROR_MESSAGE;
details = get_apphost_details_message();
url = get_download_url();
url.append(_X("&apphost_version="));
url.append(_STRINGIFY(COMMON_HOST_PKG_VER));
}
}

if (dialogMsg.empty())
if (instruction == nullptr)
return;
}
else
{
return;
}

dialogMsg.append(
_X("Learn about "));
dialogMsg.append(error_code == StatusCode::FrameworkMissingFailure ? _X("framework resolution:") : _X("runtime installation:"));
dialogMsg.append(_X("\n") DOTNET_APP_LAUNCH_FAILED_URL _X("\n\n")
_X("Would you like to download it now?"));

assert(url.length() > 0);
assert(is_gui_application());
url.append(_X("&gui=true"));

enable_visual_styles();
trace::verbose(_X("Showing error dialog for application: '%s' - error code: 0x%x - url: '%s' - details: %s"), executable_name, error_code, url.c_str(), details.c_str());

if (enable_visual_styles())
{
// Task dialog requires enabling visual styles
if (try_show_error_with_task_dialog(executable_name, instruction, details.c_str(), url.c_str()))
return;
}

trace::verbose(_X("Showing error dialog for application: '%s' - error code: 0x%x - url: '%s' - dialog message: %s"), executable_name, error_code, url.c_str(), dialogMsg.c_str());
if (::MessageBoxW(nullptr, dialogMsg.c_str(), executable_name, MB_ICONERROR | MB_YESNO) == IDYES)
pal::string_t dialog_message(instruction);
dialog_message.append(_X("\n\n"));
dialog_message.append(details);
dialog_message.append(DOC_LINK_INTRO _X("\n") DOTNET_APP_LAUNCH_FAILED_URL _X("\n\n")
_X("Would you like to download it now?"));
if (::MessageBoxW(nullptr, dialog_message.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);
open_url(url.c_str());
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/native/corehost/apphost/standalone/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ set(HEADERS
)

if(CLR_CMAKE_TARGET_WIN32)
add_definitions(-DUNICODE)
list(APPEND SOURCES
../apphost.windows.cpp)

Expand Down
1 change: 1 addition & 0 deletions src/native/corehost/apphost/static/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES GNU)
endif()

if(CLR_CMAKE_TARGET_WIN32)
add_definitions(-DUNICODE)
list(APPEND SOURCES
../apphost.windows.cpp)

Expand Down
17 changes: 3 additions & 14 deletions src/native/corehost/corehost.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,8 @@ bool is_exe_enabled_for_execution(pal::string_t* app_dll)
void need_newer_framework_error(const pal::string_t& dotnet_root, const pal::string_t& host_path)
{
trace::error(
INSTALL_OR_UPDATE_NET_ERROR_MESSAGE
_X("\n\n")
_X("App: %s\n")
_X("Architecture: %s\n")
_X("App host version: %s\n")
_X(".NET location: %s\n")
_X("\n")
_X("Learn about runtime installation:\n")
DOTNET_APP_LAUNCH_FAILED_URL
_X("\n\n")
_X("Download the .NET runtime:\n")
_X("%s&apphost_version=%s"),
MISSING_RUNTIME_ERROR_FORMAT,
INSTALL_OR_UPDATE_NET_ERROR_MESSAGE,
host_path.c_str(),
get_current_arch_name(),
_STRINGIFY(COMMON_HOST_PKG_VER),
Expand Down Expand Up @@ -126,7 +116,6 @@ int exe_start(const int argc, const pal::char_t* argv[])
pal::string_t embedded_app_name;
if (!is_exe_enabled_for_execution(&embedded_app_name))
{
trace::error(_X("A fatal error was encountered. This executable was not bound to load a managed DLL."));
return StatusCode::AppHostExeNotBoundFailure;
}

Expand Down Expand Up @@ -165,7 +154,7 @@ int exe_start(const int argc, const pal::char_t* argv[])
// dotnet.exe is signed by Microsoft. It is technically possible to rename the file MyApp.exe and include it in the application.
// Then one can create a shortcut for "MyApp.exe MyApp.dll" which works. The end result is that MyApp looks like it's signed by Microsoft.
// To prevent this dotnet.exe must not be renamed, otherwise it won't run.
trace::error(_X("A fatal error was encountered. Cannot execute %s when renamed to %s."), CURHOST_TYPE, own_name.c_str());
trace::error(_X("Error: cannot execute %s when renamed to %s."), CURHOST_TYPE, own_name.c_str());
return StatusCode::CoreHostEntryPointFailure;
}

Expand Down
2 changes: 1 addition & 1 deletion src/native/corehost/fxr/fx_resolver.messages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ void fx_resolver_t::display_missing_framework_error(
pal::string_t url = get_download_url(fx_name.c_str(), fx_version.c_str());
trace::error(
_X("\n")
_X("Learn about framework resolution:\n")
DOC_LINK_INTRO _X("\n")
DOTNET_APP_LAUNCH_FAILED_URL
_X("\n\n")
_X("To install missing framework, download:\n")
Expand Down
Loading

0 comments on commit 7392fb7

Please sign in to comment.