diff --git a/src/installer/tests/HostActivation.Tests/DotnetArgValidation.cs b/src/installer/tests/HostActivation.Tests/DotnetArgValidation.cs index e1bd6522751f21..53d689e3a9b059 100644 --- a/src/installer/tests/HostActivation.Tests/DotnetArgValidation.cs +++ b/src/installer/tests/HostActivation.Tests/DotnetArgValidation.cs @@ -1,9 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.IO; - +using System.Text; using Microsoft.DotNet.Cli.Build; using Microsoft.DotNet.TestUtils; using Xunit; @@ -95,6 +95,23 @@ public void DotNetInfo_NoSDK() .And.HaveStdOutMatching($@"RID:\s*{TestContext.BuildRID}"); } + [Fact] + public void DotNetInfo_Utf8Path() + { + string installLocation = Encoding.UTF8.GetString("utf8-龯蝌灋齅ㄥ䶱"u8); + DotNetCli dotnet = new DotNetBuilder(sharedTestState.BaseDirectory.Location, TestContext.BuiltDotNet.BinPath, installLocation) + .Build(); + + var result = dotnet.Exec("--info") + .DotNetRoot(Path.Combine(sharedTestState.BaseDirectory.Location, installLocation)) + .CaptureStdErr() + .CaptureStdOut(Encoding.UTF8) + .Execute(); + + result.Should().Pass() + .And.HaveStdOutMatching($@"DOTNET_ROOT.*{installLocation}"); + } + [Fact] public void DotNetInfo_WithSDK() { diff --git a/src/installer/tests/TestUtils/Command.cs b/src/installer/tests/TestUtils/Command.cs index 1f403c4c4000cc..5d64c2a3872871 100644 --- a/src/installer/tests/TestUtils/Command.cs +++ b/src/installer/tests/TestUtils/Command.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; namespace Microsoft.DotNet.Cli.Build.Framework @@ -218,7 +219,7 @@ public Command Start() /// Result of the command public CommandResult WaitForExit(bool expectedToFail, int timeoutMilliseconds = Timeout.Infinite) { - ReportExecWaitOnExit(); + ReportExecWaitOnExit(); int exitCode; if (!Process.WaitForExit(timeoutMilliseconds)) @@ -283,18 +284,20 @@ public Command RemoveEnvironmentVariable(string name) return this; } - public Command CaptureStdOut() + public Command CaptureStdOut(Encoding? stdOutEncoding = null) { ThrowIfRunning(); Process.StartInfo.RedirectStandardOutput = true; + Process.StartInfo.StandardOutputEncoding = stdOutEncoding; _stdOutCapture = new StringWriter(); return this; } - public Command CaptureStdErr() + public Command CaptureStdErr(Encoding? stdErrEncoding = null) { ThrowIfRunning(); Process.StartInfo.RedirectStandardError = true; + Process.StartInfo.StandardErrorEncoding = stdErrEncoding; _stdErrCapture = new StringWriter(); return this; } diff --git a/src/native/corehost/apphost/apphost.windows.cpp b/src/native/corehost/apphost/apphost.windows.cpp index b8167cf6c0d89c..effbdb8b9437df 100644 --- a/src/native/corehost/apphost/apphost.windows.cpp +++ b/src/native/corehost/apphost/apphost.windows.cpp @@ -19,7 +19,7 @@ namespace // Add to buffer for later use. g_buffered_errors.append(message).append(_X("\n")); // Also write to stderr immediately - pal::err_fputs(message); + pal::err_print_line(message); } // Determines if the current module (apphost executable) is marked as a Windows GUI application diff --git a/src/native/corehost/hostmisc/pal.h b/src/native/corehost/hostmisc/pal.h index f482b6df63456b..575afc278c0f22 100644 --- a/src/native/corehost/hostmisc/pal.h +++ b/src/native/corehost/hostmisc/pal.h @@ -159,9 +159,9 @@ namespace pal inline FILE* file_open(const string_t& path, const char_t* mode) { return ::_wfsopen(path.c_str(), mode, _SH_DENYNO); } - inline void file_vprintf(FILE* f, const char_t* format, va_list vl) { ::vfwprintf(f, format, vl); ::fputwc(_X('\n'), f); } - inline void err_fputs(const char_t* message) { ::fputws(message, stderr); ::fputwc(_X('\n'), stderr); } - inline void out_vprintf(const char_t* format, va_list vl) { ::vfwprintf(stdout, format, vl); ::fputwc(_X('\n'), stdout); } + void file_vprintf(FILE* f, const char_t* format, va_list vl); + void err_print_line(const char_t* message); + void out_vprint_line(const char_t* format, va_list vl); inline int str_vprintf(char_t* buffer, size_t count, const char_t* format, va_list vl) { return ::_vsnwprintf_s(buffer, count, _TRUNCATE, format, vl); } inline int strlen_vprintf(const char_t* format, va_list vl) { return ::_vscwprintf(format, vl); } @@ -188,7 +188,7 @@ namespace pal inline bool munmap(void* addr, size_t length) { return UnmapViewOfFile(addr) != 0; } inline int get_pid() { return GetCurrentProcessId(); } inline void sleep(uint32_t milliseconds) { Sleep(milliseconds); } -#else +#else // _WIN32 #ifdef EXPORT_SHARED_API #define SHARED_API extern "C" __attribute__((__visibility__("default"))) #else @@ -226,8 +226,8 @@ namespace pal inline size_t strlen(const char_t* str) { return ::strlen(str); } inline FILE* file_open(const string_t& path, const char_t* mode) { return fopen(path.c_str(), mode); } inline void file_vprintf(FILE* f, const char_t* format, va_list vl) { ::vfprintf(f, format, vl); ::fputc('\n', f); } - inline void err_fputs(const char_t* message) { ::fputs(message, stderr); ::fputc(_X('\n'), stderr); } - inline void out_vprintf(const char_t* format, va_list vl) { ::vfprintf(stdout, format, vl); ::fputc('\n', stdout); } + inline void err_print_line(const char_t* message) { ::fputs(message, stderr); ::fputc(_X('\n'), stderr); } + inline void out_vprint_line(const char_t* format, va_list vl) { ::vfprintf(stdout, format, vl); ::fputc('\n', stdout); } inline int str_vprintf(char_t* str, size_t size, const char_t* format, va_list vl) { return ::vsnprintf(str, size, format, vl); } inline int strlen_vprintf(const char_t* format, va_list vl) { return ::vsnprintf(nullptr, 0, format, vl); } @@ -254,7 +254,7 @@ namespace pal inline bool munmap(void* addr, size_t length) { return ::munmap(addr, length) == 0; } inline int get_pid() { return getpid(); } inline void sleep(uint32_t milliseconds) { usleep(milliseconds * 1000); } -#endif +#endif // _WIN32 inline int snwprintf(char_t* buffer, size_t count, const char_t* format, ...) { diff --git a/src/native/corehost/hostmisc/pal.windows.cpp b/src/native/corehost/hostmisc/pal.windows.cpp index b11610492d3214..df855c9067de57 100644 --- a/src/native/corehost/hostmisc/pal.windows.cpp +++ b/src/native/corehost/hostmisc/pal.windows.cpp @@ -5,11 +5,67 @@ #include "trace.h" #include "utils.h" #include "longfile.h" - +#include #include #include #include + +void pal::file_vprintf(FILE* f, const pal::char_t* format, va_list vl) +{ + // String functions like vfwprintf convert wide to multi-byte characters as if wcrtomb were called - that is, using the current C locale (LC_TYPE). + // In order to properly print UTF-8 and GB18030 characters, we need to use the version of vfwprintf that takes a locale. + _locale_t loc = _create_locale(LC_ALL, ".utf8"); + ::_vfwprintf_l(f, format, loc, vl); + ::fputwc(_X('\n'), f); + _free_locale(loc); +} + +namespace { + void print_line_to_handle(const pal::char_t* message, HANDLE handle, FILE* fallbackFileHandle) { + // String functions like vfwprintf convert wide to multi-byte characters as if wcrtomb were called - that is, using the current C locale (LC_TYPE). + // In order to properly print UTF-8 and GB18030 characters to the console without requiring the user to use chcp to a compatible locale, we use WriteConsoleW. + // However, WriteConsoleW will fail if the output is redirected to a file - in that case we will write to the fallbackFileHandle + DWORD output; + // GetConsoleMode returns FALSE when the output is redirected to a file, and we need to output to the fallback file handle. + BOOL isConsoleOutput = ::GetConsoleMode(handle, &output); + if (isConsoleOutput == FALSE) + { + // We use file_vprintf to handle UTF-8 formatting. The WriteFile api will output the bytes directly with Unicode bytes, + // while pal::file_vprintf will convert the characters to UTF-8. + pal::file_vprintf(fallbackFileHandle, message, va_list()); + } + else { + ::WriteConsoleW(handle, message, (int)pal::strlen(message), NULL, NULL); + ::WriteConsoleW(handle, _X("\n"), 1, NULL, NULL); + } + } +} + +void pal::err_print_line(const pal::char_t* message) { + // Forward to helper to handle UTF-8 formatting and redirection + print_line_to_handle(message, ::GetStdHandle(STD_ERROR_HANDLE), stderr); +} + +void pal::out_vprint_line(const pal::char_t* format, va_list vl) { + va_list vl_copy; + va_copy(vl_copy, vl); + // Get the length of the formatted string + 1 for null terminator + int len = 1 + pal::strlen_vprintf(format, vl_copy); + if (len < 0) + { + return; + } + std::vector buffer(len); + int written = pal::str_vprintf(&buffer[0], len, format, vl); + if (written != len - 1) + { + return; + } + // Forward to helper to handle UTF-8 formatting and redirection + print_line_to_handle(&buffer[0], ::GetStdHandle(STD_OUTPUT_HANDLE), stdout); +} + bool GetModuleFileNameWrapper(HMODULE hModule, pal::string_t* recv) { pal::string_t path; diff --git a/src/native/corehost/hostmisc/trace.cpp b/src/native/corehost/hostmisc/trace.cpp index 7266d041afd1b3..fa1daed7a45b2a 100644 --- a/src/native/corehost/hostmisc/trace.cpp +++ b/src/native/corehost/hostmisc/trace.cpp @@ -179,7 +179,7 @@ void trace::error(const pal::char_t* format, ...) if (g_error_writer == nullptr) { - pal::err_fputs(buffer.data()); + pal::err_print_line(buffer.data()); } else { @@ -200,7 +200,7 @@ void trace::println(const pal::char_t* format, ...) va_start(args, format); { std::lock_guard lock(g_trace_lock); - pal::out_vprintf(format, args); + pal::out_vprint_line(format, args); } va_end(args); }