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);
}